【ABP框架实践】从零构建微服务解决方案 - 搭建基础服务

更新于 2026-07-01。本文参考 abpframework/eShopOnAbp 的服务拆分方式,以及 abpframework/abp 当前模板中的宿主组织思路,结合本系列前两篇已经搭好的共享模块,整理出一套更适合当前 ABP 版本的基础服务搭建路线。

如果你希望先理解 ABP 微服务方案的整体骨架,再进入具体业务服务,这一篇可以作为后续章节的基础。

在上一篇文章【ABP框架实践】从零构建微服务解决方案 - 共享项目中,我们已经把共享模块搭好了:本地化、公共宿主配置、网关辅助代码,以及微服务宿主层会复用的基础能力都已经有了落点。接下来就可以开始创建微服务。

这一篇不直接进入 CatalogOrder 这类自定义业务服务,而是先搭三套基础服务:

  • AdministrationService
  • IdentityService
  • SaasService

这样安排的原因是,在一套完整的 ABP 微服务架构里,权限、设置、身份、租户这些能力不应散落在各个业务服务里,而应先有明确的服务边界和统一的宿主模型。

读完这一篇,应该可以明确三件事情:

  • 理解为什么 ABP 微服务方案通常会先拆出管理、身份、租户三类底座服务。
  • 理解一个基础服务的标准分层骨架应该如何组织。
  • 理解这些服务在宿主层如何统一接入认证、Swagger、跨域和迁移检查,为后续网关与业务服务打底。

为什么先搭基础服务

在开始做微服务拆分时,一个常见问题是直接从业务模块入手。这样虽然也能运行,但往往会把认证授权、多租户、设置管理、审计日志这些平台能力混进业务服务里,最终造成边界不清、依赖发散。

eShopOnAbp 这个示例项目虽然已经有些年份,但它有一个思路仍然值得借鉴:先把平台级能力拆成独立服务,再让业务服务围绕这些能力组织。这样做的好处是,业务服务可以更专注于领域本身,而身份、权限、租户、配置这些横切能力则有稳定的归属。

从微服务框架的角度看,这三套基础服务分别对应三类底座能力:

  • 管理底座:由 AdministrationService 承载,用来集中处理权限、设置、功能和审计等后台通用能力。
  • 身份底座:由 IdentityService 承载,用来集中处理用户、角色、组织单元和身份数据。
  • 租户底座:由 SaasService 承载,用来集中处理租户及其配置,是多租户系统的核心边界。

先把这三层搭起来,后续再去写网关、认证服务器和业务服务,整体结构会更清晰。

基础服务总览

基础服务的统一骨架

参考 eShopOnAbp 的服务组织方式,你会发现这三套基础服务虽然职责不同,但骨架几乎完全一致。一个标准的 ABP 微服务,通常至少会包含下面这些项目层次:

  • Domain.Shared
  • Domain
  • Application.Contracts
  • Application
  • EntityFrameworkCore
  • HttpApi
  • HttpApi.Host

这套分层之所以重要,不是因为“官方示例都这么写”,而是因为它正好对应了微服务里几个最容易混淆的职责边界:

  • Domain.Shared:放整个服务范围内共享的常量、错误码、本地化资源和跨层定义。
  • Domain:放领域模块依赖,以及真正属于领域层的规则和服务。
  • Application.Contracts:向外暴露 DTO、应用服务接口和远程服务契约。
  • Application:实现应用服务、对象映射和应用层编排逻辑。
  • EntityFrameworkCore:承载 DbContext、实体映射和数据库迁移。
  • HttpApi:负责把应用服务暴露成 HTTP API。
  • HttpApi.Host:负责真正启动服务,并接入认证、Swagger、跨域、日志、迁移检查等宿主层能力。

如果把这些边界在一开始就理顺,后续新增业务服务时,通常可以直接复用这套模式,只替换服务自身的领域和模块依赖即可。

基础服务分层结构

推荐的目录组织

前两篇文章里,我们已经在解决方案根目录下准备好了 shared/etc/。从这一篇开始,建议把每个微服务单独放进 services/ 目录下,再按服务拆分自己的解决方案和 src/ 目录。整体结构可以组织成这样:

1
2
3
4
5
6
services/
administration/
identity/
saas/
shared/
etc/

这种组织方式有几个明显好处:

  • 每个服务都能拥有独立的解决方案和项目分层,便于单独构建和演进。
  • 共享模块继续留在根目录统一维护,不会和某个具体服务强耦合。
  • 当后续再新增 catalogordering 这类业务服务时,目录层次依旧保持一致。

创建服务解决方案和项目

有了目录组织之后,再来看具体的创建步骤。这里依然以 AdministrationService 为例,但重点不是记住命令本身,而是理解这一套服务骨架的创建方式。

先创建目录和解决方案:

1
2
3
mkdir services/administration
cd services/administration
dotnet new solution -n MyCompanyName.MyProjectName.AdministrationService --format sln

然后按统一骨架创建七个项目:

1
2
3
4
5
6
7
dotnet new classlib -n MyCompanyName.MyProjectName.AdministrationService.Domain.Shared -o src/MyCompanyName.MyProjectName.AdministrationService.Domain.Shared --no-restore
dotnet new classlib -n MyCompanyName.MyProjectName.AdministrationService.Domain -o src/MyCompanyName.MyProjectName.AdministrationService.Domain --no-restore
dotnet new classlib -n MyCompanyName.MyProjectName.AdministrationService.Application.Contracts -o src/MyCompanyName.MyProjectName.AdministrationService.Application.Contracts --no-restore
dotnet new classlib -n MyCompanyName.MyProjectName.AdministrationService.Application -o src/MyCompanyName.MyProjectName.AdministrationService.Application --no-restore
dotnet new classlib -n MyCompanyName.MyProjectName.AdministrationService.EntityFrameworkCore -o src/MyCompanyName.MyProjectName.AdministrationService.EntityFrameworkCore --no-restore
dotnet new classlib -n MyCompanyName.MyProjectName.AdministrationService.HttpApi -o src/MyCompanyName.MyProjectName.AdministrationService.HttpApi --no-restore
dotnet new web -n MyCompanyName.MyProjectName.AdministrationService.HttpApi.Host -o src/MyCompanyName.MyProjectName.AdministrationService.HttpApi.Host --no-restore

IdentityServiceSaasService 的创建方式完全一致,只需要替换服务名前缀即可。

创建项目之后,记得做两件事情:

  • 把各项目加入服务自己的解决方案。
  • 继续引用根目录里的 common.props,保持和前两篇相同的基础构建约定。由于服务项目位于 services/<service>/src/<project>/ 这一层级,项目文件中的相对路径通常要写成 ../../../../common.props

这些步骤看起来琐碎,但会直接影响后续整套方案的一致性。越早把项目结构和构建约定统一下来,后面新增服务时越容易保持整洁。

项目引用关系

基础服务的分层不是“摆着好看”,项目引用关系才是真正体现架构边界的地方。三套服务都可以先遵循下面这组稳定关系:

  • Domain 引用 Domain.Shared
  • Application.Contracts 引用 Domain.Shared
  • Application 引用 Application.ContractsDomain
  • EntityFrameworkCore 引用 Domain
  • HttpApi 引用 Application.Contracts
  • HttpApi.Host 引用 ApplicationHttpApiEntityFrameworkCore

除此之外,三个 HttpApi.Host 还应该统一引用前两篇已经准备好的共享模块:

  • MyCompanyName.MyProjectName.Shared.Hosting.Microservices

这样做的意义在于,宿主层的公共能力只需要在共享模块里维护一次,具体服务只负责声明自己的业务边界,而不必各自重复实现日志、认证、缓存、Swagger 这些横切配置。

AdministrationService:管理底座

AdministrationService 是三套基础服务里最偏平台管理能力的一层。它的边界不在于承接某个具体业务领域,而在于集中承接平台级管理能力,例如:

  • 权限管理
  • 设置管理
  • 功能管理
  • 审计日志
  • Blob 存储相关后台能力

之所以把这些能力独立成服务,是因为它们通常会被多个业务服务和后台应用共同依赖。如果把它们分散到业务服务里,后续维护成本会明显上升;独立出来之后,它就成为整个平台的管理能力中心。

核心模块依赖

从模块边界上看,AdministrationServiceDomain 层通常围绕这些 ABP 模块组织:

1
2
3
4
5
6
7
8
9
10
[DependsOn(
typeof(AdministrationServiceDomainSharedModule),
typeof(AbpFeatureManagementDomainModule),
typeof(AbpSettingManagementDomainModule),
typeof(AbpAuditLoggingDomainModule),
typeof(AbpPermissionManagementDomainIdentityModule)
)]
public class AdministrationServiceDomainModule : AbpModule
{
}

这里最重要的不是把所有旧示例里的模块全抄过来,而是理解它的设计意图:这个服务负责承接“平台管理能力”的聚合,因此它的依赖应该围绕管理型模块展开。具体依赖组合,仍应以你当前启用的模块和目标部署拓扑为准。

数据库边界

在持久化层,AdministrationService 的重点是接管各类管理模块对应的 DbContext 接口:

1
2
3
4
5
6
7
8
9
10
context.Services.AddAbpDbContext<AdministrationServiceDbContext>(options =>
{
options.ReplaceDbContext<IPermissionManagementDbContext>();
options.ReplaceDbContext<ISettingManagementDbContext>();
options.ReplaceDbContext<IFeatureManagementDbContext>();
options.ReplaceDbContext<IAuditLoggingDbContext>();
options.ReplaceDbContext<IBlobStoringDbContext>();

options.AddDefaultRepositories(includeAllEntities: true);
});

这一步的含义是:权限、设置、功能、审计和 Blob 存储相关的数据,不再分散由各个模块默认持有,而是统一归入 AdministrationService 的数据库边界。

由于本系列前两篇已经以 PostgreSQL 为本地开发数据库基线,因此迁移配置也应保持一致:

1
2
3
4
5
6
7
8
9
10
Configure<AbpDbContextOptions>(options =>
{
options.Configure<AdministrationServiceDbContext>(c =>
{
c.UseNpgsql(b =>
{
b.MigrationsHistoryTable("__AdministrationService_Migrations");
});
});
});

和上一篇文章里的连接映射对应起来看,AdministrationService 实际承接的是这些模块:

  • AbpAuditLogging
  • AbpPermissionManagement
  • AbpSettingManagement
  • AbpFeatureManagement
  • AbpBlobStoring

这就是这个服务的数据库归属边界。

与后续服务的关系

后面的业务服务本身通常不直接实现这些平台级管理能力,而是通过远程调用、共享配置或统一后台入口与 AdministrationService 协作。因此把它先搭好,本质上是在为整个平台的后台能力先立规矩。

IdentityService:身份底座

IdentityService 负责的是整套系统的身份数据,而不是认证服务器本身。它的角色更接近“用户与身份中心”,通常会集中承接这些能力:

  • 用户
  • 角色
  • 组织单元
  • 身份相关持久化模型
  • 为认证体系提供用户与权限基础数据

把这一层独立出来的价值在于:无论后面有多少业务服务,它们都不应该各自维护用户体系。身份数据本身就属于平台底座,应由统一服务治理。

核心模块依赖

在当前系列的叙事里,IdentityService 的核心领域依赖首先应围绕 AbpIdentity 组织:

1
2
3
4
5
6
7
[DependsOn(
typeof(IdentityServiceDomainSharedModule),
typeof(AbpIdentityDomainModule)
)]
public class IdentityServiceDomainModule : AbpModule
{
}

如果你看的是较早的 ABP 示例,可能还会看到 IdentityServer 相关依赖。但在当前 ABP 体系下,更适合按照 OpenIddict 的结构来理解认证体系:IdentityService 负责身份数据,而令牌签发和认证服务器宿主会在后续章节单独展开。

数据库边界

IdentityService 的持久化层重点是接管身份相关上下文,并为认证体系保留数据落点:

1
2
3
4
5
6
7
context.Services.AddAbpDbContext<IdentityServiceDbContext>(options =>
{
options.ReplaceDbContext<IIdentityDbContext>();
options.ReplaceDbContext<IOpenIddictDbContext>();

options.AddDefaultRepositories(includeAllEntities: true);
});

这里的设计意图很明确:

  • IIdentityDbContext 代表用户、角色等身份数据。
  • IOpenIddictDbContext 代表当前认证体系所依赖的持久化数据。

如果你后续决定把认证服务器持久化进一步拆分,也可以再调整边界;但在基础服务阶段,先把身份数据中心立起来,是最稳妥的路线。

和上一篇文章中的连接映射对应起来,IdentityService 至少应该承接:

  • AbpIdentity
  • OpenIddict

与后续服务的关系

网关、认证服务器、后台应用以及各业务服务,最终都会围绕身份体系工作。因此在整套微服务框架里,IdentityService 更像是一套稳定的身份能力底座,而不是某个可选模块。

SaasService:租户底座

如果系统从一开始就需要支持多租户,那么 SaasService 通常应尽早拆出来。它的职责非常明确:承接租户管理及其相关配置,是整套多租户系统的权威来源。

一旦租户边界明确,后续的网关、认证、业务服务才能在统一租户上下文中运行。否则,不同服务很容易各自维护一套租户识别逻辑,后续协调成本会越来越高。

核心模块依赖

SaasService 的领域层通常会直接围绕租户管理模块组织:

1
2
3
4
5
6
7
[DependsOn(
typeof(SaasServiceDomainSharedModule),
typeof(AbpTenantManagementDomainModule)
)]
public class SaasServiceDomainModule : AbpModule
{
}

这个服务的边界非常纯粹:它承接的是平台的多租户能力,而非业务领域建模。

数据库边界

持久化层也同样清晰,核心就是接管租户管理上下文:

1
2
3
4
5
context.Services.AddAbpDbContext<SaasServiceDbContext>(options =>
{
options.ReplaceDbContext<ITenantManagementDbContext>();
options.AddDefaultRepositories(includeAllEntities: true);
});

迁移表继续按服务独立命名:

1
2
3
4
5
6
7
8
9
10
Configure<AbpDbContextOptions>(options =>
{
options.Configure<SaasServiceDbContext>(c =>
{
c.UseNpgsql(b =>
{
b.MigrationsHistoryTable("__SaasService_Migrations");
});
});
});

这样做的意义在于,租户数据从一开始就有清晰的数据库归属,不会和身份或业务数据混在一起。

与后续服务的关系

后续无论是认证服务器、网关,还是具体业务服务,只要涉及租户识别、租户配置或租户上下文传递,最终都要围绕 SaasService 这一层展开。因此它虽然不直接承接业务功能,却会影响整套系统的运行方式。

宿主层的统一基础能力

共享宿主模块与服务宿主关系

三套基础服务真正运行起来,关键不在前面的类库项目,而在 HttpApi.Host。这一层很容易被低估,因为它看起来只是启动程序,实际上却承载了整套服务的运行入口。

在当前 ABP 版本下,更推荐采用 WebApplicationBuilder 这一套现代宿主模型,而不是继续沿用早期示例里常见的 Program + Startup 叙事。原因很简单:当前模板、扩展点和中间件组织方式都已经围绕这一模式稳定下来,继续跟旧写法绑定,只会增加读者的迁移成本。

一个典型的宿主入口可以组织成这样:

1
2
3
4
5
6
7
8
9
10
11
var builder = WebApplication.CreateBuilder(args);
builder.Host
.AddAppSettingsSecretsJson()
.UseAutofac()
.UseSerilog();

await builder.AddApplicationAsync<AdministrationServiceHttpApiHostModule>();

var app = builder.Build();
await app.InitializeApplicationAsync();
await app.RunAsync();

在本系列里,前一篇已经通过共享宿主模块准备好了这套基础能力,因此三个服务的 HttpApi.Host 可以统一围绕“宿主层基础能力”来组织。

模块依赖

三个 HttpApi.Host 模块都应统一依赖:

  • 本服务的 HttpApi 模块
  • 本服务的 Application 模块
  • 本服务的 EntityFrameworkCore 模块
  • MyProjectNameSharedHostingMicroservicesModule

这意味着,服务自身只需要声明自己的能力边界,而认证、缓存、多租户、日志等基础能力都通过共享宿主模块向下沉淀。

这里还有一个容易忽略的实践点:共享宿主模块并不是把所有基础设施依赖都预先塞进去,而是收敛当前这套方案真正共用的宿主能力。在本文对应的实现里,它主要保留 Redis、分布式锁、多租户、消息总线、JWT Bearer 等通用配置;数据库迁移与具体持久化映射仍然留在各服务自己的 EntityFrameworkCoreHttpApi.Host 中。

JWT Bearer

服务间鉴权和 API 访问的第一层入口,是统一的 JWT Bearer 校验。前一篇文章里我们已经准备了 JwtBearerConfigurationHelper,这里可以继续复用:

1
2
3
JwtBearerConfigurationHelper.Configure(context, "AdministrationService");
JwtBearerConfigurationHelper.Configure(context, "IdentityService");
JwtBearerConfigurationHelper.Configure(context, "SaasService");

这样每个服务只需要声明自己的 Audience,而 AuthorityRequireHttpsMetadata 则继续从统一配置读取。对于读者来说,更重要的是理解:这类能力属于宿主层,不属于某个具体业务模块。

CORS

跨域配置也应保持统一,避免每个宿主各写一套不同规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
context.Services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder
.WithOrigins(
configuration["App:CorsOrigins"]
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.Trim().RemovePostFix("/"))
.ToArray()
)
.WithAbpExposedHeaders()
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});

这类配置本身并不复杂,但它们决定了所有服务在对外暴露 API 时的运行一致性。

Swagger OAuth 集成

对于一套微服务方案来说,Swagger 不只是“为了好看”,它还是你验证 API 契约和认证链路是否正确的重要入口。这里继续复用上一篇文章封装的共享辅助类即可:

1
2
3
4
5
6
7
8
9
SwaggerConfigurationHelper.ConfigureWithAuth(
context,
authority: configuration["AuthServer:Authority"]!,
scopes: new Dictionary<string, string>
{
["IdentityService"] = "Identity Service API"
},
apiTitle: "Identity Service API"
);

从框架搭建视角看,Swagger OAuth 集成属于宿主层基础能力,而不是某个具体应用服务的职责。

数据库迁移检查

还有一个容易被忽略,但在微服务环境里很实用的能力,是启动阶段的数据库迁移检查。

宿主模块里可以统一采用类似下面的模式:

1
2
3
4
5
6
7
8
9
10
public override void OnPostApplicationInitialization(ApplicationInitializationContext context)
{
using var scope = context.ServiceProvider.CreateScope();

AsyncHelper.RunSync(
() => scope.ServiceProvider
.GetRequiredService<AdministrationServiceDatabaseMigrationChecker>()
.CheckAndApplyDatabaseMigrationsAsync()
);
}

不同服务只需要替换成各自的迁移检查器即可。这样做的价值在于,服务启动时就能发现数据库状态是否满足运行要求,而不是把问题拖到第一次请求进来之后。

参考资料

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