如何使用 built-in xml 或 json 格式化程序在 .Net Core 2.0 中自定义接受 header 值

How to use built-in xml or json formatter for custom accept header value in .Net Core 2.0

更新: 我已经上传了一个小测试项目到github:link

我正在使用 .Net Core 2 创建一个小型 Web 服务,并希望让客户能够指定他们是否需要响应中的导航信息。 web api 应该只支持 xml 和 json,但如果客户端可以使用就好了 接受:application/xml+hateoas 或者 接受:application/json+hateoas 在他们的要求下。

我试过像这样设置我的 AddMvc 方法:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options =>
        {
            options.RespectBrowserAcceptHeader = true;
            options.ReturnHttpNotAcceptable = true;
            options.FormatterMappings.SetMediaTypeMappingForFormat(
                "xml", MediaTypeHeaderValue.Parse("application/xml"));
            options.FormatterMappings.SetMediaTypeMappingForFormat(
                "json", MediaTypeHeaderValue.Parse("application/json"));
            options.FormatterMappings.SetMediaTypeMappingForFormat(
                "xml+hateoas", MediaTypeHeaderValue.Parse("application/xml"));
            options.FormatterMappings.SetMediaTypeMappingForFormat(
                "json+hateoas", MediaTypeHeaderValue.Parse("application/json"));
        })            
        .AddJsonOptions(options => {
            // Force Camel Case to JSON
            options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        })
        .AddXmlSerializerFormatters()
        .AddXmlDataContractSerializerFormatters()
        ;

并且我在我的控制器方法中使用接受 header 来区分正常的 xml/json 响应和 hateoas-like 响应,如下所示:

[HttpGet]
[Route("GetAllSomething")]
public async Task<IActionResult> GetAllSomething([FromHeader(Name = "Accept")]string accept)
{
...
bool generateLinks = !string.IsNullOrWhiteSpace(accept) && accept.ToLower().EndsWith("hateoas");
...
if (generateLinks)
{
    AddNavigationLink(Url.Link("GetSomethingById", new { Something.Id }), "self", "GET");
}
...
}

所以,简而言之,我不想创建自定义格式化程序,因为唯一 "custom" 的事情是在我的响应中包含或排除导航链接,但响应本身应该是 xml 或 json 基于接受 header 值。

我的模型 class 看起来像这样(里面主要有字符串和基本值):

[DataContract]
public class SomethingResponse
{
    [DataMember]
    public int Id { get; private set; }

当从 Fiddler 调用我的服务时,对于不同的 Accept 值,我得到了以下结果:

  1. 接受:application/json -> 状态代码 200,只有请求的数据。
  2. 接受:application/json+hateoas -> 状态代码 406(不可接受)。
  3. 接受:application/xml -> 状态代码 504。[Fiddler] ReadResponse() 失败:服务器没有 return 对此请求的完整响应。服务器 returned 468 字节。
  4. 接受:application/xml+hateoas -> 状态代码 406(不可接受)。

有人能告诉我哪个设置有问题吗?

格式到媒体类型的映射(SetMediaTypeMappingForFormat 调用)与您预期的不同。此映射不在请求中使用 Accept header。它从路由数据中名为 format 的参数或 URL 查询字符串中读取请求的格式。您还应该使用 FormatFilter 属性标记您的控制器或操作。有几篇关于基于 FormatFilter 属性的响应格式化的好文章,检查 here and here.

要修复您当前的格式映射,您应该执行以下操作:

  1. 重命名格式,使其不包含加号。特殊 + 字符传入 URL 时会给您带来麻烦。最好换成-:

    options.FormatterMappings.SetMediaTypeMappingForFormat(
        "xml-hateoas", MediaTypeHeaderValue.Parse("application/xml"));
    options.FormatterMappings.SetMediaTypeMappingForFormat(
        "json-hateoas", MediaTypeHeaderValue.Parse("application/json"));
    
  2. 在路由中添加format参数:

    [Route("GetAllSomething/{format}")]
    
  3. 用于格式映射的格式无法从Acceptheader中提取,因此您将在URL中传递它。由于您需要知道控制器中逻辑的格式,因此您可以在 format 上方映射从路由到操作参数,以避免在 Accept header:

    中重复
    public async Task<IActionResult> GetAllSomething(string format)
    

    现在您不需要在 Accept header 中传递所需的格式,因为格式将从请求 URL.

  4. 映射
  5. FormatFilter 属性标记控制器或动作。

    最终行动:

    [HttpGet]
    [Route("GetAllSomething/{format}")]
    [FormatFilter]
    public async Task<IActionResult> GetAllSomething(string format)
    {
        bool generateLinks = !string.IsNullOrWhiteSpace(format) && format.ToLower().EndsWith("hateoas");
    
        //  ...
    
        return await Task.FromResult(Ok(new SomeModel { SomeProperty = "Test" }));
    }
    

现在,如果您请求 URL /GetAllSomething/xml-hateoas(即使缺少 Accept header),FormatFilter 将映射 formatxml-hateoasapplication/xml 和 XML 格式化程序将用于响应。请求的格式也可以在 GetAllSomething 操作的 format 参数中访问。

Sample Project with formatter mappings on GitHub

除了格式化程序映射之外,您还可以通过向现有媒体类型格式化程序添加新的受支持媒体类型来实现您的目标。支持的媒体类型存储在 OutputFormatter.SupportedMediaTypes collection 中,并填充在具体输出格式化程序的构造函数中,例如XmlSerializerOutputFormatter。您可以自己创建格式化程序实例(而不是使用 AddXmlSerializerFormatters 扩展调用)并将所需的媒体类型添加到 SupportedMediaTypes collection。要调整默认添加的 JSON 格式化程序,只需在 options.OutputFormatters:

中找到它的实例即可
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
        {
            options.RespectBrowserAcceptHeader = true;
            options.ReturnHttpNotAcceptable = true;

            options.InputFormatters.Add(new XmlSerializerInputFormatter());
            var xmlOutputFormatter = new XmlSerializerOutputFormatter();
            xmlOutputFormatter.SupportedMediaTypes.Add("application/xml+hateoas");
            options.OutputFormatters.Add(xmlOutputFormatter);

            var jsonOutputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault();
            jsonOutputFormatter?.SupportedMediaTypes.Add("application/json+hateoas");
        })
        .AddJsonOptions(options => {
            // Force Camel Case to JSON
            options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        })
        .AddXmlDataContractSerializerFormatters();
}

在这种情况下,GetAllSomething 应该与您原来的问题相同。您还应该在 Accept header 中传递所需的格式,例如Accept: application/xml+hateoas.

Sample Project with custom media types on GitHub