c# - 更改单个 ASP.NET Core 控制器的 JSON 反序列化/序列化策略
问题描述
我有一个控制器用于第三方 API,它使用蛇形命名约定。我的其余控制器用于我自己的应用程序,它使用驼峰式 JSON 约定。我想为那个控制器中的 API 自动反序列化和序列化我的模型。这个问题解释了如何在整个应用程序中为 JSON 使用蛇形案例命名策略,但是有没有一种方法可以指定仅对单个控制器使用命名策略?
我见过更改单个 ASP.NET Core 控制器的 JSON 序列化设置,它建议使用 ActionFilter,但这仅有助于确保正确序列化传出的 JSON。如何将传入的 JSON 也正确反序列化为我的模型?我知道我可以[JsonPropertyName]
在模型属性名称上使用,但我更希望能够在控制器级别而不是模型级别进行设置。
解决方案
您问题中共享链接上的解决方案可以用于序列化(由 实现IOutputFormatter
),尽管我们可能有另一种方法,通过其他可扩展点对其进行扩展。
在这里,我想重点关注缺失的方向(由 实现的反序列化方向IInputFormatter
)。您可以实现自定义IModelBinder
,但它需要您重新实现BodyModelBinder
,BodyModelBinderProvider
这并不容易。除非您接受克隆它们的所有源代码并修改您想要的方式。这对可维护性和更新框架更改的内容不是很友好。
研究了源码后,我发现要找到一个可以根据不同的控制器(或动作)自定义反序列化行为的点并不容易。基本上,默认实现对 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+,您可以替换JsonInputFormatter
withNewtonsoftJsonInputFormatter
和MvcJsonOptions
with MvcNewtonsoftJsonOptions
。
推荐阅读
- tableau-api - 如何在 Tableau 中编写一个计算字段来计算价格标签丢失(null)且价格 <> 99 美元的产品数量?
- rabbitmq - RabbitMQ 队列中未确认的消息数
- jenkins - Jenkins:仅在选定的 SVN 修订版上触发
- asp.net - 如何在 MVC 中显示来自外键的数据块的名称
- sql - 如何将新数据从本地表插入到远程表但不复制现有数据
- javascript - 期望在reactjs上的箭头函数末尾返回一个值如何解决此错误?
- dc.js - 如何将函数应用于 DC.js 中的数组
- android - 如果它们在滚动视图的可见屏幕中,如何仅加载回收器视图的数据?
- swift - 当我尝试隐藏 SKSpriteNode 时没有响应(剧透:SKAction 计时问题)
- python - 遍历字典并使用 iterrows 将它们附加到数据框