ABP Framework:移除 EF Core Migrations 项目,统一数据上下文
原文:Unifying DbContexts for EF Core / Removing the EF Core Migrations Project[1]
導(dǎo)讀:軟件開發(fā)的一切都需要平衡
在?ABP Framework V4.4 RC 新增功能介紹?中,對(duì)應(yīng)用程序啟動(dòng)解決方案模板做了一個(gè)重要改變:刪除?EntityFrameworkCore.DbMigrations?項(xiàng)目。
本文將詳細(xì)解讀背后的原因和解決方案。
1.理解動(dòng)機(jī)很重要:為什么先前的版本要將要數(shù)據(jù)上下文進(jìn)行分離,而現(xiàn)在為什么要合并?2.合并之后存在什么缺陷,以及如何解決?
這篇文件演示如何將解決方案中?EntityFrameworkCore.DbMigrations?項(xiàng)目移除,并實(shí)現(xiàn)使用?單個(gè)?DbContext?進(jìn)行數(shù)據(jù)實(shí)體映射和數(shù)據(jù)遷移。
本篇文章項(xiàng)目源碼[3]
關(guān)注?ABP Framework?最新開發(fā)進(jìn)度,后面還會(huì)陸續(xù)發(fā)布新功能詳解、新功能示例等系列文章,敬請(qǐng)關(guān)注!?ABP Framework 研習(xí)社(QQ群:726299208)?專注 ABP Framework 學(xué)習(xí),經(jīng)驗(yàn)分享、問(wèn)題討論、示例源碼、電子書共享,歡迎加入!
動(dòng)機(jī)
如果使用啟動(dòng)模板生成解決方案,數(shù)據(jù)庫(kù)提供程序是 Entity Framework Core,那么在解決方案中會(huì)存在依賴 EF Core的兩個(gè)項(xiàng)目:
?.EntityFrameworkCore?.EntityFrameworkCore.DbMigrations
.EntityFrameworkCore項(xiàng)目:包含應(yīng)用程序真實(shí)的?DbContext**、數(shù)據(jù)庫(kù)映射和倉(cāng)儲(chǔ)實(shí)現(xiàn)**。
.EntityFrameworkCore.DbMigrations項(xiàng)目:包含另一個(gè)?DbContext?只用于創(chuàng)建和數(shù)據(jù)遷移。包含所有正在使用的模塊的數(shù)據(jù)實(shí)體映射,生成統(tǒng)一的數(shù)據(jù)庫(kù)表結(jié)構(gòu)。
分離的原因有兩個(gè):
1.讓真實(shí) DbContext?保持簡(jiǎn)單和專注。只包含當(dāng)前項(xiàng)目相關(guān)的實(shí)體,而與在應(yīng)用程序使用的模塊的實(shí)體和數(shù)據(jù)上下文無(wú)關(guān),因?yàn)槊總€(gè)模塊都有自己的 DbContext ,而將模型創(chuàng)建方法單獨(dú)放在?EntityFrameworkCore.DbMigrations?項(xiàng)目中。2.復(fù)用依賴模塊中的表,通過(guò)創(chuàng)建自己的類,映射到依賴模塊中的表。舉例,自定義?AppUser?實(shí)體映射到數(shù)據(jù)庫(kù)中?AbpUsers?表,實(shí)際上該表由?Identity 模塊[4]?的?IdentityUser?實(shí)體映射生成。他們共用相同的數(shù)據(jù)庫(kù)表。和?IdentityServer?實(shí)體相比?AppUser?包含的屬性更少,可以根據(jù)需要在?AppUser?中添加所需的屬性,只需要設(shè)置好數(shù)據(jù)庫(kù)映射,新增字段會(huì)添加到映射表中。
我們?cè)敿?xì)的描述了這種結(jié)構(gòu)[5]。然而,對(duì)于開發(fā)者,仍然存在問(wèn)題,因?yàn)楫?dāng)需要復(fù)用依賴模塊中的表時(shí),這種結(jié)構(gòu)會(huì)使的數(shù)據(jù)實(shí)體映射變得復(fù)雜。
許多開發(fā)者在映射這些類時(shí)容易產(chǎn)生誤解或犯錯(cuò),特別是當(dāng)試圖使用的實(shí)體與其他實(shí)體存在關(guān)聯(lián)關(guān)系時(shí)。
所以我們?cè)?V4.4?版本中決定取消這種分離,刪除?EntityFrameworkCore.DbMigrations?項(xiàng)目。新的啟動(dòng)方案將帶只有一個(gè)?EntityFrameworkCore?項(xiàng)目和一個(gè)?DbContext?類。
如果你想在你的解決方案中加入今天的內(nèi)容,請(qǐng)遵循本文的步驟。
警告
新的設(shè)計(jì)有一個(gè)缺點(diǎn)。我們必須刪除 AppUser 實(shí)體,因?yàn)?strong>不能在同一個(gè)?DbContext?中很好地處理沒有繼承關(guān)系的兩個(gè)類映射到同一張表中。在本文的后面會(huì)介紹這個(gè)問(wèn)題,并提供處理它的建議。
如果您使用 ABP Commercial 商業(yè)版,ABP套件代碼生成功能還不會(huì)采用本文中提到的設(shè)計(jì)方法,建議等待下一個(gè)版本。
步驟
我們的目標(biāo)是刪除?EntityFrameworkCore.DbMigrations?項(xiàng)目,在?EntityFrameworkCore?項(xiàng)目中啟用數(shù)據(jù)庫(kù)遷移,替換遷移項(xiàng)目的依賴。
原解決方案是基于 v4.3 創(chuàng)建一個(gè)新的解決方案,然后在 pull request 中記錄所有的修改,所以你可以逐行看到所有的修改。雖然這篇文章將涵蓋所有的內(nèi)容,但如果你在實(shí)現(xiàn)過(guò)程中遇到問(wèn)題,你可能想檢查這個(gè)PR中所做的修改[6]。
第一步:添加 Microsoft.EntityFrameworkCore.Tools 包到 EntityFrameworkCore 項(xiàng)目
將下面代碼添加到?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>第二步:創(chuàng)建設(shè)計(jì)時(shí) DbContext 工廠
在?EntityFrameworkCore?項(xiàng)目中創(chuàng)建實(shí)現(xiàn)?IDesignTimeDbContextFactory<T>?接口的數(shù)據(jù)上下文工廠
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?項(xiàng)目中復(fù)制的,重命名并使用應(yīng)用程序的實(shí)際 DbContext?。
第三步:創(chuàng)建 數(shù)據(jù)庫(kù)模式遷移器
復(fù)制?EntityFrameworkCore...DbSchemaMigrator(省略號(hào)表示項(xiàng)目命名)類到 EntityFrameworkCore 項(xiàng)目中,修改?MigrateAsync?方法中的代碼,以使用真實(shí) 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();}} }第四步 轉(zhuǎn)移數(shù)據(jù)庫(kù)實(shí)體映射配置
在?遷移 DbContext?中包含?builder.ConfigureXXX()?對(duì)應(yīng)每個(gè)使用的模塊的數(shù)據(jù)實(shí)體映射配置。移動(dòng)這些配置到?EntityFrameworkCore?項(xiàng)目的?真實(shí) DbContext?中,并移除?AppUser?數(shù)據(jù)庫(kù)實(shí)體映射。
可以選擇將自己定義的實(shí)體數(shù)據(jù)庫(kù)映射代碼從...DbContextModelCreatingExtensions類中移到?真實(shí) DbContext?的?OnModelCreating?方法中,并刪除該靜態(tài)擴(kuò)展類。
示例解決方案中,最終 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 項(xiàng)目
從解決方案中移除?EntityFrameworkCore.DbMigrations?項(xiàng)目,將對(duì)該項(xiàng)目的引用替換為?EntityFrameworkCore?項(xiàng)目引用。
同樣地,將模塊依賴?...EntityFrameworkCoreDbMigrationsModule?替換為?...EntityFrameworkCoreModule?。
示例項(xiàng)目中,涉及的項(xiàng)目為?DbMigrator?Web?和?Web and EntityFrameworkCore.Tests?。
第六步:移除 AppUser 實(shí)體
我們需要將?AppUser?這個(gè)實(shí)體移除,因?yàn)?EF Core 不能兩個(gè)非繼承關(guān)系的類映射到單個(gè)表。所以,刪除這個(gè)類和所有的對(duì)該類的使用。如果你需要在應(yīng)用程序代碼中查詢用戶,可以用?IdentityUser?替換。更多信息請(qǐng)參見 AppUser 實(shí)體和自定義屬性部分。
第七步:創(chuàng)建數(shù)據(jù)遷移
如果需要使用數(shù)據(jù)遷移歷史記錄,可以直接將?EntityFrameworkCore.DbMigrations?項(xiàng)目中生成的?migrations?復(fù)制到?EntityFrameworkCore?項(xiàng)目,并手動(dòng)修改其中的?DbContext?類型。
如果需要在已經(jīng)應(yīng)用了數(shù)據(jù)遷移的數(shù)據(jù)庫(kù)中,繼續(xù)應(yīng)用新的數(shù)據(jù)遷移,在?EntityFrameworkCore?項(xiàng)目中,創(chuàng)建新的數(shù)據(jù)庫(kù)遷移,執(zhí)行命令:
dotnet ef migrations add InitialUnified你可以指定一個(gè)不同的遷移名稱,這將創(chuàng)建一個(gè)遷移類,其中包含你在數(shù)據(jù)庫(kù)中已有的所有數(shù)據(jù)庫(kù)表。注意,刪除?Up?和?Down?方法中的所有內(nèi)容,然后就可以將遷移應(yīng)用到數(shù)據(jù)庫(kù)中。
dotnet ef database update數(shù)據(jù)庫(kù)不會(huì)有任何變化,因?yàn)檫w移是空的,什么都不做。從現(xiàn)在開始,可以在改變實(shí)體時(shí),創(chuàng)建新的遷移,就像平時(shí)做的那樣。
DbContext 合并已經(jīng)完成。接下來(lái)將解決如何基于這種設(shè)計(jì)為依賴模塊的實(shí)體添加自定義屬性。
AppUser 實(shí)體 和自定義屬性
數(shù)據(jù)庫(kù)映射邏輯、解決方案結(jié)構(gòu)和數(shù)據(jù)遷移,變得簡(jiǎn)單和易于管理。
帶來(lái)的弊端是,我們必須移除?AppUser?實(shí)體,因?yàn)槠渑c?Identity?模塊中?IdentityUser?實(shí)體共享?AbpUsers?表。幸運(yùn)的是,ABP提供了一個(gè)靈活的系統(tǒng)來(lái)?擴(kuò)展現(xiàn)有的實(shí)體[7]?,如果你需要定義一些自定義屬性的話。
在本節(jié)中,我將展示如何向?IdentityUser?實(shí)體添加一個(gè)自定義屬性,并在你的應(yīng)用程序代碼和數(shù)據(jù)庫(kù)查詢中使用它。
我已經(jīng)把這部分的所有修改作為一個(gè)單獨(dú)的PR完成了,所以如果你在實(shí)現(xiàn)上有問(wèn)題,你可能想檢查這個(gè)PR中的修改[8]。
定義一個(gè)自定義屬性
應(yīng)用程序啟動(dòng)模板提供一個(gè)配置點(diǎn),為實(shí)體添加自定義屬性,位于 Domain.Shared 項(xiàng)目中?...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));});});});設(shè)置完成后,只要運(yùn)行應(yīng)用程序就可以看到用戶表上的新屬性。
新的SocialSecurityNumber屬性也將在創(chuàng)建和編輯模式中應(yīng)用添加的驗(yàn)證規(guī)則。
參看?模塊實(shí)體擴(kuò)展[9]?文檔,理解和使用自定義屬性。
映射到數(shù)據(jù)庫(kù)表
ABP默認(rèn)將所有自定義屬性作為一個(gè) Json 對(duì)象保存到?ExtraProperties?字段。如果要為自定義屬性創(chuàng)建表字段,可以在?EntityFrameworkCore?項(xiàng)目?...EfCoreEntityExtensionMappings.cs?中配置,在該類(OneTimeRunner.Run)中添加如下代碼:
ObjectExtensionManager.Instance.MapEfCoreProperty<IdentityUser, string>("SocialSecurityNumber",(entityBuilder, propertyBuilder) =>{propertyBuilder.HasMaxLength(64).IsRequired().HasDefaultValue("");});然后,直接在 EntityFrameworkCore 項(xiàng)目中執(zhí)行添加數(shù)據(jù)遷移命令:
dotnet ef migrations add Added_SocialSecurityNumber_To_IdentityUser將在項(xiàng)目匯總添加一個(gè)新的數(shù)據(jù)遷移類,接著可以通過(guò)運(yùn)行?.DbMigrator?應(yīng)用或如下命令應(yīng)用修改到數(shù)據(jù)庫(kù):
dotnet ef database update將會(huì)在數(shù)據(jù)庫(kù)?AbpUsers?表中添加字段 SocialSecurityNumber 。
使用自定義屬性
現(xiàn)在,可以使用 IdentityUser 實(shí)體中?GetProperty?和?SetProperty?方法操作新添加的屬性。下面示例代碼演示如何獲取和設(shè)置自定義屬性:
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?使用字符串屬性名可能會(huì)很繁瑣,而且容易出錯(cuò)。建議創(chuàng)建以下擴(kuò)展方法:
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 來(lái)實(shí)現(xiàn)?有兩種方式實(shí)現(xiàn)在應(yīng)用程序中使用EF Core API:(這與自定義屬性無(wú)關(guān),與 EF Core有關(guān)。)
1.領(lǐng)域?qū)踊驊?yīng)用層引用?Microsoft.EntityFrameworkCore[10]?Nuget包,在那個(gè)項(xiàng)目中引用取決于你要在哪需要使用 EF Core API。(DDD中數(shù)據(jù)提供程序無(wú)關(guān)性原則沖突)2.在領(lǐng)域?qū)觿?chuàng)建倉(cāng)儲(chǔ)接口,然后在?EntityFrameworkCore?項(xiàng)目中實(shí)現(xiàn)接口。
推薦使用第二種方式,在?Domain?項(xiàng)目中定義一個(gè)新的倉(cāng)儲(chǔ)接口:
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 項(xiàng)目中實(shí)現(xiàn)接口:
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();}} }提示:應(yīng)該使用一個(gè)常量代替SocialSecurityNumber魔術(shù)字符串。(不會(huì)產(chǎn)生拼寫錯(cuò)誤)
現(xiàn)在,我可以在應(yīng)用服務(wù)中依賴注入?IMyUserRepository?使用倉(cāng)儲(chǔ)接口:
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);} }使用自定義倉(cāng)儲(chǔ)接口?IMyUserRepository?代替泛型倉(cāng)儲(chǔ)接口?IRepository<IdentityUser, Guid>。
討論 Github
這篇文章演示了,如何將?EntityFrameworkCore.DbMigrations?項(xiàng)目從解決方案中移除,以簡(jiǎn)化數(shù)據(jù)庫(kù)實(shí)體映射、數(shù)據(jù)遷移和應(yīng)用程序中的代碼。
在下一個(gè)版本(4.4),將作為默認(rèn)處理。
討論: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]?項(xiàng)目源碼:?https://github.com/abpframework/abp-samples/tree/master/UnifiedEfCoreMigrations
[4]?Identity 模塊:?https://docs.abp.io/en/abp/latest/Modules/Identity
[5]?描述了這種結(jié)構(gòu):?https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Migrations
[6]?這個(gè)PR中所做的修改:?https://github.com/abpframework/abp-samples/pull/88
[7]?擴(kuò)展現(xiàn)有的實(shí)體:?https://docs.abp.io/en/abp/latest/Module-Entity-Extensions
[8]?檢查這個(gè)PR中的修改:?https://github.com/abpframework/abp-samples/pull/89
[9]?模塊實(shí)體擴(kuò)展:?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
總結(jié)
以上是生活随笔為你收集整理的ABP Framework:移除 EF Core Migrations 项目,统一数据上下文的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: NLog源码解读——StringBuil
- 下一篇: 关于c#:Filter Serilog日