Freigeben über


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 von modernen clientseitigen Frameworks und Entwicklungsworkflows
  • Ein cloudfähiges auf der Umgebung basierendes Konfigurationssystem
  • Integrierte Abhängigkeitsinjektion
  • Eine schlanke, leistungsstarke und modulare HTTP-Anforderungspipeline
  • Möglichkeit zum Hosten auf Kestrel, IIS, HTTP.sys, Nginx, Apache und Docker.
  • Parallele Versionsverwaltung
  • Tools zum Vereinfachen einer modernen Webentwicklung

In diesem Artikel wird beschrieben, wie Web-API Parameter bindet und wie Sie den Bindungsprozess anpassen können. Wenn Web-API eine Methode auf einem Controller aufruft, muss sie Werte für die Parameter festlegen, ein Prozess, der als Bindung bezeichnet wird.

Standardmäßig verwendet die Web-API die folgenden Regeln zum Binden von Parametern:

  • Wenn der Parameter ein "einfacher" Typ ist, versucht Die Web-API, den Wert aus dem URI abzurufen. Einfache Typen umfassen die .NET-Grundtypen (int, bool, double usw.), sowie TimeSpan, DateTime, Guid, decimal und string sowie 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 aus dem Nachrichtentext mithilfe eines Medienformatierers zu lesen.

Hier ist 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 Elementparameter ist ein komplexer Typ, sodass die Web-API einen Medienformatierer verwendet, um den Wert aus dem Anforderungstext zu lesen.

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

Im restlichen Artikel zeige ich, wie Sie den Modellbindungsprozess anpassen können. Bei komplexen Typen sollten Sie jedoch nach Möglichkeit Medienformatierer verwenden. Ein Schlüsselprinzip von HTTP ist, dass Ressourcen im Nachrichtentext gesendet werden, wobei inhaltsverhandlung verwendet wird, um die Darstellung der Ressource anzugeben. Für genau diesen Zweck wurden Medienformatierer entwickelt.

Verwenden von [FromUri]

Um die Web-API zum Lesen eines komplexen Typs aus dem URI zu erzwingen, fügen Sie dem Parameter das Attribut [FromUri] hinzu. Im folgenden Beispiel wird ein GeoPoint Typ sowie eine Controllermethode definiert, die den GeoPoint 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 Werte für Breiten- und Längengrad in die Abfragezeichenfolge einfügen, und die Web-API verwendet diese zum Erstellen einer GeoPoint. Zum Beispiel:

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

Verwenden von [FromBody]

Um die Web-API zum Lesen eines einfachen Typs aus dem Anforderungstext zu erzwingen, fügen Sie dem Parameter das Attribut [FromBody] hinzu:

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

In diesem Beispiel verwendet die Web-API einen Medienformatierer, um den Wert des Namens aus dem Anforderungstext zu lesen. Hier ist ein Beispiel für eine 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 [FromBody] aufweist, verwendet die Web-API den Inhaltstypheader, um einen Formatierer auszuwählen. In diesem Beispiel ist der Inhaltstyp "application/json", und der Anforderungstext ist eine unformatierte JSON-Zeichenfolge (nicht ein JSON-Objekt).

Höchstens ein Parameter darf aus dem Nachrichtentext gelesen werden. 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 in einem nicht gepufferten Datenstrom gespeichert werden kann, der nur einmal gelesen werden kann.

Typkonverter

Sie können web-API eine Klasse als einfachen Typ behandeln (sodass 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 wird. Die GeoPoint Klasse ist mit einem [TypeConverter] -Attribut versehen, um den Typkonverter anzugeben. (Dieses Beispiel wurde von Mike Stalls Blogbeitrag inspiriertSo wird's ausgeführt: Binden an benutzerdefinierte Objekte in Aktionssignaturen in 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);
    }
}

Die Web-API wird nun als einfacher Typ behandelt GeoPoint , d. h. sie versucht, Parameter aus dem URI zu binden GeoPoint . [FromUri] muss nicht in den Parameter eingeschlossen werden.

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 besteht darin, einen benutzerdefinierten Modellbinder zu erstellen. Mit einem Modellordner haben Sie Zugriff auf Elemente wie die HTTP-Anforderung, die Aktionsbeschreibung und die Rohwerte aus den Routendaten.

Implementieren Sie zum Erstellen eines Modellordners die IModelBinder-Schnittstelle . Diese Schnittstelle definiert eine einzelne Methode, BindModel:

bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);

Hier ist ein Modellordner 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 Modellordner ruft unformatierte Eingabewerte von einem Wertanbieter ab. Dieses Design trennt zwei unterschiedliche Funktionen:

  • Der Wertanbieter verwendet die HTTP-Anforderung und füllt ein Wörterbuch mit Schlüsselwertpaaren auf.
  • Der Modellordner 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üsselwertpaare:

  • 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 Modellordner sucht nach einem Schlüssel mit diesem Wert im Wörterbuch. Wenn der Wert vorhanden ist und in einen GeoPointkonvertiert werden kann, weist der Modellordner der ModelBindingContext.Model-Eigenschaft den gebundenen Wert zu.

Beachten Sie, dass der Modellordner nicht auf eine einfache Typkonvertierung beschränkt ist. In diesem Beispiel sucht der Modellordner zuerst in einer Tabelle bekannter Speicherorte, und wenn dies fehlschlägt, wird die Typkonvertierung verwendet.

Festlegen des Modellordners

Es gibt mehrere Möglichkeiten zum Festlegen eines Modellordners. Zuerst 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 Modellordner für alle Parameter dieses Typs.

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

Schließlich können Sie dem HttpConfiguration-Objekt einen Modellordneranbieter hinzufügen. Ein Modellbinderanbieter ist einfach eine Factoryklasse, die einen Modellbinder erstellt. Sie können einen Anbieter erstellen, indem Sie von der ModelBinderProvider-Klasse abgeleitet werden. Wenn ihr Modellordner 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 der Web-API mitzuteilen, dass sie einen Modellordner und keinen Medientypformatierer verwenden soll. Jetzt müssen Sie jedoch nicht den Typ des Modellordners im Attribut angeben:

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

Wertanbieter

Ich habe erwähnt, dass ein Modellbinder Werte von einem Wertanbieter abruft. Implementieren Sie zum Schreiben eines benutzerdefinierten Wertanbieters die IValueProvider-Schnittstelle . Hier ist 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 die Wertanbieterfactory wie folgt zur HttpConfiguration hinzu.

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

    // ...
}

Die Web-API verfasst alle Wertanbieter. Wenn also ein Modellbinder ValueProvider.GetValue aufruft, empfängt der Modellordner den Wert vom ersten Wertanbieter, der ihn erzeugen kann.

Alternativ können Sie die Wertanbieterfactory auf Parameterebene mithilfe des ValueProvider-Attributs wie folgt festlegen:

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

Dies weist die Web-API an, die Modellbindung mit der angegebenen Wertanbieterfactory zu verwenden und keine der anderen registrierten Wertanbieter zu verwenden.

HttpParameterBinding

Modellordner sind eine bestimmte Instanz eines allgemeineren Mechanismus. Wenn Sie das [ModelBinder] -Attribut betrachten, sehen Sie, dass es von der abstrakten ParameterBindingAttribute-Klasse abgeleitet wird. 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 die Bindung eines Parameters an einen Wert verantwortlich. Im Fall von [ModelBinder] gibt das Attribut eine HttpParameterBinding-Implementierung zurück, die eine IModelBinder verwendet, um die tatsächliche Bindung auszuführen. Sie können auch Eigene HttpParameterBinding implementieren.

Angenommen, Sie möchten ETags aus if-match und if-none-match Kopfzeilen 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 Aufzählung, um anzugeben, ob das ETag aus der if-match Kopfzeile oder dem if-none-match Header abgerufen werden soll.

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

Hier ist eine HttpParameterBinding , die 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 in dieser Methode den gebundenen Parameterwert dem ActionArgument-Wörterbuch im HttpActionContext hinzu.

Hinweis

Wenn Die ExecuteBindingAsync-Methode den Textkörper der Anforderungsnachricht liest, überschreiben Sie die WillReadBody-Eigenschaft , um "true" zurückzugeben. Der Anforderungstext kann ein nicht gepufferter Datenstrom sein, der nur einmal gelesen werden kann, sodass die Web-API eine Regel erzwingt, die 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 ETagParameterBinding, wir definieren zwei Attribute, eine für if-match Kopfzeilen und eine für if-none-match Kopfzeilen. Beide abgeleitet von einer abstrakten Basisklasse.

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 ist 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 vom Typ (HttpParameterDescriptor ->HttpParameterBinding). Sie können z. B. eine Regel hinzufügen, die jeder ETag-Parameter für eine GET-Methode verwendetETagParameterBinding: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;
    }
});

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

IActionValueBinder

Der gesamte Parameterbindungsprozess wird durch einen austauschbaren Dienst, IActionValueBinder, gesteuert. Die Standardimplementierung von IActionValueBinder führt folgende Aktionen aus:

  1. Suchen Sie nach einem ParameterBindingAttribute für den Parameter. Dazu gehören [FromBody], [FromUri] und [ModelBinder] oder benutzerdefinierte Attribute.

  2. Suchen Sie andernfalls in "HttpConfiguration.ParameterBindingRules" nach einer Funktion, die einen Nicht-Null-HttpParameterBinding-Wert zurückgibt.

  3. Verwenden Sie andernfalls die zuvor beschriebenen Standardregeln.

    • Wenn der Parametertyp "einfach" ist oder über einen Typkonverter verfügt, binden Sie von der URI. 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önnten Sie den gesamten IActionValueBinder-Dienst durch eine benutzerdefinierte Implementierung ersetzen.

Weitere Ressourcen

Beispiel für benutzerdefinierte Parameterbindung

Mike Stall schrieb eine gute Reihe von Blogbeiträgen zur Web-API-Parameterbindung: