[深入浅出依赖注入容器——Autofac]
IOC(控制反转Inversion of Control)
控制反转(Inversion of Control)就是使用对象容器反过来控制应用程序所需要的外部资源,这样的一种程序开发思想,调用者不再创建被调用者的实例,由IOC框架实现(容器创建)所以称为控制反转;创建对象和对象非托管资源的释放都由外部容器去完成,实现项目层与层之间的解耦的一种设计思想。
DI(依赖注入)和DIP(依赖倒置原则)
相信很多人还分不清楚DI和DIP这两个词,甚至认为它们就是同一个词。
依赖倒置原则(Dependency Inversion Principle)为我们提供了降低模块间耦合度的一种思路,而依赖注入(Dependency Injection)是一种具体的实施方法,容器创建好实例后再注入调用者称为依赖注入,就是应用程序依赖IOC容器来注入所需要的外部资源,这样一种程序的开发思想。
基本使用
通过nuget引入autofac;
准备几个实例对象:
public class Doge
{
public void SayHello()
{
Console.WriteLine("我是小狗,汪汪汪~");
}
}
public class Person
{
public string Name { get; set; }
public int Age {get; set; }
}
接下来准备IOC容器,通过IOC容器来实例化对象;
var builder = new ContainerBuilder();//准备容器
builder.RegisterType<Doge>();//注册对象
var container = builder.Build();//创建容器完毕
var dog = container.Resolve<Doge>();//通过IOC容器创建对象
dog.SayHello();
以接口方式注入
接着刚才的例子,添加个接口IAnimal,让Doge来实现它;
public interface IAnimal
{
void SayHello();
}
public class Doge : IAnimal
{
public void SayHello()
{
Console.WriteLine("我是小狗,汪汪汪~");
}
}
public class Cat : IAnimal
{
public void SayHello()
{
Console.WriteLine("我是小猫,喵喵喵~");
}
}
public class Pig : IAnimal
{
public void SayHello()
{
Console.WriteLine("我是小猪,呼呼呼~");
}
}
然后IOC注册对象的方式改变为:
var builder = new ContainerBuilder();//准备容器
builder.RegisterType<Doge>().As<IAnimal>();//映射对象
var container = builder.Build();//创建容器完毕
var dog = container.Resolve<IAnimal>();//通过IOC容器创建对象
dog.SayHello();
如果一个类型被多次注册,以最后注册的为准。通过使用PreserveExistingDefaults() 修饰符,可以指定某个注册为非默认值。
builder.RegisterType<Doge>().As<IAnimal>();//映射对象
builder.RegisterType<Cat>().As<IAnimal>().PreserveExistingDefaults();//指定Cat为非默认值
var dog = container.Resolve<IAnimal>();//通过IOC容器创建对象
dog.SayHello();
改造Person类:
public class Person
{
public Person() { }
public Person(string name)
{
Name = name;
}
public Person(string name, int age) : this(name)
{
Age = age;
}
public string Name { get; set; }
public int Age { get; set; }
}
注入时指定构造函数
Autofac默认从容器中选择参数最多的构造函数。如果想要选择一个不同的构造函数,就需要在注册的时候就指定它:
builder.RegisterType<Person>().UsingConstructor(typeof(string));
这种写法将指定调用Person(string)构造函数,如该构造函数不存在则报错。
额外的构造函数参数:
有两种方式可以添加额外的构造函数参数,在注册的时候和在检索的时候。在使用自动装配实例的时候这两种都会用到。
注册时添加参数,使用WithParameters()方法在每一次创建对象的时候将组件和参数关联起来。
List<NamedParameter> pars = new List<NamedParameter>() { new NamedParameter("Age", 20), new NamedParameter("Name", "张三") };
builder.RegisterType<Person>().WithParameters(pars);
在检索阶段添加参数:
在Resolve()的时候提供的参数会覆盖所有名字相同的参数,在注册阶段提供的参数会覆盖容器中所有可能的服务。
MVC控制器和WebAPI控制器注入
当然是web MVC项目了,要在MVC或WebApi项目中用autofac,当然需要以下nuget包了,
准备几个Repository和Service;
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
public interface IRepository
{
List<Person> GetPersons();
}
public class RepositoryBase : IRepository
{
public List<Person> Persons { get; set; } = new List<Person>();
public RepositoryBase()
{
for (int i = 0; i < 10; i++)
{
Persons.Add(new Person()
{
Id = i + 1,
Name = "张三" + i,
Age = 10 + i * 2
});
}
}
public List<Person> GetPersons()
{
return Persons;
}
}
public class PersonRepository : RepositoryBase
{
}
public interface IService
{
List<Person> GetPersons();
}
public class ServiceBase : IService
{
public IRepository Repository { get; set; }
public ServiceBase(IRepository repository)
{
Repository = repository;
}
public List<Person> GetPersons()
{
return Repository.GetPersons();
}
}
public class PersonService : ServiceBase
{
public PersonService(IRepository repository) : base(repository)
{
}
}
网站启动时注册容器,在Global的Application_Start方法中注册IOC容器;
//注册IOC容器
var builder = new ContainerBuilder();
//告诉autofac将来要创建的控制器类存放在哪个程序集
builder.RegisterControllers(Assembly.GetExecutingAssembly());//注册MVC控制器
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());//注册WebAPI控制器
//注册Repository和Service
builder.RegisterType<PersonRepository>().As<IRepository>().InstancePerDependency();
builder.RegisterType<PersonService>().As<IService>().InstancePerDependency();
var container = builder.Build();
//将当前容器交给MVC底层,保证容器不被销毁,控制器由autofac来创建
GlobalConfiguration.Configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container);//先给WebAPI注册
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));//再给MVC注册
InstancePerDependency:为你注入的这个服务的生命周期.(注:生命周期我们后面讲)
现在就可以在控制器中通过构造函数注入对象了
public class HomeController : Controller
{
public IService Service { get; set; }
public HomeController(IService service)
{
Service = service;
}
// GET: Home
public ActionResult Index()
{
var ps = Service.GetPersons();
return Json(ps, JsonRequestBehavior.AllowGet);
}
}
属性注入
有时候我们需要对对象的属性进行注入,比如EF上下文对象DbContext,很简单,两句话搞定;
我们先来模拟一个DbContext:
public class DataContext
{
public ICollection<Person> Persons { get; set; } = new List<Person>();
public DataContext()
{
for (int i = 0; i < 10; i++)
{
Persons.Add(new Person()
{
Id = i + 1,
Name = "张三" + i,
Age = 10 + i * 2
});
}
}
}
在IOC容器中注入;
//注册IOC容器
var builder = new ContainerBuilder();
//告诉autofac将来要创建的控制器类存放在哪个程序集
builder.RegisterControllers(Assembly.GetExecutingAssembly()).PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies).InstancePerDependency();//注册MVC控制器
builder.RegisterApiControllers(Assembly.GetExecutingAssembly()).PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies).InstancePerDependency();//注册WebAPI控制器
builder.RegisterFilterProvider();//特性注入
builder.RegisterType<DataContext>().InstancePerRequest();
//注册Repository和Service
builder.RegisterType<PersonRepository>().As<IRepository>().PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies).InstancePerDependency();
builder.RegisterType<PersonService>().As<IService>().PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies).InstancePerDependency();
var container = builder.Build();
//将当前容器交给MVC底层,保证容器不被销毁,控制器由autofac来创建
GlobalConfiguration.Configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container);//先给WebAPI注册
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));//再给MVC注册
PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies)表示属性注入,当实例对象存在有被IOC容器对象托管的时候,IOC容器会自动给属性实例化,Controller则可以不通过构造函数注入,直接属性注入;
关于参数PropertyWiringOptions的三个选项:
PropertyWiringOptions.None:默认的注入方式,当发现注入的对象有属性依赖的时候,会注入一个新的对象;
PropertyWiringOptions.AllowCircularDependencies:循环依赖注入方式,当发现注入的对象有循环依赖关系的时候,会循环注入;
PropertyWiringOptions.PreserveSetValues:保留预设值注入,当注入时发现属性已经有初始值,会自动忽略。
public class HomeController : Controller
{
public DataContext DataContext { get; set; }
public ActionResult GetPersons()
{
return Json(DataContext.Persons, JsonRequestBehavior.AllowGet);
}
}
上面是自动注入属性,有时候我们还可以手动去注入属性:
builder.RegisterType<Person>().OnActivated(e => e.Instance.Name = "李四");
如果你预先知道属性的名字和值,你还可以使用:
builder.RegisterType<Person>().WithProperty("Name", "李四");
方法注入
可以实现方法注入的方式有两种。
1、使用Activator
如果你使用委托来激活,只要调用这个方法在激活中:
builder.Register(c =>
{
var result = new Person();
result.SayHello("my name is van");
return result;
});
注意,使用这种方法,Person类里必须要有这个方法:
public void SayHello(string hello)
{
Console.WriteLine(hello);
}
2、使用Activating Handler
如果你使用另外一种激活,比如反射激活,创建激活的事件接口OnActivating,这种方式仅需一行代码:
builder.RegisterType<Person>().OnActivating(e => e.Instance.SayHello("my name is van!"));
SignalR注入
如果我们项目中还用到了SignalR这样的socket通信框架,我们也可以通过autofac来进行对象的依赖注入;按照刚才hangfire的套路,我们先引入SignalR的一些基础包,并配置好SignalR;当然,还要引入SignalR的autofac支持库:
注册容器的方式和上面的例子都有些不同了,SignalR的IOC容器注入必须在Startup里面注入,不能在Global中注入,因为SignalR是Owin项目,OWIN 集成常见错误为使用GlobalHost。OWIN中配置你会抓狂. OWIN集成中,任何地方你都不能引用 。当年博主我也不太清楚,反正当时也踩了这一大坑。至于hangfire也是owin项目,为什么可以在Global里面注入,因为hangfire不是用GlobalHost去注入的,而是GlobalConfiguration。
public class Startup
{
public void Configuration(IAppBuilder app)
{
var builder = new ContainerBuilder();
var config = new HubConfiguration();
builder.RegisterHubs(Assembly.GetExecutingAssembly()).PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies);
builder.RegisterType<DataContext>().InstancePerLifetimeScope();
var container = builder.Build();
config.Resolver = new AutofacDependencyResolver(container);
app.UseAutofacMiddleware(container);
app.MapSignalR("/signalr", config);
}
}
然后,hub就可以用属性注入,构造函数注入等方式了;
[HubName("myhub")]//声明hub的显式名字
public class MyHub : Hub
{
public DataContext DataContext { get; set; }
public void Send()
{
Clients.All.hello(DataContext.Persons.ToJsonString());
}
}
前端代码:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>WebSocket</title>
<script src="~/Scripts/jquery-3.3.1.js"></script>
<script src="~/Scripts/jquery.signalR-2.2.3.js"></script>
<script src="~/signalr/hubs"></script><!--后端SignalR根据注册的路由生成的js脚本-->
</head>
<body>
<span class="msg"></span>
</body>
</html>
<script>
$(function () {
//客户端都以驼峰命名法使用
let hubProxy = $.connection["myhub"]; //hub代理对象
var $msg = $(".msg");
//注册客户端方法
hubProxy.client.hello = function (msg) {
$msg.text(msg);
}
//向服务端发数据
$.connection.hub.start().done(function () {
hubProxy.server.send();
});
});
</script>
注意:由于 SignalR 是内部构件,所以不支持SignalR每请求的生命周期依赖。
对象生命周期InstancePerDependency
对每一个依赖或每一次调用创建一个新的唯一的实例。也称作瞬态或者工厂,使用PerDependency作用域,服务对于每次请求都会返回互补影响实例。在没有指定其他参数的情况下,这也是默认的创建实例的方式。
InstancePerLifetimeScope
在一个生命周期域中,每一个依赖或调用创建一个单一的共享的实例,且每一个不同的生命周期域,实例是唯一的,不共享的,也就是线程内唯一对象。
InstancePerMatchingLifetimeScope
在一个做标识的生命周期域中,每一个依赖或调用创建一个单一的共享的实例。打了标识了的生命周期域中的子标识域中可以共享父级域中的实例。若在整个继承层次中没有找到打标识的生命周期域,则会抛出异常:DependencyResolutionException。
InstancePerOwned
在一个生命周期域中所拥有的实例创建的生命周期中,每一个依赖组件或调用Resolve()方法创建一个单一的共享的实例,并且子生命周期域共享父生命周期域中的实例。若在继承层级中没有发现合适的拥有子实例的生命周期域,则抛出异常:
SingleInstance
每一次依赖组件或调用Resolve()方法都会得到一个相同的共享的实例。其实就是单例模式。
InstancePerRequest
在一次Http请求上下文中,共享一个组件实例。仅适用于asp.net mvc开发。
绑定事件
在对象生命周期的不同阶段使用事件。Autofac暴露五个事件接口供实例的按如下顺序调用
1)OnRegistered
2)OnPreparing
3)OnActivated
4)OnActivating
5)OnRelease
这些事件会在注册的时候被订阅,或者被附加到IComponentRegistration 的时候。
builder.RegisterType<Person>().OnRegistered(e => Console.WriteLine("在注册的时候调用!")).OnPreparing(e => Console.WriteLine("在准备创建的时候调用!")).OnActivating(e => Console.WriteLine("在创建之前调用!")).OnActivated(e => Console.WriteLine("创建之后调用!")).OnRelease(e => Console.WriteLine("在释放占用的资源之前调用!"));
以上示例输出如下:
OnActivating
组件被创建之前调用,在这里你可以:
1)将实例转向另外一个或者使用代理封装它
2)进行属性注入
3)执行其他初始化工作
OnActivated
在component被完全创建的时候调用一次。在这个时候你可以执行程序级别的一些工作(这些工作依赖于对象被完全创建)-这种情况很罕见。
OnRelease
替代component的标准清理方法。实现了IDisposable 接口的标准清理方法(没有标记为ExternallyOwned) 通过调用Dispose 方法。没有实现IDisposable或者被标记为ExternallyOwned的清理方法是一个空函数-不执行任何操作。OnRelease 就是用来覆盖默认的清理行为的。
.NetCore中使用Autofac
在 Starpup 中 配置 Autofac,注意的是要将 ConfigureServices 的返回类型从void类型 改成IServiceProvider,并 return new AutofacServiceProvider(ApplicationContainer); 官方解释是,让第三方容器接管Core的默认DI。
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddControllersAsServices().AddViewComponentsAsServices().AddTagHelpersAsServices();//将控制器以及视图开启属性注入方式
var builder = new ContainerBuilder();
builder.RegisterType<DataContext>().InstancePerLifetimeScope();
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()).Where(t => t.Name.EndsWith("Repository") || t.Name.EndsWith("Service")).AsSelf().AsImplementedInterfaces().PropertiesAutowired(PropertyWiringOptions.PreserveSetValues).InstancePerDependency();
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()).Where(t => t.Name.EndsWith("Controller")).AsSelf().PropertiesAutowired().InstancePerDependency();//注册控制器为属性注入,如果你想要控制器支持属性注入,这句很重要
builder.Populate(services);
var container = builder.Build();
return new AutofacServiceProvider(container);
}