C# Assembly Manipulation
Assembly manipulation is a powerful feature of C# Reflection that allows you to interact with .NET assemblies programmatically. Whether you need to load assemblies dynamically, extract information from existing assemblies, or even create new assemblies at runtime, understanding assembly manipulation opens doors to flexible and extensible application designs.
What Are Assemblies in .NET?
In .NET, an assembly is a compiled code library that serves as the fundamental unit of deployment, version control, reuse, and security permissions. An assembly can be:
- An executable (
.exe
) file - A dynamic link library (
.dll
) file - A collection of types and resources that work together to form a logical unit of functionality
Basic Assembly Operations
Loading Assemblies
You can load assemblies in several ways, depending on your needs:
// Method 1: Load from GAC or current directory
Assembly assembly1 = Assembly.Load("MyAssembly");
// Method 2: Load from a specific file path
Assembly assembly2 = Assembly.LoadFrom(@"C:\MyApp\Libraries\MyAssembly.dll");
// Method 3: Load from a byte array (useful for embedded resources)
byte[] assemblyBytes = File.ReadAllBytes(@"C:\MyApp\Libraries\MyAssembly.dll");
Assembly assembly3 = Assembly.Load(assemblyBytes);
Getting the Currently Executing Assembly
To get a reference to the assembly that contains the currently executing code:
Assembly currentAssembly = Assembly.GetExecutingAssembly();
Console.WriteLine($"Current assembly: {currentAssembly.FullName}");
// Output might look like:
// Current assembly: MyApplication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Exploring Assembly Information
Once you have a reference to an assembly, you can explore its metadata:
Assembly assembly = Assembly.GetExecutingAssembly();
// Get assembly name details
AssemblyName name = assembly.GetName();
Console.WriteLine($"Assembly Name: {name.Name}");
Console.WriteLine($"Version: {name.Version}");
Console.WriteLine($"Culture: {name.CultureInfo.DisplayName}");
// List all types in the assembly
Type[] types = assembly.GetTypes();
Console.WriteLine("\nTypes defined in the assembly:");
foreach (Type type in types)
{
Console.WriteLine($"- {type.FullName}");
}
// Get all modules
Console.WriteLine("\nModules in the assembly:");
foreach (Module module in assembly.GetModules())
{
Console.WriteLine($"- {module.Name}");
}
Working with Assembly Resources
.NET assemblies can contain embedded resources, which are files that are compiled into the assembly. These can be images, text files, or any other type of file.
Accessing Embedded Resources
Assembly assembly = Assembly.GetExecutingAssembly();
// Get all resource names
string[] resourceNames = assembly.GetManifestResourceNames();
Console.WriteLine("Embedded resources:");
foreach (string name in resourceNames)
{
Console.WriteLine($"- {name}");
}
// Read a specific resource
string resourceName = "MyNamespace.Resources.config.json";
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
using (StreamReader reader = new StreamReader(stream))
{
string content = reader.ReadToEnd();
Console.WriteLine($"Resource content: {content}");
}
Creating Dynamic Assemblies
One of the most powerful features of assembly manipulation is the ability to create new assemblies at runtime using the System.Reflection.Emit
namespace.
Basic Dynamic Assembly Creation
Here's a simple example that creates a dynamic assembly with a basic class:
using System;
using System.Reflection;
using System.Reflection.Emit;
public class DynamicAssemblyDemo
{
public static void Main()
{
// Define assembly name
AssemblyName assemblyName = new AssemblyName("DynamicAssembly");
// Create a dynamic assembly
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.Run);
// Create a module within the assembly
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
// Define a type (class) within the module
TypeBuilder typeBuilder = moduleBuilder.DefineType(
"DynamicType",
TypeAttributes.Public);
// Define a method in the type
MethodBuilder methodBuilder = typeBuilder.DefineMethod(
"SayHello",
MethodAttributes.Public | MethodAttributes.Static,
typeof(void),
new Type[] { typeof(string) });
// Generate the method's IL code
ILGenerator ilGenerator = methodBuilder.GetILGenerator();
ilGenerator.EmitWriteLine("Hello, ");
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.EmitWriteLine(null);
ilGenerator.Emit(OpCodes.Ret);
// Create the type
Type dynamicType = typeBuilder.CreateType();
// Invoke the method
MethodInfo sayHello = dynamicType.GetMethod("SayHello");
sayHello.Invoke(null, new object[] { "Dynamic World!" });
// Output:
// Hello,
// Dynamic World!
}
}
This example creates a dynamic assembly with a class containing a static method that prints a greeting message.
Practical Applications of Assembly Manipulation
Plugin Systems
Assembly manipulation enables the creation of plugin systems where your application can load and use code modules that weren't known at compile time:
public class PluginLoader
{
public static IEnumerable<IPlugin> LoadPlugins(string directory)
{
List<IPlugin> plugins = new List<IPlugin>();
// Get all DLL files in the directory
string[] files = Directory.GetFiles(directory, "*.dll");
foreach (string file in files)
{
try
{
// Load the assembly
Assembly assembly = Assembly.LoadFrom(file);
// Find types that implement IPlugin
foreach (Type type in assembly.GetTypes())
{
if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
{
// Create an instance
IPlugin plugin = (IPlugin)Activator.CreateInstance(type);
plugins.Add(plugin);
Console.WriteLine($"Loaded plugin: {plugin.Name}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading plugin from {file}: {ex.Message}");
}
}
return plugins;
}
}
// Assuming we have this interface in our main app
public interface IPlugin
{
string Name { get; }
void Execute();
}
Cross-Platform Deployment
Assembly manipulation allows for conditional loading of platform-specific implementations:
public static class PlatformServices
{
public static IFileSystem GetFileSystemService()
{
string platformAssemblyName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
platformAssemblyName = "MyApp.Windows";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
platformAssemblyName = "MyApp.Linux";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
platformAssemblyName = "MyApp.MacOS";
}
else
{
throw new PlatformNotSupportedException();
}
// Load the platform-specific assembly
Assembly platformAssembly = Assembly.Load(platformAssemblyName);
// Create the implementation
Type fileSystemType = platformAssembly.GetType($"{platformAssemblyName}.FileSystemService");
return (IFileSystem)Activator.CreateInstance(fileSystemType);
}
}
Dynamic Code Generation
You can use assembly manipulation to generate and compile code at runtime:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection;
public class DynamicCompiler
{
public static Assembly CompileCode(string sourceCode)
{
// Parse the source code
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
// Define compilation parameters
string assemblyName = Path.GetRandomFileName();
MetadataReference[] references = new MetadataReference[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location)
};
// Create compilation
CSharpCompilation compilation = CSharpCompilation.Create(
assemblyName,
syntaxTrees: new[] { syntaxTree },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
// Compile to memory stream
using (MemoryStream ms = new MemoryStream())
{
var result = compilation.Emit(ms);
if (!result.Success)
{
// Handle compilation errors
var failures = result.Diagnostics.Where(
diagnostic => diagnostic.IsWarningAsError ||
diagnostic.Severity == DiagnosticSeverity.Error);
foreach (var diagnostic in failures)
{
Console.Error.WriteLine($"{diagnostic.Id}: {diagnostic.GetMessage()}");
}
throw new Exception("Compilation failed");
}
// Load the assembly
ms.Seek(0, SeekOrigin.Begin);
return Assembly.Load(ms.ToArray());
}
}
}
// Example usage
public void RunDynamicCode()
{
string code = @"
using System;
namespace DynamicNamespace
{
public class DynamicClass
{
public static void Run()
{
Console.WriteLine(""This code was compiled at runtime!"");
}
}
}
";
Assembly dynamicAssembly = DynamicCompiler.CompileCode(code);
Type dynamicType = dynamicAssembly.GetType("DynamicNamespace.DynamicClass");
MethodInfo runMethod = dynamicType.GetMethod("Run");
runMethod.Invoke(null, null);
// Output: This code was compiled at runtime!
}
Best Practices and Security Considerations
When working with dynamic assemblies and runtime code execution, keep these important guidelines in mind:
-
Security Risks: Loading assemblies from untrusted sources can introduce security vulnerabilities. Always validate and sandbox code from external sources.
-
Performance Considerations: Dynamic assembly loading and creation is more resource-intensive than static compilation. Use it judiciously.
-
Error Handling: Robust exception handling is crucial when working with assemblies that might not be available or might contain errors.
-
AppDomain Isolation: Consider using separate AppDomains for loading untrusted assemblies to provide isolation (in .NET Framework) or AssemblyLoadContext in .NET Core/.NET 5+.
-
Versioning: Be mindful of assembly version conflicts when dynamically loading assemblies.
Summary
Assembly manipulation in C# offers powerful capabilities for creating flexible, extensible applications. By understanding how to load, inspect, and create assemblies at runtime, you can implement plugin architectures, platform-specific code loading, and dynamic code generation.
These techniques are advanced but provide solutions to complex problems like supporting plugins, managing cross-platform code, and enabling runtime customization of your applications.
Additional Resources
- Microsoft Documentation on Assembly Class
- Reflection.Emit Namespace Documentation
- Dynamically Loading Assemblies (Microsoft Docs)
Exercises
-
Create a simple plugin system that loads all assemblies from a "plugins" folder and finds classes implementing a specific interface.
-
Build a dynamic calculator that loads mathematical operations from separate assemblies based on user input.
-
Write a program that inspects an assembly and generates documentation on all its public types and members.
-
Create a dynamic proxy generator that can wrap an object instance and log all method calls before forwarding them to the original object.
-
Implement a simple scripting engine that compiles and executes C# code provided as strings at runtime.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)