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()));