首页 > 解决方案 > 更改单个 ASP.NET Core 控制器的 JSON 反序列化/序列化策略

问题描述

我有一个控制器用于第三方 API,它使用蛇形命名约定。我的其余控制器用于我自己的应用程序,它使用驼峰式 JSON 约定。我想为那个控制器中的 API 自动反序列化和序列化我的模型。这个问题解释了如何在整个应用程序中为 JSON 使用蛇形案例命名策略,但是有没有一种方法可以指定仅对单个控制器使用命名策略?

我见过更改单个 ASP.NET Core 控制器的 JSON 序列化设置,它建议使用 ActionFilter,但这仅有助于确保正确序列化传出的 JSON。如何将传入的 JSON 也正确反序列化为我的模型?我知道我可以[JsonPropertyName]在模型属性名称上使用,但我更希望能够在控制器级别而不是模型级别进行设置。

标签: c#jsonasp.net-core

解决方案


您问题中共享链接上的解决方案可以用于序列化(由 实现IOutputFormatter),尽管我们可能有另一种方法,通过其他可扩展点对其进行扩展。

在这里,我想重点关注缺失的方向(由 实现的反序列化方向IInputFormatter)。您可以实现自定义IModelBinder,但它需要您重新实现BodyModelBinderBodyModelBinderProvider这并不容易。除非您接受克隆它们的所有源代码并修改您想要的方式。这对可维护性和更新框架更改的内容不是很友好。

研究了源码后,我发现要找到一个可以根据不同的控制器(或动作)自定义反序列化行为的点并不容易。基本上,默认实现对 json 使用一次性初始化IInputFormatter(默认JsonInputFormatter为 asp.net core < 3.0)。链中将共享一个JsonSerializerSettings. 在您的场景中,实际上您需要该设置的多个实例(对于每个控制器或操作)。我认为最简单的一点是自定义一个IInputFormatter(扩展默认值JsonInputFormatter)。当默认实现ObjectPool用于JsonSerializer(与JsonSerializerSettings)。为了遵循这种池化对象的风格(为了获得更好的性能),您需要一个对象池列表(我们将在此处使用字典),而不仅仅是一个对象池用于共享对象池JsonSerializer以及关联对象池JsonSerializerSettings(默认实现JsonInputFormatter) .

这里的重点是基于当前InputFormatterContext,你需要构建相应JsonSerializerSettings的以及JsonSerializer要使用的。这听起来很简单,但是一旦涉及到一个完整的实现(具有相当完整的设计),代码一点也不短。我把它设计成多个类。如果你真的想看到它工作,请耐心仔细地复制代码(当然建议通读一遍理解)。这是所有代码:

public abstract class ContextAwareSerializerJsonInputFormatter : JsonInputFormatter
{        
    public ContextAwareSerializerJsonInputFormatter(ILogger logger, 
        JsonSerializerSettings serializerSettings, 
        ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
    {
        PoolProvider = objectPoolProvider;
    }
    readonly AsyncLocal<InputFormatterContext> _currentContextAsyncLocal = new AsyncLocal<InputFormatterContext>();
    readonly AsyncLocal<ActionContext> _currentActionAsyncLocal = new AsyncLocal<ActionContext>();
    protected InputFormatterContext CurrentContext => _currentContextAsyncLocal.Value;
    protected ActionContext CurrentAction => _currentActionAsyncLocal.Value;
    protected ObjectPoolProvider PoolProvider { get; }
    public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
    {
        _currentContextAsyncLocal.Value = context;
        _currentActionAsyncLocal.Value = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>().ActionContext;
        return base.ReadRequestBodyAsync(context, encoding); 
    }
    public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
    {
        _currentContextAsyncLocal.Value = context;
        _currentActionAsyncLocal.Value = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>().ActionContext;
        return base.ReadRequestBodyAsync(context);
    }
    protected virtual JsonSerializer CreateJsonSerializer(InputFormatterContext context) => null;
    protected override JsonSerializer CreateJsonSerializer()
    {
        var context = CurrentContext;
        return (context == null ? null : CreateJsonSerializer(context)) ?? base.CreateJsonSerializer();
    }
}

public abstract class ContextAwareMultiPooledSerializerJsonInputFormatter : ContextAwareSerializerJsonInputFormatter
{
    public ContextAwareMultiPooledSerializerJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) 
        : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
    {
        
    }
    readonly IDictionary<object, ObjectPool<JsonSerializer>> _serializerPools = new ConcurrentDictionary<object, ObjectPool<JsonSerializer>>();
    readonly AsyncLocal<object> _currentPoolKeyAsyncLocal = new AsyncLocal<object>();
    protected object CurrentPoolKey => _currentPoolKeyAsyncLocal.Value;
    protected abstract object GetSerializerPoolKey(InputFormatterContext context);
    protected override JsonSerializer CreateJsonSerializer(InputFormatterContext context)
    {
        object poolKey = GetSerializerPoolKey(context) ?? "";
        if(!_serializerPools.TryGetValue(poolKey, out var pool))
        {
            //clone the settings
            var serializerSettings = new JsonSerializerSettings();
            foreach(var prop in typeof(JsonSerializerSettings).GetProperties().Where(e => e.CanWrite))
            {
                prop.SetValue(serializerSettings, prop.GetValue(SerializerSettings));
            }
            ConfigureSerializerSettings(serializerSettings, poolKey, context);
            pool = PoolProvider.Create(new JsonSerializerPooledPolicy(serializerSettings));
            _serializerPools[poolKey] = pool;
        }
        _currentPoolKeyAsyncLocal.Value = poolKey;
        return pool.Get();
    }
    protected override void ReleaseJsonSerializer(JsonSerializer serializer)
    {            
        if(_serializerPools.TryGetValue(CurrentPoolKey ?? "", out var pool))
        {
            pool.Return(serializer);
        }         
    }
    protected virtual void ConfigureSerializerSettings(JsonSerializerSettings serializerSettings, object poolKey, InputFormatterContext context) { }
}

//there is a similar class like this implemented by the framework 
//but it's a pity that it's internal
//So we define our own class here (which is exactly the same from the source code)
//It's quite simple like this
public class JsonSerializerPooledPolicy : IPooledObjectPolicy<JsonSerializer>
{
    private readonly JsonSerializerSettings _serializerSettings;
    
    public JsonSerializerPooledPolicy(JsonSerializerSettings serializerSettings)
    {
        _serializerSettings = serializerSettings;
    }

    public JsonSerializer Create() => JsonSerializer.Create(_serializerSettings);
    
    public bool Return(JsonSerializer serializer) => true;
}

public class ControllerBasedJsonInputFormatter : ContextAwareMultiPooledSerializerJsonInputFormatter,
    IControllerBasedJsonSerializerSettingsBuilder
{
    public ControllerBasedJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
    {
    }
    readonly IDictionary<object, Action<JsonSerializerSettings>> _configureSerializerSettings
             = new Dictionary<object, Action<JsonSerializerSettings>>();
    readonly HashSet<object> _beingAppliedConfigurationKeys = new HashSet<object>();
    protected override object GetSerializerPoolKey(InputFormatterContext context)
    {
        var routeValues = context.HttpContext.GetRouteData()?.Values;
        var controllerName = routeValues == null ? null : routeValues["controller"]?.ToString();
        if(controllerName != null && _configureSerializerSettings.ContainsKey(controllerName))
        {
            return controllerName;
        }
        var actionContext = CurrentAction;
        if (actionContext != null && actionContext.ActionDescriptor is ControllerActionDescriptor actionDesc)
        {
            foreach (var attr in actionDesc.MethodInfo.GetCustomAttributes(true)
                                           .Concat(actionDesc.ControllerTypeInfo.GetCustomAttributes(true)))
            {
                var key = attr.GetType();
                if (_configureSerializerSettings.ContainsKey(key))
                {                        
                    return key;
                }
            }
        }
        return null;
    }
    public IControllerBasedJsonSerializerSettingsBuilder ForControllers(params string[] controllerNames)
    {
        foreach(var controllerName in controllerNames ?? Enumerable.Empty<string>())
        {                
            _beingAppliedConfigurationKeys.Add((controllerName ?? "").ToLowerInvariant());
        }            
        return this;
    }
    public IControllerBasedJsonSerializerSettingsBuilder ForControllersWithAttribute<T>()
    {
        _beingAppliedConfigurationKeys.Add(typeof(T));
        return this;
    }
    public IControllerBasedJsonSerializerSettingsBuilder ForActionsWithAttribute<T>()
    {
        _beingAppliedConfigurationKeys.Add(typeof(T));
        return this;
    }
    ControllerBasedJsonInputFormatter IControllerBasedJsonSerializerSettingsBuilder.WithSerializerSettingsConfigurer(Action<JsonSerializerSettings> configurer)
    {
        if (configurer == null) throw new ArgumentNullException(nameof(configurer));
        foreach(var key in _beingAppliedConfigurationKeys)
        {
            _configureSerializerSettings[key] = configurer;
        }
        _beingAppliedConfigurationKeys.Clear();
        return this;
    }
    protected override void ConfigureSerializerSettings(JsonSerializerSettings serializerSettings, object poolKey, InputFormatterContext context)
    {            
        if (_configureSerializerSettings.TryGetValue(poolKey, out var configurer))
        {
            configurer.Invoke(serializerSettings);
        }
    }
}
public interface IControllerBasedJsonSerializerSettingsBuilder
{
    ControllerBasedJsonInputFormatter WithSerializerSettingsConfigurer(Action<JsonSerializerSettings> configurer);
    IControllerBasedJsonSerializerSettingsBuilder ForControllers(params string[] controllerNames);
    IControllerBasedJsonSerializerSettingsBuilder ForControllersWithAttribute<T>();
    IControllerBasedJsonSerializerSettingsBuilder ForActionsWithAttribute<T>();
}

为了帮助方便地配置服务以替换默认值JsonInputFormatter,我们有以下代码:

public class ControllerBasedJsonInputFormatterMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
    private readonly ILoggerFactory _loggerFactory;
    private readonly MvcJsonOptions _jsonOptions;
    private readonly ArrayPool<char> _charPool;
    private readonly ObjectPoolProvider _objectPoolProvider;
    public ControllerBasedJsonInputFormatterMvcOptionsSetup(
        ILoggerFactory loggerFactory,
        IOptions<MvcJsonOptions> jsonOptions,
        ArrayPool<char> charPool,
        ObjectPoolProvider objectPoolProvider)
    {
        if (loggerFactory == null)
        {
            throw new ArgumentNullException(nameof(loggerFactory));
        }

        if (jsonOptions == null)
        {
            throw new ArgumentNullException(nameof(jsonOptions));
        }

        if (charPool == null)
        {
            throw new ArgumentNullException(nameof(charPool));
        }

        if (objectPoolProvider == null)
        {
            throw new ArgumentNullException(nameof(objectPoolProvider));
        }

        _loggerFactory = loggerFactory;
        _jsonOptions = jsonOptions.Value;
        _charPool = charPool;
        _objectPoolProvider = objectPoolProvider;
    }
    public void Configure(MvcOptions options)
    {
        //remove the default
        options.InputFormatters.RemoveType<JsonInputFormatter>();
        //add our own
        var jsonInputLogger = _loggerFactory.CreateLogger<ControllerBasedJsonInputFormatter>();

        options.InputFormatters.Add(new ControllerBasedJsonInputFormatter(
            jsonInputLogger,
            _jsonOptions.SerializerSettings,
            _charPool,
            _objectPoolProvider,
            options,
            _jsonOptions));
    }
}
public static class ControllerBasedJsonInputFormatterServiceCollectionExtensions
{
    public static IServiceCollection AddControllerBasedJsonInputFormatter(this IServiceCollection services,
        Action<ControllerBasedJsonInputFormatter> configureFormatter)
    {
        if(configureFormatter == null)
        {
            throw new ArgumentNullException(nameof(configureFormatter));
        }
        services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
        return services.ConfigureOptions<ControllerBasedJsonInputFormatterMvcOptionsSetup>()
                       .PostConfigure<MvcOptions>(o => {
                           var jsonInputFormatter = o.InputFormatters.OfType<ControllerBasedJsonInputFormatter>().FirstOrDefault();
                           if(jsonInputFormatter != null)
                           {
                               configureFormatter(jsonInputFormatter);
                           }
                       });
    }
}

//This attribute is used as a marker to decorate any controllers 
//or actions that you want to apply your custom input formatter
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class UseSnakeCaseJsonInputFormatterAttribute : Attribute
{
}

最后是一个示例配置代码:

//inside Startup.ConfigureServices
services.AddControllerBasedJsonInputFormatter(formatter => {
            formatter.ForControllersWithAttribute<UseSnakeCaseJsonInputFormatterAttribute>()
                     .ForActionsWithAttribute<UseSnakeCaseJsonInputFormatterAttribute>()
                     .WithSerializerSettingsConfigurer(settings => {
                        var contractResolver = settings.ContractResolver as DefaultContractResolver ?? new DefaultContractResolver();
                        contractResolver.NamingStrategy = new SnakeCaseNamingStrategy();
                        settings.ContractResolver = contractResolver;
                     });
        });

UseSnakeCaseJsonInputFormatterAttribute现在,您可以在要应用蛇形 json 输入格式化程序的任何控制器(或操作方法)上使用标记属性,如下所示:

[UseSnakeCaseJsonInputFormatter]
public class YourController : Controller {
     //...
}

请注意,上面的代码使用asp.net core 2.2,对于asp.net core 3.0+,您可以替换JsonInputFormatterwithNewtonsoftJsonInputFormatterMvcJsonOptionswith MvcNewtonsoftJsonOptions


推荐阅读