【问题标题】:Attach a JsonConverter to a single action in a controller将 JsonConverter 附加到控制器中的单个操作
【发布时间】:2020-09-14 15:41:45
【问题描述】:

所以我有一个 WebAPI 控制器,其操作将接口作为参数,例如:

[HttpPost]
public IFoo RunBar(IBar parameters)
{
    //...
}

当然,这不会按原样运行,因为它不知道如何将IBar 反序列化为具体类,因此parameters 始终是null

我有一个转换器来处理这个问题,它被称为ConcreteTypeConverter<Bar,IBar>,它基本上只是将任何IBar 反序列化为具体类Bar,如果我在全球范围内注册它,我可以使它工作,例如:

config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new ConcreteTypeConverter<Bar,IBar>())

但这是 a) 乏味和 b) 如果我注册一大堆这些,可能不会对性能产生影响吗?那么有没有更简单的方法呢?有没有办法将转换器附加到 action 以仅用于 this 特定操作?也许在概念上等同于:

[JsonConverter(typeof(ConcreteTypeConverter<Bar, IBar >))]
[HttpPost]
public IFoo RunBar(IBar parameters)
{
   //...
}

这显然不起作用,因为您可以在方法上使用JsonConverterAttribute

如果这样的东西尚不存在,您必须在哪里连接到管道以拦截对操作的调用并在转换器尝试反序列化参数之前插入转换器?

【问题讨论】:

  • 这样做有充分的理由吗?好像您在这里与 WebAPI 作斗争。为什么不简单地使用具体类(无论如何实现接口)作为操作方法的参数,这样默认绑定器就可以发挥它的魔力?

标签: c# asp.net-web-api json.net


【解决方案1】:

所以在闲逛了一段时间后,我想出了这个:

public class JsonConverterBindingAttribute : ParameterBindingAttribute
{
    public IEnumerable<JsonConverter> Converters { get; private set; }

    public JsonConverterBindingAttribute(params Type[] converters)
    {
        if (converters.Any(converter => !typeof(JsonConverter).IsAssignableFrom(converter)))
        {
            throw new ArgumentException($"a converter does not derive from JsonConverter");
        }
        Converters = converters.Select(converter => (JsonConverter)Activator.CreateInstance(converter));
    }

    public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
    {
        if (parameter == null)
            throw new ArgumentException("Invalid parameter");

        return new JsonConverterParameterBinding(parameter, Converters.ToArray());
    }
}

public class JsonConverterParameterBinding : HttpParameterBinding
{
    public JsonConverter[] Converters { get; private set; }

    public JsonConverterParameterBinding(HttpParameterDescriptor parameter, JsonConverter[] converter) : base(parameter)
    {
        Converters = converter;
    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, 
        HttpActionContext actionContext, 
        CancellationToken cancellationToken)
    {
        var binding = actionContext
            .ActionDescriptor
            .ActionBinding;

        var type = binding
            .ParameterBindings[0]
            .Descriptor.ParameterType;

        var existingConverters = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters;

        return actionContext.Request.Content
            .ReadAsStringAsync()
            .ContinueWith(t =>
            {
                var str = t.Result;
                var obj = JsonConvert.DeserializeObject(str, type, existingConverters.Concat(Converters).ToArray());
                SetValue(actionContext, obj);
            });           
    }

    public override bool WillReadBody => true;
}

可以这样使用:

[HttpPost]
public IFoo RunBar([JsonConverterBinding(typeof(ConcreteTypeConverter<Bar,IBar>)]IBar parameters)
{
   //...
}

这可行,但我不确定我是否 100% 喜欢这个解决方案。几点说明:

我将它设置为接受多个转换器,因为如果IBar 包含一个属性,它本身也是一个接口(我们称之为IFar),那么您也需要能够为此提供一个转换器!所以你可以这样做:

public IFoo RunBar([JsonConverterBinding(typeof(ConcreteTypeConverter<Bar,IBar>),
 typeof(ConcreteTypeConverter<Far, IFar>)]IBar parameters)

但这也有点笨拙,所以我认为能够使用您已经在全球范围内注册的任何现有转换器会很有用(因为它们在很多地方都使用过),因此我将转换器从GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters,最好从那里克隆所有设置,然后添加我需要/想要的额外转换器。或者也许这只是有点破坏了整个观点。

另外,我可能应该做一些错误检查,但作为初稿只是为了看看它是否可以完成,原则上它是有效的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-06
    • 1970-01-01
    相关资源
    最近更新 更多