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 müşteri tarafı çerçeveler ve geliştirme iş akışlarının entegrasyonu.
  • 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'da barındırabilme.
  • Yan yana sürümleme.
  • 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ğırdığında, bağlama adlı bir işlem olan parametreler için değerler ayarlaması gerekir.

Varsayılan olarak, Web API'si 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, decimal ve string ile bir dizeden dönüştürebilen 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övdesindeki 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'sinin istek gövdesindeki değeri okumak için medya türü biçimleyicisi kullanılır.

URI'den bir değer almak için Web API'si yol verilerini ve URI sorgu dizesini arar. Yönlendirme sistemi URI'yi ayrıştırdığında ve bir yolla 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ğunca 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] kullanımı

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 GeoPoint öğesini alan 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 Sorgu dizesine Enlem ve Boylam değerlerini koyabilir ve Web API'si bunları kullanarak bir GeoPointoluşturur. Örneğin:

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'si istek gövdesinden ad değerini okumak için medya türü bir biçimlendirici kullanacaktı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 İçerik Türü ü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şlem çalışmaz:

// 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 tamponlanmayan bir akışta saklanabilmesidir.

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

Web API'nin bir sınıfı basit bir tür olarak (URI'den bağlamaya çalışması için) ele almasını sağlamak amacıyla bir TypeConverter oluşturabilir ve bir dizeye dönüştürme işlemi sağlayabilirsiniz.

Aşağıdaki kod, coğrafi bir noktayı temsil eden bir GeoPoint sınıfı ve dizelerden GeoPoint örneklerine dönüştüren bir TypeConverter gösterir. sınıfı GeoPoint , tür dönüştürücüsünü belirtmek için [ TypeConverter] özniteliğiyle dekore edilmiştir. (Bu örnek, Mike Stall'ın MVC/WebAPI'de eylem imzaları içindeki özel nesnelere bağlanma konusundaki blog gönderisinden ilham alınarak yapılmıştır.)

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

Web API bundan sonra GeoPoint türünü basit bir tür olarak değerlendirecek, dolayısıyla GeoPoint parametrelerini URI'dan bağlamaya çalışacaktır. Parametresine [FromUri] eklemeniz gerekmez.

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

İstemci aşağıdaki gibi bir URI ile yöntemini ç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 verilerinden 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);

İşte GeoPoint nesneleri için bir model bağlayıcı.

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ğerleri alır. Bu tasarım iki farklı işlevi birbirinden ayırır:

  • Değer sağlayıcısı HTTP isteğini alır ve anahtar-değer çiftleri 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 bir GeoPointiçine dö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ı unutmayın. 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ısı 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ı, bir model bağlayıcısı oluşturan basit 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ç için tasarlanmış 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ında, 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ısının 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. İstekteki tanımlama bilgilerinden değerleri alan bir örnek burada 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 ayrıca bir değer sağlayıcı fabrikası 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 tüm değer sağlayıcılarını oluşturur, bu nedenle model bağlayıcısı ValueProvider.GetValue'u ç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 değer sağlayıcısı fabrikasını parametre düzeyinde 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öntemi tanımlar:

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

HttpParameterBinding, parametreyi bir değere bağlamakla sorumludur. [ModelBinder] durumunda, öznitelik, 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, if-match ve if-none-match üst bilgilerinden ETag'leri 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 ETag'in hangi if-match veya if-none-match üst bilgisinden alınacağını belirtmek için bir sabitler listesi tanımlayacağız.

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

İşte ETag'i istenen üst bilgiden alıp ETag türünde bir parametreye bağlayan bir HttpParameterBinding:

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

ExecuteBindingAsync yöntemi bağlamayı yapar. Bu yöntemde, bağlı parametre değerini HttpActionContext içindeki ActionArgument sözlüğüne ekleyin.

Not

ExecuteBindingAsync yönteminiz istek iletisinin gövdesini okursa WillReadBody özelliğini geçersiz kılarak true değerini döndürün. İstek gövdesi yalnızca bir kez okunabilen tamponlanmamış bir akış olabilir, bu nedenle Web API, ileti gövdesinin en fazla bir bağlama tarafından okunabileceği kuralını uygular.

Özel bir HttpParameterBinding uygulamak için, ParameterBindingAttribute'tan türetilen bir öznitelik tanımlayabilirsiniz. ETagParameterBinding için, biri if-match üst bilgileri, diğeri if-none-match üst bilgileri olmak üzere 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)
    {
    }
}

Aşağıda [IfNoneMatch] özniteliğini kullanan bir denetleyici yöntemi bulunmaktadır.

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

ParameterBindingAttribute'un yanı sıra, özel bir HttpParameterBinding eklemek için başka bir mekanizma vardır. HttpConfiguration nesnesinde ParameterBindingRules özelliği, (HttpParameterDescriptor ->HttpParameterBinding) türündeki anonim işlevlerin koleksiyonudur. Örneğin, GET yöntemindeki herhangi bir ETag parametresi ETagParameterBinding ile if-none-match kullandığında 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. Parametre üzerinde bir ParameterBindingAttribute arayın. Buna [FromBody], [FromUri] ve [ModelBinder] veya özel öznitelikler dahildir.

  2. Aksine, null olmayan bir HttpParameterBinding döndüren bir işlev için HttpConfiguration.ParameterBindingRules'a 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, parametreye [FromBody] koymakla 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önderileri serisi yazdı: