Одной из новых функций, которая появилась в ASP.NET Core 1.1, стала возможность использовать промежуточные уровни в качестве фильтров MVC. В этом посте я рассмотрю исходный код реализации этой функции, а не то, как ее использовать. В следующем посте я вернусь к тому, как ее использовать, чтобы добиться большего повторного использования кода.
Промежуточные уровни и фильтры
Для начала рассмотрим причины, по которым вы можете использовать помемежуточные уровни поверх фильтров и наоборот. Обе эти возможности разработаны для внедряемых действий (cross-cutting concern) в вашем приложении и обе используются в рамках конвейера обработки запроса, поэтому в некоторых случаях вы можете успешно использовать любую из них.
Главное отличие состоит в границах их действия. Фильтры являются частью MVC и они ограничены промежуточным слоем MVC. Промежуточный уровень имеет доступ только к HttpContext и всему, что было добавлено предыдущими middleware. Фильтры, напротив, имеют доступ ко всему контексту MVC и могут обращаться, например, к данным маршрутизации и информации биндинга модели.
В общем, если у вас есть внедряемые действия (cross-cutting concern), которые не привязаны к MVC, то используйте промежуточный уровень; если ваши внедряемые действия (cross-cutting concern) зависят от MVC или выполняются в рамках конвейера MVC, то используйте фильтры.
public class MyPipeline { public void Configure(IApplicationBuilder applicationBuilder) { var options = // any additional configuration applicationBuilder.UseMyCustomMiddleware(options); } }
[MiddlewareFilter(typeof(MyPipeline))] public IActionResult ActionThatNeedsCustomfilter() { return View(); }
Атрибут 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); } }
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).
Обратите внимание, что
Заключение
Мы закончили заглядывать под капот промежуточных уровней-фильтров, надеюсь, что это было интересно. Я был рад изучить исходный код в качестве источника вдохновения, которое мне понадобится в будущем для реализации чего-то подобного. Надеюсь, и вам понравилось.