Оригинальный пост
Одной из новых функций, которая появилась в ASP.NET Core 1.1, стала возможность использовать промежуточные уровни в качестве фильтров MVC. В этом посте я рассмотрю исходный код реализации этой функции, а не то, как ее использовать. В следующем посте я вернусь к тому, как ее использовать, чтобы добиться большего повторного использования кода.
Промежуточные уровни и фильтры
Для начала рассмотрим причины, по которым вы можете использовать помемежуточные уровни поверх фильтров и наоборот. Обе эти возможности разработаны для внедряемых действий (cross-cutting concern) в вашем приложении и обе используются в рамках конвейера обработки запроса, поэтому в некоторых случаях вы можете успешно использовать любую из них.
Главное отличие состоит в границах их действия. Фильтры являются частью MVC и они ограничены промежуточным слоем MVC. Промежуточный уровень имеет доступ только к HttpContext и всему, что было добавлено предыдущими middleware. Фильтры, напротив, имеют доступ ко всему контексту MVC и могут обращаться, например, к данным маршрутизации и информации биндинга модели.
В общем, если у вас есть внедряемые действия (cross-cutting concern), которые не привязаны к MVC, то используйте промежуточный уровень; если ваши внедряемые действия (cross-cutting concern) зависят от MVC или выполняются в рамках конвейера MVC, то используйте фильтры.
Этот промежуточный уровень выполняет две главные функции. Первая цель - убедиться, что конвейер фильтров не прерывается после вызова MiddlewareFilter. Это реализуется путем получения IMiddlewareFeatureFeature, сохраненной в HttpContext до вызова фильтра. После этого можно получить доступ к следующему фильтру через его свойство ResourceExecutionDelegate и асинхронно выполнить его с помощью await.
Вторая цель - сэмулировать поведение конвейера промежуточного уровня вместо конвейера фильтров в тот момент, когда возбуждается исключение. То есть, если один из последующих фильтров или действий возбуждает исключение и ни один фильтр не обрабатывает его, то "конечный-middleware" возбуждает его снова; таким образом, конвейер middleware, используемый в фильтре, может обработать его обычным образом (с помощью try-catch).
Обратите внимание, чтоGet() будет вызван внутри каждого MiddlewareFilter. Если у вас будет несколько MiddlewareFilters в конвейере, то каждый из них будет добавлять экземпляр IMiddlewareFilterFeature, перезаписывая значение, добавленное ранее. Я не буду углубляться, но это может вызвать проблему, если у вас есть промежуточный уровень в вашем MyCustomMiddleware, которые оба обрабатывают ответ конвейера выполнения других middleware и пытаются получить IMiddlewareFilterFeature. В этом случае они получат IMiddlewareFilterFeature, ассоциированную с разными MiddlewareFilter. Этот сценарий маловероятен, но имейте его в виду.
Заключение
Мы закончили заглядывать под капот промежуточных уровней-фильтров, надеюсь, что это было интересно. Я был рад изучить исходный код в качестве источника вдохновения, которое мне понадобится в будущем для реализации чего-то подобного. Надеюсь, и вам понравилось.
Одной из новых функций, которая появилась в ASP.NET Core 1.1, стала возможность использовать промежуточные уровни в качестве фильтров MVC. В этом посте я рассмотрю исходный код реализации этой функции, а не то, как ее использовать. В следующем посте я вернусь к тому, как ее использовать, чтобы добиться большего повторного использования кода.
Промежуточные уровни и фильтры
Для начала рассмотрим причины, по которым вы можете использовать помемежуточные уровни поверх фильтров и наоборот. Обе эти возможности разработаны для внедряемых действий (cross-cutting concern) в вашем приложении и обе используются в рамках конвейера обработки запроса, поэтому в некоторых случаях вы можете успешно использовать любую из них.
Главное отличие состоит в границах их действия. Фильтры являются частью MVC и они ограничены промежуточным слоем MVC. Промежуточный уровень имеет доступ только к HttpContext и всему, что было добавлено предыдущими middleware. Фильтры, напротив, имеют доступ ко всему контексту MVC и могут обращаться, например, к данным маршрутизации и информации биндинга модели.
В общем, если у вас есть внедряемые действия (cross-cutting concern), которые не привязаны к MVC, то используйте промежуточный уровень; если ваши внедряемые действия (cross-cutting concern) зависят от MVC или выполняются в рамках конвейера MVC, то используйте фильтры.
Итак, почему теперь вы можете захотеть использовать промежуточный уровень в качестве фильтров. Я могу представить несколько причин для этого.
Во-первых, у вас уже может реализован некоторый промежуточный уровень, который выполняет что-то полезное, и вам хочется использовать его поведение в рамках конвейера MVC. Вы можете переписать этот промежуточный уровень для использования в качестве фильтр, но было бы лучше просто использовать его так, как есть. Это особенно актуально, если вы используйте часть внешнего промежуточного уровня и не имеете доступа к его исходному коду.
Во-вторых, вы хотите использовать одну и ту же функциональность и в качестве промежуточного уровня, и в качестве фильтра. В этом случае у вас просто будет одна реализация, которую вы сможете использовать в обоих случаях.
Использование MiddlewareFilterAttribute
В посте с анонсом можно найти пример того, как использовать промежуточный уровень в качестве фильтра. Здесь я покажу обратный пример, в котором я хочу выполнить MyCustomMiddleware в тот момент, когда выполняется действие MVC.
Требуется реализовать две вещи. Во-первых нужно создать объект конвейера промежуточного уровня:
public class MyPipeline { public void Configure(IApplicationBuilder applicationBuilder) { var options = // any additional configuration applicationBuilder.UseMyCustomMiddleware(options); } }
и во-вторых пометить атрибутом MiddlewareFilterAttribute действие контроллера или сам контроллер:
[MiddlewareFilter(typeof(MyPipeline))] public IActionResult ActionThatNeedsCustomfilter() { return View(); }
При таком объявлении MyCustomMiddleware будет выполняться каждый раз, когда вызывается метод действия ActionThatNeedsCustomfilter.
Стоит отметить, что MiddlewareFilterAttribute для метода действия сам не принимает тип компонента промежуточного уровня (MyCustomMiddleware), он на самом деле принимает объект конвейера, который сам настраивает промежуточный уровень. Не сильно об этом беспокойтесь сейчас, мы вернемся к этому позже.
В конце этого поста я покажу в коде MVC как реализована эта функциональность.
Атрибут MiddlewareFilterAttribute
Как мы могли убедиться ранее, функциональность промежуточного уровня-фильтра начитается с атрибута MiddlewareFilterAttribute, которым помечается весь контроллер или отдельный его метод. Этот атрибут реализует интерфейс IFilterFactory, который используется для инжектирования сервисов в фильтры MVC. Реализация этого интерфейса требует всего один метод CreateInstance(IServiceProvider provider):
Атрибут MiddlewareFilterAttribute
Как мы могли убедиться ранее, функциональность промежуточного уровня-фильтра начитается с атрибута MiddlewareFilterAttribute, которым помечается весь контроллер или отдельный его метод. Этот атрибут реализует интерфейс IFilterFactory, который используется для инжектирования сервисов в фильтры MVC. Реализация этого интерфейса требует всего один метод CreateInstance(IServiceProvider provider):
public class MiddlewareFilterAttribute : Attribute, IFilterFactory, IOrderedFilter { public MiddlewareFilterAttribute(Type configurationType) { ConfigurationType = configurationType; } public Type ConfigurationType { get; } public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) { var middlewarePipelineService = serviceProvider.GetRequiredService(); var pipeline = middlewarePipelineService.GetPipeline(ConfigurationType); return new MiddlewareFilter(pipeline); } }
Реализация этого атрибута весьма выразительна. Сначала из IoC контейнера извлекается объект MiddlewareFilterBuilder. Затем у этого объекта вызывается метод GetPipeline, в него передается ConfigurationType, который был задан при объявлении атрибута (MyPipeline в предыдущем примере).
GetPipeline возвращает RequestDelegate, который представляет собой конвейер промежуточного уровня, который принимает HttpContext и возвращает Task:
Построение конвейера с использованием MiddlewareFilterBuilder
В последнем примере кода мы видели, как MiddlewareFilterBuilder использовался для превращения типа MyPipeline в актуальную, запускаемую часть промежуточного уровня. Вызов GetPipeline инициализирует конвейер для переданного типа, используя метод BuildPipeline, который представлен ниже в краткой форме:
Создание конвейера из MyPipeline выполняется в MiddlewareFilterConfigurationProvider, в котором ищется подходящий для него метод Configure.
Вы можете подумать, что класс MyPipeline является классом мини-Startup. Как и в классе Startup вам потребуется метод Configure для добавления промежуточного уровня в IApplicationBuilder, и, как и в классе Startup, вы можете инжектировать дополнительные сервисы в этот метод. Одним из основных отличий является то, что у вас нет специфичных для окружения методов Configure, таких как ConfigureDevelopment, - ваш класс должен иметь один и только один конфигурационный метод Configure.
Фильтр MiddlewareFilter
Итак, в качестве резюме - вы помечаете атрибутом MiddlewareFilterAttribute один из ваших методов действия или контроллер, передаете конвейер в качестве фильтра, например, MyPipeline. Он использует MiddlewareFilterBuilder для создания RequestDelegate, который, в свою очередь, создает MiddlewareFilter. И уже этот объект передается в конвейер обработки фильтров MVC.
MiddlewareFilter реализует IAsyncResourceFilter, поэтому в конвейере фильтров он выполняется раньше - сразу после того, как отработает AuthorizationFilter, но перед фильтрами Model Binding и Action. Это позволит вам полностью прервать выполнение запроса, если потребуется.
MiddlewareFilter реализует всего один обязательный метод OnResourceExecutionAsync. Этот метод очень простой. Сначала он сохраняет в отдельный объект MiddlewareFilterFeature контекст выполнения ResourceExecutingContext и следующий фильтр ResourceExecutionDelegate. Затем с помощью HttpContext вызывается следующий промежуточный уровень в конвейере.
Если вы раньше писали фильтры, то вам может показаться, что чего-то не хватает. Обычно, перед возвратом из метода вы вызываете await next() для выполнения следующего фильтра в конвейере, но мы просто возвращаем Task, который получили от вызова RequestDelegate. Но как в этом случае продолжается выполнение конвейера? Для этого мы вернемся к "конечному промежуточного уровню", который я рассматривал в рамках BuildPipeline.
Использование "конечного-middleware" для продолжения конвейера фильтров
Промежуточный уровень, который мы добавили в конце метода BuildPipeline отвечает за продолжение выполнения конвейера фильтров. Схематично он выглядит так:
public delegate Task RequestDelegate(HttpContext context).Наконец, этот делегат используется для создания нового MiddlewareFilter, который возвращается как результат метода. Этот подход использования атрибута IFilterFactory для создания актуального экземпляра фильтра очень часто встречается в коде MVC, и решает проблему внедрения сервисов в атрибуты, а так же заставляет каждый компонент следовать принципу единственной ответственности.
Построение конвейера с использованием MiddlewareFilterBuilder
В последнем примере кода мы видели, как MiddlewareFilterBuilder использовался для превращения типа MyPipeline в актуальную, запускаемую часть промежуточного уровня. Вызов GetPipeline инициализирует конвейер для переданного типа, используя метод BuildPipeline, который представлен ниже в краткой форме:
private RequestDelegate BuildPipeline(Type middlewarePipelineProviderType) { var nestedAppBuilder = ApplicationBuilder.New(); // Get the 'Configure' method from the user provided type. var configureDelegate = _configurationProvider.CreateConfigureDelegate(middlewarePipelineProviderType); configureDelegate(nestedAppBuilder); nestedAppBuilder.Run(async (httpContext) => { // additional end-middleware, covered later }); return nestedAppBuilder.Build(); }Этот метод создает новый IApplicationBuilder и использует его для настройки конвейера промежуточного уровня, используя конвейер, заданный ранее (MyPipeline). Затем он добавляет кусочек "конечного промежуточного уровня" в конец конвейера, к которому я вернусь позже, и создает конвейер в RequestDelegate.
Создание конвейера из MyPipeline выполняется в MiddlewareFilterConfigurationProvider, в котором ищется подходящий для него метод Configure.
Вы можете подумать, что класс MyPipeline является классом мини-Startup. Как и в классе Startup вам потребуется метод Configure для добавления промежуточного уровня в IApplicationBuilder, и, как и в классе Startup, вы можете инжектировать дополнительные сервисы в этот метод. Одним из основных отличий является то, что у вас нет специфичных для окружения методов Configure, таких как ConfigureDevelopment, - ваш класс должен иметь один и только один конфигурационный метод Configure.
Фильтр MiddlewareFilter
Итак, в качестве резюме - вы помечаете атрибутом MiddlewareFilterAttribute один из ваших методов действия или контроллер, передаете конвейер в качестве фильтра, например, MyPipeline. Он использует MiddlewareFilterBuilder для создания RequestDelegate, который, в свою очередь, создает MiddlewareFilter. И уже этот объект передается в конвейер обработки фильтров MVC.
MiddlewareFilter реализует IAsyncResourceFilter, поэтому в конвейере фильтров он выполняется раньше - сразу после того, как отработает AuthorizationFilter, но перед фильтрами Model Binding и Action. Это позволит вам полностью прервать выполнение запроса, если потребуется.
MiddlewareFilter реализует всего один обязательный метод OnResourceExecutionAsync. Этот метод очень простой. Сначала он сохраняет в отдельный объект MiddlewareFilterFeature контекст выполнения ResourceExecutingContext и следующий фильтр ResourceExecutionDelegate. Затем с помощью HttpContext вызывается следующий промежуточный уровень в конвейере.
public class MiddlewareFilter : IAsyncResourceFilter { private readonly RequestDelegate _middlewarePipeline; public MiddlewareFilter(RequestDelegate middlewarePipeline) { _middlewarePipeline = middlewarePipeline; } public Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { var httpContext = context.HttpContext; var feature = new MiddlewareFilterFeature() { ResourceExecutionDelegate = next, ResourceExecutingContext = context }; httpContext.Features.Set<IMiddlewareFilterFeature>(feature); return _middlewarePipeline(httpContext); } }Промежуточный уровень, который мы только что создали, считает, что он вызывается в рамках нормального конвейера - он принимает только HttpContext. Если ему потребуется, он может получить доступ к MVC контексту через доступ к MiddlewareFilterFeature.
Если вы раньше писали фильтры, то вам может показаться, что чего-то не хватает. Обычно, перед возвратом из метода вы вызываете await next() для выполнения следующего фильтра в конвейере, но мы просто возвращаем Task, который получили от вызова RequestDelegate. Но как в этом случае продолжается выполнение конвейера? Для этого мы вернемся к "конечному промежуточного уровню", который я рассматривал в рамках BuildPipeline.
Использование "конечного-middleware" для продолжения конвейера фильтров
Промежуточный уровень, который мы добавили в конце метода BuildPipeline отвечает за продолжение выполнения конвейера фильтров. Схематично он выглядит так:
nestedAppBuilder.Run(async (httpContext) => { var feature = httpContext.Features.Get<IMiddlewareFilterFeature>(); var resourceExecutionDelegate = feature.ResourceExecutionDelegate; var resourceExecutedContext = await resourceExecutionDelegate(); if (!resourceExecutedContext.ExceptionHandled && resourceExecutedContext.Exception != null) { throw resourceExecutedContext.Exception; } });
Этот промежуточный уровень выполняет две главные функции. Первая цель - убедиться, что конвейер фильтров не прерывается после вызова MiddlewareFilter. Это реализуется путем получения IMiddlewareFeatureFeature, сохраненной в HttpContext до вызова фильтра. После этого можно получить доступ к следующему фильтру через его свойство ResourceExecutionDelegate и асинхронно выполнить его с помощью await.
Вторая цель - сэмулировать поведение конвейера промежуточного уровня вместо конвейера фильтров в тот момент, когда возбуждается исключение. То есть, если один из последующих фильтров или действий возбуждает исключение и ни один фильтр не обрабатывает его, то "конечный-middleware" возбуждает его снова; таким образом, конвейер middleware, используемый в фильтре, может обработать его обычным образом (с помощью try-catch).
Обратите внимание, что
Заключение
Мы закончили заглядывать под капот промежуточных уровней-фильтров, надеюсь, что это было интересно. Я был рад изучить исходный код в качестве источника вдохновения, которое мне понадобится в будущем для реализации чего-то подобного. Надеюсь, и вам понравилось.
Комментариев нет:
Отправить комментарий