Autofac系列一、注册
一、注册概念
我们通过创建ContainerBuilder来注册组件并告诉容器哪些组件暴露了哪些服务。
组件可以通过反射(注册指定的.net类或开发结构的泛型)创建;通过提供现成的实例(你已创建的一个对象实例)创建,或者通过lambda表达式(一个执行实例化对象的匿名方法)来创建ContainerBuilder包含一组Register()方法来帮你实现以上操作。
每个组件暴露一个或多个服务,它们使用ContainerBuilder上的As()方法连接起来。
// Create the builder with which components/services are registered. var builder = new ContainerBuilder(); // Register types that expose interfaces... builder.RegisterType<ConsoleLogger>().As<ILogger>(); // Register instances of objects you create... var output = new StringWriter(); builder.RegisterInstance(output).As<TextWriter>(); // Register expressions that execute to create objects... builder.Register(c => new ConfigReader("mysection")).As<IConfigReader>(); // Build the container to finalize registrations // and prepare for object resolution. var container = builder.Build(); // Now you can resolve services using Autofac. For example, // this line will execute the lambda expression registered // to the IConfigReader service. using(var scope = container.BeginLifetimeScope()) { var reader = scope.Resolve<IConfigReader>(); }
1、反射组件
1.1、通过类型注册
通过反射生成的组件通常是由类型注册的。
var builder = new ContainerBuilder(); builder.RegisterType<ConsoleLogger>(); builder.RegisterType(typeof(ConfigReader));
当使用基于反射的组件时,Autofac自动为你的类从容器中寻找匹配拥有最多参数的构造方法。
假如,你有三个构造函数的类
public class MyComponent { public MyComponent() { /* ... */ } public MyComponent(ILogger logger) { /* ... */ } public MyComponent(ILogger logger, IConfigReader reader) { /* ... */ } }
现在在你的容器中注册组件和服务
var builder = new ContainerBuilder(); builder.RegisterType<MyComponent>(); builder.RegisterType<ConsoleLogger>().As<ILogger>(); var container = builder.Build(); using(var scope = container.BeginLifetimeScope()) { var component = scope.Resolve<MyComponent>(); }
当你解析组件时,Autofac发现ILogger已被注册,但你并没有注册IConfigReader,在这种情况下,第二个构造方法会被选中因为它是能在容器中找到最多参数的那个。
基于反射的组件有个重要的需要注意的地方:任何通过RegisterType 注册的组件必须是具体的类型,虽然组件可以暴漏抽象类和接口作为服务,但你不能注册一个抽象类/接口组件,你这样想就明白了:在幕后Autofac其实是创建了一个你注册对象的实例,你 无法'new up"一个抽象类或一个接口,你得有个具体的实现,对吧。
1.2、指定构造函数
你可以使用UsingConstructor方法和构造方法中一系列代表参数类型的类型来手动指定一个构造函数,通过这种方式使用和覆盖注册组件自动选择的构造函数:
builder.RegisterType<MyComponent>() .UsingConstructor(typeof(ILogger), typeof(IConfigReader));
要注意的是,在解析时你仍然需要提供必要的参数,否则在你尝试解析对象时将出现错误,你可以在注册时传参或在解析时传递参。
2、实例组件
有时候,你也许会希望提前生成一个对象的实例并将它加入容器以提供注册组件时使用,你可以通过使用RegisterInstance方法:
var output = new StringWriter(); builder.RegisterInstance(output).As<TextWriter>();
当你这样做时你需要考虑一些事情,Autofac自动处理已注册组件的释放,你也许想自己来控制生命周期而不是让Autofac来帮你调用Dispose释放对象。在这种情况下,你需要通过ExternalOwned方法来注册实例:
var output = new StringWriter(); builder.RegisterInstance(output) .As<TextWriter>() .ExternallyOwned();
当将autofac集成到一个现有的应用程序(已存在一个单例实例且需要在容器中被组件使用)时,注册已提供的实例组件同样非常方便,而不是直接把这些组件绑定到单例实例上,它可以在容器中注册为一个实例:
builder.RegisterInstance(MySingleton.Instance).ExternallyOwned();
这样能确保静态单例最终能被容器管理取而代之。
通过某一实例暴露的默认服务是该实例的实体类。详见“服务vs组件”。
3、Lambda表达式组件
反射在组件创建时是个很好的默认选择,但是当组件创建不只是简单的构造方法调用时,事情将变得混乱起来。
Autofac接收一个委托或者lambda表达式,用作组件创建者:
builder.Register(c => new A(c.Resolve<B>()));
表达式提供的参数c是组件上下文(一个IComponentContext对象),此处该组件被创建,你可以使用它来解析容器中的其它组件。从而帮助你创建组件,重要的是使用它而不是一个闭包来访问容器,这样可以保证对象精确的释放并且可以很好的支持嵌套容器和自定义清理。
使用该下文参数可以满足其他依赖的成功引入-例如,A需要一个构造方法参数B,而B可能还有其他的依赖关系。
通过expression-created提供的默认服务是推断表达式的返回类型。
下面是一些需求的例子,通过反射组件创建非常笨拙,但是通过lambda表达式非常漂亮的处理来。
-
复杂参数
构造函数参数不能总是用简单的常量值声明。而不是用xml配置语法构造某种类型的值,使用代码:
builder.Register(c => new UserSession(DateTime.Now.AddMinutes(25)));
当然会话到期你可能想在配置文件中指定一些事情。
-
参数注入
Autofac提供了一流的方法可用来完成参数注入,你可以使用表达式和属性初始化来填充参数:
builder.Register(c => new A(){ MyB = c.ResolveOptional<B>() });
ResolveOptional 方法会尝试解析。但如果服务没有注册,也不会抛出异常(如果服务注册成功,但是无法解析成功,依然抛出异常)这是解析服务的一种方式。
在大多数情况下不推荐使用属性注入。替代空对象模式,重载构造函数或构造函数参数默认值,可以创造更清洁、“不变”的组件,使用构造函数注入依赖项。
-
通过参数值选择具体的实现
分离组件创建最大的好处就是具体类型可以变化,这通常是在运行时完成,而不仅仅是在配置时:
builder.Register<CreditCard>( (c, p) => { var accountId = p.Named<string>("accountId"); if (accountId.StartsWith("9")) { return new GoldCard(accountId); } else { return new StandardCard(accountId); } });
在这个例子中,CreateCard通过两个类实现,GoldCard和StandardCard,实例化哪个类依赖于运行时提供的accountId。
在这个例子中,创建方法通过第二个参数p提供参数。
使用下面的代码进行注册:
var card = container.Resolve<CreditCard>(new NamedParameter("accountId", "12345"));
如果使用委托创建CreditCard
实例声明和一个委托工厂,可以获得一个清洁,类型安全的语法。
4、开放泛型组件
Autofac支持开放泛型,使用RegisterGeneric()方法:
builder.RegisterGeneric(typeof(NHibernateRepository<>)) .As(typeof(IRepository<>)) .InstancePerLifetimeScope();
当容器请求一个匹配的服务类型时,Autofac将会找到对应的封闭类型的具体实现:
// Autofac will return an NHibernateRepository<Task> var tasks = container.Resolve<IRepository<Task>>();
注册具体的服务类型 (e.g. IRepository<Person>
) 会覆盖开放类型的版本
5、服务vs组件
注册组件时,我们得告诉Autofac,组件需要暴露哪些服务,默认的类型注册时大部分情况下暴露它们自身:
// This exposes the service "CallLogger" builder.RegisterType<CallLogger>();
组件能够被它暴露的服务解析:
// This will work because the component // exposes the type by default: scope.Resolve<CallLogger>(); // This will NOT work because we didn't // tell the registration to also expose // the ILogger interface on CallLogger: scope.Resolve<ILogger>();
你可以让一个组件暴露任意数量的服务:
builder.RegisterType<CallLogger>() .As<ILogger>() .As<ICallInterceptor>();
暴露服务后,你就可以解析基于该服务的组件了,一旦你将组件暴露为一个特定的服务,默认的服务(组件类型)将被覆盖:
// These will both work because we exposed // the appropriate services in the registration: scope.Resolve<ILogger>(); scope.Resolve<ICallInterceptor>(); // This WON'T WORK anymore because we specified // service overrides on the component: scope.Resolve<CallLogger>();
如果你既想组件暴露一系列特定的服务,又想让它暴露默认的服务,可以使用AsSelf方法:
builder.RegisterType<CallLogger>() .AsSelf() .As<ILogger>() .As<ICallInterceptor>();
这样所有的解析就能成功了:
// These will all work because we exposed // the appropriate services in the registration: scope.Resolve<ILogger>(); scope.Resolve<ICallInterceptor>(); scope.Resolve<CallLogger>();
6、默认注册
如果不止一个组件暴露了相同的服务,Autofac将使用最后一个注册的组件作为服务的提供方:
builder.RegisterType<ConsoleLogger>().As<ILogger>();
builder.RegisterType<FileLogger>().As<ILogger>();
上例中,FileLogger将会作为ILogger默认的服务提供方,因为它是最后被注册的。
想要覆盖这种行为,使用PreserveExistingDefaults()方法修改:
builder.RegisterType<ConsoleLogger>().As<ILogger>();
builder.RegisterType<FileLogger>().As<ILogger>().PreserveExistingDefaults();
上例中ConsoleLogger将会作为ILogger默认的服务提供方因为最后注册的FileLogger使用了PreserveExistingDefaults()。
7、有条件的注册
提示:有条件的注册自Autofac 4.4.0 引入
大多数情况下,像上面那样默认的注册其实已经足够让我们在运行时成功的解析正确的组件了。我们可以使用PreserveExistingDefaults()来保证组件以正确的顺序被注册。。对于复杂的条件和行为我们可以利用lambda表达式/委托 注册处理。
但依然有这样的场景:
- 你不想在程序中有些功能在正常运作的情况下某个组件还会出现. 例如, 如果你解析了
IEnumerable<T>
的服务(一堆服务), 所有实现了这些服务的已注册组件都将被返回, 不管你是否使用了PreserveExistingDefaults()
. 大多数情况下这样也行, 但在某些极端情况下你不希望如此。 - 你只想要在其他一些组件 未被 注册的情况下才注册组件; 或者只想在其他一些组件 已被 注册的情况下. 你不会从容器中解析出你不想要的东西, 并且你也不用修改已经创建的容器. 能够基于其他的注册情况来进行有条件的组件注册非常好用。
有两种好用的注册扩赞方法可用:
- OnlyIf() : 提供一个使用
IComponentRegistry
的lambda
确定是否应该进行注册。 - IfNotRegistered() : 如果其他服务已经注册,则停止注册。
这些方法在ContainerBuilder.Build() 时执行并且以实际组件注册的顺序执行。示例:
var builder = new ContainerBuilder(); // Only ServiceA will be registered. // Note the IfNotRegistered takes the SERVICE TYPE to // check for (the As<T>), NOT the COMPONENT TYPE // (the RegisterType<T>). builder.RegisterType<ServiceA>() .As<IService>(); builder.RegisterType<ServiceB>() .As<IService>() .IfNotRegistered(typeof(IService)); // HandlerA WILL be registered - it's running // BEFORE HandlerB has a chance to be registered // so the IfNotRegistered check won't find it. // // HandlerC will NOT be registered because it // runs AFTER HandlerB. Note it can check for // the type "HandlerB" because HandlerB registered // AsSelf() not just As<IHandler>(). Again, // IfNotRegistered can only check for "As" // types. builder.RegisterType<HandlerA>() .AsSelf() .As<IHandler>() .IfNotRegistered(typeof(HandlerB)); builder.RegisterType<HandlerB>() .AsSelf() .As<IHandler>(); builder.RegisterType<HandlerC>() .AsSelf() .As<IHandler>() .IfNotRegistered(typeof(HandlerB)); // Manager will be registered because both an IService // and HandlerB are registered. The OnlyIf predicate // can allow a lot more flexibility. builder.RegisterType<Manager>() .As<IManager>() .OnlyIf(reg => reg.IsRegistered(new TypedService(typeof(IService))) && reg.IsRegistered(new TypedService(typeof(HandlerB)))); // This is when the conditionals actually run. Again, // they run in the order the registrations were added // to the ContainerBuilder. var container = builder.Build();
8、注册配置
可以使用XML或编程配置“modules”来提供一组注册或者在运行时改变注册。也可以使用Autofac modules提供一些动态注册生成或条件注册逻辑。
9、提供动态注册
Autofac modules 对于介绍动态注册逻辑是最简单的方法。你可以在解析的时候使用一个module 为一个服务动态的添加一个log4net 日志实例,
如果你需要更多的动态特性,如添加一个新的隐式关系类型的支持,你可能想要用先进的理念检查注册源部分。
二、注册时传参
注册组件时你可以提供一组参数,可以在基于该组件的服务解析时使用(如果你想要在解析时提供参数,当然也可以)
1、可使用的参数类型
Autofac提供了多种不同的参数匹配机制:
- NamedParameter: 通过名称匹配目标参数
- TypeofParameter: 通过匹配目标参数(需要匹配具体的类型)
- ResolvedParameter: 复杂参数的匹配
NamedParameter和TypeParameter只支持常量值。
ResolvedParameter 可以用于提供不同的值来从容器中动态获取对象, 例如, 通过名字解析服务
2、反射组件的参数
当注册一个基于反射的组件时,类型的构造方法也许会需要一个无法从容器中解析出来的参数,你可以在注册时提供该值。
假如有个configuration reader需要传入一个configuration section name:
public class ConfigReader : IConfigReader { public ConfigReader(string configSectionName) { // Store config section name } // ...read configuration based on the section name. }
你可以使用lambda表达式组件:
builder.Register(c => new ConfigReader("sectionName")).As<IConfigReader>();
或者在反射组件注册时传参:
// Using a NAMED parameter: builder.RegisterType<ConfigReader>() .As<IConfigReader>() .WithParameter("configSectionName", "sectionName"); // Using a TYPED parameter: builder.RegisterType<ConfigReader>() .As<IConfigReader>() .WithParameter(new TypedParameter(typeof(string), "sectionName")); // Using a RESOLVED parameter: builder.RegisterType<ConfigReader>() .As<IConfigReader>() .WithParameter( new ResolvedParameter( (pi, ctx) => pi.ParameterType == typeof(string) && pi.Name == "configSectionName", (pi, ctx) => "sectionName"));
3、Lambda表达式组件的参数
使用lambda表达式组件注册,不是在注册是传入参数,而是在服务解析时传入具体的参数。
在组件注册表达式时,你可以通过改变委托签名使用传入参数进行注册,代替接收一个IComponentContext参数,一个IConpontentContext和IEnumerable<Parameter>参数:
// 使用两个参数来注册委托 // c = The current IComponentContext to dynamically resolve dependencies // p = An IEnumerable<Parameter> with the incoming parameter set builder.Register((c, p) => new ConfigReader(p.Named<string>("configSectionName"))) .As<IConfigReader>();
当你解析参数时,你的lambda将使用这些参数来传入值:
var reader = scope.Resolve<IConfigReader>(new NamedParameter("configSectionName", "sectionName"));
三、属性和方法的注入
尽管使用构造方法注入是一种传值给组件的首选方法,还可以使用属性或方法注入来传值。
属性注入 使用可写属性而不是构造方法传参来完成注入。方法注入 通过调用方法来设置依赖。
1、属性注入
如果组件是一个lambda表达式组件,使用对象构造器:
builder.Register(c => new A { B = c.Resolve<B>() });
为了支持循环依赖,可以使用 actived event handler:
builder.Register(c => new A()).OnActivated(e => e.Instance.B = e.Context.Resolve<B>());
如果组件是一个反射组件,使用PropertiesAutowired()修饰语来注入属性:
builder.RegisterType<A>().PropertiesAutowired();
如果需要绑定一个特定的属性和它的值,使用WithProperty()修饰语:
builder.RegisterType<A>().WithProperty("PropertyName", propertyValue);
2、方法注入
想要调用一个方法来设置组件上的某个值,最简单的方法是使用lambda表达式组件,然后在activator中进行正确的方法调用:
builder.Register(c => { var result = new MyObjectType(); var dep = c.Resolve<TheDependency>(); result.SetTheDependency(dep); return result; });
如果你无法使用lambda表达式,你可以添加一个activeing event handler:
builder .Register<MyObjectType>() .OnActivating(e => { var dep = e.Context.Resolve<TheDependency>(); e.Instance.SetTheDependency(dep); });
四、程序集扫描
Autofac可以在程序集中通过约定找到和注册组件,你可以扫描和注册单独的类型,也可以专门扫描Autofac模块。
1、扫描类型
Autofac也可以被认为是约定驱动注册或扫描的,它可以根据用户指定的规则注册一组类型:
var dataAccess = Assembly.GetExecutingAssembly(); builder.RegisterAssemblyTypes(dataAccess) .Where(t => t.Name.EndsWith("Repository")) .AsImplementedInterfaces();
每次RegisterAssemblyTypes()方法调用将应用一组规则-如果有多组不同的组件注册时,我们有必要多次调用RegisterAssemblyTypes()。
1.1、过滤类型
RegisterAssemblyTypes()包含一个或多个程序集的数组作为参数,默认地,程序中所有具体的类将被注册。包括内部类(internal)和嵌套私有类,你可以使用linq表达式过滤注册的类型集合。
4.8.0版本中PuiblicOnly()扩展方法被引入了,这使得数据的封装变得更容易了,如果你只想要你的公有方法被注册,使用PublicOnly():
builder.RegisterAssemblyTypes(asm)
.PublicOnly();
要过滤这册的类型,使用Where()表达式:
builder.RegisterAssemblyTypes(asm) .Where(t => t.Name.EndsWith("Repository"));
要从扫描类型中排除类型,使用Except()表达式:
builder.RegisterAssemblyTypes(asm)
.Except<MyUnwantedType>();
Except() 表达式同样允许你自定义被排除类型的注册规则:
builder.RegisterAssemblyTypes(asm) .Except<MyCustomisedType>(ct => ct.As<ISpecial>().SingleInstance());
也可以使用多个过滤条件,它们将会以AND逻辑连接。
builder.RegisterAssemblyTypes(asm) .PublicOnly() .Where(t => t.Name.EndsWith("Repository")) .Except<MyUnwantedRepository>();
1.2、指定服务
RegisterAssemblyTypes()的注册语法是单个类型注册语法的超集,因此类似As()方法同样适用于程序集:
builder.RegisterAssemblyTypes(asm) .Where(t => t.Name.EndsWith("Repository")) .As<IRepository>();
As()和Named()方法还有接收lambda表达式的重载,表达式决定了对于一个类型而言,它提供了哪些服务:
builder.RegisterAssemblyTypes(asm) .As(t => t.GetInterfaces()[0]);
和普通组件注册一样,多次调用As()方法,暴露的服务叠加。
为了能够更容易的建立常用的约定,Autofac添加了一些额外的注册方法。
Method | Description | Example |
---|---|---|
AsImplementedInterfaces() |
Register the type as providing all of its public interfaces as services (excluding IDisposable ). |
builder.RegisterAssemblyTypes(asm)
.Where(t => t.Name.EndsWith("Repository"))
.AsImplementedInterfaces();
|
AsClosedTypesOf(open) |
Register types that are assignable to a closed instance of the open generic type. |
builder.RegisterAssemblyTypes(asm)
.AsClosedTypesOf(typeof(IRepository<>));
|
AsSelf() |
The default: register types as themselves - useful when also overriding the default with another service specification. |
builder.RegisterAssemblyTypes(asm)
.AsImplementedInterfaces()
.AsSelf();
|
2、扫描模块
我们通过RegisterAssemblyModules()方法进行模块的扫描,正如它名字表达式的意思那样,它通过提供的程序集扫描Autofac模块,创建模块的实例,然后使用当前的container builder来注册它们。
例如,假设两个普通的模块类在同一个程序集中,并且每个模块注册一个组件:
public class AModule : Module { protected override void Load(ContainerBuilder builder) { builder.Register(c => new AComponent()).As<AComponent>(); } } public class BModule : Module { protected override void Load(ContainerBuilder builder) { builder.Register(c => new BComponent()).As<BComponent>(); } }
RegisterAssemblyModules()的重载 不接受类型参数,它将会注册所提供程序集列表中的所有实现IModule的类,下面的例子中 所有的模块都将被注册:
var assembly = typeof(AComponent).Assembly; var builder = new ContainerBuilder(); // Registers both modules builder.RegisterAssemblyModules(assembly);
使用泛型类型参数的RegisterAssemblyModules()的重载允许你指定一个所有模块都必须从它派生的基类,在下面的示例中,只有一个模块被注册了,因为扫描被限制了:
var assembly = typeof(AComponent).Assembly; var builder = new ContainerBuilder(); // Registers AModule but not BModule builder.RegisterAssemblyModules<AModule>(assembly);
使用 一个Type对象参数 的 RegisterAssemblyModules()
和使用泛型类型参数的重载作用差不多但它允许你指定一个也许会在运行时才被决定的type. 在下面的示例中 只有一个模块 被注册了因为扫描被限制了:
var assembly = typeof(AComponent).Assembly; var builder = new ContainerBuilder(); // Registers AModule but not BModule builder.RegisterAssemblyModules(typeof(AModule), assembly);
3、IIS托管的Web应用
当使用IIS托管的应用中使用程序集扫描时,你可能会因为程序集的位置遇到一个小小的问题。
应用第一次启动时IIS托管应用里面所有的程序集都被加载进AppDomain,但是 当AppDomain被IIS回收时,程序集只会按需加载。
为了避免这个问题,使用位于System.Web.Compilation.BuildManager 的 GetReferencedAssemblies()方法来获取相关程序集的列表:
var assemblies = BuildManager.GetReferencedAssemblies().Cast<Assembly>();
它会立刻强制相关的程序集加载斤AppDomain,使其可以被用于模块扫描。