==== 目錄 ====
跟我學: 使用 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
