ABP Framework:移除 EF Core Migrations 项目,统一数据上下文
原文:Unifying DbContexts for EF Core / Removing the EF Core Migrations Project[1]
導讀:軟件開發的一切都需要平衡
在?ABP Framework V4.4 RC 新增功能介紹?中,對應用程序啟動解決方案模板做了一個重要改變:刪除?EntityFrameworkCore.DbMigrations?項目。
本文將詳細解讀背后的原因和解決方案。
1.理解動機很重要:為什么先前的版本要將要數據上下文進行分離,而現在為什么要合并?2.合并之后存在什么缺陷,以及如何解決?
這篇文件演示如何將解決方案中?EntityFrameworkCore.DbMigrations?項目移除,并實現使用?單個?DbContext?進行數據實體映射和數據遷移。
本篇文章項目源碼[3]
關注?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 模塊[4]?的?IdentityUser?實體映射生成。他們共用相同的數據庫表。和?IdentityServer?實體相比?AppUser?包含的屬性更少,可以根據需要在?AppUser?中添加所需的屬性,只需要設置好數據庫映射,新增字段會添加到映射表中。
我們詳細的描述了這種結構[5]。然而,對于開發者,仍然存在問題,因為當需要復用依賴模塊中的表時,這種結構會使的數據實體映射變得復雜。
許多開發者在映射這些類時容易產生誤解或犯錯,特別是當試圖使用的實體與其他實體存在關聯關系時。
所以我們在?V4.4?版本中決定取消這種分離,刪除?EntityFrameworkCore.DbMigrations?項目。新的啟動方案將帶只有一個?EntityFrameworkCore?項目和一個?DbContext?類。
如果你想在你的解決方案中加入今天的內容,請遵循本文的步驟。
警告
新的設計有一個缺點。我們必須刪除 AppUser 實體,因為不能在同一個?DbContext?中很好地處理沒有繼承關系的兩個類映射到同一張表中。在本文的后面會介紹這個問題,并提供處理它的建議。
如果您使用 ABP Commercial 商業版,ABP套件代碼生成功能還不會采用本文中提到的設計方法,建議等待下一個版本。
步驟
我們的目標是刪除?EntityFrameworkCore.DbMigrations?項目,在?EntityFrameworkCore?項目中啟用數據庫遷移,替換遷移項目的依賴。
原解決方案是基于 v4.3 創建一個新的解決方案,然后在 pull request 中記錄所有的修改,所以你可以逐行看到所有的修改。雖然這篇文章將涵蓋所有的內容,但如果你在實現過程中遇到問題,你可能想檢查這個PR中所做的修改[6]。
第一步:添加 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類中移到?真實 DbContext?的?OnModelCreating?方法中,并刪除該靜態擴展類。
示例解決方案中,最終 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?Web?和?Web and EntityFrameworkCore.Tests?。
第六步:移除 AppUser 實體
我們需要將?AppUser?這個實體移除,因為 EF Core 不能兩個非繼承關系的類映射到單個表。所以,刪除這個類和所有的對該類的使用。如果你需要在應用程序代碼中查詢用戶,可以用?IdentityUser?替換。更多信息請參見 AppUser 實體和自定義屬性部分。
第七步:創建數據遷移
如果需要使用數據遷移歷史記錄,可以直接將?EntityFrameworkCore.DbMigrations?項目中生成的?migrations?復制到?EntityFrameworkCore?項目,并手動修改其中的?DbContext?類型。
如果需要在已經應用了數據遷移的數據庫中,繼續應用新的數據遷移,在?EntityFrameworkCore?項目中,創建新的數據庫遷移,執行命令:
dotnet ef migrations add InitialUnified你可以指定一個不同的遷移名稱,這將創建一個遷移類,其中包含你在數據庫中已有的所有數據庫表。注意,刪除?Up?和?Down?方法中的所有內容,然后就可以將遷移應用到數據庫中。
dotnet ef database update數據庫不會有任何變化,因為遷移是空的,什么都不做。從現在開始,可以在改變實體時,創建新的遷移,就像平時做的那樣。
DbContext 合并已經完成。接下來將解決如何基于這種設計為依賴模塊的實體添加自定義屬性。
AppUser 實體 和自定義屬性
數據庫映射邏輯、解決方案結構和數據遷移,變得簡單和易于管理。
帶來的弊端是,我們必須移除?AppUser?實體,因為其與?Identity?模塊中?IdentityUser?實體共享?AbpUsers?表。幸運的是,ABP提供了一個靈活的系統來?擴展現有的實體[7]?,如果你需要定義一些自定義屬性的話。
在本節中,我將展示如何向?IdentityUser?實體添加一個自定義屬性,并在你的應用程序代碼和數據庫查詢中使用它。
我已經把這部分的所有修改作為一個單獨的PR完成了,所以如果你在實現上有問題,你可能想檢查這個PR中的修改[8]。
定義一個自定義屬性
應用程序啟動模板提供一個配置點,為實體添加自定義屬性,位于 Domain.Shared 項目中?...ModuleExtensionConfigurator.cs?類,在?ConfigureExtraProperties?方法中,添加代碼:
ObjectExtensionManager.Instance.Modules().ConfigureIdentity(identity =>{identity.ConfigureUser(user =>{user.AddOrUpdateProperty<string>( //屬性類型: string"SocialSecurityNumber", //屬性名property =>{//validation rulesproperty.Attributes.Add(new RequiredAttribute());property.Attributes.Add(new StringLengthAttribute(64));});});});設置完成后,只要運行應用程序就可以看到用戶表上的新屬性。
新的SocialSecurityNumber屬性也將在創建和編輯模式中應用添加的驗證規則。
參看?模塊實體擴展[9]?文檔,理解和使用自定義屬性。
映射到數據庫表
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 實體中?GetProperty?和?SetProperty?方法操作新添加的屬性。下面示例代碼演示如何獲取和設置自定義屬性:
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");} }提示:使用?SetProperty?和?GetProperty?使用字符串屬性名可能會很繁瑣,而且容易出錯。建議創建以下擴展方法:
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 propertyawait _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[10]?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 methodspublic 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[11]
References
[1]?Unifying DbContexts for EF Core / Removing the EF Core Migrations Project:?https://community.abp.io/articles/unifying-dbcontexts-for-ef-core-removing-the-ef-core-migrations-project-nsyhrtna
[2]?ABP Framework V4.4 RC 新增功能介紹:?https://www.cnblogs.com/YGYH/p/14973806.html
[3]?項目源碼:?https://github.com/abpframework/abp-samples/tree/master/UnifiedEfCoreMigrations
[4]?Identity 模塊:?https://docs.abp.io/en/abp/latest/Modules/Identity
[5]?描述了這種結構:?https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Migrations
[6]?這個PR中所做的修改:?https://github.com/abpframework/abp-samples/pull/88
[7]?擴展現有的實體:?https://docs.abp.io/en/abp/latest/Module-Entity-Extensions
[8]?檢查這個PR中的修改:?https://github.com/abpframework/abp-samples/pull/89
[9]?模塊實體擴展:?https://docs.abp.io/en/abp/latest/Module-Entity-Extensions
[10]?Microsoft.EntityFrameworkCore:?https://www.nuget.org/packages/Microsoft.EntityFrameworkCore
[11]?Consider to remove EntityFrameworkCore.DbMigrations project from the solution #8776:?https://github.com/abpframework/abp/issues/8776
總結
以上是生活随笔為你收集整理的ABP Framework:移除 EF Core Migrations 项目,统一数据上下文的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NLog源码解读——StringBuil
- 下一篇: 关于c#:Filter Serilog日