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

==== 目录 ====

  跟我学: 使用 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

posted @ 2019-05-21 16:12  fireasy  阅读(916)  评论(0编辑  收藏  举报