实体框架核心2.0中的全局查询过滤器


实体框架核心2.0引入了全局查询过滤器,可以在创建模型时应用于实体。它使构建多租户应用程序变得更加容易,并支持实体的软删除。这篇博文对如何在现实应用程序中使用全局查询过滤器以及如何将全局查询过滤器自动应用到域实体进行了更深入的概述。

示例解决方案:我构建了一个示例解决方案EFCoreGlobalQueryFilters在ASP.NET核心2上演示了在更复杂的上下文中的全局查询过滤器。它演示了一些关于如何将全局查询过滤器自动应用于域实体的想法。创建一个简单数据库并用测试数据填充它的SQL脚本也在那里。

全局查询过滤器是什么样的?

这就是全局查询过滤器寻找软删除的方式。对数据库上下文类的OnModelCreating方法的此重写:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Playlist>().HasKey(e => e.Id);
    modelBuilder.Entity<Playlist>().HasQueryFilter(e => !e.IsDeleted);
    modelBuilder.Entity<Song>().HasKey(e => e.Id);
    modelBuilder.Entity<Song>().HasQueryFilter(e => !e.IsDeleted);       base.OnModelCreating(modelBuilder);
}

当查询给定类型的实体时,总是应用这些过滤器。

真正的应用需要什么?

上面的代码被简化了,没有考虑现实生活中的场景。当考虑作为数字核心或企业的一部分的任务关键型应用时,将不仅仅是几个类别,尽管应用的体系结构通常是复杂的。这篇文章的目的是演示以下内容:

  • 如何支持多租户,
  • 如何支持实体的软删除,以及
  • 如何自动检测实体。

示例解决方案有助于从更复杂的场景开始,但它没有为此提供完全灵活和复杂的框架。当涉及到现实生活中的应用程序时,有太多的细微差别,每个应用程序通常都有自己的一套解决不同问题的方法。

定义实体

让我们从定义一些实体开始。它们使用一个简单的基类,并且期望所有的实体都从基类扩展。

public abstract class BaseEntity
{
    public int Id { get; set; }
    public Guid TenantId { get; set; }
    public bool IsDeleted { get; set; }
}   

public class Playlist : BaseEntity
{
    public string Title { get; set; }       
    public IList<Song> Songs { get; set; }
}   

public class Song : BaseEntity
{
    public string Artist { get; set; }
    public string Title { get; set; }
    public string Location { get; set; }
}

现在有了一些简单的实体,是时候朝着多租户和软删除实体迈出下一步了。

租户提供商

在讨论多租户之前,web应用程序必须有某种方法来检测与当前请求相关的租户。它可以是基于主机头的检测,但也可以是其他的。这篇文章使用了一个虚拟的提供者来保持简单。

public interface ITenantProvider
{
    Guid GetTenantId();
}   public class DummyTenantProvider : ITenantProvider
{
    public Guid GetTenantId()
    {
        return Guid.Parse("069b57ab-6ec7-479c-b6d4-a61ba3001c86");
    }
}

此提供程序必须在启动类的配置服务方法中注册。

创建数据上下文

我希望此时数据库已经创建,并且应用程序已经配置为使用它。让我们从一个已经支持租户提供者的简单数据上下文开始。

public class PlaylistContext : DbContext
{
    private Guid _tenantId;
    private readonly IEntityTypeProvider _entityTypeProvider;       

    public virtual DbSet<Playlist> Playlists { get; set; }
    public virtual DbSet<Song> Songs { get; set; }       

    public PlaylistContext(DbContextOptions<PlaylistContext> options,
                            ITenantProvider tenantProvider)
        : base(options)
    {
        _tenantId = tenantProvider.GetTenantId();
    }       

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Playlist>().HasKey(e => e.Id);
        modelBuilder.Entity<Song>().HasKey(e => e.Id);           base.OnModelCreating(modelBuilder);
    }       
}

有了数据上下文和可用的租户标识,是时候向自动创建的全局查询过滤器迈出下一步了。

检测实体类型

在为所有实体类型添加全局查询过滤器之前,必须检测实体类型。如果基本实体类型是已知的,那么很容易阅读这些类型。有一个问题——模型是根据每个请求构建的,但是每次创建模型时扫描程序集并不是一个好主意。因此,类型检测必须支持某种缓存。这两个方法转到数据上下文类。

private static IList<Type> _entityTypeCache;
private static IList<Type> GetEntityTypes()
{
    if(_entityTypeCache != null)
    {
        return _entityTypeCache.ToList();
    }       

    _entityTypeCache = (from a in GetReferencingAssemblies()
                        from t in a.DefinedTypes
                        where t.BaseType == typeof(BaseEntity)
                        select t.AsType()).ToList();       

    return _entityTypeCache;
}   

private static IEnumerable<Assembly> GetReferencingAssemblies()
{
    var assemblies = new List<Assembly>();
    var dependencies = DependencyContext.Default.RuntimeLibraries;       

    foreach (var library in dependencies)
    {
        try
        {
            var assembly = Assembly.Load(new AssemblyName(library.Name));
            assemblies.Add(assembly);
        }
        catch (FileNotFoundException)
        { }
    }
    return assemblies;
}

警告!就架构而言,如果有一个单独的返回实体类型的服务,这可能是一个更好的主意。在上面的代码中,可以直接使用实体类型变量,更糟糕的是,可以调用GetReferencingAssemblies方法。如果你写了一个真正的应用程序,那么最好用一个单独的提供者。

现在,数据上下文知道了实体类型,可以编写一些代码来将查询过滤器应用于所有实体。

将查询过滤器应用于所有实体

这听起来像是一件容易的事情,但它不是。有一个实体类型列表,没有办法直接使用方便的泛型方法。在这一点上,需要一点技巧。我从代码转储页面找到了一个解决方案EF-Core 2.0 Filter all queries (trying to achieve soft delete)。那里的代码不能按原样使用,因为这里的数据上下文具有对ITenantProvider的实例级依赖。但是要点是一样的:让我们创建一个对数据上下文中存在的某个泛型方法的泛型方法调用。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    foreach (var type in GetEntityTypes())
    {           
        var method = SetGlobalQueryMethod.MakeGenericMethod(type);
        method.Invoke(this, new object[] { modelBuilder });
    }       

    base.OnModelCreating(modelBuilder);
}   

static readonly MethodInfo SetGlobalQueryMethod = typeof(PlaylistContext).GetMethods(BindingFlags.Public | BindingFlags.Instance)
                                                        .Single(t => t.IsGenericMethod && t.Name == "SetGlobalQuery");   public void SetGlobalQuery<T>(ModelBuilder builder) where T : BaseEntity
{
    builder.Entity<T>().HasKey(e => e.Id);
    //Debug.WriteLine("Adding global query for: " + typeof(T));
    builder.Entity<T>().HasQueryFilter(e => e.TenantId == _tenantId && !e.IsDeleted);
}

这不是简单直观的代码。当我看着这个代码的时候,我都瞪大了眼睛。即使我看了一百遍,它仍然看起来疯狂和尴尬。SetGlobalQuery方法也是为实体定义主键的好地方,因为它们都继承自同一个基本实体类。

试车

为了测试全局查询过滤器是如何工作的,可以使用示例应用程序的HomeController。

public class HomeController : Controller
{
    private readonly PlaylistContext _context;       

    public HomeController(PlaylistContext context)
    {
        _context = context;
    }       

    public IActionResult Index()
    {
        var playlists = _context.Playlists.OrderBy(p => p.Title);           

        return View(playlists);
    }
}

我修改了默认视图来显示查询返回的所有播放列表。

@model IEnumerable<Playlist>

<div class="row">
    <div class="col-lg-8">
        <h2>Playlists</h2>         <table class="table table-bordered">
            <thead>
                <tr>
                    <th>Playlist</th>
                    <th>Tenant ID</th>
                    <th>Is deleted</th>
                </tr>
            </thead>
            <tbody>
                @foreach(var playlist in Model)
                {
                    <tr>
                        <td>@playlist.Title</td>
                        <td>@playlist.TenantId</td>
                        <td>@playlist.IsDeleted</td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
</div>

web应用程序现在可以运行了。这是我正在使用的示例数据。让我们记住,示例应用程序使用的租户标识是069 b57 ab-6ec 7-479 c-B6 D4-a61 ba 3001 c 86。

Global query filters: data in playlists table

当web应用程序运行时,将显示下表:

Global query filters: results of global filters

当比较这两个表时,很容易注意到全局查询过滤器工作并给出预期的结果。

包扎

全局查询过滤器是对实体框架核心2.0的一个很好的补充,在没有很多实体之前,可以使用文档中给出的简单例子。对于更复杂的场景,需要一些复杂的代码来自动应用全局查询过滤器。希望将来会有更好的解决方案,但是目前,这里给出的解决方案也非常有效。