Powiązanie parametrów w internetowym interfejsie API ASP.NET
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.
- Przechowywanie wersji równoległych.
- 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", internetowy interfejs API próbuje pobrać wartość z identyfikatora URI. Proste typy obejmują typy pierwotne .NET (int, bool, double itd.), plus TimeSpan, DateTime, Guid, decimal i ciąg, a także dowolny typ z konwerterem typów, który może konwertować z ciągu. (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ść z treści komunikatu przy użyciu formatującego typu nośnika.
Na przykład poniżej przedstawiono typową metodę kontrolera internetowego interfejsu 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 internetowy interfejs API używa formatującego typu nośnika do odczytywania wartości z treści żądania.
Aby uzyskać wartość z identyfikatora URI, internetowy interfejs API wyszukuje dane trasy i ciąg zapytania identyfikatora 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 nośnika zostały zaprojektowane do tego celu.
Korzystanie z [FromUri]
Aby wymusić odczyt typu złożonego interfejsu API z identyfikatora URI, dodaj do parametru atrybut [FromUri]. W poniższym przykładzie zdefiniowano GeoPoint
typ wraz z metodą kontrolera, która pobiera wartość GeoPoint
z identyfikatora 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ć odczyt prostego typu interfejsu API sieci Web z treści żądania, dodaj atrybut [FromBody] do parametru:
public HttpResponseMessage Post([FromBody] string name) { ... }
W tym przykładzie internetowy interfejs API będzie używać formatującego typu nośnika 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 wartość [FromBody], internetowy interfejs API używa nagłówka Content-Type do wybrania formatującego. W tym przykładzie typ zawartości to "application/json", a treść żądania jest nieprzetworzonym ciągiem JSON (a nie obiektem JSON).
Co najwyżej jeden parametr może odczytywać z treści komunikatu. 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
Internetowy interfejs API może traktować klasę jako prosty typ (dzięki czemu internetowy interfejs API spróbuje powiązać go z identyfikatorem URI), tworząc klasę 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 StallaJak 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 internetowy interfejs API będzie traktować GeoPoint
jako prosty typ, co oznacza, że spróbuje powiązać GeoPoint
parametry z identyfikatora URI. Parametru nie trzeba dołączać do parametru [FromUri].
public HttpResponseMessage Get(GeoPoint location) { ... }
Klient może wywołać metodę za pomocą identyfikatora URI w następujący 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ć powią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;
}
}
Powiązanie modelu pobiera nieprzetworzone wartości wejściowe od dostawcy wartości. Ten projekt oddziela dwie odrębne funkcje:
- Dostawca wartości pobiera żądanie HTTP i wypełnia słownik par klucz-wartość.
- Powiązanie modelu używa tego słownika do wypeł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"
- location = "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
element , powiązanie modelu przypisuje powiązaną wartość do właściwości ModelBindingContext.Model .
Zwróć uwagę, że powiązanie modelu nie jest ograniczone do prostej konwersji typów. W tym przykładzie powiązanie modelu najpierw wyszukuje w tabeli znanych lokalizacji, a jeśli to się nie powiedzie, używa konwersji typów.
Ustawianie powiązania modelu
Istnieje kilka sposobów ustawiania powią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ązania modelu do konfiguracji HttpConfiguration. Dostawca powiązania modelu to po prostu klasa fabryki, która tworzy powiązanie 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);
// ...
}
}
W przypadku dostawcy powiązania modelu nadal trzeba dodać atrybut [ModelBinder] do parametru, aby poinformować internetowy interfejs API, że powinien używać powiązania modelu, a nie formatera typu nośnika. Ale teraz nie musisz określać typu powiązania modelu w atrybucie:
public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }
Dostawcy wartości
Wspomniałem, że powiązanie modelu pobiera wartości od 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 konfiguracji HttpConfiguration w następujący sposób.
public static void Register(HttpConfiguration config)
{
config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory());
// ...
}
Internetowy interfejs API komponuje wszystkich dostawców wartości, więc gdy binder modelu wywołuje element ValueProvider.GetValue, binder modelu odbiera wartość od pierwszego dostawcy wartości, który może go wygenerować.
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 internetowy interfejs API używa powiązania modelu z określoną fabryką dostawcy wartości, a nie do używania ż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łasną właściwość HttpParameterBinding.
Załóżmy na przykład, że chcesz pobrać elementy ETag z if-match
i if-none-match
nagłówki w żądaniu. Zaczniemy od zdefiniowania klasy do reprezentowania elementów ETag.
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 obiekcie HttpActionContext.
Uwaga
Jeśli metoda ExecuteBindingAsync odczytuje treść komunikatu żądania, przesłoń właściwość WillReadBody, aby zwrócić 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
elementu 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 parametruBindingAttribute istnieje inny punkt zaczepienia do dodawania niestandardowego elementu HttpParameterBinding. W obiekcie HttpConfiguration właściwość ParameterBindingRules jest kolekcją anonimowych funkcji typu (HttpParameterDescriptor ->HttpParameterBinding). Można na przykład dodać regułę, której używa dowolny parametr ETag w metodzie GET za pomocą if-none-match
polecenia ETagParameterBinding
:
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
parametry, 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] i [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ć z obiektami niestandardowymi w podpisach akcji w interfejsie MVC/internetowym interfejsie API
- Jak utworzyć niestandardowego dostawcę wartości w internetowym interfejsie API
- Powiązanie parametru internetowego interfejsu API pod maską