Присоедините JsonConverter к одному действию в контроллере - PullRequest
1 голос
/ 27 мая 2020

Итак, у меня есть контроллер 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>())

Но это а) утомительно и б) если я зарегистрирую их целую кучу, может ли это не повлиять на производительность? Так есть ли более простой способ? Есть ли способ прикрепить конвертер к действию , который будет использоваться только для этого конкретного действия? Возможно, что-то концептуально эквивалентное:

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

Что явно не работает, потому что вы можете использовать JsonConverterAttribute в методе.

И если чего-то подобного еще не существует, где вы должны подключиться к конвейеру, чтобы перехватить вызов действия и вставить преобразователь, прежде чем он попытается десериализовать параметры?

1 Ответ

0 голосов
/ 27 мая 2020

Итак, покопавшись некоторое время, я придумал следующее:

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, вероятно, лучше было бы клонировать все настройки оттуда и просто добавить дополнительные преобразователи, которые мне нужны / нужны. Или, может быть, это просто побеждает весь смысл.

Кроме того, вероятно, есть некоторая проверка ошибок, которую я должен сделать, но в качестве первого наброска, чтобы посмотреть, можно ли сделать, работает в принципе.

...