Привязка к пользовательской модели ASP.Net Web API с отправляемыми данными x-www-form-urlencoded - ничего не работает

У меня возникли проблемы с привязкой пользовательской модели к работе при отправке данных x-www-form-urlencoded. Я пробовал все, о чем я могу думать, и ничто, кажется, не дает желаемого результата. Обратите внимание, что при отправке данных JSON, моих JsonConverters и т.д. Все работает нормально. Это когда я отправляю как x-www-form-urlencoded, что система не может понять, как связать мою модель.

Мой тестовый пример: я хотел бы привязать объект TimeZoneInfo как часть моей модели.

Здесь мое модельное связующее:

public class TimeZoneModelBinder : SystemizerModelBinder
{
    protected override object BindModel(string attemptedValue, Action<string> addModelError)
    {
        try
        {
            return TimeZoneInfo.FindSystemTimeZoneById(attemptedValue);
        }
        catch(TimeZoneNotFoundException)
        {
            addModelError("The value was not a valid time zone ID. See the GetSupportedTimeZones Api call for a list of valid time zone IDs.");
            return null;
        }
    }
}

Вот базовый класс, который я использую:

public abstract class SystemizerModelBinder : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        var name = GetModelName(bindingContext.ModelName);
        var valueProviderResult = bindingContext.ValueProvider.GetValue(name);
        if(valueProviderResult == null || string.IsNullOrWhiteSpace(valueProviderResult.AttemptedValue))
            return false;

        var success = true;
        var value = BindModel(valueProviderResult.AttemptedValue, s =>
        {
            success = false;
            bindingContext.ModelState.AddModelError(name, s);
        });
        bindingContext.Model = value;
        bindingContext.ModelState.SetModelValue(name, new System.Web.Http.ValueProviders.ValueProviderResult(value, valueProviderResult.AttemptedValue, valueProviderResult.Culture));
        return success;
    }

    private string GetModelName(string name)
    {
        var n = name.LastIndexOf(".", StringComparison.Ordinal);
        return n < 0 || n >= name.Length - 1 ? name : name.Substring(n + 1);
    }

    protected abstract object BindModel(string attemptedValue, Action<string> addModelError);
}

Я использовал базовый класс, подобный этому, чтобы упростить создание дополнительных пользовательских привязок к образцам.

Здесь моя модель поставщика связующего. Обратите внимание, что это правильно вызвано из моего контейнера IoC, поэтому я не буду пытаться показать этот аспект моего кода.

public class SystemizerModelBinderProvider : ModelBinderProvider
{
    public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
    {
        if(modelType == typeof(TimeZoneInfo))
            return new TimeZoneModelBinder();

        return null;
    }
}

Наконец, здесь метод действия и класс модели:

[DataContract)]
public class TestModel
{
    [DataMember]
    public TimeZoneInfo TimeZone { get; set; }
}

[HttpPost]
public HttpResponseMessage Test(TestModel model)
{
    return Request.CreateResponse(HttpStatusCode.OK, model);
}

Для метода действия я попытался:

public HttpResponseMessage Test([FromBody] TestModel model)

Это вызывает FormUrlEncodedMediaFormatter, который, кажется, вообще игнорирует мое собственное связующее устройство.

public HttpResponseMessage Test([ModelBinder] TestModel model)

Это вызывает мое настраиваемое связующее устройство, как и ожидалось, но тогда оно предоставляет только ValueProviders для RouteData и QueryString и по какой-то причине не предоставляет ничего для содержимого тела. См. Ниже:

Value Providers

Я также попытался украсить сам класс ModelBinder(typeof(SystemizerModelBinderProvider))

Почему привязка модели ТОЛЬКО возникает, когда я использую атрибут [ModelBinder] и почему она ТОЛЬКО пытается читать значения маршрута и запроса и игнорировать содержимое тела? Почему FromBody игнорирует моего поставщика привязки для конкретной модели?

Как создать сценарий, в котором я могу получать данные POSTED x-www-form-urlencoded и успешно связывать свойства модели с помощью пользовательской логики?

Ответ 1

Я бы порекомендовал вам прочитать following blog post, в котором Майк Столл подробно объясняет, как привязка модели работает в веб-интерфейсе API:

Существует 2 метода привязки параметров: привязка модели и Форматтеры. На практике WebAPI использует привязку модели для чтения из строку запроса и Formatters для чтения из тела.

Вот основные правила, чтобы определить, читается ли параметр с помощью привязка модели или форматировщик:

  • Если параметр не имеет атрибута на нем, то решение принимается чисто по параметрам типа .NET. "Простые типы" использует модель связывание. В сложных типах используются форматирующие элементы. "Простой тип" включает в себя: примитивы, TimeSpan, DateTime, Guid, Decimal, String или что-то еще с TypeConverter, который преобразует из строк.
  • Вы можете использовать атрибут [FromBody], чтобы указать, что параметр должен быть прочитан из тело.
  • Вы можете использовать атрибут [ModelBinder] для параметра или тип параметров, чтобы указать, что параметр должен быть привязан к модели. Этот атрибут также позволяет настраивать связующее устройство модели. [FromUri]производный экземпляр [ModelBinder], который специально настраивает модель, чтобы смотреть только в URI.
  • Тело можно читать только один раз. Поэтому, если в сигнатуре есть 2 сложных типа, по крайней мере один из них должен иметь атрибут [ModelBinder].

Итак, если источником ваших данных является тело запроса, вы можете создать собственный MediaTypeFormatter, а не привязку к модели.

Ответ 2

ModelBinder выглядит относительно лучше, чем MediaTypeFormatter. Вам не нужно регистрировать его по всему миру.

Я нашел другую альтернативу использованию связующего устройства для связывания сложных типов объектов в веб-API. В связующем модуле я читаю тело запроса как строку, а затем используя JSON.NET для десериализации его для требуемого типа объекта. Его также можно использовать для сопоставления массива сложных типов объектов.

Я добавил модельное связующее следующим образом:

public class PollRequestModelBinder : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        var body = actionContext.Request.Content.ReadAsStringAsync().Result;
        var pollRequest = JsonConvert.DeserializeObject<PollRequest>(body);
        bindingContext.Model = pollRequest;
        return true;
    }
}

И затем я использую его в контроллере Web API следующим образом:

    public async Task<PollResponse> Post(Guid instanceId, [ModelBinder(typeof(PollRequestModelBinder))]PollRequest request)
    {
       // api implementation  
    }