使用接口和反射动态加载 .NET 程序集






4.75/5 (4投票s)
本文介绍您可能希望在运行时加载程序集的原因以及如何实现它。
引言
.NET Framework 提供了在运行时加载程序集的功能,而无需在设计时直接引用它们。这使得在不重新编译整个项目的情况下,可以扩展或自定义应用程序以进行简单的(或复杂的)更改。这一概念有助于您为客户设计和创建可定制的现成解决方案 (COTS),同时还能使您的应用程序更具可伸缩性和稳定性。
背景
例如,假设您正在设计一个电子商务应用程序,并且您希望提供的一项功能是对隔夜和次日送达的运费计算器。最简单的选择是创建一组处理这些计算的内部函数,然后根据用户的选择调用适当的函数。起初这会运行良好,但是当需要新的选项时会怎样?正如您所能想象到的,这将无法很好地扩展,并可能在将来导致一些严重的麻烦。
为了解决这个障碍,您可以将所有运费逻辑打包到独立的程序集中,然后在需要时于运行时加载它们。当出现新的需求时,您只需创建一个满足该需求的新项目,将其编译成一个新的程序集,将该程序集放在项目的“/bin”文件夹中,然后通过在 web.config 文件中引用它或向数据库中添加新记录来引用它。然后,这个新的程序集将通过反射被加载,从而可以在您的应用程序中使用。
正如您所能想象到的,在实现这种方法时需要考虑一些设计因素;但是,这并不像听起来那么难。首先,您将需要确定对象所需的所有必要方法和属性。在上面的运费示例中,我们将创建一个 IShipping
接口,它将定义所有运费对象都将派生的契约。为了简单起见,该接口将定义一个接受 OrderHeader
对象作为单个参数的 CalculateShipping
方法,以及一些我们将用于描述每个独立对象的只读属性。接下来,我们将在其自己的类库项目中创建一个新类,然后实现 IShipping
接口。然后,该类将包含一个 CalculateShipping
方法,该方法执行所需的自定义逻辑,以根据给定的 OrderHeader
对象计算运费。
Using the Code
听起来很简单,让我们来看看代码。首先,让我们回顾一下 OrderHeader
对象。正如您所看到的,它是一个简单的 CLR 对象,包含所有属性但没有方法。反过来,它引用一个 OrderDetail
对象列表,而 OrderDetail
对象又包含一个正在订购的 Products
列表。
namespace Dynamic.Model
{
public class OrderHeader
{
public int OrderHeaderId { get; set; }
public DateTime OrderDate { get; set; }
public int OrderNumber { get; set; }
public string ShipToName { get; set; }
public string ShipToStreet { get; set; }
public string ShipToCity { get; set; }
public string ShipToState { get; set; }
public string ShipToZip { get; set; }
public double OrderTotal { get; set; }
public List<orderdetail> OrderDetails { get; set; }
public OrderHeader()
{
this.OrderDetails = new List<orderdetail>();
}
}
public class OrderDetail
{
public int OrderDetailId { get; set; }
public Product Product { get; set; }
public int Quantity { get; set; }
}
public class Product
{
public int ProductId { get; set; }
public string Sku { get; set; }
public string ProductName { get; set; }
public string ProductDescription { get; set; }
public double Weight { get; set; }
public double Price { get; set; }
}
}
接下来是我们的 IShipping
接口。请注意,这里没有描述任何代码,因为它严格用于定义所有其他对象将实现的契约。
namespace Dynamic.Model
{
public interface IShipping
{
double CalculateShipping(OrderHeader orderHeader);
string ShippingType { get; }
string Description { get; }
}
}
最后,我们有一个进行简单计算并返回值的运费对象。此对象可以驻留在其自己的独立程序集中,但是您需要引用包含 IShipping
接口的程序集,以便您的库能够正确地实现和使用它。
namespace Overnight
{
public class Shipping : Dynamic.Model.IShipping
{
public double CalculateShipping(OrderHeader orderHeader)
{
// Our shipping is simply 5 percent of the current order
return orderHeader.OrderTotal * .05;
}
public string ShippingType { get { return "Over night shipping rate"; } }
public string Description { get {
return "This class calculates shipping to be five percent of the order total"; } }
}
}
由于我们定义了一个接口并且正在运行时动态加载运费程序集,因此我们不限于单一的运费解决方案。下面是另一个稍微复杂的计算器的示例,该计算器根据传入订单中所有产品的总重量来确定运费。
namespace SecondDayShipping
{
public class Shipping : Dynamic.Model.IShipping
{
public double CalculateShipping(OrderHeader orderHeader)
{
double totalWeight = 0;
double shippingRate = 0;
// Do some extra logic here to find out how much our order weighs
foreach (OrderDetail detail in orderHeader.OrderDetails)
{
totalWeight += detail.Product.Weight * detail.Quantity;
}
// Different rates for different weights
if (totalWeight > 100)
shippingRate = 20;
else if (shippingRate > 50)
shippingRate = 10;
else
shippingRate = 5;
return shippingRate;
}
public string ShippingType { get { return "Second day shipping rate"; } }
public string Description { get
{ return "This class calculates shipping for Second Day rates"; } }
}
}
现在我们已经设置并定义了我们的接口和运费对象,我们必须对我们的应用程序进行编程以实际使用它们。一种不那么动态的方法是使用早期绑定方法创建运费对象实例,例如下面的示例,但这正是我们试图避免的。
function Main()
{
// This example shows how an Overnight shipping object is created
// using early binding. This method will work,
// however it is not very flexible for future updates
Overnight.Shipping shipping = new Overnight.Shipping();
double overNightRate = shipping.CalculateShipping(orderHeader);
}
理想情况下,我们将使用反射来加载给定 IShipping
契约的程序集,然后实例化该对象,以便我们可以调用其 CalculateShipping
方法。为了使此方法起作用,我们必须首先能够访问已编译的程序集,方法是将其放置在 GAC 或应用程序的 /bin 文件夹中。其次,我们必须使用以下格式传递我们要实例化的资源的完全限定名:“Namespace.Classname, AssemblyName
”。请注意,这是区分大小写的,如果您不确定程序集名称,请单击项目 - 属性 - 应用程序,名称将在程序集名称下显示。
function Main()
{
// This example shows how to create a SecondDay shipping object via reflection
// First, we must get a reference to the proper assembly.
// To do so, we pass in the name of the fully qualified class name
// and the name of the assembly where the class is located
IShipping secondDay = this.CreateShippingInstance(
"SecondDayShipping.Shipping,SecondDayShipping");
// Once we have an instance of this, we can call the method
// defined by our IShipping interface to return the calculated price
double secondDayRate = secondDay.CalculateShipping(orderHeader);
}
public IShipping CreateShippingInstance(string assemblyInfo)
{
Type assemblyType = Type.GetType(assemblyInfo);
if (assemblyType != null)
{
Type[] argTypes = new Type[] { };
ConstructorInfo cInfo = assemblyType.GetConstructor(argTypes);
IShipping shippingClass = (IShipping)cInfo.Invoke(null);
return shippingClass;
}
else
{
// Error checking is needed to help catch instances where
throw new NotImplementedException();
}
}
关注点
包含的项目包含查看此功能的全部代码,但是,在我调用 CreateShippingInstance
并向 Main
方法中传递硬编码的 string
时,我的示例会采取一些捷径。理想情况下,此方法会从数据库或其他配置文件中提取程序集信息,以便在不重新编译整个项目的情况下添加、删除或更新它们。如何做到这一点取决于您,并且可能因您自己独特的需要和要求而异。
通过正确设计的系统,更新和增强将变得轻而易举,您的应用程序也将因此更具可伸缩性、稳定性和灵活性。
历史
- 2011年4月27日:首次发布