跟我学: 使用 fireasy 搭建 asp.net core 项目系列之三 —— 配置

时间:2021-09-13 15:19:26

==== 目录 ====

跟我学: 使用 fireasy 搭建 asp.net core 项目系列之一 —— 开篇

跟我学: 使用 fireasy 搭建 asp.net core 项目系列之二 —— 准备

跟我学: 使用 fireasy 搭建 asp.net core 项目系列之三 —— 配置

其实从 mvc5 迁移到 core,项目的差异化主要就体现在配置上。在 core 的世界里,万物都依赖于 ioc,因此,对于初学 core 的人来说,首先要搞懂的一个知识点就是 ioc。

fireasy 支持 core 项目,因此在配置上也有一些特殊的地方。

    一、appsettings.json

appsettings.json 是 core 项目的标准配置文件,你当然可以使用其他的文件名来存储,但应注意要在 Program.cs 中手动指定文件路径。

        public static IWebHost BuildWebHost(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("hosting.json", optional: true)
.AddCommandLine(args)
.Build(); return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>()
.Build();
}

fireasy 将日志、缓存、订阅发布、数据库连接、ioc等全放在 appsettings.json 里,以下是一个完整的配置实例:

{
"fireasy": {
"dataGlobal": { //数据层的全局设置
"options": {
"attachQuote": true //是否在sql语句中自动附加逃逸符,即[]、``等
}
},
"dataInstances": { //数据库连接实例
"default": "sqlite", //默认使用的实例,如果没有指定,则使用 settings 中的第一项
"settings": {
"sqlite": {
"providerType": "SQLite",
"connectionString": "Data source=|datadirectory|../../../../database/zero.db3;version=3;tracking=true"
},
"mysql": {
"providerType": "MySql",
"connectionString": "Data Source=localhost;database=zero;User Id=root;password=faib;pooling=true;charset=utf8;Treat Tiny As Boolean=false;tracking=true"
},
"sqlserver": {
"providerType": "MsSql",
"connectionString": "data source=.;user id=sa;password=123;initial catalog=zero;tracking=true"
},
"oracle": {
"providerType": "Oracle",
"connectionString": "Data Source=orcl;User ID=ZERO;Password=123;tracking=true"
}
}
},
"dataConverters": { //数据转换器
"settings": [
{
"sourceType": "Fireasy.Data.CodedData, Fireasy.Data",
"converterType": "Fireasy.Zero.Infrastructure.CodedDataConverter, Fireasy.Zero.Infrastructure"
}
]
},
"loggings": { //日志组件
"settings": {
"db": {
"type": "Fireasy.Zero.Services.Impls.LogService, Fireasy.Zero.Services"
}
}
},
"cachings": { //缓存组件
"settings": {
"redis": {
"type": "Fireasy.Redis.CacheManager, Fireasy.Redis",
"config": {
"defaultDb": 1,
"password": "test",
"host": [
{
"server": "localhost"
}
]
}
}
}
},
"subscribers": { //订阅发布
"default": "rabbit", //默认使用的实例
"settings": {
"redis": { //使用redis
"type": "Fireasy.Redis.RedisSubscribeManager, Fireasy.Redis",
"config": {
"host": [
{
"server": "localhost"
}
]
}
},
"rabbit": { //使用rabbit
"type": "Fireasy.RabbitMQ.SubscribeManager, Fireasy.RabbitMQ",
"config": {
"userName": "test",
"password": "test",
"server": "amqp://localhost:5672"
}
}
}
},
"containers": { //ioc配置
"settings": {
"default": [
{
"assembly": "Fireasy.Zero.Services" //整个程序集导入
},
{
"serviceType": "Fireasy.Zero.Infrastructure.IFileStorageProvider, Fireasy.Zero.Infrastructure",
"implementationType": "Fireasy.Zero.Infrastructure.FileServerStorageProvider, Fireasy.Zero.Infrastructure"
}
]
}
}
}
}

    二、基本配置

定位到 Fireasy.Zero.Web 项目的 Startup.cs 文件,找到 ConfigureServices 方法,将以下代码加入到方法里面:

        // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddFireasy(Configuration)
.AddIoc(ContainerUnity.GetContainer()); //添加 appsettings.json 里的 ioc 配置 services.AddMvc()
.ConfigureFireasyMvc() // fireasy.web.mvc 相关的配置
.ConfigureEasyUI(); //easyui 相关的配置
}

扩展方法 AddFireasy 为的是将 appsettings.json 中的相关配置加载到到环境中。这里它的原理可以多给大家说一下,以便了解它是如何工作的。查看 AddFireasy 方法,源码如下:

        public static IServiceCollection AddFireasy(this IServiceCollection services, IConfiguration configuration, Action<Fireasy.Common.CoreOptions> setupAction = null)
{
ConfigurationUnity.Bind(Assembly.GetCallingAssembly(), configuration, services); var options = new Fireasy.Common.CoreOptions();
setupAction?.Invoke(options); return services;
}

查看 ConfigurationUnity.Bind 方法:

        public static void Bind(Assembly callAssembly, IConfiguration configuration, IServiceCollection services = null)
{
var assemblies = new List<Assembly>(); FindReferenceAssemblies(callAssembly, assemblies); foreach (var assembly in assemblies)
{
var type = assembly.GetType("Microsoft.Extensions.DependencyInjection.ConfigurationBinder");
if (type != null)
{
var method = type.GetMethod("Bind", BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(IServiceCollection), typeof(IConfiguration) }, null);
if (method != null)
{
method.Invoke(null, new object[] { services, configuration });
}
}
} assemblies.Clear();
}

它实际上是遍列当前程序集所引用的所有程序集,查看每个程序集下的特定类 Microsoft.Extensions.DependencyInjection.ConfigurationBinder,然后进行反射调用 Bind 方法。因此,每一个 fireasy 的类库都会有这样一个类,来接收 AddFireasy 的统一配置。

比如 Fireasy.Common 下的这个类的内容为:

internal class ConfigurationBinder
{
internal static void Bind(IServiceCollection services, IConfiguration configuration)
{
ConfigurationUnity.Bind<LoggingConfigurationSection>(configuration);
ConfigurationUnity.Bind<CachingConfigurationSection>(configuration);
ConfigurationUnity.Bind<ContainerConfigurationSection>(configuration);
ConfigurationUnity.Bind<SubscribeConfigurationSection>(configuration);
ConfigurationUnity.Bind<ImportConfigurationSection>(configuration); if (services != null)
{
services.AddLogger().AddCaching().AddSubscriber();
}
}
}

比如 Fireasy.Data 下的这个类的内容为:

internal class ConfigurationBinder
{
internal static void Bind(IServiceCollection services, IConfiguration configuration)
{
ConfigurationUnity.Bind<GlobalConfigurationSection>(configuration);
ConfigurationUnity.Bind<ProviderConfigurationSection>(configuration);
ConfigurationUnity.Bind<ConverterConfigurationSection>(configuration);
ConfigurationUnity.Bind<InstanceConfigurationSection>(configuration);
}
}

可见它们实际上将 IConfiguration 对象进行配置,将日志、缓存、ioc容器、订阅发布等从配置中读出,放到内存当中。这样,在项目中的任何地方,都可以使用以下的方法来获取相对应的对象:

        private class TestClass
{
void Test()
{
//获取日志的配置
var logCfg = ConfigurationUnity.GetSection<Fireasy.Common.Logging.Configuration.LoggingConfigurationSection>(); //获取默认日志记录对象
var log = Fireasy.Common.Logging.LoggerFactory.CreateLogger(); //获取缓存的配置
var cacheCfg = ConfigurationUnity.GetSection<Fireasy.Common.Caching.Configuration.CachingConfigurationSection>(); //获取默认缓存管理对象
var cache = Fireasy.Common.Caching.CacheManagerFactory.CreateManager();
}
}

扩展方法 AddIoc 是将 fireasy 中的 ioc 容器中的相关抽象与实现映射添加到 core 本身的 ioc 集合中,使两者融合为一体,在 fireasy 中,ioc 是由 ContainerUnity 来管理的,它可以配置多个容器。源码如下:

        public static IServiceCollection AddIoc(this IServiceCollection services, Container container = null)
{
container = container ?? ContainerUnity.GetContainer();
foreach (AbstractRegistration reg in container.GetRegistrations())
{
if (reg is SingletonRegistration singReg)
{
services.AddSingleton(singReg.ServiceType, CheckAopProxyType(singReg.ImplementationType));
}
else if (reg.GetType().IsGenericType && reg.GetType().GetGenericTypeDefinition() == typeof(FuncRegistration<>))
{
services.AddTransient(reg.ServiceType, s => reg.Resolve());
}
else
{
services.AddTransient(reg.ServiceType, CheckAopProxyType(reg.ImplementationType));
}
} return services;
}

    二、mvc 配置

扩展方法 ConfigureFireasyMvc 中本 mvc 的一些配置。

        public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.ConfigureFireasyMvc(options =>
{
options.DisableModelValidator = true;
options.UseErrorHandleFilter = true;
options.UseJsonModelBinder = true;
options.UseTypicalJsonSerializer = true;
options.JsonSerializeOption.IgnoreNull = true;
options.JsonSerializeOption.Converters.Add(new Fireasy.Data.Entity.LightEntityJsonConverter());
options.JsonSerializeOption.Converters.Add(new Common.Serialization.FullDateTimeJsonConverter());
});
}

可以设置 MvcOptions 参数对象中的某些属性来达到不同的效果:

DisableModelValidator 覆盖本身 mvc 自带的 IObjectModelValidator 对象,使它在调用 action 时不对 model 进行验证。因为在此示例中,我们使用 easyui 前端框架,在 ui 上就有数据的验证,并且在 Entity 层还有一次验证,因此将其关闭。

UseJsonModelBinder 是使用 fireasy 特有的 model 绑定方式,即使用 json 充序列化的方式传递复杂的对象及集合,众所周知,在 mvc 里要传递一个对象,或一个集合,只能使用 name=hxd&sex=1&birthday=2019-1-1 这种方式,因此对于复杂的对象来说,就先麻烦了。使用此开关后,只需要传递 info={ name: "hxd", sex: 1, birthday: "2019-1-1" } 就行了。

UseErrorHandleFilter 使用自定义的异常处理过滤器。在 HandleErrorAttribute 这个类中,当异常类型是 ClientNotificationException 时,将直接返回其 Message,否则记录日志,并返回友好的错误提示信息。因此,在业务层,可以多使用 ClientNotificationException  来通知前端具体的异常信息。

UseTypicalJsonSerializer 使用 fireasy 的 json 序列化方法,它将抛弃 Newtonsoft。原因是,Entity 返回时不再做 ViewModel 的映射处理,那么不可避免地,在 Entity 对象中会包含一些延迟加载的属性,在使用 Newtonsoft 时将发生不可原谅的循环引用异常,造成程序崩溃。fireasy 中引入了一个 ILazyManager 接口,Entity 受此管理后,那些未加载出来的属性,则不会被序列化。另外一种解决办法是,引入 Fireasy.Fireasy.Newtonsoft,将 LazyObjectJsonConverter 添加到 Converters 中去。

    services.AddMvc()
.AddJsonOptions(options =>
{
options.SerializerSettings.Converters.Add(new Fireasy.Newtonsoft.LazyObjectJsonConverter());
options.SerializerSettings.ContractResolver = new DefaultContractResolver();
});

  JsonSerializeOption 即 fireasy json 序列化的一些全局配置,尤其要注意的是,这里在 Converters 里添加了一个 LightEntityJsonConverter ,它的目的是在 action model 绑定时,通过它来进行反序列化,这是为什么呢,后面的章节中会提到。

扩展方法 ConfigureEasyUI 主要是用来配置 easyui 的一些数据验证规则,它默认绑定了ValidateBoxSettingBinder 和 NumberBoxSettingBinder 两种规则,这里就不再介绍了。

    三、数据库配置

数据库配置是核心,所以着重说一下。参见 appsettings.json 文件中的 fireasy:dataInstances 节点,它的配置其实很易懂,无非就是指定 providerType 和 connectionString。

providerType 是数据库的提供者,对应不同的数据库,这里可以取 MsSql、MySQL、Oracle、SQLite、Firebird、PostgreSql、以及 OleDb。

如果这些都还不能满足你,你可以自行去实现 provider ,然后通过 providerName 来进行指定。这个暂时先不说了,后面有一个 Mongodb 的章节介绍。

不同的 provider 需要从 nuget 里引用相对应的程序集,从上至下优先,可对照下表:

providerType .net core .net framework
MsSql 不需要 不需要
MySQL MySql.Data
MySqlConnector
同 .net core
SQLIte System.Data.SQLite
Microsoft.Data.Sqlite
Spreads.SQLite
System.Data.SQLite
Oracle Oracle.ManagedDataAccess
Mono.Data.OracleClientCore
Oracle.ManagedDataAccess
Oracle.DataAccess
System.Data.OracleClient
Firebird FirebirdSql.Data.FirebirdClient 同 .net core
PostgreSql Npgsql 同 .net core
OleDb 不需要 不需要

    四、DbContext 配置

DbContext 与 上节的数据库配置息息相关。DbContext 是继承自 EntityContext 的,EntityContext 有两个构造函数。

    public class DbContext : EntityContext
{
/// <summary>
/// 自定义 EntityContextOptions 参数方式
/// </summary>
/// <param name="options"></param>
public DbContext(EntityContextOptions options)
: base (options)
{
} /// <summary>
/// 使用数据库配置实例名方式
/// </summary>
/// <param name="name"></param>
public DbContext(string name)
: base (name)
{
}
}

一般是使用第二种方式,name 即数据库配置中的实例名,如果不指定,则由 default 来决定,从 appsettings.json 可得知,默认是使用 sqlite 数据库,如果这里使用了 mysql 则会使用 MySQL 数据库。

第一种方式则用在需要在程序中动态指定 provider 和 connection string 的时候使用,它主要通过 ContextFactory 这个委托来指定。下面就是一个很好的例子。

    public class TestClass
{
void Test()
{
var providerName = "SQLite";
var connectionStr = "Data source=|datadirectory|../../../../database/zero.db3;version=3;tracking=true"; using (var db = new DbContext(new EntityContextOptions
{
ContextFactory = () => new EntityContextInitializeContext(Data.Provider.ProviderHelper.GetDefinedProviderInstance(providerName), connectionStr)
}))
{ }
}
}

原来业务层中使用 DbContext 是在每个方法里 using (var db = new DbContext()) 来使用的,当时是对于 ioc 对象的释放机制不是太了解。经过测试后,将 DbContext 通过构造器注入的方式注入也是完全没有问题的。修改一下 Startup.cs  中的 ConfigureServices 方法,与 Entity Framework 类似的,使用 AddEntityContext 方法(Entity Framework 中是 AddDbContext 方法)。

        public void ConfigureServices(IServiceCollection services)
{
services.AddEntityContext<DbContext>(options =>
{
options.AutoCreateTables = true; //此项为 true 时, 采用 codefirst 模式维护数据库表
options.NotifyEvents = true; //此项设为 true 时, 上面的实体持久化订阅通知才会触发
});
}

 这里的 EntityContextOptions 参数有以下几个设置项:

AutoCreateTables 使用类似于 CodeFirlst 的方式,检查实体映射的数据表是否存在,没有的话则创建,同时对于已经存在的数据表,会对属性进行比对,增加新的字段,删除的字段不进行处理。

NotifyEvents 是否触发持久化事件,比如实体的创建之前、创建之后、修改之前、修改之后等等,都会以事件消息的方式通过消息订阅进行发布,定义一个消费者来接收进行处理。

RecompileAssembly 是否重新编译实体程序集。由于 fireasy 中的实体类的属性使用了 virtual 修饰,此开关打开时,将使用 aop 技术对实体类进行动态编译,使之在属性被修改时能够记录下来,达到按需更新的效果。

ValidateEntity 是否在持久化之前进行实体的验证,如果前端把控严格的话,可以将此开关关闭,免得影响性能。

上面的 AddEntityContext 还存在一个问题,即 DbContext 的引用,你也可以将 DbContext 放到 appsettings.json 的 ioc 配置节中,这样 core 项目就不必要引用 DbContext 的项目了。如下配置后,可以直接使用 services.AddEntityContext() 方法。

{
"fireasy": {
"containers": { //ioc配置
"settings": {
"default": [
{
"serviceType": "Fireasy.Zero.Services.Impls.DbContext, Fireasy.Zero.Services"
}
}
}
}
}
}

  

好了,配置这块还是算比较复杂的了,但是通过这样的配置,项目的灵活度却是提高了不少。写这篇的目的,其实更多的目的是给大家提供一种思路,使大家对 .net core 有一个更深一步的了解。

==================================相关资源==================================

fireasy源码:  https://github.com/faib920/fireasy2

zero源码:  https://github.com/faib920/zero

代码生成器:  http://www.fireasy.cn/soft/codebuilder/CodeBuilder2setup.exe