Entity Framework Core 2.0 全局查询过滤器

时间:2022-07-26 14:22:11

不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址

本博文翻译自:

http://gunnarpeipman.com/2017/08/ef-core-global-query-filters/

Entity Framework Core 2.0 全局查询过滤器

Entity Framework Core 2.0引入了全局查询过滤器,可以在创建模型时应用到实体 。它使得构建多租户应用程序和支持对实体 的软删除变得更加容易。这篇博客文章提供了关于如何在实际应用中使用全局查询过滤器的更深入的概述,以及如何将全局查询过滤器自动应用到领域实体。

示例解决方案。 我在 ASP.NET Core 2中构建了示例解决方案EFCoreGlobalQueryFilters 在更复杂的上下文中演示了全局查询过滤器。它演示了如何自动地将全局查询过滤器应用到领域实体。创建简单的数据库并使用sql脚本填充测试数据。

How global query filters look like?

全局查询过滤器是什么?

这就是全局查询筛选器在软删除时的样子。我们在DbContext类中重写了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应用程序必须有某种方式来检测与当前请求相关的租户。它可以是基于host的header检测,但也可以是别的东西。在这篇文章中我们使用虚拟的提供者以便于我们提供简单的示例。


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

这个提供者必须在启动类的ConfigureServices方法中注册。

创建数据上下文

我希望在这一点上,已经创建了这个数据库,并配置了应用程序来使用它,好了现在让我们从支持租户提供程序的简单数据上下文开始


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

现在我们有了可操作的context和租户ID,那么接下来我们就可以对自动创建的全局查询过滤器进行下一步操作了。

检测实体类型

在为所有实体类型添加全局查询过滤器之前,必须检测实体类型。如果我们知道基本实体类型,那么就很容易读取这些类型。但是有一个问题-model是建立在每个请求之上的,而我们每次在创建model时都要扫描程序集,显然这并不是一个好主意。因此,类型检测必须支持某种类型的缓存。下面示例中的这两个方法用于数据上下文类。


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方法。如果您编写真正的应用程序,那么最好使用单独的提供程序。

现在,数据上下文知道实体类型,并且可以编写一些代码来获得适用于所有实体的查询过滤器。

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

这听起来很容易做,但事实并非如此。有些实体类型的列表,并没有直接使用方便的通用方法。在这一点上,需要一个小技巧。我从CodeDump页面找到了解决方案EF-Core 2.0 过滤所有查询 (并试图实现软删除). 这里的代码不能使用,因为这里的数据上下文对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应用程序现在可以运行了。下面是我使用的示例数据。让我们记住,示例应用程序使用的租户ID是069b57ab-6ec7-479c-b6d4-a61ba3001c86。

Entity Framework Core 2.0 全局查询过滤器

当运行web应用程序时,将显示下面的表。

Entity Framework Core 2.0 全局查询过滤器

当我们比较这两个表时,我们会很容易发现全局查询过滤器在工作中给出的预期结果。

结束

全局查询过滤器是Entity Framework Core 2.0的完美补充,如果没有很多实体,那么我们可以通过文档中给出的简单示例来实现。在更复杂的情况下,需要一些复杂的代码来自动应用全局查询过滤器。希望将来会有更好的解决方案,但目前这里给出的解决方案也做得很好。

欢迎转载,转载请注明翻译原文出处(本文章),原文出处(原博客地址),然后谢谢观看

如果觉得我的翻译对您有帮助,请点击推荐支持:)