C# 更改单个ASP.NET核心控制器的JSON反序列化/序列化策略

C# 更改单个ASP.NET核心控制器的JSON反序列化/序列化策略,c#,json,asp.net-core,C#,Json,Asp.net Core,我有一个用于第三方API的控制器,它使用一个蛇壳命名约定。我的其余控制器用于我自己的应用程序,它使用camelcase JSON约定。我想在一个控制器中为API自动从/到snake case反序列化和序列化我的模型。解释如何在整个应用程序中使用JSON的蛇形命名策略,但是否有一种方法可以指定仅对单个控制器使用命名策略 我已经看到了使用ActionFilter的建议,但这仅有助于确保正确序列化传出JSON。如何将传入的JSON反序列化到我的模型中?我知道我可以在模型属性名称上使用[JsonProp

我有一个用于第三方API的控制器,它使用一个蛇壳命名约定。我的其余控制器用于我自己的应用程序,它使用camelcase JSON约定。我想在一个控制器中为API自动从/到snake case反序列化和序列化我的模型。解释如何在整个应用程序中使用JSON的蛇形命名策略,但是否有一种方法可以指定仅对单个控制器使用命名策略


我已经看到了使用ActionFilter的建议,但这仅有助于确保正确序列化传出JSON。如何将传入的JSON反序列化到我的模型中?我知道我可以在模型属性名称上使用
[JsonPropertyName]
,但我更希望能够在控制器级别而不是模型级别进行设置。

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

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

在研究了源代码之后,我发现很难找到一个基于不同控制器(或操作)定制反序列化行为的点。基本上,对于json,默认实现使用一次性init
IInputFormatter
(对于asp.net核心<3.0,默认为
JsonInputFormatter
)。链中的将共享一个
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>();
}
最后,这里是一个示例配置代码:

//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;
                     });
        });

请注意,上面的代码使用asp.net core 2.2,对于asp.net core 3.0+,您可以将
JsonInputFormatter
替换为
NewtonsoftJsonInputFormatter
,将
MVCJSONOOptions
替换为
MvcNewtonsoftJsonOptions
我将返回
内容
,并将json序列化为所需设置:

var serializeOptions = new JsonSerializerOptions
{
    ...
};

return Content(JsonSerializer.Serialize(data, options), "application/json");

对于多个方法,我会创建一个helper方法。

注意第一个答案下的注释:“自动”蛇格转换不一定成功:例如,“CPURegister”将变为“c_p___寄存器”。无论如何,我不会依赖自动字符串转换。而是使用具有适当映射方法的DTO。这样一来,发生的事情就更清楚,维护起来也更容易。您是否考虑过在控制器中编写一个私有共享方法来序列化JSON?
[UseSnakeCaseJsonInputFormatter]
public class YourController : Controller {
     //...
}
var serializeOptions = new JsonSerializerOptions
{
    ...
};

return Content(JsonSerializer.Serialize(data, options), "application/json");