Иногда в разработке возникают задачи, требующие создания типов в рантайме. Чаще всего это необходимо при написании декларативных сервисов, высокопроизводительных мапперов или систем с динамическим проксированием.

Допустим, мы хотим сгенерировать тип с таким интерфейсом:

public interface IStudent
{
    string Name { get; set; }
    int Some(string value);
}

Логика метода Some (просто для примера):

public int Some(string value)
{
    string str = Name + value;
    Console.WriteLine(str);
    return str.Length;
}

Reflection.Emit

Можно использовать System.Reflection.Emit.

// 1. Создаем сборку, модуль и тип
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("StudentReflectionEmitAssembly"), AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("StudentReflectionEmitModule");
TypeBuilder typeBuilder = moduleBuilder.DefineType("Student", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable, typeof(object), new[] { typeof(IStudent) });

// 2. Объявляем backing-поле и свойство
FieldBuilder nameField = typeBuilder.DefineField("_name", typeof(string), FieldAttributes.Private);
PropertyBuilder nameProperty = typeBuilder.DefineProperty("Name", PropertyAttributes.None, typeof(string), Type.EmptyTypes);

// 3. Создаем сеттеры, геттеры и метод
MethodBuilder getter = typeBuilder.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.NewSlot | MethodAttributes.Final, typeof(string), Type.EmptyTypes);
MethodBuilder setter = typeBuilder.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.NewSlot | MethodAttributes.Final, typeof(void), new[] { typeof(string) });
MethodBuilder someMethod = typeBuilder.DefineMethod("Some", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Final, typeof(int), new[] { typeof(string) });

// 4. Пишем логику свойств
ILGenerator getterIl = getter.GetILGenerator();
getterIl.Emit(OpCodes.Ldarg_0);
getterIl.Emit(OpCodes.Ldfld, nameField);
getterIl.Emit(OpCodes.Ret);
nameProperty.SetGetMethod(getter);

ILGenerator setterIl = setter.GetILGenerator();
setterIl.Emit(OpCodes.Ldarg_0);
setterIl.Emit(OpCodes.Ldarg_1);
setterIl.Emit(OpCodes.Stfld, nameField);
setterIl.Emit(OpCodes.Ret);
nameProperty.SetSetMethod(setter);

// 5. Пишем логику метода Some
ILGenerator someIl = someMethod.GetILGenerator();
LocalBuilder str = someIl.DeclareLocal(typeof(string));
someIl.Emit(OpCodes.Ldarg_0);
someIl.Emit(OpCodes.Call, getter);
someIl.Emit(OpCodes.Ldarg_1);
someIl.Emit(OpCodes.Call, typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) }));
someIl.Emit(OpCodes.Stloc, str);
someIl.Emit(OpCodes.Ldloc, str);
someIl.Emit(OpCodes.Call, typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) }));
someIl.Emit(OpCodes.Ldloc, str);
someIl.Emit(OpCodes.Call, typeof(string).GetProperty(nameof(string.Length)).GetGetMethod());
someIl.Emit(OpCodes.Ret);

// 6. Финализация типа
typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);
Type studentType = typeBuilder.CreateTypeInfo().AsType();

Получился многословный код. Хотя он очень шаблонный, можно написать небольшую обертку и получить:

AssemblyFactory

Это позволит описывать типы в декларативном стиле:

Type studentType = AssemblyFactory.CreateAssembly("StudentReflectionEmitAssembly")
    .CreateClass("Student", typeof(object), new[] { typeof(IStudent) })
    .AddProperty(typeof(string), nameof(IStudent.Name))
    .AddMethod(typeof(int), nameof(IStudent.Some), new[] { typeof(string) }, (ilGenerator, typeBuilder) =>
    {
        LocalBuilder str = ilGenerator.DeclareLocal(typeof(string));
        ilGenerator.Emit(OpCodes.Ldarg_0);
        ilGenerator.Emit(OpCodes.Call, typeBuilder.GetProperty(nameof(IStudent.Name)).GetGetMethod());
        ilGenerator.Emit(OpCodes.Ldarg_1);
        ilGenerator.Emit(OpCodes.Call, typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) }));
        ilGenerator.Emit(OpCodes.Stloc, str);
        ilGenerator.Emit(OpCodes.Ldloc, str);
        ilGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) }));
        ilGenerator.Emit(OpCodes.Ldloc, str);
        ilGenerator.Emit(OpCodes.Call, typeof(string).GetProperty(nameof(string.Length)).GetGetMethod());
        ilGenerator.Emit(OpCodes.Ret);
    })
    .Build();

Для этого напишем фабрику:

private class AssemblyFactory
{
    // Создает сборку
    public static AssemblyFactory CreateAssembly(string name)
    {
        AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(name), AssemblyBuilderAccess.Run);
        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(name);
        return new AssemblyFactory(moduleBuilder);
    }

    private readonly ModuleBuilder moduleBuilder;

    private AssemblyFactory(ModuleBuilder moduleBuilder)
    {
        this.moduleBuilder = moduleBuilder;
    }

    // Создает класс в сборке
    public DynamicTypeBuilder CreateClass(string name, Type baseType, Type[] interfaces)
    {
        TypeBuilder typeBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable, baseType, interfaces);
        return new DynamicTypeBuilder(typeBuilder);
    }
}

И билдер классов:

private sealed class DynamicTypeBuilder(TypeBuilder typeBuilder)
{
    // те же атрибуты, что и раньше
    private const MethodAttributes DefaultMethodAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Final;
    private const MethodAttributes PropertyMethodAttributes = DefaultMethodAttributes | MethodAttributes.SpecialName;

    // сохраняем поля и свойства для доступа
    private readonly IDictionary<string, FieldBuilder> fields = new Dictionary<string, FieldBuilder>(StringComparer.Ordinal);
    private readonly IDictionary<string, PropertyBuilder> properties = new Dictionary<string, PropertyBuilder>(StringComparer.Ordinal);

    // создание полей очень простое
    public DynamicTypeBuilder AddField(Type type, string name)
    {
        fields[name] = typeBuilder.DefineField(name, type, FieldAttributes.Public);
        return this;
    }

    // свойство чуть сложнее, но так же взято из кода выше
    public DynamicTypeBuilder AddProperty(Type type, string name)
    {
        FieldBuilder field = typeBuilder.DefineField($"_{name}", type, FieldAttributes.Private);
        fields[name] = field;
        PropertyBuilder property = typeBuilder.DefineProperty(name, PropertyAttributes.None, type, Type.EmptyTypes);
        properties[name] = property;

        MethodBuilder getter = typeBuilder.DefineMethod($"get_{name}", PropertyMethodAttributes, type, Type.EmptyTypes);
        ILGenerator getterIl = getter.GetILGenerator();
        getterIl.Emit(OpCodes.Ldarg_0);
        getterIl.Emit(OpCodes.Ldfld, field);
        getterIl.Emit(OpCodes.Ret);
        property.SetGetMethod(getter);

        MethodBuilder setter = typeBuilder.DefineMethod($"set_{name}", PropertyMethodAttributes, typeof(void), new[] { type });
        ILGenerator setterIl = setter.GetILGenerator();
        setterIl.Emit(OpCodes.Ldarg_0);
        setterIl.Emit(OpCodes.Ldarg_1);
        setterIl.Emit(OpCodes.Stfld, field);
        setterIl.Emit(OpCodes.Ret);
        property.SetSetMethod(setter);

        return this;
    }

    public FieldBuilder GetField(string name)
    {
        return fields[name];
    }

    public PropertyBuilder GetProperty(string name)
    {
        return properties[name];
    }

    // создание методов делегируется вызывающей стороне
    public DynamicTypeBuilder AddMethod(Type returnType, string name, Type[] parameterTypes, Action<ILGenerator, DynamicTypeBuilder> emit)
    {
        MethodBuilder method = typeBuilder.DefineMethod(name, DefaultMethodAttributes, returnType, parameterTypes);
        emit(method.GetILGenerator(), this);
        return this;
    }

    // и само создание типа
    public Type Build()
    {
        typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);
        return typeBuilder.CreateTypeInfo().AsType();
    }
}

Этого уже достаточно, чтобы создавать DTO и писать простые методы, но писать IL — не самое приятное занятие. На этом моменте нужно подумать: а что может генерировать логику в IL?

Expression Trees

Сначала нужно вкратце разобраться, как оно работает. Работа с ним идет через статические методы System.Linq.Expressions.Expression.

Допустим, мы хотим построить дерево для (User u) => u.Age >= 18.

Для построения дерева вызывается метод Expression.Lambda, он получает дженерик делегата Func<>, Action<>, тело и параметры. Нужно сначала создать параметры через Expression.Parameter и передать в Lambda. Если типы параметров и делегатов не совпадают — будет выброшено исключение.

// входящие параметры описываются какой тип и какое имя (имена могут повторяться или их может не быть, они нужны в основном для отладки)
ParameterExpression paramUser = Expression.Parameter(typeof(User), "u");

Expression body = ...;

Expression<Predicate<User>> lambda = Expression.Lambda<Predicate<User>>(
    body, // тело выражения
    paramUser // и входящие параметры передаются здесь
);

Теперь нужно получить поле Age и сравнить его с 18.

// получаем поле, с которым хотим работать
MemberExpression propAge = Expression.PropertyOrField(paramUser, "Age");

// создаем константу, так как можно работать только с Expression 
ConstantExpression const18 = Expression.Constant(18, typeof(int));

// и делаем проверку «больше или равно». Любое Expression может быть телом, в нашем случае это будет greaterOrEqual
BinaryExpression greaterOrEqual = Expression.GreaterThanOrEqual(propAge, const18);

Полный код выглядит так:

ParameterExpression paramUser = Expression.Parameter(typeof(User), "u");

MemberExpression propAge = Expression.PropertyOrField(paramUser, "Age");
ConstantExpression const18 = Expression.Constant(18, typeof(int));
BinaryExpression greaterOrEqual = Expression.GreaterThanOrEqual(propAge, const18);

Expression<Predicate<User>> lambda = Expression.Lambda<Predicate<User>>(
    greaterOrEqual,
    paramUser
);

// и когда дерево собрано его можно скомпилировать
Predicate<User> compiled = lambda.Compile();

// и использовать
if (compiled(new User(name: "Anton", age: 20)))
{
    Console.WriteLine("Hello");
}

Объеденяем

И тут приходит мысль: что если попробовать писать методы, используя Expression Tree?

Сам по себе IL не работает в ООП, и все методы по своей сути — это статические функции. А когда функция используется как метод класса, нулевым аргументом подставляется экземпляр типа. Тогда теоретически можно написать делегат с первым параметром “self” и написать метод на Expression Tree.

Легальных способов подсунуть IL нет, поэтому прибегнем к грязному свинству и черной магии — к рефлексии.

Если сильно покопаться в Expression Tree, а точнее в том, как и где компилируется IL, можно найти тип System.Linq.Expressions.Compiler.LambdaCompiler - он записывает IL. В конструктор принимает LambdaExpression и AnalyzedTree. AnalyzedTree — это проанализированное дерево, оно создает scope генерации и создается через System.Linq.Expressions.Compiler.VariableBinder.Bind. Естественно, всё это internal-классы.

Найдем типы:

Assembly expressionsAssembly = typeof(Expression).Assembly;
Type variableBinderType = expressionsAssembly.GetType("System.Linq.Expressions.Compiler.VariableBinder", throwOnError: true);
Type lambdaCompilerType = expressionsAssembly.GetType("System.Linq.Expressions.Compiler.LambdaCompiler", throwOnError: true);

Создадим LambdaCompiler:

object analyzedTree = variableBinderType.GetMethod("Bind", PrivateStatic).Invoke(null, new object[] { expression });
object compiler = lambdaCompilerType.GetConstructor(
    PrivateInstance,
    null,
    new[] { analyzedTree.GetType(), typeof(LambdaExpression) },
    null).Invoke(new[] { analyzedTree, expression });

Создаем метод в билдере. Не забываем, что в делегате первым аргументом указывается объект, которому должен принадлежать метод, но в самом методе он, естественно, не виден. И подсунем ILGenerator в компилятор в поле _ilg:

MethodBuilder method = typeBuilder.DefineMethod(name, DefaultMethodAttributes, expression.ReturnType, delegateParameters.Skip(1).ToArray());
lambdaCompilerType.GetField("_ilg", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, method.GetILGenerator());

К сожалению для нас, это не всё. По умолчанию лямбда имеет первым аргументом контекст замыкания. Нужно сказать компилятору, что замыкания нет (_hasClosureArgument = false), и подменить структуру метода в _method:

DynamicMethod signatureMethod = new DynamicMethod(method.Name + "_ExpressionSignature", expression.ReturnType, delegateParameters, method.Module, skipVisibility: true);
lambdaCompilerType.GetField("_hasClosureArgument", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, false);
lambdaCompilerType.GetField("_method", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, signatureMethod);

Осталось вызвать метод EmitLambdaBody, который запишет IL в наш ILGenerator:

lambdaCompilerType.GetMethod("EmitLambdaBody", BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null).Invoke(compiler, null);

И вуаля! Теперь методы можно писать на Expression Tree. Но есть нюансы работы с self. Перепишем наш метод Some на Expression Tree:

Type studentType = AssemblyFactory.CreateAssembly("StudentWithoutInterfaceExpressionTreeAssembly")
    .CreateClass("Student", typeof(object), new[] { typeof(IStudent) })
    .AddProperty(typeof(string), "Name")
    .AddMethod("Some", typeBuilder =>
    {
        // Так как тип ещё не создан, нужно принимать object
        ParameterExpression self = Expression.Parameter(typeof(object), "self");
        ParameterExpression value = Expression.Parameter(typeof(string), "value");

        // И конвертировать в ожидаемый тип. Повезло, что TypeBuilder — наследник Type.
        UnaryExpression typedSelf = Expression.Convert(self, typeBuilder.Type);

        // Мы создавали свойство, но при попытке доступа к нему будет ошибка. Опять же из-за нескомпилированного типа.
        // Поэтому можно работать только с полями, зато можно работать с любыми полями.
        MemberExpression name = Expression.Field(typedSelf, typeBuilder.GetField("_Name"));

        // находим методы string.Concat и Console.WriteLine
        MethodInfo concatMethod = typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) });
        MethodInfo writeLineMethod = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) });


        // Описываем переменную, в которую сохраним конкатенацию
        ParameterExpression str = Expression.Variable(typeof(string), "str");
        ParameterExpression[] variables = new[] { str };

        // string.Concat(_Name, value)
        MethodCallExpression concatExpression = Expression.Call(concatMethod, name, value);
        
        // str = string.Concat(_Name, value)
        BinaryExpression assign = Expression.Assign(str, concatExpression);

        // Console.WriteLine(str);
        MethodCallExpression callWriteLine = Expression.Call(writeLineMethod, str);
        
        // str.Length
        MemberExpression returnValue = Expression.Property(str, nameof(string.Length));

        // создаем блок, первым аргументом всегда идут переменные которые используются в этом блоке
        // а последним должен быть return
        BlockExpression body = Expression.Block(
            variables,
            assign,
            callWriteLine,
            returnValue
        );

        return Expression.Lambda<Func<object, string, int>>(
            body,
            self, value
        );
    })
    .Build();
Полный листинг фабрики:
private class AssemblyFactory
{
    public static AssemblyFactory CreateAssembly(string name)
    {
        AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(name), AssemblyBuilderAccess.Run);
        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(name);
        return new AssemblyFactory(moduleBuilder);
    }

    private readonly ModuleBuilder moduleBuilder;

    private AssemblyFactory(ModuleBuilder moduleBuilder)
    {
        this.moduleBuilder = moduleBuilder;
    }

    public DynamicTypeBuilder CreateClass(string name, Type baseType, Type[] interfaces)
    {
        TypeBuilder typeBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable, baseType, interfaces);
        return new DynamicTypeBuilder(typeBuilder);
    }
}

private sealed class DynamicTypeBuilder(TypeBuilder typeBuilder)
{
    private const MethodAttributes DefaultMethodAttributes =
        MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Final;

    private const MethodAttributes PropertyMethodAttributes = DefaultMethodAttributes | MethodAttributes.SpecialName;

    private readonly IDictionary<string, FieldBuilder> fields = new Dictionary<string, FieldBuilder>(StringComparer.Ordinal);
    private readonly IDictionary<string, PropertyBuilder> properties = new Dictionary<string, PropertyBuilder>(StringComparer.Ordinal);

    public Type Type => typeBuilder;

    public DynamicTypeBuilder AddField(Type type, string name)
    {
        fields[name] = typeBuilder.DefineField(name, type, FieldAttributes.Public);
        return this;
    }

    public DynamicTypeBuilder AddProperty(Type type, string name)
    {
        FieldBuilder field = typeBuilder.DefineField($"_{name}", type, FieldAttributes.Private);
        fields[field.Name] = field;
        PropertyBuilder property = typeBuilder.DefineProperty(name, PropertyAttributes.None, type, Type.EmptyTypes);
        properties[name] = property;

        MethodBuilder getter = typeBuilder.DefineMethod($"get_{name}", PropertyMethodAttributes, type, Type.EmptyTypes);
        ILGenerator getterIl = getter.GetILGenerator();
        getterIl.Emit(OpCodes.Ldarg_0);
        getterIl.Emit(OpCodes.Ldfld, field);
        getterIl.Emit(OpCodes.Ret);
        property.SetGetMethod(getter);

        MethodBuilder setter = typeBuilder.DefineMethod($"set_{name}", PropertyMethodAttributes, typeof(void), new[] { type });
        ILGenerator setterIl = setter.GetILGenerator();
        setterIl.Emit(OpCodes.Ldarg_0);
        setterIl.Emit(OpCodes.Ldarg_1);
        setterIl.Emit(OpCodes.Stfld, field);
        setterIl.Emit(OpCodes.Ret);
        property.SetSetMethod(setter);

        return this;
    }

    public FieldBuilder GetField(string name)
    {
        return fields[name];
    }

    public PropertyBuilder GetProperty(string name)
    {
        return properties[name];
    }

    public DynamicTypeBuilder AddMethod(Type returnType, string name, Type[] parameterTypes, Action<ILGenerator, DynamicTypeBuilder> emit)
    {
        MethodBuilder method = typeBuilder.DefineMethod(name, DefaultMethodAttributes, returnType, parameterTypes);
        emit(method.GetILGenerator(), this);
        return this;
    }


    public DynamicTypeBuilder AddMethod<TDelegate>(string name, Func<DynamicTypeBuilder, Expression<TDelegate>> expressionFactory)
        where TDelegate : Delegate
    {
        Expression<TDelegate> expression = expressionFactory(this);
        Type[] delegateParameters = expression.Parameters.Select(x => x.Type).ToArray();
        if (delegateParameters.Length == 0)
        {
            throw new ArgumentException("Expression must have the instance as its first parameter.", nameof(expression));
        }


        Assembly expressionsAssembly = typeof(Expression).Assembly;
        Type variableBinderType = expressionsAssembly.GetType("System.Linq.Expressions.Compiler.VariableBinder", throwOnError: true);
        Type lambdaCompilerType = expressionsAssembly.GetType("System.Linq.Expressions.Compiler.LambdaCompiler", throwOnError: true);

        object analyzedTree = variableBinderType.GetMethod("Bind", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { expression });
        object compiler = lambdaCompilerType.GetConstructor(
            BindingFlags.NonPublic | BindingFlags.Instance,
            null,
            new[] { analyzedTree.GetType(), typeof(LambdaExpression) },
            null).Invoke(new[] { analyzedTree, expression });


        MethodBuilder method = typeBuilder.DefineMethod(name, DefaultMethodAttributes, expression.ReturnType, delegateParameters.Skip(1).ToArray());
        DynamicMethod signatureMethod = new DynamicMethod(method.Name + "_ExpressionSignature", expression.ReturnType, delegateParameters, method.Module, skipVisibility: true);

        lambdaCompilerType.GetField("_ilg", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, method.GetILGenerator());
        lambdaCompilerType.GetField("_hasClosureArgument", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, false);
        lambdaCompilerType.GetField("_method", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, signatureMethod);

        lambdaCompilerType.GetMethod("EmitLambdaBody", BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null).Invoke(compiler, null);
        return this;
    }

    public Type Build()
    {
        typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);
        return typeBuilder.CreateTypeInfo().AsType();
    }
}

Заключение

Мы прошли путь от низкоуровневых IL до высокоуровневых Expression Trees. Такой подход позволяет создавать динамические типы, не жертвуя при этом читаемостью.

Статья и так получилась большой, может позже разберу Roslyn как альтернативный способ.