ASP.NET Core Mvc中空返回值的處理方式
原文地址: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, 這說明我們的代碼又生效了。
ASP.NET Core Mvc中空返回值的處理方式