Bagikan melalui


Pengikatan Parameter di ASP.NET Web API

Pertimbangkan untuk menggunakan API web ASP.NET Core. Ini memiliki keuntungan berikut daripada ASP.NET 4.x Web API:

  • ASP.NET Core adalah kerangka kerja lintas platform sumber terbuka untuk membangun aplikasi web modern berbasis cloud di Windows, macOS, dan Linux.
  • Pengontrol MVC Inti ASP.NET dan pengontrol API web disatukan.
  • Dirancang untuk dapat diuji.
  • Kemampuan untuk mengembangkan dan menjalankan di Windows, macOS, dan Linux.
  • Sumber terbuka dan berfokus pada komunitas.
  • Integrasi kerangka kerja sisi klien modern dan alur kerja pengembangan.
  • Sistem konfigurasi berbasis lingkungan yang siap dengan cloud.
  • Injeksi dependensi bawaan.
  • Alur permintaan HTTP yang ringan, berperforma tinggi, dan modular.
  • Kemampuan untuk menjadi tuan rumah di Kestrel, IIS, HTTP.sys, Nginx, Apache, dan Docker.
  • Penerapan versi berdampingan.
  • Alat yang menyederhanakan pengembangan web modern.

Artikel ini menjelaskan bagaimana API Web mengikat parameter, dan bagaimana Anda dapat menyesuaikan proses pengikatan. Saat Api Web memanggil metode pada pengontrol, API web harus mengatur nilai untuk parameter, proses yang disebut pengikatan.

Secara default, Web API menggunakan aturan berikut untuk mengikat parameter:

  • Jika parameter adalah jenis "sederhana", Api Web mencoba mendapatkan nilai dari URI. Jenis sederhana termasuk jenis primitif .NET (int, bool, double, dan sebagainya), ditambah TimeSpan, DateTime, Guid, desimal, dan string, ditambah jenis apa pun dengan pengonversi jenis yang dapat dikonversi dari string. (Selengkapnya tentang pengonversi jenis nanti.)
  • Untuk jenis kompleks, Web API mencoba membaca nilai dari isi pesan, menggunakan formatter jenis media.

Misalnya, berikut adalah metode pengontrol API Web yang khas:

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

Parameter id adalah jenis "sederhana", sehingga Web API mencoba mendapatkan nilai dari URI permintaan. Parameter item adalah jenis kompleks, sehingga Web API menggunakan formatter jenis media untuk membaca nilai dari isi permintaan.

Untuk mendapatkan nilai dari URI, Api Web terlihat di data rute dan string kueri URI. Data rute diisi ketika sistem perutean mengurai URI dan mencocokkannya dengan rute. Untuk informasi selengkapnya, lihat Perutean dan Pemilihan Tindakan.

Di sisa artikel ini, saya akan menunjukkan bagaimana Anda dapat menyesuaikan proses pengikatan model. Namun, untuk jenis kompleks, pertimbangkan untuk menggunakan formatter jenis media jika memungkinkan. Prinsip utama HTTP adalah bahwa sumber daya dikirim dalam isi pesan, menggunakan negosiasi konten untuk menentukan representasi sumber daya. Pemformat jenis media dirancang untuk tujuan ini.

Menggunakan [FromUri]

Untuk memaksa Web API membaca jenis kompleks dari URI, tambahkan atribut [FromUri] ke parameter . Contoh berikut mendefinisikan GeoPoint jenis, bersama dengan metode pengontrol yang mendapatkan GeoPoint dari URI.

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

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

Klien dapat menempatkan nilai Lintang dan Bujur dalam string kueri dan API Web akan menggunakannya untuk membangun GeoPoint. Contohnya:

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

Menggunakan [FromBody]

Untuk memaksa Web API membaca jenis sederhana dari isi permintaan, tambahkan atribut [FromBody] ke parameter :

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

Dalam contoh ini, Web API akan menggunakan pemformat jenis media untuk membaca nilai nama dari isi permintaan. Berikut adalah contoh permintaan klien.

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

"Alice"

Ketika parameter memiliki [FromBody], API Web menggunakan header Content-Type untuk memilih formatter. Dalam contoh ini, jenis konten adalah "application/json" dan isi permintaan adalah string JSON mentah (bukan objek JSON).

Paling banyak satu parameter diizinkan untuk membaca dari isi pesan. Jadi ini tidak akan berhasil:

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

Alasan untuk aturan ini adalah bahwa isi permintaan mungkin disimpan dalam aliran yang tidak di-buffer yang hanya dapat dibaca sekali.

Ketik Pengonversi

Anda dapat membuat Web API memperlakukan kelas sebagai jenis sederhana (sehingga API Web akan mencoba mengikatnya dari URI) dengan membuat TypeConverter dan menyediakan konversi string.

Kode berikut menunjukkan GeoPoint kelas yang mewakili titik geografis, ditambah TypeConverter yang mengonversi dari string ke GeoPoint instans. Kelas GeoPoint dihiasi dengan atribut [TypeConverter] untuk menentukan pengonversi jenis. (Contoh ini terinspirasi oleh posting blog Mike StallCara mengikat objek kustom dalam tanda tangan tindakan di 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);
    }
}

Sekarang Api Web akan memperlakukan GeoPoint sebagai jenis sederhana, yang berarti akan mencoba mengikat GeoPoint parameter dari URI. Anda tidak perlu menyertakan [FromUri] pada parameter .

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

Klien dapat memanggil metode dengan URI seperti ini:

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

Pengikat Model

Opsi yang lebih fleksibel daripada pengonversi jenis adalah membuat pengikat model kustom. Dengan pengikat model, Anda memiliki akses ke hal-hal seperti permintaan HTTP, deskripsi tindakan, dan nilai mentah dari data rute.

Untuk membuat pengikat model, terapkan antarmuka IModelBinder . Antarmuka ini mendefinisikan satu metode, BindModel:

bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);

Berikut adalah pengikat model untuk GeoPoint objek.

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

Pengikat model mendapatkan nilai input mentah dari penyedia nilai. Desain ini memisahkan dua fungsi yang berbeda:

  • Penyedia nilai mengambil permintaan HTTP dan mengisi kamus pasangan kunci-nilai.
  • Pengikat model menggunakan kamus ini untuk mengisi model.

Penyedia nilai default di Api Web mendapatkan nilai dari data rute dan string kueri. Misalnya, jika URI adalah http://localhost/api/values/1?location=48,-122, penyedia nilai membuat pasangan kunci-nilai berikut:

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

(Saya berasumsi templat rute default, yaitu "api/{controller}/{id}".)

Nama parameter untuk mengikat disimpan di properti ModelBindingContext.ModelName . Pengikat model mencari kunci dengan nilai ini dalam kamus. Jika nilai ada dan dapat dikonversi menjadi GeoPoint, pengikat model menetapkan nilai terikat ke properti ModelBindingContext.Model .

Perhatikan bahwa pengikat model tidak terbatas pada konversi jenis sederhana. Dalam contoh ini, pengikat model pertama kali melihat dalam tabel lokasi yang diketahui, dan jika gagal, model menggunakan konversi jenis.

Mengatur Model Binder

Ada beberapa cara untuk mengatur pengikat model. Pertama, Anda dapat menambahkan atribut [ModelBinder] ke parameter .

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

Anda juga dapat menambahkan atribut [ModelBinder] ke jenis . API Web akan menggunakan pengikat model yang ditentukan untuk semua parameter jenis tersebut.

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

Terakhir, Anda dapat menambahkan penyedia binder model ke HttpConfiguration. Penyedia model-binder hanyalah kelas pabrik yang membuat pengikat model. Anda dapat membuat penyedia dengan berasal dari kelas ModelBinderProvider . Namun, jika pengikat model Anda menangani satu jenis, lebih mudah untuk menggunakan SimpleModelBinderProvider bawaan, yang dirancang untuk tujuan ini. Kode berikut menunjukkan cara melakukannya.

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

        // ...
    }
}

Dengan penyedia pengikatan model, Anda masih perlu menambahkan atribut [ModelBinder] ke parameter , untuk memberi tahu Web API bahwa ia harus menggunakan pengikat model dan bukan formatter jenis media. Tetapi sekarang Anda tidak perlu menentukan jenis pengikat model dalam atribut :

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

Penyedia Nilai

Saya menyebutkan bahwa pengikat model mendapatkan nilai dari penyedia nilai. Untuk menulis penyedia nilai kustom, terapkan antarmuka IValueProvider . Berikut adalah contoh yang menarik nilai dari cookie dalam permintaan:

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

Anda juga perlu membuat pabrik penyedia nilai dengan berasal dari kelas ValueProviderFactory .

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

Tambahkan pabrik penyedia nilai ke HttpConfiguration sebagai berikut.

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

    // ...
}

API Web menyusun semua penyedia nilai, jadi ketika pengikat model memanggil ValueProvider.GetValue, pengikat model menerima nilai dari penyedia nilai pertama yang dapat menghasilkannya.

Atau, Anda dapat mengatur pabrik penyedia nilai di tingkat parameter dengan menggunakan atribut ValueProvider , sebagai berikut:

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

Ini memberi tahu Web API untuk menggunakan pengikatan model dengan pabrik penyedia nilai yang ditentukan, dan tidak menggunakan salah satu penyedia nilai terdaftar lainnya.

HttpParameterBinding

Pengikat model adalah instans tertentu dari mekanisme yang lebih umum. Jika Anda melihat atribut [ModelBinder], Anda akan melihat bahwa atribut tersebut berasal dari kelas ParameterBindingAttribute abstrak. Kelas ini mendefinisikan satu metode, GetBinding, yang mengembalikan objek HttpParameterBinding :

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

HttpParameterBinding bertanggung jawab untuk mengikat parameter ke nilai. Dalam kasus [ModelBinder], atribut mengembalikan implementasi HttpParameterBinding yang menggunakan IModelBinder untuk melakukan pengikatan aktual. Anda juga dapat menerapkan HttpParameterBinding Anda sendiri.

Misalnya, Anda ingin mendapatkan ETag dari if-match header dan if-none-match dalam permintaan. Kita akan mulai dengan mendefinisikan kelas untuk mewakili ETags.

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

Kami juga akan menentukan enumerasi untuk menunjukkan apakah akan mendapatkan ETag dari if-match header atau if-none-match header.

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

Berikut adalah HttpParameterBinding yang mendapatkan ETag dari header yang diinginkan dan mengikatnya ke parameter jenis 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;
    }
}

Metode ExecuteBindingAsync melakukan pengikatan. Dalam metode ini, tambahkan nilai parameter terikat ke kamus ActionArgument di HttpActionContext.

Catatan

Jika metode ExecuteBindingAsync Anda membaca isi pesan permintaan, ambil alih properti WillReadBody untuk mengembalikan true. Isi permintaan mungkin merupakan aliran yang tidak dibuffer yang hanya dapat dibaca sekali, sehingga WEB API memberlakukan aturan yang paling banyak satu pengikatan dapat membaca isi pesan.

Untuk menerapkan HttpParameterBinding kustom, Anda dapat menentukan atribut yang berasal dari ParameterBindingAttribute. Untuk ETagParameterBinding, kita akan menentukan dua atribut, satu untuk if-match header dan satu untuk if-none-match header. Keduanya berasal dari kelas dasar abstrak.

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

Berikut adalah metode pengontrol yang menggunakan [IfNoneMatch] atribut .

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

Selain ParameterBindingAttribute, ada kait lain untuk menambahkan HttpParameterBinding kustom. Pada objek HttpConfiguration, properti ParameterBindingRules adalah kumpulan fungsi tipe anonim (HttpParameterDescriptor ->HttpParameterBinding). Misalnya, Anda dapat menambahkan aturan yang digunakan ETagParameterBinding parameter ETag apa pun pada metode GET dengan 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;
    }
});

Fungsi harus kembali null untuk parameter di mana pengikatan tidak berlaku.

IActionValueBinder

Seluruh proses pengikatan parameter dikontrol oleh layanan yang dapat dicolokkan, IActionValueBinder. Implementasi default IActionValueBinder melakukan hal berikut:

  1. Cari ParameterBindingAttribute pada parameter . Ini termasuk [FromBody], [FromUri], dan [ModelBinder], atau atribut kustom.

  2. Jika tidak, lihat di HttpConfiguration.ParameterBindingRules untuk fungsi yang mengembalikan HttpParameterBinding non-null.

  3. Jika tidak, gunakan aturan default yang saya jelaskan sebelumnya.

    • Jika jenis parameter "sederhana"atau memiliki pengonversi jenis, ikat dari URI. Ini setara dengan menempatkan atribut [FromUri] pada parameter .
    • Jika tidak, coba baca parameter dari isi pesan. Ini setara dengan menempatkan [FromBody] pada parameter .

Jika mau, Anda dapat mengganti seluruh layanan IActionValueBinder dengan implementasi kustom.

Sumber Tambahan

Sampel Pengikatan Parameter Kustom

Mike Stall menulis serangkaian posting blog yang baik tentang pengikatan parameter API Web: