Introduction
The .NET Framework provides the capability of loading assemblies at runtime without having to reference them directly within your project at design time. This makes it possible to extend or customize an application without having to re-compile the entire project for that one simple (or complex) change. This concept helps you design and create customizable off the shelf solutions (COTS) for your customers while also making your applications more scalable and stable.
Background
For instance, say you’re designing an e-commerce application and one of the features you wish to provide is a shipping rate calculator for over-night and second day delivery. The easiest option is to create a set of internal functions that handles these calculations, and then call the appropriate one based on the user’s selection. This will work fine at first, however what happens when a new option is required? As you can imagine, this would not scale very well and could eventually lead to some big headaches down the road.
To resolve this obstacle, you could instead package up all of the shipping rate logic into self contained assemblies and then load them at runtime as they are needed. When a new requirement comes along, you simply create a new project that fulfills that requirement, compile it into a new assembly, place that assembly into the project’s “/bin” folder and then reference it within your web.config file or by adding a new record into your database. This new assembly will then get loaded via reflection, thereby making it available for use within your application.
As you can imagine, there are a few design considerations that must be factored in when implementing this approach; however, it is not as difficult as it may sound. To begin, you will first need to identify all of the necessary methods and properties that your object will require. In the shipping example above, we will create an IShipping
interface that will define our contracts from which all shipping objects will derive from. For simplicity, this interface will define a single CalculateShipping
method that accepts an OrderHeader
object as a single parameter, along with some other read-only properties that we will use for descriptions of each individual object. Next up, we will create a new class within its own class library project and then implement the IShipping
interface. This class will then contain a CalculateShipping
method that performs the custom logic required to calculate shipping based on the given OrderHeader
object.
Using the Code
Sounds simple enough, let’s examine the code. First up, let’s review the OrderHeader
object. As you can see, it’s a simple CLR object containing all properties and no methods. In turn, it references a list of OrderDetail
objects which in turn contains a list of Products
that are being ordered.
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; }
}
}
Next up is our IShipping
interface. Note how there is no code described here as it is strictly used to define a contract from which all other objects will implement.
namespace Dynamic.Model
{
public interface IShipping
{
double CalculateShipping(OrderHeader orderHeader);
string ShippingType { get; }
string Description { get; }
}
}
Lastly, we have a shipping object that performs a simple calculation and returns a value. This object can reside within its own separate assembly, however you will need to reference the assembly containing the IShipping
interface so that it can be properly implemented and used by your library.
namespace Overnight
{
public class Shipping : Dynamic.Model.IShipping
{
public double CalculateShipping(OrderHeader orderHeader)
{
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"; } }
}
}
Since we defined an interface and we are loading our shipping assemblies dynamically at runtime, we are not limited to a single shipping solution. Below is another example of a slightly more complex calculator that determines the shipping rate based on the total weight of all products within the incoming order.
namespace SecondDayShipping
{
public class Shipping : Dynamic.Model.IShipping
{
public double CalculateShipping(OrderHeader orderHeader)
{
double totalWeight = 0;
double shippingRate = 0;
foreach (OrderDetail detail in orderHeader.OrderDetails)
{
totalWeight += detail.Product.Weight * detail.Quantity;
}
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"; } }
}
}
Now that we have our interfaces and shipping objects set up and defined, we have to program our application to actually use them. A less dynamic approach would be to create an instance of a shipping object using an early binding approach such as the example below, however this is exactly what we are trying to avoid.
function Main()
{
Overnight.Shipping shipping = new Overnight.Shipping();
double overNightRate = shipping.CalculateShipping(orderHeader);
}
Ideally, we will instead use reflection to load an assembly given our IShipping
contract, and then instantiate that object so that we can call its CalculateShipping
method. For this approach to work, we must first have access to the compiled assembly by either placing it within the GAC or the application's /bin folder. Second, we must then pass in the fully qualified name of the resource that we are trying to instantiate using the following format: “Namespace.Classname, AssemblyName
”. Note that this is case sensitive and if you are unsure what your assembly name is, click on Project – Properties – Application and the name will be listed under Assembly name.
function Main()
{
IShipping secondDay = this.CreateShippingInstance(
"SecondDayShipping.Shipping,SecondDayShipping");
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
{
throw new NotImplementedException();
}
}
Points of Interest
The included project contains all of the code required to see this in action, however, my example does take a slight shortcut when I call CreateShippingInstance
and pass in a hardcoded string
within the Main
method. Ideally, this method would pull the assembly information from a database or some other configuration file so that they can be added, removed or updated without having to re-compile the entire project. How you do that is up to you and may be different depending upon your own unique needs and requirements.
With a properly designed system, updates and enhancements become a breeze and your applications will be much more scalable, stable and flexible as a result.
History
- 27th April, 2011: Initial post