項目背景說明
最近接手一個 “老” 項目的需求修改,項目整體基于?.net core 3.1
?平臺,以傳統的三層架構為基礎構建。了解需求后,逐步對原有項目框架進行大概的了解,主要是熟悉一些框架的開發規范,基本工具類庫的使用,然后嘗試修改業務需求以達到客戶方的要求。
剛開始了解業務需求,先大概的了解了一些直觀可視化的界面操作和基本菜單目錄結構,此時還沒來得及看項目框架代碼,需求大概清楚后,開始對后端項目框架查看,隨之逐步使用。這不使用不知道,一使用嚇一跳,針對目前遇到的幾個問題,先在這里列舉部分:
1.?通常情況下?
BLL
?層依賴引用?DAL
?層,該項目中剛好與之相反;2.?單表的?
CRUD
?操作基本被封裝到基類中(BaseController、BaseBll 和 BaseDal
),對多條件查詢的提供方法不太靈活(不好用),從基類可以看出,各層職責混淆,并且封裝的操作?DB
?的基本方法返回值都為?void
?。比如在修改一個相對簡單的需求,在業務整合的時候,使用框架封裝提供的方法反而更麻煩;3.?在?
BLL
?層的具體方法中,看到一個功能大概是批量列表操作的方法,方法內竟然多次循環操作?DB
(此處不多說,自己體會)。聽同事說,這個?項目剛上線就存在內存泄漏?的情況(感到驚訝!)。4.?還有同事在做需求業務修改時,發現?(單體項目)同庫環境多表之間的關聯操作沒有走數據庫事務處理,而是直接采用應用程序服務依賴處理(我還沒接觸到這類需求,暫時不太清楚怎么設計的)。
這個項目唯一的好處就是?模塊名稱映射,業務模塊的命名在前端、服務端再到數據庫名稱和實體表之間都保持較好的映射規則,這是欣慰的地方。
目前所了解或者聽聞的就大概這些,基于以上的這些問題,在不破壞現有的框架編碼體系的前提下,又要滿足自己修改業務需求時編寫代碼的順手體驗(專業描述叫?漸進式修改,說難聽一點叫偷梁換柱),仔細看了下?DAL
(數據訪問層)的基本代碼,幸好預留了一個?IDbContext
?對象,于是基于該對象提供的信息(解析該對象)并引入順手的?orm
?操作(符合框架的擴展),決定不使用框架內置的封裝方法。
產生了該想法,說干就干,本文將從以下幾個方面來談談項目改造和相關的框架說明:
1. 回顧經典的三層架構(單體項目中常用);
2.?第三方?
DI
?框架?Autofac
?的使用(項目中看到應用,順便了解學習);3.?解析?
DAL
?層預留的?IDbContext
?對象(在本項目中是?EF
?提供的上下文對象),引入更輕量級的?ORM
(此處使用?FreeSql
);4. 最后基于三層架構,簡單快速的搭建一個單體項目的框架雛形(實操);
說明:在原有的項目中,目的是跳出框架封裝的基本 CRUD 操作方法(不太好用),主要改造點是解析 DAL 層的?
IDbContext
?對象并引入輕量化的 FreeSql 操作。
三層架構的理解
因為項目業務環境,基本上都是單體三層架構為主,三層架構想必大家都不陌生,在回顧之前我們先來了解生活中一個常見且形象的應用場景,以商場購買豬肉為例,先看下面的圖:
通過上圖,我們可以很清晰直觀的了解到,豬從豬圈里面逐步變成商品被流通市場?的大概過程,并且每一個過程中?職責分工比較明確,那么對應的在我們計算機程序設計中,也是可以依據此來抽象劃分各個模塊的,如下圖所示:
從上圖中,我們按照【豬 => 工廠 => 商品】的模式抽象,形成了?職責明確的各個模塊層,由于項目初期起步規模不大,各個模塊組合的單體項目可以部署同一個服務器環境,也可以安裝上圖分離環境部署。
說明:此處不討論架構設計,只是針對目前接手業務需求的項目回顧一下基本的三層架構,具體的服務資源部署可以依據公司的業務規模,考慮經濟成本且滿足當下使用需求即可。個人見解,架構是業務驅動的,而不是被過渡的設計。
上面的架構圖中,分別標注了三種角色類型:
??客戶端(Client),用于向【服務端應用程序】發起請求;
??應用服務層(App Server),用于接收 Client 的請求,經過一系列的數據處理,響應相應的反饋結果;
??數據服務層(Data Server),主要用于存儲應用系統的基礎數據和相關業務處理的數據。在該層通常為了減緩 DB 的 I/O 直接交互,通常會引入一個緩存組件(單體環境通常內存緩存,或者分布式部署環境的 Redis)提升應用系統的性能。
這里我們重點說下應用服務層(App Server),分別包含以下幾個職責模塊:
??UI 層,接收 Client 的請求,承擔展示頁面直觀的視覺效果和數據校驗等相關工作。通常包括:
winfrom/wpf/.aspx/.cshtml/.html
?等。在前后端分離的項目中,相對前端應用程序來說,后端提供的?webapi/controller
?層即代表該層。??BLL 層,接收?
Client
?的數據后,通常情況下使用?IBLL
?定義接口規范,然后在?BLL
?層實現相應的業務邏輯處理方法(依賴?IDAL
?層提供的數據),比如方法或者服務的整合等。??DAL 層,提供對?
DB
?數據庫的訪問操作(數據源相關環節交涉),通常情況也在?IDAL
?層定義接口規范,然后在?DAL
?層實現對應的數據訪問操作方法(比如單表的?CRUD
?操作)。通常該層會借助一些?ORM
?輔助類庫,比如:ADO.NET、DbHelper/SqlHelper、FreeSql 、EF/EF Core、Dapper
?等。??Model 層(數據模型的載體),為了更佳細化的分類規制,此處暫時考慮分三類模型,分別是?
BaseEntity、BusinessEntity 和 ViewEntity
。??Common 層(通用類庫和工具助手),該層有自定義封裝整合的,也有依賴外部 Nuget 包封裝處理的,依據項目業務情況按需獲取組裝。
基于上面的架構圖,一個單體環境下基本的框架雛形就可以搭建了,但具體落地項目還需考慮以下幾點細節和原則(包括但不限于):
1.?系統開發的?命名規范,建議各個業務模塊,在數據庫表設計、前后端應用程序里面一一映射,這樣可以很直觀、方便的上手;
2.?請求入口處?參數合法性的基礎校驗(必備常識),無論前端部分還是服務端部分,參數的校驗還是很有必要性的。無效的參數,可能會導致程序的異常;
3.?統一的入參格式,比如請求參數?
JSON
?格式化,遵循?HTTP/RESTful API
?風格;4.?統一的數據響應載體,對比原生的數據格式返回,很多情況下的?
null
?結果無法確定接口在業務意義的成功或失敗。5.?統一的異常處理機制和數據格式,通常采用?
AOP
?思想全局異常捕獲(ExceptionFilterAttribute
?異常過濾器),數據信息推薦?JSON
?格式化記錄;6.?系統日志的記錄(數據建議?
JSON
?格式化),通常情況下會在框架層面采用?AOP
?方式獲取用戶在系統中操作的全生命周期數據記錄,也可提供日志寫入方法,在關鍵業務邏輯處精細化的記錄邏輯操作信息。從一定方面可以起到還原 “真相” 的保障;7.?整體遵循?單一職責原則(
SRP:Singleresponsibilityprinciple
),該點也是最難做到的理想化指導原則,在編寫業務方法的時候,一個方法盡量做到功能單一,比如復雜的業務處理,可以使用每個相關的單一方法整合;8.?依賴抽象,不應依賴具體實現,這也是?開-閉原則(
OCP:Open - Close Principle
)的體現。比如:Controller => IBLL 接口規范 / BLL 具體實現 => IDAL 接口規范 / DAL 具體實現。還有在框架中無處不在的 DI 應用;
說明:此處只是列舉部分比較常見或者基本必備的點,框架設計還有很多細節考慮,這里不再詳細論述。
任何框架無論封裝的如何優秀,關鍵還是在于局中 “玩游戲” 的開發者,框架只是提供了基本的開發規則,要大家共同的遵循,這其中最難的就是團隊小伙伴達成一致的思維認知和共識。約定由于配置,任何框架不可能面面俱到,過渡設計框架務必會失去部分靈活性(物極必反的道理想必大家都知道),個人建議框架架構設計應該以?業務為驅動、技術為主導、約定和思想共識為輔助、開發規范為底線?這幾個方面加強。
Autofac 基本概述
Autofac 官方地址 => https://autofac.org/
了解到項目的基本情況后,由于項目是?.net core 3.1
?平臺構建的,與老平臺的?.netfx
?相比,變化最大有以下幾點(這里嘮嗑一下):
1. 框架平臺的福利:開源、跨平臺、高性能(看怎么使用,比如批量列表操作直接多次循環 DB 操作,這樣的玩法神仙框架也無解);
2.?無處不在的?
DependencyInjection
(DI,依賴注入),最直觀的使用體驗就是解放了了傳統的?new
實例化對象;3.?靈活的?
Middleware
(中間件)和透明的?HTTP Pipeline
?(http 管道)機制;
這里只說下?DI
?依賴注入,在基本簡單的注入方面?asp.net core
?框架中默認提供的 DI 很方便,但在有些場景下就沒有第三方的 DI 更佳靈活方便了。
默認 DI 框架
??
Microsoft.Extensions.DependencyInjection.Abstractions
(容器的抽象)??
Microsoft.Extensions.DependencyInjection
(容器的具體實現)
當你要在自己的程序中使用時,使用nuget包管理工具安裝?Microsoft.Extensions.DependencyInjection
?包即可。
老牌第三方 DI 框架 Autofac
??
Autofac.Extensions.DependencyInjection
當然這里還有其他第三方?DI
?框架,這里不再一一列舉,感興趣的小伙伴自行查看相關資料。
Autofac 基本知識點
這里先簡單的回顧下 Autofac 的基本知識點 :
1.?Autofac 框架?DI 生命周期;
2.?Autofac 框架?DI 注入方式;
3. Autofac 在 asp.net core 框架中的應用(簡單提下,預留在下面的項目改造中描述)。
Autofac 框架 DI 的生命周期
??InstancePerDependency(瞬時的),與?
Microsoft DI
?容器中的?Transient
?類似,每次調用都會生成新的實例,這種模式也是?Autofac 的默認模式;??SingleInstance(單例),和?
Microsoft DI
?容器中的?Singleton
?類似,所有的調用均返回同一個實例;??InstancePerLifetimeScope(作用域),與?
Microsoft DI
?容器中的?Scoped
?類似,它在一個作用域中獲取的實例相同,在?asp.net core
?中常用(大多數情況下推薦使用);??InstancePerMatchingLifetimeScope (匹配作用域),與?
InstancePerLifetimeScope
?類似,但是它支持對實例共享進行更精細的控制;??InstancePerRequest (每次請求一個實例),在老舊的?
asp.net webform
?和?asp.net mvc
?中使用,此處不再介紹;??InstancePerOwned (Owned 隱式關系類型創建了一個新的嵌套生命周期Scope);
??ThreadScope (線程作用域),代表每個線程的一個實例,對于多線程場景,必須非常小心,不要在派生線程下釋放父作用域。如果您生成了線程,然后釋放了父作用域,那么可能會陷入一個糟糕的情況,即無法解析組件。
//?創建容器對象實例
var?builder?=?new?ContainerBuilder();//?1、InstancePerDependency(瞬時的)
//?注冊?Worker?類為?InstancePerDependency(瞬時的),每次獲取都會生成新的實例
builder.RegisterType<Worker>().InstancePerDependency();
//?如果不指定與?InstancePerDependency(默認的模式)相同
builder.RegisterType<Worker>();//?2、SingleInstance(單例)
//?注冊?Worker?類為?SingleInstance(單例),每次獲取均返回同一個實例
builder.RegisterType<Worker>().SingleInstance();//?3、InstancePerLifetimeScope(作用域)
//?注冊?Worker?類為?InstancePerLifetimeScope(作用域),
//?在同一個作用域中獲得的是相同實例,在不同作用域獲得的是不同實例
builder.RegisterType<Worker>().InstancePerLifetimeScope();
//?InstancePerLifetimeScope(作用域)實例對象驗證
using(var?scope1?=?container.BeginLifetimeScope())
{for(var?i?=?0;?i?<?100;?i++){//?在?scope1?中獲取的?Worker?都是同一個實例var?w1?=?scope1.Resolve<Worker>();}
}using(var?scope2?=?container.BeginLifetimeScope())
{for(var?i?=?0;?i?<?100;?i++){//?在?scope2?中獲取的?Worker?都是同一個實例//?在?scope2?中獲取的?Worker?實例和?scope1?中獲取的?Worker?實例不相同,因為他們是兩個不同的作用域var?w2?=?scope2.Resolve<Worker>();}
}using(var?scope3?=?container.BeginLifetimeScope())
{var?w3?=?scope3.Resolve<Worker>();using(var?scope4?=?scope3.BeginLifetimeScope()){//?w3?和?w4?是不同的實例,因為他們是在不同的作用域中請求的var?w4?=?scope4.Resolve<Worker>();}
}var?w5?=?container.Resolve<Worker>();
using(var?scope5?=?container.BeginLifetimeScope())
{//?w5?和?w6?不同//?Scope?是一個生命周期范圍,如果從?Scope?中解析一個?InstancePerLifetimeScope?服務,//?該實例將在?Scope?的持續時間內存在,并且實際上是一個單例//?它將在容器的生命周期內被保存,以防止其他對象試圖從容器解析?Worker//?解釋:注冊為 InstancePerLifetimeScope 的服務,在每個 Scope 中請求類似于請求單例,//?在這個單例的生命周期于?Scope?的生命周期相同,在?Scope?中請求對應實例則返回對應的單例,//?這樣就避免沖突,每個Scope請求的都是自己的實例var?w6?=?scope5.Resolve<Worker>();
}//?4、InstancePerMatchingLifetimeScope?(匹配作用域)
//?當您創建一個嵌套(多層級)的生命周期 Scope 時,您可以?“標記”?或?“命名”?該 Scope。
//?每個匹配標記的生命周期 Scope 作用域內最多只有一個與給定名稱匹配的服務實例,包括嵌套的生命周期Scope。
//?這允許您創建一種“作用域單例”,在這種單例中,其他嵌套的生命周期作用域可以共享組件的實例,而無需聲明全局共享實例。
builder.RegisterType<Worker>().InstancePerMatchingLifetimeScope("my-request");
//?創建標記的作用域Scope
using(var?scope1?=?container.BeginLifetimeScope("my-request"))
{for(var?i?=?0;?i?<?100;?i++){var?w1?=?scope1.Resolve<Worker>();using(var?scope2?=?scope1.BeginLifetimeScope()){var?w2?=?scope2.Resolve<Worker>();//?w1?和?w2?的實例總是相同的,因為使用?InstancePerMatchingLifetimeScope?且為指定標記的Scope,//?嵌套的生命周期作用域可以共享實例,它實際上在標記作用域中是一個單例}}
}//?創建另一個標記的作用域?Scope
using(var?scope3?=?container.BeginLifetimeScope("my-request"))
{for(var?i?=?0;?i?<?100;?i++){//?w3和w1/w2是不同的實例,因為他們是兩個不同的生命周期,雖然它們的標記相同//?InstancePerMatchingLifetimeScope?依然是在不同的生命周期作用域創建新的實例var?w3?=?scope3.Resolve<Worker>();using(var?scope4?=?scope3.BeginLifetimeScope()){var?w4?=?scope4.Resolve<Worker>();//?w3和w4是相同的實例,因為使用?InstancePerMatchingLifetimeScope?且為指定標記的?Scope//?嵌套的生命周期作用域可以共享實例//?w3和w4是同樣的實例,w1和w2是同樣的實例,但是w1/w2和w3/w4是不同的實例,因為他們是兩個不同的作用域?Scope}}
}//?注意:不能在標記不匹配的生命周期 Scope 中獲取 InstancePerMatchingLifetimeScope 中標記的實例會拋出異常。
using(var?noTagScope?=?container.BeginLifetimeScope())
{//?在沒有正確命名(標記)的生命周期Scope的情況下試圖解析每個匹配生命周期Scope的組件,會拋出異常var?fail?=?noTagScope.Resolve<Worker>();
}//?5、InstancePerOwned
// Owned隱式關系類型創建了一個新的嵌套生命周期Scope。
//?可以使用每個擁有的實例注冊將依賴關系限定到擁有的實例,簡單講就是將依賴注入限定到對應泛型實例
builder.RegisterType<MessageHandler>();
builder.RegisterType<ServiceForHandler>().InstancePerOwned<MessageHandler>();
using(var?scope?=?container.BeginLifetimeScope())
{//?MessageHandler?依賴?ServiceForHandler,它們的生命周期處于scope(當前Scope的名稱)下的子生命周期范圍內var?h1?=?scope.Resolve<Owned<MessageHandler>>();//?但是?InstancePerOwned?的實例需要自己處理,所以這里需要手動釋放h1.Dispose();
}//?6、ThreadScope?(線程作用域)
builder.RegisterType<MyThreadScopedComponent>().InstancePerLifetimeScope();
var?container?=?builder.Build();
void?ThreadStart()
{using?(var?scope?=?container.BeginLifetimeScope()){//?從容器的一個子作用域解析服務var?thisThreadsInstance?=?scope.Resolve<MyThreadScopedComponent>();//?還可以創建嵌套的作用域using(var?unitOfWorkScope?=?scope.BeginLifetimeScope()){var?anotherService?=?unitOfWorkScope.Resolve();}}
}
Autofac 框架 DI 注入方式
??
RegisterType
,類型注入??
RegisterInstance
,實例注入??
Lambda
?表達式注入??
Property
?屬性注入??
RegisterGeneric
,泛型注入??多種類型注入
??條件注入(Autofac 4.4+ 引入)
var?builder?=?new?ContainerBuilder();//?1、RegisterType?類型注入
//?可以通過泛型的方式直接注冊對應的類型
builder.RegisterType<ConsoleLogger>();
//?也可以通過?typeof?運算符得到對應的類型作為參數提供給?RegisterType?方法,這種方式在注冊泛型類型時非常有用
builder.RegisterType(typeof(ConfigReader));
//?通過?UsingConstructor?指定【構造函數】中傳遞的類型,以確定使用與之對應參數的構造函數實例化對象
builder.RegisterType<MyComponent>().UsingConstructor(typeof(ILogger),?typeof(IConfigReader));//?2、實例注入
//?預先得到一個實例
var?output?=?new?StringWriter();
//?通過?RegisterInstance?將實例注入到容器中,在通過容器獲取?TextWriter?的實例時就會獲得到?output?這個實例
builder.RegisterInstance(output).As<TextWriter>();
//?Autofac會自己管理實例的生命周期,如果注冊為瞬時的,那么這個實例在獲取一次后就會被調用其對應的Dispose方法,
//?如果希望自己控制對象的生命周期,在注入時需要跟上?ExternallyOwned()?方法
builder.RegisterInstance(output).As<TextWriter>().ExternallyOwned();?//?使用?ExternallyOwned?方法告知?Autofac?這個被注入的實例對象的生命周期由自己掌控,不需要自動調用?Dispose?方法
//?將一個單例實例注入到容器中,其他被注入的對象就可以直接獲取到這個單例,Autofac也不會釋放這個單例
builder.RegisterInstance(MySingleton.Instance).ExternallyOwned();//?3、Lambda?表達式注入
//?反射是一種很好的創建依賴注入的方式,但是有時候需要注入的對象并不是使用簡單的無參構造函數實例化一個對象,
//?它還需要一些其他的參數或者動作來得到一個對應的實例,這時候可以使用Lambda表達式注入。//?在容器中注入?A,但是?A?不是使用無參構造函數獲得實例的,它使用從?Autofac?中取出的一個?B?對象實例作為參數,調用需要一個?B?對象實例的構造函數
builder.Register(c?=>?new?A(c.Resolve<B>()));
//?這里的c是一個IComponentContext對象,通過?IcomponentContext?對象可以從Autofac容器中解析出相應的對象,然后作為實參提供給A對象的構造函數//?Lambda表達式對復雜參數的注入非常有用
//?有時候構造函數并不是簡單的一個固定參數,而可能是一個變化的情況,如果沒有這種方式,可能就需要復雜的配置文件才能完成對應的功能
builder.Register(c?=>?new?UserSession(DateTime.Now.AddMinutes(30)));//?4、Property?屬性注入
//?相比上面直接在構造時給屬性賦值,Autofac 有更優雅的屬性注入。
//?給?A?實例的?MyB?屬性賦一個從容器中解析出來的?B?實例
builder.Register(c?=>?new?A(){?MyB?=?c.ResolveOptional<B>()?});//?屬性注入在以下情況特別有用
//?通過參數值選擇實現能夠提供一種運行時選擇,它不僅僅是最開始時的參數決定的,這個參數在運行時也是可以改變以返回不同的實現
builder.Register<CreditCard>((c,?p)?=>?{var?accountId?=?p.Named<string>("accountId");if?(accountId.StartsWith("9")){return?new?GoldCard(accountId);}else{return?new?StandardCard(accountId);}});
//?此處推薦使用工廠模式,通過傳入工廠委托的方式獲取不同的實例。//?5、RegisterGeneric?泛型注入
//?通常情況 IoC 容器都支持泛型注入,Autofac 支持以特別的語法強調特別的泛型,它的優先級比默認的泛型高,但是性能沒有默認的泛型好,因為不能緩存。
//?IoC?容器獲取?IRepository<T>?類型的實例時容器會返回一個?NHibernateRepository<T>?實例
builder.RegisterGeneric(typeof(NHibernateRepository<>)).As(typeof(IRepository<>)).InstancePerLifetimeScope();//?6、多種類型注入
//?有時候一個實例對應很多接口,通過不同的接口請求到的實例都是同一個實例,那么可以指定實例對應的類型
//?注意:要注冊多個接口,那么具體類必須實現繼承的這些接口。
//?通過?ILogger?和?ICallInterceptor?得到的都是?CallLogger?實例
builder.RegisterType<CallLogger>().As<ILogger>().As<ICallInterceptor>();//?多種類型注入,涉及到多個服務注入的選擇
//?如果一個類型進行了多個實例的注冊,Autofac?默認以最后一次注入的為準
//?請求ILogger實例容器返回?ConsoleLogger?實例
builder.RegisterType<ConsoleLogger>().As<ILogger>();
//?請求?ILogger?實例容器返回?FileLogger?實例
builder.RegisterType<FileLogger>().As<ILogger>();
//?最終請求ILogger實例容器返回的是FileLogger實例,Autofac?默認以最后的為準//?對于上面的情況,如果需要手動指定默認的,而不是使用最后一個,可以使用?PreserveExistingDefaults()?修飾
builder.RegisterType<ConsoleLogger>().As<ILogger>().PreserveExistingDefaults();
builder.RegisterType<FileLogger>().As<ILogger>();?//?此時該注入就無效了
//?最終請求?ILogger?實例容器返回的是?ConsoleLogger?實例,因為使用了PreserveExistingDefaults()修飾//?7、條件注入
//?條件注入在?Autofac4.4?引入,使用?4.4?以后的版本可以使用
//?大多數情況下,如果一個類型注入了多個實現,使用PreserveExistingDefaults()手動指定就夠了,但是有時候這還不夠,這時候就可以使用條件注入//?請求?IService?接口得到?ServiceA?
builder.RegisterType<ServiceA>().As<IService>();
//?請求?IService?接口得到?ServiceB
builder.RegisterType<ServiceB>().As<IService>()//?僅當?IService?沒有注冊過才會注冊.IfNotRegistered(typeof(IService));
//?最后請求?IService?獲得的實例是?ServiceAbuilder.RegisterType<HandlerA>().AsSelf().As<IHandler>()//?注冊?HandlerA?在?HandlerB?之前,所以檢查會認為沒有注冊//?最后這條注冊語句會成功執行.IfNotRegistered(typeof(HandlerB));
builder.RegisterType<HandlerB>()//?注冊自己的類型,即?HandlerB.AsSelf().As<IHandler>();
builder.RegisterType<HandlerC>()//?注冊自己的類型,即?HandlerC.AsSelf().As<IHandler>()//?不會執行,因為?HandlerB?已經注冊了.IfNotRegistered(typeof(HandlerB));//?注冊?IManager
builder.RegisterType<Manager>().As<IManager>()//?僅當IService和HandlerB都注冊了對應服務時才會執行.OnlyIf(reg?=>reg.IsRegistered(new?TypedService(typeof(IService)))?&®.IsRegistered(new?TypedService(typeof(HandlerB))));
項目改造
說明:此處項目改造并非是在原來項目中改造,這里只是基于三層架構的理解,快速搭建一個基本的基礎框架。這里盡量描述部分細節改造,注重思路循序漸進的理解。
項目準備工作或改造步驟:
1. 三層架構基礎框架搭建(展示項目結構);
2. CommonHelper 層單例對象構造器創建;
3. DbHelper 層使用單例構造器封裝 IFreeSql 對象;
4. WebAPI 層【Startup.cs + Program.cs】改造;
5. WebAPI 層 Swagger 配置使用;
6. WebAPI 層 Autofac 在 asp.net core 中的應用;
7. WebAPI 層請求入參的校驗(FluentValidation);
8. 結構化日志記錄 Serilog;
9. AutoMapper 的使用(DTO 數據模型轉換);
在實際項目應用中,使用到的 nuget 依賴包可能不只這些(還有其他未列舉的),依據項目實際使用情況引入,合理封裝到對應項目層中使用即可。
說明:其中第 5、7、8、9 點此處不做詳述,這些類庫的使用自行查閱相關資料。
? FluentValidation 是一個非常流行的構建強類型驗證規則的 .NET 庫,這里不做講述,可以參考該文章:https://www.cnblogs.com/lwqlun/p/10311945.html
? Serilog 是 .NET 中著名的結構化日志類庫。
? Swagger 在 asp.net core 中默認集成 Swashbuckle.AspNetCore。
項目框架目錄結構
依據架構圖規劃,搭建項目框架和依賴關系目錄結構如下:

CommonHelper 層
??單例對象構造器,
SingletonConstructor
namespace?Jeff.Mes.Common;///?<summary>
///?單例對象構造器
///?</summary>
///?<typeparam?name="T"></typeparam>
public?class?SingletonConstructor<T>?where?T?:?class,?new()
{private?static?T??_Instance;private?readonly?static?object?_lockObj?=?new();///?<summary>///?獲取單例對象的實例///?</summary>///?<returns></returns>public?static?T?GetInstance(){if?(_Instance?!=?null)?return?_Instance;lock?(_lockObj){if?(_Instance?==?null){var?item?=?System.Activator.CreateInstance<T>();System.Threading.Interlocked.Exchange(ref?_Instance,?item);}}return?_Instance;}
}
DbHelper 層
說明:該層可以使用工廠模式,封裝支持更多的 DB 類型,此處主要是描述下 FreeSql 對象的單例模式構建。這里推薦下 FreeSql 輕量級 ORM,支持多種關系型 DB,切換數據源方便,基本保持一致的使用體驗,遵循 MIT 協議開源,上手簡單方便,性能也不差,測試用例覆蓋較全面。
該層可以添加?dbconfig.json
?配置文件,考慮相對安全,該文件配置的字符串連接信息,可以使用密文編碼(比如:base64編碼,或者采用其他加密方式生產的密文)。
??dbconfig.json
?配置文件內容,此處依據自己的喜好定義,注意對敏感信息適當的安全考慮。
{"ConnectionStrings":?{"Development":?{"DbType":?"SqlServer","ConnectionString":?""},"Production":?{"DbType":?"SqlServer","ConnectionString":?""}}
}
??使用?
SingletonConstructor
?構建?FreeSqlHelper
using?System.Reflection;
using?System.Text;
using?System.Text.RegularExpressions;
using?FreeSql;
using?Jeff.Mes.Common;namespace?Jeff.Mes.DbHelper;///?<summary>
///?【Singleton?單例模式】構建?freesql?對象
///?</summary>
public?sealed?class?FreeSqlHelper?:?SingletonConstructor<FreeSqlHelper>
{//連接字符串作為?key,存儲構建的?IFreeSql?對象的字典集合private?readonly?static?Dictionary<string,?IFreeSql>?_FreeDic?=?new();///?<summary>///?構建?freesql?對象///?</summary>///?<param?name="dbType">DB類型</param>///?<param?name="connStr">連接字符串</param>///?<returns>IFreeSql</returns>public?IFreeSql??FreeBuilder(string?dbType,?string?connStr){if?(string.IsNullOrWhiteSpace(dbType)?||?string.IsNullOrWhiteSpace(connStr)){return?default;}bool?hasKey?=?_FreeDic.ContainsKey(connStr);if?(hasKey){return?_FreeDic[connStr];}IFreeSql?fsql;string?myDbType?=?dbType.Contains('.')???dbType.Substring(dbType.LastIndexOf('.')?+?1)?:?dbType;switch?(myDbType){case?"MySql":fsql?=?new?FreeSqlBuilder().UseConnectionString(DataType.MySql,?connStr).UseAutoSyncStructure(false)?//自動同步實體結構到數據庫.Build();?//請務必定義成?Singleton?單例模式break;default:fsql?=?new?FreeSqlBuilder().UseConnectionString(DataType.SqlServer,?connStr).UseAutoSyncStructure(false)?//自動同步實體結構到數據庫.Build();?//請務必定義成?Singleton?單例模式break;}bool?isAdd?=?_FreeDic.TryAdd(connStr,?fsql);if?(isAdd){return?fsql;}else{fsql.Dispose();return?_FreeDic[connStr];}}public?IFreeSql??FreeBuilder(DataType?dbType,?string?connStr)?{if?(string.IsNullOrWhiteSpace(connStr)){return?default;}/*bool?hasKey?=?_FreeDic.ContainsKey(connStr);if?(hasKey){return?_FreeDic[connStr];}*/bool?isOk?=?_FreeDic.TryGetValue(connStr,?out?IFreeSql??fsql);if?(isOk){return?fsql;}fsql?=?new?FreeSqlBuilder().UseConnectionString(dbType,?connStr).UseAutoSyncStructure(false)?//自動同步實體結構到數據庫.Build();?//請務必定義成?Singleton?單例模式?bool?isAdd?=?_FreeDic.TryAdd(connStr,?fsql);if?(isAdd){return?fsql;}else{fsql.Dispose();return?_FreeDic[connStr];}}public?(bool?isOk,?IFreeSql??fsql)?GetFreeSql(DataType?dbType,?string?connStr)?{bool?isOk?=?_FreeDic.TryGetValue(connStr,?out?IFreeSql??fsql);if?(!isOk)?{fsql?=?FreeBuilder(dbType,?connStr);isOk?=?fsql?!=?null;}return?(isOk,?fsql????default);}///?<summary>///?反射獲取【IDbContext】對象信息///?</summary>///?<typeparam?name="T">IDbContext</typeparam>///?<param?name="t">IDbContext</param>///?<returns>Dictionary(string,string)</returns>public?static?Dictionary<string,?string>?GetProperties<T>(T?t)?where?T?:?class{Type?type?=?t.GetType();var?sb?=?new?StringBuilder();foreach?(PropertyInfo?property?in?type.GetProperties().OrderBy(p?=>?p.Name)){object??obj?=?property.GetValue(t,?null);if?(obj?==?null){continue;}else{if?(string.IsNullOrWhiteSpace(obj.ToString())){continue;}}sb.Append(property.Name?+?"=");if?(property.PropertyType.IsGenericType){var?listVal?=?property.GetValue(t,?null)?as?IEnumerable<object>;if?(listVal?==?null)?continue;foreach?(var?item?in?listVal){sb.Append(GetProperties(item));}}else?if?(property.PropertyType.IsArray){var?listVal?=?property.GetValue(t,?null)?as?IEnumerable<object>;if?(listVal?==?null)?continue;foreach?(var?item?in?listVal){sb.Append(GetProperties(item));}}else{sb.Append(property.GetValue(t,?null));sb.Append("&");}}var?dic?=?new?Dictionary<string,?string>();var?sbArray?=?sb.ToString().Trim('&').Split('&');foreach?(var?item?in?sbArray){if?(item.Contains('=')){int?count?=?Regex.Matches(item,?"=").Count;if?(count?<=?1){var?itemArray?=?item.Split('=');dic.Add(itemArray[0],?itemArray[1]);}else{int?index?=?item.IndexOf('=');string?key?=?item.Substring(0,?index);string?val?=?item.Substring(index?+?1);dic.Add(key,?val);}}}return?dic;}
}
在原始項目中解析?IDbContext
?對象的方法就是?GetProperties()
?,該方法借助【反射 + 遞歸】獲取上下文對象,對于?FreeSql
?對象實例而言,只需獲取?db
?的連接字符串信息即可快速使用,而 DB 字符串連接信息就存儲于?IDbContext
?對象中。
原始項目?Dal
?層中預留的代碼:
public?class?OrderDal?:?BaseDal<OrderInfo>,?IOrderDal{public?OrderDal(IDbContext?dbContext,?ILoggerFactory?loggerFactory)?:?base(dbContext,?loggerFactory)?{}}
在項目改造之前,該?Dal
?層中需要引入相應?DB
?的?FreeSql
?包,如下格式:
??
FreeSql.Provider.Xxx
(Xxx 是數據庫類型名稱)
添加?nuget
?依賴包后,原始項目?Dal
?層改造后的代碼如下:
public?class?OrderDal?:?BaseDal<OrderInfo>,?IOrderDal
{private?readonly?IDbContext?_IDbContext;public?OrderDal(IDbContext?dbContext,?ILoggerFactory?loggerFactory)?:?base(dbContext,?loggerFactory)?{_IDbContext?=?dbContext;}public?(IDbContext?dbContext,?IFreeSql?fsql)?GetDbContext(){/*((Xxx.Framework.Dal.EFDbContext)_IDbContext).ConnStr((Microsoft.EntityFrameworkCore.DbContext)ss).Database.ProviderName*///-?Database?{Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade}?DatabaseFacade?dbFacade?=?_IDbContext.GetProperty<Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade>("Database");var?dic?=?FreeSqlHelper.GetProperties(_IDbContext);dic.Add("DbType",?dbFacade.ProviderName);IFreeSql?fsql?=?FreeSqlHelper.GetInstance().FreeBuilder(dic["DbType"],?dic["ConnStr"]);return?(_IDbContext,?fsql);}
}
在?IOrderDal.cs
?文件中添加如下代碼:
public?interface?IOrderDal?:?IBaseDal<OrderInfo>,IDependency
{///?<summary>///?獲取?DB?上下文對象,并構建?IFreeSql?實例///?</summary>///?<returns></returns>public?(IDbContext?dbContext,?IFreeSql?fsql)?GetDbContext();
}
在 BLL 層中方法中的調用,代碼如下:
public?class?OrderBll?:?BaseBll<OrderInfo>,?IOrderBll
{private?readonly?IOrderDal?_orderDal;public?OrderBll(IOrderDal?orderDal){_orderDal?=?orderDal;?//構造函數注入?IOrderDal?}public?(bool?state,?List<OrderInfo>?orderInfos)?Test(){var?(dbContext,?fsql)?=??_ppsOrderDal.GetDbContext();//?此處獲取到構建的 fsql 對象,即可使用。}
}
有了?FreeSql
?的引入,從此在?BLL
層就可以方便的編寫操作業務方法了,不再局限于框架內提供的方法。此處利于?Dal
?層預留的?IDbContext
?進行擴展,不修改上層項目的玩法,該怎么用還是怎么用。
WebApi 層
在?WebApi
?中添加?Nuget
?包:
??
Autofac.Extensions.DependencyInjection
?v8.0.0??
Swashbuckle.AspNetCore
?v6.4.0
接下來逐步改造以下兩點:
??
MiniAPI
?改造【Startup.cs + Program.cs
】模式;??在?
asp.net core
?中使用?Autofac
;
首先新建?Startup.cs
?文件,添加如下代碼:
using?Autofac;
using?Autofac.Extensions.DependencyInjection;
using?Jeff.Mes.WebApi.Modules;
using?Microsoft.AspNetCore.Mvc.Controllers;
using?Microsoft.Extensions.DependencyInjection.Extensions;namespace?Jeff.Mes.WebApi;public?class?Startup
{public?IConfiguration?Configuration;public?Startup(IConfiguration?configuration){Configuration?=?configuration;}//?Add?services?to?the?container.?注冊服務到?Ioc?容器public?void?RegisterServices(IServiceCollection?services,?IHostBuilder?host){#region?在?host?中注冊?Autofachost.UseServiceProviderFactory(new?AutofacServiceProviderFactory());host.ConfigureContainer<ContainerBuilder>(builder?=>{builder.RegisterModule<AutofacModule>();?//此處編寫相關服務&屬性的注入?});services.Replace(ServiceDescriptor.Transient<IControllerActivator,?ServiceBasedControllerActivator>());#endregionhost.ConfigureAppConfiguration((hostContext,?config)?=>?{var?env?=?hostContext.HostingEnvironment;string?path?=?Path.Combine(env.ContentRootPath,?"Configuration");config.SetBasePath(path).AddJsonFile(path:?"appsettings.json",?optional:?false,?reloadOnChange:?true).AddJsonFile(path:?$"appsettings.{env.EnvironmentName}.json",?optional:?true,?reloadOnChange:?true);});services.AddControllers();//?Learn?more?about?configuring?Swagger/OpenAPI?at?https://aka.ms/aspnetcore/swashbuckleservices.AddEndpointsApiExplorer();services.AddSwaggerGen();}//?Configure?the?HTTP?request?pipeline.?配置?HTTP?請求管道(中間件管道即中間件委托鏈)public?void?SetupMiddlewares(IApplicationBuilder?app,?IWebHostEnvironment?env){if?(env.IsDevelopment()){app.UseSwagger();app.UseSwaggerUI();}app.UseRouting();app.UseAuthorization();app.UseEndpoints(endpoints?=>?{endpoints.MapControllers();endpoints.MapGet("/env",?async?context?=>{//?獲取環境變量信息await?context.Response.WriteAsync($"EnvironmentName:{env.EnvironmentName},IsDevelopment:{env.IsDevelopment()}");});});}
}
其次修改?Program.cs
?文件中的代碼,修改如下:
namespace?Jeff.Mes.WebApi;public?class?Program
{public?static?async?Task?Main(string[]?args){var?builder?=?WebApplication.CreateBuilder(args);var?startup?=?new?Startup(builder.Configuration);var?services?=?builder.Services;var?host?=??builder.Host;startup.RegisterServices(services,?host);var?app?=?builder.Build();var?env?=?builder.Environment;startup.SetupMiddlewares(app,?env);await?app.RunAsync();}
}
接下來在項目中新建?Modules
?文件夾,分別存放如下文件:
??
AutowiredAttribute.cs
,該文件用于標記屬性注入的特性,和?AutowiredPropertySelector.cs
?文件搭配使用;??
AutowiredPropertySelector.cs
,該文件主要用于自定義屬性的選擇器;??
AutofacModule.cs
,該文件主要定義?Autofac
?中業務類的注入方式;
此處主要描述?Autofac
?在?asp.net core
?項目中的使用,同時還自定義了特性標記的屬性注入模式。
首先新建?Modules
?文件夾,用于存放上面的?.cs
?文件,各文件的完整代碼分別如下:
1、AutowiredAttribute.cs
?文件代碼:
namespace?Jeff.Mes.WebApi.Modules;[AttributeUsage(AttributeTargets.Property)]
public?class?AutowiredAttribute?:?Attribute
{?}
2、AutowiredPropertySelector.cs
?文件代碼:
using?Autofac.Core;
using?System.Reflection;namespace?Jeff.Mes.WebApi.Modules;public?class?AutowiredPropertySelector?:?IPropertySelector
{?//?屬性注入public?bool?InjectProperty(PropertyInfo?propertyInfo,?object?instance){//?自定義屬性特性標記?[Autowired]?的才生效return?propertyInfo.CustomAttributes.Any(it?=>?it.AttributeType?==?typeof(AutowiredAttribute));}
}
3、AutofacModule.cs
?文件代碼:
using?Autofac;
using?Jeff.Mes.Bll.BLL;
using?Jeff.Mes.Bll.IBLL;
using?Microsoft.AspNetCore.Mvc;namespace?Jeff.Mes.WebApi.Modules;public?class?AutofacModule?:?Module
{protected?override?void?Load(ContainerBuilder?builder){//?The?generic?ILogger<TCategoryName>?service?was?added?to?the?ServiceCollection?by?ASP.NET?Core.//?It?was?then?registered?with?Autofac?using?the?Populate?method.?All?of?this?starts//?with?the?`UseServiceProviderFactory(new?AutofacServiceProviderFactory())`?that?happens?in?Program?and?registers?Autofac//?as?the?service?provider.#region?此處編寫服務(BaseServiceRegister)的注冊規則//?ValuesService?構造函數有參builder.Register(c?=>?new?ValuesService(c.Resolve<ILogger<ValuesService>>(),?c.Resolve<IConfiguration>())).As<IValuesService>().InstancePerLifetimeScope();/*//?ValuesService?構造函數無參builder.Register<ValuesService>().As<IValuesService>();?//不聲明生命周期默認是瞬態。*/builder.Register(c?=>?new?ValuesService(c.Resolve<ILogger<ValuesService>>(),?c.Resolve<IConfiguration>())).As<IValuesService>().InstancePerLifetimeScope();builder.RegisterType<UserService>().As<IUserService>().InstancePerLifetimeScope();builder.RegisterType<UserService>().As<IUserService>().PropertiesAutowired().InstancePerLifetimeScope();builder.Register(c?=>?new?UserService()).As<IUserService>().PropertiesAutowired().InstancePerLifetimeScope();//builder.RegisterType<OrdersService>().As<IOrdersService>().InstancePerLifetimeScope();builder.Register(c?=>?new?OrdersService(c.Resolve<ILogger<OrdersService>>(),?c.Resolve<IConfiguration>())).As<IOrdersService>().InstancePerLifetimeScope();#endregion#region?此處編寫屬性(PropertiesAutowired)的注冊規則var?controllerBaseType?=?typeof(ControllerBase);//?說明:以下 1、2 兩種方式等效。//?1、獲取所有控制器類型并使用屬性注入var?controllersTypesInAssemblyAll?=?typeof(Startup).Assembly.GetExportedTypes().Where(type?=>?typeof(ControllerBase).IsAssignableFrom(type)).ToArray();builder.RegisterTypes(controllersTypesInAssemblyAll).PropertiesAutowired();//?2、自定義特性并使用屬性注入/*builder.RegisterAssemblyTypes(typeof(Program).Assembly).Where(type?=>?controllerBaseType.IsAssignableFrom(type)?&&?type?!=?controllerBaseType).PropertiesAutowired(new?AutowiredPropertySelector());*///?2.1?從?Startup?程序集中篩選出?ControllerBase?派生類程序集(ControllerBase?類中的程序集,并且排除本身?ControllerBase)var?controllersTypesInAssembly?=?typeof(Startup).Assembly.GetExportedTypes().Where(type?=>?controllerBaseType.IsAssignableFrom(type)?&&?type?!=?controllerBaseType).ToArray();//?2.2?注冊篩選出的?ControllerBase?派生類的程序集,使用自定義特性標注的屬性注入builder.RegisterTypes(controllersTypesInAssembly).PropertiesAutowired(new?AutowiredPropertySelector());#endregion}
}
接下來(習慣性)新增一個?Configuration
?文件夾,用于存放?appsettings.json
?相關配置文件。因為上面的項目中我們在?host.ConfigureAppConfiguration
?方法中調整了文件路徑。
其他項目層沒啥好講的,從架構圖中即可看出對應的功能職責,到這里遵循架構圖規則搭建的基礎項目結構就基本完成了,該文章主要目的是相對于原始項目做一個對照,重新梳理了一遍經典的三層架構,方便初學人員有一個基礎的模型參照。
總結
在項目實戰開發中,理解三層架構并遵循該架構合理化的搭建項目基礎框架是必要保障,而不是 DAL 層項目去依賴 BLL 層項目(?_? ),每一層盡量做到職責分明,各司其職,各個項目層之間協調配合,項目層之間的調用應該依賴抽象而非具體實現,該項目框架的基本雛形就搭建完畢了,有了良好的地基,里面很多細節的裝修就相對方便了,比如:很多 nuget 依賴包的使用,自行查看相關文檔。該文章的目的是提供一個基本的模型參照,理解思路然后逐步按需完善部分框架細節,并應用到實踐中才會有更深的體會和記憶。