发布时间:2025-12-09 11:47:21 浏览次数:1
上篇我们实现了认证服务和网关服务,基本我们的基础服务已经完成了,接下来我们才需要做服务的数据迁移。
这里我们需要使用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迁移程序可以一次性执行多个服务的迁移任务,当然我们也可以每个服务单独去执行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模板来操作。
[DependsOn( typeof(FunShowSharedHostingModule), typeof(IdentityServiceEntityFrameworkCoreModule), typeof(IdentityServiceApplicationContractsModule), typeof(LoggingServiceEntityFrameworkCoreModule), typeof(LoggingServiceApplicationContractsModule), typeof(AdministrationServiceEntityFrameworkCoreModule), typeof(AdministrationServiceApplicationContractsModule))]public class FunShowDbMigratorModule : AbpModule{}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初始化写进数据库。
在配置文件中添加数据库连接字符串和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个数据库,里面也包含我们的种子数据。
到这我们基本完成了微服务的搭建。