Zack库说明
| Nuget包 | 类 | 说明 |
|---|---|---|
| Zack.ASPNETCore | DistributedCacheHelper | 分布式缓存帮助类 |
| MemoryCacheHelper | 内存缓存帮助类 | |
| UnitOfWorkFilter | 工作单元筛选器 | |
| Zack.Commons | Validators文件夹 | FluentValidation的扩展类 |
| LoggerExtensions | 使用FormattableString简化日志的代码 | |
| ModuleInitializerExtensions | 把服务注册代码放到各自的项目中 | |
| Zack.DomainCommons | IAggregateRoot | 聚合根标识接口 |
| BaseEntity、AggregateRootEntity | 领域事件的发布 | |
| MultilingualString | 多语言值对象 | |
| Zack.EventBus | 集成事件总线 | |
| Zack.Infrastructure | BaseDbContext | 领域事件的发布 |
| EFCoreInitializerHelper | 上下文的自动化注册。自动为所有的DbContext注册连接配置 | |
| ExpressionHelper | 简化值对象的相等性比较 | |
| MediatorExtensions | 领域事件的注册 | |
| MultilingualStringEFCoreExtensions | 多语言值对象的配置 | |
| Zack.JWT | 使用JWT实现登录令牌 |
Zack.DomainCommons
类、接口说明
| 类名、接口 | 说明 |
|---|---|
| IHasCreationTime | 多一个创建时间的属性 |
| IEntity | 多一个Guid类型的Id顺序 |
| IDomainEvents | 定义了领域事件的简单用法 |
| BaseEntity | 实现IEntity、IDomainEvents,具有Id属性,实体类中领域事件的操作 |
Zack.Commons
服务自注册
每个项目中都可以自己写一些实现了IModuleInitializer接口的类,在其中注册自己需要的服务,这样避免所有内容到入口项目中注册
public static class ModuleInitializerExtensions
{
public static IServiceCollection RunModuleInitializers(this IServiceCollection services,
IEnumerable<Assembly> assemblies)
{
foreach(var asm in assemblies)
{
Type[] types = asm.GetTypes();
var moduleInitializerTypes = types.Where(t => !t.IsAbstract && typeof(IModuleInitializer).IsAssignableFrom(t));
foreach(var implType in moduleInitializerTypes)
{
var initializer = (IModuleInitializer?)Activator.CreateInstance(implType);
if (initializer == null)
{
throw new ApplicationException($"Cannot create ${implType}");
}
initializer.Initialize(services);
}
}
return services;
}
}
Zack.Infrastructure
自动为所有的DbContext注册连接配置
public static class EFCoreInitializerHelper
{
public static IServiceCollection AddAllDbContexts(this IServiceCollection services, Action<DbContextOptionsBuilder> builder,
IEnumerable<Assembly> assemblies)
{
//AddDbContextPool不支持DbContext注入其他对象,而且使用不当有内存暴涨的问题,因此不用AddDbContextPool
Type[] types = new Type[] { typeof(IServiceCollection), typeof(Action<DbContextOptionsBuilder>), typeof(ServiceLifetime), typeof(ServiceLifetime) };
var methodAddDbContext = typeof(EntityFrameworkServiceCollectionExtensions)
.GetMethod(nameof(EntityFrameworkServiceCollectionExtensions.AddDbContext), 1, types);
foreach (var asmToLoad in assemblies)
{
Type[] typesInAsm = asmToLoad.GetTypes();
//Register DbContext
//GetTypes() include public/protected ones
//GetExportedTypes only include public ones
//so that XXDbContext in Agrregation can be internal to keep insulated
foreach (var dbCtxType in typesInAsm
.Where(t => !t.IsAbstract && typeof(DbContext).IsAssignableFrom(t)))
{
//similar to serviceCollection.AddDbContextPool<ECDictDbContext>(opt=>new DbContextOptionsBuilder(dbCtxOpt));
var methodGenericAddDbContext = methodAddDbContext.MakeGenericMethod(dbCtxType);
methodGenericAddDbContext.Invoke(null, new object[] { services, builder, ServiceLifetime.Scoped, ServiceLifetime.Scoped });
}
}
return services;
}
}
运行项目
分别让FileService.WebAPI、IdentityService.WebAPI、Listening.Admin.WebAPI、Listening.Main.WebAPI、MediaEncoder.WebAPI、SearchService.WebAPI运行在44339、44392、44352、44375、44353、44310端口下。
4、配置Nginx代理
目标:访问https://localhost/IdentityService/来访问IdentityService的接口,其他服务是一样的,前端就可以通过统一的端口来访问后端服务
配置Nginx来反向代理后端的接口,以IdentityService服务为例,在Nginx的nginx.conf文件中的server节点下增加:
# proxy_set_header是用来方便我们在ASP.NET Core中获取客户端IP地址等使用的,需要配合ForwardedHeaders中间件使用。
location /IdentityService/ {
proxy_pass https://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;
}
5、在环境变量中配置数据库连接字符串
CommonInitializer中的初始化代码是设定从“DefaultDB:ConnStr”这个路径中读取数据库的连接字符串,因此请在环境变量中配置名字为DefaultDB:ConnStr的数据库连接字符串,然后在各个项目中运行EF Core数据库迁移来生成数据库表。
6、创建T_Configs表
在数据库中增加一个名字为T_Configs的表,并且在表中增加配置项(用数据库做中心配置服务器)
7、设置多个启动项目
在【解决方案资源管理器】中的根节点上单击右键,选择【设置启动项目】,选中【多个启动项目】,把名字以WebAPI结尾的几个项目的【操作】都设置为【启动】
8、创建初始用户
在IdentityService.WebAPI的LoginController中编写了一个自动创建默认角色和管理员的操作方法CreateWorld,我们直接访问它就可以创建出用户名为admin、密码为123456的管理员。生产环境一定要删除这个方法
9、运行前端项目
再运行前端项目,ListeningAdminUI、ListeningMainUI分别是管理后台和网站前台的前端项目。我们在命令提示符中,分别进入这两个文件夹,然后依次执行yarn dev和yarn就能运行这两个前端项目了。如果两个前端项目同时运行,那么它们的端口就会不同,我们需要把它们两个的网站根目录都要配置到T_Configs的Cors配置项中
通用类库
一些项目初始化的代码放到这里,因为不是所有项目通用的东西,只是我这个项目里通用的东西,所以这个项目不妨到Commons解决方案文件夹下。
因为所有的项目都用到了领域事件、集成事件、中心配置服务器、JWT、工作单元、CORS、FluentValidation等,因此作者开发了CommonInitializer项目用来复用这些组件的初始化代码
CommonInitializer是一个类库项目,在类库项目中不能直接使用WebApplicationBuilder、IApplicationBuilder、IWebHostEnvironment等类,不能通过Nuget安装,请在csproj中添加
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
WebApplicationBuilder的扩展
连接数据库读取配置表
ConfigureDbConfiguration:取中心配置服务器的配置,这个DefaultDB:ConnStr是从正式的电脑环境变量中读取
public static class WebApplicationBuilderExtensions
{
public static void ConfigureDbConfiguration(this WebApplicationBuilder builder)
{
builder.Host.ConfigureAppConfiguration((hostCtx, configBuilder) =>
{
//不能使用ConfigureAppConfiguration中的configBuilder去读取配置,否则就循环调用了,因此这里直接自己去读取配置文件
//var configRoot = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
//string connStr = configRoot.GetValue<string>("DefaultDB:ConnStr");
string connStr = builder.Configuration.GetValue<string>("DefaultDB:ConnStr");
configBuilder.AddDbConfiguration(() => new SqlConnection(connStr), reloadOnChange: true, reloadInterval: TimeSpan.FromSeconds(5));
});
}
//...
}
上端调用
builder.ConfigureDbConfiguration();
其他服务(都在ConfigureExtraServices中进行配置),下述都是其他服务的配置
public static void ConfigureExtraServices(this WebApplicationBuilder builder, InitializerOptions initOptions)
{
IServiceCollection services = builder.Services;
IConfiguration configuration = builder.Configuration;
//配置其他服务...
}
需要接收一个初始化配置InitializerOptions
public class InitializerOptions
{
public string LogFilePath { get; set; }
//用于EventBus的QueueName,因此要维持“同一个项目值保持一直,不同项目不能冲突”
public string EventBusQueueName { get; set; }
}
服务批量自注册
//服务批量自注册
var assemblies = ReflectionHelper.GetAllReferencedAssemblies();
services.RunModuleInitializers(assemblies);
为DbContext注册连接配置
//为DbContext注册连接配置
services.AddAllDbContexts(ctx =>
{
//连接字符串如果放到appsettings.json中,会有泄密的风险
//如果放到UserSecrets中,每个项目都要配置,很麻烦
//因此这里推荐放到环境变量中。
string connStr = configuration.GetValue<string>("DefaultDB:ConnStr");
ctx.UseSqlServer(connStr);
}, assemblies);
鉴权授权
开启【Authorize】按钮、jwt的配置。
认证的配置、jwt初始化
//只要需要校验Authentication报文头的地方(非IdentityService.WebAPI项目)也需要启用这些
//IdentityService项目还需要启用AddIdentityCore
builder.Services.AddAuthorization();
builder.Services.AddAuthentication();
JWTOptions jwtOpt = configuration.GetSection("JWT").Get<JWTOptions>();
builder.Services.AddJWTAuthentication(jwtOpt);
//启用Swagger中的【Authorize】按钮。这样就不用每个项目的AddSwaggerGen中单独配置了
builder.Services.Configure<SwaggerGenOptions>(c =>
{
c.AddAuthenticationHeader();
});
services.Configure<JWTOptions>(configuration.GetSection("JWT"));
注册MediatR相关类
services.AddMediatR(assemblies);
配置工作单元的Filter(UnitOfWorkFilter)
//现在不用手动AddMVC了,因此把文档中的services.AddMvc(options =>{})改写成Configure<MvcOptions>(options=> {})这个问题很多都类似
services.Configure<MvcOptions>(options =>
{
options.Filters.Add<UnitOfWorkFilter>();
});
设置时间格式
services.Configure<JsonOptions>(options =>
{
//设置时间格式。而非“2008-08-08T08:08:08”这样的格式
options.JsonSerializerOptions.Converters.Add(new DateTimeJsonConverter("yyyy-MM-dd HH:mm:ss"));
});
跨域配置
需要先创建实体类
public class CorsSettings
{
public string[] Origins { get; set; }
}
其他服务中
services.AddCors(options =>
{
//更好的在Program.cs中用绑定方式读取配置的方法:https://github.com/dotnet/aspnetcore/issues/21491
//不过比较麻烦。
var corsOpt = configuration.GetSection("Cors").Get<CorsSettings>();
string[] urls = corsOpt.Origins;
options.AddDefaultPolicy(builder => builder.WithOrigins(urls)
.AllowAnyMethod().AllowAnyHeader().AllowCredentials());
}
);
日志配置
services.AddLogging(builder =>
{
Log.Logger = new LoggerConfiguration()
// .MinimumLevel.Information().Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File(initOptions.LogFilePath)
.CreateLogger();
builder.AddSerilog();
});
FluentValidation校验的配置
services.AddFluentValidation(fv =>
{
fv.RegisterValidatorsFromAssemblies(assemblies);
});
RabbitMQ
services.Configure<IntegrationEventRabbitMQOptions>(configuration.GetSection("RabbitMQ"));
事件总线
services.AddEventBus(initOptions.EventBusQueueName, assemblies);
Redis的配置
//Redis的配置
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;
});
启用中间件
事件总线、跨域、鉴权授权
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder UseZackDefault(this IApplicationBuilder app)
{
app.UseEventBus();//初始化EventBus
app.UseCors();//启用Cors
app.UseForwardedHeaders();//启用Nginx
//app.UseHttpsRedirection();//不能与ForwardedHeaders很好的工作,而且webapi项目也没必要配置这个
app.UseAuthentication();
app.UseAuthorization();
return app;
}
}
分层项目数据库迁移
DbContext的配置,给数据库迁移用的
public static class DbContextOptionsBuilderFactory
{
public static DbContextOptionsBuilder<TDbContext> Create<TDbContext>()
where TDbContext : DbContext
{
var connStr = Environment.GetEnvironmentVariable("DefaultDB:ConnStr");
var optionsBuilder = new DbContextOptionsBuilder<TDbContext>();
//optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=YouzackVNextDB;User ID=sa;Password=dLLikhQWy5TBz1uM;");
optionsBuilder.UseSqlServer(connStr);
return optionsBuilder;
}
}