ASP.NET Web API'sinde Parametre Bağlama

ASP.NET Core web API'lerini kullanmayı göz önünde bulundurun. ASP.NET 4.x Web API'sine göre aşağıdaki avantajlara sahiptir:

  • ASP.NET Core, Windows, macOS ve Linux üzerinde modern, bulut tabanlı web uygulamaları oluşturmaya yönelik açık kaynaklı, platformlar arası bir çerçevedir.
  • ASP.NET Core MVC denetleyicileri ve web API denetleyicileri birleştirilir.
  • Test edilebilirlik için tasarlanmış.
  • Windows, macOS ve Linux üzerinde geliştirme ve çalıştırma olanağı.
  • Açık kaynak ve topluluk odaklı.
  • Modern istemci tarafı çerçeveler ile geliştirme iş akışlarının tümleştirilmesi.
  • Bulutta kullanıma hazır, ortam tabanlı bir yapılandırma sistemi.
  • Yerleşik bağımlılık ekleme.
  • Basit, yüksek performanslı ve modüler bir HTTP istek işlem hattı.
  • Kestrel, IIS, HTTP.sys, Nginx, Apache ve Docker üzerinde barındırma olanağı.
  • Yan yana sürüm oluşturma.
  • Modern web geliştirmeyi basitleştiren araçlar.

Bu makalede, Web API'sinin parametreleri nasıl bağladığı ve bağlama işlemini nasıl özelleştirebileceğiniz açıklanır. Web API'sinin denetleyicide bir yöntemi çağırması, bağlama adlı bir işlem olan parametreler için değerler ayarlaması gerekir.

Varsayılan olarak, Web API parametreleri bağlamak için aşağıdaki kuralları kullanır:

  • Parametre "basit" bir türse, Web API değeri URI'den almaya çalışır. Basit türler .NET temel türleri (int, bool, double vb.) ile TimeSpan, DateTime, Guid, ondalık ve dizeninyanı sıra bir dizeden dönüştürülebilen tür dönüştürücüsü olan herhangi bir türü içerir. (Daha sonra tür dönüştürücüleri hakkında daha fazla bilgi.)
  • Karmaşık türler için Web API'si, medya türü biçimlendirici kullanarak ileti gövdesinden değeri okumaya çalışır.

Örneğin, tipik bir Web API denetleyicisi yöntemi aşağıda verilmiştir:

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

Id parametresi "basit" bir tür olduğundan, Web API'si istek URI'sinden değeri almaya çalışır. Öğe parametresi karmaşık bir tür olduğundan, Web API'sinde istek gövdesinden değeri okumak için medya türü biçimlendirici kullanılır.

URI'den bir değer almak için Web API'si yol verilerine ve URI sorgu dizesine bakar. Yönlendirme sistemi URI'yi ayrıştırdığında ve bir rotayla eşleştirdiğinde yol verileri doldurulur. Daha fazla bilgi için bkz . Yönlendirme ve Eylem Seçimi.

Bu makalenin geri kalanında model bağlama işlemini nasıl özelleştirebileceğinizi göstereceğim. Ancak karmaşık türler için mümkün olduğunda medya türü biçimlendiricileri kullanmayı göz önünde bulundurun. HTTP'nin temel ilkelerinden biri, kaynakların ileti gövdesinde, kaynağın gösterimini belirtmek için içerik anlaşması kullanılarak gönderilmesidir. Medya türü biçimlendiriciler tam olarak bu amaçla tasarlanmıştır.

[FromUri] kullanma

Web API'sini URI'den karmaşık bir türü okumaya zorlamak için parametresine [FromUri] özniteliğini ekleyin. Aşağıdaki örnek, URI'den öğesini alan GeoPoint bir denetleyici yöntemiyle birlikte bir GeoPoint türü tanımlar.

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

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

İstemci Enlem ve Boylam değerlerini sorgu dizesine koyabilir ve Web API'si bunları kullanarak bir GeoPointoluşturur. Örnek:

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

[FromBody] kullanma

Web API'sini istek gövdesinden basit bir türü okumaya zorlamak için parametresine [FromBody] özniteliğini ekleyin:

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

Bu örnekte, Web API'sinde istek gövdesinden ad değerini okumak için medya türü bir biçimlendirici kullanılır. Aşağıda örnek bir istemci isteği verilmiştir.

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

"Alice"

Bir parametre [FromBody] olduğunda, Web API bir biçimlendirici seçmek için content-Type üst bilgisini kullanır. Bu örnekte, içerik türü "application/json" ve istek gövdesi ham bir JSON dizesidir (JSON nesnesi değildir).

En fazla bir parametrenin ileti gövdesinden okumasına izin verilir. Bu nedenle bu işe yaramaz:

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

Bu kuralın nedeni, istek gövdesinin yalnızca bir kez okunabilen arabelleğe alınamayan bir akışta depolanabilmesidir.

Tür Dönüştürücüleri

TypeConverter oluşturup dize dönüştürmesi sağlayarak Web API'sinin bir sınıfı basit bir tür olarak (Web API'sinin URI'den bağlamaya çalışması için) yapmasını sağlayabilirsiniz.

Aşağıdaki kod, bir coğrafi noktayı temsil eden bir GeoPoint sınıfın yanı sıra dizelerden örneklere dönüştüren bir TypeConverter'ıGeoPoint gösterir. sınıfı GeoPoint , tür dönüştürücüsünü belirtmek için bir [TypeConverter] özniteliğiyle dekore edilmiştir. (Bu örnekte, Mike Stall'ın MVC/WebAPI'de eylem imzalarındaki özel nesnelere bağlanma blog gönderisi ilham alınmıştı.)

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

Şimdi Web API'sinin basit bir tür olarak ele GeoPoint alınacağı, yani URI'den parametreleri bağlamaya GeoPoint çalışacağı anlamına gelir. Parametresine [FromUri] eklemeniz gerekmez.

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

İstemci, yöntemini aşağıdaki gibi bir URI ile çağırabilir:

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

Model Bağlayıcıları

Tür dönüştürücüden daha esnek bir seçenek, özel model bağlayıcısı oluşturmaktır. Model bağlayıcısı ile HTTP isteği, eylem açıklaması ve yol verilerindeki ham değerler gibi şeylere erişebilirsiniz.

Model bağlayıcısı oluşturmak için IModelBinder arabirimini uygulayın. Bu arabirim, BindModel adlı tek bir yöntemi tanımlar:

bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);

Nesneler için GeoPoint bir model bağlayıcısı aşağıdadır.

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

Model bağlayıcısı, bir değer sağlayıcısından ham giriş değerlerini alır. Bu tasarım iki farklı işlevi ayırır:

  • Değer sağlayıcısı HTTP isteğini alır ve anahtar-değer çiftlerinden oluşan bir sözlüğü doldurur.
  • Model bağlayıcısı, modeli doldurmak için bu sözlüğü kullanır.

Web API'sindeki varsayılan değer sağlayıcısı, rota verilerinden ve sorgu dizesinden değerleri alır. Örneğin, URI ise http://localhost/api/values/1?location=48,-122, değer sağlayıcısı aşağıdaki anahtar-değer çiftlerini oluşturur:

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

(Varsayılan yol şablonunun "api/{controller}/{id}" olduğunu varsayıyorum.)

Bağlanacak parametrenin adı ModelBindingContext.ModelName özelliğinde depolanır. Model bağlayıcısı sözlükte bu değere sahip bir anahtar arar. Değer varsa ve içine GeoPointdönüştürülebiliyorsa, model bağlayıcısı bağlı değeri ModelBindingContext.Model özelliğine atar.

Model bağlayıcısının basit bir tür dönüştürme ile sınırlı olmadığını fark edin. Bu örnekte, model bağlayıcısı önce bilinen konumlardan oluşan bir tabloya bakar ve bu başarısız olursa tür dönüştürmeyi kullanır.

Model Bağlayıcısı'nı ayarlama

Model bağlayıcısı ayarlamanın çeşitli yolları vardır. İlk olarak, parametresine bir [ModelBinder] özniteliği ekleyebilirsiniz.

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

Türüne [ModelBinder] özniteliği de ekleyebilirsiniz. Web API'sinde bu türdeki tüm parametreler için belirtilen model bağlayıcı kullanılır.

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

Son olarak, HttpConfiguration'a bir model bağlayıcı sağlayıcısı ekleyebilirsiniz. Model bağlayıcı sağlayıcısı, yalnızca model bağlayıcısı oluşturan bir fabrika sınıfıdır. ModelBinderProvider sınıfından türeterek bir sağlayıcı oluşturabilirsiniz. Ancak, model bağlayıcınız tek bir türü işlerse, bu amaçla tasarlanan yerleşik SimpleModelBinderProvider'ı kullanmak daha kolaydır. Aşağıdaki kod bunun nasıl yapılacağını gösterir.

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

        // ...
    }
}

Model bağlama sağlayıcısıyla, Web API'sine medya türü biçimlendirici değil model bağlayıcısı kullanması gerektiğini bildirmek için parametresine [ModelBinder] özniteliğini eklemeniz gerekir. Ancak artık özniteliğinde model bağlayıcı türünü belirtmeniz gerekmez:

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

Değer Sağlayıcıları

Model bağlayıcının değer sağlayıcısından değer aldığından bahsettim. Özel bir değer sağlayıcısı yazmak için IValueProvider arabirimini uygulayın. burada, istekteki tanımlama bilgilerinden değerleri çeken bir örnek verilmiştir:

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

ValueProviderFactory sınıfından türeterek bir değer sağlayıcısı fabrikası da oluşturmanız gerekir.

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

HttpConfiguration'a aşağıdaki gibi değer sağlayıcısı fabrikasını ekleyin.

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

    // ...
}

Web API'si tüm değer sağlayıcılarını oluşturur, bu nedenle bir model bağlayıcısı ValueProvider.GetValue'yi çağırdığında, model bağlayıcısı değeri üretebilen ilk değer sağlayıcısından alır.

Alternatif olarak, ValueProvider özniteliğini kullanarak parametre düzeyinde değer sağlayıcısı fabrikasını aşağıdaki gibi ayarlayabilirsiniz:

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

Bu, Web API'sine belirtilen değer sağlayıcısı fabrikasıyla model bağlamayı kullanmasını ve diğer kayıtlı değer sağlayıcılarından hiçbirini kullanmamalarını söyler.

HttpParameterBinding

Model bağlayıcıları daha genel bir mekanizmanın belirli bir örneğidir. [ModelBinder] özniteliğine bakarsanız, bunun abstract ParameterBindingAttribute sınıfından türetildiğini görürsünüz. Bu sınıf, HttpParameterBinding nesnesi döndüren getbinding adlı tek bir yöntem tanımlar:

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

HttpParameterBinding, bir parametreyi bir değere bağlamakla sorumludur. [ModelBinder] durumunda özniteliği, gerçek bağlamayı gerçekleştirmek için IModelBinder kullanan bir HttpParameterBinding uygulaması döndürür. Kendi HttpParameterBinding'inizi de uygulayabilirsiniz.

Örneğin, istekte if-match ETag'leri ve if-none-match üst bilgileri almak istediğinizi varsayalım. ETag'leri temsil eden bir sınıf tanımlayarak başlayacağız.

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

Ayrıca üst bilgiden mi if-none-match yoksa üst bilgiden if-match mi ETag alındığını belirtmek için bir numaralandırma tanımlayacağız.

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

ETag'i istenen üst bilgiden alan ve ETag türünde bir parametreye bağlayan bir HttpParameterBinding aşağıdadır:

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

Bağlamayı ExecuteBindingAsync yöntemi yapar. Bu yöntem içinde, HttpActionContext içindeki ActionArgument sözlüğüne ilişkili parametre değerini ekleyin.

Not

ExecuteBindingAsync yönteminiz istek iletisinin gövdesini okursa WillReadBody özelliğini geçersiz kılarak true değerini döndürebilirsiniz. İstek gövdesi, yalnızca bir kez okunabilen, aralanmamış bir akış olabilir, bu nedenle Web API'sinin en fazla bir bağlamanın ileti gövdesini okuyabileceği bir kural zorlar.

Özel bir HttpParameterBinding uygulamak için ParameterBindingAttribute'tan türetilen bir öznitelik tanımlayabilirsiniz. için ETagParameterBindingbiri üst bilgiler, diğeri üst bilgiler için if-matchif-none-match iki öznitelik tanımlayacağız. Her ikisi de soyut bir temel sınıftan türetilir.

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

Özniteliğini kullanan bir denetleyici yöntemi aşağıdadır [IfNoneMatch] .

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

ParameterBindingAttribute'un yanı sıra, özel bir HttpParameterBinding eklemek için başka bir kanca vardır. HttpConfiguration nesnesinde ParameterBindingRules özelliği, (HttpParameterDescriptor ->HttpParameterBinding) türünde anonim işlevlerin koleksiyonudur. Örneğin, bir GET yöntemindeki herhangi bir ETag parametresinin ile if-none-matchkullandığı ETagParameterBinding bir kural ekleyebilirsiniz:

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

İşlev, bağlamanın geçerli olmadığı parametreler için döndürmelidir null .

IActionValueBinder

Parametre bağlama işleminin tamamı takılabilir bir hizmet olan IActionValueBinder tarafından denetlenir. IActionValueBinder'ın varsayılan uygulaması aşağıdakileri yapar:

  1. parametresinde ParameterBindingAttribute öğesini arayın. Buna [FromBody], [FromUri] ve [ModelBinder] veya özel öznitelikler dahildir.

  2. Aksi takdirde, null olmayan bir HttpParameterBinding döndüren bir işlev için HttpConfiguration.ParameterBindingRules bölümüne bakın.

  3. Aksi takdirde, daha önce açıkladığım varsayılan kuralları kullanın.

    • Parametre türü "basit" ise veya bir tür dönüştürücüsü varsa, URI'den bağlayın. Bu, [FromUri] özniteliğini parametresine yerleştirmeye eşdeğerdir.
    • Aksi takdirde, ileti gövdesinden parametresini okumayı deneyin. Bu, [ FromBody] öğesini parametresine yerleştirmeye eşdeğerdir.

İsterseniz, IActionValueBinder hizmetinin tamamını özel bir uygulamayla değiştirebilirsiniz.

Ek Kaynaklar

Özel Parametre Bağlama Örneği

Mike Stall, Web API parametre bağlaması hakkında iyi bir blog gönderisi serisi yazdı: