ASP.NET Core API每个请求的JSON序列化设置

10

根据请求中的某个值(在头部或URL中)我想要改变我的DTO对象的序列化方式。 为什么?因为我已经将 [JsonProperty("A")] 应用到了我的DTO,但是取决于客户端(网站或移动应用),它是否想要使用该属性。 我从...开始

services
.AddMvc()
.AddJsonOptions(opt =>
{
#if DEBUG
    opt.SerializerSettings.ContractResolver = new NoJsonPropertyNameContractResolver();
#endif
}

在调试时,我得到了具有完整属性名称的 JSON。我使用 JsonProperty 属性来缩短响应 JSON,这对于反序列化回相同 DTO 的移动应用程序(Xamarin)效果很好。

但是现在我有一个网站,它使用相同的 API 通过 jQuery 获取数据,但我希望处理 DTO 的完整属性名称,而不是JsonProperty 属性中给出的名称。

网站和 WebApi 在同一服务器上,因此如果响应稍微大一些也没有问题。

我从一个中间件类开始,以响应客户标头值,这有效,但现在我不知道如何获取 JSON SerializerSettings。我搜索了网络但找不到。

在搜索过程中,我了解了输入格式化程序和输出格式化程序,以及内容协商,但我不知道我应该走哪个方向。

我不想部署相同的 API 两次并且采用不同的设置。
如果有所帮助,我可以更改诸如路由配置之类的东西。

更新
不仅要将 JSON 响应序列化为 2 种不同的方式,还必须采用 2 种不同的方式进行反序列化。


你想在网站中使用不同的属性名称有什么原因吗? - Ignas
1
是的。对于应用程序,我们使用属性名称的缩写来尽可能缩短JSON。在应用程序中,JSON被序列化回DTO,因此在代码中我们有完整的属性名称。 在网站上,我们使用jQuery来获取/发布/放置数据,因此我们希望使用完整的属性名称以保持JavaScript代码的可读性。我认为输入和输出格式化程序是正确的方法。现在正在搜索如何初始化JsonInputFormatter。 - ArieKanarie
关于如何在序列化JSON时忽略JsonProperty(PropertyName="SomeName")和自定义模型绑定,您觉得怎么样? - Ignas
2个回答

20

至少有两种与序列化无关的方式可以实现这一点:通过向 JsonResult 传递自定义选项,以及创建自定义响应过滤器。这些选项在下面涵盖了 Newtonsoft 和 System.Json 库。

1. 配置 JsonResult

创建/修改一个设置实例(仅其类型取决于库),并将其传递到 JsonResult 构造函数中。可以通过 DI 通过注入 IOptions<JsonOptions> 来获取要修改的全局 JSON 设置。

Newtonsoft:

public IActionResult Foo()
{
    var settings = new JsonSerializerSettings
    {
        ContractResolver = new NoJsonPropertyNameContractResolver()
    };

    return new JsonResult(new FooApiModel(), settings);
}

System.Json:

public IActionResult Foo()
{
    var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);

    return new JsonResult(new FooApiModel(), options);
}

2. 添加结果过滤器

同样的思路:只需使用依赖于库的设置实例创建一个新的JsonResult,因此我只展示Newtonsoft版本:

public class ModifyResultFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(
        ResultExecutingContext context,
        ResultExecutionDelegate next)
    {
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new NoJsonPropertyNameContractResolver()
        };

        var originResult = context.Result as JsonResult;

        context.Result = new JsonResult(originResult.Value, settings);

        await next();
    }
}

必须在 DI 中注册过滤器:

builder.Services.AddScoped<ModifyResultFilter>();

最后,让我们在一个操作/控制器上使用它:

[ServiceFilter(typeof(ModifyResultFilter))]
public IActionResult Index() {}

了解更多关于文档中不同的过滤器。


谢谢,但这意味着要更改所有控制器(虽然目前还不是很多)。 - ArieKanarie
@ArieKanarie,我思考了两次并添加了结果筛选器,所以唯一的更改可能是使用属性标记控制器。 - Ilya Chumakov
现在我也考虑了两次,这只会处理结果(=输出),这意味着我们还必须为输入创建过滤器,因为由客户端发送的JSON也因客户而异。我认为这在我的问题中不太清楚。 - ArieKanarie
1
如果你只需要改变输出,我建议使用这个而不是我的答案。这样会少做一些工作。 - ArieKanarie
1
我认为从 .net core 6.0 开始,我们需要使用 IOptions<JsonOptions> 而不是 IOptions<MvcJsonOptions>,因为 MvcJsonOptions 已经在 .net core 3.0 中被移除了。 - Jay Shah
显示剩余2条评论

3
感谢评论和回答。我使用输入和输出格式化程序找到了解决方案。在此感谢 http://rovani.net/Explicit-Model-Constructor/指引我朝着正确的方向发展。
我创建了自己的输入和输出格式化器,它们继承自JsonInputFormatter以保持尽可能多的功能相同。在构造函数中,我设置了支持的媒体类型(使用了与现有JSON类似的一些类型)。同时还必须重写CreateJsonSerializer以将ContractResolver设置为所需的内容(可以实现单例)。必须这样做,因为在构造函数中更改serializerSettings将更改所有输入/输出格式化器的序列化设置,这意味着默认的JSON格式化器也将使用新的合同解析器。此外,这样做还意味着您可以通过AddMvc().AddJsonOption()设置一些默认的JSON选项。
示例输入格式化器,输出格式化器使用相同的原则:
static MediaTypeHeaderValue protoMediaType = MediaTypeHeaderValue.Parse("application/jsonfull");

public JsonFullInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider) 
    : base(logger, serializerSettings, charPool, objectPoolProvider)
{
    this.SupportedMediaTypes.Clear();
    this.SupportedMediaTypes.Add(protoMediaType);
}

protected override JsonSerializer CreateJsonSerializer()
{
    var serializer = base.CreateJsonSerializer();            
    serializer.ContractResolver = new NoJsonPropertyNameContractResolver();

    return serializer;
}

根据上述URL中提到的设置类:
public class YourMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
    private readonly ILoggerFactory _loggerFactory;
    private readonly JsonSerializerSettings _jsonSerializerSettings;
    private readonly ArrayPool<char> _charPool;
    private readonly ObjectPoolProvider _objectPoolProvider;

    public YourMvcOptionsSetup(ILoggerFactory loggerFactory, IOptions<MvcJsonOptions> jsonOptions, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider)
    {
        //Validate parameters and set fields
    }

    public void Configure(MvcOptions options)
    {
        var jsonFullInputFormatter = new JsonFullInputFormatter(
            _loggerFactory.CreateLogger<JsonFullInputFormatter>(),
            _jsonSerializerSettings,
            _charPool,
            _objectPoolProvider
        );

        options.InputFormatters.Add(jsonFullInputFormatter);

        options.OutputFormatters.Add(new JsonFullOutputFormatter(
            _jsonSerializerSettings,
            _charPool
        ));
    }

然后是注册它的扩展方法:
public static class MvcBuilderExtensions
{
    public static IMvcBuilder AddJsonFullFormatters(this IMvcBuilder builder)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }
        ServiceDescriptor descriptor = ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, YourMvcOptionsSetup>();
        builder.Services.TryAddEnumerable(descriptor);
        return builder;
    }
}

ConfigureServices中调用它:

services.AddMvc(config =>
{
    config.RespectBrowserAcceptHeader = true; // To use the JsonFullFormatters if clients asks about it via Accept Header
})
.AddJsonFullFormatters() //Add our own JSON Formatters
.AddJsonOptions(opt =>
{
     //Set up some default options all JSON formatters must use (if any)
});

现在我们的Xamarin应用程序可以访问webapi并通过JsonProperty属性接收具有(短)属性名称的JSON。
而在网站上,我们可以通过添加Accept(get调用)和ContentType(post / put调用)标头来获取完整的JSON属性名称。我们使用jQuery的$.ajaxSetup(在一次调用中完成此操作。

$.ajaxSetup({
    contentType: "application/jsonfull; charset=utf-8",
    headers: { 'Accept': 'application/jsonfull' }
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接