摘要
既然在插件模型里,每一个服务类型可以被映射到多个实现,绑定方法不用决定要返回哪个实现。因为kernel应该返回所有的实现。然而,上下文绑定是多个绑定场景,在这个场景里,kernel需要根据给定的条件,在多个提供的类型里选择一个实现。
附:代码下载
在下面的例子里,我们将要实现一个数据迁移的应用程序,可以将数据从SQL数据库迁移到XML数据文件。将有一个表现层,一个业务逻辑层和一个数据访问层。
按下面的步骤建立DataMigration基本程序结构。
1. 下载Northwind数据库备份,还原到SQL Server。
2. 创建解决方案DataMigration,并在解决方案下添加下面的工程。
3. 在DataMigration.Business工程下添加如下文件夹结构。
4. 在Model文件夹下添加Shipper.cs文件。
1 namespace DataMigration.Business.Model 2 { 3 public class Shipper 4 { 5 public int ShipperID { get; set; } 6 7 public string CompanyName { get; set; } 8 } 9 }
5. 在Interface文件夹内添加IShippersRepository.cs文件。
1 using DataMigration.Business.Model; 2 using System.Collections.Generic; 3 4 namespace DataMigration.Business.Interface 5 { 6 public interface IShippersRepository 7 { 8 IEnumerable<Shipper> GetShippers(); 9 10 void AddShipper(Shipper shipper); 11 } 12 }
6. 在DataMigration.SqlDataAccess工程里创建ShippersSqlRepository类,使用EntityFramework读写数据库,实现IShippersRepository接口。
1 using DataMigration.Business.Interface; 2 using System.Collections.Generic; 3 using DataMigration.Business.Model; 4 5 namespace DataMigration.SqlDataAccess 6 { 7 public class ShippersSqlRepository : IShippersRepository 8 { 9 private readonly NorthwindContext _context; 10 11 public ShippersSqlRepository(string connectionString) 12 { 13 _context = new NorthwindContext(connectionString); 14 } 15 16 public void AddShipper(Shipper shipper) 17 { 18 if (shipper.ShipperID == 0) 19 { 20 _context.Shippers.Add(shipper); 21 } 22 else 23 { 24 var entity = _context.Shippers.Find(shipper.ShipperID); 25 if (entity != null) 26 { 27 entity.CompanyName = shipper.CompanyName; 28 } 29 } 30 _context.SaveChanges(); 31 } 32 33 public IEnumerable<Shipper> GetShippers() 34 { 35 return _context.Shippers; 36 } 37 } 38 }
NorthwindContext:
1 using DataMigration.Business.Model; 2 using System.Data.Entity; 3 4 namespace DataMigration.SqlDataAccess 5 { 6 public class NorthwindContext : DbContext 7 { 8 public NorthwindContext(string connectionString) 9 { 10 base.Database.Connection.ConnectionString = connectionString; 11 } 12 public DbSet<Shipper> Shippers { get; set; } 13 } 14 }
7. 在DataMigration.XMLDataAccess工程里添加ShippersXmlRepository类,使用System.Xml.Linq读写XML文件,实现IShippersRepository接口。
1 using DataMigration.Business.Interface; 2 using DataMigration.Business.Model; 3 using System; 4 using System.Collections.Generic; 5 using System.Linq; 6 using System.Xml.Linq; 7 8 namespace DataMigration.XMLDataAccess 9 { 10 public class ShippersXmlRepository : IShippersRepository 11 { 12 private readonly string documentPath; 13 14 public ShippersXmlRepository(string xmlRepositoryPath) 15 { 16 this.documentPath = xmlRepositoryPath; 17 } 18 19 public IEnumerable<Shipper> GetShippers() 20 { 21 var document = XDocument.Load(documentPath); 22 return from e in document.Elements("Shipper") 23 select new Shipper 24 { 25 ShipperID = Convert.ToInt32(e.Element("ShipperID").Value), 26 CompanyName = e.Element("CompanyName").Value 27 }; 28 } 29 30 public void AddShipper(Shipper shipper) 31 { 32 var document = XDocument.Load(documentPath); 33 document.Root.Add(new XElement("Shipper", 34 new XElement("ShipperID", shipper.ShipperID), 35 new XElement("CompanyName", shipper.CompanyName))); 36 document.Save(documentPath); 37 } 38 } 39 }
在DataMigration.XMLDataAccess工程里添加文件Northwind.xml,并设置文件属性Copy to Output Directory:
Northwind.xml文件内容:
<?xml version="1.0" encoding="utf-8" ?> <Northwind> </Northwind>
8. 修改DataMigration.Console工程里的App.config文件。
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> </configSections> <connectionStrings> <add name="connectionString" providerName="System.Data.SqlClient" connectionString="Data Source=localhost;Initial Catalog=NORTHWND;Integrated Security=True" /> </connectionStrings> <appSettings> <add key="xmlRepositoryPath" value="Northwind.xml"/> </appSettings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> </configuration>
connectionString:数据库连接字符串。xmlRepositoryPath:Northwind.xml文件名
9. 添加ShippersService.cs文件。
1 using DataMigration.Business.Interface; 2 using DataMigration.Business.Model; 3 4 namespace DataMigration.Business 5 { 6 public class ShippersService 7 { 8 private readonly IShippersRepository sourceRepository; 9 private readonly IShippersRepository targetRepository; 10 11 public void MigrateShippers() 12 { 13 foreach (Shipper shipper in sourceRepository.GetShippers()) 14 { 15 targetRepository.AddShipper(shipper); 16 } 17 } 18 } 19 }
这些repositories类型应该是什么,以及他们应该怎样生成?这些问题的答案可以让这个应用程序变成松耦合易维护的程序,或者变成高耦合很难维护的代码。最容易的方法是创建一个XmlRepository对象和一个SQLRepository对象,在ShippersService类里像下面这样实例化他们:
1 // The following code leads to a tightly coupled code 2 var sourceRepository = new ShippersSqlRepository(); 3 var targetRepository = new ShippersXmlRepository();
用这种方法,我们将使得我们的服务依赖于这些具体的repository,将我们的业务逻辑层绑定到数据访问层。可能会修改或者替换数据访问层,而不修改业务逻辑层,然后重新编译。尽管我们的应用层看起来是分离的,他们实际上是紧密的耦合的,代码很难维护。
也可以像下面使用构造函数生成repository。
1 public ShippersService(IShippersRepository sourceRepository, 2 IShippersRepository targetRepository) 3 { 4 this.sourceRepository = sourceRepository; 5 this.targetRepository = targetRepository; 6 }
ShippersService类现在变成好的可重用性了。可以将Shipper实例不但从SQL迁移到XML,也可以在任何数据源中间迁移数据,只要他们实现了IShippersRepository接口。有趣的是我们可以很容易地向相反方向迁移数据而不用修改我们的ShippersService类或者数据访问层。
我们知道Ninject将注入具体的repository到ShipperService类的构造函数中。但是请等一下,构造函数的两个参数的类型都是IShippersRepository接口。Ninject怎么知道哪一个具体类型注册到哪个参数?上下文绑定是这个问题的答案。让我们一个一个地看这些不同的解决方案。
准备工作:
1. 使用NutGet向工程DataMigration.Console添加Ninject Package和Entity Framework Package。
2. 在工程DataMigration.Console里添加CompositionModule类。
1 using Ninject.Modules; 2 3 namespace DataMigration 4 { 5 public class CompositionModule : NinjectModule 6 { 7 public override void Load() 8 { 9 10 } 11 } 12 }
CompositionModule类继承NinjectModule接口,在Load方法里调用一系列Bind方法,管理绑定类型。
名称绑定
名称绑定是最简单的方法,在这个方法里我们可以向我们的绑定和我们的目标参数指派名称,Ninject就可以决定在哪个目标参数上使用哪个绑定。我们需要向目标以及他们对应的绑定插入名称:
1 public ShippersService( 2 [Named("Source")]IShippersRepository sourceRepository, 3 [Named("Target")]IShippersRepository targetRepository) 4 { 5 this.sourceRepository = sourceRepository; 6 this.targetRepository = targetRepository; 7 }
DataMigration.Console工程添加System.Configuration引用。在CompositionModule类添加using System.Configuration语句。添加下面的代码到CompositionModule类的Load方法:
1 Bind<IShippersRepository>().To<ShippersSqlRepository>().Named("Source") 2 .WithConstructorArgument("connectionString", ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString); 3 Bind<IShippersRepository>().To<ShippersXmlRepository>().Named("Target") 4 .WithConstructorArgument("xmlRepositoryPath", ConfigurationManager.AppSettings["xmlRepositoryPath"]);
修改DataMigration.Console工程里的Main函数:
1 using DataMigration.Business; 2 using Ninject; 3 using System; 4 5 namespace DataMigration 6 { 7 class Program 8 { 9 static void Main(string[] args) 10 { 11 var kernel = new StandardKernel(new CompositionModule()); 12 var shippersService = kernel.Get<ShippersService>(); 13 shippersService.MigrateShippers(); 14 15 Console.ReadLine(); 16 } 17 } 18 }
运行程序后,到$\DataMigration.Console\bin\Debug文件夹下,找到Northwind.xml文件。打开该文件,得到从SQL Server迁移到XML文件的结果:
<?xml version="1.0" encoding="utf-8"?> <Northwind> <Shipper> <ShipperID>1</ShipperID> <CompanyName>Speedy Express</CompanyName> </Shipper> <Shipper> <ShipperID>2</ShipperID> <CompanyName>United Package</CompanyName> </Shipper> <Shipper> <ShipperID>3</ShipperID> <CompanyName>Federal Shipping</CompanyName> </Shipper> </Northwind>
既然我们已经用名称区分了IShipperRepository不同的实现,也可以用下面的语法从kernel对象获得他们:
kernel.Get<IShippersRepository>("Source");
然而,用这种方法解析实例是不推荐的,因为使用这种方法,Ninject将被误用,变成实现服务定位器的反模式。
解析元数据
1. 使用NuGet向DataMigration.Business工程添加Ninject引用。
2. 为每一个绑定提供一些元数据,在类型解析的时候这些元数据将被鉴定(判断)。下面演示如何在Bind方法中设置元数据。
在CompositionModule类的Load方法中,在Bind方法调用后,调用WithMetadata方法,设置(提供或注入)元数据:
1 Bind<IShippersRepository>().To<ShippersSqlRepository>().WithMetadata("IsSource", true) 2 .WithConstructorArgument("connectionString", ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString); 3 Bind<IShippersRepository>().To<ShippersXmlRepository>().WithMetadata("IsSource", false) 4 .WithConstructorArgument("xmlRepositoryPath", ConfigurationManager.AppSettings["xmlRepositoryPath"]);
3. 联系目标和它们对应的绑定。定义一个自定义的ConstraintAttribute类,这是一个抽象类,提供了一个方法匹配特性目标和它所需要的绑定。下面添加这么一个特性类。
在DataMigration.Business工程的Attributes文件夹下,添加类IsSourceAttribute。
1 using Ninject; 2 3 namespace DataMigration.Business.Attributes 4 { 5 public class IsSourceAttribute : ConstraintAttribute 6 { 7 private readonly bool isSource; 8 9 public IsSourceAttribute(bool isSource) 10 { 11 this.isSource = isSource; 12 } 13 14 public override bool Matches(Ninject.Planning.Bindings.IBindingMetadata metadata) 15 { 16 return metadata.Has("IsSource") && metadata.Get<bool>("IsSource") == isSource; 17 } 18 } 19 }
IsSourceAttribute类继承ConstraintAttribute抽象类,重载了抽象方法Matches。
public abstract bool Matches(IBindingMetadata metadata);
参数metadata包含了Bind注入的元数据信息。使用Has方法和Get方法获得这些信息。
3. 应用这个特性到目标上面,将他们和他们对应的绑定关联起来。
在ShippersService构造函数中插入IsSource特性。
1 public ShippersService( 2 [IsSource(true)]IShippersRepository sourceRepository, 3 [IsSource(false)]IShippersRepository targetRepository) 4 { 5 this.sourceRepository = sourceRepository; 6 this.targetRepository = targetRepository; 7 }
注意:
- 我们可以在解析关联服务的时候,提供尽量多的正在使用的,需要的元数据到我们的绑定。
Bind<IService>().To<Component>() .WithMetadata("Key1", value1) .WithMetadata("Key2", value2) .WithMetadata("Key3", value3);
- 我们也可以提供尽量多的需要的约束特性到绑定目标上。像下面的代码:
public Consumer([Constraint1(value1, value2), Constraint2(value), Constraint3]IService dependency) { }
请记住名称绑定场景也是使用元数据实现的。下面的代码演示了怎样实现一个基于正则匹配而不是基于相同名称的,自定义的约束特性,来解析名称绑定。
1 public class NamedLikeAttribute : ConstraintAttribute 2 { 3 private readonly string pattern; 4 5 public NamedLikeAttribute(string namePattern) 6 { 7 this.pattern = namePattern; 8 } 9 10 public override bool Matches(IBindingMetadata metadata) 11 { 12 return metadata.Has("Named") && System.Text.RegularExpressions.Regex.IsMatch(metadata.Get<string>("Named"), pattern);13 } 14 }
给定一个正则表达式,上面的特性可以运用到目标上。绑定的名称将被正则表达式判断,名称是否匹配。
为绑定提供元数据:
1 Bind<IShippersRepository>().To<ShippersSqlRepository>().WithMetadata("Named", "SourceRepository"); 2 Bind<IShippersRepository>().To<ShippersXmlRepository>().WithMetadata("Named", "TargetRepository");
将特性插入到目标参数,关联对应的绑定:
1 public Consumer([NamedLike(@"source\w+") dependency) 2 { 3 ... 4 }
基于特性的绑定
尽管名称绑定用起来很简单,元数据很灵活很强大,但是这两个方法都需要依赖的类的类库引用Ninject类库,这样更容易出错。打错了名字或者元数据的键,编译器不会发出警告。
下面的代码演示怎样使用这个基于特性的绑定技术而不引用Ninject类库。
先定义一些自定义特性:
1 using System; 2 3 namespace DataMigration.Business.Attributes 4 { 5 public class SourceAttribute : Attribute { } 6 public class TargetAttribute : Attribute { } 7 }
然后这些特性可以像下面运用在目标参数上:
1 public ShippersService( 2 [Source]IShippersRepository sourceRepository, 3 [Target]IShippersRepository targetRepository) 4 { 5 this.sourceRepository = sourceRepository; 6 this.targetRepository = targetRepository; 7 }
现在,我们需要用下面的代码注册我们的绑定:
1 Bind<IShippersRepository>().To<ShippersSqlRepository>().WhenTargetHas<SourceAttribute>() 2 .WithConstructorArgument("connectionString", ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString); 3 Bind<IShippersRepository>().To<ShippersXmlRepository>().WhenTargetHas<TargetAttribute>() 4 .WithConstructorArgument("xmlRepositoryPath", ConfigurationManager.AppSettings["xmlRepositoryPath"]);
我们不但可以在参数上运用这些特性,我们也可以在类上面或者在类的其他注册成员上运用这些特性,例如,自身的构造函数。
下面的绑定演示如何基于一个特性,在一个消费者类上做条件绑定:
Bind<IService>().To<MyService>().WhenClassHas<MyAttribute>();
下面是一个关联MyAttribute的消费者类:
1 [MyAttribute] 2 Public class Consumer {...}
这是我们怎样在构造函数中运用这样一个特性:
1 [MyAttribute] 2 public Consumer(IServive service) { ... }
类成员可以是构造函数本身,或者甚至是另一个方法,或者一个注入的属性。
基于目标条件
另一种决定使用哪个绑定的方法是基于目标条件。Ninject提供了几个帮助方法,可以限制匹配的绑定的数量。下面演示一个例子。
在这个例子中,我们有两个服务类名称是SourceShipperService和TargetShipperService,两个都依赖于IShippersRepository接口。
下面是服务类的结构:
1 public class SourceShipperService 2 { 3 public SourceShipperService(IShippersRepository repository) 4 { ... } 5 } 6 public class TargetShipperService 7 { 8 public TargetShipperService(IShippersRepository repository) 9 { ... } 10 }
为了告诉Ninject哪个具体repository应该被注入到哪个服务,我们可以基于服务类型本身为条件,而不是任何的特性或者元数据。
下面的代码演示如何用这种方式注册我们的类型,将ShippersXmlRepository和ShippersSqlRepository实例分别注入到SourceShipperService和TargetShipperService类:
1 Bind<IShippersRepository>().To<ShippersXmlRepository>() 2 .WhenInjectedInto<SourceShipperService>(); 3 Bind<IShippersRepository>().To<ShippersSqlRepository>() 4 .WhenInjectedInto<TargetShipperService>();
注意,即使目标类是T类型的子类,WhenInjectedInto<T>方法也将被匹配。如果我们确切想要指定的类型,我们应该使用下面替代的方法:
1 Bind<IShippersRepository>().To<ShippersSqlRepository>() 2 .WhenInjectedExactlyInto<TargetShipperService>();
一般帮助方法
正如我们已经看到的,前面所有的方式都是利用了名字以WhenXXX结尾的帮助方法。所有的这些方法是一个更一般化的When方法的具体版本。这个功能强大的帮助方法提供一个回馈机制的参数,参数包含当前绑定请求的所有信息,这些信息就包含了目标信息。下面演示如何为数据迁移程序使用帮助方法注册类型:
1 Bind<IShippersRepository>().To<ShippersSqlRepository>().When(r => r.Target.Name.StartsWith("source")) 2 .WithConstructorArgument("connectionString", ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString); 3 Bind<IShippersRepository>().To<ShippersXmlRepository>().When(r => r.Target.Name.StartsWith("target")) 4 .WithConstructorArgument("xmlRepositoryPath", ConfigurationManager.AppSettings["xmlRepositoryPath"]);
前面的代码,只要目标IShippersRepository参数的名称以source开头,就绑定到ShippersSqlRepository。第二个绑定也用了类似的规则。