Sdílet prostřednictvím


Vazba parametrů ve webovém rozhraní API ASP.NET

Zvažte použití webového rozhraní API ASP.NET Core. Má následující výhody oproti ASP.NET webovému rozhraní API 4.x:

  • ASP.NET Core je opensourcová multiplatformní architektura pro vytváření moderních cloudových webových aplikací ve Windows, macOS a Linuxu.
  • Řadiče ASP.NET Core MVC a řadiče webového rozhraní API jsou sjednocené.
  • Navrženo pro testování.
  • Schopnost vyvíjet a spouštět ve Windows, macOS a Linuxu
  • Architektura zaměřená na open-source a komunitu
  • Integrace moderní architektury klienta a vývojových pracovních postupů
  • Konfigurační systém založený na prostředí, který je připravený pro cloud.
  • Integrovaná injektáž závislostí.
  • Odlehčený, vysoce výkonný, modulární kanál požadavků HTTP
  • Schopnost hostovat v Kestrel, IIS, HTTP.sys, Nginx, Apache a Dockeru.
  • Souběžná správa verzí.
  • Nabízí nástroje, které usnadňují vývoj moderních webů.

Tento článek popisuje, jak webové rozhraní API sváže parametry a jak můžete přizpůsobit proces vazby. Když webové rozhraní API volá metodu na kontroleru, musí nastavit hodnoty parametrů, proces označovaný jako vazba.

Webové rozhraní API ve výchozím nastavení používá k vytvoření vazby parametrů následující pravidla:

Tady je například typická metoda kontroleru webového rozhraní API:

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

Parametr ID je "jednoduchý" typ, takže webové rozhraní API se pokusí získat hodnotu z identifikátoru URI požadavku. Parametr položky je složitý typ, takže webové rozhraní API používá k načtení hodnoty z textu požadavku formátovací modul typu média.

Pokud chcete získat hodnotu z identifikátoru URI, webové rozhraní API hledá směrovací data a řetězec dotazu URI. Data trasy se vyplní, když směrovací systém analyzuje identifikátor URI a odpovídá tomuto identifikátoru trasy. Další informace naleznete v tématu Směrování a výběr akce.

Ve zbývající části tohoto článku vám ukážeme, jak můžete přizpůsobit proces vazby modelu. U složitých typů však zvažte použití formátovacích typů médií, kdykoli je to možné. Klíčovým principem HTTP je, že prostředky se odesílají v textu zprávy pomocí vyjednávání obsahu k určení reprezentace prostředku. Formátovací moduly typu média byly navrženy přesně pro tento účel.

Použití [FromUri]

Pokud chcete webové rozhraní API vynutit čtení komplexního typu z identifikátoru URI, přidejte do parametru atribut [FromUri ]. Následující příklad definuje GeoPoint typ spolu s metodou kontroleru, která získá z identifikátoru GeoPoint URI.

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

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

Klient může hodnoty zeměpisné šířky a délky vložit do řetězce dotazu a webového rozhraní API je použije k vytvoření GeoPoint. Příklad:

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

Použití funkce [FromBody]

Pokud chcete vynutit, aby webové rozhraní API četlo jednoduchý typ z textu požadavku, přidejte do parametru atribut [FromBody] :

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

V tomto příkladu webové rozhraní API použije k načtení hodnoty názvu z textu požadavku formátovací modul typu média. Tady je příklad požadavku klienta.

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

"Alice"

Pokud má parametr [FromBody], webové rozhraní API použije hlavičku Content-Type k výběru formátovače. V tomto příkladu je typ obsahu "application/json" a text požadavku je nezpracovaný řetězec JSON (nikoli objekt JSON).

Maximálně jeden parametr může číst z textu zprávy. Proto to nebude fungovat:

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

Důvodem tohoto pravidla je, že tělo požadavku může být uloženo v ne vyrovnávací paměti datového proudu, který lze číst pouze jednou.

Převaděče typů

Webové rozhraní API může zacházet s třídou jako s jednoduchým typem (aby se ho webové rozhraní API pokusilo vytvořit vazbu z identifikátoru URI) vytvořením typeConverter a poskytnutím převodu řetězců.

Následující kód ukazuje GeoPoint třídu, která představuje geografický bod a TypeConverter , který převádí z řetězců na GeoPoint instance. Třída GeoPoint je zdobena atributem [TypeConverter] k určení převaděče typů. (Tento příklad byl inspirovaný blogovým příspěvkem Mikea Stalla.Jak vytvořit vazbu na vlastní objekty v podpisech akcí v 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);
    }
}

Webové rozhraní API bude považovat GeoPoint za jednoduchý typ, což znamená, že se pokusí svázat GeoPoint parametry z identifikátoru URI. Do parametru není nutné zahrnout [FromUri].

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

Klient může metodu vyvolat pomocí identifikátoru URI, například takto:

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

Pořadače modelů

Flexibilnější možností než převaděč typů je vytvoření vlastního pořadače modelu. S pořadačem modelu máte přístup k věcem, jako je požadavek HTTP, popis akce a nezpracované hodnoty ze směrovacích dat.

Pokud chcete vytvořit pořadač modelů, implementujte rozhraní IModelBinder . Toto rozhraní definuje jednu metodu BindModel:

bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);

Tady je pořadač modelů pro GeoPoint objekty.

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

Pořadač modelu získá nezpracované vstupní hodnoty od zprostředkovatele hodnot. Tento návrh odděluje dvě odlišné funkce:

  • Zprostředkovatel hodnot vezme požadavek HTTP a naplní slovník párů klíč-hodnota.
  • Pořadač modelů používá tento slovník k naplnění modelu.

Výchozí zprostředkovatel hodnot ve webovém rozhraní API získá hodnoty ze směrovacích dat a řetězce dotazu. Pokud je http://localhost/api/values/1?location=48,-122například identifikátor URI, zprostředkovatel hodnot vytvoří následující páry klíč-hodnota:

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

(Předpokládám výchozí šablonu trasy, což je api/{controller}/{id}.)

Název parametru, který se má svázat, je uložen ve vlastnosti ModelBindingContext.ModelName . Pořadač modelů hledá klíč s touto hodnotou ve slovníku. Pokud hodnota existuje a lze ji převést na GeoPoint, binder modelu přiřadí vázanou hodnotu k ModelBindingContext.Model vlastnost.

Všimněte si, že pořadač modelu není omezen na jednoduchý převod typu. V tomto příkladu pořadač modelů nejprve vypadá v tabulce známých umístění a pokud selže, použije převod typu.

Nastavení pořadače modelu

Existuje několik způsobů, jak nastavit pořadač modelů. Nejprve můžete k parametru přidat atribut [ModelBinder].

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

Do typu můžete také přidat atribut [ModelBinder]. Webové rozhraní API použije zadaný pořadač modelu pro všechny parametry tohoto typu.

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

Nakonec můžete do httpConfiguration přidat zprostředkovatele pořadače modelu. Zprostředkovatel pořadače modelu je jednoduše třída továrny, která vytváří pořadač modelů. Zprostředkovatele můžete vytvořit odvozením z ModelBinderProvider třídy. Pokud však pořadač modelu zpracovává jeden typ, je jednodušší použít integrovaný SimpleModelBinderProvider, který je určený pro tento účel. Následující kód ukazuje, jak to provést.

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

        // ...
    }
}

U zprostředkovatele vazby modelu stále potřebujete k parametru přidat atribut [ModelBinder], aby webové rozhraní API řeklo, že by mělo používat pořadač modelů, a ne formátovací modul typu média. Teď ale v atributu nemusíte zadávat typ pořadače modelu:

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

Zprostředkovatelé hodnot

Zmínil jsem se, že pořadač modelu získává hodnoty od zprostředkovatele hodnot. Pokud chcete napsat vlastního zprostředkovatele hodnot, implementujte rozhraní IValueProvider . Tady je příklad, který načítá hodnoty z souborů cookie v požadavku:

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

Musíte také vytvořit objekt pro vytváření zprostředkovatele hodnot odvozením z ValueProviderFactory třídy.

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

Přidejte objekt pro vytváření zprostředkovatelů hodnot do httpConfiguration následujícím způsobem.

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

    // ...
}

Webové rozhraní API se skládá ze všech zprostředkovatelů hodnot, takže když binder modelu volá ValueProvider.GetValue, binder modelu obdrží hodnotu od prvního zprostředkovatele hodnot, který je schopen jej vytvořit.

Alternativně můžete nastavit objekt pro vytváření zprostředkovatele hodnot na úrovni parametru pomocí atributu ValueProvider následujícím způsobem:

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

To říká webovému rozhraní API, aby používalo vazbu modelu se zadanou továrnou zprostředkovatele hodnot, a ne k použití žádného jiného registrovaného zprostředkovatele hodnot.

HttpParameterBinding

Pořadače modelů jsou konkrétní instancí obecnějšího mechanismu. Pokud se podíváte na atribut [ModelBinder], uvidíte, že je odvozen z abstraktní ParameterBindingAttribute třídy. Tato třída definuje jednu metodu GetBinding, která vrací HttpParameterBinding objekt:

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

HttpParameterBinding zodpovídá za vytvoření vazby parametru na hodnotu. V případě [ModelBinder], atribut vrátí HttpParameterBinding implementace, která používá IModelBinder k provedení skutečné vazby. Můžete také implementovat vlastní HttpParameterBinding.

Předpokládejme například, že chcete v požadavku získat značky if-match ETag a if-none-match hlavičky. Začneme definováním třídy, která bude reprezentovat značky ETag.

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

Definujeme také výčet, který označuje, jestli chcete získat značku ETag z if-match hlavičky nebo hlavičky if-none-match .

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

Tady je HttpParameterBinding , který získá značku ETag z požadované hlavičky a vytvoří vazbu s 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 provede vazbu. V rámci této metody přidejte vázanou hodnotu parametru do slovníku ActionArgument v HttpActionContext.

Poznámka:

Pokud vaše ExecuteBindingAsync metoda přečte text zprávy požadavku, přepsat WillReadBody vlastnost vrátit true. Text požadavku může být nepřečtený datový proud, který se dá číst jen jednou, takže webové rozhraní API vynucuje pravidlo, které může načíst text zprávy maximálně jedna vazba.

Pokud chcete použít vlastní HttpParameterBinding, můžete definovat atribut odvozený z ParameterBindingAttribute. Pro ETagParameterBinding, budeme definovat dva atributy, jeden pro if-match hlavičky a jeden pro if-none-match hlavičky. Oba jsou odvozeny z abstraktní základní třídy.

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

Tady je metoda kontroleru, která používá [IfNoneMatch] atribut.

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

Kromě ParametrBindingAttribute je další háček pro přidání vlastní HttpParameterBinding. V HttpConfiguration objektu ParameterBindingRules vlastnost je kolekce anonymních funkcí typu (HttpParameterDescriptor ->HttpParameterBinding). Můžete například přidat pravidlo, které libovolný parametr ETag v metodě GET používá ETagParameterBinding s 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;
    }
});

Funkce by se měla vrátit null pro parametry, u kterých není vazba použitelná.

IActionValueBinder

Celý proces vazby parametrů je řízen připojitelnou službou IActionValueBinder. Výchozí implementace IActionValueBinder dělá následující:

  1. Vyhledejte parametr ParameterBindingAttribute. To zahrnuje [FromBody], [FromUri] a [ModelBinder] nebo vlastní atributy.

  2. V opačném případě vyhledejte httpConfiguration.ParameterBindingRules pro funkci, která vrací nenulovou hodnotu HttpParameterBinding.

  3. Jinak použijte výchozí pravidla, která jsem popsal dříve.

    • Pokud je typ parametru "jednoduchý" nebo má převaděč typů, vytvořte vazbu z identifikátoru URI. To je ekvivalentem vložení atributu [FromUri] na parametr.
    • V opačném případě zkuste přečíst parametr z textu zprávy. To je ekvivalentem vložení [FromBody] na parametr.

Pokud chcete, můžete nahradit celou službu IActionValueBinder vlastní implementací.

Další materiály

Ukázka vazby vlastních parametrů

Mike Stall napsal dobrou řadu blogových příspěvků o vazbě parametrů webového rozhraní API: