1

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

我正在使用 .Net Core 2 创建一个小型 Web 服务,并希望让客户能够指定他们是否需要响应中的导航信息。web api 应该只支持 xml 和 json,但是如果客户端可以在他们的请求中使用 Accept: application/xml+hateoas 或 Accept: 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()
        ;

我在我的控制器方法中使用接受标头来区分正常的 xml/json 响应和类似讨厌的响应,如下所示:

[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");
}
...
}

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

我的模型类看起来像这样(主要是字符串和基本值):

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

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

  1. 接受:application/json -> 状态码 200,仅包含请求的数据。
  2. 接受:application/json+hateoas -> 状态码 406(不可接受)。
  3. 接受:应用程序/xml -> 状态码 504。 [Fiddler] ReadResponse() 失败:服务器没有为此请求返回完整的响应。服务器返回 468 字节。
  4. 接受:application/xml+hateoas -> 状态码 406(不可接受)。

谁能告诉我哪个设置错了?

4

1 回答 1

0

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

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

  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. 用于格式映射的格式无法从Accept标头中提取,因此您将在 URL 中传递它。由于您需要知道控制器中逻辑的格式,因此您可以将上面format的路由映射到操作参数以避免Accept标题中的重复:

    public async Task<IActionResult> GetAllSomething(string format)
    

    现在您不需要在Accept标头中传递所需的格式,因为格式将从请求 URL 映射。

  4. 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标头),FormatFilter将映射toformat的值,并且 XML 格式化程序将用于响应。请求的格式也可以在操作参数中访问。xml-hateoasapplication/xmlformatGetAllSomething

GitHub 上带有格式化程序映射的示例项目

除了格式化程序映射之外,您还可以通过将新支持的媒体类型添加到现有的媒体类型格式化程序来实现您的目标。支持的媒体类型存储在OutputFormatter.SupportedMediaTypes集合中,并填写在具体输出格式化程序的构造函数中,例如XmlSerializerOutputFormatter. 您可以自己创建格式化程序实例(而不是使用AddXmlSerializerFormatters扩展调用)并将所需的媒体类型添加到SupportedMediaTypes集合中。要调整默认添加的 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标题中传递所需的格式,例如Accept: application/xml+hateoas.

GitHub 上具有自定义媒体类型的示例项目

于 2018-04-21T10:26:12.127 回答