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