Practical .NET

Dynamically Loading Classes at Runtime

Making the right runtime design decisions can help -- or harm -- your program.

Extensible applications allow you to add functionality without rewriting your application: you dynamically select the classes that make up your application at runtime rather than design time. I once used this technique in an application created for a client who, in turn, sold the application to its customers. Because the application delegated much of its work to a set of classes, it could be extended by replacing those classes. Because I was using runtime composition, customers only had to drop a DLL into my application's folder and make some entries in a configuration file. My application read the configuration file and instantiated the classes specified.

The Microsoft .NET Framework gives you two options for implementing runtime configuration: Reflection and the Managed Extensibility Framework (MEF). Reflection, which has always been part of the .NET Framework, has two benefits: complete flexibility and the ability to work with any .NET class. The MEF provides a more structured way to load DLLs at runtime, but requires classes built to work with it and is only available in the .NET Framework 4. Regardless of which mechanism you choose, you need to make some critical design decisions around managing the runtime dependencies between your application and the DLLs you load.

Implementing Reflection
For dynamic runtime configuration, Reflection gives you the ability to instantiate a class with just two pieces of information: the path to the DLL and the full name of the class (including the namespace). This example loads a DLL called DelegatedAssembly.DLL and creates an instance of a class called DynamicClass from that DLL:

Dim asb As System.Reflection.Assembly
asb = System.Reflection.Assembly.Load("DelegatedAssembly")

Dim cls As Object
cls = asb.CreateInstance("DelegatedAssembly.DynamicClass")

Where you get the names of your DLL and class is up to you: your application can read them in from a configuration file or a database table, or just deduce the right names from other clues in the environment.

However, working with an Object isn't desirable: Your application will need to call specific methods on the class, so you'll need to know what the method names are, what parameters should be passed and what will be returned from the method. While you're loading the object at runtime, at design time you want to know what methods you have access to. There are two techniques that will let you do that: inheritance and interfaces.

Defining the Dynamic Class
To use inheritance, provide a base class from which your dynamically loaded classes inherit. This is the right design in two scenarios: If you know the dynamically loaded classes won't have to inherit from any other class, or if you have base code to be shared among all the implementations of dynamic classes. A typical base class might look like this:

Public MustInherit Class BaseDynamic

  Public MustOverride Function SayHello(Name As String) As String

  Public Overridable Function DefaultSayHello(Name As String) As String
    Return "Hi, " & Name
End Function

End Class

However, if you want to allow any object -- no matter what other class it may be derived from -- to be used as a dynamically loaded class, you're better off using interfaces. With an interface, any object can be used as a dynamic object regardless of what class it might inherit from. A typical interface might look like this:

Public Interface IDynamic

  Function SayHello(Name As String) As String

End Interface
A dynamically loaded class based on the interface would look like this:
Public Class DynamicAddedClass
  Implements DynamicInterface.IDynamic

Public Function SayHello(Name As String) As String _
  Implements DynamicInterface.IDynamic.SayHello
  Return "Hello, " & Name
End Function

End Class

You don't need to add a reference to the class library's DLL in order to load the DLL at runtime. However, at design time, when writing code for your application, Visual Studio won't let you use the interface to declare variables that will work with your dynamically loaded classes unless it knows about the interface. You have two options: Declare your variables using the dynamic keyword and give up IntelliSense, or add to your project a reference to the DLL holding your interface or base class (you can set the reference's CopyLocal property to False).

Regardless of whether you use interfaces or inheritance, you've made all the dynamically loaded classes look alike, regardless of their actual name. The application only needs to declare a variable using the base type (if you're using inheritance) or the interface type (if you've defined a common interface) to work with any dynamically loaded class.

Injecting the Dependency
While the code to load your class is simple, deciding where to put it is more interesting. The first design decision you'll need to make falls under the heading "dependency resolution": internal or external. If you put the code that loads your DLL in the mainline of your application, your application becomes responsible for finding and loading the class (an internal dependency).

To create your dependency externally, use a factory class to create your dynamic object and returning it to your application. Using an external dependency has two benefits. First, if the code for creating the dynamic class is at all complicated, using a factory makes the code in your application simpler. And second, your factory method is also easier to maintain and extend because it has only one job: To find and load the dynamic object. If the application needs to exercise some control over how the dynamic object is created, the factory method can accept parameters from the application.

For me, the choice is a no-brainer: I use an external dependency injection with a factory class. Generally speaking, I declare my factory methods as static so that the class doesn't need to be instantiated, and put it in the same library project that holds the base class or interface that defines my dynamic class.

A typical factory might look like this:

Public Class DynamicFactory

Public Shared Function GetComposite() As Object
  Dim asb As System.Reflection.Assembly
  asb = System.Reflection.Assembly.Load("DelegatedAssembly")
   
  Dim cls As Object = 
    asb.CreateInstance("DelegatedAssembly.DynamicClass")
  Return cls
End Function

End Class

Typical application code that uses the factory would look like this:

Dim cls As DynamicInterface.IDynamic
cls = DynamicInterface.DynamicFactory.GetComposite

After retrieving the dynamic class from the factory, the application should pass it to a class that contains the code that uses the dynamic class (the host class). This lets you control when and where you'll pass the reference to the dynamic object to the host class. Here, you have three choices as to where the dynamic class will be injected into the host class: site, setter or constructor.

Site injection means that the dynamic object is passed as a parameter to the method on the host class that needs it. A method that accepts the dynamic object looks like this:

Public Function SaysHello(DClass As DynamicInterface.IDynamic) As String
  Return DClass.SayHello("Pat")
End Function

The application code that accepts the dynamic object from the factory and passes it to the method looks like this:

Dim cls As DynamicInterface.IDynamic
cls = DynamicInterface.DynamicFactory.GetComposite
res = udc.SaysHello(cls)

This design makes sense if the dynamic object is only needed for that method and, especially, if you might pass a different object every time the method is called. If, however, the dynamic object is used in many places through the life of its host -- and especially if the host object will only use one instance of the dynamic object -- a better choice is constructor injection. With constructor injection, the dynamic object is passed to the host when the host is instantiated.

The host object has code like this:

Private ReadOnly _Dclass As DynamicInterface.IDynamic
Public Sub New(Dclass As DynamicInterface.IDynamic)
  _Dclass = Dclass
End Sub

The application code to retrieve the dynamic object from the factory and pass it to the host object looks like this:

Dim cls As DynamicInterface.IDynamic
cls = DynamicInterface.DynamicFactory.GetComposite
Dim udc As New UsesDelegatedClass(cls)

Setter dependency strikes a middle-ground: The dynamic object is passed to a property on the host and code within the host object accesses the dynamic object through that property. This design makes sense if the host doesn't always need the dynamic object or if the dynamic object might be changed during the life of the host. The host class needs code like this:

Public Property DClass As DynamicInterface.IDynamic

The application code to set the property on the host with the dynamic class retrieved from the factory looks like this:

Dim cls As DynamicInterface.IDynamic
cls = DynamicInterface.DynamicFactory.GetComposite
udc.DClass = cls

A method that uses the dynamic class would look like this:

Public Function Hello(HelloName As String) As String
  Return DClass.SayHello(HelloName)
End Function

At this point you have a design that incorporates five components.

The first is the interface or base object that defines the dynamic classes. Second are the dynamic classes themselves. Third, you have the host class that uses the dynamic class (accepting the dynamic class at the constructor, through a property, or at the site where the dynamic class is used). The fourth component is the factory responsible for creating the dynamic class at class. The fifth (and final) component is the application responsible for coordinating the objects: calling methods on the factory and passing the returned dynamic class to the host class that uses it.

The Managed Extensibility Framework
While using Reflection gives you a great deal of flexibility, it doesn't provide much support for dynamic configuration. If you'd like to put some structure around dynamic runtime configuration while accessing some higher-order functionality in dynamic composition, you want the MEF. You'll have to upgrade to the .NET Framework 4 (if you haven't already).

Many things don't change when you move to the MEF. For instance, it's still a good idea to define a base class or an interface to specify the dynamic class. You'll also still need to decide where to inject your dependency (site, setter, constructor). What the MEF gives you is a built-in factory for finding and loading your application, which you can wrap inside your factory class. In addition, the MEF gives you more ways to find your assembly and adds in the ability to define criteria for selecting among multiple different assemblies.

To use the MEF, you'll need a reference to the System.Component-Model.Composition library and three Import statements:

Imports System.ComponentModel.Composition
Imports System.ComponentModel.Composition.Hosting
Imports System.ComponentModel.Composition.AttributedModelServices

In the MEF, a CompositionContainer contains Catalogs that, in turn, contain references to assemblies that may (or may not) contain classes you want to load. Rather than load assemblies directly, however, Catalogs give you the ability to specify how assemblies are to be selected. For instance, if you want to select all the assemblies in a folder, you would use a DirectoryCatalog and pass that catalog to the CompositionContainer.

Typically, your factory class will hold the logic that creates the CompositionContainer an application will use. The following method creates a DirectoryCatalog using the pathname for the factory's assembly (because I'm using a Shared/static method, I can't use a reference to Me, and so have to specify the type of the class):

Public Shared Function GetContainer() As CompositionContainer
Dim asm As System.Reflection.Assembly = 
  GetType(DynamicInterface.DynamicFactory).Assembly
Dim strPath = System.IO.Path.GetDirectoryName(asm.Location)
Dim dirCat As New DirectoryCatalog(strPath)

If you want to search multiple locations for assemblies, you can create an AggregateCatalog and load multiple Catalogs into it:

Dim agCat As New AggregateCatalog
agCat.Catalogs.Add(dirCat)
agCat.Catalogs.Add(dirCat2)

Once you've created your Catalog you can create your Composition-Container, passing the Catalog to it. Your factory method would then return the container to the application:

Dim con As CompositionContainer
con = New CompositionContainer(dirCat)
Return con

To gain access to the dynamic class, you define a property to be bound to the dynamically loaded class. That property must be decorated with an Import attribute that specifies the basis for selecting the class (its "contract"). Assuming that you're using an interface to define your dynamic class, you'd use code like this to create a property called DClass that specifies the interface as its contract:

<Import(GetType(DynamicInterface.IDynamic))>
Public Property DClass As DynamicInterface.IDynamic

To load the class and bind it to the property, you call the Compose-Parts extension method on the container. This code in an application calls the factory method to get the container, and then calls the ComposeParts method on the container to kick off dynamic loading:

Dim con As CompositionContainer con = DynamicInterface.DynamicFactory.GetContainer con.ComposeParts(Me)

The ComposeParts method searches the Catalogs and binds classes to the properties in the object passed to the method decorated with Import attributes. Because I'm using Me in this code, ComposeParts will scan the application for properties decorated with the Import attribute. Because my Import attribute uses my IDynamic interface as its contract, ComposeParts will then look through the Catalogs for classes using that interface as their contract.

Once the property is bound, your application code can then use the property to inject the dynamic class into the host class. In this case, I'm using site injection to pass the dynamic class to the method that needs it:

Dim udc As New UsesDelegatedClass
Dim res As String = udc.SaysHello(Me.DClass)

Unlike Reflection, however, the MEF requires the dynamic class to contain specific code. To be loaded into a container, a class must be decorated with an Export attribute that specifies a contract. The ComposeParts method uses the Export attribute to match classes to properties decorated with Import attributes by looking for matching contracts. In this case, the contract is an interface:

<Export(GetType(DynamicInterface.IDynamic))>
Public Class DynamicAddedClass
  Implements DynamicInterface.IDynamic

Enhancing Binding
Setter injection is actually a better fit for the MEF -- just add the bound property to the host class rather than to the application:

Public Class UsesMEFClass
  <Import(GetType(DynamicInterface.IDynamic))>
  Public Property DClass As DynamicInterface.IDynamic
Then pass the host class to ComposeParts instead of the application:
Dim mdc As New UsesMEFClass
con.ComposeParts(mdc)
res = mdc.Hello("Jan")

When the ComposeParts method is called, the dynamic class binds to the property on the host class. Methods in the host class can then access the dynamic class through the bound property, like this:

Public Function Hello(HelloName As String) As String
  Return DClass.SayHello(HelloName)
End Function

You can also export individual methods instead of whole classes. If you export a method, add the Export attribute to the method and use the Func declaration to specify the signature for the method. This example specifies a method that accepts a single String parameter and returns an Integer:

<Export(GetType(Func(Of String, Integer)))>
Public Function GetAge(Name As String) As Integer
  'body of method
End Function

Define the property that the method will be bound to using an identical Func declaration. The property must be decorated with the Import attribute, but you don't have to pass a value to it:

<Import()>
Public Property ReturnAge As Func(Of String, Integer)

To call the method, use the Invoke method on the bound property, passing any parameters that the method requires:

Dim i As Integer
i = ReturnAge.Invoke("Peter")

Choosing Classes
You don't have to use an interface or base class in the Export and Import attributes. If you want, you can just use a bare string:

<Export("MyContract")>
Public Function GetAge(Name As String) As Integer

Using a string as the contract name (because it doesn't specify a data type) lets you arbitrarily load any class you want -- which, however, takes you back to using MethodInfo, Reflection and some set of conventions to negotiate with the dynamic class. In a complex environment, using a string as the control also creates the potential for name collision, causing you to load the wrong class. You're better off using an interface or base class.

Instead of Reflection, you can use metadata to support negotiation between the application and the dynamic class. The first step in this process is to define a new interface listing the properties that will hold the values you'll use in selecting the dynamic class. This example defines an interface with two properties, Version and SupportOption:

Public Interface IDMetadata
  ReadOnly Property Version As Integer
  ReadOnly Property SupportOption As String
End Interface

The best place to put this interface is in the same project as the interface or base class that defines the dynamic class.

The dynamic class must be decorated with ExportMetadata attrib-utes that specify values for the properties in the metadata interface. This example sets the Version and SupportOption properties:

<Export(GetType(DynamicInterface.IDynamic))>
<ExportMetadata("SupportOption", "Complete")>
<ExportMetadata("Version", 2)>
Public Class DynamicAddedClass
  Implements DynamicInterface.IDynamic

You tie the dynamic class to the metadata interface by declaring the bound property using the Lazy generic type. This example binds the property to a class that implements the IDynamic interface and ties it to the IDMetadata interface:

<Import()>
Public Property ClassWithMetaData As Lazy(
  Of DynamicInterface.IDynamic, DynamicInterface.IDMetadata)

When declared with the Lazy generic, the property acquires a new property called Metadata, which gives you access to the values set in the ExportMetadata attributes on the dynamic class. This example, for instance, checks the Version value set on the dynamic class:

con.ComposeParts(Me)
If Me.ClassWithMetaData.Metadata.Version > 1 Then
  udc = New UsesDelegatedClass(Me.DClass)
End If

You can also load multiple classes and select the class you want, using LINQ to check the metadata. To do that, the bound property must be decorated with the ImportMany attribute and declared using the IEnumerable generic type, passing the Lazy generic type that ties the class to its metadata:

<ImportMany()>
Public Property AllClasses As IEnumerable(
  Of Lazy(Of DynamicInterface.IDynamic, DynamicInterface.IDMetadata))

After calling ComposeParts, you can use LINQ to query the bound property to find the dynamic class you want. This example finds the class with its SupportOption set to "Complete":

Dim clss = (From dcls In AllClasses
              Where dcls.Metadata.SupportOption = "Complete"
              Select dcls).FirstOrDefault

The object returned from the LINQ query will have a property called Value that holds the selected dynamic class. This example passes the selected class to the constructor of the host class:

udc = New UsesDelegatedClass(clss.Value)

Depending on where you want the negotiation to take place, you may want to put this code in your factory method.

Debugging and More
For all the flexibility dynamic composition gives you, it does increase the likelihood of runtime errors. Keep your compositions simple -- don't have your dynamic classes load other classes dynamically. For Reflection, make the Assembly Binding Log Viewer your friend: It will give you detailed error messages when a DLL doesn't load. With the MEF, the CompositionInfo object, when passed a container and a catalog, will provide a list of reasons for why things may not have worked out as you expected. You can also use the mefx command-line utility to analyze the files in a folder for potential failures.

But there's also a lot more power here. While I've used a Directory-Catalog, there are other Catalogs (based on assemblies and type arrays) available. You can also filter Catalogs to control which assemblies are searched. Providers give you the ability to extend the composition ability of the MEF, and recomposition allows you to change dynamic classes without shutting down the application. You might never add a reference at design time again.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube