丹尼大叔

数学专业毕业,爱上编程的大叔,兴趣广泛。使用博客园这个平台分享我工作和业余的学习内容,以编程交友。有朋自远方来,不亦乐乎。

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

摘要

提供者是特殊的工厂类,Ninject使用它来实例化解析类型。任何时候我们绑定一个服务类型到一个组件,我们都隐式地关联那个服务类型到一个可以实例化那个组件的提供者。这个隐藏的提供者被称为StandardProvider,是一个通用的工厂,他可以创建每一个给定类型的实例。尽管我们可以经常依赖StandardProvider而不用对他在背后做了什么费心,Ninject也允许我们创建和注册我们自己自定义的提供者,只要我们需要自定义这个下面的激活过程:

1 Bind<IService>().ToProvider<MyService>();
2 public class MyServiceProvider : Provider<MyService>
3 {
4     protected override MyService CreateInstance(IContext context)
5     {
6         return new MyService();
7     }
8 }

尽管继承这个Provider<T>类是推荐的方法来创建一个自定义提供者。作为一个提供者,为一个类继承这个IProvider接口在Ninject就足够了:

1 public interface IProvider
2 {
3     Type Type { get; }
4     object Create(IContext context);
5 }

回顾上一篇文章的ShippersSqlRepository类和ShippersXmlRepository类,他们的的构造函数都有一个字符串类型的参数。

ShippersSqlRepository构造函数参数northwindConnectionString提供连接字符串:

 1 public class ShippersSqlRepository : IShippersRepository
 2 {
 3     private readonly NorthwindContext objectContext;
 4     public ShippersSqlRepository(string northwindConnectionString)
 5     {
 6         objectContext = new NorthwindContext(northwindConnectionString);
 7     }
 8     public IEnumerable<Business.Model.Shipper> GetShippers()
 9     { ... }
10     public void AddShipper(Business.Model.Shipper shipper)
11     { ... }
12 }

ShippersXmlRepository构造函数参数xmlRepositoryPath提供xml文件路径:

 1 public class ShippersXmlRepository : IShippersRepository
 2 {
 3     private readonly string documentPath;
 4     public ShippersXmlRepository(string xmlRepositoryPath)
 5     {
 6         this.documentPath = xmlRepositoryPath;
 7     }
 8     public IEnumerable<Shipper> GetShippers()
 9     { ... }
10     public void AddShipper(Shipper shipper)
11     { ... }
12 }

在这个情况下,这些参数阻止了Ninject实例化我们的repository,因为这个kernel对怎样解析字符串参数没有任何主意。因此,下面几行对于注册我们的repository还不够:

1   Bind<IShippersRepository>().To<ShippersSqlRepository>()
2   .When(r => r.Target.Name.StartsWith("source"));
3   Bind<IShippersRepository>().To<ShippersXmlRepository>()
4   .When(r => r.Target.Name.StartsWith("target"));

一个提供需要的参数的方法是使用这个WithConstructorArgument方法:

 1     connection = ConfigurationManager.AppSettings["northwindConnectionString"];
 2     Bind<IShippersRepository>()
 3     .To<ShippersSqlRepository>()
 4     .When(r => r.Target.Name.StartsWith("source"))
 5     .WithConstructorArgument("NorthwindConnectionString", connection);
 6     path = ConfigurationManager.ConnectionStrings["xmlRepositoryPath"];
 7     Bind<IShippersRepository>()
 8     .To<ShippersXmlRepository>()
 9     .When(r => r.Target.Name.StartsWith("target"))
10     .WithConstructorArgument("XmlRepositoryPath",path);

它看起来很好,这时候我们不需要注册很多需要这样的配置的repository。然而,在更复杂的情况下,我们需要以某种方式自动注入这些参数。这里所有的这些设置是字符串的实例。因此,我们可以为字符串类型创建一个提供者,基于参数名称来生成我们的配置字符串。这个提供者将在应用程序配置文件(web.config或者app.config)的键里查找参数名,如果这样的一个配置是定义好的(像下面代码一样),它就返回它的值:

 1 using Ninject.Activation;
 2 using System;
 3 using System.Configuration;
 4 
 5 namespace DataMigration.Business.Provider
 6 {
 7     public class ConfigurationProvider : Provider<string>
 8     {
 9         protected override string CreateInstance(IContext context)
10         {
11             if (context.Request.Target == null)
12             {
13                 throw new Exception("Target required.");
14             }
15             var paramName = context.Request.Target.Name;
16             string value = ConfigurationManager.AppSettings[paramName];
17             if (string.IsNullOrEmpty(value))
18             {
19                 value = ConfigurationManager.ConnectionStrings[paramName] == null ? "" 
20                     : ConfigurationManager.ConnectionStrings[paramName].ConnectionString;
21             }
22             return value;
23         }
24     }
25 }

 

ConfigurationProvider被提供了一个包含了当前激活过程所有信息的context对象,包含在这篇文章之前提到过的请求对象。这个请求对象包含目标信息。在这种情况下,目标信息是哪个注入的字符串对象作为构造函数参数。如果被请求的字符串类型是直接从kernel使用Get<string>()方法请求的,目标对象将为空。因为我们需要参数名称作为配置键,我们首先检查目标。使用目标名称,我们可以查找AppSettings,如果我们没有找到这样一个配置,我们将继续在ConnectionStrings部分查找。最后,返回得到的值。

唯一的问题是这个提供者将被注册为字符串类型,他将影响到将要被Ninject解析的任何字符串。为了明确规定是那些将要被认为是应用程序配置的字符串,我们定义一个自定义特性,像下面这样在那些参数上运用它:

1 using System;
2 
3 namespace DataMigration.Business.Attributes
4 {
5     [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
6     public class ConfigurationAttribute : Attribute { }
7 }

我们已经声明了这个特性只能运用在属性和参数上。下面是这个特性怎样运用到我们repository类构造函数参数上:

1      public ShippersSqlRepository([Configuration]string connectionString)
2         {
3             _context = new NorthwindContext(connectionString);
4         }
5 
6      public ShippersXmlRepository([Configuration]string xmlRepositoryPath)
7         {
8             this.documentPath = xmlRepositoryPath;
9         }

最后,绑定代码像下面这样:

1          Bind<string>().ToProvider<ConfigurationProvider>().WhenTargetHas<ConfigurationAttribute>();

激活上下文

当我们的提供者重载这个CreateInstance方法,我们使用这个上下文context对象,通过方法参数传入。这个对象的类继承IContext接口,这个接口包含了非常多所有跟当前激活过程相关的信息。使用这个对象,我们可以访问当前绑定对象,正在被解析的类型,正在被注入的类型,我们当前在依赖图的什么位置,谁请求了这个解析,等等。在解析一个依赖图的时候,为每一个正在解析的类型创建一个上下文对象,这就导致了一个激活上下文图。从每一个上下文对象开始,我们也可以从他的父上下文节点导航,直到到达图的根,就是最初的发起请求的点。在使用Niinject时,任何我们需要决定怎样解析依赖的地方上下文对象都是可以得到的。

工厂方法

工厂方法是另一个通知Ninject怎样解析一个依赖。像创建一个提供者一样,我们已经访问了这个激活上下文对象,来帮助我们做出决定怎样解析请求类型。然而,我们不需要创建一个新类,我们可以只是内联地写出我们的解析逻辑。工程方法是提供者类的一个很好的替代,在这里解析逻辑是简单和简短的。一个好的使用工厂方法的例子是在一个类里实例化一个日志对象。下面是不使用DI实例化一个日志对象的代码:

1 class ConsumerClass
2 {
3     private ILog log = LogManager.GetLogger(typeof(ConsumerClass));
4 }

我们可以在前面的类中使用下面的代码实现DI:

1 class ConsumerClass
2 {
3     private ILog log;
4     public ConsumerClass(ILog log)
5     {
6         this.log = log;
7     }
8 }    

为ILogger接口用To<T>()方法注册一个类型绑定是不合适的,因为具体的日志对象必须通过调用LogManager.GetLogger方法来创建,而不是通过具体日志类的构造函数。在这种情况下,我们可以使用一个工厂方法来通知Ninject创建一个新日志对象:

1 Bind<ILog>().ToMethod(ctx => LogManager.GetLogger(ctx.Request.ParentRequest.Service));

这个ctx的类型是IContext,我们从这个Ninject激活上下文父请求的服务属性中,得到消费者类的类型。

 

posted on 2016-11-20 21:38  丹尼大叔  阅读(875)  评论(2编辑  收藏  举报