Enabling Project Expansion by Generating Classes and DLLs at Runtime with .Net Core

Buse Nur Şahin
7 min readDec 26, 2024

--

Photo by clau alexa on Unsplash

Let’s assume that we have different payment methods, log types, in short, operation classes that implement the same interface in our project, and when we add a new operation type (for example, class type) from the data source (with a db-first-like approach), how can the project continue to work by expanding without the need for change?

I would like to share with you how I approached such a need, what problems I encountered and how I found a solution.

Scenario

We use different payment methods in our project and we want to have a code structure that can adapt to new payment method integration without resistance.

We want the relevant classes to be created automatically for the payment types we read from the data source without the need to change the project code, and after the classes are created, we want to add the relevant dll files at runtime without the need to recompile, and we want the classes created to meet the requirements of the relevant interface.

Demo

Payment Types Table

Payment method selection screen

Let’s run it again by adding a new payment type to the data source without making any changes in the project code.

Payment Types Table

After adding new payment system

Payment method selection screen

After adding new payment system

Test

The test passed, so what happened?

Since the Select Box already reads the data from the data source, the newly added payment method is immediately selectable, so how was the class of the new payment system created and a method of that class can be executed?

Classes Generated at Runtime

DLL files generated in Runtime

Code Structure

The code structure was created in accordance with the open-closed principle.

public interface IPayment
{
string Pay(int amount);
}

Since we want to build an extensible code structure, we have a payment methods interface and payment methods use this interface.

public class CreditCardManager : IPayment
{
public string Pay(int amount)
{
return $"paid by Credit Card amount: {amount}";
}
}

We can think of the above code fragment as an example of a payment method class that we foresee that we will use in the project.

public class PaymentService
{
private readonly IPayment _payment;
public PaymentService(IPayment _payment)
{
_payment = payment;
}

public string Pay(int amount)
{
return _payment.Pay(amount);
}
}

In the code above, we created the class where we can access the payment method classes with dependency-injection.

Data Source

public class PaymentType
{
public int Id { get; set; }
public string Title { get; set; }
public string ClassName { get; set; }
}

The code above is the payment type model. Using this model you can provide data with any ORM or ADO.NET or with your own static elements. The ClassName attribute in this model will be the name of the manager class we will create and the Title field will provide a user-friendly text on the Payment Type selection screen so that we do not need to change the part we present to the user.

Class Generation

public class ClassGenerator
{
public void GenerateClass(string className)
{
using (StreamWriter writer = new StreamWriter($"{Constants.GeneratedFolder}/{className}.cs"))
{
writer.WriteLine("using System;");
writer.WriteLine("using System.Collections.Generic;");
writer.WriteLine("using ProjectName.Abstract;");
writer.WriteLine("using System.ComponentModel.DataAnnotations;");
writer.WriteLine("using System.Linq;");
writer.WriteLine("using System.Text;");
writer.WriteLine("using System.Threading.Tasks;");
writer.WriteLine("namespace ProjectName.Concrete;"); //namespace adını düzenlememiz gerekiyor.
writer.WriteLine($"public class {className} : IPayment");
writer.WriteLine("{");
writer.WriteLine("public string Pay(int amount)");
writer.WriteLine("{");
writer.WriteLine($"return $\"paid by {className} " + "amount:{amount}\";");
writer.WriteLine("}");
writer.WriteLine("}");
}
}
}

We will create a Helper Class called ClassGenerator and manage file creation and writing to the file through this method.

Note: Here it is obvious that the method content can vary significantly according to a payment method, but it can still be thought that after the code is created, the code in the method will be integrated into the payment method without taking the code in the method via input and deploying it.

public void CreatePaymentManagerClassFromDb()
{
ClassGenerator classGenerator = new ClassGenerator();

foreach (PaymentType paymentType in GetPaymentTypes())
{
classGenerator.GenerateClass(paymentType.ClassName);
}
}

For each row (each data in the list) that we read using this method, we can use the namespace defined with the namespace we edited in the ClassGenerator class.

"namespace ProjectName.Concrete;"
$"{your_folder}/{className}.cs"

We will create the relevant class file in the location specified by the folder information.

Creating an Object from a Class with Reflection

public class PaymentFactory
{

public IPayment GetInstance(string className)
{
string assembly = $"ProjectName.Concrete.{className}";
return (IPayment) System.Reflection.Assembly.GetAssembly(typeof(IPayment)).CreateInstance(assembly);
}
}

In this code fragment we used factory-design-pattern.

Our goal is to get an instance of the given class type by calling the GetInstance method of the factory class, not with the new() keyword when creating an object from the payment method classes. This way we can create a dynamic structure.

We use the Reflection structure to get the object created by giving the class name.

Now we can run the project without getting an error at compile time, but we will get an error at runtime.

When PaymentFactory/GetInstance method runs, while the project is being built under the relevant namespace, since there is no such class (for example MailOrder.cs) yet, there is no assembly information of this class, so it will return null and the related object will not be created in Reflection. How can we solve this?

Runtime DLL creation (Roslyn)

At this stage, we can use AssemblyBuilder or CsharpCodeProvider with .NET Framework to generate DLLs from code fragments at runtime, but since these methods are not yet fully compatible with .NET Core, we will use Roslyn compiler. See: https://github.com/dotnet/roslyn

Although the project codes remain the same in general, we will need to create a DLL and include it in the project before creating an instance. For this, we will make a few edits in the PaymentFactory class.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;

The packages we will use for this operation are in the code fragment above.

We rewrite the GetInstance method

string classFilePath = $"{your_folder}/{className}.cs";
string code = File.ReadAllText(classFilePath);
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code);

We read the relevant code file, get the code fragment and parse this code fragment into a SyntaxTree object.

var references = AppDomain.CurrentDomain.GetAssemblies(
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
.Select(a => MetadataReference.CreateFromFile(a.Location));)

Since the newly generated class uses the IPayment interface, it will give an error if it does not have the relevant references when we compile it, we get the project references in the above code fragment.

CSharpCompilation compilation = CSharpCompilation.Create(
$"DynamicAssembly-{className}.dll",
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);

With this code, it compiles the related class by giving references.

DynamicAssembly-{className}.dll

so that we create a separate DLL for each class and avoid getting errors or file squashing by creating a dynamic name.

EmitResult result = compilation.Emit($"DynamicAssembly-{className}.dll");

We use this code fragment to include the DLL in the project and get an instance of the related class.

if (result.Success)
{
Console.WriteLine($"DLL generated: DynamicAssembly-{className}.dll");
}
else
{
Console.WriteLine("Derleme hatası:");
foreach (var diagnostic in result.Diagnostics)
{
Console.WriteLine(diagnostic);
}
}

In this way we do error management and we get the output “DLL generated” when the dll is generated.

Generating Instance with Reflection after DLL creation

Assembly dynamicAssembly = Assembly.LoadFrom($"DynamicAssembly-{className}.dll")

string namespaceName = "ProjectName.Concrete";
string fullClassName = namespaceName != null ? $"{namespaceName}.{className}" : className;

Type myClassType = dynamicAssembly.GetType(fullClassName);

IPayment instance = (IPayment)Activator.CreateInstance(myClassType);
return instance;With the above code fragment, we create an object of type IPayment by loading the DLL and taking the class type in the DLL.

With the above code fragment, we create an object of type IPayment by loading the DLL and taking the class type in the DLL.

Now, when there is a change in the data source, we can extend the project without the need to change the project code (except for specific implementation) and without the need to recompile the project after the relevant classes are generated.

You can access the code I wrote for the Log Types scenario with a similar approach here.

Thank you for reading

--

--

No responses yet