Entering data directly to consumers who need it may work in some circumstances, but in most cases, the consumer determines what data he needs by querying this data (requesting data using the provided arguments).
For those few cases where data can be entered directly (you can see it as without parameters), injecting data directly will make your DI configuration extremely complex. Take, for example, the consumer, which depends on the current time in the system. You can enter a DateTime in a consumer (or even a Lazy<DateTime> ). Other consumers may need the date of birth of the current user, so this consumer also depends on DateTime . But now you have ambiguity in the system, as the DateTime dependency has two meanings. DI containers do a poor job of this, and to fix this you will need to explicitly tell the container what DateTime means for every consumer that needs it. This results in a fragile configuration that is difficult to contain.
In this latter case (parameterless queries), the solution should define unambiguous interfaces that can be resolved. In the above example, you can define the ITimeProvider interface and the IUserContext interface. Each consumer can depend on the right interface.
In the first case (parameterized queries), this does not require a framework; you need the right design.
You are talking about querying a database and web service and need a way to cache the returned data. So, you need an abstraction to define requests and do it so that you can apply caching and other cross-cutting issues to them in a way that can be connected and saves you from having to make any changes to your code.
An effective way to do this is to define the interface that defines the request object (input parameters of the request) + the type of the return value, and the interface that defines the logic that processes this request:
public interface IQuery<TResult> { } public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { TResult Handle(TQuery query); }
Using these abstractions, you can define a query object as follows:
public class FindUsersBySearchTextQuery : IQuery<User[]> { public string SearchText { get; set; } public bool IncludeInactiveUsers { get; set; } }
This object defines a query that finds users in the search text and allows you to include or exclude inactive users, and the query result is an array of User objects.
The logic executing this request can be implemented as follows:
public class FindUsersBySearchTextQueryHandler : IQueryHandler<FindUsersBySearchTextQuery, User[]> { private readonly NorthwindUnitOfWork db; public FindUsersBySearchTextQueryHandler( NorthwindUnitOfWork db) { this.db = db; } public User[] Handle(FindUsersBySearchTextQuery query) { return ( from user in this.db.Users where user.Name.Contains(query.SearchText) where user.IsActive || query.IncludeInactiveUsers select user) .ToArray(); } }
And why exactly does this solve the problem that you have? This solves your problem because users can depend on the IQueryHandler<FindUsersBySearchTextQuery, User[]> interface, while you can wrap the IQueryHandler<T> with decorators without knowing it. Writing a decorator that caches the result in a tread is not easy:
public class TlsCachingQueryHandlerDecorator<TQuery, TResult> : IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> where TResult : class { [ThreadStatic] private static TResult cache; private readonly IQueryHandler<TQuery, TResult> decorated; public ValidationQueryHandlerDecorator( IQueryHandler<TQuery, TResult> decorated) { this.decorated = decorated; } public TResult Handle(TQuery query) { return cache ?? (cache = this.decorated.Handle(query)); } }
Any solid DI container allows you to register these decorators and allow you to register decorators conditional (based on information such as the decorated type). This allows you to place this cache decoder only on types that can be safely cached (according to your conditions).
You can find more information about this model in this article: Meanwhile ... on the request side of my architecture .