网站结构说明

学习杨中科老师开源项目在线英语网站微服务

  1. 功能:听力练习。
  2. 业务概念:类别(Category)、专辑(Album)、片段(Episode)。
  3. 听力原文字幕文件查看。
  4. 网站后台允许进行资源的CRUD。
  5. 其他格式的音频的追踪在部分浏览器上有问题,统一用M4A。
  6. 音频文件放到单独的文件服务器上。
  7. 原文的搜素。

项目结构说明

为了便于管理,我们把不同服务的项目放到不同的解决方案文件夹下,解决方案文件夹Commons下的项目是一些公用的类库。
各服务的解决方案文件夹下都包含Domain、Infrastucture、WebAPI这3个项目,它们分别对应领域层、基础设施层、应用服务层。听力网站前台和听力网站后台共享相同的领域层和基础设施层,因此在解决方案文件夹Listening下有4个项目。
因为所有的项目都用到了领域事件、集成事件、中心配置服务器、JWT、工作单元、CORS、FluentValidation等,创建Commonlnitializer项目来复用这些组件的初始化代码。

有一点需要特别注意,如果我们创建的是ASP.NET Core项目,在项目中我们可以使用WebApplicationBuilder、IApplicationBuilder、IWebHostEnvironment等类型,但是在类库项目中我们则不能直接使用这些类型。这些类型都定义在Microsoft.AspNetCore.Hosting.Abstractions、Microsoft.AspNetCore等程序集中,版本非常低,ASP.NET Core的包不在单独发布到NuGet中,而是直接内建在.NET Core SDK中。如果想在ASP.NET Core中引用这些ASP.NET Core的类型,请在csproj中添加<FrameworkReference Include=”Microsoft.AspNetCore.App”/>。

项目运行环境搭建

这个项目使用Microsoft SQL Server作为数据库服务器、用Nginx作为网关、用Redis实现分布式缓存、用RabbitMQ实现领域事件、用Elasticsearch作为搜索引擎服务器。

第一步,在生产环境下,我们一般会把不同的服务放到不同的服务器上,因此不会出现多个ASP.NET Core网站同时运行造成的端口冲突问题,但是如果我们需要在Visual Studio中同时运行多个ASP.NET Core项目,就可能会遇到这些项目的端口冲突的问题。如果我们用Visual Studio中的IIS Express来运行网站的话,可以修改ASP.NET Core项目的Properties文件夹下的launchSettings.json文件,在iisExpress节点下配置指定项目运行的端口。

我们分别让FileService.WebAPI、IdentityService.WebAPI、Listening.Admin.WebAPI、Listening.Main.WebAPI、MediaEncoder.WebAPI、SearchService.WebAPI运行在44339、44392、44352、44375、44353、44310端口下。

第二步,为了统一前端访问后端的不同服务的接口,我们配置Nginx来反向代理后端的接口。我们在nginx的nginx.conf文件中的server节点下增加代码。

配置nginx,这里端口号一定要和你的swagger端口号统一,因为你不是用iis,其次就是跨域的话,默认swagger的url有2个,只能留一个
假设你的 Swagger 服务运行在127.0.0.1:8080,Nginx 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80; # 对外暴露的端口,可根据需要修改
server_name localhost; # 你的域名或服务器IP

# 代理到Swagger服务,确保端口一致
location / {
proxy_pass http://127.0.0.1:8080; # 与Swagger服务端口保持一致
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
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

server {
listen 80;
server_name localhost;

location /FileService/{
proxy_pass http://localhost:44339/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100m;
}

location /IdentityService/{
proxy_pass http://localhost:44392/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /Listening.Admin/{
proxy_pass http://localhost:44352/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}

localhost /Listening.Main/{
proxy_pass http://localhost:44375/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

localhost /MediaEncoder/{
proxy_pass http://localhost:44353/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remoto_port;
proxy_set_header X-For-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /SearchService/{
proxy_pass http://localhost:44310/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
}
}

上面配置的proxy_set_header是用来方便我们在ASP.NET Core中获取客户端IP地址的,需要配合ForwardedHeaders中间件使用。
配置完Nginx后,只要重启Nginx服务器即可让配置生效,然后我们访问https://localhost/IdentityService/就可以访问IdentityService的接口了,这样前端就可以通过统一的端口来访问后端服务。

第三步,CommonInitializer中的初始化代码是设定从“DefaultDB:ConnStr”路径中读取数据库的连接字符串,因此请在环境变量中配置名字为DefaultDB:ConnStr的数据库连接字符串,然后在各个项目中运行EF Core数据库迁移来生成数据库表。

第四步,我们在数据库中增加一个名字为T_Configs的表,并且在表中增加如下配置。

Cors的配置项为项目的跨域设置。
FileService:SMB的配置项为文件备份服务器的根目录。
Redis的配置项为Redis服务器的连接配置。
RabbitMQ的配置项为集成事件相关RabbitMQ的配置,HostName属性为服务器的地址,ExchangeName属性为集成事件的交换机名字。
ElasticSearch的配置项为ElasticSearch服务器的配置,其中的用户名、密码需要和读者安装的ElasticSearch服务器的配置一致。
JWT的配置项为登录令牌的JWT配置。

Commons

一些项目初始化的代码放到这里,项目里通用的东西放到Commons解决方案文件夹下

项目源码

项目 说明
YU.ASPNETCore DistributedCacheHelper 分布式缓存帮助类
YU.Commons 验证器文件夹 FluentValidation的扩展类
YU.DomainCommons IAggregateRoot 聚合根标识接口
YU.EventBus 订阅、撤销、发布事件 集成事件总线
YU.Infrastructure BaseDbContext 领域事件的发布
YU.JWT Token 使用JWT实现登录令牌

所用到的NuGet包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="FluentValidation" Version="12.0.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
<PackageReference Include="MediatR" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.18" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.18" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="StackExchange.Redis" Version="2.8.58" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="8.1.4" />
<PackageReference Include="Zack.AnyDBConfigProvider" Version="1.1.4" />
</ItemGroup>

ApplicationBuilderExtensions

首现我们创建一个Commonlnitializer类库项目,在csproj中添加<FrameworkReference Include=”Microsoft.AspNetCore.App”/>这个类库把我们所有需要用到的配置领域事件、集成事件、中心配置服务器、JWT、工作单元、CORS、FluentValidation等,用来复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace Commonlnitializer
{
/// <summary>
/// 提供对IApplicationBuilder的扩展方法,用于配置应用的默认中间件。
/// </summary>
public static class ApplicationBuilderExtensions
{
/// <summary>
/// 配置应用的默认中间件,包括事件总线、CORS、转发头、认证和授权。
/// </summary>
/// <param name="app">IApplicationBuilder 实例。</param>
/// <returns>配置后的 IApplicationBuilder 实例。</returns>
public static IApplicationBuilder UseZackDefault(this IApplicationBuilder app)
{
app.UseEventBus(); // 启用事件总线
app.UseCors(); // 启用跨域资源共享(CORS)
app.UseForwardedHeaders(); // 处理代理服务器转发的头信息
//app.UseHttpsRedirection();//不能与ForwardedHeaders很好的工作,而且webapi项目也没必要配置这个
app.UseAuthentication(); // 启用认证
app.UseAuthorization(); // 启用授权
return app;
}
}
}

UseEventBus在应用程序中启用事件总线,是我们给IApplicationBuilder类扩展的方法,是检查是否事件总线服务是否注册如果未注册,将抛出异常提醒开发者。
app.UseForwardedHeaders(); 获取Nignx的原始客户端的 IP 地址,原始请求的协议(HTTP 或 HTTPS),原始主机地址

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
namespace YU.EventBus
{
/// <summary>
/// 提供扩展方法,用于在应用程序中启用事件总线功能。
/// </summary>
public static class ApplicationBuilderExtensions
{
/// <summary>
/// 启用事件总线功能。
/// 该方法会检查事件总线服务(IEventBus)是否已注册。
/// 如果未注册,将抛出异常提醒开发者。
/// </summary>
/// <param name="appBuilder">应用程序构建器对象。</param>
/// <returns>返回原始的应用程序构建器对象,便于链式调用。</returns>
/// <exception cref="ApplicationException">如果未找到IEventBus的实现,则抛出异常。</exception>
public static IApplicationBuilder UseEventBus(this IApplicationBuilder appBuilder)
{
// 尝试从依赖注入容器中获取IEventBus服务
object? eventBus = appBuilder.ApplicationServices.GetService(typeof(IEventBus));
if (eventBus == null)
{
// 如果没有找到IEventBus的实现,抛出异常,提示开发者需要注册服务
throw new ApplicationException("找不到IEventBus的实现,请确保已正确注册EventBus服务。");
}
// 返回appBuilder,支持链式调用
return appBuilder;
}
}
}
// 此扩展方法允许应用程序构建器使用事件总线功能。

其中的object? eventBus = appBuilder.ApplicationServices.GetService(typeof(IEventBus));在 ASP.NET Core 中从依赖注入(DI)容器获取一个类型为 IEventBus 的事件接口,并赋值给一个 object? 类型的变量 eventBus。

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
namespace YU.EventBus
{
/// <summary>
/// 事件总线接口,定义了事件的发布与订阅相关操作。
/// </summary>
public interface IEventBus
{
/// <summary>
/// 发布事件。
/// </summary>
/// <param name="eventName">事件名称。</param>
/// <param name="eventData">事件数据,可以为 null。</param>
void publish(string eventName, object? eventData);

/// <summary>
/// 订阅指定事件。
/// </summary>
/// <param name="eventName">事件名称。</param>
/// <param name="handlerType">处理该事件的处理器类型。</param>
void Subscribe(string eventName, Type handlerType);

/// <summary>
/// 取消订阅指定事件。
/// </summary>
/// <param name="eventName">事件名称。</param>
/// <param name="handlerType">要移除的处理器类型。</param>
void Unsubscribe(string eventName, Type handlerType);
}
}

app.UseCors();
app.UseForwardedHeaders();
app.UseAuthentication();
app.UseAuthorization();
是Microsoft.AspNetCore.BuilderNuGet包,安装后可直接配置扩展方法,后面的项目可以直接引用。

CorsSettings

CorsSettings类用于配置 跨域资源共享(CORS)

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace Commonlnitializer
{
/// <summary>
/// 表示跨域资源共享(CORS)相关的配置设置。
/// </summary>
public class CorsSettings
{
/// <summary>
/// 允许的跨域来源地址列表。
/// </summary>
public string[] Origins { get; set; }
}
}

Origins 表示允许跨域请求的来源地址列表(如 http://localhost:3000

DbContextOptionsBuilderFactory

我们创建一个DbContextOptionsBuilderFactory的实用工厂类。

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
namespace Commonlnitializer
{
/// <summary>
/// 提供用于创建 <see cref="DbContextOptionsBuilder{TContext}"/> 的工厂方法。
/// </summary>
public static class DbContextOptionsBuilderFactory
{
/// <summary>
/// 创建并配置 <see cref="DbContextOptionsBuilder{TDbContext}"/> 实例,使用环境变量 "DefaultDB:ConnStr" 作为连接字符串。
/// </summary>
/// <typeparam name="TDbContext">要配置的 DbContext 类型。</typeparam>
/// <returns>配置好的 <see cref="DbContextOptionsBuilder{TDbContext}"/> 实例。</returns>
public static DbContextOptionsBuilder<TDbContext> Create<TDbContext>()
where TDbContext : DbContext
{
// 从环境变量获取数据库连接字符串
var connStr = Environment.GetEnvironmentVariable("DefaultDB:ConnStr");
if (string.IsNullOrWhiteSpace(connStr))
{
throw new InvalidOperationException("Environment variable 'DefaultDB:ConnStr' is not set.");
}
// 检查连接字符串是否有效
// 创建 DbContextOptionsBuilder 实例
var optionsBuilder = new DbContextOptionsBuilder<TDbContext>();
// 配置使用 SQL Server 数据库
optionsBuilder.UseSqlServer(connStr);
return optionsBuilder;
}
}
}

DbContextOptionsBuilder<TDbContext> 实例,用于统一创建并配置。
var connStr = Environment.GetEnvironmentVariable(“DefaultDB:ConnStr”);从环境变量中读取数据库连接字符串,不在appsettings.json,是防止信息的泄露。
new DbContextOptionsBuilder<TDbContext>();创建并配置一个DbContextOptionsBuilder 实例。
UseSqlServer();指定使用SQL Server数据库。

InitializerOptions

InitializerOptions类,用于集中配置一些初始化相关的参数,例如日志路径和 EventBus 队列名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace Commonlnitializer
{
/// <summary>
/// 初始化选项配置类。
/// </summary>
public class InitializerOptions
{
/// <summary>
/// 日志文件路径。
/// </summary>
public string LogFilePath { get; set; }

/// <summary>
/// 用于 EventBus 的队列名称。
/// 需保证同一项目值保持一致,不同项目不能冲突。
/// </summary>
public string EventBusQueueName { get; set; }
}
}

WebApplicationBuilderExtensions

给WebApplicationBuilder提供扩展方法。

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
121
namespace Commonlnitializer
{
/// <summary>
/// 提供 WebApplicationBuilder 的扩展方法。
/// </summary>
public static class WebApplicationBuilderExtensions
{
/// <summary>
/// 配置数据库配置源,将数据库中的配置信息添加到应用配置中。
/// </summary>
/// <param name="builder">WebApplicationBuilder 实例。</param>
public static void ConfigureDbConfiguration(this WebApplicationBuilder builder)
{
// 配置主机构建器的应用配置
builder.Host.ConfigureAppConfiguration((hostCtx, configBuilder) =>
{
// 从应用配置中获取数据库连接字符串,键为 "DefaultDB:ConnStr"
string connStr = builder.Configuration.GetValue<string>("DefaultDB:ConnStr");
// 添加自定义的数据库配置源,支持自动刷新,刷新间隔为5秒
configBuilder.AddDbConfiguration(() => new SqlConnection(connStr), reloadOnChange: true, reloadInterval: TimeSpan.FromSeconds(5));
});
}

/// <summary>
/// 配置应用所需的额外服务,包括数据库、认证、授权、日志、CORS、Redis、事件总线等。
/// </summary>
/// <param name="builder">WebApplicationBuilder 实例。</param>
/// <param name="initOptions">初始化选项,包含日志路径和事件总线队列名等。</param>
public static void ConfigureExtraServices(this WebApplicationBuilder builder, InitializerOptions initOptions)
{
// 获取服务集合
IServiceCollection services = builder.Services;
// 获取应用配置
IConfiguration configuration = builder.Configuration;
// 获取所有引用的程序集,用于后续模块初始化和依赖注入
var assemblies = ReflectionHelper.GetAllReferencedAssemblies();
// 执行所有模块的初始化方法
services.RunModuleInitializers(assemblies);
// 注册所有数据库上下文,使用 SQL Server,连接字符串从配置中获取
services.AddAllDbContexts(ctx =>
{
string connStr = configuration.GetValue<string>("DefaultDB:ConnStr");
ctx.UseSqlServer(connStr);
}, assemblies);

// 添加认证服务
builder.Services.AddAuthentication();
// 添加授权服务
builder.Services.AddAuthorization();
// 从配置中获取 JWT 相关配置
JWTOptions jwtOpt = configuration.GetSection("JWT").Get<JWTOptions>();
// 添加 JWT 认证服务
builder.Services.AddJWTAuthentication(jwtOpt);
// 配置 Swagger,添加认证头部
builder.Services.Configure<SwaggerGenOptions>(c =>
{
c.AddAuthenticationHeader();
});

// 注册 MediatR,用于领域事件和命令处理
services.AddMediatR(assemblies);
// 配置 MVC 选项,添加工作单元过滤器
services.Configure<MvcOptions>(options =>
{
options.Filters.Add<UnitOfWorkFilter>();
});
// 配置 JSON 选项,添加自定义的 DateTime 序列化转换器
services.Configure<JsonOptions>(options =>
{
options.JsonSerializerOptions.Converters.Add(new DateTimeJsonConverter("yyyy-MM-dd HH:mm:ss"));
});

// 配置跨域资源共享(CORS)策略
services.AddCors(options =>
{
// 从配置中获取 CORS 设置
var corsOpt = configuration.GetSection("Cors").Get<CorsSettings>();
// 获取允许的来源列表
string[] urls = corsOpt.Origins;
// 添加默认策略,允许指定来源、任意头、任意方法,并允许携带凭据
options.AddDefaultPolicy(builder => builder.WithOrigins(urls)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
});
// 配置日志服务,使用 Serilog,日志输出到控制台和文件
services.AddLogging(builder =>
{
// 配置 Serilog 日志记录器
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File(initOptions.LogFilePath)
.CreateLogger();
// 将 Serilog 集成到日志系统
builder.AddSerilog();
});
// 获取应用程序主程序集,用于注册 FluentValidation 验证器
var applicationAssembly = typeof(UserModelValidator).Assembly;
// 注册所有验证器
services.AddValidatorsFromAssembly(applicationAssembly);
// 配置 JWT 选项绑定
services.Configure<JWTOptions>(configuration.GetSection("JWT"));
// 配置 RabbitMQ 事件总线选项绑定
services.Configure<IntegrationEventRabbitMQOptions>(configuration.GetSection("RabbitMQ"));
// 注册事件总线服务
services.AddEventBus(initOptions.EventBusQueueName, assemblies);

// 从配置中获取 Redis 连接字符串
string redisConnStr = configuration.GetValue<string>("Redis:ConnStr");
// 创建 Redis 连接多路复用器
IConnectionMultiplexer redisConnMultiplexer = ConnectionMultiplexer.Connect(redisConnStr);
// 注册 Redis 连接为单例服务
services.AddSingleton(typeof(IConnectionMultiplexer), redisConnMultiplexer);
// 配置转发头部选项,支持所有转发头
services.Configure<ForwardedHeadersOptions>(Options =>
{
Options.ForwardedHeaders = ForwardedHeaders.All;
});
}
}
}

ConfigureDbConfiguration

我们创建了一个对WebApplicationBuilder的扩展方法,读取数据库配置源。
将数据库作为配置源,并将其配置加载到 WebApplicationBuilder.Configuration 中,支持自动刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>
/// 配置数据库配置源,将数据库中的配置信息添加到应用配置中。
/// </summary>
/// <param name="builder">WebApplicationBuilder 实例。</param>
public static void ConfigureDbConfiguration(this WebApplicationBuilder builder)
{
// 配置主机构建器的应用配置
builder.Host.ConfigureAppConfiguration((hostCtx, configBuilder) =>
{
// 从应用配置中获取数据库连接字符串,键为 "DefaultDB:ConnStr"
string connStr = builder.Configuration.GetValue<string>("DefaultDB:ConnStr");
// 添加自定义的数据库配置源,支持自动刷新,刷新间隔为5秒
configBuilder.AddDbConfiguration(() => new SqlConnection(connStr), reloadOnChange: true, reloadInterval: TimeSpan.FromSeconds(5));
});
}

其中builder.Host.ConfigureAppConfiguration((hostCtx, configBuilder) =>是修改应用的主机(IHostBuilder)的配置构建逻辑。
这是对应用配置系统进行扩展(而不是仅仅修改运行时服务)。
hostCtx 是当前主机的上下文;configBuilder 是正在构建的配置。

而configBuilder.AddDbConfiguration中的AddDbConfiguration是作用是从数据库中读取配置项,作为应用配置源之一添加到 Configuration 中。
() => new SqlConnection(connStr)是一个延迟执行的委托,当配置系统需要刷新或初始化时才会创建连接对象,避免启动时立即连接数据库
reloadOnChange: true启用自动刷新配置功能,意思是当数据库中的配置值发生变更时,可以自动重新加载,不重启服务
reloadInterval: TimeSpan.FromSeconds(5)配置刷新周期为 5 秒,即配置系统每隔 5 秒去检查数据库配置是否有变更,如果有则刷新。

数据库变成了应用的一个配置源,优先级可能比 appsettings.json 高(取决于添加顺序)。

ConfigureExtraServices

ConfigureExtraServices配置了配置应用所需的额外服务,包括数据库、认证、授权、日志、CORS、Redis、事件总线等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void ConfigureExtraServices(this WebApplicationBuilder builder, InitializerOptions initOptions)
{
// 获取服务集合
IServiceCollection services = builder.Services;
// 获取应用配置
IConfiguration configuration = builder.Configuration;
// 获取所有引用的程序集,用于后续模块初始化和依赖注入
var assemblies = ReflectionHelper.GetAllReferencedAssemblies();
// 执行所有模块的初始化方法
services.RunModuleInitializers(assemblies);
// 注册所有数据库上下文,使用 SQL Server,连接字符串从配置中获取
services.AddAllDbContexts(ctx =>
{
string connStr = configuration.GetValue<string>("DefaultDB:ConnStr");
ctx.UseSqlServer(connStr);
}, assemblies);

IServiceCollection services = builder.Services从 WebApplicationBuilder 中获取服务注册容器(IServiceCollection),用于注册依赖后续所有的依赖注入(AddScoped、AddDbContext 等)都会往这个集合中注册。

IConfiguration configuration = builder.Configuration获取应用程序的配置系统接口(例如 appsettings.json、环境变量、命令行等)后续可通过 configuration.GetValue<string>(“xxx”) 方式读取配置值

var assemblies = ReflectionHelper.GetAllReferencedAssemblies()中的ReflectionHelper.GetAllReferencedAssemblies()是用于通过反射获取当前项目引用的所有程序集(Assembly)

services.RunModuleInitializers(assemblies)这是一个模块化系统的入口,会从传入的 assemblies 中寻找实现了某个约定接口(例如 IModuleInitializer)的类,并调用其初始化方法。

services.AddAllDbContexts(ctx =>从配置中读取数据库连接字符串,在多个程序集内查找所有继承自 DbContext 的类,自动为这些 DbContext 注册到 DI 容器中,统一使用 UseSqlServer(…) 进行数据库配置。

而ReflectionHelper.GetAllReferencedAssemblies()、RunModuleInitializers()、AddAllDbContexts,都是自定义的扩展方法。

JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
// 添加认证服务
builder.Services.AddAuthentication();
// 添加授权服务
builder.Services.AddAuthorization();
// 从配置中获取 JWT 相关配置
JWTOptions jwtOpt = configuration.GetSection("JWT").Get<JWTOptions>();
// 添加 JWT 认证服务
builder.Services.AddJWTAuthentication(jwtOpt);
// 配置 Swagger,添加认证头部
builder.Services.Configure<SwaggerGenOptions>(c =>
{
c.AddAuthenticationHeader();
});

其中的JWTOptions是JWT 配置选项类,用于存储生成和验证 JWT 所需的参数。

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
namespace YU.JWT
{
/// <summary>
/// JWT 配置选项类,用于存储生成和验证 JWT 所需的参数。
/// </summary>
public class JWTOptions
{
/// <summary>
/// 令牌颁发者(Issuer)。
/// </summary>
public string Issuer { get; set; }

/// <summary>
/// 令牌接收者(Audience)。
/// </summary>
public string Audience { get; set; }

/// <summary>
/// 用于签名 JWT 的密钥。
/// </summary>
public string Key { get; set; }

/// <summary>
/// 令牌过期时间(秒)。
/// </summary>
public int ExpireSeconds { get; set; }
}
}

而配置JWT还要配置配置JWT令牌验证和添加在 Swagger 文档中添加认证头部信息,分别对这两个配置封装为AddJWTAuthentication、AddAuthenticationHeader扩展方法。

AddJWTAuthentication

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
namespace YU.JWT
{
/// <summary>
/// 提供JWT认证相关的扩展方法。
/// </summary>
public static class AuthenticationExtensions
{
/// <summary>
/// 向服务集合中添加JWT认证。
/// </summary>
/// <param name="services">服务集合。</param>
/// <param name="jwtOpt">JWT配置选项。</param>
/// <returns>认证生成器。</returns>
public static AuthenticationBuilder AddJWTAuthentication(this IServiceCollection services, JWTOptions jwtOpt)
{
return services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
// 配置JWT令牌验证参数
x.TokenValidationParameters = new()
{
ValidateIssuer = true, // 验证签发者
ValidateAudience = true, // 验证接收者
ValidateLifetime = true, // 验证有效期
ValidateIssuerSigningKey = true, // 验证签名密钥
ValidIssuer = jwtOpt.Issuer, // 有效签发者
ValidAudience = jwtOpt.Audience, // 有效接收者
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOpt.Key)), // 签名密钥
};
});
}
}
}

AddAuthenticationHeader

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
namespace YU.JWT
{
/// <summary>
/// SwaggerGenOptions 的扩展方法,用于在 Swagger 文档中添加认证头部信息。
/// </summary>
public static class SwaggerGenOptionsExtensions
{
/// <summary>
/// 向 Swagger 配置中添加认证头(Authorization Header),
/// 使得在 Swagger UI 中可以输入 JWT Token 进行接口测试。
/// </summary>
/// <param name="c">SwaggerGenOptions 实例。</param>
public static void AddAuthenticationHeader(this SwaggerGenOptions c)
{
// 添加安全定义,指定名称为 "Authorization",类型为 ApiKey,位置在 Header。
// 这样在 Swagger UI 中会出现一个输入框用于填写 Token。
c.AddSecurityDefinition("Authorization", new OpenApiSecurityScheme
{
// 描述信息,指导用户如何填写 Token。
Description = "Authorization header .\r\n Example:'Bearer 12345abcdef'",
// 指定参数在 HTTP Header 中传递。
In = ParameterLocation.Header,
// 安全方案类型为 ApiKey。
Type = SecuritySchemeType.ApiKey,
// 方案名称,通常为 "Authorization"。
Scheme = "Authorization"
});

// 添加安全需求,指定所有接口都需要携带 "Authorization" 头部。
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
// 引用上面定义的安全方案。
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id="Authorization"
},
// 方案类型为 oauth2(虽然实际类型为 ApiKey,这里用于兼容性)。
Scheme = "oauth2",
// 头部名称。
Name="Authorization",
// 参数位置为 Header。
In=ParameterLocation.Header,
},
// 作用域列表,这里为空表示不限定作用域。
new List<string>()
}
});
}
}
}
1
2
// 配置 JWT 选项绑定
services.Configure<JWTOptions>(configuration.GetSection("JWT"));

这行代码的作用是将应用配置文件中的 JWT 配置节绑定到 JWTOptions 类型,并注册到依赖注入容器,方便后续通过依赖注入获取配置信息。

MediatR

1
2
// 注册 MediatR,用于领域事件和命令处理
services.AddMediatR(assemblies);

而AddMediatR是对注册MediatR服务的配置和派发领域事件。

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
namespace YU.Infrastructure.EFCore
{
/// <summary>
/// 提供与MediatR相关的扩展方法。
/// </summary>
public static class MediatorExtensions
{
/// <summary>
/// 扩展IServiceCollection以批量注册MediatR相关服务。
/// </summary>
/// <param name="services">依赖注入服务集合。</param>
/// <param name="assemblies">包含MediatR处理程序的程序集集合。</param>
/// <returns>服务集合本身,便于链式调用。</returns>
public static IServiceCollection AddMediatR(this IServiceCollection services, IEnumerable<Assembly> assemblies)
{
// 将指定程序集中的MediatR服务注册到依赖注入容器
return services.AddMediatR(assemblies.ToArray());
}

/// <summary>
/// 派发当前DbContext中所有聚合根的领域事件。
/// </summary>
/// <param name="mediator">MediatR中介者实例。</param>
/// <param name="ctx">当前的EF Core DbContext实例。</param>
/// <returns>异步任务。</returns>
public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx)
{
// 获取所有实现了IDomainEvents接口且包含领域事件的实体
var domainEntities = ctx.ChangeTracker
.Entries<IDomainEvents>()
.Where(x => x.Entity.GetDomainEvents().Any());

// 收集所有领域事件
var domainEvents = domainEntities
.SelectMany(x => x.Entity.GetDomainEvents())
.ToList();

// 清空实体上的领域事件,避免重复派发
domainEntities.ToList()
.ForEach(entity => entity.Entity.ClearDomainEvents());

// 逐个通过MediatR发布领域事件
foreach (var domainEvent in domainEvents)
{
await mediator.Publish(domainEvent);
}
}
}
}

要实现IDomainEvents接口且包含领域事件的实体

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
namespace YU.DomainCommons.Models
{
/// <summary>
/// 定义领域事件集合的接口。
/// 该接口用于聚合根或实体中管理领域事件的添加、去重、获取和清理操作。
/// 领域事件用于在领域模型内部或外部传播重要的业务状态变化。
/// </summary>
public interface IDomainEvents
{
/// <summary>
/// 获取当前已注册的所有领域事件。
/// </summary>
/// <returns>领域事件的只读集合。</returns>
IEnumerable<INotification> GetDomainEvents();

/// <summary>
/// 添加一个新的领域事件到集合中。
/// </summary>
/// <param name="eventItem">要添加的领域事件。</param>
void AddDomainEvent(INotification eventItem);

/// <summary>
/// 如果集合中不存在该事件,则添加一个新的领域事件。
/// 用于避免重复添加相同的事件。
/// </summary>
/// <param name="eventItem">要添加的领域事件。</param>
void AddDomainEventIfAbsent(INotification eventItem);

/// <summary>
/// 清空当前所有已注册的领域事件。
/// </summary>
void ClearDomainEvents();
}
}

筛选器(过滤器)

1
2
3
4
5
// 配置 MVC 选项,添加工作单元过滤器
services.Configure<MvcOptions>(options =>
{
options.Filters.Add<UnitOfWorkFilter>();
});

options.Filters.Add<UnitOfWorkFilter>()给所有的控制器动作方法添加一个全局过滤器。
UnitOfWorkFilter 是自定义的类,实现了IAsyncActionFilter。

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
namespace YU.ASPNETCore
{
/// <summary>
/// 实现基于特性(UnitOfWorkAttribute)的工作单元过滤器。
/// 用于在 ASP.NET Core 控制器或方法上自动管理事务和 DbContext 的 SaveChanges。
/// </summary>
public class UnitOfWorkFilter : IAsyncActionFilter
{
/// <summary>
/// 获取应用于控制器或方法的 UnitOfWorkAttribute 特性。
/// 优先获取控制器上的特性,如果没有则获取方法上的特性。
/// </summary>
/// <param name="actionDesc">当前 Action 的描述信息。</param>
/// <returns>UnitOfWorkAttribute 实例或 null。</returns>
private static UnitOfWorkAttribute? GetUoWAttr(ActionDescriptor actionDesc)
{
var caDesc = actionDesc as ControllerActionDescriptor;
if (caDesc == null)
{
// 不是控制器 Action,直接返回 null
return null;
}
// 优先获取控制器上的 UnitOfWorkAttribute
var uowAttr = caDesc.ControllerTypeInfo
.GetCustomAttribute<UnitOfWorkAttribute>();
if (uowAttr != null)
{
return uowAttr;
}
else
{
// 如果控制器上没有,则获取方法上的 UnitOfWorkAttribute
return caDesc.MethodInfo
.GetCustomAttribute<UnitOfWorkAttribute>();
}
}

/// <summary>
/// 拦截 Action 执行过程,实现工作单元事务控制。
/// </summary>
/// <param name="context">Action 执行上下文。</param>
/// <param name="next">委托,执行下一个中间件或 Action。</param>
/// <returns>异步任务。</returns>
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 获取当前 Action 或控制器上的 UnitOfWorkAttribute
var uowAttr = GetUoWAttr(context.ActionDescriptor);
if (uowAttr == null)
{
// 未标记 UnitOfWorkAttribute,直接执行下一个中间件
await next();
return;
}
// 启用支持异步流的事务作用域
using TransactionScope txScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
List<DbContext> dbCtxs = new List<DbContext>();
// 遍历特性中声明的所有 DbContext 类型
foreach (var dbCtxType in uowAttr.DbContextTypes)
{
// 通过依赖注入获取 DbContext 实例
var sp = context.HttpContext.RequestServices;
DbContext dbCtx = (DbContext)sp.GetRequiredService(dbCtxType);
dbCtxs.Add(dbCtx);
}
// 执行 Action
var result = await next();
if (result.Exception == null)
{
// 如果没有异常,依次保存所有 DbContext 的更改
foreach (var dbCtx in dbCtxs)
{
await dbCtx.SaveChangesAsync();
}
// 提交事务
txScope.Complete();
}
// 如果有异常,事务会自动回滚
}
}
}

序列化转换器

1
2
3
4
5
6
// 配置 JSON 选项,添加自定义的 DateTime 序列化转换器
services.Configure<JsonOptions>(options =>
{
//设置时间格式。而非“2008-08-08T08:08:08”这样的格式,而是"2008-08-08 08:08:08"
options.JsonSerializerOptions.Converters.Add(new DateTimeJsonConverter("yyyy-MM-dd HH:mm:ss"));
});

会替换默认的 DateTime 序列化规则。
options.JsonSerializerOptions.Converters.Add(…)添加一个自定义的JsonConverter<DateTime&gt,DateTimeJsonConverter

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
namespace YU.Commons.JsonConverters
{
/// <summary>
/// 自定义的 DateTime 类型 JSON 序列化与反序列化转换器。
/// 支持指定日期时间格式进行序列化与反序列化。
/// </summary>
public class DateTimeJsonConverter : JsonConverter<DateTime>
{
/// <summary>
/// 日期时间格式字符串。
/// </summary>
private readonly string _dateFormatString;

/// <summary>
/// 默认构造函数,使用 "yyyy-MM-dd HH:mm:ss" 作为日期时间格式。
/// </summary>
public DateTimeJsonConverter()
{
_dateFormatString = "yyyy-MM-dd HH:mm:ss"; // 默认日期格式
}

/// <summary>
/// 构造函数,允许自定义日期时间格式。
/// </summary>
/// <param name="dateformatString">自定义的日期时间格式字符串</param>
public DateTimeJsonConverter(string dateformatString)
{
_dateFormatString = dateformatString; // 使用自定义日期格式
}

/// <summary>
/// 反序列化:将 JSON 字符串转换为 DateTime 对象。
/// </summary>
/// <param name="reader">JSON 读取器</param>
/// <param name="typeToConvert">要转换的类型</param>
/// <param name="options">序列化选项</param>
/// <returns>反序列化得到的 DateTime 对象</returns>
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? str = reader.GetString();
if (str == null)
{
// 如果 JSON 字符串为 null,则返回默认值
return default(DateTime);
}
else
{
// 将字符串解析为 DateTime 对象
return DateTime.Parse(str);
}
}

/// <summary>
/// 序列化:将 DateTime 对象转换为指定格式的 JSON 字符串。
/// </summary>
/// <param name="writer">JSON 写入器</param>
/// <param name="value">要序列化的 DateTime 值</param>
/// <param name="options">序列化选项</param>
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
// 按指定格式将 DateTime 写入为字符串
writer.WriteStringValue(value.ToString(_dateFormatString));
}
}
}

Cors

1
2
3
4
5
6
7
8
9
10
11
12
13
// 配置跨域资源共享(CORS)策略
services.AddCors(options =>
{
// 从配置中获取 CORS 设置
var corsOpt = configuration.GetSection("Cors").Get<CorsSettings>();
// 获取允许的来源列表
string[] urls = corsOpt.Origins;
// 添加默认策略,允许指定来源、任意头、任意方法,并允许携带凭据
options.AddDefaultPolicy(builder => builder.WithOrigins(urls)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
});

注意:AllowCredentials() 和 WithOrigins(…) 必须一起使用,不能搭配 AllowAnyOrigin(),否则会抛出异常。

日志服务

1
2
3
4
5
6
7
8
9
10
11
// 配置日志服务,使用 Serilog,日志输出到控制台和文件
services.AddLogging(builder =>
{
// 配置 Serilog 日志记录器
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File(initOptions.LogFilePath)
.CreateLogger();
// 将 Serilog 集成到日志系统
builder.AddSerilog();
});

验证器

1
2
3
4
// 获取应用程序主程序集,用于注册 FluentValidation 验证器
var applicationAssembly = typeof(UserModelValidator).Assembly;
// 注册所有验证器
services.AddValidatorsFromAssembly(applicationAssembly);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace YU.ASPNETCore
{
/// <summary>
/// 用户模型验证器,使用FluentValidation对UserValidation对象进行属性校验。
/// </summary>
public class UserModelValidator : AbstractValidator<UserValidation>
{
/// <summary>
/// 构造函数,定义了对UserValidation属性的校验规则。
/// </summary>
public UserModelValidator()
{
// 校验用户名不能为空
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("用户名不能为空");

// 校验年龄必须在0到100之间
RuleFor(x => x.Age)
.InclusiveBetween(0, 100)
.WithMessage("年龄必须在0到100之间");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace YU.DomainCommons.Models
{
/// <summary>
/// 用户验证信息模型。
/// </summary>
public class UserValidation
{
/// <summary>
/// 用户名。
/// </summary>
public string Name { get; set; }

/// <summary>
/// 用户年龄。
/// </summary>
public int Age { get; set; }
}
}

通过某个验证器类型 UserModelValidator 获取它所在的程序集(Assembly)。
这样就能知道“应用程序的主要代码程序集”,用于后续自动扫描和注册。

RabbitMQ

1
2
// 配置 RabbitMQ 事件总线选项绑定
services.Configure<IntegrationEventRabbitMQOptions>(configuration.GetSection("RabbitMQ"));

将配置文件中 “RabbitMQ” 节点的内容绑定到 IntegrationEventRabbitMQOptions 类,并注入到依赖注入容器中。

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
namespace YU.EventBus
{
/// <summary>
/// 表示用于配置RabbitMQ集成事件的选项。
/// </summary>
public class IntegrationEventRabbitMQOptions
{
/// <summary>
/// RabbitMQ 主机名或 IP 地址。
/// </summary>
public string HostName { get; set; }

/// <summary>
/// 交换机名称。
/// </summary>
public string ExchangeName { get; set; }

/// <summary>
/// 用于连接 RabbitMQ 的用户名(可选)。
/// </summary>
public string? UserName { get; set; }

/// <summary>
/// 用于连接 RabbitMQ 的密码(可选)。
/// </summary>
public string? Password { get; set; }
}
}

事件

1
2
// 注册事件总线服务
services.AddEventBus(initOptions.EventBusQueueName, assemblies);

注册事件总线(EventBus)服务,并初始化事件处理器扫描、队列名称等设置。

initOptions.EventBusQueueName
当前服务的事件队列名,例如 “user-service”。
不同服务应该有不同的队列名,防止消费互串。

assemblies
所有需要扫描的程序集(Assembly[]),用于从中找出所有实现了 IIntegrationEventHandler<T> 的事件处理器。

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
namespace YU.EventBus
{
/// <summary>
/// 扩展 IServiceCollection,提供事件总线(EventBus)相关的依赖注入注册方法。
/// </summary>
public static class ServicesCollectionExtensions
{
/// <summary>
/// 注册事件总线服务,自动扫描指定程序集中的事件处理器类型。
/// </summary>
/// <param name="services">依赖注入服务集合。</param>
/// <param name="queueName">RabbitMQ 队列名称。</param>
/// <param name="assemblies">要扫描的程序集列表。</param>
/// <returns>服务集合本身,便于链式调用。</returns>
public static IServiceCollection AddEventBus(this IServiceCollection services, string queueName,
params Assembly[] assemblies)
{
// 将 params 形式的程序集数组转换为 List 形式,调用重载方法
return AddEventBus(services, queueName, assemblies.ToList());
}

/// <summary>
/// 注册事件总线服务,自动扫描指定程序集中的事件处理器类型。
/// </summary>
/// <param name="services">依赖注入服务集合。</param>
/// <param name="queueName">RabbitMQ 队列名称。</param>
/// <param name="assemblies">要扫描的程序集集合。</param>
/// <returns>服务集合本身,便于链式调用。</returns>
public static IServiceCollection AddEventBus(this IServiceCollection services, string queueName,
IEnumerable<Assembly> assemblies)
{
// 用于存储所有扫描到的事件处理器类型
List<Type> eventHandlers = new List<Type>();
// 遍历每个程序集
foreach (var asm in assemblies)
{
// 查找所有非抽象且实现了 IIntegrationEventHandler 接口的类型
var types = asm.GetTypes().Where(t => t.IsAbstract == false && t.IsAssignableTo(typeof(IIntegrationEventHandler)));
// 添加到事件处理器类型列表
eventHandlers.AddRange(types);
}
// 调用重载方法,注册事件处理器类型
return AddEventBus(services, queueName, eventHandlers);
}

/// <summary>
/// 注册事件总线服务,并将指定的事件处理器类型注册到依赖注入容器。
/// </summary>
/// <param name="services">依赖注入服务集合。</param>
/// <param name="queueName">RabbitMQ 队列名称。</param>
/// <param name="eventHandlerTypes">事件处理器类型集合。</param>
/// <returns>服务集合本身,便于链式调用。</returns>
public static IServiceCollection AddEventBus(this IServiceCollection services, string queueName,
IEnumerable<Type> eventHandlerTypes)
{
// 遍历所有事件处理器类型
foreach (Type type in eventHandlerTypes)
{
// 将事件处理器类型注册为 Scoped 生命周期
services.AddScoped(type, type);
}
// 注册 IEventBus 单例服务
services.AddSingleton<IEventBus>(sp =>
{
// 获取 RabbitMQ 配置选项
var optionMQ = sp.GetRequiredService<IOptions<IntegrationEventRabbitMQOptions>>().Value;
// 创建 RabbitMQ 连接工厂
var factory = new ConnectionFactory()
{
HostName = optionMQ.HostName, // 设置主机名
DispatchConsumersAsync = true // 启用异步消费者
};
// 如果配置了用户名,则设置用户名
if (optionMQ.UserName != null)
{
factory.UserName = optionMQ.UserName;
}
// 如果配置了密码,则设置密码
if (optionMQ.Password != null)
{
factory.Password = optionMQ.Password;
}
// 创建 RabbitMQ 持久连接对象
RabbitMQConnection mqConnection = new RabbitMQConnection(factory);
// 获取服务作用域工厂,用于事件处理器的依赖注入
var serviceScopeFactory = sp.GetRequiredService<IServiceScopeFactory>();
// 创建 RabbitMQEventBus 实例
var eventBus = new RabbitMQEventBus(mqConnection, serviceScopeFactory, optionMQ.ExchangeName, queueName);
// 返回事件总线实例
return eventBus;
});
// 返回服务集合本身
return services;
}
}
}

IIntegrationEventHandler定义集成事件处理程序的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace YU.EventBus
{
/// <summary>
/// 定义集成事件处理程序的接口。
/// </summary>
public interface IIntegrationEventHandler
{
/// <summary>
/// 处理集成事件的方法。
/// </summary>
/// <param name="eventName">事件名称。</param>
/// <param name="eventData">事件数据(序列化后的字符串)。</param>
/// <returns>表示异步操作的任务。</returns>
Task Handle(string eventName, string eventData);
}
}

IntegrationEventRabbitMQOptions接口自定义用于配置RabbitMQ集成事件的选项。

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
namespace YU.EventBus
{
/// <summary>
/// 表示用于配置RabbitMQ集成事件的选项。
/// </summary>
public class IntegrationEventRabbitMQOptions
{
/// <summary>
/// RabbitMQ 主机名或 IP 地址。
/// </summary>
public string HostName { get; set; }

/// <summary>
/// 交换机名称。
/// </summary>
public string ExchangeName { get; set; }

/// <summary>
/// 用于连接 RabbitMQ 的用户名(可选)。
/// </summary>
public string? UserName { get; set; }

/// <summary>
/// 用于连接 RabbitMQ 的密码(可选)。
/// </summary>
public string? Password { get; set; }
}
}

Redis

1
2
3
4
5
6
7
8
9
10
11
// 从配置中获取 Redis 连接字符串
string redisConnStr = configuration.GetValue<string>("Redis:ConnStr");
// 创建 Redis 连接多路复用器
IConnectionMultiplexer redisConnMultiplexer = ConnectionMultiplexer.Connect(redisConnStr);
// 注册 Redis 连接为单例服务
services.AddSingleton(typeof(IConnectionMultiplexer), redisConnMultiplexer);
// 配置转发头部选项,支持所有转发头
services.Configure<ForwardedHeadersOptions>(Options =>
{
Options.ForwardedHeaders = ForwardedHeaders.All;
});