网站结构说明
学习杨中科老师开源项目在线英语网站微服务
- 功能:听力练习。
- 业务概念:类别(Category)、专辑(Album)、片段(Episode)。
- 听力原文字幕文件查看。
- 网站后台允许进行资源的CRUD。
- 其他格式的音频的追踪在部分浏览器上有问题,统一用M4A。
- 音频文件放到单独的文件服务器上。
- 原文的搜素。

项目结构说明
为了便于管理,我们把不同服务的项目放到不同的解决方案文件夹下,解决方案文件夹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;
location / { proxy_pass http://127.0.0.1:8080; 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 { public static class ApplicationBuilderExtensions { public static IApplicationBuilder UseZackDefault(this IApplicationBuilder app) { app.UseEventBus(); app.UseCors(); app.UseForwardedHeaders(); 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 { public static class ApplicationBuilderExtensions { public static IApplicationBuilder UseEventBus(this IApplicationBuilder appBuilder) { object? eventBus = appBuilder.ApplicationServices.GetService(typeof(IEventBus)); if (eventBus == null) { throw new ApplicationException("找不到IEventBus的实现,请确保已正确注册EventBus服务。"); } 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 { public interface IEventBus { void publish(string eventName, object? eventData);
void Subscribe(string eventName, Type handlerType);
void Unsubscribe(string eventName, Type handlerType); } }
|
app.UseCors();
app.UseForwardedHeaders();
app.UseAuthentication();
app.UseAuthorization();
是Microsoft.AspNetCore.BuilderNuGet包,安装后可直接配置扩展方法,后面的项目可以直接引用。
CorsSettings类用于配置 跨域资源共享(CORS)
1 2 3 4 5 6 7 8 9 10 11 12 13
| namespace Commonlnitializer { public class CorsSettings { 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 { public static class DbContextOptionsBuilderFactory { 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."); } var optionsBuilder = new DbContextOptionsBuilder<TDbContext>(); 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 { public class InitializerOptions { public string LogFilePath { get; set; }
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 { public static class WebApplicationBuilderExtensions { public static void ConfigureDbConfiguration(this WebApplicationBuilder builder) { builder.Host.ConfigureAppConfiguration((hostCtx, configBuilder) => { string connStr = builder.Configuration.GetValue<string>("DefaultDB:ConnStr"); configBuilder.AddDbConfiguration(() => new SqlConnection(connStr), reloadOnChange: true, reloadInterval: TimeSpan.FromSeconds(5)); }); }
public static void ConfigureExtraServices(this WebApplicationBuilder builder, InitializerOptions initOptions) { IServiceCollection services = builder.Services; IConfiguration configuration = builder.Configuration; var assemblies = ReflectionHelper.GetAllReferencedAssemblies(); services.RunModuleInitializers(assemblies); services.AddAllDbContexts(ctx => { string connStr = configuration.GetValue<string>("DefaultDB:ConnStr"); ctx.UseSqlServer(connStr); }, assemblies);
builder.Services.AddAuthentication(); builder.Services.AddAuthorization(); JWTOptions jwtOpt = configuration.GetSection("JWT").Get<JWTOptions>(); builder.Services.AddJWTAuthentication(jwtOpt); builder.Services.Configure<SwaggerGenOptions>(c => { c.AddAuthenticationHeader(); });
services.AddMediatR(assemblies); services.Configure<MvcOptions>(options => { options.Filters.Add<UnitOfWorkFilter>(); }); services.Configure<JsonOptions>(options => { options.JsonSerializerOptions.Converters.Add(new DateTimeJsonConverter("yyyy-MM-dd HH:mm:ss")); });
services.AddCors(options => { var corsOpt = configuration.GetSection("Cors").Get<CorsSettings>(); string[] urls = corsOpt.Origins; options.AddDefaultPolicy(builder => builder.WithOrigins(urls) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials()); }); services.AddLogging(builder => { Log.Logger = new LoggerConfiguration() .WriteTo.Console() .WriteTo.File(initOptions.LogFilePath) .CreateLogger(); builder.AddSerilog(); }); var applicationAssembly = typeof(UserModelValidator).Assembly; services.AddValidatorsFromAssembly(applicationAssembly); services.Configure<JWTOptions>(configuration.GetSection("JWT")); services.Configure<IntegrationEventRabbitMQOptions>(configuration.GetSection("RabbitMQ")); services.AddEventBus(initOptions.EventBusQueueName, assemblies);
string redisConnStr = configuration.GetValue<string>("Redis:ConnStr"); IConnectionMultiplexer redisConnMultiplexer = ConnectionMultiplexer.Connect(redisConnStr); services.AddSingleton(typeof(IConnectionMultiplexer), redisConnMultiplexer); services.Configure<ForwardedHeadersOptions>(Options => { Options.ForwardedHeaders = ForwardedHeaders.All; }); } } }
|
我们创建了一个对WebApplicationBuilder的扩展方法,读取数据库配置源。
将数据库作为配置源,并将其配置加载到 WebApplicationBuilder.Configuration 中,支持自动刷新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
public static void ConfigureDbConfiguration(this WebApplicationBuilder builder) { builder.Host.ConfigureAppConfiguration((hostCtx, configBuilder) => { string connStr = builder.Configuration.GetValue<string>("DefaultDB:ConnStr"); 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配置了配置应用所需的额外服务,包括数据库、认证、授权、日志、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); 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();
JWTOptions jwtOpt = configuration.GetSection("JWT").Get<JWTOptions>();
builder.Services.AddJWTAuthentication(jwtOpt);
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 { public class JWTOptions { public string Issuer { get; set; }
public string Audience { get; set; }
public string Key { get; set; }
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 { public static class AuthenticationExtensions { public static AuthenticationBuilder AddJWTAuthentication(this IServiceCollection services, JWTOptions jwtOpt) { return services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(x => { 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 { public static class SwaggerGenOptionsExtensions { public static void AddAuthenticationHeader(this SwaggerGenOptions c) { c.AddSecurityDefinition("Authorization", new OpenApiSecurityScheme { Description = "Authorization header .\r\n Example:'Bearer 12345abcdef'", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Scheme = "Authorization" });
c.AddSecurityRequirement(new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id="Authorization" }, Scheme = "oauth2", Name="Authorization", In=ParameterLocation.Header, }, new List<string>() } }); } } }
|
1 2
| services.Configure<JWTOptions>(configuration.GetSection("JWT"));
|
这行代码的作用是将应用配置文件中的 JWT 配置节绑定到 JWTOptions 类型,并注册到依赖注入容器,方便后续通过依赖注入获取配置信息。
1 2
| 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 { public static class MediatorExtensions { public static IServiceCollection AddMediatR(this IServiceCollection services, IEnumerable<Assembly> assemblies) { return services.AddMediatR(assemblies.ToArray()); }
public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx) { 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());
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 { public interface IDomainEvents { IEnumerable<INotification> GetDomainEvents();
void AddDomainEvent(INotification eventItem);
void AddDomainEventIfAbsent(INotification eventItem);
void ClearDomainEvents(); } }
|
筛选器(过滤器)
1 2 3 4 5
| 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 { public class UnitOfWorkFilter : IAsyncActionFilter { private static UnitOfWorkAttribute? GetUoWAttr(ActionDescriptor actionDesc) { var caDesc = actionDesc as ControllerActionDescriptor; if (caDesc == null) { return null; } var uowAttr = caDesc.ControllerTypeInfo .GetCustomAttribute<UnitOfWorkAttribute>(); if (uowAttr != null) { return uowAttr; } else { return caDesc.MethodInfo .GetCustomAttribute<UnitOfWorkAttribute>(); } }
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var uowAttr = GetUoWAttr(context.ActionDescriptor); if (uowAttr == null) { await next(); return; } using TransactionScope txScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); List<DbContext> dbCtxs = new List<DbContext>(); foreach (var dbCtxType in uowAttr.DbContextTypes) { var sp = context.HttpContext.RequestServices; DbContext dbCtx = (DbContext)sp.GetRequiredService(dbCtxType); dbCtxs.Add(dbCtx); } var result = await next(); if (result.Exception == null) { foreach (var dbCtx in dbCtxs) { await dbCtx.SaveChangesAsync(); } txScope.Complete(); } } } }
|
序列化转换器
1 2 3 4 5 6
| services.Configure<JsonOptions>(options => { options.JsonSerializerOptions.Converters.Add(new DateTimeJsonConverter("yyyy-MM-dd HH:mm:ss")); });
|
会替换默认的 DateTime 序列化规则。
options.JsonSerializerOptions.Converters.Add(…)添加一个自定义的JsonConverter<DateTime>,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 { public class DateTimeJsonConverter : JsonConverter<DateTime> { private readonly string _dateFormatString;
public DateTimeJsonConverter() { _dateFormatString = "yyyy-MM-dd HH:mm:ss"; }
public DateTimeJsonConverter(string dateformatString) { _dateFormatString = dateformatString; }
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { string? str = reader.GetString(); if (str == null) { return default(DateTime); } else { return DateTime.Parse(str); } }
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString(_dateFormatString)); } } }
|
Cors
1 2 3 4 5 6 7 8 9 10 11 12 13
| services.AddCors(options => { 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
| services.AddLogging(builder => { Log.Logger = new LoggerConfiguration() .WriteTo.Console() .WriteTo.File(initOptions.LogFilePath) .CreateLogger(); builder.AddSerilog(); });
|
验证器
1 2 3 4
| 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 { public class UserModelValidator : AbstractValidator<UserValidation> { public UserModelValidator() { RuleFor(x => x.Name) .NotEmpty() .WithMessage("用户名不能为空");
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 { public class UserValidation { public string Name { get; set; }
public int Age { get; set; } } }
|
通过某个验证器类型 UserModelValidator 获取它所在的程序集(Assembly)。
这样就能知道“应用程序的主要代码程序集”,用于后续自动扫描和注册。
RabbitMQ
1 2
| 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 { public class IntegrationEventRabbitMQOptions { public string HostName { get; set; }
public string ExchangeName { get; set; }
public string? UserName { get; set; }
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 { public static class ServicesCollectionExtensions { public static IServiceCollection AddEventBus(this IServiceCollection services, string queueName, params Assembly[] assemblies) { return AddEventBus(services, queueName, assemblies.ToList()); }
public static IServiceCollection AddEventBus(this IServiceCollection services, string queueName, IEnumerable<Assembly> assemblies) { List<Type> eventHandlers = new List<Type>(); foreach (var asm in assemblies) { var types = asm.GetTypes().Where(t => t.IsAbstract == false && t.IsAssignableTo(typeof(IIntegrationEventHandler))); eventHandlers.AddRange(types); } return AddEventBus(services, queueName, eventHandlers); }
public static IServiceCollection AddEventBus(this IServiceCollection services, string queueName, IEnumerable<Type> eventHandlerTypes) { foreach (Type type in eventHandlerTypes) { services.AddScoped(type, type); } services.AddSingleton<IEventBus>(sp => { var optionMQ = sp.GetRequiredService<IOptions<IntegrationEventRabbitMQOptions>>().Value; 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; } RabbitMQConnection mqConnection = new RabbitMQConnection(factory); var serviceScopeFactory = sp.GetRequiredService<IServiceScopeFactory>(); 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 { public interface IIntegrationEventHandler { 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 { public class IntegrationEventRabbitMQOptions { public string HostName { get; set; }
public string ExchangeName { get; set; }
public string? UserName { get; set; }
public string? Password { get; set; } } }
|
Redis
1 2 3 4 5 6 7 8 9 10 11
| string redisConnStr = configuration.GetValue<string>("Redis:ConnStr");
IConnectionMultiplexer redisConnMultiplexer = ConnectionMultiplexer.Connect(redisConnStr);
services.AddSingleton(typeof(IConnectionMultiplexer), redisConnMultiplexer);
services.Configure<ForwardedHeadersOptions>(Options => { Options.ForwardedHeaders = ForwardedHeaders.All; });
|