惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
博客园 - 【当耐特】
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
D
Docker
GbyAI
GbyAI
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
The Cloudflare Blog
雷峰网
雷峰网
A
About on SuperTechFans
小众软件
小众软件
博客园 - Franky
博客园 - 聂微东
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
MongoDB | Blog
MongoDB | Blog
G
Google Developers Blog
Microsoft Azure Blog
Microsoft Azure Blog
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
Engineering at Meta
Engineering at Meta
宝玉的分享
宝玉的分享
aimingoo的专栏
aimingoo的专栏
量子位
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
罗磊的独立博客
Martin Fowler
Martin Fowler
D
DataBreaches.Net
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Secure Thoughts
Project Zero
Project Zero
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tailwind CSS Blog
S
Schneier on Security
Blog — PlanetScale
Blog — PlanetScale
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
Security Latest
Security Latest
NISL@THU
NISL@THU
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
J
Java Code Geeks

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Генерация типов в Runtime
Energizet · 2026-05-10 · via Все публикации подряд на Хабре

Уровень сложностиСложный

Время на прочтение13 мин

Охват и читатели481

Туториал

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

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

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 как альтернативный способ.