.Net Core Api: mendapatkan data dalam permintaan dari berbagai sumber

.Net Core memiliki mekanisme Binding Model bawaan, yang memungkinkan tidak hanya untuk menerima parameter input pada pengontrol, tetapi juga untuk segera menerima objek dengan bidang isian. Ini memungkinkan Anda untuk menanamkan semua pemeriksaan yang diperlukan ke objek tersebut menggunakan Validasi Model.

Berikut ini hanya data yang diperlukan agar API berfungsi, datang kepada kami tidak hanya dari Kueri atau Badan. Beberapa data perlu diterima dari Header (dalam kasus saya ada json di base64), beberapa data harus dari layanan eksternal atau ActionRoute jika Anda menggunakan REST. Anda dapat menggunakan Binding Anda untuk mendapatkan data dari sana. Benar, ada masalah: jika Anda memutuskan untuk tidak merusak enkapsulasi dan menginisialisasi model melalui konstruktor, Anda harus dukun.

Untuk saya sendiri dan untuk generasi mendatang, saya memutuskan untuk menulis sesuatu seperti instruksi untuk menggunakan Binding dan perdukunan dengannya.

Masalah


Kontroler tipikal terlihat seperti ini:

[HttpGet]
public async Task<IActionResult> GetSomeData([FromQuery[IncomeData someData)
{
    var moreData = GetFromHeaderAndDecode("X-Property");
    if (moreData.Id == 0)
    {
        return StatusCode(400, "Nginx doesnt know your id");
    }
    var externalData = GetFromExternalService("http://myservice.com/MoreData");
    if (externalData == null)
    {
        return StatusCode(500, "Cant connect to external service");
    }
    var finalData = new FinalData(someData, moreData, externalData);
    return _myService.Handle(finalData);
}

Akibatnya, kami mendapatkan masalah berikut:

  1. Logika validasi diolesi oleh objek permintaan, metode permintaan dari header, metode permintaan dari layanan, dan metode pengontrol. Untuk memastikan bahwa pemeriksaan yang diperlukan sudah pasti ada, Anda perlu melakukan penyelidikan menyeluruh!
  2. Metode pengontrol yang berdekatan akan memiliki kode yang persis sama. Copy-paste pemrograman dalam serangan.
  3. Biasanya ada lebih banyak pemeriksaan daripada dalam contoh, dan sebagai hasilnya, satu-satunya jalur signifikan - panggilan ke metode pemrosesan logika bisnis - disembunyikan dalam tumpukan kode. Untuk melihatnya dan memahami apa yang terjadi di sini secara umum membutuhkan usaha.

Penjilidan Kustom (Mode Mudah)


Sebagian dari masalah dapat diselesaikan dengan mengimplementasikan handler Anda di jalur pemroses permintaan. Untuk melakukan ini, koreksi dahulu controller kita dengan mengirimkan objek terakhir ke metode dengan segera. Terlihat jauh lebih baik, bukan?

[HttpGet]
public async Task<IActionResult> GetSomeData([FromQuery]FinalData finalData)
{
    return _myService.Handle(finalData);
}

Selanjutnya, buat binder Anda untuk jenis MoreData.

public class MoreDataBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var moreData = GetFromHeaderAndDecode(bindingContext.HttpContext.Request.Headers);
        if (moreData != null)
        {
            bindingContext.Result = ModelBindingResult.Success(moreData);
        }
        return Task.CompletedTask;
    }
    private MoreData GetFromHeaderAndDecode(IHeaderDictionary headers) { ... }
}

Terakhir, kami akan memperbaiki model FinalData dengan menambahkan pengikat yang mengikat ke properti di sana:

public class FinalData
{
    public int SomeDataNumber { get; set; }

    public string SomeDataText { get; set; }

    [ModelBinder(BinderType = typeof(MoreDataBinder))]
    public MoreData MoreData { get; set; }
}

Sudah lebih baik, tetapi wasir telah meningkat: sekarang Anda perlu tahu bahwa kami memiliki penangan khusus dan menunjukkannya di semua model. Tapi itu bisa dipecahkan.

Buat BinderProvider Anda:

public class MoreDataBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        var modelType = context.Metadata.UnderlyingOrModelType;
        if (modelType == typeof(MoreData))
        {
            return new BinderTypeModelBinder(typeof(MoreDataBinder));
        }
        return null;
    }
}

Dan daftarkan di Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMvc(options =>
    {
        options.ModelBinderProviders.Insert(0, new MoreDataBinderProvider());
     });
}

Penyedia dipanggil untuk setiap objek model dalam urutan antrian. Jika penyedia kami memenuhi jenis yang diinginkan, itu akan mengembalikan binder yang diinginkan. Dan jika tidak, maka binder default akan berfungsi. Jadi sekarang setiap kali kita menentukan jenis MoreData, itu akan diambil dan didekodekan dari Header dan atribut khusus dalam model tidak perlu ditentukan.

Penjilidan Kustom (Mode Keras)


Semua ini luar biasa, tetapi ada satu hal kecuali: agar keajaiban bekerja, model kita harus memiliki properti publik dengan perangkat. Tetapi bagaimana dengan enkapsulasi? Bagaimana jika saya ingin mentransfer data permintaan ke berbagai tempat di cloud dan tahu bahwa mereka tidak akan berubah di sana?

Masalahnya adalah bahwa binder default tidak berfungsi untuk model yang tidak memiliki konstruktor default. Tetapi apa yang menghalangi kita untuk menulis milik kita sendiri?
Layanan yang saya tuliskan kode ini tidak menggunakan REST, parameter dikirimkan hanya melalui Query dan Body, dan hanya dua jenis pertanyaan yang digunakan - Dapatkan
dan Posting. Karenanya, dalam kasus REST API, logika pemrosesan akan sedikit berbeda.
Secara umum, kode akan tetap tidak berubah, hanya binder kami yang perlu disempurnakan sehingga membuat objek dan mengisi bidang privasinya. Selanjutnya saya akan memberikan potongan kode dengan komentar, yang tidak tertarik - di akhir artikel di bawah kucing adalah seluruh daftar kelas.

Pertama, mari kita tentukan apakah MoreData adalah satu-satunya properti kelas. Jika ya, maka Anda perlu membuat objek sendiri (halo, Aktivator), dan jika tidak, maka JsonConvert akan melakukan pekerjaan dengan sempurna, dan kami hanya memasukkan data yang diperlukan ke dalam properti.

private static bool NeedActivator(IReflect modelType)
{
    var propFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
    var properties = modelType.GetProperties(propFlags);

    return properties.Select(p => p.Name).Distinct().Count() == 1;
}

Membuat objek melalui JsonConvert sederhana, untuk permintaan dengan Tubuh:

private static object? GetModelFromBody(ModelBindingContext bindingContext, Type modelType)
{
    using var reader = new StreamReader(bindingContext.HttpContext.Request.Body);
    var jsonString = reader.ReadToEnd();
    var data = JsonConvert.DeserializeObject(jsonString, modelType);
    return data;
}

Tetapi dengan Query aku harus berbaring. Saya akan senang jika seseorang dapat menyarankan solusi yang lebih indah.

Saat melewati array, beberapa parameter dengan nama yang sama diperoleh. Melakukan casting ke tipe flat membantu, tetapi serialisasi memberi tanda kutip tambahan pada array [], yang harus dihapus secara manual.

private static object? GetModelFromQuery(ModelBindingContext bindingContext, Type modelType)
{
    var valuesDictionary = QueryHelpers.ParseQuery(bindingContext.HttpContext.Request.QueryString.Value);
    var jsonDictionary = valuesDictionary.ToDictionary(pair => pair.Key, pair => pair.Value.Count < 2 ? pair.Value.ToString() : $"[{pair.Value}]");

    var jsonStr = JsonConvert.SerializeObject(jsonDictionary).Replace("\"[", "[").Replace("]\"", "]");
    var data = JsonConvert.DeserializeObject(jsonStr, modelType);
    return data;
}

Akhirnya, menciptakan objek, perlu untuk menulis data kami ke properti pribadinya. Tentang perdukunan inilah yang saya bicarakan di awal artikel. Saya menemukan solusi ini di sini , yang banyak terima kasih kepada penulis.

private void ForceSetValue(PropertyInfo propertyInfo, object obj, object value)
{
    var propName = $"<{propertyInfo.Name}>k__BackingField";
    var propFlags = BindingFlags.Instance | BindingFlags.NonPublic;
            
    obj.GetType().GetField(propName, propFlags)?.SetValue(obj, value);
}

Kami menggabungkan metode ini dalam satu panggilan:

public Task BindModelAsync(ModelBindingContext bindingContext)
{
    var moreData = GetFromHeaderAndDecode(bindingContext.HttpContext.Request.Headers);
    if (moreData == null)
    {
        return Task.CompletedTask;
    }

    var modelType = bindingContext.ModelMetadata.UnderlyingOrModelType;
    if (NeedActivator(modelType))
    {
        var data = Activator.CreateInstance(modelType, moreData);
        bindingContext.Result = ModelBindingResult.Success(data);

        return Task.CompletedTask;
    }

    var model = bindingContext.HttpContext.Request.Method == "GET"
                            ? GetModelFromQuery(bindingContext, modelType)
                            : GetModelFromBody(bindingContext, modelType);

    if (model is null)
    {
        throw new Exception("  ");
    }

    var ignoreCase = StringComparison.InvariantCultureIgnoreCase;
    var dataProperty = modelType.GetProperties()
                            .FirstOrDefault(p => p.Name.Equals(typeof(T).Name, ignoreCase));

    if (dataProperty != null)
    {
        ForceSetValue(dataProperty, model, moreData);
    }

    bindingContext.Result = ModelBindingResult.Success(model);
    return Task.CompletedTask;
}

Tetap memperbaiki BinderProvider sehingga merespons setiap kelas dengan properti yang diinginkan:

public class MoreDataBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
       var modelType = context.Metadata.UnderlyingOrModelType;
       if (HasDataProperty(modelType))
       {
           return new BinderTypeModelBinder(typeof(PrivateDataBinder<MoreData>));
       }
       return null;
    }
    private bool HasDataProperty(IReflect modelType)
    {
        var propFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
        var properties = modelType.GetProperties(propFlags);

        return properties.Select(p => p.Name) .Contains(nameof(MoreData));
    }
}


Itu saja. Binder ternyata sedikit lebih rumit daripada di Mode Mudah, tetapi sekarang kita dapat mengikat properti "eksternal" di semua metode semua pengendali tanpa upaya tambahan. Dari minus:

  1. Konstruktor objek dengan bidang pribadi harus menentukan atribut [JsonConstrustor]. Tetapi ini sepenuhnya cocok dengan logika model dan tidak mengganggu persepsinya.
  2. Di suatu tempat, Anda mungkin perlu mendapatkan MoreData bukan dari header. Tetapi ini diperlakukan dengan membuat kelas terpisah.
  3. Anggota tim yang lain harus menyadari keberadaan sihir. Tetapi dokumentasinya akan menyelamatkan umat manusia.

Daftar lengkap dari Binder yang dihasilkan ada di sini:

PrivateMoreDataBinder.cs
public class PrivateDataBinder<T> : IModelBinder
    {
        /// <summary></summary>
        /// <param name="bindingContext"></param>
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var moreData = GetFromHeaderAndDecode(bindingContext.HttpContext.Request.Headers);
            if (moreData == null)
            {
                return Task.CompletedTask;
            }

            var modelType = bindingContext.ModelMetadata.UnderlyingOrModelType;
            if (NeedActivator(modelType))
            {
                var data = Activator.CreateInstance(modelType, moreData);
                bindingContext.Result = ModelBindingResult.Success(data);

                return Task.CompletedTask;
            }

            var model = bindingContext.HttpContext.Request.Method == "GET"
                            ? GetModelFromQuery(bindingContext, modelType)
                            : GetModelFromBody(bindingContext, modelType);

            if (model is null)
            {
                throw new Exception("  ");
            }

            var ignoreCase = StringComparison.InvariantCultureIgnoreCase;
            var dataProperty = modelType.GetProperties()
                                        .FirstOrDefault(p => p.Name.Equals(typeof(T).Name, ignoreCase));

            if (dataProperty != null)
            {
                ForceSetValue(dataProperty, model, moreData);
            }

            bindingContext.Result = ModelBindingResult.Success(model);

            return Task.CompletedTask;
        }

        private static object? GetModelFromQuery(ModelBindingContext bindingContext,
                                                 Type modelType)
        {
            var valuesDictionary = QueryHelpers.ParseQuery(bindingContext.HttpContext.Request.QueryString.Value);

            var jsonDictionary = valuesDictionary.ToDictionary(pair => pair.Key, pair => pair.Value.Count < 2 ? pair.Value.ToString() : $"[{pair.Value}]");

            var jsonStr = JsonConvert.SerializeObject(jsonDictionary)
                                     .Replace("\"[", "[")
                                     .Replace("]\"", "]");

            var data = JsonConvert.DeserializeObject(jsonStr, modelType);

            return data;
        }

        private static object? GetModelFromBody(ModelBindingContext bindingContext,
                                                Type modelType)
        {
            using var reader = new StreamReader(bindingContext.HttpContext.Request.Body);
            var jsonString = reader.ReadToEnd();
            var data = JsonConvert.DeserializeObject(jsonString, modelType);

            return data;
        }

        private static bool NeedActivator(IReflect modelType)
        {
            var propFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
            var properties = modelType.GetProperties(propFlags);

            return properties.Select(p => p.Name).Distinct().Count() == 1;
        }

        private void ForceSetValue(PropertyInfo propertyInfo, object obj, object value)
        {
            var propName = $"<{propertyInfo.Name}>k__BackingField";
            var propFlags = BindingFlags.Instance | BindingFlags.NonPublic;
            
            obj.GetType().GetField(propName, propFlags)?.SetValue(obj, value);
        }

        private T GetFromHeaderAndDecode(IHeaderDictionary headers) { return (T)Activator.CreateInstance(typeof(T), new object[] { "ok" }); }
    }


All Articles