上篇我们实现了认证服务和网关服务,基本我们的基础服务已经完成了,接下来我们才需要做服务的数据迁移。
这里我们需要使用EF的CodeFirst模式。通过DotnetCli的命令去操作:

dotnet ef migrations add init

修改项目

编辑我们每个服务的EfCore项目的项目文件,添加Microsoft.EntityFrameworkCore.Tools的依赖,也可以通过VS的nuget包管理器安装。只有添加了这个依赖,我们才能使用dotnet ef命令。
在项目文件中添加如下内容:

<ItemGroup>
  <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.1">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
</ItemGroup>

只添加这个依赖还不行,若直接运行dotnet ef命令的话,会提示我们需要实现一个DbContextFactory类。
所以我们在每个服务的EFCore项目中都添加一个DbContextFactory类,类结构如下,每个服务对应修改一下名字即可

using System.IO;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using Volo.Abp;


namespace FunShow.AdministrationService.EntityFrameworkCore.EntityFrameworkCore
{
    /* This class is needed for EF Core console commands
    * (like Add-Migration and Update-Database commands)
    * */
    public class AdministrationServiceDbContextFactory : IDesignTimeDbContextFactory<AdministrationServiceDbContext>
    {        
    	private readonly string _connectionString;
    
    	/* This constructor is used when you use EF Core tooling (e.g. Update-Database) */
        public AdministrationServiceDbContextFactory()
        {
        	_connectionString = GetConnectionStringFromConfiguration();
        }
        
        /* This constructor is used by DbMigrator application */
        public AdministrationServiceDbContextFactory([NotNull] string connectionString)
        {
            Check.NotNullOrWhiteSpace(connectionString, nameof(connectionString));
            _connectionString = connectionString;
        }
        public AdministrationServiceDbContext CreateDbContext(string[] args)
        {
        	AdministrationServiceEfCoreEntityExtensionMappings.Configure();
        
        var builder = new DbContextOptionsBuilder<AdministrationServiceDbContext>()
            .UseNpgsql(_connectionString, b =>
            {
            	b.MigrationsHistoryTable("__AdministrationService_Migrations");
            });
        
        	return new AdministrationServiceDbContext(builder.Options);
        }
        
        private static string GetConnectionStringFromConfiguration()
        {
            return BuildConfiguration()
            	.GetConnectionString(AdministrationServiceDbProperties.ConnectionStringName);
        }
        
        private static IConfigurationRoot BuildConfiguration()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(
                Path.Combine(
                Directory.GetCurrentDirectory(),
                $"..{Path.DirectorySeparatorChar}FunShow.AdministrationService.HttpApi.Host"
                )
                )
                .AddJsonFile("appsettings.json", optional: false);
        
            return builder.Build();
        }
    }

}

然后我们就可以执行dotnet ef migrations add init生成数据迁移文件了。

实现DbMigrator迁移程序

使用DbMigrator迁移程序可以一次性执行多个服务的迁移任务,当然我们也可以每个服务单独去执行dotnet ef database update这个命令,如果不嫌麻烦的话。
同时DbMigrator程序可以添加一些初始化数据的DataSeeder。
在前面我们DbMigrator只是创建了个项目,并没有实现功能,接下来我们就需要实现DbMigrator了。
第一步当然是修改项目文件添加我们的项目依赖,我们需要添加每个服务的EntityFrameworkCore和Application.Contracts项目,以及Shared.Hosting项目,当然最重要是需要Microsoft.EntityFrameworkCore.Tools,不然无法执行迁移命令。完整项目文件内容如下:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.1" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\FunShow.Shared.Hosting\FunShow.Shared.Hosting.csproj" />
    <ProjectReference Include="..\..\services\administration\src\FunShow.AdministrationService.Application.Contracts\FunShow.AdministrationService.Application.Contracts.csproj" />
    <ProjectReference Include="..\..\services\administration\src\FunShow.AdministrationService.EntityFrameworkCore\FunShow.AdministrationService.EntityFrameworkCore.csproj" />
    <ProjectReference Include="..\..\services\identity\src\FunShow.IdentityService.EntityFrameworkCore\FunShow.IdentityService.EntityFrameworkCore.csproj" />
    <ProjectReference Include="..\..\services\identity\src\FunShow.IdentityService.Application.Contracts\FunShow.IdentityService.Application.Contracts.csproj" />
    <ProjectReference Include="..\..\services\logging\src\FunShow.LoggingService.EntityFrameworkCore\FunShow.LoggingService.EntityFrameworkCore.csproj" />
    <ProjectReference Include="..\..\services\logging\src\FunShow.LoggingService.Application.Contracts\FunShow.LoggingService.Application.Contracts.csproj" />

  </ItemGroup>
  <ItemGroup>
    <None Remove="appsettings.json" />
    <Content Include="appsettings.json">
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <None Remove="appsettings.secrets.json" />
    <Content Include="appsettings.secrets.json">
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>
</Project>

根据ABP的Console模板,我们需要添加一个HostedService文件和Module文件,当然也可以直接新建一个ABP的console模板来操作。

在module文件中添加DepensOn依赖

[DependsOn(
    typeof(FunShowSharedHostingModule),
    typeof(IdentityServiceEntityFrameworkCoreModule),
    typeof(IdentityServiceApplicationContractsModule),
    typeof(LoggingServiceEntityFrameworkCoreModule),
    typeof(LoggingServiceApplicationContractsModule),
    typeof(AdministrationServiceEntityFrameworkCoreModule),
    typeof(AdministrationServiceApplicationContractsModule)
)]
public class FunShowDbMigratorModule : AbpModule
{

}

实现DbMigrationService

DbMigrationService负责执行我们的数据迁移文件以及初始化种子数据。完整的内容如下:

using FunShow.AdministrationService.EntityFrameworkCore;
using FunShow.IdentityService;
using FunShow.IdentityService.EntityFrameworkCore;
using FunShow.LoggingService.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.TenantManagement;
using Volo.Abp.Uow;

namespace FunShow.DbMigrator;

public class FunShowDbMigrationService : ITransientDependency
{
    private readonly ILogger<FunShowDbMigrationService> _logger;
    private readonly ITenantRepository _tenantRepository;
    private readonly IDataSeeder _dataSeeder;
    private readonly ICurrentTenant _currentTenant;
    private readonly IUnitOfWorkManager _unitOfWorkManager;

    public FunShowDbMigrationService(
        ILogger<FunShowDbMigrationService> logger,
        ITenantRepository tenantRepository,
        IDataSeeder dataSeeder,
        ICurrentTenant currentTenant,
        IUnitOfWorkManager unitOfWorkManager)
    {
        _logger = logger;
        _tenantRepository = tenantRepository;
        _dataSeeder = dataSeeder;
        _currentTenant = currentTenant;
        _unitOfWorkManager = unitOfWorkManager;
    }

    public async Task MigrateAsync(CancellationToken cancellationToken)
    {
        await MigrateHostAsync(cancellationToken);
        await MigrateTenantsAsync(cancellationToken);
        _logger.LogInformation("Migration completed!");
    }

    private async Task MigrateHostAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Migrating Host side...");
        await MigrateAllDatabasesAsync(null, cancellationToken);
        await SeedDataAsync();
    }

    private async Task MigrateTenantsAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Migrating tenants...");

        var tenants =
            await _tenantRepository.GetListAsync(includeDetails: true, cancellationToken: cancellationToken);
        var migratedDatabaseSchemas = new HashSet<string>();
        foreach (var tenant in tenants)
        {
            using (_currentTenant.Change(tenant.Id))
            {
                // Database schema migration
                var connectionString = tenant.FindDefaultConnectionString();
                if (!connectionString.IsNullOrWhiteSpace() && //tenant has a separate database
                    !migratedDatabaseSchemas.Contains(connectionString)) //the database was not migrated yet
                {
                    _logger.LogInformation($"Migrating tenant database: {tenant.Name} ({tenant.Id})");
                    await MigrateAllDatabasesAsync(tenant.Id, cancellationToken);
                    migratedDatabaseSchemas.AddIfNotContains(connectionString);
                }

                //Seed data
                _logger.LogInformation($"Seeding tenant data: {tenant.Name} ({tenant.Id})");
                await SeedDataAsync();
            }
        }
    }

    private async Task MigrateAllDatabasesAsync(
        Guid? tenantId,
        CancellationToken cancellationToken)
    {
        using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
        {
            await MigrateDatabaseAsync<AdministrationServiceDbContext>(cancellationToken);
            await MigrateDatabaseAsync<IdentityServiceDbContext>(cancellationToken);
            await MigrateDatabaseAsync<LoggingServiceDbContext>(cancellationToken);

            await uow.CompleteAsync(cancellationToken);
        }

        _logger.LogInformation(
            $"All databases have been successfully migrated ({(tenantId.HasValue ? $"tenantId: {tenantId}" : "HOST")}).");
    }

    private async Task MigrateDatabaseAsync<TDbContext>(
        CancellationToken cancellationToken)
        where TDbContext : DbContext, IEfCoreDbContext
    {
        _logger.LogInformation($"Migrating {typeof(TDbContext).Name.RemovePostFix("DbContext")} database...");

        var dbContext = await _unitOfWorkManager.Current.ServiceProvider
            .GetRequiredService<IDbContextProvider<TDbContext>>()
            .GetDbContextAsync();

        await dbContext
            .Database
            .MigrateAsync(cancellationToken);
    }

    private async Task SeedDataAsync()
    {
        await _dataSeeder.SeedAsync(
            new DataSeedContext(_currentTenant.Id)
                .WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName,
                    IdentityServiceDbProperties.DefaultAdminEmailAddress)
                .WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName,
                    IdentityServiceDbProperties.DefaultAdminPassword)
        );
    }
}

MigrateDatabaseAsync方法负责执行我们之前生成的迁移文件,SeedDataAsync则负责执行初始化我们项目中的种子数据。
后续添加更多的服务,我们只需要在MigrateAllDatabasesAsync中添加我们服务对应的DBContext文件即可。

初始化种子数据

上面说了DbMigrationService可以负责执行初始化种子数据。
根据我们需要添加一个DataSeedContributor和DataSeeder类。
这里我们初始化一下OpenIddict的种子数据。

using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;

namespace FunShow.DbMigrator;

public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDependency
{
    private readonly OpenIddictDataSeeder _openIddictDataSeeder;

    public OpenIddictDataSeedContributor(OpenIddictDataSeeder openIddictDataSeeder)
    {
        _openIddictDataSeeder = openIddictDataSeeder;
    }

    public async Task SeedAsync(DataSeedContext context)
    {
        await _openIddictDataSeeder.SeedAsync();
    }
}

主要是继承并实现IDataSeedContributor接口,这个接口会在DbMigrationService中获取并执行SeedAsync方法。
OpenIddictDataSeeder执行的初始化数据太多,这里就不贴代码了。主要就是读取配置文件的Applications和Resources初始化写进数据库。

编辑appsettings.json文件

在配置文件中添加数据库连接字符串和OpenIddict配置

{
  "ConnectionStrings": {
    "AdministrationService": "Host=localhost;Port=5432;User ID=postgres;password=myPassw0rd;Pooling=true;Database=FunShow_Administration;",
    "IdentityService": "Host=localhost;Port=5432;User ID=postgres;password=myPassw0rd;Pooling=true;Database=FunShow_Identity;",
    "LoggingService": "Host=localhost;Port=5432;User ID=postgres;password=myPassw0rd;Pooling=true;Database=FunShow_LoggingService;"
  },
  "OpenIddict": {
    "Applications": {
      "FunShow_Vue": {
        "RootUrl": "http://localhost:4200"
      },
      "WebGateway": {
        "RootUrl": "https://localhost:44325"
      }
    },
    "Resources": {
      "AccountService": {
        "RootUrl": "https://localhost:44322"
      },
      "IdentityService": {
        "RootUrl": "https://localhost:44388"
      },
      "AdministrationService": {
        "RootUrl": "https://localhost:44367"
      },
      "LoggingService": {
        "RootUrl": "https://localhost:45124"
      }
    }
  }
}

到这我们的DbMigrator迁移程序也实现完了,后续添加新服务也只需要添加修改对应的地方,然后执行程序即可。
执行之后我们会生成3个数据库,里面也包含我们的种子数据。
ABP微服务系列学习-搭建自己的微服务结构(四)-小白菜博客
到这我们基本完成了微服务的搭建。