深入浅出依赖注入容器——Autofac

1,061 阅读9分钟

[深入浅出依赖注入容器——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 { getset; }
    public int Age {getset; }
}

接下来准备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);
}