原文地址:https://www.strathweb.com/2018/10/convert-null-valued-results-to-404-in-asp-net-core-mvc/
作者: Filip W.
译者: Lamond Lu
.NET Core MVC在如何返回操作结果方面非常灵活的。
你可以返回一个实现IActionResult
接口的对象, 比如我们熟知的ViewResult, FileResult, ContentResult等。
[HttpGet] public IActionResult SayGood() { return Content("Good!"); }
当然你还可以直接返回一个类的实例。
[HttpGet] public string HelloWorld() { return "Hello World"; }
在.NET Core 2.1中, 你还可以返回一个ActionResult
的泛型对象。
[HttpGet] public ActionResult<IEnumerable<string>> Get() { return new string[] { "value1", "value2" }; }
今天的博客中,我们将一起看一下.NET Core Mvc是如何返回一个空值对象的,以及如何改变.NET Core Mvc针对空值对象结果的默认行为。
.NET Core Mvc针对空值对象的默认处理行为
那么当我们在Action中返回null时, 结果是什么样的呢?
下面我们新建一个ASP.NET Core WebApi项目,并添加一个BookController
, 其代码如下:
[Route("api/[controller]")] [ApiController] public class BookController : ControllerBase { private readonly List<Book> _books = new List<Book> { new Book(1, "CLR via C#"), new Book(2, ".NET Core Programming") }; [HttpGet("{id}")] public IActionResult GetById(int id) { var item = _books.FirstOrDefault(p => p.BookId == id); return Ok(item); } //[HttpGet("{id}")] //public ActionResult<Book> GetById(int id) //{ // var book = _books.FirstOrDefault(p => p.BookId == id); // return book; //} //[HttpGet("{id}")] //public Book GetById(int id) //{ // var book = _books.FirstOrDefault(p => p.BookId == id); // return book; //} } public class Book { public Book(int bookId, string bookName) { BookId = bookId; BookName = bookName; } public int BookId { get; set; } public string BookName { get; set; } }
在这个Controller中,我们定义了一个图书的集合,并提供了根据图书ID查询图书的三种实现方式。
然后我们启动项目, 并使用Postman, 并请求/api/book/3, 结果如下:
你会发现返回的Status是204 NoContent, 而不是我们想象中的200 OK。你可修改之前代码的注释, 使用其他2种方式,结果也是一样的。
你可以尝试创建一个普通的ASP.NET Mvc项目, 添加相似的代码,结果如下
返回的结果是200 OK, 内容是null
为什么会出现结果呢?
与前辈们(ASP.NET Mvc, ASP.NET WebApi)不同,ASP.NET Core Mvc非常巧妙的处理了null值,在以上的例子中,ASP.NET Core Mvc会选择一个合适的输出格式化器(output formatter)来输出响应内容。通常这个输出格式化器会是一个JSON格式化器或XML格式化器。
但是对于null值, ASP.NET Core Mvc使用了一种特殊的格式化器HttpNoContentOutputFormatter
, 它会将null值转换成204的状态码。这意味着null值不会被序列化成JSON或XML, 这可能不是我们期望的结果, 有时候我们希望返回200 OK, 响应内容为null。
Tips: 当Action返回值是
void
或Task
时,ASP.NET Core Mvc默认也会使用HttpNoContentOutputFormatter
通过修改配置移除默认的null值格式化器
我们可以通过设置HttpNoContentOutputFormatter
对象的TreatNullValueAsNoContent
属性为false,去除默认的HttpNoContentOutputFormatter对null值的格式化。
在Startup.cs文件的ConfigureService
方法中, 我们在添加Mvc服务的地方,修改默认的输出格式化器,代码如下
public void ConfigureServices(IServiceCollection services) { services.AddMvc(o => { o.OutputFormatters.RemoveType(typeof(HttpNoContentOutputFormatter)); o.OutputFormatters.Insert(0, new HttpNoContentOutputFormatter { TreatNullValueAsNoContent = false; }); }); }
修改之后我们重新运行程序,并使用Postman访问/api/book/3
结果如下, 返回值200 OK, 内容为null, 这说明我们的修改成功了。
使用404 Not Found代替204 No Content
在上面的例子中, 我们禁用了204 No Content行为,响应结果变为了200 OK, 内容为null。 但是有时候,我们期望当找不到任何结果时,返回404 Not Found , 那么这时候我们应该修改代码,进行扩展呢?
在.NET Core Mvc中我们可以使用自定义过滤器(Custom Filter), 来改变这一行为。
这里我们创建2个特性类NotFoundActionFilterAttribute
和NotFoundResultFilterAttribute
, 代码如下:
public class NotFoundActionFilterAttribute : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext context) { if (context.Result is ObjectResult objectResult && objectResult.Value == null) { context.Result = new NotFoundResult(); } } } public class NotFoundResultFilterAttribute : ResultFilterAttribute { public override void OnResultExecuting(ResultExecutingContext context) { if (context.Result is ObjectResult objectResult && objectResult.Value == null) { context.Result = new NotFoundResult(); } } }
代码解释
- 这里使用了
ActionFilterAttribute
和ResultFilterAttribute
,ActionFilterAttribute
中的OnActionExecuted
方法会在action执行完后触发,?ResultFilterAttribute
的OnResultExecuting
会在action返回结果前触发。 - 这2个方法都是针对action的返回结果进行了替换操作,如果返回结果的值是null, 就将其替换成
NotFoundResult
添加完成后,你可以任选一个类,将他们添加在
controller头部
[Route("api/[controller]")] [ApiController] [NotFoundResultFilter] public class BookController : ControllerBase { ... }
或者action头部
[HttpGet("{id}")] [NotFoundResultFilter] public IActionResult GetById(int id) { var item = _books.FirstOrDefault(p => p.BookId == id); return Ok(item); }
你还可以在添加Mvc服务的时候配置他们
public void ConfigureServices(IServiceCollection services) { services.AddMvc(o => { o.Filters.Add(new NotFoundResultFilterAttribute()); }); }
选择一种重新运行项目之后,效果和通过修改配置移除默认的null值格式化器是一样的。
IAlwaysRunResultFilter
以上的几种解决方案看似完美无缺,但实际上还是存在一点瑕疵。由于ASP.NET Core Mvc中过滤器的短路机制(即在任何一个过滤器中对Result赋值都会导致程序跳过管道中剩余的过滤器),可能现在使用某些第三方组件后, 第三方组件在管道中插入自己的短路过滤器,从而导致我们的代码失效。
ASP.NET Core Mvc的过滤器,可以参见这篇文章
下面我们添加以下的短路过滤器。
public class ShortCircuitingFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { context.Result = new ObjectResult(null); } }
然后修改BookController中GetById的方法
[HttpGet("{id}")] [ShortCircuitingFilter] [NotFoundActionFilter] public IActionResult GetById(int id) { var item = _books.FirstOrDefault(p => p.BookId == id); return Ok(item); }
重新运行程序后,使用Postman访问/api/book/3, 程序又返回了204 Not Content, 这说明我们的代码失效了。
这时候,为了解决这个问题,我们需要使用.NET Core 2.1中新引入的接口IAlwaysRunResultFilter
。实现IAlwaysRunResultFilter
接口的过滤器总是会执行,不论前面的过滤器是否触发短路。
这里我们添加一个新的过滤器NotFoundAlwaysRunFilterAttribute
。
public class NotFoundAlwaysRunFilterAttribute : Attribute, IAlwaysRunResultFilter { public void OnResultExecuted(ResultExecutedContext context) { } public void OnResultExecuting(ResultExecutingContext context) { if (context.Result is ObjectResult objectResult && objectResult.Value == null) { context.Result = new NotFoundResult(); } } }
然后我们继续修改BookController中的GetById方法, 为其添加NotFoundAlwaysRunFilter
特性
[HttpGet("{id}")] [ShortCircuitingFilter] [NotFoundActionFilter] [NotFoundAlwaysRunFilter] public IActionResult GetById(int id) { var item = _books.FirstOrDefault(p => p.BookId == id); return Ok(item); }
重新运行程序后,使用Postman访问/api/book/3, 程序又成功返回了404 Not Found, 这说明我们的代码又生效了。