Извлечение метаданных из обработчика Minimal API
Эта вторая статья из серии "За кулисами Minimal API" Эндрю Лока. В предыдущей статье мы достаточно подробно рассмотрели, как простой minimal API MapGet("/", () => "Hello world", превращается в RequestDelegate, который может вызываться EndpointMiddleware. В этой статье я сосредоточусь на методе RequestDelegateFactory.InferMetadata() и разберу, как он работает.
Получение метаданных от делегата
В предыдущей статье основное внимание уделялось методу CreateRouteEndpointBuilder(). Этот метод вызывается, когда приложение впервые получает запрос, и отвечает за преобразование RouteEntry и RoutePattern, связанных с эндпоинтом, в RouteEndpointBuilder. Затем всё это используется для создания RouteEndpoint, который можно вызывать для обработки данного запроса.
CreateRouteEndpointBuilder() отвечает как за составление списка метаданных для эндпоинта, так и за создание его RequestDelegate. Напомним, что RequestDelegate — это функция, которая фактически вызывается для обработки запроса и выглядит следующим образом:
public delegate Task RequestDelegate(HttpContext context);Обратите внимание, что эта сигнатура не соответствует лямбда делегату, который мы передали в MapGet, в примере Hello World - () => «Hello world!» имеет сигнатуру, которая выглядит следующим образом:
public string SomeMethod();CreateRouteEndpointBuilder() в сочетании с классом RequestDelegateFactory отвечает за создание функции, которая соответствует сигнатуре RequestDelegate на основе предоставленного делегата.
В рамках создания этого метода RequestDelegateFactory необходимо проанализировать предоставленный делегат, чтобы определить типы параметров и возвращаемые типы, используемые обработчиком. Эта информация нужна фабрике RequestDelegateFactory, чтобы она могла выдавать код, связывающий аргументы метода со значениями маршрута, со службами в контейнере DI или, например, с телом запроса.
Рассмотрим немного более сложный пример, который внедряет объект HttpRequest в обработчик: MapGet("/", (HttpRequest request) => "Hello world!"). RequestDelegateFactory должен создать метод с требуемой сигнатурой RequestDelegate, но который создает аргументы, необходимые для вызова метода обработчика. Окончательный результат выглядит примерно так:
Task Invoke(HttpContext httpContext)
{
// handler — оригинальный метод лямбда-обработчика.
// Параметр HttpRequest автоматически создается из HttpContext аргумента
string text = handler.Invoke(httpContext.Request);
// Возвращаемое значение записывается в ответ, как и ожидалось.
httpContext.Response.ContentType ??= "text/plain; charset=utf-8";
return httpContext.Response.WriteAsync(text);
}ExpersionНекоторая информация о функции-обработчике и ее параметрах явно добавляется в качестве метаданных в эндпоинт, а также используется для непосредственного построения RequestDelegate. В следующем разделе мы рассмотрим функцию RequestDelegateFactory.InferMetadata(), чтобы увидеть, как выводится эта информация.
Вывод метаданных о методе обработчика
В предыдущей статье я показал метод RouteEndpointDataSource.CreateRDFOptions(), который создает экземпляр RequestDelegateFactoryOptions на основе методов обработчика RouteEntry и RoutePattern. Этот объект представляет собой простой набор опций для управления тем, как создается окончательный RequestDelegate. Большинство свойств объекта говорят сами за себя, но я подписал их назначение ниже:
public sealed class RequestDelegateFactoryOptions
{
// DI контейннер для приложения
public IServiceProvider? ServiceProvider { get; init; }
// Имена любых параметров в маршруте.
// например для маршрута /view/{organization}/{id}
// содержит два значения, "organization" и "id"
public IEnumerable<string>? RouteParameterNames { get; init; }
// True если RequestDelegate должен прерывать плохие запросы
public bool ThrowOnBadRequest { get; init; }
// Должен ли RequestDelegate пытаться связать тело запроса по умолчанию
// Смотрите предыдущую статью для подробного объяснения
public bool DisableInferBodyFromParameters { get; init; }
// Используется для создания RequestDelegate и для применения фильтров к эндпоинтам
public EndpointBuilder? EndpointBuilder { get; init; }
}Вы можете прочитать, как метод CreateRouteEndpointBuilder() создает объект, вызвав CreateRDFOptions в предыдущем посте. После создания параметров у нас есть вызов RequestDelegateFactory.InferMetadata, показанный ниже.
public static RequestDelegateMetadataResult InferMetadata(
MethodInfo methodInfo, // 👈 Информация об отражении метода обработчика
RequestDelegateFactoryOptions? options = null) // 👈 Опции объекта
{
// Создает "context" объекь (показан кратко)
RequestDelegateFactoryContext factoryContext = CreateFactoryContext(options);
// Читает информацию об аргуметах метода обработчика
factoryContext.ArgumentExpressions = CreateArgumentsAndInferMetadata(methodInfo, factoryContext);
// Сохраняет метаданные в объект, который в последствии используется для создания RequestDelegate
return new RequestDelegateMetadataResult
{
EndpointMetadata = AsReadOnlyList(factoryContext.EndpointBuilder.Metadata),
};
}Этот метод принимает объект MethodInfo, который содержит всю «отражающую» информацию о методе. Обратите внимание, что в моих примерах эндпоинтов API я показывал обработчик в виде лямбда выражения, но вы получили бы то же самое, если бы использовали статический метод, метод экземпляра или локальную функцию в качестве обработчика.
InferMetadata, показанный выше, состоит из двух основных шагов:
- Создание объекта "context"
RequestDelegateFactoryContext - Получение информации о параметрах метода обработчика
Метод CreateFactoryContext вызывается как здесь, в InferMetadata(), так и позже, при создании RequestDelegate в RequestDelegateFactory.Create(), поэтому он имеет несколько необязательных параметров. При выводе метаданных предоставляется только первый параметр, поэтому большая часть метода на данный момент не используется. Схема ниже показывает упрощенную версию CreateFactoryContext с учетом этого:
private static RequestDelegateFactoryContext CreateFactoryContext(
RequestDelegateFactoryOptions? options,
RequestDelegateMetadataResult? metadataResult = null, // 👈 всегда null в InferMetadata
Delegate? handler = null) // 👈 всегда null в InferMetadata
{
if (metadataResult?.CachedFactoryContext is not null)
{
// детали скрыты, потомучто metadataResult ровняется null в InferMetadata и не вызывается
}
// ServiceProvider не null и устанавливается в CreateRDFOptions()
IServiceProvider serviceProvider = options?.ServiceProvider;
// EndpointBuilder ровняется null в InferMetadata, поэтому всегда создает новый билдер
var endpointBuilder = options?.EndpointBuilder ?? new RDFEndpointBuilder(serviceProvider);
var factoryContext = new RequestDelegateFactoryContext
{
Handler = handler, // null, поскольку не указан в InferMetadata
ServiceProvider = options.ServiceProvider,
ServiceProviderIsService = serviceProvider.GetService<IServiceProviderIsService>(),
RouteParameters = options?.RouteParameterNames?.ToList(),
ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false,
DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false,
EndpointBuilder = endpointBuilder
MetadataAlreadyInferred = metadataResult is not null, // false
};
return factoryContext;
}Как видно из приведенного выше фрагмента, RequestDelegateFactoryContext в основном содержит копии значений из RequestDelegateFactoryOptions. RDFEndpointBuilder — это очень простая реализация EndpointBuilder, которая предотвращает вызов Build().
private sealed class RDFEndpointBuilder : EndpointBuilder
{
public RDFEndpointBuilder(IServiceProvider applicationServices)
{
ApplicationServices = applicationServices;
}
public override Endpoint Build() => throw new NotSupportedException();
}После создания RequestDelegateFactoryContext следующий большой шаг — анализ параметров обработчика, чтобы понять, как их создавать (из параметров запроса, служб и т. д.), и добавление метаданных в коллекцию эндпоинта.
Анализ параметров обработчика
RequestDelegateFactory анализирует параметры MethodInfo обработчика в CreateArgumentsAndInferMetadata(), передавая метод обработчика и новый объект контекста:
private static Expression[] CreateArgumentsAndInferMetadata(
MethodInfo methodInfo, RequestDelegateFactoryContext factoryContext)
{
// Добавляет любые принимаемые по умолчанию метаданные.
// При этом выполняется много рефлекшенов и построения деревьев выражений,
// поэтому результаты кэшируются в RequestDelegateFactoryOptions.FactoryContext,
// а затем повторно используются в RequestDelegateFactory.Create().
var args = CreateArguments(methodInfo.GetParameters(), factoryContext);
if (!factoryContext.MetadataAlreadyInferred)
{
PopulateBuiltInResponseTypeMetadata(methodInfo.ReturnType, factoryContext.EndpointBuilder);
// Добавляет метаданные, предоставленные типом возвращаемого
// делегата и типами параметров, вводит данные
EndpointMetadataPopulator.PopulateMetadata(
methodInfo,
factoryContext.EndpointBuilder,
factoryContext.Parameters);
}
return args;
}Комментарии и имена методов в этом методе довольно хорошо объясняют, что происходит, но здесь важно понимать, что мы берем MethodInfo и генерируем массив Expression[] (одно выражение для каждого параметра, который принимает метод обработчика).
Создание деревьев выражений может быть… трудоемким. В этом посте мы достаточно подробно рассмотрим, как проверяются аргументы, а в последующих постах мы подробно рассмотрим, как создаются выражения для реализации минимального поведения привязки модели API.
В следующем разделе мы подробно рассмотрим вызов CreateArguments(), где создается выражение для каждого из параметров метода обработчика.
Создание выражений аргументов.
Первым вызовом метода в CreateArgumentsAndInferMetadata() является CreateArguments(), в котором передаются сведения ParameterInfo[] для метода обработчика, а также объект контекста. Этот метод слишком объемный для чтения в одном блоке кода, поэтому я разобью его на разделы и буду обсуждать каждый по ходу дела.
private static Expression[] CreateArguments(
ParameterInfo[]? parameters, RequestDelegateFactoryContext factoryContext)
{
if (parameters is null || parameters.Length == 0)
{
return Array.Empty<Expression>();
}
var args = new Expression[parameters.Length];
factoryContext.ArgumentTypes = new Type[parameters.Length];
factoryContext.BoxedArgs = new Expression[parameters.Length];
factoryContext.Parameters = new List<ParameterInfo>(parameters);
var hasFilters = factoryContext.EndpointBuilder.FilterFactories.Count > 0;
// ...
}Аргумент массива параметров содержит экземпляр ParameterInfo для каждого параметра в обработчике эндпоинта. Если обработчик не принимает никаких параметров, то массив параметров пуст, и делать больше нечего. В противном случае мы инициализируем некоторые массивы, которые используем для записи метаданных о параметрах. Мы также проверяем, применяются ли к эндпоинту какие-либо фильтры, так как мы можем избежать некоторых действий, если их нет.
Сафии Абдаллы
После инициализации всех массивов мы перебираем параметры метода и создаем Expression, используемый для привязки аргумента к запросу:
for (var i = 0; i < parameters.Length; i++)
{
args[i] = CreateArgument(parameters[i], factoryContext);
if (hasFilters)
{
// Если метод содержит фильтры, создаем expression`ы
// для построения EndpointFilterInvocationContext
}
factoryContext.ArgumentTypes[i] = parameters[i].ParameterType;
factoryContext.BoxedArgs[i] = Expression.Convert(args[i], typeof(object));
}Метод CreateArgument() выполняет много работы. Он принимает ParameterInfo и считывает различные метаданные, чтобы установить, каким образом создать Expression, который можно использовать для «создания» аргумента обработчику. Например, в начале этого поста вы видели, что параметр HttpRequest был создан с использованием выражения httpContext.Request:
Task Invoke(HttpContext httpContext)
{
string text = handler.Invoke(httpContext.Request);
// ...
}В зависимости от источника аргумента (т. е. на основе поведения привязки модели) создается другое выражение. Например, если параметр должен быть привязан к параметру запроса, например, в эндпоинте MapGet("/", (string? search) => search), будет создано выражение, которое считывается из строки запроса:
Task Invoke(HttpContext httpContext)
{
string text = handler.Invoke(httpContext.Request.Query["search"]);
// ...
}Для приведенного выше примера Expression, представляющий
httpContext.Request.Query["search"]будет возвращен из CreateArgument() и сохранен в массиве args. Дополнительные сведения также будут установлены в RequestDelegateFactoryContext, чтобы уменьшить количество доработок в RequestDelegateFactory.Create(). При необходимости CreateArgument также выводит метаданные [Accepts] об ожидаемом формате тела запроса и добавляет их в коллекцию метаданных.
CreateArgument()"Запакованная" версия Expression (где результат выражения приводится к object) также создается для использования в фильтрах эндпоинтов (где это необходимо), а типы параметров сохраняются в factoryContext.
Последним шагом в методе CreateArguments() является выполнение некоторых проверок работоспособности, проверка распространенных неподдерживаемых сценариев перед возвратом сгенерированных аргументов Expression[].
// Пытались ли мы сделать вывод о привязке к телу, когда не должны?
// (подробности о том, как вычисляется DisableInferredFromBody, см. в предыдущем посте)
if (factoryContext.HasInferredBody && factoryContext.DisableInferredFromBody)
{
var errorMessage = BuildErrorMessageForInferredBodyParameter(factoryContext);
throw new InvalidOperationException(errorMessage);
}
// Пытались ли мы выполнить привязку как к телу JSON, так и к телу формы?
// Не может быть и того, и другого!
if (factoryContext.JsonRequestBodyParameter is not null &&
factoryContext.FirstFormRequestBodyParameter is not null)
{
var errorMessage = BuildErrorMessageForFormAndJsonBodyParameters(factoryContext);
throw new InvalidOperationException(errorMessage);
}
// Пытались ли мы выполнить привязку к телу несколько раз?
if (factoryContext.HasMultipleBodyParameters)
{
var errorMessage = BuildErrorMessageForMultipleBodyParameters(factoryContext);
throw new InvalidOperationException(errorMessage);
}
return args;Теперь у нас есть выражение для каждого из параметров обработчика, описывающее, как он должен быть создан с учетом доступного параметра HttpContext. Следующим шагом является чтение возвращаемого значения метода обработчика и его использование для вывода HTTP-ответа, генерируемого эндпоинтом.
Вывод типа ответа HTTP из обработчика.
Метод PopulateBuiltInResponseTypeMetadata() отвечает за эффективную попытку вывести атрибут [Produces] для обработчика на основе возвращаемого типа метода и добавить его в коллекцию метаданных. Вы можете прочитать подробную логику метода здесь, но он эффективно использует следующую последовательность.
- Возвращаемый тип
Task<T>илиValueTask<T>(или другие поддерживаемыеawaitтипы)? (Если да, возвращаемый тип обрабатывается как типТи выполнение продолжается.). - Возвращаемый тип
void,Task,ValueTask, илиIResult? (Если да, то не можем вывести тип ответа, поэтому выходим). - Возвращаемый тип
string? (Если да, то добавляем[Produces("text/plain")], если нет, то добавляем[Produces(returnType)])
Обратите внимание, что начиная с .NET 7 типы IResult могут реализовывать IEndpointMetadataProvider для предоставления дополнительной [Produces] информации (например, с помощью помощников TypedResults), но IEndpointMetadataProvider не обрабатывается в методе PopulateBuiltInResponseTypeMetadata(). Вместо этого, он обрабатывается в следующем методе.
Заполнение метаданных из параметров с самоописанием.
В .NET 7 добавлены интерфейсы IEndpointMetadataProvider и IEndpointParameterMetadataProvider, чтобы параметры вашего обработчика и возвращаемые типы были «самоописываемыми». Параметры, которые реализуют один (или оба) из этих интерфейсов, могут заполнять метаданные о себе, что обычно означает, что вам нужно меньше атрибутов и гибких методов для описания ваших API для OpenAPI.
Оба интерфейса содержат один статический абстрактный метод, поэтому вы реализуете их, реализуя статический метод в своем классе:
public interface IEndpointMetadataProvider
{
static abstract void PopulateMetadata(MethodInfo method, EndpointBuilder builder);
}
public interface IEndpointParameterMetadataProvider
{
static abstract void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder);
}Статическая часть важна, так как это означает, что RequestDelegateFactory может прочитать детали метаданных один раз, не имея экземпляра вашего параметра. Это происходит в методе RequestDelegateFactory.PopulateBuiltInResponseTypeMetadata(), который вызывает вспомогательный метод EndpointMetadataPopulator.PopulateMetadata(), передавая метод обработчика, EndpointBuilder и параметры обработчика.
Я полностью воспроизвел EndpointMetadataPopulator ниже и добавил несколько пояснительных комментариев. Метод PopulateMetadata() перебирает каждый из параметров метода, проверяет, реализует ли он какой-либо из интерфейсов, и, если да, вызывает реализованную функцию. Однако все это приходится делать с помощью рефлексии, что немного усложняет понимание!
internal static class EndpointMetadataPopulator
{
public static void PopulateMetadata(
MethodInfo methodInfo,
EndpointBuilder builder,
IEnumerable<ParameterInfo>? parameters = null)
{
// Эта переменная массива создается здесь, чтобы массив
// можно было переиспользовать для каждого аргумента, уменьшая алокацию
object?[]? invokeArgs = null;
parameters ??= methodInfo.GetParameters();
// Получение метаданных из параметров типа
foreach (var parameter in parameters)
{
if (typeof(IEndpointParameterMetadataProvider)
.IsAssignableFrom(parameter.ParameterType))
{
// Параметры типа реализуют IEndpointParameterMetadataProvider
invokeArgs ??= new object[2];
invokeArgs[0] = parameter;
invokeArgs[1] = builder;
// Использование рефлексии для вызова PopulateMetadata для типа параметра
// Использование универсального метода между ними — хитрый способ для неявного кэширования
PopulateMetadataForParameterMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs);
}
if (typeof(IEndpointMetadataProvider).IsAssignableFrom(parameter.ParameterType))
{
// Parameter type implements IEndpointMetadataProvider
invokeArgs ??= new object[2];
invokeArgs[0] = methodInfo;
invokeArgs[1] = builder;
// Использование рефлексии для вызова PopulateMetadata для типа параметра
// Использование универсального метода между ними — хитрый способ для неявного кэширования
PopulateMetadataForEndpointMethod.MakeGenericMethod(parameter.ParameterType).Invoke(null, invokeArgs);
}
}
// Получение метаданных из возвращаемого типа
var returnType = methodInfo.ReturnType;
if (AwaitableInfo.IsTypeAwaitable(returnType, out var awaitableInfo))
{
// If it's a Task<T> or ValueTask<T>, use the T as the returnType
returnType = awaitableInfo.ResultType;
}
if (returnType is not null
&& typeof(IEndpointMetadataProvider).IsAssignableFrom(returnType))
{
// Return type implements IEndpointMetadataProvider
invokeArgs ??= new object[2];
invokeArgs[0] = methodInfo;
invokeArgs[1] = builder;
// Используется рефлексия, чтобы вызвать PopulateMetadata для возвращаемого типа.
PopulateMetadataForEndpointMethod.MakeGenericMethod(returnType).Invoke(null, invokeArgs);
}
}
// Вспомогательные методы и свойства для эффективного вызова членов интерфейса
private static readonly MethodInfo PopulateMetadataForParameterMethod = typeof(EndpointMetadataPopulator).GetMethod(nameof(PopulateMetadataForParameter), BindingFlags.NonPublic | BindingFlags.Static)!;
private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(EndpointMetadataPopulator).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!;
private static void PopulateMetadataForParameter<T>(ParameterInfo parameter, EndpointBuilder builder)
where T : IEndpointParameterMetadataProvider
{
T.PopulateMetadata(parameter, builder);
}
private static void PopulateMetadataForEndpoint<T>(MethodInfo method, EndpointBuilder builder)
where T : IEndpointMetadataProvider
{
T.PopulateMetadata(method, builder);
}
}Это подводит нас к последнему методу RequestDelegateFactory.CreateArgumentsAndInferMetadata(). На данный момент Expression для каждого из параметров обработчика создано, включая определение того, откуда должен быть связан аргумент (на основе минимальных правил привязки модели API — подробнее об этом в следующем посте). Все метаданные, связанные с этим процессом, заполнены и добавлены в список метаданных EndpointBuilder. На этом этапе остается только вернуть RequestDelegateMetadataResult из InferMetadata(), содержащий все метаданные, которые мы получили из функции-обработчика:
public static RequestDelegateMetadataResult InferMetadata(MethodInfo methodInfo, RequestDelegateFactoryOptions? options = null)
{
var factoryContext = CreateFactoryContext(options);
factoryContext.ArgumentExpressions = CreateArgumentsAndInferMetadata(methodInfo, factoryContext);
// возвращает метаданные как IReadOnlyList<T>
return new RequestDelegateMetadataResult
{
EndpointMetadata = AsReadOnlyList(factoryContext.EndpointBuilder.Metadata),
};
}CachedFactoryContextпоэтому я поднял здесь проблему, которая исправлена для .NET 8Наконец, мы добрались до конца нашего первого взгляда на RequestDelegateFactory.InferMetadata(). Один важный метод, который я упустил из виду, — это CreateArgument(), который отвечает за создание деревьев выражений, которые заполняют параметры метода обработчика из аргумента HttpContext, переданного в RequestDelegate.
В следующем посте я рассмотрю алгоритм, который CreateArgument() использует для создания каждого аргумента Expression, и, следовательно, правила привязки модели для minimal API.
Подводя итог.
В этом посте я впервые рассмотрел класс RequestDelegateFactory, который используется для создания экземпляра RequestDelegate из минимального метода обработчика API, чтобы его можно было вызвать в EndpointMiddleware. В этом посте я рассмотрел функцию InferMetadata().
InferMetadata() вызывается как часть конструкции эндпоинта для извлечения метаданных о методе обработчика, таких как типы его аргументов, тип возвращаемого значения, подразумеваемые атрибуты [Produces] и другие сведения. В рамках этого процесса InferMetadata() также строит деревья выражений, которые создают аргументы обработчика из HttpContext в окончательном RequestDelegate.
コスプレ衣装は写真映えだけでなく、移動や休憩のしやすさも含めて選びたいところです。写真映えを意識する場面では、Fate スペースエレシュキガル 衣装を確認して全体のシルエットを見られます。写真で見える部分を意識して準備すると、キャラクターの印象を整えやすくなります。
Оригинал статьи можно прочитать по ссылке.