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

推荐订阅源

GbyAI
GbyAI
博客园 - 三生石上(FineUI控件)
S
Securelist
U
Unit 42
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Simon Willison's Weblog
Simon Willison's Weblog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
B
Blog
T
Tenable Blog
The Hacker News
The Hacker News
The Register - Security
The Register - Security
IT之家
IT之家
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
P
Privacy & Cybersecurity Law Blog
博客园_首页
T
Tailwind CSS Blog
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tor Project blog
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
A
Arctic Wolf
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
V2EX
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
V
Visual Studio Blog
月光博客
月光博客
爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 миллионов точек без потерь
Interceptors и SourceGenerators: отказываемся от IL-инъекций
dmitry-sipak · 2026-04-28 · via Все публикации подряд на Хабре

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

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

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

Туториал

Привет! Я Дмитрий Сипаков, ведущий разработчик в Госсервисах Т-Банка. Расскажу про относительно новую фичу .NET — Interceptors: как они работают вместе со Source Generators и как с их помощью можно избавиться от бойлерплейта без IL-инъекций и сторонней магии.

Задача: автоматизировать шаги в Allure

Разберем кейс. Допустим, мы пишем автотесты и хотим формировать отчет для Allure TestOps. Помимо тестовых методов, в отчет нужно добавлять промежуточные шаги для отладки или передачи дополнительной информации.

Allure TestOps — Test Management System (TMS) для управления тестами приложения. Отчеты по тестам строятся на основании Allure reports. Основные возможности:

  • управление тест-кейсами;

  • планирование тестирования;

  • анализ результатов и отчетов (allure reports);

  • интеграция с CI/CD;

  • выявление flaky-тестов.

Если решать задачу в лоб, код выглядит примерно так:

[Fact]
public async Task Test()
{
    await Step1(123);
}
public async Task Step1(int i)
{
    var parameters = new List<Allure.Net.Commons.Parameter>();
    parameters.Add(new Allure.Net.Commons.Parameter
    {
        name = "defined",
        value = i.ToString()
    });
    Allure.Net.Commons.ExtendedApi.StartStep("Step1", step => step.parameters = parameters);
    try
    {
        await Task.Yield();

        Allure.Net.Commons.ExtendedApi.PassStep();
    }
    catch(Exception)
    {
        Allure.Net.Commons.ExtendedApi.FailStep();
        throw;
    }
}

На небольшом примере — терпимо. Но если шагов становится много, копировать одинаковый код в каждый метод уже неудобно.

Почему в задаче напрашивается AOP

Иногда приходится многократно повторять один и тот же шаблон: начать шаг, собрать параметры, обработать успех, обработать ошибку. Это классический случай для аспектно-ориентированного программирования.

Аспектно-ориентированное программирование (АОП) — парадигма программирования, основная идея которой — разделение функциональности для улучшения разбиения программы на модули.

Аспект — это модуль, который реализует сквозную функциональность (в нашем примере это логирование шага в отчет), не затрагивая основную логику.

Идея проста: мы хотим повесить на метод атрибут вроде AllureStep, а дальше чтобы код автоматически «оборачивался» в нужное логирование и отправлялся в TestOps.

Посмотрим на пример:

public class AsyncStepsTest
{
	[Fact]
	public async Task Test()
	{
    	await Step1(123);
	}
  
	[AllureStep("Step1")]
	private static async Task Step1([Name("defined")] int i)
	{
    	await Task.Yield();
	}
}

В отчете мы хотим увидеть шаг Step1 с параметром defined=123. Пример фрагмента JSON может выглядеть так:

{
  	"name": "Step1",
  	"status": "passed",
 	//...остальные поля
  	"stage": "finished",
  	"steps": [],
  	"attachments": [],
  	"parameters": [
    	{
      	"name": "defined",
      	"value": "123",
      	"excluded": false
    	}
  	],
  	"start": 1771570531445,
  	"stop": 1771570531446
}

Мы хотим, чтобы при компиляции AllureStep оборачивал метод в готовую обертку с логированием шага. Это уже очень похоже на AOP.

Почему IL-инъекции могут быть неудобны

Когда говорим о реализации АОП в разрезе .NET, то можно выделить две библиотеки: AspectInjector и PostSharp, они основаны на подходе IL-инъекций.

IL (Intermediate Language) Injection — низкоуровневая техника внедрения логики программы в скомпилированный IL-код. Мы собираем приложение и после этого в IL внедряется необходимый аспект.

К примеру, в PostSharp есть аспект OnMethodBoundaryAspect, который имеет методы OnEntry и OnExit.

Создаем атрибут аспекта:

[Serializable]
public class LogAttribute : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        Console.WriteLine($"Вход в метод: {args.Method.Name}");
    }

    public override void OnExit(MethodExecutionArgs args)
    {
        Console.WriteLine($"Выход из метода: {args.Method.Name}");
    }
}

Затем навешиваем атрибут на метод:

class Program
{
    [Log]
    static void DoWork()
    {
        Console.WriteLine("Выполняется работа...");
    }

    static void Main(string[] args)
    {
        DoWork();
    }
}

Если скомпилировать проект и открыть сборку через ILSpy, увидим, что вызовы OnEntry и OnExit внедрены в IL. Именно это и называется IL-инъекцией.

Проблема в том, что такой код не виден в исходниках и появляется только после компиляции. Это усложняет сопровождение и диагностику.

Недостатки IL-инъекций:

  • внедренный код не видно в исходниках, только после компиляции;

  • стек вызовов может быть искажен;

  • ReSharper, Roslyn, SonarQube не видят внедренный код и могут пропустить уязвимости или ошибки;

  • сложность с поддержкой новых версий .NET или с сохранением обратной совместимости;

  • проблемы с JIT-оптимизациями.

Source Generators решают часть проблем 

Чтобы уйти от перечисленных ограничений, в .NET 5 появились Source Generators. В отличие от IL-инъекций, Source Generators создают код на этапе компиляции. После этого сгенерированные файлы можно открыть, отрефакторить и проверить их поведение. Они видны в IDE и доступны всем инструментам анализа кода.

Эта технология стала очень популярной в экосистеме .NET. На ней строят решения не только Microsoft, но и множество независимых разработчиков. Многие проекты уходят от Reflection в сторону Source Generators, что помогает повысить производительность.

В статье я не буду акцентировать внимание на том, как сделать свой Source Generator, сосредоточимся на решении проблемы: как сделать аспект.

Source Generator по-настоящему стала важной фичей, но она имеет один существенный недостаток, который кроется в самом названии: мы не можем менять существующий код. 

Разберем на примере, что это значит. У нас есть исходный код:

public class AsyncStepsTest
{
    [Fact]
    public async Task Test()
    {
        await Step1(123);
    }

    [AllureStep("Step1")]
    private static async Task Step1([Name("defined")] int i)
    {
        await Task.Yield();
    }
}

А вот что мог бы сгенерировать Source Generator:

public static async Task Step1([Name("defined")] int i)
{
    var parameters = new List<Allure.Net.Commons.Parameter>();
    parameters.Add(new Allure.Net.Commons.Parameter
    {
        name = "defined",
        value = i.ToString()
    });
    Allure.Net.Commons.ExtendedApi.StartStep("Step1", step => step.parameters = parameters);
    try
    {
        await Task.Yield();
        Allure.Net.Commons.ExtendedApi.PassStep();
    }
    catch(Exception)
    {
        Allure.Net.Commons.ExtendedApi.FailStep();
        throw;
    }
}

Логика аспекта сгенерирована, но оригинальный метод остался нетронутым. И дальше возникает ключевой вопрос: как заставить тесты вызывать именно сгенерированный код, а не оригинал?

На первый взгляд может показаться, что поможет partial class. Но это не решает проблему. Метод нельзя просто «перекрыть» через partial так, чтобы компилятор начал подставлять новую реализацию вместо старой. Оригинальный и сгенерированный метод начнут конфликтовать.

Interceptors: недостающий элемент

Получается, что Source Generator не АОП, возвращаться к IL-инъекциям больно. И вот здесь появляются Interceptors. В .NET 8 эта возможность пришла как preview, а в .NET 9 стала полноценной частью платформы.

Идея Interceptors простая: можно указать, какой метод должен быть перехвачен другим методом. В результате компилятор перепишет вызов так, чтобы вместо оригинала вызывался нужный interceptor. Это уже очень похоже на то, что нам нужно.

В .NET 8 фича была доступна в preview, поэтому в проекте нужно было явно включить соответствующий флаг:

<PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <Features>InterceptorsPreview</InterceptorsNamespaces>
</PropertyGroup>

Дальше нужно создать атрибут перехвата:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
sealed class InterceptsLocationAttribute(string filePath, int line, int column) : Attribute
{
}

Здесь filePath указывает на файл, где расположен перехватываемый метод, а line и column задают его координаты в файле. Затем создается static class с методом-перехватчиком:

static class Interception
{
    [InterceptsLocation(@"C:\ConsoleApp\TestProject\UnitTest1.cs", line: 4, column: 5)]
    public static void Step1([Name("defined")] int i)                              
    {
        await Task.Yield();
        // здесь пишем уже наш аспект
    }
}

При сборке компилятор подставит вызов перехватчика вместо оригинального метода. Но здесь есть важный нюанс: сигнатура interceptor-метода должна полностью совпадать с сигнатурой перехватываемого метода. Кроме того, перехватчик должен находиться в правильном namespace.

Если изменить оригинальный файл, например добавить отступы, переносы строк или новые строки, координаты могут съехать, и все сломается. Это одна из причин, почему в .NET 9 API был переработан. 

В .NET 9 Interceptors вышли из preview, и способ подключения стал удобнее. Теперь в проекте достаточно указать namespace, где находятся методы, которые мы хотим перехватывать:

<PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <InterceptorsNamespaces>$(InterceptorsNamespaces);TestProject1</InterceptorsNamespaces>
</PropertyGroup>

Теперь нам нужен Source Generator, который будет искать методы с атрибутом AllureStep, определять их местоположение и генерировать перехватчики автоматически.

Как генератор создает атрибут перехвата

Сначала сгенерируем сам атрибут перехватчика:

[global::System.Diagnostics.Conditional("DEBUG")]
[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
sealed file class InterceptsLocationAttribute : global::System.Attribute
{
	public InterceptsLocationAttribute(int version, string data)
	{
    	_ = version;
    	_ = data;
	}
}

Сигнатура упростилась: появились version и data. Не нужно вычислять их вручную — это делает сам генератор. Дальше генератор ищет все вызовы нужного метода в дереве синтаксиса.

private List<InvocationExpressionSyntax> GetInvocationsForMethod(
    Compilation compilation,
    IMethodSymbol methodSymbol,
    CancellationToken ct)
{
    var result = new List<InvocationExpressionSyntax>();
    foreach (var tree in compilation.SyntaxTrees)
    {
        var model = compilation.GetSemanticModel(tree);
        foreach (var node in tree.GetRoot(ct).DescendantNodes())
        {
            if (node is InvocationExpressionSyntax invocation)
            {
                var invokedSymbol = model.GetSymbolInfo(invocation, ct).Symbol 
                  as IMethodSymbol;

                if (invokedSymbol != null &&
                    SymbolEqualityComparer.Default.Equals(invokedSymbol, methodSymbol))
                {
                    result.Add(invocation);
                }
            }
        }
    }

    return result;
}

Из Compilation мы получаем все места вызова метода. Затем берем список invocation-выражений и строим обертку:

var invocationExpressions = 
  GetInvocationsForMethod(compilation, method, context.CancellationToken);
var model = compilation.GetSemanticModel(methodSyntax.SyntaxTree);
foreach (var invocation in invocationExpressions)
{
    // Получаем позицию вызова
    var location = model.GetInterceptableLocation(invocation);
    if (location != null)
    {
        var wrapped = WrapMethod(methodSyntax, method,
            location.Version,
            location.Data);
        methodsBuilder.AppendLine(wrapped);
    }
}

Метод GetInterceptableLocation возвращает полную информацию о месте вызова, включая Data и Version. Именно эти значения и используются в атрибуте перехвата. Если вызов не может быть перехвачен, метод вернет null, поэтому проверка обязательна.

Внутри WrapMethod добавляем атрибут к сгенерированному методу:

return $@"
        	{attributes}
            [System.Runtime.CompilerServices.InterceptsLocation({version}, ""{locationData}"")]
            public static {returnType} {method.Identifier.Text}({classSignatureOriginal}{methodParameters})
            {{
                {newBody}
            }}";

Метод-перехватчик должен полностью повторять сигнатуру оригинального метода, поэтому мы сохраняем все параметры и передаем их в генератор.

Что получилось в итоге и ограничения подхода

Исходный код:

public class AsyncStepsTest
{
    [Fact]
    public async Task Test()
    {
        await Step1(123);
    }
  
    [AllureStep("Step1")]
    private static async Task Step1([Name("defined")] int i)
    {
        await Task.Yield();
    }
}

Сгенерированный код:

namespace System.Runtime.CompilerServices
{
    [global::System.Diagnostics.Conditional("DEBUG")]
    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
    sealed file class InterceptsLocationAttribute : global::System.Attribute
    {
        public InterceptsLocationAttribute(int version, string data)
        {
            _ = version;
            _ = data;
        }
    }
}
namespace TestProject1
{
    public static class AsyncStepsTestGenerated
    {
        [AllureStep("Step1")]
        [System.Runtime.CompilerServices.InterceptsLocation(1, "E8F6MzL5PEiXqlbd94aMNh0BAABVbml0VGVzdDEuY3M=")]
        public static async Task Step1([Name("defined")] int i)
        {
            var parameters = new List<Allure.Net.Commons.Parameter>();
            parameters.Add(new Allure.Net.Commons.Parameter
            {
                name = "defined",
                value = i.ToString()
            });
          
            Allure.Net.Commons.ExtendedApi.StartStep("Step1", 
                                                     step => step.parameters = parameters);
            try
            {
                await Task.Yield();
                Allure.Net.Commons.ExtendedApi.PassStep();
            }
            catch(Exception)
            {
                Allure.Net.Commons.ExtendedApi.FailStep();
                throw;
            }
        }
    }
}

После запуска тестов в allure-results мы увидим шаг Step1 с параметрами. Из примеров видно, что в атрибуте InterceptsLocationAttribute мы указываем AllowMultiple = true — это нужно для множественных перехватов, если у вас Step1 вызывается в нескольких местах.

Если посмотреть на результат через ILSpy, будет видно, что сгенерированный код почти не изменился, а вот в оригинальном классе вызов был подменен:

public static class AsyncStepsTestGenerated
{
    [AllureStep("Step1")]
    [<AsyncStepsTest_AllureGenerated>F6685BD4543E2AFCFA3025AAB3D6BC3426E6F4A6FDC825606995182F7112E28E8__InterceptsLocation(1, "E8F6MzL5PEiXqlbd94aMNh0BAABVbml0VGVzdDEuY3M=")]
    public static async Task Step1([Name("defined")] int i)
    {
        List<Parameter> parameters = new List<Parameter>();
        parameters.Add(new Parameter
        {
            name = "defined",
            value = i.ToString()
        });
        ExtendedApi.StartStep("Step1", delegate(StepResult step)
        {
            step.parameters = parameters;
        });
        try
        {
            await Task.Yield();
            ExtendedApi.PassStep();
        }
        catch (Exception)
        {
            ExtendedApi.FailStep();
            throw;
        }
    }
}
public class UnitTest1
{
    public class AsyncStepsTest
    {
        [Fact]
        public async Task Test()
        {
            await AsyncStepsTestGenerated.Step1(123);
        }
 
        [AllureStep("Step1")]
        private static async Task Step1([Name("defined")] int i)
        {
            await Task.Yield();
        }
    }
}

То есть компилятор подставляет перехватчик в Test, а не вызывает оригинальный метод напрямую. Это возможно потому, что класс с перехватчиками должен быть static.

Ограничения подхода:

  • Можно перехватывать только код, который виден на этапе компиляции. Методы из зависимых библиотек таким способом не перехватить, потому что это все еще генерация кода, а не runtime-подмена.

  • Если метод вообще не вызывается внутри проекта, он не будет перехвачен.

Например, такой код не сработает, и шаги не попадут в отчет:

public class UnitTest1
{
    public class AsyncStepsTest
    {
        [Fact]
        [Allure("Step1")]
        public async Task Test()
        {
            await Task.Yield();
        }
 
        [Fact]
        [AllureStep("Step2")]
        public async Task Test2()
        {
            await Task.Yield();
        }
    }
}

Итоги

Несмотря на ограничения, Interceptors оказались для меня очень полезными. Они позволяют реализовать аспектное поведение без IL-инъекций, без сторонних runtime-хаков и без потери контроля над кодом.

Source Generators отвечают за генерацию логики, а Interceptors — за перенаправление вызова. Вместе они дают именно тот уровень гибкости, который нужен для аккуратной автоматизации шагов в отчетах.

Надеюсь, статья была полезной и помогла лучше понять, как Interceptors и Source Generators решают задачу, которую раньше приходилось закрывать через IL-инъекции.