【ABP框架实践】从零构建微服务解决方案 - 共享项目

更新于 2026-06-30。本文已按当前 ABP 官方文档重新核对,并保留了 2023 年这套“手工搭建共享项目”的讲解路线。

需要注意的是,当前 ABP 更常见的起步方式已经是使用官方模板或 ABP Studio 直接生成解决方案;如果你是首次接触 ABP 微服务,建议同时参考最新官方文档中的模板说明。本文更适合用于理解共享项目的职责划分和底层配置方式。

本文中的代码片段现已同步到 net10.0 和 ABP 10.5.0 基线,并已在配套示例代码中通过构建验证。

在上一篇文章【ABP框架实践】从零构建微服务解决方案 - 准备工作中,我们已经搭好了运行微服务解决方案所需的基础环境。本文会继续创建一组共享项目,用于集中存放公共配置和模块。

共享项目,顾名思义,就是供其他应用程序、服务和微服务复用的模块与配置。这样既可以减少重复代码,也能集中维护基础配置。即使在当前版本的 ABP 中,使用共享模块来收敛基础设施配置、本地化资源和网关通用逻辑,依然是手工搭建微服务方案时一种合理的组织方式。

这里继续沿用 eShopOnAbp 当年的思路,创建五个共享项目:

  • MyCompanyName.MyProjectName.Shared.Hosting
  • MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore
  • MyCompanyName.MyProjectName.Shared.Hosting.Gateways
  • MyCompanyName.MyProjectName.Shared.Hosting.Microservices
  • MyCompanyName.MyProjectName.Shared.Localization

不用在意 MyCompanyName.MyProjectName 这样的名称,后面会将其替换为自己的项目名称。

创建解决方案和项目

首先进入上一篇文章创建的目录,然后打开命令行窗口。

本文中的命令行窗口均为 PowerShell。如果使用的是 cmd,请自行替换命令。
可以右击选择“在终端中打开”,或者使用 Shift + 右键 选择“在此处打开 PowerShell 窗口”。

这里使用 dotnet new 命令创建解决方案和项目。先创建一个解决方案,再创建五个共享项目的类库。

1
2
3
4
5
6
dotnet new solution -n MyCompanyName.MyProjectName
dotnet new classlib -n MyCompanyName.MyProjectName.Shared.Hosting -o shared/MyCompanyName.MyProjectName.Shared.Hosting --no-restore
dotnet new classlib -n MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore -o shared/MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore --no-restore
dotnet new classlib -n MyCompanyName.MyProjectName.Shared.Hosting.Gateways -o shared/MyCompanyName.MyProjectName.Shared.Hosting.Gateways --no-restore
dotnet new classlib -n MyCompanyName.MyProjectName.Shared.Hosting.Microservices -o shared/MyCompanyName.MyProjectName.Shared.Hosting.Microservices --no-restore
dotnet new classlib -n MyCompanyName.MyProjectName.Shared.Localization -o shared/MyCompanyName.MyProjectName.Shared.Localization --no-restore

-n 参数用于指定项目名称。
-o 参数用于指定项目输出路径。
--no-restore 参数用于避免在创建项目时自动还原依赖项,因为此时还没有添加任何依赖项。

删除默认生成的Class1.cs文件。

1
2
3
4
5
rm shared/MyCompanyName.MyProjectName.Shared.Hosting/Class1.cs
rm shared/MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore/Class1.cs
rm shared/MyCompanyName.MyProjectName.Shared.Hosting.Gateways/Class1.cs
rm shared/MyCompanyName.MyProjectName.Shared.Hosting.Microservices/Class1.cs
rm shared/MyCompanyName.MyProjectName.Shared.Localization/Class1.cs

然后使用 dotnet sln 命令将这些共享项目添加到解决方案。

1
2
3
4
5
dotnet sln MyCompanyName.MyProjectName.sln add shared/MyCompanyName.MyProjectName.Shared.Hosting/MyCompanyName.MyProjectName.Shared.Hosting.csproj -s shared
dotnet sln MyCompanyName.MyProjectName.sln add shared/MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore/MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore.csproj -s shared
dotnet sln MyCompanyName.MyProjectName.sln add shared/MyCompanyName.MyProjectName.Shared.Hosting.Gateways/MyCompanyName.MyProjectName.Shared.Hosting.Gateways.csproj -s shared
dotnet sln MyCompanyName.MyProjectName.sln add shared/MyCompanyName.MyProjectName.Shared.Hosting.Microservices/MyCompanyName.MyProjectName.Shared.Hosting.Microservices.csproj -s shared
dotnet sln MyCompanyName.MyProjectName.sln add shared/MyCompanyName.MyProjectName.Shared.Localization/MyCompanyName.MyProjectName.Shared.Localization.csproj -s shared

-s 参数用于指定项目所属的解决方案文件夹。

在解决方案所在目录创建 common.props 文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>1.0.0</Version>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<AbpProjectType>ms</AbpProjectType>
</PropertyGroup>

<Target Name="NoWarnOnRazorViewImportedTypeConflicts" BeforeTargets="RazorCoreCompile">
<PropertyGroup>
<NoWarn>$(NoWarn);0436</NoWarn>
</PropertyGroup>
</Target>

<ItemGroup>
<Content Remove="$(UserProfile)\.nuget\packages\*\*\contentFiles\any\*\*.abppkg*.json" />
</ItemGroup>

</Project>

接着让之前创建的五个项目引用 common.props 文件。依次打开各项目的 .csproj 文件,在 <Project> 节点下添加以下内容即可:

1
<Import Project="..\..\common.props" />

common.props

Localization

Shared.Localization 项目包含虚拟文件系统和整个解决方案范围内的本地化配置。它也是公共 Web 应用程序使用的本地化资源。如果计划开发单体 UI 作为公共 Web 应用程序,也会用到这里的本地化资源。

根据当前 ABP 官方文档,本地化资源使用 JSON 文件并通过虚拟文件系统嵌入程序集的写法仍然有效,因此这一部分的整体结构可以保留。

首先添加项目依赖。进入 Shared.Localization 项目所在目录,执行下面的命令:

1
2
dotnet add package Microsoft.Extensions.FileProviders.Embedded
dotnet add package Volo.Abp.Validation

上面的命令不显式指定版本时,会按当前 SDK 与 NuGet 源解析最新稳定版本。本文配套源码当前已经验证通过的组合是 .NET 10.0 + ABP 10.5.0

创建本地化资源类 Localization/MyProjectNameResource.cs,并添加以下内容:

1
2
3
4
5
6
7
8
9
using Volo.Abp.Localization;

namespace MyCompanyName.MyProjectName.Localization;

[LocalizationResourceName("MyProjectName")]
public class MyProjectNameResource
{

}

在项目中添加本地化文件,如下所示:

  • Localization/MyProjectName/en.json
  • Localization/MyProjectName/zh-Hans.json
1
2
3
4
5
6
7
{
"culture": "en",
"texts": {
"Menu:Home": "Home",
"Login": "Login"
}
}
1
2
3
4
5
6
7
{
"culture": "zh-Hans",
"texts": {
"Menu:Home": "首页",
"Login": "登录"
}
}

将本地化资源文件作为嵌入式资源添加到项目中,在 <Project> 节点里添加下面内容:

1
2
3
4
<ItemGroup>
<EmbeddedResource Include="Localization\MyProjectName\*.json" />
<Content Remove="Localization\MyProjectName\*.json" />
</ItemGroup>

在项目根目录创建 MyProjectNameSharedLocalizationModule.cs 文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using MyCompanyName.MyProjectName.Localization;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.Validation;
using Volo.Abp.Validation.Localization;
using Volo.Abp.VirtualFileSystem;

namespace MyCompanyName.MyProjectName;

[DependsOn(
typeof(AbpValidationModule)
)]
public class MyProjectNameSharedLocalizationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyProjectNameSharedLocalizationModule>();
});

Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<MyProjectNameResource>("en")
.AddBaseTypes(
typeof(AbpValidationResource)
).AddVirtualJson("/Localization/MyProjectName");

options.DefaultResourceType = typeof(MyProjectNameResource);
});
}
}

Hosting

Shared.Hosting 项目包含 SharedHostingModule 服务配置中的数据库配置,这些配置会被其他模块复用。

首先添加项目依赖。进入 Shared.Hosting 项目所在目录,执行下面的命令:

1
2
3
4
5
6
dotnet add package Serilog.Extensions.Logging
dotnet add package Serilog.Sinks.Async
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Console
dotnet add package Volo.Abp.Autofac
dotnet add package Volo.Abp.Data

在 ABP 中,每个需要数据库连接的模块都可以拥有自己的连接字符串,方便在需要时迁移到不同数据库。这一能力通过连接字符串管理实现。由于 AdministrationService、IdentityService、SaasService 等基础设施微服务会使用一个或多个模块,因此需要显式配置各模块对应的数据库连接,确保模块能够连接到自己的数据库。

为了避免在每个微服务中重复添加所有模块连接字符串,可以把相关连接字符串映射到同一个连接字符串上。

这里有一个和版本强相关的点需要特别说明:MappedConnections 里到底要添加哪些模块,并没有一份“永远固定”的标准答案,而是取决于你当前解决方案实际启用了哪些 ABP 模块。下面这份映射来自本文配套示例代码,在当前分支中依然成立,但如果你的解决方案额外启用了 Language Management、Text Template Management 等模块,就需要按实际情况继续补充。

因此,在项目根目录创建 MyProjectNameSharedHostingModule.cs 文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using Volo.Abp.Autofac;
using Volo.Abp.Data;
using Volo.Abp.Modularity;

namespace MyCompanyName.MyProjectName.Shared.Hosting;

[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpDataModule)
)]
public class MyProjectNameSharedHostingModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
ConfigureDatabaseConnections();
}

private void ConfigureDatabaseConnections()
{
Configure<AbpDbConnectionOptions>(options =>
{
options.Databases.Configure("AdministrationService", database =>
{
database.MappedConnections.Add("AbpAuditLogging");
database.MappedConnections.Add("AbpPermissionManagement");
database.MappedConnections.Add("AbpSettingManagement");
database.MappedConnections.Add("AbpFeatureManagement");
database.MappedConnections.Add("AbpBlobStoring");
});

options.Databases.Configure("IdentityService", database =>
{
database.MappedConnections.Add("AbpIdentity");
database.MappedConnections.Add("OpenIddict");
});
});
}
}

Hosting AspNetCore

Shared.Hosting.AspNetCore 项目是 AuthServer、Web、PublicWeb 等应用的基础依赖,需要依赖下列模块:

  • SharedHostingModule
  • AbpAspNetCoreSerilogModule
  • AbpSwashbuckleModule

进入 Shared.Hosting.AspNetCore 项目所在目录,执行下面的命令:

1
2
3
4
5
6
dotnet add package Microsoft.OpenApi
dotnet add package Serilog.AspNetCore
dotnet add package Volo.Abp.AspNetCore.Serilog
dotnet add package Volo.Abp.Swashbuckle

dotnet add reference ../MyCompanyName.MyProjectName.Shared.Hosting/MyCompanyName.MyProjectName.Shared.Hosting.csproj

在项目根目录创建 MyProjectNameSharedHostingAspNetCoreModule.cs 文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.Modularity;
using Volo.Abp.Swashbuckle;

namespace MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore;

[DependsOn(
typeof(MyProjectNameSharedHostingModule),
typeof(AbpSwashbuckleModule),
typeof(AbpAspNetCoreSerilogModule)
)]
public class MyProjectNameSharedHostingAspNetCoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
}
}

创建 SerilogConfigurationHelper.cs,添加 Serilog 的基础配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using Serilog;
using Serilog.Events;

namespace MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore;

public static class SerilogConfigurationHelper
{
public static void Configure(string applicationName)
{
// TODO: Uncomment following lines for ElasticSearch configuration
// var configuration = new ConfigurationBuilder()
// .SetBasePath(Directory.GetCurrentDirectory())
// .AddJsonFile("appsettings.json")
// .AddEnvironmentVariables()
// .Build();

Log.Logger = new LoggerConfiguration()
#if DEBUG
.MinimumLevel.Debug()
#else
.MinimumLevel.Information()
#endif
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", $"{applicationName}")
.WriteTo.Async(c => c.File("Logs/logs.txt"))
// TODO: Uncomment following lines for ElasticSearch configuration
// .WriteTo.Elasticsearch(
// new ElasticsearchSinkOptions(new Uri(configuration["ElasticSearch:Url"]))
// {
// AutoRegisterTemplate = true,
// AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6,
// IndexFormat = "MyProjectName-log-{0:yyyy.MM}"
// })
.WriteTo.Async(c => c.Console())
.CreateLogger();
}
}

创建 SwaggerConfigurationHelper.cs,添加 Swagger 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;
using Volo.Abp.Modularity;

namespace MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore;

public static class SwaggerConfigurationHelper
{
public static void Configure(
ServiceConfigurationContext context,
string apiTitle
)
{
context.Services.AddAbpSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = apiTitle, Version = "v1" });
options.DocInclusionPredicate((docName, description) => true);
options.CustomSchemaIds(type => type.FullName);
});
}

public static void ConfigureWithAuth(
ServiceConfigurationContext context,
string authority,
Dictionary<string, string> scopes,
string apiTitle,
string apiVersion = "v1",
string apiName = "v1"
)
{
context.Services.AddAbpSwaggerGenWithOAuth(
authority: authority,
scopes: scopes,
options =>
{
options.SwaggerDoc(apiName, new OpenApiInfo { Title = apiTitle, Version = apiVersion });
options.DocInclusionPredicate((docName, description) => true);
options.CustomSchemaIds(type => type.FullName);
});
}
}

当前 ABP 官方文档中,AddAbpSwaggerGenWithOAuth(...) 这套集成方式仍然可用,因此这里保留原写法。需要注意的是,较新的部署文档里也会提到基于 OIDC 的 Swagger 集成方案,尤其是在容器化或反向代理场景下更常见;如果你使用的是更新模板,请以当下 ABP 版本的官方示例为准。

另外,如果你从较早版本升级上来,要注意 OpenApiInfo 在当前 Microsoft.OpenApi 包里位于 Microsoft.OpenApi 命名空间,而不是以前常见的 Microsoft.OpenApi.Models

创建 ApplicationBuilderHelper.cs,用于简化 WebApplication 的创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Volo.Abp.Modularity;

namespace MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore;

public static class ApplicationBuilderHelper
{
public static async Task<WebApplication> BuildApplicationAsync<TStartupModule>(string[] args)
where TStartupModule : IAbpModule
{
var builder = WebApplication.CreateBuilder(args);
builder.Host
.AddAppSettingsSecretsJson()
.UseAutofac()
.UseSerilog();

await builder.AddApplicationAsync<TStartupModule>();
return builder.Build();
}
}

这里的 AddAppSettingsSecretsJson() 不是 .NET 自带的 API,而是 ABP 提供的主机配置扩展方法,用来按 ABP 的约定补充额外配置源。保留它的好处是能与后续章节里的配置组织方式保持一致;如果你完全按自己的宿主配置体系来组织配置文件,也可以改成显式调用标准的 ConfigurationBuilder 配置方法。

创建 MyProjectNameBrandingProvider.cs,替换默认的 BrandingProvider

1
2
3
4
5
6
7
8
9
10
using Volo.Abp.DependencyInjection;
using Volo.Abp.Ui.Branding;

namespace MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore;

[Dependency(ReplaceServices = true)]
public class MyProjectNameBrandingProvider : DefaultBrandingProvider
{
public override string AppName => "MyProjectName";
}

创建 MyProjectNameConstants.cs,添加匿名用户的 ClaimName。

1
2
3
4
5
6
namespace MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore;

public static class MyProjectNameConstants
{
public const string AnonymousUserClaimName = "anonymous_id";
}

Hosting Gateways

Shared.Hosting.Gateways 是 WebGateway、PublicWebGateway 等网关的基础依赖。该模块依赖于:

  • SharedHostingAspNetCoreModule 用于Serilog配置
  • Yarp.ReverseProxy 用于反向代理

上面第二条是早期模板里常见的职责划分,但在本文当前这份示例代码里,Shared.Hosting.Gateways 本身并没有直接依赖 AbpAspNetCoreMvcUiMultiTenancyModule。如果你的网关后续需要处理租户解析、头转发或多租户 UI 相关能力,再按实际模块依赖补充即可。

进入 Shared.Hosting.Gateways 项目所在目录,执行下面的命令:

1
2
3
dotnet add package Yarp.ReverseProxy

dotnet add reference ../MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore/MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore.csproj

在项目根目录创建 MyProjectNameSharedHostingGatewaysModule.cs 文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using Microsoft.Extensions.DependencyInjection;
using MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore;
using Volo.Abp.Modularity;
using Volo.Abp.Swashbuckle;

namespace MyCompanyName.MyProjectName.Shared.Hosting.Gateways;

[DependsOn(
typeof(MyProjectNameSharedHostingAspNetCoreModule),
typeof(AbpSwashbuckleModule)
)]
public class MyProjectNameSharedHostingGatewaysModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();

context.Services.AddReverseProxy()
.LoadFromConfig(configuration.GetSection("ReverseProxy"));
}
}

创建 YarpSwaggerUIBuilderExtensions.cs,添加 Swagger UI 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp;
using Yarp.ReverseProxy.Configuration;

namespace MyCompanyName.MyProjectName.Shared.Hosting.Gateways;

public static class YarpSwaggerUIBuilderExtensions
{
public static IApplicationBuilder UseSwaggerUIWithYarp(this IApplicationBuilder app,
ApplicationInitializationContext context)
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>();
var logger = context.ServiceProvider.GetRequiredService<ILogger<ApplicationInitializationContext>>();
var proxyConfigProvider = context.ServiceProvider.GetRequiredService<IProxyConfigProvider>();
var yarpConfig = proxyConfigProvider.GetConfig();

var routedClusters = yarpConfig.Clusters
.SelectMany(t => t.Destinations ?? new Dictionary<string, DestinationConfig>(),
(clusterId, destination) => new { clusterId.ClusterId, destination.Value });

var groupedClusters = routedClusters
.GroupBy(q => q.Value.Address)
.Select(t => t.First())
.Distinct()
.ToList();

foreach (var clusterGroup in groupedClusters)
{
var routeConfig = yarpConfig.Routes.FirstOrDefault(q =>
q.ClusterId == clusterGroup.ClusterId);
if (routeConfig?.RouteId is null)
{
logger.LogWarning($"Swagger UI: Couldn't find route configuration for {clusterGroup.ClusterId}...");
continue;
}

options.SwaggerEndpoint($"{clusterGroup.Value.Address}/swagger/v1/swagger.json", $"{routeConfig.RouteId} API");
options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]);
options.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);
}
});

return app;
}
}

创建 GatewayHostBuilderExtensions.cs,用于向 IHostBuilder 添加 Yarp 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.Hosting;

public static class AbpHostingHostBuilderExtensions
{
public const string AppYarpJsonPath = "yarp.json";

public static IHostBuilder AddYarpJson(
this IHostBuilder hostBuilder,
bool optional = true,
bool reloadOnChange = true,
string path = AppYarpJsonPath)
{
return hostBuilder.ConfigureAppConfiguration((_, builder) =>
{
builder.AddJsonFile(
path: AppYarpJsonPath,
optional: optional,
reloadOnChange: reloadOnChange
)
.AddEnvironmentVariables();
});
}
}

Hosting Microservices

Shared.Hosting.Microservices 项目是 AdministrationServiceIdentityServiceSaasService 等微服务的基础托管依赖。该模块依赖于 SharedHostingModule,并包含以下内容:

  • AbpDistributedCacheOptions、AbpMultiTenancyCacheOptions和Redis的配置
  • 具有基本的 JwtBearer 身份验证配置的 JwtBearerConfigurationHelper
  • DbMigrations文件夹包含用于自动迁移的PendingMigrationsCheckerBase和DatabaseMigrationEventHandlerBase(本文暂不涉及)

后续还会依赖 AdministrationServiceSaasService 的 EntityFrameworkCore 层,用于租户和语言的自动迁移。

这一部分和当前 ABP 版本的差异主要不在于 API 是否失效,而在于具体选用哪些基础设施组件。比如当前模板里是否使用 RabbitMQ、Redis、MongoDB、分布式锁,仍然取决于你的部署方案和模块组合;本文保持配套示例中的选型不变,重点是说明如何把这些横切配置集中放进共享宿主模块。

首先进入 Shared.Hosting.Microservices 项目所在目录,执行下面的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
dotnet add package Microsoft.AspNetCore.DataProtection.StackExchangeRedis
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package DistributedLock.Redis

dotnet add package Volo.Abp.AspNetCore.MultiTenancy
dotnet add package Volo.Abp.EventBus.RabbitMQ
dotnet add package Volo.Abp.BackgroundJobs.RabbitMQ
dotnet add package Volo.Abp.Caching.StackExchangeRedis
dotnet add package Volo.Abp.MongoDB
dotnet add package Volo.Abp.DistributedLocking
dotnet add package Volo.Abp.EntityFrameworkCore

dotnet add reference ../MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore/MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore.csproj

在项目根目录创建 MyProjectNameSharedHostingMicroservicesModule.cs 文件,并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System;
using Medallion.Threading;
using Medallion.Threading.Redis;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using MyCompanyName.MyProjectName.Shared.Hosting.AspNetCore;
using StackExchange.Redis;
using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.BackgroundJobs.RabbitMQ;
using Volo.Abp.Caching;
using Volo.Abp.Caching.StackExchangeRedis;
using Volo.Abp.DistributedLocking;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EventBus.RabbitMq;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;

namespace MyCompanyName.MyProjectName.Shared.Hosting.Microservices;

[DependsOn(
typeof(MyProjectNameSharedHostingAspNetCoreModule),
typeof(AbpBackgroundJobsRabbitMqModule),
typeof(AbpAspNetCoreMultiTenancyModule),
typeof(AbpDistributedLockingModule),
typeof(AbpEventBusRabbitMqModule),
typeof(AbpCachingStackExchangeRedisModule),
typeof(AbpEntityFrameworkCoreModule)
)]
public class MyProjectNameSharedHostingMicroservicesModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
var hostingEnvironment = context.Services.GetHostingEnvironment();

Configure<AbpMultiTenancyOptions>(options =>
{
options.IsEnabled = true;
});

Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "MyProjectName:";
});

var redisConfiguration = configuration["Redis:Configuration"]
?? throw new InvalidOperationException("Redis:Configuration is not configured.");
var redis = ConnectionMultiplexer.Connect(redisConfiguration);

context.Services
.AddDataProtection()
.SetApplicationName("MyProjectName")
.PersistKeysToStackExchangeRedis(redis, "MyProjectName-Protection-Keys");

context.Services.AddSingleton<IDistributedLockProvider>(_ =>
new RedisDistributedSynchronizationProvider(redis.GetDatabase()));
}
}

创建 JwtBearerConfigurationHelper.cs,添加 JwtBearer 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;

namespace MyCompanyName.MyProjectName.Shared.Hosting.Microservices;

public static class JwtBearerConfigurationHelper
{
public static void Configure(
ServiceConfigurationContext context,
string audience)
{
var configuration = context.Services.GetConfiguration();

context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
options.Audience = audience;
});
}
}

创建 DbMigrations/PendingMigrationsCheckerBase.cs,用于检查是否存在待迁移的数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp;
using Volo.Abp.DependencyInjection;

namespace MyCompanyName.MyProjectName.Shared.Hosting.Microservices.DbMigrations;

public abstract class PendingMigrationsCheckerBase : ITransientDependency
{
private readonly ILogger<PendingMigrationsCheckerBase> _logger;
protected PendingMigrationsCheckerBase(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<PendingMigrationsCheckerBase>();
}

public async Task TryAsync(Func<Task> task, int retryCount = 3)
{
try
{
await task();
}
catch (Exception ex)
{
retryCount--;

if (retryCount <= 0)
{
throw;
}

_logger.LogWarning($"{ex.GetType().Name} has been thrown. The operation will be tried {retryCount} times more. Exception:\n{ex.Message}");

await Task.Delay(RandomHelper.GetRandom(5000, 15000));

await TryAsync(task, retryCount);
}
}
}

创建 DbMigrations/EfCore/PendingEfCoreMigrationsChecker.cs,用于检查是否存在待迁移的 EntityFrameworkCore 数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp.DistributedLocking;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;

namespace MyCompanyName.MyProjectName.Shared.Hosting.Microservices.DbMigrations.EfCore;

public abstract class PendingEfCoreMigrationsChecker<TDbContext> : PendingMigrationsCheckerBase where TDbContext : DbContext, IEfCoreDbContext
{
protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected IServiceProvider ServiceProvider { get; }
protected ICurrentTenant CurrentTenant { get; }
protected IDistributedEventBus DistributedEventBus { get; }
protected IAbpDistributedLock DistributedLock { get; }
protected ILogger<PendingEfCoreMigrationsChecker<TDbContext>> Logger { get; }
protected string DatabaseName { get; }

protected PendingEfCoreMigrationsChecker(
ILoggerFactory loggerFactory,
IUnitOfWorkManager unitOfWorkManager,
IServiceProvider serviceProvider,
ICurrentTenant currentTenant,
IDistributedEventBus distributedEventBus,
IAbpDistributedLock abpDistributedLock,
string databaseName):base(loggerFactory)
{
UnitOfWorkManager = unitOfWorkManager;
ServiceProvider = serviceProvider;
CurrentTenant = currentTenant;
DistributedEventBus = distributedEventBus;
DistributedLock = abpDistributedLock;
DatabaseName = databaseName;
Logger = loggerFactory.CreateLogger<PendingEfCoreMigrationsChecker<TDbContext>>();
}

public virtual async Task CheckAndApplyDatabaseMigrationsAsync()
{
await TryAsync(LockAndApplyDatabaseMigrationsAsync);
}

protected virtual async Task LockAndApplyDatabaseMigrationsAsync()
{
await using (var handle = await DistributedLock.TryAcquireAsync("Migration_" + DatabaseName))
{
Logger.LogInformation($"Lock is acquired for db migration and seeding on database named: {DatabaseName}...");

if (handle is null)
{
Logger.LogInformation($"Handle is null because of the locking for : {DatabaseName}");
return;
}

using (CurrentTenant.Change(null))
{
// Create database tables if needed
using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
var dbContext = await ServiceProvider
.GetRequiredService<IDbContextProvider<TDbContext>>()
.GetDbContextAsync();

var pendingMigrations = await dbContext
.Database
.GetPendingMigrationsAsync();

if (pendingMigrations.Any())
{
await dbContext.Database.MigrateAsync();
}

await uow.CompleteAsync();
}
}

Logger.LogInformation($"Lock is released for db migration and seeding on database named: {DatabaseName}...");
}
}
}

创建 DbMigrations/MongoDb/PendingMongoDbMigrationsChecker.cs,用于检查是否存在待迁移的 MongoDB 数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
using System;
using System.Threading.Tasks;
using MyCompanyName.MyProjectName.Shared.Hosting.Microservices.DbMigrations.EfCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using Serilog;
using Volo.Abp.Data;
using Volo.Abp.DistributedLocking;
using Volo.Abp.MongoDB;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;

namespace MyCompanyName.MyProjectName.Shared.Hosting.Microservices.DbMigrations.MongoDb;

public class PendingMongoDbMigrationsChecker<TDbContext> : PendingMigrationsCheckerBase where TDbContext : AbpMongoDbContext
{
protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected IServiceProvider ServiceProvider { get; }
protected ICurrentTenant CurrentTenant { get; }
protected IDataSeeder DataSeeder { get; }
protected IAbpDistributedLock DistributedLockProvider { get; }
protected string DatabaseName { get; }
protected ILogger<PendingMongoDbMigrationsChecker<TDbContext>> Logger { get; }

protected PendingMongoDbMigrationsChecker(
ILoggerFactory loggerFactory,
IUnitOfWorkManager unitOfWorkManager,
IServiceProvider serviceProvider,
ICurrentTenant currentTenant,
IDataSeeder dataSeeder,
IAbpDistributedLock distributedLockProvider,
string databaseName) : base(loggerFactory)
{
UnitOfWorkManager = unitOfWorkManager;
ServiceProvider = serviceProvider;
CurrentTenant = currentTenant;
DataSeeder = dataSeeder;
DistributedLockProvider = distributedLockProvider;
DatabaseName = databaseName;
Logger = loggerFactory.CreateLogger<PendingMongoDbMigrationsChecker<TDbContext>>();
}

public virtual async Task CheckAndApplyDatabaseMigrationsAsync()
{
await TryAsync(async () =>
{
using (CurrentTenant.Change(null))
{
// Create database tables if needed
using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
await MigrateDatabaseSchemaAsync();

await DataSeeder.SeedAsync();

await uow.CompleteAsync();
}
}
});
}

/// <summary>
/// Apply scheme update for MongoDB Database.
/// </summary>
protected virtual async Task<bool> MigrateDatabaseSchemaAsync()
{
var result = false;
await using (var handle = await DistributedLockProvider.TryAcquireAsync("Migration_" + DatabaseName))
{
using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
Logger.LogInformation($"Lock is acquired for db migration and seeding on database named: {DatabaseName}...");

if (handle is null)
{
Logger.LogInformation($"Handle is null because of the locking for : {DatabaseName}");
return false;
}

async Task<bool> MigrateDatabaseSchemaWithDbContextAsync()
{
var dbContexts = ServiceProvider.GetServices<IAbpMongoDbContext>();
var connectionStringResolver = ServiceProvider.GetRequiredService<IConnectionStringResolver>();

foreach (var dbContext in dbContexts)
{
var connectionString =
await connectionStringResolver.ResolveAsync(
ConnectionStringNameAttribute.GetConnStringName(dbContext.GetType()));
if (connectionString.IsNullOrWhiteSpace())
{
continue;
}

var mongoUrl = new MongoUrl(connectionString);
var databaseName = mongoUrl.DatabaseName;
var client = new MongoClient(mongoUrl);

if (databaseName.IsNullOrWhiteSpace())
{
databaseName = ConnectionStringNameAttribute.GetConnStringName(dbContext.GetType());
}

(dbContext as AbpMongoDbContext)?.InitializeCollections(client.GetDatabase(databaseName));
}

return true;
}

//Migrating the host database
result = await MigrateDatabaseSchemaWithDbContextAsync();

await uow.CompleteAsync();
}

return result;
}
}
}

参考资料

文章作者: Ender
文章链接: https://www.fengyeju.net/archives/building-microservice-with-abp-framework-part-2-shared-projects
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 枫叶居