yzk学英语项目-启动方式及封装的类库说明

175 阅读6分钟

Zack库说明

Nuget包说明
Zack.ASPNETCoreDistributedCacheHelper分布式缓存帮助类
MemoryCacheHelper内存缓存帮助类
UnitOfWorkFilter工作单元筛选器
Zack.CommonsValidators文件夹FluentValidation的扩展类
LoggerExtensions使用FormattableString简化日志的代码
ModuleInitializerExtensions把服务注册代码放到各自的项目中
Zack.DomainCommonsIAggregateRoot聚合根标识接口
BaseEntity、AggregateRootEntity领域事件的发布
MultilingualString多语言值对象
Zack.EventBus集成事件总线
Zack.InfrastructureBaseDbContext领域事件的发布
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;
        }
    }