{"id":1128,"date":"2025-05-27T14:46:59","date_gmt":"2025-05-27T06:46:59","guid":{"rendered":"https:\/\/www.hyy.net\/?p=1128"},"modified":"2025-05-27T14:46:59","modified_gmt":"2025-05-27T06:46:59","slug":"ultimate-asp-net-core-web-api-15-action-filters","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=1128","title":{"rendered":"Ultimate ASP.NET Core Web API 15 ACTION FILTERS"},"content":{"rendered":"<p>15 ACTION FILTERS<br \/>\n15 \u52a8\u4f5c\u8fc7\u6ee4\u5668<\/p>\n<p>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.\u200c<br \/>\n.NET \u4e2d\u7684\u7b5b\u9009\u5668\u63d0\u4f9b\u4e86\u4e00\u79cd\u4e0e MVC\u4f5c\u8c03\u7528\u7ba1\u9053\u6302\u94a9\u7684\u597d\u65b9\u6cd5\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 filters \u6765\u63d0\u53d6\u53ef\u91cd\u7528\u7684\u4ee3\u7801\uff0c\u5e76\u4f7f\u6211\u4eec\u7684\u4f5c\u66f4\u7b80\u6d01\u3001\u66f4\u6613\u4e8e\u7ef4\u62a4\u3002\u4e00\u4e9b\u8fc7\u6ee4\u5668\u5df2\u7ecf\u7531 .NET \u63d0\u4f9b\uff0c\u4f8b\u5982\u6388\u6743\u8fc7\u6ee4\u5668\uff0c\u8fd8\u6709\u4e00\u4e9b\u6211\u4eec\u53ef\u4ee5\u81ea\u5df1\u521b\u5efa\u7684\u81ea\u5b9a\u4e49\u8fc7\u6ee4\u5668\u3002<\/p>\n<p>There are different filter types:<br \/>\n\u6709\u4e0d\u540c\u7684\u8fc7\u6ee4\u5668\u7c7b\u578b\uff1a<\/p>\n<p>\u2022 Authorization filters \u2013 They run first to determine whether a user is authorized for the current request.<br \/>\n\u6388\u6743\u7b5b\u9009\u6761\u4ef6 \u2013 \u5b83\u4eec\u9996\u5148\u8fd0\u884c\u4ee5\u786e\u5b9a\u7528\u6237\u662f\u5426\u6709\u6743\u5904\u7406\u5f53\u524d\u8bf7\u6c42\u3002<\/p>\n<p>\u2022 Resource filters \u2013 They run right after the authorization filters and are very useful for caching and performance.<br \/>\n\u8d44\u6e90\u7b5b\u9009\u5668 \u2013 \u5b83\u4eec\u5728\u6388\u6743\u7b5b\u9009\u5668\u4e4b\u540e\u8fd0\u884c\uff0c\u5bf9\u4e8e\u7f13\u5b58\u548c\u6027\u80fd\u975e\u5e38\u6709\u7528\u3002<\/p>\n<p>\u2022 Action filters \u2013 They run right before and after action method execution.<br \/>\n\u4f5c\u7b5b\u9009\u5668 \u2013 \u5b83\u4eec\u5728\u4f5c\u65b9\u6cd5\u6267\u884c\u4e4b\u524d\u548c\u4e4b\u540e\u7acb\u5373\u8fd0\u884c\u3002<\/p>\n<p>\u2022 Exception filters \u2013 They are used to handle exceptions before the response body is populated.<br \/>\n\u5f02\u5e38\u7b5b\u9009\u5668 \u2013 \u5b83\u4eec\u7528\u4e8e\u5728\u586b\u5145\u54cd\u5e94\u6b63\u6587\u4e4b\u524d\u5904\u7406\u5f02\u5e38\u3002<\/p>\n<p>\u2022 Result filters \u2013 They run before and after the execution of the action methods result.<br \/>\n\u7ed3\u679c\u7b5b\u9009\u5668 \u2013 \u5b83\u4eec\u5728\u6267\u884c\u4f5c\u65b9\u6cd5\u7ed3\u679c\u4e4b\u524d\u548c\u4e4b\u540e\u8fd0\u884c\u3002<\/p>\n<p>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.<br \/>\n\u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u8ba8\u8bba Action \u8fc7\u6ee4\u5668\u4ee5\u53ca\u5982\u4f55\u4f7f\u7528\u5b83\u4eec\u5728\u6211\u4eec\u7684 Web API \u4e2d\u521b\u5efa\u66f4\u6e05\u6670\u4e14\u53ef\u91cd\u7528\u7684\u4ee3\u7801\u3002<\/p>\n<h2>\u300015.1 Action Filters Implementation<\/h2>\n<p>15.1 \u52a8\u4f5c\u8fc7\u6ee4\u5668\u5b9e\u73b0<\/p>\n<p>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 \u2014 which is the implementation of IActionFilter, IAsyncActionFilter, and a few different interfaces as well:\u200c<br \/>\n\u8981\u521b\u5efa\u4f5c\u7b5b\u9009\u5668\uff0c\u6211\u4eec\u9700\u8981\u521b\u5efa\u4e00\u4e2a\u7ee7\u627f\u81ea IActionFilter \u63a5\u53e3\u3001IAsyncActionFilter \u63a5\u53e3\u6216 ActionFilterAttribute \u7c7b\u7684\u7c7b\uff0c\u8be5\u7c7b\u662f IActionFilter\u3001IAsyncActionFilter \u548c\u4e00\u4e9b\u4e0d\u540c\u63a5\u53e3\u7684\u5b9e\u73b0\uff1a<\/p>\n<pre><code>public abstract class ActionFilterAttribute : Attribute, IActionFilter, IFilterMetadata, IAsyncActionFilter, IResultFilter, IAsyncResultFilter, IOrderedFilter<\/code><\/pre>\n<p>To implement the synchronous Action filter that runs before and after action method execution, we need to implement the OnActionExecuting and OnActionExecuted methods:<br \/>\n\u8981\u5b9e\u73b0\u5728\u4f5c\u65b9\u6cd5\u6267\u884c\u4e4b\u524d\u548c\u4e4b\u540e\u8fd0\u884c\u7684\u540c\u6b65 Action \u8fc7\u6ee4\u5668\uff0c\u6211\u4eec\u9700\u8981\u5b9e\u73b0 OnActionExecuting \u548c OnActionExecuted \u65b9\u6cd5\uff1a<\/p>\n<pre><code>namespace ActionFilters.Filters\n{\n    public class ActionFilterExample : IActionFilter\n    {\n        public void OnActionExecuting(ActionExecutingContext context)\n        {\n            \/\/ our code before action executes\n            \/\/\n        }\n        public void OnActionExecuted(ActionExecutedContext context)\n        {\n            \/\/ our code after action executes\n        }\n    }\n}<\/code><\/pre>\n<p>We can do the same thing with an asynchronous filter by inheriting from IAsyncActionFilter, but we only have one method to implement \u2014 the OnActionExecutionAsync:<br \/>\n\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u4ece IAsyncActionFilter \u7ee7\u627f\u6765\u5bf9\u5f02\u6b65\u7b5b\u9009\u5668\u6267\u884c\u76f8\u540c\u7684\u4f5c\uff0c\u4f46\u6211\u4eec\u53ea\u6709\u4e00\u79cd\u65b9\u6cd5\u8981\u5b9e\u73b0 \u2014 OnActionExecutionAsync\uff1a<\/p>\n<pre><code>namespace ActionFilters.Filters\n{\n    public class AsyncActionFilterExample : IAsyncActionFilter\n    {\n        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)\n        {\n            \/\/ execute any code before the action executes\n            var result = await next();\n            \/\/ execute any code after the action executes\n        }\n    }\n}<\/code><\/pre>\n<h2>15.2 The Scope of Action Filters<\/h2>\n<p>15.2\u4f5c\u7b5b\u9009\u5668\u7684\u8303\u56f4<\/p>\n<p>Like the other types of filters, the action filter can be added to different scope levels: Global, Action, and Controller.\u200c<br \/>\n\u4e0e\u5176\u4ed6\u7c7b\u578b\u7684\u7b5b\u9009\u5668\u4e00\u6837\uff0c\u4f5c\u7b5b\u9009\u5668\u53ef\u4ee5\u6dfb\u52a0\u5230\u4e0d\u540c\u7684\u8303\u56f4\u7ea7\u522b\uff1aGlobal\u3001Action \u548c Controller\u3002<\/p>\n<p>If we want to use our filter globally, we need to register it inside the AddControllers() method in the Program class:<br \/>\n\u5982\u679c\u6211\u4eec\u60f3\u5168\u5c40\u4f7f\u7528\u6211\u4eec\u7684\u8fc7\u6ee4\u5668\uff0c\u6211\u4eec\u9700\u8981\u5728 Program \u7c7b\u7684 AddControllers\uff08\uff09 \u65b9\u6cd5\u4e2d\u6ce8\u518c\u5b83\uff1a<\/p>\n<pre><code>builder.Services.AddControllers(config =&gt; { config.Filters.Add(new GlobalFilterExample()); });<\/code><\/pre>\n<p>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:<br \/>\n\u4f46\u662f\uff0c\u5982\u679c\u6211\u4eec\u60f3\u5c06\u8fc7\u6ee4\u5668\u7528\u4f5c Action \u6216 Controller \u7ea7\u522b\u7684\u670d\u52a1\u7c7b\u578b\uff0c\u5219\u9700\u8981\u5c06\u5176\u6ce8\u518c\uff0c\u4f46\u8981\u4f5c\u4e3a IoC \u5bb9\u5668\u4e2d\u7684\u670d\u52a1\u8fdb\u884c\u6ce8\u518c\uff1a<\/p>\n<pre><code>builder.Services.AddScoped&lt;ActionFilterExample&gt;(); \nbuilder.Services.AddScoped&lt;ControllerFilterExample&gt;();<\/code><\/pre>\n<p>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:<br \/>\n\u6700\u540e\uff0c\u8981\u4f7f\u7528\u5728 Action \u6216 Controller \u7ea7\u522b\u6ce8\u518c\u7684\u8fc7\u6ee4\u5668\uff0c\u6211\u4eec\u9700\u8981\u5c06\u5176\u4f5c\u4e3a ServiceType \u653e\u5728 Controller \u6216 Action \u7684\u9876\u90e8\uff1a<\/p>\n<pre><code>namespace AspNetCore.Controllers\n{\n    [ServiceFilter(typeof(ControllerFilterExample))]\n    [Route(&quot;api\/[controller]&quot;)]\n    [ApiController]\n    public class TestController : ControllerBase\n    {\n        [HttpGet]\n        [ServiceFilter(typeof(ActionFilterExample))]\n        public IEnumerable&lt;string&gt; Get()\n        {\n            return new string[] { &quot;example&quot;, &quot;data&quot; };\n        }\n    }\n}<\/code><\/pre>\n<h2>15.3  Order of Invocation<\/h2>\n<p>15.3 \u8c03\u7528\u987a\u5e8f<\/p>\n<p>The order in which our filters are executed is as follows:\u200c<br \/>\n\u8fc7\u6ee4\u5668\u7684\u6267\u884c\u987a\u5e8f\u5982\u4e0b\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1501.jpg\" alt=\"alt text\" \/><\/p>\n<p>Of course, we can change the order of invocation by adding the Order property to the invocation statement:<br \/>\n\u5f53\u7136\uff0c\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u5c06 Order \u5c5e\u6027\u6dfb\u52a0\u5230\u8c03\u7528\u8bed\u53e5\u6765\u66f4\u6539\u8c03\u7528\u987a\u5e8f\uff1a<\/p>\n<pre><code>namespace AspNetCore.Controllers\n{\n    [ServiceFilter(typeof(ControllerFilterExample), Order = 2)]\n    [Route(&quot;api\/[controller]&quot;)]\n    [ApiController]\n    public class TestController : ControllerBase\n    {\n        [HttpGet]\n        [ServiceFilter(typeof(ActionFilterExample), Order = 1)]\n        public IEnumerable&lt;string&gt; Get()\n        {\n            return new string[] { &quot;example&quot;, &quot;data&quot; };\n        }\n    }\n}<\/code><\/pre>\n<p>Or something like this on top of the same action:<br \/>\n\u6216\u8005\uff0c\u5728\u76f8\u540c\u7684\u4f5c\u4e4b\u4e0a\uff0c\u5982\u4e0b\u6240\u793a\uff1a<\/p>\n<pre><code>[HttpGet]\n[ServiceFilter(typeof(ActionFilterExample), Order = 2)] \n[ServiceFilter(typeof(ActionFilterExample2), Order = 1)] \npublic IEnumerable&lt;string&gt; Get() \n{ \n    return new string[] { &quot;example&quot;, &quot;data&quot; }; \n}<\/code><\/pre>\n<h2>15.4 Improving the Code with Action Filters<\/h2>\n<p>15.4 \u4f7f\u7528 Action Filters \u6539\u8fdb\u4ee3\u7801<\/p>\n<p>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.\u200c<br \/>\n\u7531\u4e8e\u5168\u5c40\u5f02\u5e38\u5904\u7406\u548c\u670d\u52a1\u5c42\u5b9e\u73b0\uff0c\u6211\u4eec\u7684\u4f5c\u5e72\u51c0\u4e14\u53ef\u8bfb\uff0c\u6ca1\u6709 try-catch \u5757\uff0c\u4f46\u6211\u4eec\u53ef\u4ee5\u8fdb\u4e00\u6b65\u6539\u8fdb\u5b83\u4eec\u3002<\/p>\n<p>So, let\u2019s start with the validation code from the POST and PUT actions.<br \/>\n\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u4ece POST \u548c PUT\u4f5c\u4e2d\u7684\u9a8c\u8bc1\u4ee3\u7801\u5f00\u59cb\u3002<\/p>\n<h2>15.5 Validation with Action Filters<\/h2>\n<p>15.5 \u4f7f\u7528\u52a8\u4f5c\u8fc7\u6ee4\u5668\u8fdb\u884c\u9a8c\u8bc1<\/p>\n<p>If we take a look at our POST and PUT actions, we can notice the repeated code in which we validate our Company model:\u200c<br \/>\n\u5982\u679c\u6211\u4eec\u770b\u4e00\u4e0b POST \u548c PUT\u4f5c\uff0c\u6211\u4eec\u4f1a\u6ce8\u610f\u5230\u9a8c\u8bc1 Company \u6a21\u578b\u7684\u91cd\u590d\u4ee3\u7801\uff1a<\/p>\n<pre><code>if (company is null) \n    return BadRequest(&quot;CompanyForUpdateDto object is null&quot;); \nif (!ModelState.IsValid) \n    return UnprocessableEntity(ModelState);<\/code><\/pre>\n<p>We can extract that code into a custom Action Filter class, thus making this code reusable and the action cleaner.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u5c06\u8be5\u4ee3\u7801\u63d0\u53d6\u5230\u81ea\u5b9a\u4e49 Action Filter \u7c7b\u4e2d\uff0c\u4ece\u800c\u4f7f\u6b64\u4ee3\u7801\u53ef\u91cd\u7528\u4e14\u4f5c\u66f4\u7b80\u6d01\u3002<\/p>\n<p>So, let\u2019s do that.<br \/>\n\u6240\u4ee5\uff0c\u8ba9\u6211\u4eec\u5f00\u59cb\u5427\u3002<\/p>\n<p>Let\u2019s 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:<br \/>\n\u8ba9\u6211\u4eec\u5728\u89e3\u51b3\u65b9\u6848\u8d44\u6e90\u7ba1\u7406\u5668CompanyEmployees.Presentation\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u6587\u4ef6\u5939\uff0c\u5e76\u5c06\u5b83 ActionFilters \u7684 API \u4e2d\u3002\u7136\u540e\u5728\u8be5\u6587\u4ef6\u5939\u4e2d\uff0c\u6211\u4eec\u5c06\u521b\u5efa\u4e00\u4e2a\u65b0\u7c7b ValidationFilterAttribute\uff1a<\/p>\n<pre><code>using Microsoft.AspNetCore.Mvc.Filters;\n\nnamespace CompanyEmployees.Presentation.ActionFilters\n{\n    public class ValidationFilterAttribute : IActionFilter\n    {\n        public ValidationFilterAttribute() { }\n        public void OnActionExecuting(ActionExecutingContext context) { }\n        public void OnActionExecuted(ActionExecutedContext context) { }\n    }\n}<\/code><\/pre>\n<p>Now we are going to modify the OnActionExecuting method:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u5c06\u4fee\u6539 OnActionExecuting \u65b9\u6cd5\uff1a<\/p>\n<pre><code>using Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Filters;\n\nnamespace CompanyEmployees.Presentation.ActionFilters\n{\n    public class ValidationFilterAttribute : IActionFilter\n    {\n        public ValidationFilterAttribute() { }\n        \/\/ public void OnActionExecuting(ActionExecutingContext context) { }\n        public void OnActionExecuting(ActionExecutingContext context)\n        {\n            var action = context.RouteData.Values[&quot;action&quot;];\n            var controller = context.RouteData.Values[&quot;controller&quot;];\n\n            var param = context.ActionArguments.SingleOrDefault(x =&gt; x.Value.ToString().Contains(&quot;Dto&quot;)).Value;\n            if (param is null)\n            {\n                context.Result = new BadRequestObjectResult($&quot;Object is null. Controller: {controller}, action: {action}&quot;); \n                return;\n            }\n            if (!context.ModelState.IsValid) \n                context.Result = new UnprocessableEntityObjectResult(context.ModelState);\n        }\n        public void OnActionExecuted(ActionExecutedContext context) { }\n    }\n}<\/code><\/pre>\n<p>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.<br \/>\n\u6211\u4eec\u4f7f\u7528 context \u53c2\u6570\u6765\u68c0\u7d22\u6b64\u65b9\u6cd5\u4e2d\u6240\u9700\u7684\u4e0d\u540c\u503c\u3002\u4f7f\u7528 RouteData.Values \u5b57\u5178\uff0c\u6211\u4eec\u53ef\u4ee5\u83b7\u53d6\u5f53\u524d\u8def\u7531\u8def\u5f84\u4e0a\u7684\u8def\u7531\u751f\u6210\u7684\u503c\u3002\u7531\u4e8e\u6211\u4eec\u9700\u8981\u4f5c\u548c\u63a7\u5236\u5668\u7684\u540d\u79f0\uff0c\u56e0\u6b64\u6211\u4eec\u4ece Values \u5b57\u5178\u4e2d\u63d0\u53d6\u5b83\u4eec\u3002<\/p>\n<p>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.<br \/>\n\u6b64\u5916\uff0c\u6211\u4eec\u4f7f\u7528 ActionArguments \u5b57\u5178\u6765\u63d0\u53d6\u6211\u4eec\u53d1\u9001\u5230 POST \u548c PUT\u4f5c\u7684 DTO \u53c2\u6570\u3002\u5982\u679c\u8be5\u53c2\u6570\u4e3a null\uff0c\u5219\u6211\u4eec\u5c06\u4e0a\u4e0b\u6587\u5bf9\u8c61\u7684 Result \u5c5e\u6027\u8bbe\u7f6e\u4e3a BadRequestObjectReturnResult \u7c7b\u7684\u65b0\u5b9e\u4f8b\u3002\u5982\u679c\u6a21\u578b\u65e0\u6548\uff0c\u6211\u4eec\u5c06\u521b\u5efa UnprocessableEntityObjectResult \u7c7b\u7684\u65b0\u5b9e\u4f8b\u5e76\u4f20\u9012 ModelState\u3002<\/p>\n<p>Next, let\u2019s register this action filter in the Program class above the AddControllers method:<br \/>\n\u63a5\u4e0b\u6765\uff0c\u8ba9\u6211\u4eec\u5728 AddControllers \u65b9\u6cd5\u4e0a\u65b9\u7684 Program \u7c7b\u4e2d\u6ce8\u518c\u6b64\u4f5c\u7b5b\u9009\u5668\uff1a<\/p>\n<pre><code>builder.Services.AddScoped&lt;ValidationFilterAttribute&gt;();<\/code><\/pre>\n<p>Finally, let\u2019s remove the mentioned validation code from our actions and call this action filter as a service.<br \/>\n\u6700\u540e\uff0c\u8ba9\u6211\u4eec\u4ece\u4f5c\u4e2d\u5220\u9664\u63d0\u5230\u7684\u9a8c\u8bc1\u4ee3\u7801\uff0c\u5e76\u5c06\u6b64\u4f5c\u7b5b\u9009\u6761\u4ef6\u4f5c\u4e3a\u670d\u52a1\u8c03\u7528\u3002<\/p>\n<p>POST:<\/p>\n<pre><code>[HttpPost]\n[ServiceFilter(typeof(ValidationFilterAttribute))]\npublic async Task&lt;IActionResult&gt; CreateCompany([FromBody] CompanyForCreationDto company)\n{\n    if (company is null)\n        return BadRequest(&quot;CompanyForCreationDto object is null&quot;);\n\n    if (!ModelState.IsValid)\n        return UnprocessableEntity(ModelState);\n\n    var createdCompany = await _service.CompanyService.CreateCompanyAsync(company);\n\n    return CreatedAtRoute(&quot;CompanyById&quot;, new { id = createdCompany.Id }, createdCompany);\n}<\/code><\/pre>\n<p>PUT:<\/p>\n<pre><code>[HttpPut(&quot;{id:guid}&quot;)]\n[ServiceFilter(typeof(ValidationFilterAttribute))]\npublic async Task&lt;IActionResult&gt; UpdateCompany(Guid id, [FromBody] CompanyForUpdateDto company)\n{\n    if (company is null)\n        return BadRequest(&quot;CompanyForUpdateDto object is null&quot;);\n\n    if (!ModelState.IsValid)\n        return UnprocessableEntity(ModelState);\n\n    await _service.CompanyService.UpdateCompanyAsync(id, company, trackChanges: true);\n\n    return NoContent();\n}<\/code><\/pre>\n<p>Excellent.<br \/>\n\u975e\u5e38\u597d\u3002<\/p>\n<p>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.<br \/>\n\u73b0\u5728\uff0c\u6ca1\u6709\u9a8c\u8bc1\u90e8\u5206\uff0c\u6b64\u4ee3\u7801\u66f4\u52a0\u6e05\u6670\u6613\u8bfb\u3002\u6b64\u5916\uff0c\u9a8c\u8bc1\u90e8\u5206\u73b0\u5728\u53ef\u91cd\u590d\u7528\u4e8e Company \u548c Employee DTO \u5bf9\u8c61\u7684 POST \u548c PUT\u4f5c\u3002<\/p>\n<p>If we send a POST request, for example, with the invalid model we will get the required response:<br \/>\n\u4f8b\u5982\uff0c\u5982\u679c\u6211\u4eec\u53d1\u9001 POST \u8bf7\u6c42\uff0c\u4f7f\u7528\u65e0\u6548\u6a21\u578b\uff0c\u6211\u4eec\u5c06\u83b7\u5f97\u6240\u9700\u7684\u54cd\u5e94\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1502.jpg\" alt=\"alt text\" \/><\/p>\n<p>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:<br \/>\n\u6211\u4eec\u53ef\u4ee5\u50cf\u5728 CompaniesController \u4e2d\u4e00\u6837\uff0c\u5c06\u6b64\u4f5c\u7b5b\u9009\u5668\u5e94\u7528\u4e8e EmployeesController \u4e2d\u7684 POST \u548c PUT\u4f5c\uff0c\u5e76\u5bf9\u5176\u8fdb\u884c\u6d4b\u8bd5\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/53a1237a-3ed3-4462-b9f0-5a7bb1056a33\/employees\">https:\/\/localhost:5001\/api\/companies\/53a1237a-3ed3-4462-b9f0-5a7bb1056a33\/employees<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1503.jpg\" alt=\"alt text\" \/><\/p>\n<h2>\u300015.6 Refactoring the Service Layer<\/h2>\n<p>15.6 \u91cd\u6784\u670d\u52a1\u5c42<\/p>\n<p>Because we are already working on making our code reusable in our actions, we can review our classes from the service layer.\u200c<br \/>\n\u56e0\u4e3a\u6211\u4eec\u5df2\u7ecf\u5728\u52aa\u529b\u4f7f\u6211\u4eec\u7684\u4ee3\u7801\u5728\u6211\u4eec\u7684\u4f5c\u4e2d\u53ef\u91cd\u7528\uff0c\u6240\u4ee5\u6211\u4eec\u53ef\u4ee5\u4ece\u670d\u52a1\u5c42\u67e5\u770b\u6211\u4eec\u7684\u7c7b\u3002<\/p>\n<p>Let\u2019s inspect the CompanyServrice class first.<br \/>\n\u8ba9\u6211\u4eec\u5148\u68c0\u67e5 CompanyServrice \u7c7b\u3002<\/p>\n<p>Inside the class, we can find three methods (GetCompanyAsync, DeleteCompanyAsync, and UpdateCompanyAsync) where we repeat the same code:<br \/>\n\u5728\u8be5\u7c7b\u4e2d\uff0c\u6211\u4eec\u53ef\u4ee5\u627e\u5230\u4e09\u4e2a\u65b9\u6cd5\uff08GetCompanyAsync\u3001DeleteCompanyAsync \u548c UpdateCompanyAsync\uff09\uff0c\u6211\u4eec\u5728\u5176\u4e2d\u91cd\u590d\u76f8\u540c\u7684\u4ee3\u7801\uff1a<\/p>\n<pre><code>var company = await _repository.Company.GetCompanyAsync(id, trackChanges); \nif (company is null) \n    throw new CompanyNotFoundException(id);<\/code><\/pre>\n<p>This is something we can extract in a private method in the same class:<br \/>\n\u8fd9\u662f\u6211\u4eec\u53ef\u4ee5\u5728\u540c\u4e00\u4e2a\u7c7b\u7684\u79c1\u6709\u65b9\u6cd5\u4e2d\u63d0\u53d6\u7684\u5185\u5bb9\uff1a<\/p>\n<pre><code>private async Task&lt;Company&gt; GetCompanyAndCheckIfItExists(Guid id, bool trackChanges) \n{ \n    var company = await _repository.Company.GetCompanyAsync(id, trackChanges); \n    if (company is null) \n        throw new CompanyNotFoundException(id); \n        return company; \n}<\/code><\/pre>\n<p>And then we can modify these methods.<br \/>\n\u7136\u540e\u6211\u4eec\u53ef\u4ee5\u4fee\u6539\u8fd9\u4e9b\u65b9\u6cd5\u3002<\/p>\n<p>GetCompanyAsync:<\/p>\n<pre><code>public async Task&lt;CompanyDto&gt; GetCompanyAsync(Guid id, bool trackChanges)\n{\n    \/\/ var company = await _repository.Company.GetCompanyAsync(id, trackChanges);\n    var company = await GetCompanyAndCheckIfItExists(id, trackChanges);\n    \/\/     if (company is null)\n    \/\/      throw new CompanyNotFoundException(id);\n\n    var companyDto = _mapper.Map&lt;CompanyDto&gt;(company);\n    return companyDto;\n}<\/code><\/pre>\n<p>DeleteCompanyAsync:<\/p>\n<pre><code>public async Task DeleteCompanyAsync(Guid companyId, bool trackChanges)\n{\n    \/\/var company = await _repository.Company.GetCompanyAsync(companyId, trackChanges);\n    var company = await GetCompanyAndCheckIfItExists(companyId, trackChanges);\n    \/\/ if (company is null)\n    \/\/  throw new CompanyNotFoundException(companyId);\n    _repository.Company.DeleteCompany(company);\n    await _repository.SaveAsync();\n}<\/code><\/pre>\n<p>UpdateCompanyAsync:<\/p>\n<pre><code>public async Task UpdateCompanyAsync(Guid companyId,\n    CompanyForUpdateDto companyForUpdate, bool trackChanges)\n{\n    \/\/ var companyEntity = await _repository.Company.GetCompanyAsync(companyId, trackChanges);\n    var company = await GetCompanyAndCheckIfItExists(companyId, trackChanges);\n\n    \/\/ if (companyEntity is null)\n    \/\/      throw new CompanyNotFoundException(companyId);\n    \/\/_mapper.Map(companyForUpdate, companyEntity);\n    _mapper.Map(companyForUpdate, company);\n    await _repository.SaveAsync();\n}<\/code><\/pre>\n<p>Now, this looks much better without code repetition.<br \/>\n\u73b0\u5728\uff0c\u6ca1\u6709\u4ee3\u7801\u91cd\u590d\uff0c\u8fd9\u770b\u8d77\u6765\u8981\u597d\u5f97\u591a\u3002<\/p>\n<p>Furthermore, we can find code repetition in almost all the methods inside the EmployeeService class:<br \/>\n\u6b64\u5916\uff0c\u6211\u4eec\u53ef\u4ee5\u5728 EmployeeService \u7c7b\u4e2d\u7684\u51e0\u4e4e\u6240\u6709\u65b9\u6cd5\u4e2d\u627e\u5230\u4ee3\u7801\u91cd\u590d\uff1a<\/p>\n<pre><code>var company = await _repository.Company.GetCompanyAsync(companyId, trackChanges); \nif (company is null) \n    throw new CompanyNotFoundException(companyId); \n\nvar employeeDb = await _repository.Employee.GetEmployeeAsync(companyId, id, trackChanges); if (employeeDb is null) \n    throw new EmployeeNotFoundException(id); <\/code><\/pre>\n<p>In some methods, we can find just the first check and in several others, we can find both of them.<br \/>\n\u5728\u67d0\u4e9b\u65b9\u6cd5\u4e2d\uff0c\u6211\u4eec\u53ef\u4ee5\u53ea\u627e\u5230\u7b2c\u4e00\u4e2a\u68c0\u67e5\uff0c\u800c\u5728\u5176\u4ed6\u51e0\u79cd\u65b9\u6cd5\u4e2d\uff0c\u6211\u4eec\u53ef\u4ee5\u540c\u65f6\u627e\u5230\u5b83\u4eec\u3002<\/p>\n<p>So, let\u2019s extract these checks into two separate methods:<br \/>\n\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u5c06\u8fd9\u4e9b\u68c0\u67e5\u63d0\u53d6\u4e3a\u4e24\u4e2a\u5355\u72ec\u7684\u65b9\u6cd5\uff1a<\/p>\n<pre><code>private async Task CheckIfCompanyExists(Guid companyId, bool trackChanges)\n{\n    var company = await _repository.Company.GetCompanyAsync(companyId, trackChanges);\n    if (company is null)\n        throw new CompanyNotFoundException(companyId);\n}\nprivate async Task&lt;Employee&gt; GetEmployeeForCompanyAndCheckIfItExists(Guid companyId, Guid id, bool trackChanges)\n{\n    var employeeDb = await _repository.Employee.GetEmployeeAsync(companyId, id, trackChanges);\n    if (employeeDb is null)\n        throw new EmployeeNotFoundException(id);\n    return employeeDb;\n}<\/code><\/pre>\n<p>With these two extracted methods in place, we can refactor all the other methods in the class.<br \/>\n\u6709\u4e86\u8fd9\u4e24\u4e2a\u63d0\u53d6\u7684\u65b9\u6cd5\uff0c\u6211\u4eec\u53ef\u4ee5\u91cd\u6784\u7c7b\u4e2d\u7684\u6240\u6709\u5176\u4ed6\u65b9\u6cd5\u3002<\/p>\n<p>GetEmployeesAsync:<\/p>\n<pre><code>public async Task&lt;IEnumerable&lt;EmployeeDto&gt;&gt; GetEmployeesAsync(Guid companyId, bool trackChanges)\n{\n    \/\/ var company = await _repository.Company.GetCompanyAsync(companyId, trackChanges);\n    await CheckIfCompanyExists(companyId, trackChanges);\n\n    \/\/ if (company is null)\n    \/\/      throw new CompanyNotFoundException(companyId);\n\n    var employeesFromDb = await _repository.Employee.GetEmployeesAsync(companyId, trackChanges);\n    var employeesDto = _mapper.Map&lt;IEnumerable&lt;EmployeeDto&gt;&gt;(employeesFromDb);\n\n    return employeesDto;\n}<\/code><\/pre>\n<p>GetEmployeeAsync:<\/p>\n<pre><code>public async Task&lt;EmployeeDto&gt; GetEmployeeAsync(Guid companyId, Guid id, bool trackChanges)\n{\n    await CheckIfCompanyExists(companyId, trackChanges);\n    var employeeDb = await GetEmployeeForCompanyAndCheckIfItExists(companyId, id, trackChanges);\n    var employee = _mapper.Map&lt;EmployeeDto&gt;(employeeDb);\n    return employee;\n}<\/code><\/pre>\n<p>CreateEmployeeForCompanyAsync:<\/p>\n<pre><code>public async Task&lt;EmployeeDto&gt; CreateEmployeeForCompanyAsync(Guid companyId, EmployeeForCreationDto employeeForCreation, bool trackChanges)\n{\n    await CheckIfCompanyExists(companyId, trackChanges);\n    var employeeEntity = _mapper.Map&lt;Employee&gt;(employeeForCreation);\n    _repository.Employee.CreateEmployeeForCompany(companyId, employeeEntity);\n    await _repository.SaveAsync();\n    var employeeToReturn = _mapper.Map&lt;EmployeeDto&gt;(employeeEntity);\n    return employeeToReturn;\n}<\/code><\/pre>\n<p>DeleteEmployeeForCompanyAsync:<\/p>\n<pre><code>public async Task DeleteEmployeeForCompanyAsync(Guid companyId, Guid id, bool trackChanges)\n{\n    await CheckIfCompanyExists(companyId, trackChanges);\n    var employeeDb = await GetEmployeeForCompanyAndCheckIfItExists(companyId, id, trackChanges);\n    _repository.Employee.DeleteEmployee(employeeDb);\n    await _repository.SaveAsync();\n}<\/code><\/pre>\n<p>UpdateEmployeeForCompanyAsync:<\/p>\n<pre><code>public async Task UpdateEmployeeForCompanyAsync(Guid companyId, Guid id, EmployeeForUpdateDto employeeForUpdate, bool compTrackChanges, bool empTrackChanges)\n{\n    await CheckIfCompanyExists(companyId, compTrackChanges);\n    var employeeDb = await GetEmployeeForCompanyAndCheckIfItExists(companyId, id, empTrackChanges);\n    _mapper.Map(employeeForUpdate, employeeDb);\n    await _repository.SaveAsync();\n}<\/code><\/pre>\n<p>GetEmployeeForPatchAsync:<\/p>\n<pre><code>public async Task&lt;(EmployeeForUpdateDto employeeToPatch, Employee employeeEntity)&gt; GetEmployeeForPatchAsync(Guid companyId, Guid id, bool compTrackChanges, bool empTrackChanges)\n{\n    await CheckIfCompanyExists(companyId, compTrackChanges);\n    var employeeDb = await GetEmployeeForCompanyAndCheckIfItExists(companyId, id, empTrackChanges);\n    var employeeToPatch = _mapper.Map&lt;EmployeeForUpdateDto&gt;(employeeDb);\n    return (employeeToPatch: employeeToPatch, employeeEntity: employeeDb);\n}<\/code><\/pre>\n<p>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\u2019s only one place we need to change.<br \/>\n\u73b0\u5728\uff0c\u6240\u6709\u65b9\u6cd5\u90fd\u66f4\u7b80\u6d01\u3001\u66f4\u6613\u4e8e\u7ef4\u62a4\uff0c\u56e0\u4e3a\u6211\u4eec\u7684\u9a8c\u8bc1\u4ee3\u7801\u4f4d\u4e8e\u4e00\u4e2a\u4f4d\u7f6e\uff0c\u5982\u679c\u6211\u4eec\u9700\u8981\u4fee\u6539\u8fd9\u4e9b\u9a8c\u8bc1\uff0c\u53ea\u9700\u66f4\u6539\u4e00\u4e2a\u4f4d\u7f6e\u3002<\/p>\n<p>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.<br \/>\n\u6b64\u5916\uff0c\u5982\u679c\u9700\u8981\uff0c\u60a8\u53ef\u4ee5\u521b\u5efa\u4e00\u4e2a\u65b0\u7c7b\u5e76\u63d0\u53d6\u8fd9\u4e9b\u65b9\u6cd5\uff0c\u5c06\u8be5\u7c7b\u6ce8\u518c\u4e3a\u670d\u52a1\uff0c\u5c06\u5176\u6ce8\u5165\u6211\u4eec\u7684\u670d\u52a1\u7c7b\u5e76\u4f7f\u7528\u9a8c\u8bc1\u65b9\u6cd5\u3002\u8fd9\u53d6\u51b3\u4e8e\u4f60\u60f3\u600e\u4e48\u505a\u3002<\/p>\n<p>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.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u5df2\u7ecf\u4e86\u89e3\u4e86\u5982\u4f55\u4f7f\u7528 action filters \u6765\u6e05\u9664\u6211\u4eec\u7684 action methods\uff0c\u4ee5\u53ca\u5982\u4f55\u63d0\u53d6\u65b9\u6cd5\u4ee5\u4f7f\u6211\u4eec\u7684\u670d\u52a1\u66f4\u7b80\u6d01\u3001\u66f4\u6613\u4e8e\u7ef4\u62a4\u3002<\/p>\n<p>With that out of the way, we can continue to Paging.<br \/>\n\u6709\u4e86\u8fd9\u4e9b\uff0c\u6211\u4eec\u53ef\u4ee5\u7ee7\u7eed\u8fdb\u884c Paging\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>15 ACTION FILTERS 15 \u52a8\u4f5c\u8fc7\u6ee4\u5668 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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1128","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1128","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1128"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1128\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1128"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1128"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1128"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}