簡體   English   中英

更改單個 ASP.NET 內核 controller 的 JSON 反序列化/序列化策略

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

我有一個 controller 用於第三方 API,它使用蛇形案例命名約定。 我的控制器的 rest 用於我自己的應用程序,它使用駝峰式 JSON 約定。 我想在 controller 中為 API 自動反序列化和序列化我的模型。 這個問題解釋了如何在整個應用程序中為 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. 如何將傳入的 JSON 也正確反序列化為我的模型? 我知道我可以在 model 屬性名稱上使用[JsonPropertyName] ,但我更希望能夠在 controller 級別而不是在 Z20F35E630DAF44DBFA4C3F68F539D 級別進行設置。

您問題中共享鏈接上的解決方案可以用於序列化(由IOutputFormatter實現),盡管我們可能有另一種方法,通過其他可擴展點對其進行擴展。

在這里,我想重點關注缺失的方向(由IInputFormatter實現的反序列化方向)。 您可以實現自定義IModelBinder ,但它需要您重新實現BodyModelBinderBodyModelBinderProvider ,這並不容易。 除非您接受克隆它們的所有源代碼並修改您想要的方式。 這對可維護性和更新框架更改的內容不是很友好。

研究了源碼后,我發現要找到一個可以根據不同的控制器(或動作)自定義反序列化行為的點並不容易。 基本上,默認實現對 json 使用一次性 init IInputFormatter (默認為JsonInputFormatter core < 3.0 的 JsonInputFormatter)。 鏈中將共享一個JsonSerializerSettings實例。 在您的場景中,實際上您需要該設置的多個實例(對於每個 controller 或操作)。 我認為最簡單的一點是自定義IInputFormatter (擴展默認的JsonInputFormatter )。 當默認實現將ObjectPool用於JsonSerializer的實例(與JsonSerializerSettings相關聯)時,它變得更加復雜。 要遵循這種池化對象的方式(以獲得更好的性能),您需要一個 object 池列表(我們將在此處使用字典),而不是共享JsonSerializer以及關聯的JsonSerializerSettings池的一個 object 池(由默認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;
                     });
        });

現在,您可以在要應用蛇形案例 json 輸入格式化程序的任何控制器(或操作方法)上使用標記屬性UseSnakeCaseJsonInputFormatterAttribute ,如下所示:

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

請注意,上面的代碼使用asp.net core 2.2 ,對於asp.net core 3.0+ ,您可以將JsonInputFormatter替換為NewtonsoftJsonInputFormatter ,將MvcJsonOptions替換為MvcNewtonsoftJsonOptions

我會返回帶有 json 的Content ,並使用所需的設置進行序列化:

var serializeOptions = new JsonSerializerOptions
{
    ...
};

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

對於多種方法,我會創建一個輔助方法。

警告:這不起作用

我一直在嘗試創建一個屬性,該屬性將重新讀取請求的正文並返回錯誤的結果:

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

但是,在我的 .NET Core 3.1 應用程序中, body始終是一個空字符串。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM