简单的注入器——为 EF Core 缓存创建一个通用的装饰器

Simple injector - create a generic decorator for EF Core caching

我正在尝试使用简单注入器作为我的 DI 在我的 .NET Core 项目中实现 EF Core 的缓存。我正在使用 CQRS 模式,所以我有一堆查询要缓存(不是全部)。

我为缓存查询创建了一个通用接口,它采用 return 类型的查询和查询参数:

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

    string CacheStringKey { get; set; }
}

这是我的一个疑问:

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;
    }

}

我的装饰器在这种情况下并不太相关,但它是签名:

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

我在 Startup.cs 中注册我的查询和装饰器,如下所示:

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

Container.Register<GetAssetsForUserQuery>();

然后我像这样注入 GetAssetsForUserQuery

readonly GetAssetsForUserQuery getAssetsForUserQuery;

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

但是我的装饰器从来没有被击中过!现在,如果我像这样将查询注册到 Startup.cs 中的接口 ICachedQuery

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

并且我注入了 ICachedQuery 而不是 GetAssetsForUserQuery,然后我的装饰器被命中了。但是 ICachedQuery 是通用的,所以我无法针对一个特定的查询解析它。

我知道我在做一些根本性的错误,有什么帮助吗?

But my decorator is never hit!

没错。要理解为什么会这样,最好可视化您希望构建的对象图:

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.

但是,如果您尝试这样做,此代码将无法编译。它不会编译,因为 GetTagsForUserQuery 在其构造函数中需要 GetAssetsForUserQuery,但 CachedCachedQueryDecorator<Task<List<Asset>>, User> 不是 GetTagsForUserQuery——它们都是 ICachedQuery<Task<List<Asset>>, User>,但这是不是 GetTagsForUserQuery 要求的。

因此,技术上 不可能CachedCachedQueryDecorator 包装 GetAssetsForUserQuery 并将装饰器注入 GetTagsForUserQuery。当你像这样直接从 Simple Injector 解析 GetAssetsForUserQuery 时也是如此:

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

在这种情况下,您从容器请求 GetAssetsForUserQuery,并且此类型是编译时强制执行的。同样在这种情况下,不可能在保留 GetAssetsForUserQuery 类型的同时用装饰器包装 GetAssetsForUserQuery

不过,可行的方法是通过其 抽象:

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

在这种情况下,您请求的是 ICachedQuery<Task<List<Asset>>, User> 并且容器可以自由 return 您 任何 类型,只要它实现 ICachedQuery<Task<List<Asset>>, User>.

您的 GetTagsForUserQuery 也是如此。只有当你让它依赖于ICachedQuery<,>,才有可能装饰它。因此,解决方案是通过抽象来注册 GetAssetsForUserQuery

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

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

这里有一些提示:

  • 您的查询(我通常称它们为 'handlers',但名称中包含的内容)是否可缓存是一个实现细节。您不必为可缓存查询定义不同的抽象,消费者也不必意识到这一点。
  • 不要公开单独的 CacheStringKey,而是尝试使用 P args 作为缓存键。例如,这可以通过将 args 序列化为 JSON 对象来完成。这使缓存更加透明。如果 args 对象非常复杂,缓存条目的数量无论如何都会太大,因此您通常只想缓存非常简单的 arg 请求的结果。
  • 是否缓存是一个实现细节,应该包含在 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 中。
  • 防止提供完整的实体作为查询(处理程序)的输入和输出。而是使用单独的以数据为中心的 POCO(如 DTO)。发送 User 作为输入是什么意思?但是,当您发送 GetAllUserAssets 对象时,它会更清楚。 GetAllUserAssets 可能只包含一个 UserId 属性。这使得将此对象变成可缓存的条目变得非常容易。这同样适用于输出对象。实体很难可靠地缓存。对于 POCO 或 DTO,这要容易得多。它们可以以更少的努力和风险进行序列化。

我自己过去也写过关于 CQRS 风格的架构的文章。例如参见 [​​=44=]。那篇文章解释了上面总结的一些要点。