صافي النواة: الحصول على البيانات في طلب من مصادر مختلفة

.Net Core يحتوي على آلية ربط نموذج مدمجة ، والتي لا تسمح فقط بقبول معلمات الإدخال في وحدات التحكم ، ولكن أيضًا لتلقي الكائنات على الفور مع الحقول المعبأة. هذا يسمح لك بتضمين جميع عمليات الفحص اللازمة في مثل هذا الكائن باستخدام التحقق من صحة النموذج.

في ما يلي البيانات المطلوبة حتى تعمل واجهة برمجة التطبيقات ، ولا تأتي إلينا من Query أو Body فقط. يجب استلام بعض البيانات من الرؤوس (في حالتي كان هناك json في base64) ، يجب أن تكون بعض البيانات من خدمات خارجية أو ActionRoute إذا كنت تستخدم REST. يمكنك استخدام ربط الخاص بك للحصول على البيانات من هناك. صحيح ، هناك مشكلة: إذا قررت عدم كسر التغليف وتهيئة النموذج من خلال المنشئ ، فسيتعين عليك الشامان.

لنفسي وللأجيال القادمة ، قررت أن أكتب شيئًا مثل تعليمات استخدام التجليد والشامانية معها.

مشكلة


تبدو وحدة التحكم النموذجية مثل هذا:

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

نتيجة لذلك ، نحصل على المشاكل التالية:

  1. يتم تلطيخ منطق التحقق بواسطة كائن الطلب ، وأسلوب الطلب من الرأس ، وأسلوب الطلب من الخدمة ، وأسلوب التحكم. للتأكد من أن الفحص الضروري موجود بالتأكيد ، تحتاج إلى إجراء تحقيق كامل!
  2. سيكون لطريقة التحكم المجاورة نفس الرمز بالضبط. برمجة النسخ واللصق في الهجوم.
  3. عادة ما يكون هناك عدد أكبر بكثير من عمليات التحقق مما هو عليه في المثال ، ونتيجة لذلك ، يتم إخفاء الخط المهم الوحيد - استدعاء أسلوب معالجة منطق الأعمال - في كومة من التعليمات البرمجية. لرؤيته وفهم ما يحدث هنا بشكل عام يتطلب بعض الجهد.

ربط مخصص (الوضع السهل)


يمكن حل جزء من المشكلة عن طريق تنفيذ معالج الخاص بك في خط أنابيب معالجة الطلب. للقيام بذلك ، قم أولاً بتصحيح وحدة التحكم الخاصة بنا عن طريق تمرير الكائن النهائي إلى الطريقة على الفور. تبدو أفضل بكثير ، أليس كذلك؟

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

بعد ذلك ، قم بإنشاء الرابط الخاص بك لنوع 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) { ... }
}

أخيرًا ، سنصلح نموذج FinalData عن طريق إضافة رابط الموثق إلى الموقع هناك:

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

    public string SomeDataText { get; set; }

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

إنه أفضل بالفعل ، لكن البواسير زادت: الآن عليك أن تعرف أن لدينا معالج خاص وتوضيح ذلك في جميع الموديلات. لكنها قابلة للحل.

إنشاء BinderProvider الخاص بك:

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

وقم بتسجيله في بدء التشغيل:

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

يتم استدعاء الموفر لكل كائن نموذج في قائمة الانتظار. إذا استوفى مزودنا النوع المطلوب ، فسوف يعيد الموثق المطلوب. وإذا لم يكن الأمر كذلك ، فسيعمل الموثق الافتراضي. حتى الآن ، كلما حددنا نوع MoreData ، سيتم أخذها وفك تشفيرها من الرأس ولن تحتاج إلى تحديد سمات خاصة في النماذج.

ربط مخصص (الوضع الصعب)


كل هذا رائع ، ولكن هناك شيء واحد ولكن: لكي يعمل السحر ، يجب أن يكون لنموذجنا خصائص عامة مع مجموعة. ولكن ماذا عن التغليف؟ ماذا لو كنت أرغب في نقل بيانات الطلب إلى أماكن مختلفة في السحابة وأعلم أنه لن يتم تغييرها هناك؟

تكمن المشكلة في أن الموثق الافتراضي لا يعمل مع النماذج التي لا تحتوي على مُنشئ افتراضي. ولكن ما الذي يمنعنا من كتابة أعمالنا الخاصة؟
لا تستخدم الخدمة التي كتبت عنها هذا الرمز REST ، ويتم إرسال المعلمات فقط من خلال Query and Body ، ويتم استخدام نوعين فقط من الاستعلامات - Get
and Post. وفقًا لذلك ، في حالة REST API ، سيكون منطق المعالجة مختلفًا قليلاً.
بشكل عام ، ستبقى الشفرة دون تغيير ، فقط الموثق الخاص بنا يحتاج إلى التنقيح بحيث ينشئ كائنًا ويملأ حقوله الخاصة. علاوة على ذلك ، سأعطي أجزاء من التعليمات البرمجية مع التعليقات ، غير المهتمين - في نهاية المقالة تحت القطة ، يتم سرد القائمة بالكامل للفئة.

أولاً ، دعنا نحدد ما إذا كانت MoreData هي الخاصية الوحيدة للفئة. إذا كانت الإجابة بنعم ، فأنت بحاجة إلى إنشاء الكائن بنفسك (مرحبًا ، Activator) ، وإذا لم يكن كذلك ، فسيقوم JsonConvert بالقيام بالمهمة بشكل مثالي ، وسنقوم فقط بسحب البيانات اللازمة إلى مكان الإقامة.

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

إنشاء كائن من خلال JsonConvert أمر بسيط ، للاستعلامات مع Body:

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

حسنًا ، ندمج هذه الطرق في مكالمة واحدة:

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

يبقى إصلاح BinderProvider بحيث يستجيب لأي فئات مع الخاصية المطلوبة:

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


هذا كل شئ. تبين أن Binder أكثر تعقيدًا قليلاً من الوضع السهل ، ولكن يمكننا الآن ربط الخصائص "الخارجية" في جميع طرق جميع وحدات التحكم دون بذل جهود إضافية. من السلبيات:

  1. يجب أن يحدد مُنشئ الكائنات ذات الحقول الخاصة السمة [JsonConstrustor]. لكن هذا يتناسب تمامًا مع منطق النموذج ولا يتداخل مع إدراكه.
  2. في مكان ما ، قد تحتاج إلى الحصول على MoreData وليس من الرأس. ولكن يتم التعامل مع هذا من خلال إنشاء فئة منفصلة.
  3. يجب أن يكون بقية الفريق على علم بوجود السحر. لكن الوثائق ستنقذ البشرية.

قائمة كاملة من الموثق الناتج هنا:

ملف خاص MoreDataBinder.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