Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Рассмотрите возможность использования веб-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, decimal и string, а также любой тип с конвертером, который может преобразовывать из строки. (Дополнительные сведения о преобразователях типов позже.)
- Для сложных типов Web 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]
Чтобы заставить Web API прочитать простой тип из тела запроса, добавьте атрибут [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"
- координаты = "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] к параметру, чтобы сообщить Web 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 (Привязка параметра HTTP)
Связывание моделей — это частный случай более общего механизма. При просмотре атрибута [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. Тело запроса может быть небуферизованным потоком, который может быть прочитан только один раз, поэтому Web 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). Например, вы могли бы добавить правило, что любой параметр ETag в методе GET используется с ETagParameterBinding и 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 по умолчанию выполняет следующие действия.
Найдите ParameterBindingAttribute у параметра. К ним относятся [FromBody], [FromUri] и [ModelBinder], или пользовательские атрибуты.
В противном случае найдите функцию HttpConfiguration.ParameterBindingRules , возвращающую непустую функцию HttpParameterBinding.
В противном случае используйте правила по умолчанию, описанные ранее.
- Если тип параметра является "простым" или имеет преобразователь типов, следует привязывать из URI. Это эквивалентно размещению атрибута [FromUri] в параметре.
- В противном случае попробуйте считывать параметр из текста сообщения. Это эквивалентно размещению [FromBody] в параметре.
Если вы хотите, можно заменить всю службу IActionValueBinder пользовательской реализацией.
Дополнительные ресурсы
Пример пользовательской привязки параметров
Майк Стойл написал хороший ряд записей блога о привязке параметров веб-API: