C# 更改单个ASP.NET核心控制器的JSON反序列化/序列化策略
我有一个用于第三方API的控制器,它使用一个蛇壳命名约定。我的其余控制器用于我自己的应用程序,它使用camelcase JSON约定。我想在一个控制器中为API自动从/到snake case反序列化和序列化我的模型。解释如何在整个应用程序中使用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
我已经看到了使用ActionFilter的建议,但这仅有助于确保正确序列化传出JSON。如何将传入的JSON反序列化到我的模型中?我知道我可以在模型属性名称上使用
[JsonPropertyName]
,但我更希望能够在控制器级别而不是模型级别进行设置。您问题中共享链接上的解决方案可以序列化(由IOutputFormatter
实现)尽管我们可能有另一种方法,通过其他扩展点来扩展它
在这里,我想重点讨论缺少的方向(由IInputFormatter
实现的反序列化方向)。您可以实现自定义的IModelBinder
,但需要重新实现BodyModelBinder
和BodyModelBinderProvider
,这并不容易。除非您接受克隆所有源代码,并按您所需的方式进行修改。这对可维护性和更新框架所改变的内容不是很友好
在研究了源代码之后,我发现很难找到一个基于不同控制器(或操作)定制反序列化行为的点。基本上,对于json,默认实现使用一次性initIInputFormatter
(对于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");