简体   繁体   English

更改单个 ASP.NET 内核 controller 的 JSON 反序列化/序列化策略

[英]Change the JSON deserialization/serialization policy for single ASP.NET Core controller

I have a controller that I use for a third party API, which uses a snake case naming convention.我有一个 controller 用于第三方 API,它使用蛇形案例命名约定。 The rest of my controllers are used for my own app, which uses a camelcase JSON convention.我的控制器的 rest 用于我自己的应用程序,它使用驼峰式 JSON 约定。 I'd like to automatically deserialize and serialize my models from/to snake case for the API in that one controller.我想在 controller 中为 API 自动反序列化和序列化我的模型。 This question explains how to use a snake case naming policy for JSON in the entire app, but is there a way that I can specify to use the naming policy only for that single controller? 这个问题解释了如何在整个应用程序中为 JSON 使用蛇形案例命名策略,但是有没有一种方法可以指定仅对单个 controller 使用命名策略?

I've seen Change the JSON serialization settings of a single ASP.NET Core controller which suggests using an ActionFilter, but that only helps for ensuring that outgoing JSON is serialized properly. I've seen Change the JSON serialization settings of a single ASP.NET Core controller which suggests using an ActionFilter, but that only helps for ensuring that outgoing JSON is serialized properly. How can I get the incoming JSON deserialized to my models properly as well?如何将传入的 JSON 也正确反序列化为我的模型? I know that I can use [JsonPropertyName] on the model property names but I'd prefer to be able to set something at the controller level, not at the model level.我知道我可以在 model 属性名称上使用[JsonPropertyName] ,但我更希望能够在 controller 级别而不是在 Z20F35E630DAF44DBFA4C3F68F539D 级别进行设置。

The solution on the shared link in your question is OK for serialization (implemented by IOutputFormatter ) although we may have another approach by extending it via other extensibility points.您问题中共享链接上的解决方案可以用于序列化(由IOutputFormatter实现),尽管我们可能有另一种方法,通过其他可扩展点对其进行扩展。

Here I would like to focus on the missing direction (the deserializing direction which is implemented by IInputFormatter ).在这里,我想重点关注缺失的方向(由IInputFormatter实现的反序列化方向)。 You can implement a custom IModelBinder but it requires you to reimplement the BodyModelBinder and BodyModelBinderProvider which is not easy.您可以实现自定义IModelBinder ,但它需要您重新实现BodyModelBinderBodyModelBinderProvider ,这并不容易。 Unless you accept to clone all the source code of them and modify the way you want.除非您接受克隆它们的所有源代码并修改您想要的方式。 That's not very friendly to maintainability and getting up-to-date to what changed by the framework.这对可维护性和更新框架更改的内容不是很友好。

After researching through the source code, I've found that it's not easy to find a point where you can customize the deserializing behavior based on different controllers (or actions).研究了源码后,我发现要找到一个可以根据不同的控制器(或动作)自定义反序列化行为的点并不容易。 Basically the default implementation uses a one-time init IInputFormatter for json (default by JsonInputFormatter for asp.net core < 3.0).基本上,默认实现对 json 使用一次性 init IInputFormatter (默认为JsonInputFormatter core < 3.0 的 JsonInputFormatter)。 That in chain will share one instance of JsonSerializerSettings .链中将共享一个JsonSerializerSettings实例。 In your scenario, actually you need multiple instances of that settings (for each controller or action).在您的场景中,实际上您需要该设置的多个实例(对于每个 controller 或操作)。 The easiest point I think is to customize an IInputFormatter (extending the default JsonInputFormatter ).我认为最简单的一点是自定义IInputFormatter (扩展默认的JsonInputFormatter )。 It becomes more complicated when the default implementation uses ObjectPool for the instance of JsonSerializer (which is associated with a JsonSerializerSettings ).当默认实现将ObjectPool用于JsonSerializer的实例(与JsonSerializerSettings相关联)时,它变得更加复杂。 To follow that style of pooling the objects (for better performance), you need a list of object pools (we will use a dictionary here) instead of just one object pool for the shared JsonSerializer as well as the associated JsonSerializerSettings (as implemented by the default JsonInputFormatter ).要遵循这种池化对象的方式(以获得更好的性能),您需要一个 object 池列表(我们将在此处使用字典),而不是共享JsonSerializer以及关联的JsonSerializerSettings池的一个 object 池(由默认JsonInputFormatter )。

The point here is to based on the current InputFormatterContext , you need to build the corresponding JsonSerializerSettings as well as the JsonSerializer to be used.这里的重点是基于当前的InputFormatterContext ,需要构建对应的JsonSerializerSettings以及要使用的JsonSerializer That sounds simple but once it comes to a full implementation (with fairly complete design), the code is not short at all.这听起来很简单,但是一旦涉及到一个完整的实现(具有相当完整的设计),代码一点也不短。 I've designed it into multiple classes.我把它设计成多个类。 If you really want to see it working, just be patient to copy the code carefully (of course reading it through to understand is recommended).如果你真的想看到它工作,请耐心仔细地复制代码(当然建议通读一遍理解)。 Here's all the code:这是所有代码:

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

To help conveniently configure the services to replace the default JsonInputFormatter , we have the following code:为了帮助方便地配置服务以替换默认的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
{
}

Finally here's a sample configuration code:最后是一个示例配置代码:

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

Now you can use the marker attribute UseSnakeCaseJsonInputFormatterAttribute on any controllers (or action methods) that you want to apply the snake-case json input formatter, like this:现在,您可以在要应用蛇形案例 json 输入格式化程序的任何控制器(或操作方法)上使用标记属性UseSnakeCaseJsonInputFormatterAttribute ,如下所示:

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

Note that the code above uses asp.net core 2.2 , for asp.net core 3.0+ , you can replace the JsonInputFormatter with NewtonsoftJsonInputFormatter and MvcJsonOptions with MvcNewtonsoftJsonOptions .请注意,上面的代码使用asp.net core 2.2 ,对于asp.net core 3.0+ ,您可以将JsonInputFormatter替换为NewtonsoftJsonInputFormatter ,将MvcJsonOptions替换为MvcNewtonsoftJsonOptions

I'd return Content with json serialised with desired settings:我会返回带有 json 的Content ,并使用所需的设置进行序列化:

var serializeOptions = new JsonSerializerOptions
{
    ...
};

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

For multiple methods I'd create a helper method.对于多种方法,我会创建一个辅助方法。

Warning: this doesn't work警告:这不起作用

I've been trying to create an attribute which would re-read the body of the request and return a bad result:我一直在尝试创建一个属性,该属性将重新读取请求的正文并返回错误的结果:

public class OnlyValidParametersAttribute : ActionFilterAttribute
{
    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (context.HttpContext.Request.ContentType.Contains("application/json"))
        {
            context.HttpContext.Request.EnableBuffering();

            StreamReader stream = new StreamReader(context.HttpContext.Request.Body);

            string body = await stream.ReadToEndAsync(); // Always set as "".

            try
            {
                JsonConvert.DeserializeObject(
                    body,
                    context.ActionDescriptor.Parameters.FirstOrDefault()!.ParameterType,
                    new JsonSerializerSettings
                    {
                        MissingMemberHandling = MissingMemberHandling.Error,
                    }
                );
            }
            catch (MissingMemberException e) // Unsure if this is the right exception to catch.
            {
                context.Result = new UnprocessableEntityResult();
            }

            base.OnActionExecuting(context);
        }

        await next();
    }
}

However, in my .NET Core 3.1 application, body is always an empty string.但是,在我的 .NET Core 3.1 应用程序中, body始终是一个空字符串。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 更改单个 ASP.NET Core 控制器的 JSON 序列化设置 - Change the JSON serialization settings of a single ASP.NET Core controller ASP.Net Core 中的 JSON 序列化/反序列化 - JSON serialization/deserialization in ASP.Net Core 在 ASP.NET Core 3 中更改单个 ASP.NET Core API 控制器或单个操作的 System.Text.Json 序列化选项 - Change System.Text.Json Serialization Options of a single ASP.NET Core API controller or single Action in ASP.NET Core 3 ASP.NET Core 中每个控制器的不同 JSON 反序列化设置 - Different JSON deserialization settings per controller in ASP.NET Core JSON 自定义序列化和反序列化 C# asp.net 内核中的现有类 - JSON custom serialization and deserialization of existing classes in C# asp.net core 迁移到 ASP.Net Core MVC 时 JSON 序列化/反序列化不起作用 - JSON serialization/deserialization not working when migrating to ASP.Net Core MVC 忽略ASP.NET Core 2中XML序列化/反序列化的属性 - Ignore property from XML serialization/deserialization in ASP.NET Core 2 如何为ASP.NET Core中的Controller Action覆盖默认反序列化? - How to override default deserialization for Controller Action in ASP.NET Core? ASP.NET 内核处理 JSON 反序列化问题 - ASP.NET Core handling JSON deserialization problems ASP.NET JSON反序列化 - Asp.net json deserialization
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM