Share via


Parameterbindung in ASP.NET-Web-API

Erwägen Sie die Verwendung ASP.NET Core Web-API. Es hat die folgenden Vorteile gegenüber ASP.NET 4.x-Web-API:

  • ASP.NET Core ist ein plattformübergreifendes Open-Source-Framework zum Erstellen moderner, cloudbasierter Web-Apps unter Windows, macOS und Linux.
  • Die ASP.NET Core MVC-Controller und Web-API-Controller sind vereinheitlicht.
  • Für Testfähigkeit entwickelt.
  • Fähigkeit zur Entwicklung und Ausführung unter Windows, macOS und Linux.
  • Open Source und mit Fokus auf der Community
  • Integration moderner, clientseitiger Frameworks und Entwicklungsworkflows.
  • Ein cloudfähiges, umgebungsbasiertes Konfigurationssystem.
  • Integrierte Abhängigkeitsinjektion.
  • Eine schlanke, leistungsstarke und modulare HTTP-Anforderungspipeline
  • Möglichkeit zum Hosten auf Kestrel, IIS, HTTP.sys, Nginx, Apache und Docker.
  • Paralleles Versioning.
  • Tools zum Vereinfachen einer modernen Webentwicklung

In diesem Artikel wird beschrieben, wie die Web-API Parameter bindet und wie Sie den Bindungsprozess anpassen können. Wenn die Web-API eine Methode auf einem Controller aufruft, müssen Werte für die Parameter festgelegt werden, ein Prozess namens Bindung.

Standardmäßig verwendet die Web-API die folgenden Regeln, um Parameter zu binden:

  • Wenn der Parameter ein "einfacher" Typ ist, versucht die Web-API, den Wert aus dem URI abzurufen. Einfache Typen umfassen die primitiven .NET-Typen (int, bool, double usw.) sowie TimeSpan, DateTime, Guid, decimal und stringsowie jeden Typ mit einem Typkonverter, der aus einer Zeichenfolge konvertiert werden kann. (Weitere Informationen zu Typkonvertern später.)
  • Bei komplexen Typen versucht die Web-API, den Wert mithilfe eines Medientypformatierers aus dem Nachrichtentext zu lesen.

Hier sehen Sie beispielsweise eine typische Web-API-Controllermethode:

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

Der id-Parameter ist ein "einfacher" Typ, sodass die Web-API versucht, den Wert aus dem Anforderungs-URI abzurufen. Der Item-Parameter ist ein komplexer Typ, sodass die Web-API einen Medientypformatierer verwendet, um den Wert aus dem Anforderungstext zu lesen.

Um einen Wert aus dem URI abzurufen, sucht die Web-API die Routendaten und die URI-Abfragezeichenfolge. Die Routendaten werden aufgefüllt, wenn das Routingsystem den URI analysiert und mit einer Route abgleicht. Weitere Informationen finden Sie unter Routing und Aktionsauswahl.

Im Weiteren Verlauf dieses Artikels wird gezeigt, wie Sie den Prozess der Modellbindung anpassen können. Für komplexe Typen sollten Sie jedoch nach Möglichkeit Medienformatierer verwenden. Ein Schlüsselprinzip von HTTP ist, dass Ressourcen im Nachrichtentext gesendet werden, wobei die Inhaltsverhandlung verwendet wird, um die Darstellung der Ressource anzugeben. Medienformatierer wurden genau für diesen Zweck entwickelt.

Verwenden von [FromUri]

Um zu erzwingen, dass die Web-API einen komplexen Typ aus dem URI liest, fügen Sie dem Parameter das Attribut [FromUri] hinzu. Im folgenden Beispiel wird ein GeoPoint Typ zusammen mit einer Controllermethode definiert, die den GeoPoint vom URI abruft.

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

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

Der Client kann die Breiten- und Längengradwerte in die Abfragezeichenfolge einfügen, und die Web-API verwendet sie zum Erstellen eines GeoPoint. Zum Beispiel:

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

Verwenden von [FromBody]

Um zu erzwingen, dass die Web-API einen einfachen Typ aus dem Anforderungstext liest, fügen Sie dem Parameter das Attribut [FromBody] hinzu:

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

In diesem Beispiel verwendet die Web-API einen Medientypformatierer, um den Wert des Namens aus dem Anforderungstext zu lesen. Hier sehen Sie eine Beispiel-Clientanforderung.

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

"Alice"

Wenn ein Parameter über [FromBody] verfügt, verwendet die Web-API den Content-Type-Header, um einen Formatierer auszuwählen. In diesem Beispiel lautet der Inhaltstyp "application/json", und der Anforderungstext ist eine unformatierte JSON-Zeichenfolge (kein JSON-Objekt).

Höchstens ein Parameter darf aus dem Nachrichtentext lesen. Dies funktioniert also nicht:

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

Der Grund für diese Regel ist, dass der Anforderungstext möglicherweise in einem nicht gepufferten Stream gespeichert wird, der nur einmal gelesen werden kann.

Typkonverter

Sie können festlegen, dass die Web-API eine Klasse als einfachen Typ behandelt (damit die Web-API versucht, sie vom URI zu binden), indem Sie einen TypeConverter erstellen und eine Zeichenfolgenkonvertierung bereitstellen.

Der folgende Code zeigt eine GeoPoint Klasse, die einen geografischen Punkt darstellt, sowie einen TypeConverter , der von Zeichenfolgen in GeoPoint Instanzen konvertiert. Die GeoPoint Klasse ist mit einem [TypeConverter]- Attribut versehen, um den Typkonverter anzugeben. (Dieses Beispiel wurde von Mike Stalls Blogbeitrag So binden Sie an benutzerdefinierte Objekte in Aktionssignaturen in MVC/WebAPI inspiriert.)

[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);
    }
}

Jetzt behandelt GeoPoint die Web-API als einfachen Typ, was bedeutet, dass sie versucht, Parameter aus dem URI zu binden GeoPoint . Sie müssen [FromUri] nicht in den Parameter einschließen.

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

Der Client kann die -Methode mit einem URI wie folgt aufrufen:

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

Modellbinder

Eine flexiblere Option als ein Typkonverter ist das Erstellen eines benutzerdefinierten Modellbinders. Mit einem Modellbinder haben Sie Zugriff auf Dinge wie die HTTP-Anforderung, die Aktionsbeschreibung und die Rohwerte aus den Routendaten.

Um einen Modellbinder zu erstellen, implementieren Sie die IModelBinder-Schnittstelle . Diese Schnittstelle definiert eine einzelne Methode, BindModel:

bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);

Hier ist ein Modellbinder für GeoPoint Objekte.

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;
    }
}

Ein Modellbinder ruft rohe Eingabewerte von einem Wertanbieter ab. Dieser Entwurf trennt zwei unterschiedliche Funktionen:

  • Der Wertanbieter übernimmt die HTTP-Anforderung und füllt ein Wörterbuch mit Schlüssel-Wert-Paaren auf.
  • Der Modellbinder verwendet dieses Wörterbuch, um das Modell aufzufüllen.

Der Standardwertanbieter in der Web-API ruft Werte aus den Routendaten und der Abfragezeichenfolge ab. Wenn der URI beispielsweise lautet http://localhost/api/values/1?location=48,-122, erstellt der Wertanbieter die folgenden Schlüssel-Wert-Paare:

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

(Ich gehe davon aus, dass die Standardroutenvorlage "api/{controller}/{id}" lautet.)

Der Name des zu bindenden Parameters wird in der ModelBindingContext.ModelName-Eigenschaft gespeichert. Der Modellbinder sucht im Wörterbuch nach einem Schlüssel mit diesem Wert. Wenn der Wert vorhanden ist und in einen GeoPointkonvertiert werden kann, weist der Modellbinder den gebundenen Wert der ModelBindingContext.Model-Eigenschaft zu.

Beachten Sie, dass der Modellbinder nicht auf eine einfache Typkonvertierung beschränkt ist. In diesem Beispiel sucht der Modellbinder zuerst in einer Tabelle mit bekannten Speicherorten, und wenn dies fehlschlägt, verwendet er die Typkonvertierung.

Festlegen des Modellbinders

Es gibt mehrere Möglichkeiten, einen Modellbinder festzulegen. Zunächst können Sie dem Parameter ein [ModelBinder] -Attribut hinzufügen.

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

Sie können dem Typ auch ein [ModelBinder] -Attribut hinzufügen. Die Web-API verwendet den angegebenen Modellbinder für alle Parameter dieses Typs.

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

Schließlich können Sie der HttpConfiguration einen Modellbinderanbieter hinzufügen. Ein Modellbinderanbieter ist einfach eine Factoryklasse, die einen Modellbinder erstellt. Sie können einen Anbieter erstellen, indem Sie von der ModelBinderProvider-Klasse ableiten. Wenn Ihr Modellbinder jedoch einen einzelnen Typ verarbeitet, ist es einfacher, den integrierten SimpleModelBinderProvider zu verwenden, der für diesen Zweck konzipiert ist. Dies wird im folgenden Code veranschaulicht.

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);

        // ...
    }
}

Bei einem Modellbindungsanbieter müssen Sie dem Parameter weiterhin das [ModelBinder] -Attribut hinzufügen, um die Web-API anweisen zu können, dass sie einen Modellbinder und keinen Medientypformatierer verwenden soll. Jetzt müssen Sie jedoch nicht mehr den Typ des Modellbinders im -Attribut angeben:

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

Wertanbieter

Ich habe erwähnt, dass ein Modellbinder Werte von einem Wertanbieter abruft. Um einen benutzerdefinierten Wertanbieter zu schreiben, implementieren Sie die IValueProvider-Schnittstelle . Hier sehen Sie ein Beispiel, das Werte aus den Cookies in der Anforderung abruft:

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;
    }
}

Sie müssen auch eine Wertanbieterfactory erstellen, indem Sie von der ValueProviderFactory-Klasse abgeleitet werden.

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

Fügen Sie der HttpConfiguration die Wertanbieterfactory wie folgt hinzu.

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

    // ...
}

Die Web-API besteht aus allen Wertanbietern. Wenn also ein Modellbinder ValueProvider.GetValue aufruft, empfängt der Modellbinder den Wert vom ersten Wertanbieter, der ihn erzeugen kann.

Alternativ können Sie die Wertanbieterfactory auf Parameterebene festlegen, indem Sie das ValueProvider-Attribut wie folgt verwenden:

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

Dadurch wird die Web-API aufgefordert, die Modellbindung mit der angegebenen Wertanbieterfactory und keinen der anderen registrierten Wertanbieter zu verwenden.

HttpParameterBinding

Modellbinder sind ein spezifischer instance eines allgemeineren Mechanismus. Wenn Sie sich das [ModelBinder] -Attribut ansehen, sehen Sie, dass es von der abstrakten ParameterBindingAttribute-Klasse abgeleitet ist. Diese Klasse definiert eine einzelne Methode, GetBinding, die ein HttpParameterBinding-Objekt zurückgibt:

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

Ein HttpParameterBinding ist für das Binden eines Parameters an einen Wert verantwortlich. Im Fall von [ModelBinder] gibt das Attribut eine HttpParameterBinding-Implementierung zurück, die einen IModelBinder verwendet, um die tatsächliche Bindung auszuführen. Sie können auch Ihr eigenes HttpParameterBinding implementieren.

Angenommen, Sie möchten ETags von if-match und if-none-match Headern in der Anforderung abrufen. Zunächst definieren wir eine Klasse, die ETags darstellt.

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

Außerdem definieren wir eine Enumeration, um anzugeben, ob das ETag aus dem if-match Header oder dem if-none-match Header abgerufen werden soll.

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

Hier ist ein HttpParameterBinding , das das ETag aus dem gewünschten Header abruft und an einen Parameter vom Typ ETag bindet:

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;
    }
}

Die ExecuteBindingAsync-Methode übernimmt die Bindung. Fügen Sie innerhalb dieser Methode den gebundenen Parameterwert dem ActionArgument-Wörterbuch im HttpActionContext hinzu.

Hinweis

Wenn Ihre ExecuteBindingAsync-Methode den Text der Anforderungsnachricht liest, überschreiben Sie die WillReadBody-Eigenschaft , um true zurückzugeben. Der Anforderungstext kann ein nicht gepufferter Stream sein, der nur einmal gelesen werden kann, sodass die Web-API eine Regel erzwingt, dass höchstens eine Bindung den Nachrichtentext lesen kann.

Um ein benutzerdefiniertes HttpParameterBinding anzuwenden, können Sie ein Attribut definieren, das von ParameterBindingAttribute abgeleitet ist. Für ETagParameterBindingdefinieren wir zwei Attribute, eines für if-match Header und eines für if-none-match Header. Beide stammen von einer abstrakten Basisklasse ab.

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)
    {
    }
}

Hier sehen Sie eine Controllermethode, die das [IfNoneMatch] -Attribut verwendet.

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

Neben ParameterBindingAttribute gibt es einen weiteren Hook zum Hinzufügen eines benutzerdefinierten HttpParameterBinding. Im HttpConfiguration-Objekt ist die ParameterBindingRules-Eigenschaft eine Auflistung anonymer Funktionen des Typs (HttpParameterDescriptor ->HttpParameterBinding). Sie können beispielsweise eine Regel hinzufügen, die jeder ETag-Parameter für eine GET-Methode mit if-none-matchverwendetETagParameterBinding:

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

Die Funktion sollte für Parameter zurückgegeben null werden, bei denen die Bindung nicht anwendbar ist.

IActionValueBinder

Der gesamte Prozess der Parameterbindung wird durch den pluggablen Dienst IActionValueBinder gesteuert. Die Standardimplementierung von IActionValueBinder führt folgendes aus:

  1. Suchen Sie nach einem ParameterBindingAttribute für den Parameter. Dies umfasst [FromBody], [FromUri] und [ModelBinder] oder benutzerdefinierte Attribute.

  2. Andernfalls suchen Sie in HttpConfiguration.ParameterBindingRules nach einer Funktion, die eine HttpParameterBinding-Eigenschaft mit ungleich null zurückgibt.

  3. Verwenden Sie andernfalls die Standardregeln, die ich zuvor beschrieben habe.

    • Wenn der Parametertyp "einfach" ist oder über einen Typkonverter verfügt, binden Sie vom URI aus. Dies entspricht dem Einfügen des [FromUri] -Attributs für den Parameter.
    • Versuchen Sie andernfalls, den Parameter aus dem Nachrichtentext zu lesen. Dies entspricht dem Einfügen von [FromBody] für den Parameter.

Wenn Sie möchten, können Sie den gesamten IActionValueBinder-Dienst durch eine benutzerdefinierte Implementierung ersetzen.

Zusätzliche Ressourcen

Beispiel für benutzerdefinierte Parameterbindung

Mike Stall hat eine reihe von Blogbeiträgen über die Web-API-Parameterbindung geschrieben: