Controller控制器

前面章节例子代码中我们已经写过一些简单的控制器了,这篇笔记我们继续学习ASP.NET Core中的控制器。本篇笔记主要介绍WebAPI控制器的编写和HTTP请求响应的基本处理方法,有关MVC开发模式中控制器的其它用法将在MVC章节专门介绍。

定义WebAPI控制器类

在实际开发中,对于WebAPI项目和MVC项目定义控制器的写法有一些区别。WebAPI的控制器通常继承ControllerBase并标注[ApiController]注解,而MVC的控制器通常继承Controller类。下面例子我们定义了一个典型的用于WebAPI项目的控制器。

using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;

namespace DemoWebAPI.Controllers;

[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
    [HttpGet]
    [Route("DemoAction")]
    public ActionResult<ApiResult> DemoAction()
    {
        return ApiResult.Success();
    }
}

例子中我们定义了DemoController类,它标注了[ApiController]注解,并继承了ControllerBase抽象类。控制器方法中,返回了一个ActionResult<ApiResult>类型的对象。

[ApiController]注解是ASP.NET Core中提供的专用于WebAPI的注解,它赋予控制器类一些额外行为使得控制器的表现更符合WebAPI环境下的预期。总而言之,对于WebAPI工程中的Controller我们应该添加这个注解。

此外,Controller定义中,我们继承了ControllerBase这个类,这个基类提供了很多基础功能,控制器类通常应该直接或间接的继承该类。不继承任何类的控制器也是可以正常运行的的,但它会缺少很多方便的功能,我们一般不这样写。

控制器类和控制器方法上的[Route("")]注解用于标注路由匹配路径,[HttpGet]注解用于标注该控制器方法用于响应GET请求,控制器方法上它们也可以合并简写为[HttpGet("")]的形式,前一篇笔记中我们已经介绍过了这部分内容。

控制器方法也被称为“Action”,这里我们定义了一个名为DemoAction()的控制器方法,它返回ActionResult<ApiResult>类型的对象。实际上,控制器方法可以返回任意对象,ASP.NET Core会自动将其序列化为JSON。但实际开发中,我们推荐控制器返回ActionResult类型,ActionResult有很多常用的派生类,比如StatusCodeResultJsonResultFileStreamResult等,我们能很方便的基于这些类构建我们需要的响应方式。

ApiResult是我们自定义的一个类,其中包含了一些通用的JSON返回信息,下面是ApiResult类内部的具体实现供参考。

namespace DemoWebAPI.Model;

public class ApiResult
{
    public string Code { get; set; }
    public string? Message { get; set; }
    public object? Data { get; set; }

    public static ApiResult Success()
    {
        return new ApiResult() { Code = "0", Message = "ok", Data = null };
    }

    // ...其它方法
}

这里还要注意的是,我们可以发现Controller方法声明的返回值是ActionResult<ApiResult>,但我们实际方法体内的返回值是ApiResult,这是因为ActionResult<>重载了隐式转换操作符,让ActionResult<>的泛型类型能隐式转换为ActionResult<>。操作符重载是个很有争议的特性,它能让代码变得简洁同时也让代码变得让人费解,不过这里ASP.NET Core就是这么设计的,我们了解其用法即可。

Request和Response对象

RequestResponseControllerBase基类中的两个属性,分别代表HTTP请求和HTTP响应,前者包含了请求中的所有数据,后者则可以用于设置各种响应信息。不过我们实际开发中一般不会直接操作这两个属性,请求数据我们一般声明在控制器方法上并由ASP.NET Core框架从参数中注入,响应则使用各种ActionResult的派生类来实现,如果这些满足不了我们的需求,才会考虑使用RequestResponse属性。

下面例子代码中,我们直接操作ControllerBase基类提供的Request属性从请求信息中读取所有的请求头和QueryString。

IHeaderDictionary headers = Request.Headers;
foreach (var header in headers)
{
    Console.WriteLine($"{header.Key} {header.Value}");
}
QueryString queryString = Request.QueryString;
Console.WriteLine(queryString.Value);

处理HTTP请求

读取GET参数

读取GET参数非常简单,我们直接在控制器方法中指定参数即可,下面例子我们在方法中指定id参数,此时会读取URL中的同名GET参数。例如我们请求/api/Demo/DemoAction?id=3,此时控制器方法会读取到id参数为3

using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;

namespace DemoWebAPI.Controllers;

[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
    [HttpGet]
    [Route("DemoAction")]
    public ActionResult<ApiResult> DemoAction(int id)
    {
        return ApiResult.Success();
    }
}

如果我们的请求参数名和方法参数名不同,我们可以使用[FromQuery]注解显式指定GET参数名,下面例子中,我们可以请求/api/Demo/DemoAction?uid=3,此时控制器方法会读取到id参数为3

public ActionResult<ApiResult> DemoAction([FromQuery(Name = "uid")] int id) {}

如果我们希望参数不存在时读取一个默认值,我们直接指定方法默认值即可。

public ActionResult<ApiResult> DemoAction(int id = 1) {}

如果我们希望参数是可选的,我们直接指定可空类型即可。

public ActionResult<ApiResult> DemoAction(int? id) {}

不过这里要注意如果我们没有指定为可空类型,int类型的id参数不存在时它默认会被赋值为0

读取路径参数

路径参数前面路由章节我们已经介绍过了,在路由设置中我们需要使用大括号{}声明将路径绑定到一个参数上,控制器方法中我们使用同名的方法参数接收即可。

[HttpGet]
[Route("DemoAction/{id:int}")]
public ActionResult<ApiResult> DemoAction(int id) {}

如果占位符参数名和方法参数名不一致,我们也可以在方法参数上显式的使用[FromRoute]注解指定对应的占位符参数名,下面是一个例子。

[HttpGet]
[Route("DemoAction/{uid:int}")]
public ActionResult<ApiResult> DemoAction([FromRoute(Name = "uid")] int id) {}

实际开发中很少出现这种情况,偶尔可能在参数有多个来源(路径参数、GET参数等同时使用)时用到。

读取请求体表单

表单请求包括form-datax-www-form-urlencoded两种格式,ASP.NET Core对这两种格式进行了统一处理。下面例子代码中,我们将表单封装为了一个FormReq类型,Controller方法中我们需要使用[FromForm]注解标注参数。

public record FormReq(string Username, string Password);
[HttpPost]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction([FromForm] FormReq formReq) {}

注意我们必须使用[FromForm]注解,否则表单参数不会被正确解析,因为ASP.NET Core默认解析的是JSON请求。此外,表单键值对的键要和FormReq中的字段对应,不过ASP.NET Core默认会忽略大小写。

处理表单文件上传

文件上传最常见的实现方式就是采用form-data格式的表单,ASP.NET Core中我们可以使用IFormFile类型接收表单文件上传。下面例子代码中,我们接收一个表单,它包含文件上传字段file和普通文本字段Username,我们在控制器方法中使用[FromForm]注解标注请求表单对象。

using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;

namespace DemoWebAPI.Controllers;

public record FormReq(IFormFile file, string Username);

[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
    [HttpPost]
    [Route("DemoAction")]
    public ActionResult<ApiResult> DemoAction([FromForm] FormReq formReq)
    {
        // 保存文件
        using FileStream fileStream = new FileStream(Path.Combine($"E:/{formReq.file.FileName}"), FileMode.OpenOrCreate);
        formReq.file.CopyTo(fileStream);
        // 输出Username字段
        Console.WriteLine(formReq.Username);
        return ApiResult.Success();
    }
}

代码中,我们将上传的文件名保存在了E盘下,然后在控制台输出了表单中的文本字段。

读取请求体JSON数据

读取JSON数据非常简单,Controller默认就会处理JSON数据,我们将JSON对应封装的对象作为方法参数即可。

using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;

namespace DemoWebAPI.Controllers;

public record JsonReq(string Username, string Password);

[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
    [HttpPost]
    [Route("DemoAction")]
    public ActionResult<ApiResult> DemoAction(JsonReq jsonReq)
    {
        // 输出请求数据
        Console.WriteLine(jsonReq);
        return ApiResult.Success();
    }
}

类似[FromQuery][FromRoute][FromForm]等控制器方法参数注解,来自请求体的请求也可以使用[FromBody]注解明确指定该参数取自HTTP请求体,通常来说如果只有一个参数的POST或PUT控制器方法是可以省略该参数的,但它在混用多个来源的参数时可能用到。

[HttpPost]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction([FromBody] JsonReq jsonReq)
{
    // ...
}

此外,和表单类似,此处Json字段的键默认也是忽略大小写的,即使我们传如下的奇怪JSON内容,服务端也能正常解析。

{
    "usernAme": "tom",
    "Password": "123"
}

忽略大小写在有些时候可能造成问题,我们可以手动设置让JSON反序列化时大小写敏感。

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.PropertyNameCaseInsensitive = false;
    });

此时,我们传入的JSON必须符合camelCase才能正确解析(即首字母小写的驼峰命名,JSON序列化器默认使用camelCase)。

读取请求头

读取请求头时,我们可以使用[FromHeader]注解,默认读取和方法名同名的Header,如果不同名,注解中我们可以指定Name参数作为Header名。

[HttpGet]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction([FromHeader(Name = "Token")] string token) {}

读取Header的另一种方法是从HttpContext.Request.Headers字典中读取。

[HttpGet]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction()
{
    string userAgent = HttpContext.Request.Headers["User-Agent"];
    // 输出User-Agent Header值
    Console.WriteLine(userAgent);
    return ApiResult.Success();
}

数据模型验证

实际开发中,我们服务端肯定要对客户端提交的数据进行各种验证,ASP.NET Core中我们可以使用System.ComponentModel.DataAnnotations的验证注解声明式实现。下面代码中,我们在请求的模型中使用了验证注解,它标注了两个字段不能为空。

using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;

namespace DemoWebAPI.Controllers;

public record JsonReq(
    [Required(ErrorMessage ="用户名不能为空")]
    string Username,
    [Required(ErrorMessage ="密码不能为空")]
    string Password
    );

[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
    [HttpPost]
    [Route("DemoAction")]
    public ActionResult<ApiResult> DemoAction(JsonReq jsonReq)
    {
        // 输出请求数据
        Console.WriteLine(jsonReq);
        return ApiResult.Success();
    }
}

一旦标注了模型字段验证注解,ASP.NET Core的ApiController就会自动对模型进行验证,如果验证失败,默认会返回一个特殊的错误JSON响应。如果希望能自定义这个错误响应,可以在Program.cs中配置ApiBehaviorOptions,下面是一个例子。

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var firstError = context.ModelState
            .Where(kv => kv.Value.Errors.Count > 0)
            .Select(kv => kv.Value.Errors.FirstOrDefault()?.ErrorMessage)
            .FirstOrDefault();
        return new ObjectResult(ApiResult.Failure("400", firstError ?? "请求参数异常"));
    };
});

代码中,我们配置了当数据验证异常发生时,将取出所有错误中的第一个错误并构造响应体返回给用户。

有关System.ComponentModel.DataAnnotations模型字段验证注解可以参考官方文档,这里就不把所有的注解逐一列出了。

返回HTTP响应

返回JSON数据

返回JSON非常简单,前面介绍过ActionResult的用法,我们在方法体中直接返回对象即可,对象会被自动序列化为JSON数据,字段风格默认使用camelCase。我个人喜欢给JSON封装一个通用返回体,比如{"code": "0", "message": "ok", "data": {}}的格式,一些严格遵循REST风格的团队则可能不喜欢这样做,我们实际开发中根据项目要求或个人喜好返回统一的数据格式即可。

using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;

namespace DemoWebAPI.Controllers;

[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
    [HttpGet]
    [Route("DemoAction")]
    public ActionResult<ApiResult> DemoAction()
    {
        Student s = new Student(1L, "汤姆", 18);
        return ApiResult.Success(s);
    }
}

返回二进制文件数据

File()ControllerBase中提供的一个方法,这个名字起的很不好因为它和System.IO.File冲突,使用后者时我们只能使用全限定名System.IO.File了。File()方法用于返回文件下载响应,它的返回值是FileStreamResult,该类是ActionResult的派生类。

using Microsoft.AspNetCore.Mvc;

namespace DemoWebAPI.Controllers;

[ApiController]
[Route("api/Demo")]
public class DemoController : ControllerBase
{
    [HttpGet]
    [Route("DemoAction")]
    public ActionResult DemoAction()
    {
        FileStream fileStream = new FileStream(Path.Combine("E:/極楽浄土.m4a"), FileMode.Open);
        return File(fileStream, "audio/mp4");
    }
}

设置响应头

设置响应头非常简单,我们直接在Response.Headers字典中添加新的值或覆盖其中的值即可。

Response.Headers["Token"] = "abc123";

返回HTTP状态码

如果要直接返回一个HTTP状态码,我们可以使用StatusCodeResult,下面是一个例子。

[HttpGet]
[Route("DemoAction")]
public ActionResult DemoAction()
{
    return new StatusCodeResult(500);
}

上面代码手动返回了一个HTTP 500状态,除了直接使用StatusCodeResult,我们也可以使用ControllerBase中提供的Ok()NotFound()NoContent()等方法返回特定的响应状态码。

内容协商

ASP.NET Core的控制器支持内容协商(Content Negotiation)机制,简单来说就是根据HTTP请求头中的Accept字段自动判断返回数据类型,例如Accept请求头给出了application/xml就返回XML响应,如果给出了application/json就返回JSON响应。

不过这里注意WebAPI工程默认没有配置XML的序列化器,我们可以添加以下代码将XML的序列化器配置上。

builder.Services.AddControllers().AddXmlSerializerFormatters();

控制器方法例子如下。

[HttpGet]
[Route("DemoAction")]
public ActionResult<ApiResult> DemoAction()
{
    return ApiResult.Success();
}

此时如果我们的请求头Accept字段为application/xml,就会生成类似如下的响应。

<ApiResult xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Code>0</Code>
    <Message>ok</Message>
</ApiResult>

请求头Accept字段为application/json则会生成类似如下的响应。

{
    "code": "0",
    "message": "ok",
    "data": null
}

读写Cookie

下面例子代码中,我们在控制器里实现了Cookie的设置、读取和删除。

using DemoWebAPI.Model;
using Microsoft.AspNetCore.Mvc;

namespace DemoWebAPI.Controllers;

[ApiController]
[Route("api/Cookie")]
public class CookieController : ControllerBase
{
    [HttpGet("WriteCookie")]
    public ActionResult<ApiResult> WriteCookie()
    {
        Response.Cookies.Append("UserPreferences", "Language=English", new CookieOptions
        {
            Expires = DateTimeOffset.Now.AddDays(7),
            HttpOnly = true,
            SameSite = SameSiteMode.Lax
        });
        return ApiResult.Success();
    }

    [HttpGet("ReadCookie")]
    public ActionResult<ApiResult> ReadCookie()
    {
        string? cookieValue = Request.Cookies["UserPreferences"];
        return ApiResult.Success(cookieValue);
    }

    [HttpGet("DeleteCookie")]
    public ActionResult<ApiResult> DeleteCookie()
    {
        Response.Cookies.Delete("UserPreferences");
        return ApiResult.Success();
    }
}

存取Cookie需要操作RequestResponse属性,我们可以在Swagger页面分别执行上述代码的控制器方法查看Cookie的存取效果。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap