ABP Vnext 4.4:统一Ef Core的DbContext/移除EF Core Migrations项目

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ABP Vnext 4.4:统一Ef Core的DbContext/移除EF Core Migrations项目相关的知识,希望对你有一定的参考价值。

Abp vnext 4.4出现了一个比较重大的变更:在Startup template中移除了EF Core Migrations项目,本文翻译自community.abp.io/articl

由于本文发布的时候Abp vnext的版本还没有到4.4,所以本文演示了如何从4.4以前的版本移除EntityFrameworkCore.DbMigrations这个项目,并且使用唯一的一个DbContext来进行数据库的映射和基于Code-First模式的迁移。

该项目的github地址如下:github.com/abpframework

动机/背景

如果你使用Ef core作为数据库provider创建一个解决方案,那么会有两个与ef core有关的项目:

EntityFrameworkCore这个项目包含了你的应用的真正的DbContext,它包含了所有的数据库映射和你的Repository的实现。

另一方面,EntityFrameworkCore.DbMigrations 项目包含了另一个DbContext用来创建和施行数据库迁移。它包含了你所使用的所有模块的数据库映射,所以你有一个单独并统一的数据库架构/方案。

当时这么做主要有两个原因:

  1. 你真正的DbContext保持了简单和集中(focused)。它只包含了你自己应用中的实体相关的内容并且不包含你所使用的关于其他模块的内容。

  2. 你可以创建自己的类,映射到依赖模块的表。例如,AppUser实体(包含在下载的解决方案中)映射到数据库中的AbpUsers表,而AbpUsers表实际上映射到Identity Module的IdentityUser实体。这意味着它们共享相同的数据库表。与IdentityServer相比,AppUser包含的属性更少。您只添加您需要的属性,而不是更多。这还允许您根据自定义需求向AppUser添加新的标准(类型安全)属性,只要您仔细地管理数据库映射。

对于这个方面的说明我们在官方的文档中有详细的说明。然而,当你重用那些你依赖的模块的表时,会存在一些问题,那就是这样的架构会导致你的数据库映射变得复杂。许多开发者在做一些诸如映射这些类/实体的工作时,会变得迷茫和犯错,特别是当他们想要将这些实体和其他实体联系起来时。

所以,我们决定在4.4的版本中取消这种分离,删除EntityFrameworkCore.DbMigrations这个项目。新版本的abp vnext中将只包含EntityFrameworkCore这个项目并且只拥有一个DbContext。

如果你今天就想尝试这么干,请接着往下看。

警告

新的设计有一个缺点(软件开发中的一切都是一种权衡)。我们需要删除AppUser实体,因为EF Core不能在没有继承关系的情况下将两个类映射到单个表。我将在本文后面介绍这一点,并提供处理它的建议。

步骤

我们的目标是在EntityFrameworkCore项目中启用数据库迁移,移除EntityFrameworkCore.DbMigrations项目并根据该包重新访问代码。

第一步:为EntityFrameworkCore添加Microsoft.EntityFrameworkCore.Tools包

在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>

第二步,创建design time DbContext factory

在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项目中的那个DbContext。

第三步,创建数据库方案迁移类

将EntityFrameworkCore...DbSchemaMigrator(...代表了你项目的名字)类复制到EntityFrameworkCore项目下,并且将其中的DbContext替换成EntityFrameworkCore项目中的那个真正的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(在迁移项目中定义的那个DbContext)通常包含你使用的每个模块的builder.ConfigureXXX()这样的代码行。我们可以将这些行移动到EntityFrameworkCore项目中的实际DbContext中。另外,删除AppUser的数据库映射(我们将删除这个实体)。或者,你可以将你自己的实体的数据库映射代码从… DbContextModelCreatingExtensions类放在实际DbContext的OnModelCreating方法中,并删除静态扩展类。

注:上文提到的AppUser数据库映射这些代码是包含在EntityFramworkCore的DbContext中,具体如下:

   /* Configure the shared tables (with included modules) here */

            builder.Entity<AppUser>(b =>
            {
                b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); //Sharing the same table "AbpUsers" with the IdentityUser
                
                b.ConfigureByConvention();
                b.ConfigureAbpUser();

                /* Configure mappings for your additional properties
                 * Also see the BlazorEfCoreEntityExtensionMappings class
                 */
            });

最终修改后的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项目。

同时,EntityFrameworkCore.DbMigrations项目的作用现在也变更为了EntityFrameworkCore项目。

在这个例子中,我需要将DbMigrator,WebEntityFrameworkCore.Tests 这三个项目的对EntityFrameworkCore.DbMigrations的引用变更为EntityframeworkCore项目。

第六步,删除AppUser实体类

你需要删除AppUser实体类,因为Abp没有办法在两个没有继承关系的类上面映射同一张表。

所以应该删除它以及和他相关的内容,如果你要查询有关用户的内容,你应该用IdentityUser来代替。可以在官方文档中查看与自定义属性和AppUser相关的内容。

第七步,创建或者移动迁移内容

现在我们已经删除了EntityFrameworkCore.DbMigrations项目。接下来我们要考虑关于数据库迁移的事情了。如果你要保持数据库的迁移历史,你需要从EntityFrameworkCore.DbMigrations项目吧Migrations目录中的内容拷贝到EntityFrameworkCore,并且将内容中的DbContext手工的替换为EntityFrameworkCore项目中定义的DbContext。

另一种做法是清除项目中的迁移历史,并在数据库中的已提交的迁移历史上继续,那你需要做的是在EntityFrameworkCore项目中创建一个数据库迁移,并在该项目的根目录下面执行下面的命令:

dotnet ef migrations add InitialUnified

你无疑需要为这个迁移命令起一个全新的名字,这个迁移肯定会生成一堆内容,你需要小心的将Up和Down这两个方法中的内容全部删除,然后就可以将这个迁移(实际上是一个空的迁移)应用到数据库了:

dotnet ef database update

这个操作不会对数据库造成任何更改,毕竟你已经将Up和Down方法里面的内容全删除了。接下来,你就可以像平常一样进行接下来的操作了。

AppUser 实体和自定义扩展属性

现在数据库映射逻辑、解决方案结构、迁移以及我们接下来要做的事情变得更简单了。

作为缺点来说,我们需要删除AppUser实体,它和Identity Module中定义的IdentityUser共享了数据库中的同一张表。幸运的是,当你需要在已存在的实体上(比如Identity module中定义的IdentityUser)增加一些自定义的属性时,Abp提供了一个相当灵活的系统。在这一节中,我将演示如何在IdentityUser上面增加一些自定义的属性,并在程序编码和数据库查询上应用这些自定义的字段。

关于这些内容我已经作为单独的pr发布到github上,你可以点击这个链接进行查看:

https://github.com/abpframework/abp-samples/pull/89 github.com

声明一个自定义的属性

启动模板中有一个关于在已存在实体上自定义属性的入口,这个入口在Domain.Share项目下面,...ModuleExtensionConfigurator.cs(...代表你项目的名称)这个文件中。打开这个文件并在ConfigureExtraProperties方法中下如下代码:

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

完事儿后,运行程序并在User table上面你可以看到这个属性:

新的SocialSecurityNumber属性也会在创建和更新Modal中显示并应用校验规则。查看如下链接了解关于扩展属性的一切信息:

https://docs.abp.io/en/abp/latest/Module-Entity-Extensions docs.abp.io

映射到数据库表

默认情况下,Abp将所有自定义的属性保存在数据库表中的ExtraProperties属性上,作为一个JSON保存 。如果你想要将自定义的字段作为单独的表字段保存,你需要在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

这会在EntityframeworkCore项目下面新增一个迁移文件,然后你要将这个迁移应用到数据库:

dotnet ef database update

你也可以运行.DbMigrator项目来应用迁移,这个项目的作用就在于此。

这会在数据库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");
    }
} 
上面的代码中我们使用了”SocialSecurityNumber“硬编码来直接调用,更好的做法是我们可以定义一些扩展方法来包装这种调用。

下面我们改进这种做法:

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来完成,基于此,我们这里给出两个解决方案:

1、引用Microsoft.EntityFrameworkCore包到你的项目中(Domain项目或者Application项目,具体看你的需求)。

2、在Domain中创建一个repository接口,并在EntityFrameworkCore项目中实现它。

我更倾向于第二个方案,所以我在repository接口中定义一些方法先:

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();
        }
    }
}
注意:使用一个常量而不是字符串硬编码来搞这样更好一些。

现在,我们可以在Service里面注入repository来使用了:)

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);
    }
}

总结

这篇文章描述了如何删除EntityFrameworkCore.DbMigrations项目来简化你的数据库映射、数据库迁移以及应用程序编码。在4.4这个版本中,我们已经在启动模板中移除了这个项目了。

源码

https://github.com/abpframework/abp-samples/tree/master/UnifiedEfCoreMigrations

以上是关于ABP Vnext 4.4:统一Ef Core的DbContext/移除EF Core Migrations项目的主要内容,如果未能解决你的问题,请参考以下文章

ef core输出sql语句(abp vnext)

Abp vNext 自定义 Ef Core 仓储引发异常

ABP Framework:移除 EF Core Migrations 项目,统一数据上下文

Abp VNext 集成sharding-core 分表分库

ABP vNext微服务架构详细教程(补充篇)——单层模板(下)

[Abp vNext 源码分析] - 19. 多租户