15 ACTION FILTERS
15 动作过滤器
Filters in .NET offer a great way to hook into the MVC action invocation pipeline. Therefore, we can use filters to extract code that can be reused and make our actions cleaner and maintainable. Some filters are already provided by .NET like the authorization filter, and there are the custom ones that we can create ourselves.
.NET 中的筛选器提供了一种与 MVC作调用管道挂钩的好方法。因此,我们可以使用 filters 来提取可重用的代码,并使我们的作更简洁、更易于维护。一些过滤器已经由 .NET 提供,例如授权过滤器,还有一些我们可以自己创建的自定义过滤器。
There are different filter types:
有不同的过滤器类型:
• Authorization filters – They run first to determine whether a user is authorized for the current request.
授权筛选条件 – 它们首先运行以确定用户是否有权处理当前请求。
• Resource filters – They run right after the authorization filters and are very useful for caching and performance.
资源筛选器 – 它们在授权筛选器之后运行,对于缓存和性能非常有用。
• Action filters – They run right before and after action method execution.
作筛选器 – 它们在作方法执行之前和之后立即运行。
• Exception filters – They are used to handle exceptions before the response body is populated.
异常筛选器 – 它们用于在填充响应正文之前处理异常。
• Result filters – They run before and after the execution of the action methods result.
结果筛选器 – 它们在执行作方法结果之前和之后运行。
In this chapter, we are going to talk about Action filters and how to use them to create a cleaner and reusable code in our Web API.
在本章中,我们将讨论 Action 过滤器以及如何使用它们在我们的 Web API 中创建更清晰且可重用的代码。
15.1 Action Filters Implementation
15.1 动作过滤器实现
To create an Action filter, we need to create a class that inherits either from the IActionFilter interface, the IAsyncActionFilter interface, or the ActionFilterAttribute class — which is the implementation of IActionFilter, IAsyncActionFilter, and a few different interfaces as well:
要创建作筛选器,我们需要创建一个继承自 IActionFilter 接口、IAsyncActionFilter 接口或 ActionFilterAttribute 类的类,该类是 IActionFilter、IAsyncActionFilter 和一些不同接口的实现:
public abstract class ActionFilterAttribute : Attribute, IActionFilter, IFilterMetadata, IAsyncActionFilter, IResultFilter, IAsyncResultFilter, IOrderedFilter
To implement the synchronous Action filter that runs before and after action method execution, we need to implement the OnActionExecuting and OnActionExecuted methods:
要实现在作方法执行之前和之后运行的同步 Action 过滤器,我们需要实现 OnActionExecuting 和 OnActionExecuted 方法:
namespace ActionFilters.Filters
{
public class ActionFilterExample : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// our code before action executes
//
}
public void OnActionExecuted(ActionExecutedContext context)
{
// our code after action executes
}
}
}
We can do the same thing with an asynchronous filter by inheriting from IAsyncActionFilter, but we only have one method to implement — the OnActionExecutionAsync:
我们可以通过从 IAsyncActionFilter 继承来对异步筛选器执行相同的作,但我们只有一种方法要实现 — OnActionExecutionAsync:
namespace ActionFilters.Filters
{
public class AsyncActionFilterExample : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// execute any code before the action executes
var result = await next();
// execute any code after the action executes
}
}
}
15.2 The Scope of Action Filters
15.2作筛选器的范围
Like the other types of filters, the action filter can be added to different scope levels: Global, Action, and Controller.
与其他类型的筛选器一样,作筛选器可以添加到不同的范围级别:Global、Action 和 Controller。
If we want to use our filter globally, we need to register it inside the AddControllers() method in the Program class:
如果我们想全局使用我们的过滤器,我们需要在 Program 类的 AddControllers() 方法中注册它:
builder.Services.AddControllers(config => { config.Filters.Add(new GlobalFilterExample()); });
But if we want to use our filter as a service type on the Action or Controller level, we need to register it, but as a service in the IoC container:
但是,如果我们想将过滤器用作 Action 或 Controller 级别的服务类型,则需要将其注册,但要作为 IoC 容器中的服务进行注册:
builder.Services.AddScoped<ActionFilterExample>();
builder.Services.AddScoped<ControllerFilterExample>();
Finally, to use a filter registered on the Action or Controller level, we need to place it on top of the Controller or Action as a ServiceType:
最后,要使用在 Action 或 Controller 级别注册的过滤器,我们需要将其作为 ServiceType 放在 Controller 或 Action 的顶部:
namespace AspNetCore.Controllers
{
[ServiceFilter(typeof(ControllerFilterExample))]
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet]
[ServiceFilter(typeof(ActionFilterExample))]
public IEnumerable<string> Get()
{
return new string[] { "example", "data" };
}
}
}
15.3 Order of Invocation
15.3 调用顺序
The order in which our filters are executed is as follows:
过滤器的执行顺序如下:
Of course, we can change the order of invocation by adding the Order property to the invocation statement:
当然,我们可以通过将 Order 属性添加到调用语句来更改调用顺序:
namespace AspNetCore.Controllers
{
[ServiceFilter(typeof(ControllerFilterExample), Order = 2)]
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet]
[ServiceFilter(typeof(ActionFilterExample), Order = 1)]
public IEnumerable<string> Get()
{
return new string[] { "example", "data" };
}
}
}
Or something like this on top of the same action:
或者,在相同的作之上,如下所示:
[HttpGet]
[ServiceFilter(typeof(ActionFilterExample), Order = 2)]
[ServiceFilter(typeof(ActionFilterExample2), Order = 1)]
public IEnumerable<string> Get()
{
return new string[] { "example", "data" };
}
15.4 Improving the Code with Action Filters
15.4 使用 Action Filters 改进代码
Our actions are clean and readable without try-catch blocks due to global exception handling and a service layer implementation, but we can improve them even further.
由于全局异常处理和服务层实现,我们的作干净且可读,没有 try-catch 块,但我们可以进一步改进它们。
So, let’s start with the validation code from the POST and PUT actions.
因此,让我们从 POST 和 PUT作中的验证代码开始。
15.5 Validation with Action Filters
15.5 使用动作过滤器进行验证
If we take a look at our POST and PUT actions, we can notice the repeated code in which we validate our Company model:
如果我们看一下 POST 和 PUT作,我们会注意到验证 Company 模型的重复代码:
if (company is null)
return BadRequest("CompanyForUpdateDto object is null");
if (!ModelState.IsValid)
return UnprocessableEntity(ModelState);
We can extract that code into a custom Action Filter class, thus making this code reusable and the action cleaner.
我们可以将该代码提取到自定义 Action Filter 类中,从而使此代码可重用且作更简洁。
So, let’s do that.
所以,让我们开始吧。
Let’s create a new folder in our solution explorer, and name it ActionFilters. Then inside that folder, we are going to create a new class ValidationFilterAttribute:
让我们在解决方案资源管理器CompanyEmployees.Presentation中创建一个新文件夹,并将它 ActionFilters 的 API 中。然后在该文件夹中,我们将创建一个新类 ValidationFilterAttribute:
using Microsoft.AspNetCore.Mvc.Filters;
namespace CompanyEmployees.Presentation.ActionFilters
{
public class ValidationFilterAttribute : IActionFilter
{
public ValidationFilterAttribute() { }
public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuted(ActionExecutedContext context) { }
}
}
Now we are going to modify the OnActionExecuting method:
现在,我们将修改 OnActionExecuting 方法:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace CompanyEmployees.Presentation.ActionFilters
{
public class ValidationFilterAttribute : IActionFilter
{
public ValidationFilterAttribute() { }
// public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuting(ActionExecutingContext context)
{
var action = context.RouteData.Values["action"];
var controller = context.RouteData.Values["controller"];
var param = context.ActionArguments.SingleOrDefault(x => x.Value.ToString().Contains("Dto")).Value;
if (param is null)
{
context.Result = new BadRequestObjectResult($"Object is null. Controller: {controller}, action: {action}");
return;
}
if (!context.ModelState.IsValid)
context.Result = new UnprocessableEntityObjectResult(context.ModelState);
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
}
We are using the context parameter to retrieve different values that we need inside this method. With the RouteData.Values dictionary, we can get the values produced by routes on the current routing path. Since we need the name of the action and the controller, we extract them from the Values dictionary.
我们使用 context 参数来检索此方法中所需的不同值。使用 RouteData.Values 字典,我们可以获取当前路由路径上的路由生成的值。由于我们需要作和控制器的名称,因此我们从 Values 字典中提取它们。
Additionally, we use the ActionArguments dictionary to extract the DTO parameter that we send to the POST and PUT actions. If that parameter is null, we set the Result property of the context object to a new instance of the BadRequestObjectReturnResult class. If the model is invalid, we create a new instance of the UnprocessableEntityObjectResult class and pass ModelState.
此外,我们使用 ActionArguments 字典来提取我们发送到 POST 和 PUT作的 DTO 参数。如果该参数为 null,则我们将上下文对象的 Result 属性设置为 BadRequestObjectReturnResult 类的新实例。如果模型无效,我们将创建 UnprocessableEntityObjectResult 类的新实例并传递 ModelState。
Next, let’s register this action filter in the Program class above the AddControllers method:
接下来,让我们在 AddControllers 方法上方的 Program 类中注册此作筛选器:
builder.Services.AddScoped<ValidationFilterAttribute>();
Finally, let’s remove the mentioned validation code from our actions and call this action filter as a service.
最后,让我们从作中删除提到的验证代码,并将此作筛选条件作为服务调用。
POST:
[HttpPost]
[ServiceFilter(typeof(ValidationFilterAttribute))]
public async Task<IActionResult> CreateCompany([FromBody] CompanyForCreationDto company)
{
if (company is null)
return BadRequest("CompanyForCreationDto object is null");
if (!ModelState.IsValid)
return UnprocessableEntity(ModelState);
var createdCompany = await _service.CompanyService.CreateCompanyAsync(company);
return CreatedAtRoute("CompanyById", new { id = createdCompany.Id }, createdCompany);
}
PUT:
[HttpPut("{id:guid}")]
[ServiceFilter(typeof(ValidationFilterAttribute))]
public async Task<IActionResult> UpdateCompany(Guid id, [FromBody] CompanyForUpdateDto company)
{
if (company is null)
return BadRequest("CompanyForUpdateDto object is null");
if (!ModelState.IsValid)
return UnprocessableEntity(ModelState);
await _service.CompanyService.UpdateCompanyAsync(id, company, trackChanges: true);
return NoContent();
}
Excellent.
非常好。
This code is much cleaner and more readable now without the validation part. Furthermore, the validation part is now reusable for the POST and PUT actions for both the Company and Employee DTO objects.
现在,没有验证部分,此代码更加清晰易读。此外,验证部分现在可重复用于 Company 和 Employee DTO 对象的 POST 和 PUT作。
If we send a POST request, for example, with the invalid model we will get the required response:
例如,如果我们发送 POST 请求,使用无效模型,我们将获得所需的响应:
https://localhost:5001/api/companies
We can apply this action filter to the POST and PUT actions in the EmployeesController the same way we did in the CompaniesController and test it as well:
我们可以像在 CompaniesController 中一样,将此作筛选器应用于 EmployeesController 中的 POST 和 PUT作,并对其进行测试:
https://localhost:5001/api/companies/53a1237a-3ed3-4462-b9f0-5a7bb1056a33/employees
15.6 Refactoring the Service Layer
15.6 重构服务层
Because we are already working on making our code reusable in our actions, we can review our classes from the service layer.
因为我们已经在努力使我们的代码在我们的作中可重用,所以我们可以从服务层查看我们的类。
Let’s inspect the CompanyServrice class first.
让我们先检查 CompanyServrice 类。
Inside the class, we can find three methods (GetCompanyAsync, DeleteCompanyAsync, and UpdateCompanyAsync) where we repeat the same code:
在该类中,我们可以找到三个方法(GetCompanyAsync、DeleteCompanyAsync 和 UpdateCompanyAsync),我们在其中重复相同的代码:
var company = await _repository.Company.GetCompanyAsync(id, trackChanges);
if (company is null)
throw new CompanyNotFoundException(id);
This is something we can extract in a private method in the same class:
这是我们可以在同一个类的私有方法中提取的内容:
private async Task<Company> GetCompanyAndCheckIfItExists(Guid id, bool trackChanges)
{
var company = await _repository.Company.GetCompanyAsync(id, trackChanges);
if (company is null)
throw new CompanyNotFoundException(id);
return company;
}
And then we can modify these methods.
然后我们可以修改这些方法。
GetCompanyAsync:
public async Task<CompanyDto> GetCompanyAsync(Guid id, bool trackChanges)
{
// var company = await _repository.Company.GetCompanyAsync(id, trackChanges);
var company = await GetCompanyAndCheckIfItExists(id, trackChanges);
// if (company is null)
// throw new CompanyNotFoundException(id);
var companyDto = _mapper.Map<CompanyDto>(company);
return companyDto;
}
DeleteCompanyAsync:
public async Task DeleteCompanyAsync(Guid companyId, bool trackChanges)
{
//var company = await _repository.Company.GetCompanyAsync(companyId, trackChanges);
var company = await GetCompanyAndCheckIfItExists(companyId, trackChanges);
// if (company is null)
// throw new CompanyNotFoundException(companyId);
_repository.Company.DeleteCompany(company);
await _repository.SaveAsync();
}
UpdateCompanyAsync:
public async Task UpdateCompanyAsync(Guid companyId,
CompanyForUpdateDto companyForUpdate, bool trackChanges)
{
// var companyEntity = await _repository.Company.GetCompanyAsync(companyId, trackChanges);
var company = await GetCompanyAndCheckIfItExists(companyId, trackChanges);
// if (companyEntity is null)
// throw new CompanyNotFoundException(companyId);
//_mapper.Map(companyForUpdate, companyEntity);
_mapper.Map(companyForUpdate, company);
await _repository.SaveAsync();
}
Now, this looks much better without code repetition.
现在,没有代码重复,这看起来要好得多。
Furthermore, we can find code repetition in almost all the methods inside the EmployeeService class:
此外,我们可以在 EmployeeService 类中的几乎所有方法中找到代码重复:
var company = await _repository.Company.GetCompanyAsync(companyId, trackChanges);
if (company is null)
throw new CompanyNotFoundException(companyId);
var employeeDb = await _repository.Employee.GetEmployeeAsync(companyId, id, trackChanges); if (employeeDb is null)
throw new EmployeeNotFoundException(id);
In some methods, we can find just the first check and in several others, we can find both of them.
在某些方法中,我们可以只找到第一个检查,而在其他几种方法中,我们可以同时找到它们。
So, let’s extract these checks into two separate methods:
因此,让我们将这些检查提取为两个单独的方法:
private async Task CheckIfCompanyExists(Guid companyId, bool trackChanges)
{
var company = await _repository.Company.GetCompanyAsync(companyId, trackChanges);
if (company is null)
throw new CompanyNotFoundException(companyId);
}
private async Task<Employee> GetEmployeeForCompanyAndCheckIfItExists(Guid companyId, Guid id, bool trackChanges)
{
var employeeDb = await _repository.Employee.GetEmployeeAsync(companyId, id, trackChanges);
if (employeeDb is null)
throw new EmployeeNotFoundException(id);
return employeeDb;
}
With these two extracted methods in place, we can refactor all the other methods in the class.
有了这两个提取的方法,我们可以重构类中的所有其他方法。
GetEmployeesAsync:
public async Task<IEnumerable<EmployeeDto>> GetEmployeesAsync(Guid companyId, bool trackChanges)
{
// var company = await _repository.Company.GetCompanyAsync(companyId, trackChanges);
await CheckIfCompanyExists(companyId, trackChanges);
// if (company is null)
// throw new CompanyNotFoundException(companyId);
var employeesFromDb = await _repository.Employee.GetEmployeesAsync(companyId, trackChanges);
var employeesDto = _mapper.Map<IEnumerable<EmployeeDto>>(employeesFromDb);
return employeesDto;
}
GetEmployeeAsync:
public async Task<EmployeeDto> GetEmployeeAsync(Guid companyId, Guid id, bool trackChanges)
{
await CheckIfCompanyExists(companyId, trackChanges);
var employeeDb = await GetEmployeeForCompanyAndCheckIfItExists(companyId, id, trackChanges);
var employee = _mapper.Map<EmployeeDto>(employeeDb);
return employee;
}
CreateEmployeeForCompanyAsync:
public async Task<EmployeeDto> CreateEmployeeForCompanyAsync(Guid companyId, EmployeeForCreationDto employeeForCreation, bool trackChanges)
{
await CheckIfCompanyExists(companyId, trackChanges);
var employeeEntity = _mapper.Map<Employee>(employeeForCreation);
_repository.Employee.CreateEmployeeForCompany(companyId, employeeEntity);
await _repository.SaveAsync();
var employeeToReturn = _mapper.Map<EmployeeDto>(employeeEntity);
return employeeToReturn;
}
DeleteEmployeeForCompanyAsync:
public async Task DeleteEmployeeForCompanyAsync(Guid companyId, Guid id, bool trackChanges)
{
await CheckIfCompanyExists(companyId, trackChanges);
var employeeDb = await GetEmployeeForCompanyAndCheckIfItExists(companyId, id, trackChanges);
_repository.Employee.DeleteEmployee(employeeDb);
await _repository.SaveAsync();
}
UpdateEmployeeForCompanyAsync:
public async Task UpdateEmployeeForCompanyAsync(Guid companyId, Guid id, EmployeeForUpdateDto employeeForUpdate, bool compTrackChanges, bool empTrackChanges)
{
await CheckIfCompanyExists(companyId, compTrackChanges);
var employeeDb = await GetEmployeeForCompanyAndCheckIfItExists(companyId, id, empTrackChanges);
_mapper.Map(employeeForUpdate, employeeDb);
await _repository.SaveAsync();
}
GetEmployeeForPatchAsync:
public async Task<(EmployeeForUpdateDto employeeToPatch, Employee employeeEntity)> GetEmployeeForPatchAsync(Guid companyId, Guid id, bool compTrackChanges, bool empTrackChanges)
{
await CheckIfCompanyExists(companyId, compTrackChanges);
var employeeDb = await GetEmployeeForCompanyAndCheckIfItExists(companyId, id, empTrackChanges);
var employeeToPatch = _mapper.Map<EmployeeForUpdateDto>(employeeDb);
return (employeeToPatch: employeeToPatch, employeeEntity: employeeDb);
}
Now, all of the methods are cleaner and easier to maintain since our validation code is in a single place, and if we need to modify these validations, there’s only one place we need to change.
现在,所有方法都更简洁、更易于维护,因为我们的验证代码位于一个位置,如果我们需要修改这些验证,只需更改一个位置。
Additionally, if you want you can create a new class and extract these methods, register that class as a service, inject it into our service classes and use the validation methods. It is up to you how you want to do it.
此外,如果需要,您可以创建一个新类并提取这些方法,将该类注册为服务,将其注入我们的服务类并使用验证方法。这取决于你想怎么做。
So, we have seen how to use action filters to clear our action methods and also how to extract methods to make our service cleaner and easier to maintain.
因此,我们已经了解了如何使用 action filters 来清除我们的 action methods,以及如何提取方法以使我们的服务更简洁、更易于维护。
With that out of the way, we can continue to Paging.
有了这些,我们可以继续进行 Paging。