Генерация типов в Runtime

от автора

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

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

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. Пишем логику метода SomeILGenerator 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 может быть телом, в нашем случае это будет greaterOrEqualBinaryExpression 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 как альтернативный способ.

ссылка на оригинал статьи https://habr.com/ru/articles/1033564/