Ninject學習筆記(一)
理解依賴注入
DI概念
什么是DI?
DI是如何工作的?
什么是DI容器
使用Ninject
如何使用Ninject
Ninject對象生命周期
暫時范圍
單例范圍
線程范圍
請求范圍
自定義范圍
Ninject模塊
從xml配置依賴(Ninject XML擴展)
Ninject約定(Ninject Convention擴展)
選擇程序集
選擇組件
選擇服務類型
綁定配置
理解依賴注入
DI概念
依賴注入,或者控制反轉,降低代碼耦合度。Ninject是一個輕量級.NET DI框架。
什么是DI?
一個例子,木匠伐木,需要工具斧子。
class Carpenter
{
Axe axe = new Axe();
void Lumber()
{
axe.Cut();
}
}
代碼中,木匠依賴于斧子。此時需求變了,木匠買了木鋸,那么上面的代碼必須從新修改然后進行編譯。再比如木匠比較愛惜工具,決定兩種工具換著用,再比如,木匠決定提高生產率,購置了電鋸等等。作為程序員來說,如果每次需求變更就重新編碼,那么你會發現自己深陷沼澤地。
DI的出現就是為了解決這一問題的。它是一種編程方式,依賴關系不需要調用者來管理,統一由框架管理。“不要找我們,我們來找你”。
DI是如何工作的?
簡簡單單一句話——對接口編程,而不是對具體實現編程。用抽象元素來實現依賴,而不是具體類,如此一來我們可以很容易地替換具體的依賴類而不影響上層的調用組件。
class ITool
{
void Cut();
}
class Carpenter
{
private ITool tool;
void Carpenter(ITool tool)
{
tool = tool;
}
void Lumber()
{
tool.Cut();
}
}
什么是DI容器
DI容器是一個注入對象,用來向對象注入依賴性。一個應用中的依賴關系組成一個錯綜復雜的依賴圖。DI容器就是來管理依賴復雜性的。它決定抽象類選擇哪個實現類來實例化對象。Ninject有兩種定義依賴的方式:
- xml配置:
<bind service="ITool
to="Axe"/> - 代碼定義:`Bind().To()
使用Ninject
如何使用Ninject
- 在VS編輯環境中->右鍵項目->選擇Nuget管理器->搜索Ninject->下載;
- 在項目中定義Kernel:
var kernel = new StandardKernel()
; - 通過
kernel.Get
方法獲取依賴的對象。
Ninject對象生命周期
暫時范圍
默認狀態,Ninject不管理它創建的對象,也就是每次請求都new一個新對象。
單例范圍
有兩種方式創建單例
- 使用單例模式
class ConsoleLogger:ILogger
{
public static readonly ConsoleLogger Instance = new ConsoleLogger();
private static ConsoleLogger()
{
// Hiding constructor
}
public void Log(string message)
{
Console.WriteLine("{0}: {1}", DateTime.Now, message);
}
}
然后在Bind方法后調用ToConstant方法指定靜態只讀對象ConsoleLogger.Instance為常量對象。 kernel.Bind<ILogger>().ToConstant(ConsoleLogger.Instance);
使用InSingletonScope方法——更簡單的方法
kernel.Bind<ILogger>().To<ConsoleLogger>().InSingletonScope();
指定某個類為單例
kernel.Bind<ConsoleLogger>.ToSelf().InThreadScope();
線程范圍
每一個線程只創建一個給定類型的對象。對象的生命周期和線程一樣長。 kernel.Bind<object>().ToSelf().InThreadScope();
請求范圍
用在Web應用程序中非常有用。在相同的請求范圍內得到一個單例的對象。需要添加Ninject.Web.Common
引用。 kernel.Bind<SampleClass>().ToSelf().InRequestScope();
自定義范圍
自定義范圍讓我們定義我們自己的范圍,在這個范圍內保持一類型的唯一對象。只要提供的回調方法返回的對象引用是一樣的,Ninject在這個范圍內返回相同的實例。只要返回的對象引用變了,將創建一新的指定類型的對象。創建的對象實例將一直保存在緩存里,直到返回的范圍對象被垃圾回收器回收。一旦范圍對象被垃圾回收器回收,Ninject創建的所有的對象實例將被從緩存中釋放和處理。
調用InScope方法傳入Lamda表達式定義自定義返回:kernel.Bind<object>().ToSelf().InScope( ctx => User.Current );
自定義范圍是最靈活的,可以實現其他的范圍:
- 線程范圍:
kernel.Bind<object>().ToSelf().InScope( ctx => Thread.CurrentThread);
- 請求范圍:
kernel.Bind<object>().ToSelf().InScope( ctx => HttpContext.Current);
Ninject模塊
如果應用程序規模比較大,那么注冊的服務列表將會非常長,維護變得困難。一種好的方式是進行分組管理。Ninject提供了這個功能。每個組稱為一個Ninject模塊,只需要編寫一個類實現INinjectModule
接口,需要實現三個方法和兩個屬性。好消息是,Ninject還提供了一個實現該接口的抽象類NinjectModule
,無需每次都實現接口的所有方法。
將多個模塊加載到單個Ninject Kernel中的方法: var kernel = new StandardKernel(new Module1(), new Module2(), ...)
也可以將應用程序中所有的模塊同時加載到Ninject Kernel中: kernel.Load(AppDomain.CurrentDomain.GetAssemblies());
從xml配置依賴(Ninject XML擴展)
需要Ninject XML擴展引用。注意記得發布xml文件時選擇“Copy if newer”。
XML配置文件格式如下:
<module name="moduleName">
<bind service="Namespace.IService1, AssemblyName"
to="Namespace.ConcreteService1, AssemblyName" />
<bind service="Namespace.IService2, AssemblyName"
to="Namespace.ConcreteService2, AssemblyName"
Scope="singleton"/>
</module>
加載XML文件到Kernel的方法: kernel.Load("module1.xml","module2.xml","module3.xml");
可以使用相對輸出路徑的路徑,也可以使用絕對路徑,還可以使用通配符: kernel.Load("Modules/*.xml);
Ninject約定(Ninject Convention擴展)
小的應用中,一個一個注冊服務類型并不困難,但是一個有上百個服務的應用程序呢?約定配置允許我們綁定一組服務,而不是一個個分別綁定。
注冊一個約定綁定需要三個步驟:選擇包含具體類的程序集、選擇程序集中的具體組件、選擇具體組件相關的服務類型。
選擇程序集
FromThisAssembly()
:選擇包含當前代碼的程序集;From(params Assembly[] assemblies)
:選擇指定程序集;FromAssemblyContaining<SomeType>()
:選擇包含指定類的程序集;Join()
:選擇多個程序集。
kernel.Bind(x => x
.FromAssemblyContaining<CustomersService>()
.SelectAllClasses()
.Join()
.FromAssemblyContaining<MessageProvider>()
.SelectAllClasses()
.BindAllInterfaces());
默認情況下只有公有類型可以在程序集中被邦迪。為包含非公有類型,需要在選擇程序集后顯式調用IncludingNonePublicTypes
方法:
kernel.Bind(x => x
.FromAssemblyContaining<CustomersService>()
.IncludingNonePublicTypes()
.SelectAllClasses()
.BindAllInterfaces());
選擇組件
選擇要注冊的組件。
SelectAllClasses()
:選擇所有的非抽象類;Select(Func<Type, bool> filter)
:選擇需要的類。
例子:選擇以“Customer"開頭的所有類:
kernel.Bind(r => r
.FromThisAssembly()
.Select(t =>t.Name.StartsWith("Customer"))
.BindBase());
例子:用條件對結果進行過濾:
kernel.Bind(x => x
.FromThisAssembly()
.SelectAllClasses()
.InNamespaces("Northwind.Controllers")
.BindBase());
選擇服務類型
BindAllInterfaces()
: 綁定所有的選擇的組件的接口到選擇的組件。BindBase()
: 綁定選擇的組件的基類型到當前的組件。BindDefaultInterface()
: 綁定指定類型的默認接口到類型。類型的默認接口跟類型同名。例如,ICustomerService是CutomerService的默認接口。BindDefaultInterfaces()
: 綁定指定類型的默認接口到類型。類型的默認接口是那些以類型的名字結尾的接口。例如,IRepository和ICustomerRepository都是SqlCustomerRepository的默認接口。BindSingleInterface()
: 要求指定類型只有一個接口。在這個情況下,這個接口綁定到這個類型。如果這個類型沒有或者有多個接口,則不添加綁定。BindToSelf()
: 綁定類型到自身。BindSelection(ServiceSelector selector)
: 綁定選擇的接口到類型。BindUsingRegex(string pattern)
: 綁定當前類型的符合正則表達式的接口到類型。
綁定配置
綁定創建后,可以像普通綁定的配置一樣進行配置:
kernel.Bind(x => x
.FromThisAssembly()
.SelectAllClasses()
.BindAllInterfaces()
.Configure(b=>b.InSingletonScope()));
我們也可以使用ConfigureFor方法對某些類型分別進行配置。下面的例子,所有的repository類在構造函數中都注入"connectionString"參數,配置成單例生命周期。SqlCustomerRepository類重載成線程生命周期:
kernel.Bind(x => x
.FromThisAssembly()
.SelectAllClasses()
.InheritedFrom<IRepository>()
.BindAllInterfaces()
.Configure(b =>b.InSingletonScope ()
.WithConstructorArgument("connectionString", ApplicationSettings.
ConnectionString))
.ConfigureFor<SqlCustomerRepository>(b =>b.InThreadScope()));