上篇我們實現了認證服務和閘道器服務,基本我們的基礎服務已經完成了,接下來我們才需要做服務的資料遷移。
這裡我們需要使用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個資料庫,裡面也包含我們的種子資料。
到這我們基本完成了微服務的搭建。