C# Dynamic Loading
Introduction
Dynamic loading is a powerful feature in C# that allows your applications to load assemblies and types at runtime rather than at compile time. This concept falls under the broader umbrella of reflection in C# and provides tremendous flexibility for creating extensible applications.
When you develop applications traditionally, all the required assemblies and types are referenced at compile time. However, there are scenarios where you may not know which assemblies or types your application will need until it's running. Dynamic loading enables you to:
- Load assemblies that weren't referenced during compilation
- Create instances of types discovered at runtime
- Build plugin-based architectures
- Create more modular and extensible applications
In this tutorial, we'll explore the fundamentals of dynamic loading in C# and learn how to implement it in practical applications.
Basic Concepts of Dynamic Loading
Dynamic loading primarily involves working with the following classes from the System.Reflection
namespace:
Assembly
: Represents a compiled assembly (.dll or .exe)AssemblyName
: Describes an assembly's identityType
: Represents a type definitionActivator
: Creates instances of types
Let's start by understanding how to load assemblies dynamically.
Loading Assemblies Dynamically
There are several ways to load assemblies at runtime in C#:
1. Loading from a File Path
using System;
using System.Reflection;
public class AssemblyLoader
{
public static void LoadAssemblyFromFile()
{
try
{
// Load assembly from a specific path
string path = @"C:\Libraries\MyCustomLibrary.dll";
Assembly assembly = Assembly.LoadFrom(path);
Console.WriteLine($"Loaded assembly: {assembly.FullName}");
// Get all types from the assembly
Type[] types = assembly.GetTypes();
Console.WriteLine($"Types found in assembly: {types.Length}");
foreach (Type type in types)
{
Console.WriteLine($"- {type.FullName}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading assembly: {ex.Message}");
}
}
}
Output:
Loaded assembly: MyCustomLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Types found in assembly: 3
- MyCustomLibrary.Calculator
- MyCustomLibrary.StringHelper
- MyCustomLibrary.Logger
2. Loading from the GAC (Global Assembly Cache)
public static void LoadAssemblyFromGAC()
{
try
{
// Create an assembly name
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = "System.Data";
assemblyName.Version = new Version("4.0.0.0");
assemblyName.CultureInfo = System.Globalization.CultureInfo.InvariantCulture;
// Load the assembly from GAC
Assembly assembly = Assembly.Load(assemblyName);
Console.WriteLine($"Loaded assembly from GAC: {assembly.FullName}");
}
catch (Exception ex)
{
Console.WriteLine($"Error loading assembly: {ex.Message}");
}
}
3. Using Assembly.Load with a Simple Name
public static void LoadAssemblyByName()
{
try
{
// Load by simple name (framework will resolve the full path)
Assembly assembly = Assembly.Load("System.Data.SqlClient");
Console.WriteLine($"Loaded assembly: {assembly.FullName}");
}
catch (Exception ex)
{
Console.WriteLine($"Error loading assembly: {ex.Message}");
}
}
Creating Instances Dynamically
Once you've loaded an assembly, you'll want to create instances of types within it. This is often called "late binding" and can be done using the Activator
class:
public static void CreateInstanceDynamically()
{
try
{
// Load the assembly
Assembly assembly = Assembly.LoadFrom(@"C:\Libraries\MyCustomLibrary.dll");
// Find a specific type
Type calculatorType = assembly.GetType("MyCustomLibrary.Calculator");
if (calculatorType != null)
{
// Create an instance of the type
object calculatorInstance = Activator.CreateInstance(calculatorType);
Console.WriteLine($"Created instance of type: {calculatorType.FullName}");
// Use reflection to call methods
MethodInfo addMethod = calculatorType.GetMethod("Add");
if (addMethod != null)
{
object result = addMethod.Invoke(calculatorInstance, new object[] { 5, 10 });
Console.WriteLine($"Result of Add(5, 10): {result}");
}
}
else
{
Console.WriteLine("Type not found in assembly");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error creating instance: {ex.Message}");
}
}
Output:
Created instance of type: MyCustomLibrary.Calculator
Result of Add(5, 10): 15
Working with Constructors
Sometimes you need to create an instance using a specific constructor with parameters. Here's how to do that:
public static void CreateInstanceWithParameters()
{
try
{
// Load the assembly
Assembly assembly = Assembly.LoadFrom(@"C:\Libraries\MyCustomLibrary.dll");
// Find a specific type
Type loggerType = assembly.GetType("MyCustomLibrary.Logger");
if (loggerType != null)
{
// Create an instance using a constructor that accepts a string parameter
object loggerInstance = Activator.CreateInstance(
loggerType,
new object[] { "application.log" }
);
Console.WriteLine($"Created logger instance with custom log file");
// You can now use the logger instance...
}
}
catch (Exception ex)
{
Console.WriteLine($"Error creating instance: {ex.Message}");
}
}
Real-World Example: Building a Plugin System
One of the most common use cases for dynamic loading is creating a plugin system. Let's implement a simple plugin architecture:
First, we'll define an interface that all plugins must implement:
// IPlugin.cs - This would be in your main application
public interface IPlugin
{
string Name { get; }
string Version { get; }
void Execute();
}
Now, let's create a plugin manager that loads plugins dynamically:
// PluginManager.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
public class PluginManager
{
private List<IPlugin> _loadedPlugins = new List<IPlugin>();
public void LoadPlugins(string pluginDirectory)
{
// Clear existing plugins
_loadedPlugins.Clear();
// Ensure directory exists
if (!Directory.Exists(pluginDirectory))
{
Console.WriteLine($"Plugin directory not found: {pluginDirectory}");
return;
}
// Get all DLL files in the directory
string[] pluginFiles = Directory.GetFiles(pluginDirectory, "*.dll");
foreach (string pluginFile in pluginFiles)
{
try
{
// Load the assembly
Assembly pluginAssembly = Assembly.LoadFrom(pluginFile);
// Find types that implement IPlugin
foreach (Type type in pluginAssembly.GetTypes())
{
if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
{
// Create an instance of the plugin
IPlugin plugin = (IPlugin)Activator.CreateInstance(type);
// Add to our loaded plugins
_loadedPlugins.Add(plugin);
Console.WriteLine($"Loaded plugin: {plugin.Name} v{plugin.Version}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading plugin {pluginFile}: {ex.Message}");
}
}
Console.WriteLine($"Total plugins loaded: {_loadedPlugins.Count}");
}
public void ExecuteAllPlugins()
{
foreach (IPlugin plugin in _loadedPlugins)
{
try
{
Console.WriteLine($"Executing plugin: {plugin.Name}");
plugin.Execute();
}
catch (Exception ex)
{
Console.WriteLine($"Error executing plugin {plugin.Name}: {ex.Message}");
}
}
}
}
To use this plugin system, developers would create separate class libraries with classes that implement the IPlugin
interface:
// In a separate project that builds to a DLL
using System;
public class ReportGeneratorPlugin : IPlugin
{
public string Name => "Report Generator";
public string Version => "1.0.0";
public void Execute()
{
Console.WriteLine("Generating reports...");
// Actual report generation code would go here
}
}
Using the plugin system in your main application:
public static void Main(string[] args)
{
// Create plugin manager
var pluginManager = new PluginManager();
// Load plugins from a directory
pluginManager.LoadPlugins(@"C:\MyApp\Plugins");
// Execute all plugins
pluginManager.ExecuteAllPlugins();
Console.ReadKey();
}
Output:
Loaded plugin: Report Generator v1.0.0
Loaded plugin: Data Synchronizer v2.1.0
Total plugins loaded: 2
Executing plugin: Report Generator
Generating reports...
Executing plugin: Data Synchronizer
Synchronizing data from external sources...
Performance Considerations
Dynamic loading is powerful but comes with some considerations:
- Performance overhead: Reflection operations are slower than direct method calls.
- Security implications: Be careful with what assemblies you load, particularly from user-provided sources.
- Versioning challenges: You may encounter compatibility issues between dynamically loaded assemblies.
- Error handling: Extra error handling is needed as issues may only be discovered at runtime.
To improve performance when using dynamic loading:
// Cache Type information rather than looking it up repeatedly
Type calculatorType = assembly.GetType("MyCustomLibrary.Calculator");
// If you need to call a method many times, create a delegate instead of using MethodInfo.Invoke repeatedly
MethodInfo addMethod = calculatorType.GetMethod("Add");
dynamic calculator = Activator.CreateInstance(calculatorType);
// Now you can call methods directly using dynamic
int result = calculator.Add(5, 10);
Summary
Dynamic loading in C# enables us to create flexible, extensible applications by loading assemblies and creating instances at runtime. We've learned how to:
- Load assemblies from file paths, the GAC, or by name
- Create instances of types dynamically using
Activator.CreateInstance
- Work with constructors that require parameters
- Build a practical plugin system using dynamic loading
- Consider performance implications
These techniques allow you to build applications that can adapt and extend without recompilation, supporting scenarios like plugin architectures, modular applications, and configurability.
Exercises
-
Create a simple calculator application that dynamically loads a library containing different calculation strategies (addition, subtraction, statistical functions, etc.).
-
Build a text processing application that can load different text processors (uppercase converter, word counter, spell checker) from a plugins folder.
-
Extend the plugin system example to include a configuration file that specifies which plugins to load and in what order.
-
Create a simple application that can dynamically load and execute different report generators, each implemented in a separate assembly.
Additional Resources
- Microsoft Docs: Assembly Class
- Microsoft Docs: Reflection in .NET
- Microsoft Docs: Dynamically Loading and Using Types
- C# in Depth by Jon Skeet - Contains excellent coverage of advanced C# topics including reflection
By mastering dynamic loading, you'll be able to create more flexible and extensible applications that can adapt to changing requirements without recompilation.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)