Иногда в разработке возникают задачи, требующие создания типов в рантайме. Чаще всего это необходимо при написании декларативных сервисов, высокопроизводительных мапперов или систем с динамическим проксированием.
Допустим, мы хотим сгенерировать тип с таким интерфейсом:
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/