ABP Framework:移除 EF Core Migrations 專案,統一資料上下文

iEricLee發表於2021-07-06

原文:Unifying DbContexts for EF Core / Removing the EF Core Migrations Project

導讀:軟體開發的一切都需要平衡

ABP Framework V4.4 RC 新增功能介紹 中,對應用程式啟動解決方案模板做了一個重要改變:刪除 EntityFrameworkCore.DbMigrations 專案。

本文將詳細解讀背後的原因和解決方案。

  1. 理解動機很重要:為什麼先前的版本要將要資料上下文進行分離,而現在為什麼要合併?
  2. 合併之後存在什麼缺陷,以及如何解決?

這篇檔案演示如何將解決方案中 EntityFrameworkCore.DbMigrations 專案移除,並實現使用 單個 DbContext 進行資料實體對映資料遷移

本篇文章專案原始碼

關注 ABP Framework 最新開發進度,後面還會陸續釋出新功能詳解新功能示例等系列文章,敬請關注!
ABP Framework 研習社(QQ群:726299208)
專注 ABP Framework 學習,經驗分享、問題討論、示例原始碼、電子書共享,歡迎加入!

動機

如果使用啟動模板生成解決方案,資料庫提供程式是 Entity Framework Core,那麼在解決方案中會存在依賴 EF Core的兩個專案:

  • .EntityFrameworkCore
  • .EntityFrameworkCore.DbMigrations

.EntityFrameworkCore專案:包含應用程式真實的 DbContext資料庫對映倉儲實現

.EntityFrameworkCore.DbMigrations專案:包含另一個 DbContext 只用於建立資料遷移。包含所有正在使用的模組的資料實體對映,生成統一的資料庫表結構。

分離的原因有兩個:

  1. 真實 DbContext 保持簡單和專注。只包含當前專案相關的實體,而與在應用程式使用的模組的實體和資料上下文無關,因為每個模組都有自己的 DbContext ,而將模型建立方法單獨放在 EntityFrameworkCore.DbMigrations 專案中。
  2. 複用依賴模組中的表,通過建立自己的類,對映到依賴模組中的表。舉例,自定義 AppUser 實體對映到資料庫中 AbpUsers 表,實際上該表由 Identity 模組IdentityUser 實體對映生成。他們共用相同的資料庫表。和 IdentityServer 實體相比 AppUser 包含的屬性更少,可以根據需要在 AppUser 中新增所需的屬性,只需要設定好資料庫對映,新增欄位會新增到對映表中。

我們詳細的描述了這種結構。然而,對於開發者,仍然存在問題,因為當需要複用依賴模組中的表時,這種結構會使的資料實體對映變得複雜。

許多開發者在對映這些類時容易產生誤解犯錯,特別是當試圖使用的實體與其他實體存在關聯關係時。

所以我們在 V4.4 版本中決定取消這種分離,刪除 EntityFrameworkCore.DbMigrations 專案。新的啟動方案將帶只有一個 EntityFrameworkCore 專案和一個 DbContext 類。

如果你想在你的解決方案中加入今天的內容,請遵循本文的步驟。

警告

新的設計有一個缺點。我們必須刪除 AppUser 實體,因為不能在同一個 DbContext 中很好地處理沒有繼承關係的兩個類對映到同一張表中。在本文的後面會介紹這個問題,並提供處理它的建議。

如果您使用 ABP Commercial 商業版,ABP套件程式碼生成功能還不會採用本文中提到的設計方法,建議等待下一個版本。

步驟

我們的目標是刪除 EntityFrameworkCore.DbMigrations 專案,在 EntityFrameworkCore 專案中啟用資料庫遷移,替換遷移專案的依賴。

原解決方案是基於 v4.3 建立一個新的解決方案,然後在 pull request 中記錄所有的修改,所以你可以逐行看到所有的修改。雖然這篇文章將涵蓋所有的內容,但如果你在實現過程中遇到問題,你可能想檢查這個PR中所做的修改

第一步:新增 Microsoft.EntityFrameworkCore.Tools 包到 EntityFrameworkCore 專案

將下面程式碼新增到 EntityFrameworkCore.csproj 檔案:

<ItemGroup>
  <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.*">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    <PrivateAssets>compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native</PrivateAssets>
  </PackageReference>
</ItemGroup>

第二步:建立設計時 DbContext 工廠

EntityFrameworkCore 專案中建立實現 IDesignTimeDbContextFactory<T> 介面的資料上下文工廠

using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;

namespace UnifiedContextsDemo.EntityFrameworkCore
{
    public class UnifiedContextsDemoDbContextFactory : IDesignTimeDbContextFactory<UnifiedContextsDemoDbContext>
    {
        public UnifiedContextsDemoDbContext CreateDbContext(string[] args)
        {
            UnifiedContextsDemoEfCoreEntityExtensionMappings.Configure();

            var configuration = BuildConfiguration();

            var builder = new DbContextOptionsBuilder<UnifiedContextsDemoDbContext>()
                .UseSqlServer(configuration.GetConnectionString("Default"));

            return new UnifiedContextsDemoDbContext(builder.Options);
        }

        private static IConfigurationRoot BuildConfiguration()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../UnifiedContextsDemo.DbMigrator/"))
                .AddJsonFile("appsettings.json", optional: false);

            return builder.Build();
        }
    }
}

基本上是從 EntityFrameworkCore.DbMigrations 專案中複製的,重新命名並使用應用程式的實際 DbContext

第三步:建立 資料庫模式遷移器

複製 EntityFrameworkCore...DbSchemaMigrator(省略號表示專案命名)類到 EntityFrameworkCore 專案中,修改 MigrateAsync 方法中的程式碼,以使用真實 DbContext

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using UnifiedContextsDemo.Data;
using Volo.Abp.DependencyInjection;

namespace UnifiedContextsDemo.EntityFrameworkCore
{
    public class EntityFrameworkCoreUnifiedContextsDemoDbSchemaMigrator
        : IUnifiedContextsDemoDbSchemaMigrator, ITransientDependency
    {
        private readonly IServiceProvider _serviceProvider;

        public EntityFrameworkCoreUnifiedContextsDemoDbSchemaMigrator(
            IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public async Task MigrateAsync()
        {
            /* We intentionally resolving the UnifiedContextsDemoMigrationsDbContext
             * from IServiceProvider (instead of directly injecting it)
             * to properly get the connection string of the current tenant in the
             * current scope.
             */

            await _serviceProvider
                .GetRequiredService<UnifiedContextsDemoDbContext>()
                .Database
                .MigrateAsync();
        }
    }
}

第四步 轉移資料庫實體對映配置

遷移 DbContext 中包含 builder.ConfigureXXX() 對應每個使用的模組的資料實體對映配置。移動這些配置到 EntityFrameworkCore 專案的 真實 DbContext 中,並移除 AppUser 資料庫實體對映。

可以選擇將自己定義的實體資料庫對映程式碼從...DbContextModelCreatingExtensions類中移到 真實 DbContextOnModelCreating 方法中,並刪除該靜態擴充套件類。

示例解決方案中,最終 DbContext 程式碼如下:

using Microsoft.EntityFrameworkCore;
using UnifiedContextsDemo.Users;
using Volo.Abp.AuditLogging.EntityFrameworkCore;
using Volo.Abp.BackgroundJobs.EntityFrameworkCore;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.FeatureManagement.EntityFrameworkCore;
using Volo.Abp.Identity.EntityFrameworkCore;
using Volo.Abp.IdentityServer.EntityFrameworkCore;
using Volo.Abp.PermissionManagement.EntityFrameworkCore;
using Volo.Abp.SettingManagement.EntityFrameworkCore;
using Volo.Abp.TenantManagement.EntityFrameworkCore;

namespace UnifiedContextsDemo.EntityFrameworkCore
{
    [ConnectionStringName("Default")]
    public class UnifiedContextsDemoDbContext
        : AbpDbContext<UnifiedContextsDemoDbContext>
    {
        public DbSet<AppUser> Users { get; set; }

        /* Add DbSet properties for your Aggregate Roots / Entities here.
         * Also map them inside UnifiedContextsDemoDbContextModelCreatingExtensions.ConfigureUnifiedContextsDemo
         */

        public UnifiedContextsDemoDbContext(
            DbContextOptions<UnifiedContextsDemoDbContext> options)
            : base(options)
        {

        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.ConfigurePermissionManagement();
            builder.ConfigureSettingManagement();
            builder.ConfigureBackgroundJobs();
            builder.ConfigureAuditLogging();
            builder.ConfigureIdentity();
            builder.ConfigureIdentityServer();
            builder.ConfigureFeatureManagement();
            builder.ConfigureTenantManagement();

            /* Configure your own tables/entities inside here */

            //builder.Entity<YourEntity>(b =>
            //{
            //    b.ToTable(UnifiedContextsDemoConsts.DbTablePrefix + "YourEntities", UnifiedContextsDemoConsts.DbSchema);
            //    b.ConfigureByConvention(); //auto configure for the base class props
            //    //...
            //});
        }
    }
}

第五步:從解決方案中移除 EntityFrameworkCore.DbMigrations 專案

從解決方案中移除 EntityFrameworkCore.DbMigrations 專案,將對該專案的引用替換為 EntityFrameworkCore 專案引用。

同樣地,將模組依賴 ...EntityFrameworkCoreDbMigrationsModule 替換為 ...EntityFrameworkCoreModule

示例專案中,涉及的專案為 DbMigrator WebWeb and EntityFrameworkCore.Tests

第六步:移除 AppUser 實體

我們需要將 AppUser 這個實體移除,因為 EF Core 不能兩個非繼承關係的類對映到單個表。所以,刪除這個類和所有的對該類的使用。如果你需要在應用程式程式碼中查詢使用者,可以用 IdentityUser 替換。更多資訊請參見 AppUser 實體和自定義屬性部分。

第七步:建立資料遷移

如果需要使用資料遷移歷史記錄,可以直接將 EntityFrameworkCore.DbMigrations 專案中生成的 migrations 複製到 EntityFrameworkCore 專案,並手動修改其中的 DbContext 型別。

如果需要在已經應用了資料遷移的資料庫中,繼續應用新的資料遷移,在 EntityFrameworkCore 專案中,建立新的資料庫遷移,執行命令:

dotnet ef migrations add InitialUnified

你可以指定一個不同的遷移名稱,這將建立一個遷移類,其中包含你在資料庫中已有的所有資料庫表。注意,刪除 UpDown 方法中的所有內容,然後就可以將遷移應用到資料庫中。

dotnet ef database update

資料庫不會有任何變化,因為遷移是空的,什麼都不做。從現在開始,可以在改變實體時,建立新的遷移,就像平時做的那樣。

DbContext 合併已經完成。接下來將解決如何基於這種設計為依賴模組的實體新增自定義屬性

AppUser 實體 和自定義屬性

資料庫對映邏輯、解決方案結構和資料遷移,變得簡單和易於管理。

帶來的弊端是,我們必須移除 AppUser 實體,因為其與 Identity 模組中 IdentityUser 實體共享 AbpUsers 表。幸運的是,ABP提供了一個靈活的系統來 擴充套件現有的實體 ,如果你需要定義一些自定義屬性的話。

在本節中,我將展示如何向 IdentityUser 實體新增一個自定義屬性,並在你的應用程式程式碼和資料庫查詢中使用它。

我已經把這部分的所有修改作為一個單獨的PR完成了,所以如果你在實現上有問題,你可能想檢查這個PR中的修改

定義一個自定義屬性

應用程式啟動模板提供一個配置點,為實體新增自定義屬性,位於 Domain.Shared 專案中 ...ModuleExtensionConfigurator.cs 類,在 ConfigureExtraProperties 方法中,新增程式碼:

ObjectExtensionManager.Instance.Modules()
    .ConfigureIdentity(identity =>
    {
        identity.ConfigureUser(user =>
        {
            user.AddOrUpdateProperty<string>( //屬性型別: string
                "SocialSecurityNumber", //屬性名
                property =>
                {
                    //validation rules
                    property.Attributes.Add(new RequiredAttribute());
                    property.Attributes.Add(new StringLengthAttribute(64));
                }
            );
        });
    });

設定完成後,只要執行應用程式就可以看到使用者表上的新屬性。

image

新的SocialSecurityNumber屬性也將在建立和編輯模式中應用新增的驗證規則。

參看 模組實體擴充套件 文件,理解和使用自定義屬性。

對映到資料庫表

ABP預設將所有自定義屬性作為一個 Json 物件儲存到 ExtraProperties 欄位。如果要為自定義屬性建立表欄位,可以在 EntityFrameworkCore 專案 ...EfCoreEntityExtensionMappings.cs 中配置,在該類(OneTimeRunner.Run)中新增如下程式碼:

ObjectExtensionManager.Instance
    .MapEfCoreProperty<IdentityUser, string>(
        "SocialSecurityNumber",
        (entityBuilder, propertyBuilder) =>
        {
            propertyBuilder.HasMaxLength(64).IsRequired().HasDefaultValue("");
        }
    );

然後,直接在 EntityFrameworkCore 專案中執行新增資料遷移命令:

dotnet ef migrations add Added_SocialSecurityNumber_To_IdentityUser

將在專案彙總新增一個新的資料遷移類,接著可以通過執行 .DbMigrator 應用或如下命令應用修改到資料庫:

dotnet ef database update

將會在資料庫 AbpUsers 表中新增欄位 SocialSecurityNumber 。

使用自定義屬性

現在,可以使用 IdentityUser 實體中 GetPropertySetProperty 方法操作新新增的屬性。下面示例程式碼演示如何獲取和設定自定義屬性:

public class MyUserService : ITransientDependency
{
    private readonly IRepository<IdentityUser, Guid> _userRepository;

    public MyUserService(IRepository<IdentityUser, Guid> userRepository)
    {
        _userRepository = userRepository;
    }

    public async Task SetSocialSecurityNumberDemoAsync(string userName, string number)
    {
        var user = await _userRepository.GetAsync(u => u.UserName == userName);
        user.SetProperty("SocialSecurityNumber", number);
        await _userRepository.UpdateAsync(user);
    }

    public async Task<string> GetSocialSecurityNumberDemoAsync(string userName)
    {
        var user = await _userRepository.GetAsync(u => u.UserName == userName);
        return user.GetProperty<string>("SocialSecurityNumber");
    }
}

提示:使用 SetPropertyGetProperty 使用字串屬性名可能會很繁瑣,而且容易出錯。建議建立以下擴充套件方法:

public static class MyUserExtensions
{
    public const string SocialSecurityNumber = "SocialSecurityNumber";

    public static void SetSocialSecurityNumber(this IdentityUser user, string number)
    {
        user.SetProperty(SocialSecurityNumber, number);
    }

    public static string GetSocialSecurityNumber(this IdentityUser user)
    {
        return user.GetProperty<string>(SocialSecurityNumber);
    }
}

然後我們可以改變之前的演示方法,如下圖所示。

public async Task SetSocialSecurityNumberDemoAsync(string userName, string number)
{
    var user = await _userRepository.GetAsync(u => u.UserName == userName);
    user.SetSocialSecurityNumber(number); //Using the new extension property
    await _userRepository.UpdateAsync(user);
}

public async Task<string> GetSocialSecurityNumberDemoAsync(string userName)
{
    var user = await _userRepository.GetAsync(u => u.UserName == userName);
    return user.GetSocialSecurityNumber(); //Using the new extension property
}

基於自定義屬性查詢

新增自定義屬性之後,我們可能需要基於自定義屬性查詢。是否可以基於 Entity Framework 的 API 來實現?有兩種方式實現在應用程式中使用EF Core API:(這與自定義屬性無關,與 EF Core有關。)

  1. 領域層或應用層引用 Microsoft.EntityFrameworkCore Nuget包,在那個專案中引用取決於你要在哪需要使用 EF Core API。(DDD中資料提供程式無關性原則衝突)
  2. 在領域層建立倉儲介面,然後在 EntityFrameworkCore 專案中實現介面。

推薦使用第二種方式,在 Domain 專案中定義一個新的倉儲介面:

using System;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Identity;

namespace UnifiedContextsDemo.Users
{
    public interface IMyUserRepository : IRepository<IdentityUser, Guid>
    {
        Task<IdentityUser> FindBySocialSecurityNumber(string number);
    }
}

在 EntityFrameworkCore 專案中實現介面:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using UnifiedContextsDemo.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Identity;

namespace UnifiedContextsDemo.Users
{
    public class MyUserRepository
        : EfCoreRepository<UnifiedContextsDemoDbContext, IdentityUser, Guid>,
          IMyUserRepository
    {
        public MyUserRepository(
            IDbContextProvider<UnifiedContextsDemoDbContext> dbContextProvider)
            : base(dbContextProvider)
        {
        }

        public async Task<IdentityUser> FindBySocialSecurityNumber(string number)
        {
            var dbContext = await GetDbContextAsync();
            return await dbContext.Set<IdentityUser>()
                .Where(u => EF.Property<string>(u, "SocialSecurityNumber") == number)
                .FirstOrDefaultAsync();
        }
    }
}

提示:應該使用一個常量代替SocialSecurityNumber魔術字串。(不會產生拼寫錯誤)

現在,我可以在應用服務中依賴注入 IMyUserRepository 使用倉儲介面:

public class MyUserService : ITransientDependency
{
    private readonly IMyUserRepository _userRepository;

    public MyUserService(IMyUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    //...other methods

    public async Task<IdentityUser> FindBySocialSecurityNumberDemoAsync(string number)
    {
        return await _userRepository.FindBySocialSecurityNumber(number);
    }
}

使用自定義倉儲介面 IMyUserRepository 代替泛型倉儲介面 IRepository<IdentityUser, Guid>

討論 Github

這篇文章演示了,如何將 EntityFrameworkCore.DbMigrations 專案從解決方案中移除,以簡化資料庫實體對映、資料遷移和應用程式中的程式碼。

在下一個版本(4.4),將作為預設處理。

討論:Consider to remove EntityFrameworkCore.DbMigrations project from the solution #8776

dotNET兄弟會-公眾號

專注.Net開源技術及跨平臺開發!致力於構建完善的.Net開放技術文庫!為.Net愛好者提供學習交流家園!

image

相關文章