1. 程式人生 > >ASP.NET Core Mvc中空返回值的處理方式

ASP.NET Core Mvc中空返回值的處理方式

將他 not 返回值 postman 行為 期望 html onf net

原文地址: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返回值是voidTask時,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個特性類NotFoundActionFilterAttributeNotFoundResultFilterAttribute , 代碼如下:

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();
        }
    }
}

代碼解釋

  • 這裏使用了ActionFilterAttributeResultFilterAttribute,ActionFilterAttribute中的OnActionExecuted方法會在action執行完後觸發, ResultFilterAttributeOnResultExecuting會在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中空返回值的處理方式