Фильтры действий, или Как просто улучшить читаемость кода

Введение

В свободное от работы время я, как и многие другие разработчики, занимаюсь созданием своих приложений, чтобы опробовать самые последние технологии, создать что-то полезное для повседневного использования или просто открыть для себя что-то новое. Одним из таких проектов было веб-приложение, которое обрабатывало данные, введённые пользователем, и планировало выполнение задач, основанных на введённых данных. Так как личные проекты не ограничены во времени, то мне хотелось по возможности избавить проект от всех раздражающих моментов. И одним из таких моментов были повторяющиеся строчки кода в методах контроллера. Я начал искать решение этой проблемы и наткнулся на фильтры. Идея использования фильтров для поддержания чистоты кода показалась мне не только интересной и эффективной, но в то же время простой, поэтому я решил поделиться этой информацией с вами.

Роль фильтров в процессе обработки запроса

Сначала обсудим сами фильтры: для чего же они нужны? Фильтры позволяют выполнять определённые действия на различных стадиях обработки запроса в ASP.NET Core. Существуют следующие встроенные фильтры:

  • Фильтры авторизации (Authorization filters) выполняются самыми первыми и определяют, может ли пользователь выполнить текущий запрос.
  • Фильтры ресурсов (Resource filters) вызываются после фильтров авторизации и необходимы, как следует из названия, для обработки ресурсов. В частности, данный тип фильтров применяют в качестве механизма кэширования.
  • Фильтры действий (Action Filters) выполняют указанные в них операции до и после выполнения метода контроллера, обрабатывающего запрос.
  • Фильтры исключений (Exception Filters) используются для перехвата необработанных исключений, произошедших при создании контроллера, привязке модели и выполнении кода фильтров действий и методов контроллера.
  • И наконец, самыми последними вызываются фильтры результатов (Result Filters), если метод контроллера был выполнен успешно. Данный тип фильтров чаще всего используется, чтобы модифицировать конечные результаты, например, мы можем создать свой заголовок ответа, в котором добавим нужную нам информацию.

Ниже представлена схема, которая показывает, в каком порядке вызываются фильтры в процессе обработки запроса:

Из всех фильтров наиболее полезным в повседневном программировании я нахожу фильтры действий. С их помощью можно вынести повторяющиеся операции и хранить их в одном месте. Далее я приведу примеры, как можно «почистить» код, но сперва расскажу о самих фильтрах действий.

Внутреннее устройство фильтров действий

Фильтры действий в ASP.NET

Интерфейс IActionFilter, который нужно реализовать, чтобы создать фильтр действий, существовал ещё в ASP.NET MVC. Он определяет методы OnActionExecuting, который вызывается перед выполнением метода контроллера, и OnActionExecuted, который вызывается сразу после. Ниже представлен пример простейшего фильтра действий, который выводит информацию во время отладки приложения до и после выполнения метода контроллера:

public class CustomActionFilter:IActionFilter 
{ 
        public void OnActionExecuting(ActionExecutingContext filterContext) 
        { 
            Debug.WriteLine("Before Action Execution"); 
        } 

        public void OnActionExecuted(ActionExecutedContext filterContext) 
        { 
            Debug.WriteLine("After Action Execution"); 
        } 
}

Чтобы использовать вышеуказанный фильтр, его нужно зарегистрировать. Для этого в файле FilterConfig.cs, который находится в папке App_Start, следует добавить следующую строку:

public static void RegisterGlobalFilters(GlobalFilterCollection filters) 
{ 
        filters.Add(new HandleErrorAttribute()); 
        filters.Add(new CustomActionFilter()); 
}

Но гораздо удобнее использовать фильтры как атрибуты. Для этих целей существует абстрактный класс ActionFilterAttribute, который унаследован от класса FilterAttribute, а также реализует интерфейсы IActionFilter и IResultFilter. Таким образом, наш класс можно переписать следующим образом:

public class CustomActionFilterAttribute:ActionFilterAttribute 
{ 
        public override void OnActionExecuting(ActionExecutingContext filterContext) 
        { 
            Debug.WriteLine("Before Action Execution"); 
        } 

        public override void OnActionExecuted(ActionExecutedContext filterContext) 
        { 
            Debug.WriteLine("After Action Execution"); 
        } 
} 

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

public class HomeController : Controller 
{ 
        [CustomActionFilter] 
        public ActionResult Index() 
        { 
            return View(); 
        } 
}

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

Фильтры действий в ASP.NET Core

С появлением ASP.NET Core в фильтрах действий произошёл ряд изменений. Кроме интерфейса IActionFilter, теперь имеется ещё и IAsyncActionFilter, который определяет единственный метод OnActionExecutionAsync. Ниже приведён пример класса, реализующего интерфейс IAsyncActionFilter:

public class AsyncCustomActionFilterAttribute:Attribute, IAsyncActionFilter 
{ 
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) 
        { 
            Debug.WriteLine("Before Action Execution"); 

            await next(); 

            Debug.WriteLine("After Action Execution"); 
        } 
} 

В качестве второго параметра методу передаётся делегат ActionExecutionDelegate, с помощью которого вызываются либо следующие по порядку фильтры действий, либо сам метод контроллера.

Применяют такой фильтр так же, как и синхронный:

public class HomeController : Controller 
{ 
        [AsyncCustomActionFilter] 
        public ActionResult Index() 
        { 
            return View(); 
        } 
}

Также изменения затронули абстрактный класс ActionFilterAttribute: теперь он наследуется от класса Attribute и реализует синхронные и асинхронные интерфейсы для фильтров действий (IActionFilter и IAsyncActionFilter) и для фильтров результатов (IResultFilter и IAsyncResultFilter), а также интерфейс IOrderedFilter.

Фильтры действий в действии

Перейдём непосредственно к случаям, когда лучше использовать фильтры действий. Возьмём, например, ситуацию, когда мы создаём веб-приложение и нам нужно сохранить данные, которые приложение получает с помощью метода POST. Допустим, мы ведём сведения о сотрудниках организации. Чтобы представить данные на сервере, мы используем следующий класс:

public class Employee 
{ 
        [Required(ErrorMessage = "First name is required")] 
        public string FirstName { get; set; } 

        [Required(ErrorMessage = "Last name is required")] 
        public string LastName { get; set; } 

        [AgeRestriction(MinAge = 18, ErrorMessage = "Date of birth is incorrect")] 
        public DateTime DateOfBirth { get; set; } 

        [StringLength(50, MinimumLength = 2)] 
        public string Position { get; set; } 

        [Range(45000, 200000)] 
        public int Salary { get; set; } 
} 

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

После того как были реализованы методы POST и PUT, мы видим, что оба метода содержат повторяющиеся части кода:

[HttpPost] 
public IActionResult Post([FromBody] Employee value) 
{ 
            if (value == null) 
            { 
                return BadRequest("Employee value cannot be null"); 
            } 

            if (!ModelState.IsValid) 
            { 
                return BadRequest(ModelState); 
            } 

            // Perform save actions 
            return Ok(); 
} 

[HttpPut] 
public IActionResult Put([FromBody] Employee value) 
{ 
            if (value == null) 
            { 
                return BadRequest("Employee value cannot be null"); 
            } 

            if (!ModelState.IsValid) 
            { 
                return BadRequest(ModelState); 
            } 

            // Perform update actions 
            return Ok(); 
} 

И здесь нам на помощь приходят фильтры действий. Создадим новый фильтр действий и перенесём в него повторяющиеся строки следующим образом:

public class EmployeeValidationFilterAttribute : ActionFilterAttribute 
{ 
        public override void OnActionExecuting(ActionExecutingContext context) 
        { 
            var employeeObject = context.ActionArguments.SingleOrDefault(p => p.Value is Employee); 
            if (employeeObject.Value == null) 
            { 
                context.Result = new BadRequestObjectResult("Employee value cannot be null"); 
                return; 
            } 

            if (!context.ModelState.IsValid) 
            { 
                context.Result = new BadRequestObjectResult(context.ModelState); 
            } 
        } 
} 

Теперь удаляем ставшие ненужными части кода и применяем созданный нами атрибут-фильтр:

public class EmployeeController : ControllerBase 
{ 
        [EmployeeValidationFilter] 
        [HttpPost] 
        public IActionResult Post([FromBody] Employee value) 
        { 
            // Perform save actions 

            return Ok(); 
        } 

        [EmployeeValidationFilter] 
        [HttpPut] 
        public IActionResult Put([FromBody] Employee value) 
        { 
            // Perform update actions 

            return Ok(); 
        } 
} 

Теперь код выглядит гораздо компактнее и красивее, но в нашем случае его ещё можно упростить: т.к. в контроллере всего 2 метода и оба используют один и тот же фильтр, то можно применить атрибут непосредственно к контроллеру:

[EmployeeValidationFilter] 
public class EmployeeController : ControllerBase 
{ 
            // Perform update actions 
} 

Таким образом, с помощью фильтров действий мы вынесли повторяющиеся отрывки кода. Выглядит это просто. Но что нужно сделать, если в фильтр действий нам необходимо передать зависимость?

Разработчики часто сталкиваются с задачей, когда требуется добавить логирование для определённых методов. Поэтому попробуем добавить в фильтры действий средство логирования, которое будет записывать информацию перед выполнением методов POST или PUT контроллера и сразу после. Наш фильтр будет выглядеть следующим образом:

public class LoggingFilter: IActionFilter 
{ 
        private readonly ILogger _logger; 

        public LoggingFilter(ILoggerFactory loggerFactory) 
        { 
            _logger = loggerFactory.CreateLogger<LoggingFilter>(); 
        } 

        public void OnActionExecuted(ActionExecutedContext context) 
        { 
            _logger.LogInformation($"{context.ActionDescriptor.DisplayName} executed"); 
        } 

        public void OnActionExecuting(ActionExecutingContext context) 
        { 
            _logger.LogInformation($"{context.ActionDescriptor.DisplayName} is executing"); 
        } 
} 

Теперь мы можем применить этот фильтр либо глобально, либо к конкретной области. Сначала попробуем зарегистрировать его глобально. Для этого нам в Startup.cs следует добавить следующие строки:

services.AddControllers(options => 
{ 
                options.Filters.Add<LoggingFilter>(); 
}); 

Если же нам нужно применить фильтр, например, к определённому методу контроллера, то следует его использовать вместе с ServiceFilterAttribute:

[HttpPost] 
[ServiceFilter(typeof(LoggingFilter))] 
public IActionResult Post([FromBody] Employee value) 

ServiceFilterAttribute является фабрикой для других фильтров, реализующей интерфейс IFilterFactory и использующей IServiceProvider для получения нужного фильтра. Поэтому в Startup.cs нам необходимо зарегистрировать наш фильтр следующим образом:

services.AddSingleton<LoggingFilter>(); 

Запустив проект, мы сможем убедиться, что фильтр действий применился только к тем контроллерам и методам, у которых он указан в качестве атрибута.

Внедрение зависимостей в фильтры действий позволяет создавать удобные атрибуты-утилиты, которые можно легко переиспользовать. Ниже представлен пример фильтра, который проверяет наличие объекта в хранилище по Id и возвращает его, если он есть:

public class ProviderFilter : IActionFilter 
{ 
        private readonly IDataProvider _dataProvider; 

        public ProviderFilter(IDataProvider dataProvider) 
        { 
            _dataProvider = dataProvider; 
        } 

        public void OnActionExecuted(ActionExecutedContext context) 
        { 
        } 

        public void OnActionExecuting(ActionExecutingContext context) 
        { 
            object idValue; 
            if (!context.ActionArguments.TryGetValue("id", out idValue)) 
            { 
                throw new ArgumentException("id"); 
            } 

            var id = (int)idValue; 
            var result = _dataProvider.GetElement(id); 
            if (result == null) 
            { 
                context.Result = new NotFoundResult(); 
            } 
            else 
            { 
                context.HttpContext.Items.Add("result", result); 
            } 
        } 
} 

Применить этот фильтр можно так же, как и фильтр из предыдущего примера, с помощью ServiceFilterAttribute.

Фильтры действий раньше очень часто применяли, чтобы заблокировать контент для определённых браузеров на основе информации о User-Agent. На ранних этапах становления веб-разработки многие сайты создавались исключительно для наиболее популярных браузеров, остальные же считались «запрещёнными». Сейчас данный подход является нежелательным, т.к. рекомендуется создавать такую HTML-разметку, которую смогло бы поддерживать большинство браузеров. Тем не менее, в некоторых случаях разработчику важно знать источник запроса. Ниже представлен пример получения User-Agent-информации в фильтре действий:

public class BrowserCheckFilter : IActionFilter 
{ 
        public void OnActionExecuting(ActionExecutingContext context) 
        { 
            var userAgent = context.HttpContext.Request.Headers[HeaderNames.UserAgent].ToString().ToLower(); 

            // Detect if a user uses IE 
            if (userAgent.Contains("msie") || userAgent.Contains("trident")) 
            { 
                // Do some actions  
            } 
        } 

        public void OnActionExecuted(ActionExecutedContext context) 
        { 
        } 
} 

Стоит, однако, заметить, что вышеуказанный метод имеет ещё один недостаток. Многие браузеры умеют прятать или подделывать значения, указанные в User-Agent, поэтому данный способ не является однозначно достоверным в определении типа пользовательского браузера.

Другой пример применения фильтров действий — локализация. Создадим фильтр, который в зависимости от указанной культуры будет выводить дату в этой культуре. Ниже представлен код, который задаёт культуру текущего потока:

public class LocalizationActionFilterAttribute: ActionFilterAttribute 
{ 
        public override void OnActionExecuting(ActionExecutingContext filterContext) 
        { 
            var language = (string)filterContext.RouteData.Values["language"] ?? "en"; 
            var culture = (string)filterContext.RouteData.Values["culture"] ?? "GB"; 

            Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo($"{language}-{culture}"); 
            Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo($"{language}-{culture}"); 
        } 
} 

Следующим шагом следует добавить маршрутизацию, которая будет перенаправлять URL с данными культуры на наш контроллер:

                endpoints.MapControllerRoute(name:"localizedRoute", 
                    pattern: "{language}-{culture}/{controller}/{action}/{id}", 
                    defaults: new 
                    { 
                        language = "en", 
                        culture = "GB", 
                        controller = "Date", 
                        action = "Index", 
                        id = "", 
                    });

Код выше создаёт маршрут с именем localizedRoute, у которого в шаблоне имеется параметр, отвечающий за локализацию. Значение по умолчанию для этого параметра — “en-GB”.

Теперь создадим контроллер с именем DateController, который будет обрабатывать наш запрос, и представление, которое будет отображать локализованную дату. Код контроллера просто возвращает представлению текущую дату:

[LocalizationActionFilter] 
public class DateController : Controller 
{ 
        public IActionResult Index() 
        { 
            ViewData["Date"] = DateTime.Now.ToShortDateString(); 
            return View(); 
        } 
}  

После того как пользователь перешёл по ссылке localhost:44338/Date, он увидит в браузере следующее:


На скриншоте выше текущая дата представлена с учётом локализации, заданной по умолчанию, т.е. с en-GB. Теперь, если пользователь перейдёт по ссылке, в которой будет явно указана культура, например, en-US, то он увидит следующее:


Таким образом, на этом примере мы можем увидеть, как сделать простую и быструю локализацию.

Заключение

В заключение следует заметить, что фильтры — это лишь один из множества механизмов, предоставляемых ASP.NET. Поэтому существует большая вероятность, что проблему можно решить другими, более привычными способами, например, через создание сервиса, в который можно вынести повторяющийся код. Большое же преимущество фильтров, как можно видеть выше, в том, что их просто реализовать и использовать. Поэтому о них стоит помнить как о несложных способах, которые можно всегда внедрить в проект, чтобы поддерживать код в чистоте.

Let’s block ads! (Why?)

Read More

Recent Posts

Роскомнадзор рекомендовал хостинг-провайдерам ограничить сбор данных с сайтов для иностранных ботов

Центр управления связью общего пользования (ЦМУ ССОП) Роскомнадзора рекомендовал компаниям из реестра провайдеров ограничить доступ поисковых ботов к информации на российских сайтах.…

5 часов ago

Apple возобновила переговоры с OpenAI и Google для интеграции ИИ в iPhone

Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…

5 дней ago

Российская «дочка» Google подготовила 23 иска к крупнейшим игрокам рекламного рынка

Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…

5 дней ago

Google завершил обновление основного алгоритма March 2024 Core Update

Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…

5 дней ago

Нейросети будут писать тексты объявления за продавцов на Авито

У частных продавцов на Авито появилась возможность составлять текст объявлений с помощью нейросети. Новый функционал доступен в категории «Обувь, одежда,…

6 дней ago

Объявлены победители международной премии Workspace Digital Awards-2024

24 апреля 2024 года в Москве состоялась церемония вручения наград международного конкурса Workspace Digital Awards. В этом году участниками стали…

6 дней ago