Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Rozważ użycie internetowego interfejsu API platformy ASP.NET Core. Ma następujące zalety w porównaniu z interfejsem API sieci Web ASP.NET 4.x:
- ASP.NET Core to międzyplatformowa platforma typu open source do tworzenia nowoczesnych, opartych na chmurze aplikacji internetowych w systemach Windows, macOS i Linux.
- Kontrolery MVC i kontrolery internetowego interfejsu API platformy ASP.NET Core są ujednolicone.
- Zaprojektowano architekturę pod kątem możliwości testowania.
- Możliwość tworzenia i uruchamiania w systemach Windows, macOS i Linux.
- Open source i koncentracja na społeczności.
- Integracja nowoczesnych struktur po stronie klienta i programistycznych przepływów pracy.
- Gotowy do pracy w chmurze, oparty na środowisku system konfiguracji.
- Wbudowane wstrzykiwanie zależności.
- Uproszczony, modułowy potok żądań HTTP zapewniający wysoką wydajność.
- Możliwość hostowania na platformie Kestrel, IIS, HTTP.sys, Nginx, Apache i Docker.
- Wersjonowanie równoległe.
- Narzędzia, które upraszczają tworzenie nowoczesnych aplikacji internetowych.
W tym artykule opisano sposób powiązania parametrów internetowego interfejsu API oraz sposób dostosowywania procesu powiązania. Gdy internetowy interfejs API wywołuje metodę na kontrolerze, musi ustawić wartości parametrów, czyli proces nazywany powiązaniem.
Domyślnie internetowy interfejs API używa następujących reguł do powiązania parametrów:
- Jeśli parametr jest typem "prostym", interfejs Web API próbuje pobrać wartość z identyfikatora URI. Proste typy obejmują typy prymitywne .NET (int, bool, double itd.), plus TimeSpan, DateTime, Guid, decimal i string, a także dowolny typ z konwerterem typów, który może konwertować z stringa. (Więcej informacji na temat konwerterów typów później).
- W przypadku typów złożonych internetowy interfejs API próbuje odczytać wartość przy użyciu formattera typu mediów z treści komunikatu.
Na przykład oto typowa metoda kontrolera Web API:
HttpResponseMessage Put(int id, Product item) { ... }
Parametr id jest typem "prostym", więc internetowy interfejs API próbuje pobrać wartość z identyfikatora URI żądania. Parametr elementu jest typem złożonym, więc Web API używa formatera typu mediów do odczytywania wartości z treści żądania.
Aby uzyskać wartość z URI, Web API wyszukuje dane trasy i ciąg zapytania URI. Dane trasy są wypełniane, gdy system routingu analizuje identyfikator URI i dopasuje je do trasy. Aby uzyskać więcej informacji, zobacz Wybieranie routingu i akcji.
W pozostałej części tego artykułu pokażę, jak dostosować proces powiązania modelu. W przypadku typów złożonych należy jednak rozważyć użycie formaterów typu nośnika, jeśli jest to możliwe. Kluczową zasadą protokołu HTTP jest to, że zasoby są wysyłane w treści komunikatu przy użyciu negocjacji zawartości w celu określenia reprezentacji zasobu. Formatery typu media są zaprojektowane właśnie do tego celu.
Korzystanie z [FromUri]
Aby wymusić, żeby Web API odczytało typ złożony z identyfikatora URI, dodaj atrybut [FromUri] do parametru. W poniższym przykładzie zdefiniowano typ GeoPoint wraz z metodą kontrolera, która pobiera GeoPoint z URI.
public class GeoPoint
{
public double Latitude { get; set; }
public double Longitude { get; set; }
}
public ValuesController : ApiController
{
public HttpResponseMessage Get([FromUri] GeoPoint location) { ... }
}
Klient może umieścić wartości Latitude (Szerokość geograficzna) i Longitude (Długość geograficzna) w ciągu zapytania, a internetowy interfejs API użyje ich do utworzenia elementu GeoPoint. Na przykład:
http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989
Korzystanie z [FromBody]
Aby wymusić na interfejsie sieci Web API odczyt prostego typu z ciała żądania, dodaj atrybut [FromBody] do parametru:
public HttpResponseMessage Post([FromBody] string name) { ... }
W tym przykładzie Web API będzie używać formatera media type do odczytywania wartości nazwy z treści żądania. Oto przykładowe żądanie klienta.
POST http://localhost:5076/api/values HTTP/1.1
User-Agent: Fiddler
Host: localhost:5076
Content-Type: application/json
Content-Length: 7
"Alice"
Jeśli parametr ma [FromBody], Web API używa nagłówka Content-Type do wyboru formatowania. W tym przykładzie typ zawartości to "application/json", a treść żądania jest nieprzetworzonym ciągiem JSON (a nie obiektem JSON).
W treści komunikatu można odczytać co najwyżej jeden parametr. Nie będzie to więc działać:
// Caution: Will not work!
public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }
Przyczyną tej reguły jest to, że treść żądania może być przechowywana w strumieniu niebuforowym, który może być odczytywany tylko raz.
Konwertery typów
Web API może traktować klasę jako typ prosty (dzięki czemu Web API spróbuje powiązać go z URI), tworząc TypeConverter i udostępniając konwersję ciągu.
Poniższy kod przedstawia klasę GeoPoint reprezentującą punkt geograficzny oraz typeConverter , który konwertuje ciągi na GeoPoint wystąpienia. Klasa GeoPoint jest ozdobiona atrybutem [TypeConverter], aby określić konwerter typów. (Ten przykład został zainspirowany wpisem w blogu Mike'a Stalla Jak powiązać z obiektami niestandardowymi w podpisach akcji w wzorcach 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);
}
}
Teraz Web API będzie traktować GeoPoint jako prosty typ, co oznacza, że spróbuje powiązać GeoPoint parametry z URI. Nie musisz dodawać [FromUri] do parametru.
public HttpResponseMessage Get(GeoPoint location) { ... }
Klient może wywołać metodę za pomocą identyfikatora URI, przykładowo w ten sposób:
http://localhost/api/values/?location=47.678558,-122.130989
Powiązania modelu
Bardziej elastyczną opcją niż konwerter typów jest utworzenie niestandardowego powiązania modelu. W przypadku powiązania modelu masz dostęp do takich elementów jak żądanie HTTP, opis akcji i nieprzetworzone wartości z danych trasy.
Aby utworzyć wiązanie modelu, zaimplementuj interfejs IModelBinder. Ten interfejs definiuje jedną metodę BindModel:
bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);
Oto powiązanie modelu dla GeoPoint obiektów.
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;
}
}
Model binder pobiera nieprzetworzone wartości wejściowe od providera wartości. Ten projekt oddziela dwie odrębne funkcje:
- Dostawca wartości pobiera żądanie HTTP i wypełnia słownik par klucz-wartość.
- Wiążący model używa tego słownika do uzupełnienia modelu.
Domyślny dostawca wartości w internetowym interfejsie API pobiera wartości z danych trasy i ciągu zapytania. Jeśli na przykład identyfikator URI to http://localhost/api/values/1?location=48,-122, dostawca wartości tworzy następujące pary klucz-wartość:
- id = "1"
- lokalizacja = "48,-122"
(Zakładam, że domyślny szablon trasy to "api/{controller}/{id}".
Nazwa parametru do powiązania jest przechowywana we właściwości ModelBindingContext.ModelName . Binder modelu szuka klucza z tą wartością w słowniku. Jeśli wartość istnieje i może zostać przekonwertowana na GeoPoint, powiązanie modelu przypisuje powiązaną wartość do właściwości ModelBindingContext.Model.
Zwróć uwagę, że wiązanie modelu nie ogranicza się do prostej konwersji typów. W tym przykładzie wiążący model najpierw wyszukuje w tabeli znanych lokalizacji, a w przypadku niepowodzenia używa konwersji typów.
Ustawianie wiązania modelu
Istnieje kilka sposobów konfigurowania wiązania modelu. Najpierw można dodać atrybut [ModelBinder] do parametru .
public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)
Do typu można również dodać atrybut [ModelBinder]. Internetowy interfejs API użyje określonego powiązania modelu dla wszystkich parametrów tego typu.
[ModelBinder(typeof(GeoPointModelBinder))]
public class GeoPoint
{
// ....
}
Na koniec możesz dodać dostawcę powiązań modeli do konfiguracji HttpConfiguration. Dostawca binderów modelu to po prostu klasa fabryczna, która tworzy binder modelu. Dostawcę można utworzyć, wyprowadzając go z klasy ModelBinderProvider . Jeśli jednak powiązanie modelu obsługuje pojedynczy typ, łatwiej jest użyć wbudowanego elementu SimpleModelBinderProvider, który jest przeznaczony do tego celu. Poniższy kod pokazuje, jak to zrobić.
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);
// ...
}
}
Przy użyciu dostawcy wiązania modelu nadal trzeba dodać atrybut [ModelBinder] do parametru, aby poinformować Web API, że powinien używać wiązania modelu, a nie formatyzera typu mediów. Jednak teraz nie musisz określać typu wiążnika modelu w atrybucie.
public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }
Dostawcy wartości
Wspomniałem, że wiązarka modelu pobiera wartości z dostawcy wartości. Aby napisać niestandardowego dostawcę wartości, zaimplementuj interfejs IValueProvider . Oto przykład, który pobiera wartości z plików cookie w żądaniu:
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;
}
}
Należy również utworzyć fabrykę dostawcy wartości, wyprowadzając z klasy ValueProviderFactory .
public class CookieValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(HttpActionContext actionContext)
{
return new CookieValueProvider(actionContext);
}
}
Dodaj fabrykę dostawcy wartości do HttpConfiguration w następujący sposób.
public static void Register(HttpConfiguration config)
{
config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory());
// ...
}
Internetowy interfejs API łączy wszystkich dostawców wartości, więc gdy model binder wywołuje ValueProvider.GetValue, model binder otrzymuje wartość od pierwszego dostawcy wartości, który jest w stanie ją dostarczyć.
Alternatywnie możesz ustawić fabrykę dostawcy wartości na poziomie parametru przy użyciu atrybutu ValueProvider w następujący sposób:
public HttpResponseMessage Get(
[ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location)
Informuje to, że interfejs API sieci Web używa powiązania modelu z określoną fabryką dostawcy wartości i nie używa żadnego z innych zarejestrowanych dostawców wartości.
HttpParameterBinding
Powiązania modelu to konkretne wystąpienie bardziej ogólnego mechanizmu. Jeśli spojrzysz na atrybut [ModelBinder], zobaczysz, że pochodzi on z klasy abstract ParameterBindingAttribute . Ta klasa definiuje pojedynczą metodę GetBinding, która zwraca obiekt HttpParameterBinding :
public abstract class ParameterBindingAttribute : Attribute
{
public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter);
}
Parametr HttpParameterBinding jest odpowiedzialny za powiązanie parametru z wartością. W przypadku elementu [ModelBinder]atrybut zwraca implementację HttpParameterBinding , która używa elementu IModelBinder do wykonania rzeczywistego powiązania. Możesz również zaimplementować własny HttpParameterBinding.
Załóżmy na przykład, że chcesz pobrać ETagi z nagłówków if-match i if-none-match w żądaniu. Zaczniemy od zdefiniowania klasy do reprezentowania ETagów.
public class ETag
{
public string Tag { get; set; }
}
Zdefiniujemy również wyliczenie wskazujące, czy element ETag ma być pobierany z nagłówka if-match , czy nagłówka if-none-match .
public enum ETagMatch
{
IfMatch,
IfNoneMatch
}
Oto element HttpParameterBinding , który pobiera element ETag z żądanego nagłówka i wiąże go z parametrem typu 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;
}
}
Metoda ExecuteBindingAsync wykonuje powiązanie. W ramach tej metody dodaj wartość powiązanego parametru do słownika ActionArgument w kontekście HttpActionContext.
Uwaga
Jeśli metoda ExecuteBindingAsync odczytuje treść komunikatu żądania, należy przesłonić właściwość WillReadBody, aby zwracała wartość true. Treść żądania może być strumieniem niebuforowanym, który może być odczytywany tylko raz, więc internetowy interfejs API wymusza regułę, że co najwyżej jedno powiązanie może odczytać treść komunikatu.
Aby zastosować niestandardowy parametr HttpParameterBinding, można zdefiniować atrybut pochodzący z parametru ParameterBindingAttribute. W przypadku ETagParameterBinding zdefiniujemy dwa atrybuty: jeden dla if-match nagłówków i jeden dla if-none-match nagłówków. Oba pochodzą z abstrakcyjnej klasy bazowej.
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)
{
}
}
Oto metoda kontrolera, która używa atrybutu [IfNoneMatch] .
public HttpResponseMessage Get([IfNoneMatch] ETag etag) { ... }
Oprócz ParameterBindingAttribute istnieje inny mechanizm do dodawania niestandardowego HttpParameterBinding. Na obiekcie HttpConfiguration właściwość ParameterBindingRules jest kolekcją anonimowych funkcji typu (HttpParameterDescriptor ->HttpParameterBinding). Można na przykład dodać regułę, zgodnie z którą dowolny parametr ETag w metodzie GET używa ETagParameterBinding z 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;
}
});
Funkcja powinna zwracać null dla parametrów, w których powiązanie nie ma zastosowania.
IActionValueBinder
Cały proces powiązania parametrów jest kontrolowany przez usługę podłączalną IActionValueBinder. Domyślna implementacja elementu IActionValueBinder wykonuje następujące czynności:
Wyszukaj parametr ParameterBindingAttribute w parametrze . Obejmuje to [FromBody], [FromUri], [ModelBinder] lub atrybuty niestandardowe.
W przeciwnym razie wyszukaj ciąg HttpConfiguration.ParameterBindingRules dla funkcji zwracającej parametr HttpParameterBinding o wartości innej niż null.
W przeciwnym razie użyj reguł domyślnych opisanych wcześniej.
- Jeśli typ parametru to "simple" lub ma konwerter typów, powiąż z identyfikatora URI. Jest to równoważne umieszczeniu atrybutu [FromUri] dla parametru .
- W przeciwnym razie spróbuj odczytać parametr z treści komunikatu. Jest to równoważne umieszczeniu parametru [FromBody] w parametrze .
Jeśli chcesz, możesz zastąpić całą usługę IActionValueBinder niestandardową implementacją.
Dodatkowe zasoby
Przykład powiązania parametrów niestandardowych
Mike Stall napisał dobrą serię wpisów w blogu dotyczących powiązania parametrów internetowego interfejsu API:
- Jak internetowy interfejs API wykonuje powiązanie parametrów
- Powiązanie parametrów stylu MVC dla internetowego interfejsu API
- Jak powiązać obiekty niestandardowe w podpisach metod akcji w MVC/Web API
- Jak utworzyć niestandardowego dostawcę wartości w internetowym interfejsie API
- Powiązanie parametru Web API od kuchni