65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (4投票s)

2011 年 4 月 27 日

CPOL

4分钟阅读

viewsIcon

65058

downloadIcon

980

本文介绍您可能希望在运行时加载程序集的原因以及如何实现它。

引言

.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日:首次发布
© . All rights reserved.