Поделиться через


Привязка параметров в веб-API ASP.NET

Рассмотрите возможность использования веб-API ASP.NET Core. Он имеет следующие преимущества по сравнению с ASP.NET веб-API версии 4.x:

  • ASP.NET Core — это кроссплатформенная платформа с открытым кодом для создания современных облачных веб-приложений в Windows, macOS и Linux.
  • Контроллеры ASP.NET Core MVC и контроллеры веб-API унифицированы.
  • Разработано для тестируемости.
  • Возможность разработки и запуска в ОС Windows, macOS и Linux.
  • Открытый исходный код и ориентация на сообщество.
  • Интеграция современных клиентских платформ и рабочих процессов разработки.
  • Облачная система конфигурации на основе среды.
  • Встроенное введение зависимостей.
  • Упрощенный высокопроизводительный модульный конвейер HTTP-запросов.
  • Возможность размещения в Kestrel, IIS, HTTP.sys, Nginx, Apache и Docker.
  • Управление параллельными версиями.
  • Инструментарий, упрощающий процесс современной веб-разработки.

В этой статье описывается, как веб-API привязывает параметры и как можно настроить процесс привязки. Когда веб-API вызывает метод на контроллере, он должен задать значения для параметров, процесс, называемый привязкой.

По умолчанию веб-API использует следующие правила для привязки параметров:

  • Если параметр является простым типом, веб-API пытается получить значение из URI. Простые типы включают примитивы .NET (int, bool, double и т. д.), а также TimeSpan, DateTime, Guid, десятичный и строковый, а также любой тип с преобразователем типов, который может преобразовывать из строки. (Дополнительные сведения о преобразователях типов позже.)
  • Для сложных типов веб-API пытается считывать значение из текста сообщения с помощью средства форматирования типа мультимедиа.

Например, вот типичный метод контроллера веб-API:

HttpResponseMessage Put(int id, Product item) { ... }

Параметр идентификатора — это простой тип, поэтому веб-API пытается получить значение из URI запроса. Параметр элемента является сложным типом, поэтому веб-API использует модуль форматирования типа мультимедиа для чтения значения из текста запроса.

Чтобы получить значение из URI, веб-API выглядит в данных маршрута и строке запроса URI. Данные маршрута заполняются, когда система маршрутизации анализирует URI и сопоставляет его с маршрутом. Дополнительные сведения см. в разделе "Маршрутизация" и "Выбор действия".

В остальной части этой статьи мы покажем, как настроить процесс привязки модели. Однако для сложных типов рекомендуется использовать средства форматирования типов мультимедиа по возможности. Ключевой принцип HTTP заключается в том, что ресурсы отправляются в тексте сообщения, используя согласование содержимого для указания представления ресурса. Средства форматирования типов мультимедиа предназначены именно для этой цели.

Использование [FromUri]

Чтобы принудительно прочитать сложный тип веб-API из URI, добавьте атрибут [FromUri] в параметр. В следующем примере определяется GeoPoint тип, а также метод контроллера, который получает GeoPoint из URI.

public class GeoPoint
{
    public double Latitude { get; set; } 
    public double Longitude { get; set; }
}

public ValuesController : ApiController
{
    public HttpResponseMessage Get([FromUri] GeoPoint location) { ... }
}

Клиент может поместить значения широты и долготы в строку запроса, а веб-API будет использовать их для создания GeoPoint. Например:

http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989

Использование [FromBody]

Чтобы принудительно прочитать простой тип из текста запроса, добавьте атрибут [FromBody] в параметр:

public HttpResponseMessage Post([FromBody] string name) { ... }

В этом примере веб-API будет использовать метод форматирования типа мультимедиа для чтения значения имени из текста запроса. Ниже приведен пример запроса клиента.

POST http://localhost:5076/api/values HTTP/1.1
User-Agent: Fiddler
Host: localhost:5076
Content-Type: application/json
Content-Length: 7

"Alice"

Если параметр имеет [FromBody], веб-API использует заголовок Content-Type для выбора модуля форматирования. В этом примере тип контента — application/json, а текст запроса — необработанная строка JSON (а не объект JSON).

По крайней мере один параметр может читаться из текста сообщения. Поэтому это не будет работать:

// Caution: Will not work!    
public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }

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

Преобразователи типов

Веб-API можно рассматривать класс как простой тип (так что веб-API попытается привязать его из URI), создав TypeConverter и предоставив строковое преобразование.

В следующем коде показан GeoPoint класс, представляющий географическую точку, а также TypeConverter , который преобразуется из строк в GeoPoint экземпляры. Класс GeoPoint украшен атрибутом [TypeConverter] , чтобы указать преобразователь типов. (Этот пример был вдохновлен записью блога Майка СтойлаКак привязаться к пользовательским объектам в сигнатурах действий в MVC/WebAPI.)

[TypeConverter(typeof(GeoPointConverter))]
public class GeoPoint
{
    public double Latitude { get; set; } 
    public double Longitude { get; set; }

    public static bool TryParse(string s, out GeoPoint result)
    {
        result = null;

        var parts = s.Split(',');
        if (parts.Length != 2)
        {
            return false;
        }

        double latitude, longitude;
        if (double.TryParse(parts[0], out latitude) &&
            double.TryParse(parts[1], out longitude))
        {
            result = new GeoPoint() { Longitude = longitude, Latitude = latitude };
            return true;
        }
        return false;
    }
}

class GeoPointConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        if (sourceType == typeof(string))
        {
            return true;
        }
        return base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, 
        CultureInfo culture, object value)
    {
        if (value is string)
        {
            GeoPoint point;
            if (GeoPoint.TryParse((string)value, out point))
            {
                return point;
            }
        }
        return base.ConvertFrom(context, culture, value);
    }
}

Теперь веб-API будет рассматриваться GeoPoint как простой тип, то есть он попытается привязать GeoPoint параметры из URI. Не нужно включать [FromUri] в параметр.

public HttpResponseMessage Get(GeoPoint location) { ... }

Клиент может вызвать метод с помощью URI следующим образом:

http://localhost/api/values/?location=47.678558,-122.130989

Привязыватели моделей

Более гибким вариантом, чем преобразователь типов, является создание пользовательского привязчика модели. С помощью привязки модели у вас есть доступ к таким вещам, как HTTP-запрос, описание действия и необработанные значения из данных маршрута.

Чтобы создать привязку модели, реализуйте интерфейс IModelBinder . Этот интерфейс определяет один метод BindModel:

bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);

Вот привязка модели для GeoPoint объектов.

public class GeoPointModelBinder : IModelBinder
{
    // List of known locations.
    private static ConcurrentDictionary<string, GeoPoint> _locations
        = new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);

    static GeoPointModelBinder()
    {
        _locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
        _locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
        _locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
    }

    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(GeoPoint))
        {
            return false;
        }

        ValueProviderResult val = bindingContext.ValueProvider.GetValue(
            bindingContext.ModelName);
        if (val == null)
        {
            return false;
        }

        string key = val.RawValue as string;
        if (key == null)
        {
            bindingContext.ModelState.AddModelError(
                bindingContext.ModelName, "Wrong value type");
            return false;
        }

        GeoPoint result;
        if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result))
        {
            bindingContext.Model = result;
            return true;
        }

        bindingContext.ModelState.AddModelError(
            bindingContext.ModelName, "Cannot convert value to GeoPoint");
        return false;
    }
}

Привязка модели получает необработанные входные значения от поставщика значений. Эта конструкция отделяет две отдельные функции:

  • Поставщик значений принимает HTTP-запрос и заполняет словарь пар "ключ-значение".
  • Привязка модели использует этот словарь для заполнения модели.

Поставщик значений по умолчанию в веб-API получает значения из данных маршрута и строки запроса. Например, если URI имеется http://localhost/api/values/1?location=48,-122, поставщик значений создает следующие пары "ключ-значение".

  • id = "1"
  • location = "48,-122"

(Предполагается, что шаблон маршрута по умолчанию — api/{controller}/{id}".)

Имя параметра для привязки хранится в свойстве ModelBindingContext.ModelName . Привязка модели ищет ключ с этим значением в словаре. Если значение существует и может быть преобразовано в GeoPointобъект, привязка модели назначает привязанное значение свойству ModelBindingContext.Model .

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

Настройка привязки модели

Существует несколько способов настройки привязки модели. Сначала можно добавить атрибут [ModelBinder] в параметр.

public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)

Можно также добавить атрибут [ModelBinder] в тип. Веб-API будет использовать указанный объект привязки модели для всех параметров этого типа.

[ModelBinder(typeof(GeoPointModelBinder))]
public class GeoPoint
{
    // ....
}

Наконец, можно добавить поставщик привязки модели к httpConfiguration. Поставщик привязки модели — это просто класс фабрики, который создает привязку модели. Вы можете создать поставщика, исходя из класса ModelBinderProvider . Однако если привязка модели обрабатывает один тип, проще использовать встроенный SimpleModelBinderProvider, предназначенный для этой цели. В следующем примере кода показано, как это сделать:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var provider = new SimpleModelBinderProvider(
            typeof(GeoPoint), new GeoPointModelBinder());
        config.Services.Insert(typeof(ModelBinderProvider), 0, provider);

        // ...
    }
}

При использовании поставщика привязки модели необходимо добавить атрибут [ModelBinder] в параметр, чтобы сообщить веб-API, что он должен использовать привязку модели, а не средство форматирования типа мультимедиа. Но теперь не нужно указывать тип привязки модели в атрибуте:

public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }

Поставщики значений

Я упомянул, что привязка модели получает значения от поставщика значений. Чтобы написать поставщик настраиваемых значений, реализуйте интерфейс IValueProvider . Ниже приведен пример извлечения значений из файлов cookie в запросе:

public class CookieValueProvider : IValueProvider
{
    private Dictionary<string, string> _values;

    public CookieValueProvider(HttpActionContext actionContext)
    {
        if (actionContext == null)
        {
            throw new ArgumentNullException("actionContext");
        }

        _values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        foreach (var cookie in actionContext.Request.Headers.GetCookies())
        {
            foreach (CookieState state in cookie.Cookies)
            {
                _values[state.Name] = state.Value;
            }
        }
    }

    public bool ContainsPrefix(string prefix)
    {
        return _values.Keys.Contains(prefix);
    }

    public ValueProviderResult GetValue(string key)
    {
        string value;
        if (_values.TryGetValue(key, out value))
        {
            return new ValueProviderResult(value, value, CultureInfo.InvariantCulture);
        }
        return null;
    }
}

Кроме того, необходимо создать фабрику поставщика значений, исходя из класса ValueProviderFactory .

public class CookieValueProviderFactory : ValueProviderFactory
{
    public override IValueProvider GetValueProvider(HttpActionContext actionContext)
    {
        return new CookieValueProvider(actionContext);
    }
}

Добавьте фабрику поставщика значений в httpConfiguration следующим образом.

public static void Register(HttpConfiguration config)
{
    config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory());

    // ...
}

Веб-API состоит из всех поставщиков значений, поэтому при вызове привязки модели ValueProvider.GetValue средство привязки модели получает значение от первого поставщика значений, который может его создать.

Кроме того, можно задать фабрику поставщиков значений на уровне параметров с помощью атрибута ValueProvider следующим образом:

public HttpResponseMessage Get(
    [ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location)

Это указывает веб-API использовать привязку модели с указанной фабрикой поставщика значений, а не использовать ни одного из других зарегистрированных поставщиков значений.

HttpParameterBinding

Привязки моделей — это конкретный экземпляр более общего механизма. При просмотре атрибута [ModelBinder] вы увидите, что он является производным от абстрактного класса ParameterBindingAttribute . Этот класс определяет один метод GetBinding, который возвращает объект HttpParameterBinding:

public abstract class ParameterBindingAttribute : Attribute
{
    public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter);
}

HttpParameterBinding отвечает за привязку параметра к значению. В случае [ModelBinder]атрибут возвращает реализацию HttpParameterBinding , которая использует IModelBinder для выполнения фактической привязки. Вы также можете реализовать собственный httpParameterBinding.

Например, предположим, что вы хотите получить ETags из if-match и if-none-match заголовков в запросе. Начнем с определения класса для представления ETags.

public class ETag
{
    public string Tag { get; set; }
}

Мы также определим перечисление, указывающее, следует ли получить ETag из if-match заголовка или заголовка if-none-match .

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

Вот httpParameterBinding, который получает ETag из требуемого заголовка и привязывает его к параметру типа ETag:

public class ETagParameterBinding : HttpParameterBinding
{
    ETagMatch _match;

    public ETagParameterBinding(HttpParameterDescriptor parameter, ETagMatch match) 
        : base(parameter)
    {
        _match = match;
    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, 
        HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        EntityTagHeaderValue etagHeader = null;
        switch (_match)
        {
            case ETagMatch.IfNoneMatch:
                etagHeader = actionContext.Request.Headers.IfNoneMatch.FirstOrDefault();
                break;

            case ETagMatch.IfMatch:
                etagHeader = actionContext.Request.Headers.IfMatch.FirstOrDefault();
                break;
        }

        ETag etag = null;
        if (etagHeader != null)
        {
            etag = new ETag { Tag = etagHeader.Tag };
        }
        actionContext.ActionArguments[Descriptor.ParameterName] = etag;

        var tsc = new TaskCompletionSource<object>();
        tsc.SetResult(null);
        return tsc.Task;
    }
}

Метод ExecuteBindingAsync выполняет привязку. В этом методе добавьте ограничивающее значение параметра в словарь ActionArgument в HttpActionContext.

Примечание.

Если метод ExecuteBindingAsync считывает текст сообщения запроса, переопределите свойство WillReadBody, чтобы вернуть значение true. Текст запроса может быть неуправляемым потоком, который может быть прочитан только один раз, поэтому веб-API применяет правило, которое по крайней мере одна привязка может считывать текст сообщения.

Чтобы применить пользовательский httpParameterBinding, можно определить атрибут, производный от ParameterBindingAttribute. Для ETagParameterBindingэтого мы определим два атрибута, один для if-match заголовков и один для if-none-match заголовков. Оба производных от абстрактного базового класса.

public abstract class ETagMatchAttribute : ParameterBindingAttribute
{
    private ETagMatch _match;

    public ETagMatchAttribute(ETagMatch match)
    {
        _match = match;
    }

    public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
    {
        if (parameter.ParameterType == typeof(ETag))
        {
            return new ETagParameterBinding(parameter, _match);
        }
        return parameter.BindAsError("Wrong parameter type");
    }
}

public class IfMatchAttribute : ETagMatchAttribute
{
    public IfMatchAttribute()
        : base(ETagMatch.IfMatch)
    {
    }
}

public class IfNoneMatchAttribute : ETagMatchAttribute
{
    public IfNoneMatchAttribute()
        : base(ETagMatch.IfNoneMatch)
    {
    }
}

Ниже приведен метод контроллера, использующий [IfNoneMatch] атрибут.

public HttpResponseMessage Get([IfNoneMatch] ETag etag) { ... }

Помимо ParameterBindingAttribute, существует еще один перехватчик для добавления пользовательского httpParameterBinding. В объекте HttpConfiguration свойство ParameterBindingRules представляет собой коллекцию анонимных функций типа (HttpParameterDescriptor ->HttpParameterBinding). Например, можно добавить правило, в которое используется ETagParameterBinding любой параметр ETag в методе GET:if-none-match

config.ParameterBindingRules.Add(p =>
{
    if (p.ParameterType == typeof(ETag) && 
        p.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get))
    {
        return new ETagParameterBinding(p, ETagMatch.IfNoneMatch);
    }
    else
    {
        return null;
    }
});

Функция должна возвращать null параметры, в которых привязка неприменима.

IActionValueBinder

Весь процесс привязки параметров управляется подключаемой службой IActionValueBinder. Реализация IActionValueBinder по умолчанию выполняет следующие действия.

  1. Найдите параметр ParameterBindingAttribute. К ним относятся [FromBody], [FromUri] и [ModelBinder], или пользовательские атрибуты.

  2. В противном случае найдите функцию HttpConfiguration.ParameterBindingRules , возвращающую непустую функцию HttpParameterBinding.

  3. В противном случае используйте правила по умолчанию, описанные ранее.

    • Если тип параметра является "простым" или имеет преобразователь типов, привязывается из URI. Это эквивалентно размещению атрибута [FromUri] в параметре.
    • В противном случае попробуйте считывать параметр из текста сообщения. Это эквивалентно размещению [FromBody] в параметре.

Если вы хотите, можно заменить всю службу IActionValueBinder пользовательской реализацией.

Дополнительные ресурсы

Пример пользовательской привязки параметров

Майк Стойл написал хороший ряд записей блога о привязке параметров веб-API: