Search code examples
c#.net-coresimple-injectoref-core-2.2

Simple injector - create a generic decorator for EF Core caching


I'm trying to implement caching for EF Core in my .NET Core project using Simple Injector as my DI. I'm using the CQRS pattern so I have a bunch of queries I'd like to cache (not all).

I have created a generic interface for a cached query, which takes a return type of the query, and the query arguments:

public interface ICachedQuery<T, P>
{
    T Execute(P args);

    string CacheStringKey { get; set; }
}

And here is one of my queries:

public class GetAssetsForUserQuery : ICachedQuery<Task<List<Asset>>, User>
{
    readonly IDataContext dataContext;
    public string CacheStringKey { get; set; }

    public GetAssetsForUserQuery(IDataContext dataContext)
    {
        CacheStringKey = "GetAssetsForUserQuery";
        this.dataContext = dataContext;
    }

    public async Task<List<Asset>> Execute(User user)
    {
        var allAssets = dataContext.Assets.ToList();
        return allAssets;
    }

}

My decorator is not too relevant in this case, but it here is the signature:

public class CachedCachedQueryDecorator<T, P> : ICachedQuery<T, P>

I register my query and decorater in Startup.cs like so:

Container.RegisterDecorator(typeof(ICachedQuery<,>), typeof(CachedCachedQueryDecorator<,>));

Container.Register<GetAssetsForUserQuery>();

And I inject my GetAssetsForUserQuery like so:

readonly GetAssetsForUserQuery getAssetsForUserQuery;

        public GetTagsForUserQuery(GetAssetsForUserQuery getAssetsForUserQuery)
        {
            this.getAssetsForUserQuery = getAssetsForUserQuery;
        }

But my decorator is never hit! Now, if I register my query to the interface ICachedQuery in Startup.cs like so:

Container.Register(typeof(ICachedQuery<,>), typeof(GetAssetsForUserQuery));

And I inject ICachedQuery instead of GetAssetsForUserQuery, then my decorator is hit. But ICachedQuery is a generic so I can't have it resolve for one specific query.

I know I am doing something fundamentally wrong, any help?


Solution

  • But my decorator is never hit!

    That's correct. To understand why this is the case, it's best to visualize the object graph that you wish to be constructed:

    new GetTagsForUserQuery(
        new CachedCachedQueryDecorator<Task<List<Asset>>, User>(
            new GetAssetsForUserQuery()))
    

    PRO TIP: For many DI-related problems, it is very useful to construct the required object graph in plain C#, as the previous code snippet shows. This presents you with a clear mental model. This not only is a useful model for yourself, it is a useful way of communicating to others what it is you are trying to achieve. This is often much harder to comprehend when just showing DI registrations.

    If you try this, however, this code won't compile. It won't compile because GetTagsForUserQuery requires a GetAssetsForUserQuery in its constructor, but a CachedCachedQueryDecorator<Task<List<Asset>>, User> is not a GetTagsForUserQuery—they are both an ICachedQuery<Task<List<Asset>>, User>, but that's not what GetTagsForUserQuery requires.

    Because of this, it is technically impossible to wrap GetAssetsForUserQuery with a CachedCachedQueryDecorator and inject that decorator into GetTagsForUserQuery. And the same holds when you would be resolving GetAssetsForUserQuery directly from Simple Injector like this:

    GetAssetsForUserQuery query = container.GetInstance<GetAssetsForUserQuery>();
    

    In this case you are requesting a GetAssetsForUserQuery from the container, and this type is compile-time enforced. Also in this case it is impossible to wrap GetAssetsForUserQuery with the decorator while preserving GetAssetsForUserQuery's type.

    What would work, though, is requesting the type by its abstraction:

    ICachedQuery<Task<List<Asset>>, User> query =
        container.GetInstance<ICachedQuery<Task<List<Asset>>, User>>();
    

    In this case, you are requesting an ICachedQuery<Task<List<Asset>>, User> and the container is free to return you any type, as long as it implements ICachedQuery<Task<List<Asset>>, User>.

    Same holds for your GetTagsForUserQuery. Only when you let it depend on ICachedQuery<,>, makes it possible to decorate it. The solution is, therefore, to register GetAssetsForUserQuery by its abstraction:

    Container.RegisterDecorator(
        typeof(ICachedQuery<,>),
        typeof(CachedCachedQueryDecorator<,>));
    
    Container.Register<ICachedQuery<Task<List<Asset>>, User>, GetAssetsForUserQuery>();
    

    Here are a few tips, though:

    • Whether or not your queries (I typically call them the 'handlers', but what's in the name) are cacheable or not is an implementation detail. You shouldn't have to define a different abstraction for cacheable queries, and consumers shouldn't have to be aware of that.
    • Instead of exposing a separate CacheStringKey, try using the P args as the cache key. This can be done, for instance, by serializing the args to a JSON object. This makes caching more transparent. In case the args object is very complex, the number of cache entries will be too big anyway, so you typically only want to cache results of very simple arg requests.
    • Whether or not to cache, is rather an implementation detail that either should be incorporated in the Composition Root, or part of the query (handler) implementation. I typically do this by marking that implementation with an attribute, but an interface can work as well. You can then apply the decorator conditionally.
    • Prevent supplying full blown entities both as input and as output for your query (handlers). Instead use seperate data-centric POCOs (like DTOs). What does it mean to send a User as input? It's much clearer, though, when you send a GetAllUserAssets object. That GetAllUserAssets can probable just contain a UserId property. This makes it very easy to turn this object into a cachable entry. The same holds for output objects. Entities are very hard to cache reliably. This is much easier with POCOs or DTOs. They can be serialized with much less effort and risk.

    I've written about CQRS-styled architectures in the past myself. See for instance this article. That article explains some of the points summed up above.