原文:Unifying DbContexts for EF Core / Removing the EF Core Migrations Project
導讀:軟件開發的一切都需要平衡
在 ABP Framework V4.4 RC 新增功能介紹 中,對應用程序啟動解決方案模板做了一個重要改變:刪除 EntityFrameworkCore.DbMigrations
項目。
本文將詳細解讀背后的原因和解決方案。
- 理解動機很重要:為什么先前的版本要將要數據上下文進行分離,而現在為什么要合並?
- 合並之后存在什么缺陷,以及如何解決?
這篇文件演示如何將解決方案中 EntityFrameworkCore.DbMigrations
項目移除,並實現使用 單個 DbContext
進行數據實體映射和數據遷移。
本篇文章項目源碼
關注 ABP Framework 最新開發進度,后面還會陸續發布新功能詳解、新功能示例等系列文章,敬請關注!
ABP Framework 研習社(QQ群:726299208)
專注 ABP Framework 學習,經驗分享、問題討論、示例源碼、電子書共享,歡迎加入!
動機
如果使用啟動模板生成解決方案,數據庫提供程序是 Entity Framework Core,那么在解決方案中會存在依賴 EF Core的兩個項目:
- .EntityFrameworkCore
- .EntityFrameworkCore.DbMigrations
.EntityFrameworkCore
項目:包含應用程序真實的 DbContext
、數據庫映射和倉儲實現。
.EntityFrameworkCore.DbMigrations
項目:包含另一個 DbContext
只用於創建和數據遷移。包含所有正在使用的模塊的數據實體映射,生成統一的數據庫表結構。
分離的原因有兩個:
- 讓真實 DbContext 保持簡單和專注。只包含當前項目相關的實體,而與在應用程序使用的模塊的實體和數據上下文無關,因為每個模塊都有自己的 DbContext ,而將模型創建方法單獨放在
EntityFrameworkCore.DbMigrations
項目中。 - 復用依賴模塊中的表,通過創建自己的類,映射到依賴模塊中的表。舉例,自定義
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
類中移到 真實 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提供了一個靈活的系統來 擴展現有的實體 ,如果你需要定義一些自定義屬性的話。
在本節中,我將展示如何向 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));
}
);
});
});
設置完成后,只要運行應用程序就可以看到用戶表上的新屬性。
新的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 實體中 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 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有關。)
- 領域層或應用層引用 Microsoft.EntityFrameworkCore Nuget包,在那個項目中引用取決於你要在哪需要使用 EF Core API。(DDD中數據提供程序無關性原則沖突)
- 在領域層創建倉儲接口,然后在
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愛好者提供學習交流家園!