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

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

  • ASP.NET Core je opensourcová multiplatformní architektura pro vytváření moderních cloudových webových aplikací ve Windows, macOS a Linuxu.
  • Kontrolery ASP.NET Core MVC a kontrolery webového rozhraní API jsou sjednocené.
  • Navržena tak, aby byla testovatelnost.
  • Schopnost vyvíjet a spouštět v systémech Windows, macOS a Linux.
  • 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 Kestrelu, 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 váže parametry a jak můžete proces vazby přizpůsobit. Když webové rozhraní API volá metodu v 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:

  • Pokud je parametr typu "jednoduchý", webové rozhraní API se pokusí získat hodnotu z identifikátoru URI. Mezi jednoduché typy patří primitivní typy .NET (int, bool, double atd.) a TimeSpan, DateTime, Guid, decimal a string, plus libovolný typ s převaděčem typů, který dokáže převést z řetězce. (Další informace o převaděčích typů najdete později.)
  • U složitých typů se webové rozhraní API pokusí přečíst hodnotu z textu zprávy pomocí formátovače typu média.

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 komplexní typ, takže webové rozhraní API používá formátovací modul typu média ke čtení hodnoty z textu požadavku.

Pokud chcete získat hodnotu z identifikátoru URI, webové rozhraní API vyhledá data trasy a řetězec dotazu URI. Data trasy se naplní, když směrovací systém analyzuje identifikátor URI a přiřadí ho k trase. Další informace najdete v tématu Směrování a výběr akcí.

Ve zbývající části tohoto článku vám ukážu, 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 protokolu HTTP je, že se prostředky odesílají v textu zprávy a k určení reprezentace prostředku se používají vyjednávání obsahu. Právě k tomuto účelu byly navrženy formátovací moduly typů médií.

Použití [FromUri]

Pokud chcete vynutit, aby webové rozhraní API četlo komplexní typ 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á GeoPoint z identifikátoru 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 do řetězce dotazu vložit hodnoty zeměpisné šířky a délky a webové rozhraní API je použije k vytvoření GeoPoint. Příklad:

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

Použití [FromBody]

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

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

V tomto příkladu použije webové rozhraní API formátovací modul typu média ke čtení hodnoty názvu z textu požadavku. 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 (ne objekt JSON).

Nejvýše jeden parametr může číst z textu zprávy. Takže to nebude fungovat:

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

Důvodem tohoto pravidla je, že text požadavku může být uložen v datovém proudu bez vyrovnávací paměti, který lze přečíst pouze jednou.

Převaděče typů

Webové rozhraní API můžete nastavit, aby s třídou zacházeli jako s jednoduchým typem (aby se ho webové rozhraní API pokusilo vytvořit vazbu z identifikátoru URI) vytvořením TypeConverteru 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í řetězce na GeoPoint instance. Třída GeoPoint je opatřena atributem [TypeConverter] pro určení převaděče typů. (Tento příklad byl inspirovaný blogovým příspěvkem Mikea Stalla How to bind to custom objects in action signatures in MVC/WebAPI(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 teď bude zacházet jako s GeoPoint jednoduchým typem, což znamená, že se pokusí vytvořit vazbu GeoPoint parametrů z identifikátoru URI. V parametru nemusíte zahrnovat [FromUri].

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

Klient může vyvolat metodu pomocí identifikátoru URI, jako je tento:

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 z dat trasy.

Chcete-li vytvořit vazač modelu, implementujte rozhraní IModelBinder . Toto rozhraní definuje jednu metodu , BindModel:

bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);

Tady je pořadač modelu 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ě různé funkce:

  • Zprostředkovatel hodnoty převezme požadavek HTTP a naplní slovník dvojic klíč-hodnota.
  • Pořadač modelu používá tento slovník k naplnění modelu.

Výchozí zprostředkovatel hodnot ve webovém rozhraní API získá hodnoty z dat trasy a řetězce dotazu. Pokud je http://localhost/api/values/1?location=48,-122například identifikátor URI , zprostředkovatel hodnoty 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 pro vazbu je uložen v ModelBindingContext.ModelName vlastnost. Pořadač modelu vyhledá klíč s touto hodnotou ve slovníku. Pokud hodnota existuje a lze ji převést na GeoPoint, pořadač 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č modelu nejprve hledá tabulku známých umístění, a pokud se to nezdaří, použije převod typu.

Nastavení pořadače modelů

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

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

K 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 modelů je jednoduše třída továrny, která vytváří pořadač modelů. Můžete vytvořit zprostředkovatele odvozením z ModelBinderProvider třídy. Pokud však váš pořadač modelu zpracovává jeden typ, je jednodušší použít předdefinovaný SimpleModelBinderProvider, který je určen pro tento účel. Následující kód ukazuje, jak to udělat.

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 musíte do parametru přidat atribut [ModelBinder], abyste webovému rozhraní API řekli, že by mělo používat pořadač modelu, a ne formátovací typ média. Teď ale nemusíte zadávat typ pořadače modelu v atributu :

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. Chcete-li napsat vlastní zprostředkovatele hodnoty, implementujte rozhraní IValueProvider . Tady je příklad, který načítá hodnoty ze 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;
    }
}

Je také nutné vytvořit objekt pro zprostředkovatele hodnot odvozením z ValueProviderFactory třídy.

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

Následujícím způsobem přidejte objekt pro zprostředkovatele hodnot do HttpConfiguration .

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

    // ...
}

Webové rozhraní API sestavuje všechny zprostředkovatele hodnot, takže když vazač modelu volá ValueProvider.GetValue, pořadač modelu obdrží hodnotu od prvního zprostředkovatele hodnoty, který je schopen ji vytvořit.

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

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

To webovému rozhraní API říká, aby používalo vazbu modelu se zadaným poskytovatelem hodnot a nepoužít žádného jiného zprostředkovatele registrovaných 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í Třídy ParameterBindingAttribute . Tato třída definuje jednu metodu GetBinding, která vrací objekt HttpParameterBinding :

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

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

Předpokládejme například, že chcete získat značky ETag z if-match a if-none-match hlaviček v požadavku. 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 se má značka ETag získat z hlavičky if-match , nebo z hlavičky if-none-match .

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

Tady je HttpParameterBinding , který získá ETag z požadované hlavičky a vytvoří vazbu na parametr 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 hodnotu vázaného parametru do slovníku ActionArgument v HttpActionContext.

Poznámka

Pokud metoda ExecuteBindingAsync přečte text zprávy požadavku, přepište WillReadBody vlastnost tak, aby vrátila hodnotu true. Tělo požadavku může být datový proud bez vyrovnávacího souboru, který se dá přečíst jenom jednou, takže webové rozhraní API vynucuje pravidlo, které maximálně jedna vazba může číst text zprávy.

Chcete-li použít vlastní HttpParameterBinding, můžete definovat atribut, který je odvozen z ParameterBindingAttribute. Pro ETagParameterBindingdefinujeme dva atributy, jeden pro if-match hlavičky a druhý 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)
    {
    }
}

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

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

Kromě ParametrBindingAttribute existuje další háček pro přidání vlastní httpParameterBinding. U objektu HttpConfiguration je vlastnost ParameterBindingRules kolekce anonymních funkcí typu (HttpParameterDescriptor ->HttpParameterBinding). Můžete například přidat pravidlo, které jakýkoli 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 měla vrátit null parametry, u kterých není vazba použitelná.

IActionValueBinder

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

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

  2. V opačném případě vyhledejte v HttpConfiguration.ParameterBindingRules funkci, která vrací httpParameterBinding bez null.

  3. V opačném případě 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 odpovídá vložení atributu [FromUri] do parametru.
    • V opačném případě zkuste přečíst parametr z textu zprávy. To odpovídá vložení [FromBody] do parametru.

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

Další materiály

Ukázka vazby vlastních parametrů

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