在上一篇烂文中老周已向各位介绍过 Produces 特性的使用,本文老周将介绍另一个特性类:FormatFilterAttribute。

这个特性算得上是筛选器的马甲,除了从 Attribute 类派生外,还实现了 IFilterFactory 接口。之所以说它是个马甲,是因为 IFilterFactory 接口要求类型实现 CreateInstance 方法来产生筛选器的对象实例。也就是说,FormatFilterAttribute 类并没有真正做筛选的代码,而是创建一个 FormatFilter 类的实例。


这厮是怎么工作的

这个特性类可以应用在类(控制器)和方法(控制器中的 Action)上,它允许 API 的调用方主动选择返回数据的格式。这是什么骚操作呢?

如果你以前(我说的是以前,因为现在很多都只支持JSON格式)做过像微博开放平台的 API 调用,可能还记得在 URL 上通过参数来选择返回 XML 还是 JSON。比如这样:

http://what.com/api/getlist?t=xml
http://what.com/api/getlist?t=json

当然了,前提是你写的 API 支持被指定的格式,要是调用者指定了 jpg,而你编写的 API 不支持是会报错的。格式名称是如何让 ASP.NET Core 识别出要返回的 Content-Type 的呢?别急,往下看就知道了。

先说说 FormatFilter 特性是如何获取到 API 调用方指定的格式的。方式有二:

  1. 从路由规则查找名为“format”的关键字。就像 MVC 路由规则中的“controller”、"action"关键字一样。如果“format”关键字识别出 json,那就返回 JSON 格式的数据;若识别出 xml 就返回 XML 格式的数据。
  2. 从请求 URL 的查询字符串中找到名为“format”的字段,若它的值为 json 表示返回 JSON 格式的数据;若为 xml 就返回 XML 格式的数据。若为其他值,你得自定义实现。

最好通过路由规则的方式来处理,一则此法比较灵活,二则不必占用 URL 查询字符串,免得把 URL 弄得太长。

刚刚老周说路由规则可以用“format”关键字来识别格式,要想知道为什么,咱们可以看看 FormatFilter 类的源代码(FormatFilter 特性只是个壳,没啥好看)。

    public virtual string? GetFormat(ActionContext context)
{
if (context.RouteData.Values.TryGetValue("format", out var obj))
{
// null and string.Empty are equivalent for route values.
var routeValue = Convert.ToString(obj, CultureInfo.InvariantCulture);
return string.IsNullOrEmpty(routeValue) ? null : routeValue;
} var query = context.HttpContext.Request.Query["format"];
if (query.Count > 0)
{
return query.ToString();
} return null;
}

它先是从 RouteData 字典中找一找有没有与“format”对应的值,如果有,就返回;如果没有,再去找 URL 查询字符串中是否存在“format”字段。

如你所见,在 FormatFilter 类中,这个 GetFormat 方法是声明为 virtual 的,说白了,你可以自定义你的查找方法,可能你找的不是名为“format”的关键字,而是叫“type”。你只要从 FormatFilter 类派生,然后覆写 GetFormat 方法。最后把你自己写的新 FormatFilter 注册到 MVC 选项的 Filters 列表中即可。


动手一试

此处用的测试数据类为 Book。

    public class Book
{
/// <summary>
/// 编号
/// </summary>
public uint ID { get; set; }
/// <summary>
/// 书名
/// </summary>
public string Title { get; set; }
/// <summary>
/// 作者
/// </summary>
public string Author { get; set; }
/// <summary>
/// 发行时间
/// </summary>
public DateTime PublishTime { get; set; }
}

我们假设 Book 对象表示一本图书的基本信息。

然后,咱们弄个控制器。

    [Route("api/bkstore")]
[ApiController, FormatFilter]
public class BooksController : ControllerBase
{
[HttpGet("list/{format?}")]
public IEnumerable<Book> ListBooks() => new Book[]
{
new() {ID=5112, Title="C语言从入门到割腕", Author="老周", PublishTime = new(2011,10,12)},
new() {ID=72543, Title="下水道里的英雄", Author="老周", PublishTime= new(2021,4,17)},
new() {ID=28565, Title="领饭盒时代", Author="老张", PublishTime= new(2022,5,1)},
new() {ID=80251, Title="钱多脑傻的城里人", Author="光头强", PublishTime= new(2017,6,8)}
};
}

Books 控制器应用了 FormatFilter 特性,使得在整个控制器内的操作方法均支持通过 format 关键字来选择数据格式。调用的 URL 格式如下:

http://localhost/api/bkstore/list/json
http://localhost/api/bkstore/list/xml

“{format?}”中有个问号,表示这个路由参数是可选的,即可以省略。如果省略,ASP.NET Core 应用程序就会从已经注册的格式列表中查找匹配的第一个项作为默认格式。例如,MVC 格式列表中注册了json、xml、audio/wav 等格式,当 {format} 参数省略后,默认会选择 json。

在 Program.cs 文件中补上其他代码,在注册 API 控制器功能时,要调用 AddXmlSerializerFormatters 方法,这样才支持返回 XML 格式的数据。

var builder = WebApplication.CreateBuilder(args);
// 添加XML格式的支持需要调用 AddXmlSerializerFormatters 方法
builder.Services.AddControllers().AddXmlSerializerFormatters();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//================================================================
var app = builder.Build();
//================================================================
app.UseSwagger();
app.UseSwaggerUI(o =>
{
o.RoutePrefix = "";
o.SwaggerEndpoint("swagger/v1/swagger.json", "swg");
}); app.MapControllers(); app.Run();

上面代码中,调用了 UseSwaggerUI 等方法,使项目支持 Web API 的测试,这个地方老周修改了一些默认配置。

app.UseSwaggerUI(o =>
{
o.RoutePrefix = "";
o.SwaggerEndpoint("swagger/v1/swagger.json", "swg");
});

RoutePrefix 属性设置访问 Swagger 页面的路径,默认要到 /swagger 下,我把它改为空字符串,表示在根路径就能访问,主要是为了测试方便。直接访问 http://localhost:xxx/ 就 OK。由于默认的前缀 /swagger 被去掉了,所以,获取描述 API 的 JSON 文档的获取路径要手动设置回默认的路径 /swagger/v1/swagger.json,否则运行后会找不到 API 信息。

由于 Swagger UI 的测试页不能将 {format?} 识别为可选参数,所以在调用时要显式加上 xxx/json 或 xxx/xml。

http://localhost:5228/api/bkstore/list/json
http://localhost:5228/api/bkstore/list/xml

用 XML 格式时返回的结果:

用 JSON 格式时返回的结果:


自己加个格式

json、xml 是 ASP.NET Core 自动注册的格式名称,我们也可以自己加一些格式。

builder.Services.AddControllers()
.AddXmlSerializerFormatters()
.AddFormatterMappings(mappings =>
{
mappings.SetMediaTypeMappingForFormat("txtj", "text/json");
});

在调用完 AddControllers、AddXmlSerializerFormatters 后,顺势调用 AddFormatterMappings 方法添加格式映射。通过 SetMediaTypeMappingForFormat 方法把名为 txtj 的格式与 text/json 关联。这么一来,想让 API 返回 Content-Type 为 text/json 的数据,只需要这样访问就行:

http://localhost:5228/api/bkstore/list/txtj

前文老周卖了个关子:ASP.NET Core 程序是如何识别出格式对应的 MIME ?这个 SetMediaTypeMappingForFormat 方法的调用就是答案。它维护了一个 Key/Value 集合(理解为一个字典吧),key 是格式的名称(这个可以自定义),如 xml、json,jpg 等,然后会有唯一的 MIME 与之对应。像 json --> application/json,xml --> application/xml、abc --> image/png 这样。

但是,若添加 txt --> text/plain 的映射,就会失败。

builder.Services.AddControllers().AddXmlSerializerFormatters()
.AddFormatterMappings(mappings =>
{
mappings.SetMediaTypeMappingForFormat("txt", "text/plain");
});

原因并不是 ASP.NET Core 不允许你这样做,而是格式不匹配。还记得老周在上一篇水文中说过吗,text/plain 默认由 StringOutputFormatter 类来处理的,只支持返回值为 string 类型的方法。而咱们上例中的 ListBooks 方法是返回一个 Book 对象的列表的,类型上不匹配。

所以,如果你想映射 txt --> text/plain 上,需要自定义一个 Formatter,让其将 Book 列表变为字符串。这个大伙可以自己试试(这个最好不要太自定义了,否则有数组有类,比较难搞,可以考虑在 Book 类中重写 ToString 方法,可能好弄些),老周接下来用另一个例子来说明一下,因为这个例子不返回数组,只返回单个实例,可以用反射来扫描所有公共属性,然后连接成字符串。当然了,这种做法局限性大,也没办法通用于所有类型,仅作演示。

先定义咱们需要的数据类,这里命名为 Goods,表示一件商品(因为老周是开杂货店的,所以用 Goods 类)。

    public class Goods
{
/// <summary>
/// 商品ID
/// </summary>
public uint ID { get; set; }
/// <summary>
/// 商品标题
/// </summary>
public string Name { get; set; } = "none";
/// <summary>
/// 单价
/// </summary>
public decimal Price { get; set; }
/// <summary>
/// 备注
/// </summary>
public string Remark { get; set; } = string.Empty;
}

接着,实现自定义的 Formatter 类,这里咱们所需的功能是将对象的公共属性拼接为字符串返回给客户端。故咱们不需要完全自己去实现 IOutputFormatter 接口,直接从 TextOutputFormatter 类派生就行了。这货是个抽象类,咱们要做两件事:

  1. 在构造函数中向 SupportedMediaTypes 列表中添加受支持的 MIME 类型。你希望它兼容哪些格式,就分别 Add 进去就 OK 了。此例中老周仅希望它支持 text/plain 格式,所以只加这个就可以了。然后还要向 SupportedEncodings 列表添加受支持的字符编码,现在一般用 UTF-8 就好,减少许多麻烦。

  2. 实现 WriteResponseBodyAsync 方法,将待处理对象转化为字符串,并回写到响应流中。

    public class MyOutputFormatter : TextOutputFormatter
{
public MyOutputFormatter()
{
/*
* 下面这两行必不能少
*/
// 添加所支持的 MIME 类型
SupportedMediaTypes.Add("text/plain");
// 添加支持的字符编码
SupportedEncodings.Add(Encoding.UTF8);
} public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
// 获取被处理的对象实例
object obj = context.Object;
// 获取对象的 Type
Type objtype = context.ObjectType;
if (obj is null || objtype is null)
{
return;
}
// 找出公共属性
var props = objtype.GetProperties(BindingFlags.Public | BindingFlags.Instance);
StringBuilder strbf = new();
// 逐个读取出来
foreach (var p in props)
{
strbf.Append($"{p.Name}=");
object val = p.GetValue(obj);
if (!(val is null))
{
strbf.Append(val);
}
strbf.AppendLine();
}
// 写响应内容
await context.HttpContext.Response.WriteAsync(strbf.ToString());
}
}

在 Program.cs 文件中,调用 AddControllers 方法,把刚刚定义的 Formatter 实例添加到 OutputFormatters 列表中。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(opt =>
{
opt.OutputFormatters.Add(new MyOutputFormatter());
})
.AddXmlSerializerFormatters()
.AddFormatterMappings(mappings =>
{
mappings.SetMediaTypeMappingForFormat("txt", "text/plain");
});
……

最后,咱们回过头来向控制器类添加一个操作方法。

        [HttpGet("buy/{format?}")]
public Goods BuySomething() => new Goods
{
ID = 93257,
Name = "恐龙皮做的女士背包",
Price = 58888.03M,
Remark = "直播带货,无需生产许可,无合格证,无需品控,无售后;无退换货,商品若有质量问题,请买家自行销毁"
};

然后运行测试一下(访问 http://localhost:xxxx/api/bkstore/buy/txt)。返回结果:

ID=93257
Name=恐龙皮做的女士背包
Price=58888.03
Remark=直播带货,无需生产许可,无合格证,无需品控,无售后;无退换货,商品若有质量问题,请买家自行销毁

【ASP.NET Core】设置 Web API 响应数据的格式——FormatFilter特性篇的更多相关文章

  1. angular4和asp.net core 2 web api

    angular4和asp.net core 2 web api 这是一篇学习笔记. angular 5 正式版都快出了, 不过主要是性能升级. 我认为angular 4还是很适合企业的, 就像.net ...

  2. 温故知新,使用ASP.NET Core创建Web API,永远第一次

    ASP.NET Core简介 ASP.NET Core是一个跨平台的高性能开源框架,用于生成启用云且连接Internet的新式应用. 使用ASP.NET Core,您可以: 生成Web应用和服务.物联 ...

  3. 使用 ASP.NET Core MVC 创建 Web API——响应数据的内容协商(七)

    使用 ASP.NET Core MVC 创建 Web API 使用 ASP.NET Core MVC 创建 Web API(一) 使用 ASP.NET Core MVC 创建 Web API(二) 使 ...

  4. 【ASP.NET Core】设置Web API 响应的数据格式——Produces 特性篇

    开春首文,今天老周就跟各位大伙伴们聊一个很简单的话题:怎么设定API响应的数据格式. 说本质一点,就是设置所返回内容的 MIME 类型(Content-Type 头).当然了,咱们不会使用在HTTP管 ...

  5. 使用angular4和asp.net core 2 web api做个练习项目(一)

    这是一篇学习笔记. angular 5 正式版都快出了, 不过主要是性能升级. 我认为angular 4还是很适合企业的, 就像.net一样. 我用的是windows 10 安装工具: git for ...

  6. 使用angular4和asp.net core 2 web api做个练习项目(四)

    第一部分: http://www.cnblogs.com/cgzl/p/7755801.html 第二部分: http://www.cnblogs.com/cgzl/p/7763397.html 第三 ...

  7. 使用angular4和asp.net core 2 web api做个练习项目(二), 这部分都是angular

    上一篇: http://www.cnblogs.com/cgzl/p/7755801.html 完成client.service.ts: import { Injectable } from '@an ...

  8. 基于ASP.NET Core 创建 Web API

    使用 Visual Studio 创建项目. 文件->新建->项目,选择创建 ASP.NET Core Web 应用程序. 基于 ASP.NET Core 2.0 ,选择API,身份验证选 ...

  9. ASP.NET Core Restful Web API 相关资源索引

    GraphQL 使用ASP.NET Core开发GraphQL服务器 -- 预备知识(上) 使用ASP.NET Core开发GraphQL服务器 -- 预备知识(下) [视频] 使用ASP.NET C ...

随机推荐

  1. TKE 用户故事 | 作业帮 Kubernetes 原生调度器优化实践

    作者 吕亚霖,2019年加入作业帮,作业帮架构研发负责人,在作业帮期间主导了云原生架构演进.推动实施容器化改造.服务治理.GO微服务框架.DevOps的落地实践. 简介 调度系统的本质是为计算服务/任 ...

  2. 分别使用time 和 datetime模块记录当前时间

    工作中经常混淆这两种方法 现记录一下 加深印象 代码如下: >>> import time>>> import datetime>>> ct1 = ...

  3. Python_上下文管理器

    上下文管理器(context manager)是 Python 编程中的重要概念,用于规定某个对象的使用范围.一旦进入或者离开该使用范围,会有特殊操作被调用 (比如为对象分配或者释放内存).它的语法形 ...

  4. Centos更换阿里云源

    1.备份 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 2.下载新的CentOS-Base ...

  5. Echart可视化学习(四)

    文档的源代码地址,需要的下载就可以了(访问密码:7567) https://url56.ctfile.com/f/34653256-527823386-04154f 正文: 地图模块高度为 810px ...

  6. day 19 C语言顺序结构基础2

    (1).算术运算符和圆括号有不同的运算优先级,对于表达式:a+b+c*(d+e),关于执行步骤,以下说法正确的是[A] (A).先执行a+b的r1,再执行(d+e)的r2,再执行c*r2的r3,最后执 ...

  7. django框架--登录注册功能(ajax)

    注册 实现一个注册功能 编写 html 内容 input 标签 csrf_token ajax 路由 视图: 提供页面 负责处理业务,返回响应 接收到   post   请求传递的参数 写库 返回   ...

  8. rocketmq实现延迟队列精确到秒级实现方案1-代理实现

    简单的来说,就是rocketmq发送消息到broker的时候,判断是否定时消息, 如果是定时消息,将消息发送到代理服务(这个是一个独立的服务,需要自己开发,定时地把消息发送出去), 当然了消息用什么来 ...

  9. MRCTF2020 套娃

    MRCTF2020套娃 打开网页查看源代码 关于$_SERVER['QUERY_STRING']取值,例如: http://localhost/aaa/?p=222 $_SERVER['QUERY_S ...

  10. azure django bug

    azure web app service azure web app service无法部署dhango网站 本地服务器测试代码 实际azure测试 django service 没有部署选项,需要 ...