Entity Framework Core 2.0 全局查詢過濾器


不定時更新翻譯系列,此系列更新毫無時間規律,文筆菜翻譯菜求各位看官老爺們輕噴,如覺得我翻譯有問題請挪步原博客地址

本博文翻譯自:
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。

Global query filters: data in playlists table

當運行web應用程序時,將顯示下面的表。

Global query filters: results of global filters

當我們比較這兩個表時,我們會很容易發現全局查詢過濾器在工作中給出的預期結果。

結束

全局查詢過濾器是Entity Framework Core 2.0的完美補充,如果沒有很多實體,那么我們可以通過文檔中給出的簡單示例來實現。在更復雜的情況下,需要一些復雜的代碼來自動應用全局查詢過濾器。希望將來會有更好的解決方案,但目前這里給出的解決方案也做得很好。

歡迎轉載,轉載請注明翻譯原文出處(本文章),原文出處(原博客地址),然后謝謝觀看

如果覺得我的翻譯對您有幫助,請點擊推薦支持:)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM