مقدمة في Filters

-

تُستخدم عوامل التصفية  Filters في ASP.NET Core لتشغيل التعليمات البرمجية قبل أو بعد مراحل معينة في مسار معالجة الطلب request processing pipeline. هناك العديد من built-in filters ل authorization, logging, caching, exception handling وما إلى ذلك. تساعد الفلاتر أيضًا في تجنب تكرار التعليمات البرمجية عبر action methods. ويمكن أيضًا إنشاء عوامل تصفية مخصصة للتعامل مع التطبيقات بالطريقة التي تريدها.

تمام خلونا نفهم الموضوع بمثال، وبنكمل شغل على مشروعنا StudnetAcadmey 

اول شي لازم نعملو نعدل على ملف  Startup.cs لتمكين ونفعيل MVC framework و Middlewares  المطلوبة في المشروع تأكد من الكود يكون مثل الكود التالي:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Filters
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }
 
            app.UseHttpsRedirection();
            app.UseStaticFiles();
 
            app.UseRouting();
 
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

النقطة الثانية المهمه التأكد من تثبيت Bootstrap Package في المشروع.

اكيد حكينا عن Bootstrap واذا نسيت هو CSS framework يستخدم في front-end web development من اجل انشاء واجهات أمامية  للتطبيقات بحيث تكون متناسقة ومناسبه لجميع الاحجام مثل الهاتف المحمول اجهزة iPad ... الخ. مشان هيك من الضروري بتثبيت حزمة Bootstrap في المشروع واكيد هي موجوده في مشروعنا لان استخدمناه سابقا في اكثر من مكان. يمكنك التحقق من كيفية تثبيت حزمة Bootstrap في تطبيق ASP.NET Core في Visual Studio بالرجوع الى الدرس : تثبيت Bootstrap Package 

تضمين Built-in Tag Helpers في Views 

كمان هذه الخطوه عملناها من قبل وللتذكير بكيفة عملها انتقل الى المجلد Share داخل مجلد Views ، ومن ثم أضف ملف Razor View Imports داخل مجلد Share باسم ViewImports_ وضيف الكود التالي:

@using StudentsAcademy
@using StudentsAcademy.Models
@using Microsoft.AspNetCore.Http
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor

تفعيل [RequireHttps] attribute .

[RequireHttps] attribute  عبارة عن built-in filter والذي عند تطبيقه على action method يفرض قيود على الطلب لتكون من نوع HTTPS فقط عند استدعائه.

لفهم عمل RequireHttps attribute  ، يجب تمكين SSL للمشروع. لتمكين SSL ، انقر بزر الماوس الأيمن على اسم المشروع في Solution Explorer  وحدد خصائص. وبعدها فعل SSL شوف الصورة.



سيكون عنوان SSL URL  الخاص بالمشروع مختلفًا عن عنوان non-SSL URL

تمام نعمل انشاء ل Controller جديد باسم FilterController ونضيف index action وبعد هيك نضيف الكود كالتالي:

        public string Index()
        {
            return "This is the Index action on the Home controller";
        }
اضف View بالنقر يمين فوق action واختر Add New View 
https://localhost:44382/filter
الآن نعمل تشغيل للتطبيق الخاص بك وسترى string التي تم إرجاعها من action method هذه يتم عرضها على المتصفح ، انظر الصورة أدناه:

تطبيق  [RequireHttps] attribute filter
نجرب نطبق attribute المسميه [RequireHttps] على هذا action وشكل الصفحه. اكيد بتكون النتيجة :

http://localhost:44382/filter




السبب ان هذا attribute  جعل action method هذه متاحة للوصول إليها فقط من خلال طلب HTTPS. واذا لاحظت الرابط الي استخدمناه كان http (Non-HTTPS ) وليس https .

عند عدم وجود هذه attribute فالي بصير ان خلف الكواليس تقوم [RequireHttps] attribute بإعادة توجيه non-https URL إلى  Https based ULR لذلك يتم إعادة توجيه الطلب
 http://localhost:44382/filter/Index  
إلى
 https://localhost:44382/filter/Index
 اكيد لاحظت ان التغيير في هذين العنوانين هو فقط "https" بينما بقية الرابط نفس الاشي.
واكيد هذا الامر في المواقع الحقيقه مختلف لان في بيئة التطوير development يكون لكل من http و https منفذ Port مختلف ، لذلك ترى الرسالة لا يمكن الوصول إلى هذا الموقع. ومع ذلك ، في المواقع الحقيقه والمباشره ، حيث لا يحتوي عنوان URL على Port، سيعمل بشكل صحيح لأنه إذا كان عنوان URL المطلوب هو http://www.SiteName.com ، فسيتم إعادة توجيهك إلى https://www.SiteName.com

واكيد يمكن تطبيق هذا [RequireHttps] built-in filter   على Controller كامل، وبالتالي جميع actions الموجوده داخل هذا Controller بنعكس عليها هذا التغيير. 

تمام بنتعرف فيما يلي على الاناع المختلفه ل Different Types of Filters

كل نوع من Filters يتم اشتقاقه من interface يوفر الخصائص الخاصه به. الجدول التالي يحتوي على اهم هذه Filters مع interface  الخاص بها 

Filter
Interfaces  التي تحتويه
الوصف
Authorization
IAuthorizationFilter, IAsyncAuthorizationFilter

تستخدم لتطبيق authorization and security policy

Action
IActionFilter, IAsyncActionFilter
يستخدم لأداء عمل محدد مباشرة قبل أو بعد تنفيذ action method 
Result
IResultFilter, IAsyncResultFilter
يستخدم لأداء عمل محدد مباشرة قبل أو بعد معالجة النتيجة من action method 
Exception
IExceptionFilter, IAsyncExceptionFilter
تستخدم للتعامل ومعالجة الاستثناءات مع exceptions
اكيد لاحظت ان لاحظ أن كل Filter يحتوي على  2Interface (Synchronous & Asynchronous) حيث يمكن أن تعمل filters بطريقة synchronous and asynchronous.

ترتيب تنفيذ Filters 

يتم تنفيذ Filters  بالترتيب التالي:
  • Authorization Filters
  •  Action Filters
  • At the last the Result Filters

ال Filters الخاصه ب Exception عند حدوث استثناءات في التطبيق. 

تمام خلونا نحكي شوي عن هذه الاناع ونحاول نفهم كيف بنستخدمها. 

Authorization Filters

يتم استخدام Authorization Filters لغرض اعطاء authorization  وإنشاء security policy. يتم تنفيذ هذه filters قبل أي Filter آخر وحتى قبل تنفيذ action method . وهذا filters يستخدم معو  IAuthorizationFilter او IAsyncAuthorizationFilter الى هم interface 

طريقة التعريف ل IAuthorizationFilter 

namespace Microsoft.AspNetCore.Mvc.Filters {
    public interface IAuthorizationFilter : IFilterMetadata {
        void OnAuthorization(AuthorizationFilterContext context);
    }
}
طريقة التعريف ل IAsyncAuthorizationFilter

using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Filters {
    public interface IAsyncAuthorizationFilter : IFilterMetadata {
        Task OnAuthorizationAsync(AuthorizationFilterContext context);
    }
}

وتستخدم IAsyncAuthorizationFilter interface  لإنشاء Asynchronous Authorization Filters.
تُستخدم methods المسماة ()OnAuthorization و ()OnAuthorizationAsync لكتابة التعليمات البرمجية بحيث يمكن لFilter تطبيق authorize على الطلبات الواردة.
يحتوي ايضا على parameter التي تسمى AuthorizationFilterContext context ، تستقبل context data التي تصف الطلب. يحتوي object من النوع AuthorizationFilterContext على خاصية تسمى Result (من النوع IActionResult) والتي تم تعيينها بواسطة authorization filter.

إذا تم تعيين Result property فإن ASP.NET Core يعرض IActionResult بدلاً من استدعاء action method.

تمام نتعلم كيف بنعمل Authorization Filter

في السطور التالية بنعمل إنشاء Authorization Filter بحيث يمكن تطبيقه على action method أو على Controller class نفسها. سيقوم Filter هذا بتقييد الطلبات التي لم يتم إجراؤها باستخدام Https protocol

أود إضافة جميع عوامل التصفية المخصصة الخاصة بي بما في ذلك هذا المرشح في مجلد يسمى  CustomFilters. لذا قم بإنشاء هذا المجلد في جذر المشروع وأضف class جديد يسمى HttpsOnly.cs إليه. بعد ذلك ، أضف الكود التالي إلى هذه الفئة:


طيب تمام لغرض تنظيم العمل بدنا نعمل Folder باسم CustomFilters بغرض اضافة جميع Custom Filter الخاصه بنا. لذا خلونا نعمل إنشاء هذا المجلد في جذر المشروع ونضيف class جديد يسمى HttpsOnly.cs إليه. بعد ذلك ، وبعدها أضف الكود التالي:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace StudentsAcademy.CustomFilters
{
    public class HttpsOnly : Attribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            if (!context.HttpContext.Request.IsHttps)
                context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden);
        }
    }
}
نظرًا لأنه سيتم تطبيق HttpsOnly class ك attribute على action method أو controller ، وبالتالي فهي مشتقة من Attribute class. أيضًا ، نظرًا لأنه سيتم استخدام هذه class لإنشاء عامل Authorization Filter ، فهي مشتقة من IAuthorizationFilter interface.
وايضا يتم العمل الرئيسي داخل OnAuthorization method التي تتحقق مما إذا كان الطلب قد تم من Https Protocol  أم لا. وفي حال لم يتم إجراء الطلب من Https ، فسنقوم بتعيين Result property ل AuthorizationFilterContext object إلى403 forbidden. هذا الامر يمنع حدوث المزيد من التنفيذ ويوفر ASP.NET Core نتيجة للعودة إلى المستخدم.
تمام نرجع الآن الى Filter Controller ونطبق هذا Class على Index action بهاذ الشكل:

using Microsoft.AspNetCore.Mvc;
using StudentsAcademy.CustomFilters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace StudentsAcademy.Controllers
{
    public class FilterController : Controller
    {
        [HttpsOnly]
        public string Index()
        {
            return "This is the Index action on the filter controller";
        }
    }
}

تمام خلونا نجرب نشغل التطبيق باستخدام الرابط :
http://localhost:44382/filter
الآن واكيد بتكون النتيجة الرسالة Status Code: 403; Forbidden  ؛
السبب هو أن الطلب قادم من a non-https URL 
لكن اذا قمنا بطلب الرابط باستخدام  HTTPS، فسيجد Authorization filter أن الطلب تم باستخدام بروتوكول Https.  لذلك لم يتم تعيين خاصية النتيجة Result property  ل AuthorizationFilterContext object ويتم تنفيذ Index Action.

تطبيق Action Filters 
يتم تنفيذ Action Filters  بعد Authorization Filters ويتم استدعاؤها قبل وبعد استدعاء Action method.وتشتق من واجهة IActionFilter أو asynchronous  IAsyncActionFilter.

طريقة تعريف IActionFilter 
namespace Microsoft.AspNetCore.Mvc.Filters {
    public interface IActionFilter : IFilterMetadata {
        void OnActionExecuting(ActionExecutingContext context);
        void OnActionExecuted(ActionExecutedContext context);
    }
}
عند تطبيق Action Filter على Action method ، يتم استدعاء OnActionExecuting method قبل استدعاء action method مباشرةً ، ويتم استدعاء OnActionExecuting method  بعد انتهاء تنفيذ action method  مباشرةً.   

تحتوي OnActionExecuting method على parameter من نوع ActionExecutingContext class.
الخصائص المهمة ل ActionExecutingContext class هي: 


الاسمالوصف
Controller
اسم controller التي سيتم استدعاء action method.
Result
هذه الخاصية من النوع  IActionResult. إذا تم تعيين هذه الخاصية على قيمة من النوع IActionResult ، فإن MVC يعرض IActionResult بدلاً من استدعاء action method.
يحتوي OnActionExecuted method على parameter من نوع ActionExecutedContext class.
الخصائص المهمة ل class ActionExecutedContext هي:


الاسم
الوصف
Controller
اسم controller التي تم استدعاء أسلوب عملها.
Exception
تحتوي هذه الخاصية على الاستثناء الذي حدث في Action method.
ExceptionHandled
عندما تقوم بتعيينها على true ، فلن يتم نشر الاستثناءات.
Result
تقوم هذه الخاصية بإرجاع IActionResult الذي تم إرجاعه بواسطة action method ، ويمكنك تغييره أو استبداله إذا كان لديك حاجة.
انشاء Action Filter

الآن بنعمل إنشاء Action filter بهدف قياس عدد milliseconds الذي يحتاجه action method للتنفيذ. لومشان نبدا ايكد بنحتاج انشاء timer في OnActionExecuting method  وبنحتاج الى ايقافه في OnActionExecuted method.

لذا سنقوم بإنشاء  class باسم TimeElapsed.cs داخل مجلد CustomFilters وبنضيف الكود التالي:

using System;using System.Collections.Generic;using System.Linq;using System.Threading.Tasks;using System.Diagnostics;using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Mvc.Filters;namespace StudentsAcademy.CustomFilters{ public class TimeElapsed : Attribute, ActionFilter { private Stopwatch timer; public void OnActionExecuting(ActionExecutingContext context)        {timer = Stopwatch.StartNew(); } public void OnActionExecuted(ActionExecutedContext context){timer.Stop();string result = " Elapsed time: " + "{timer.Elapsed.TotalMilliseconds} ms" IActionResult iActionResult = context.Result;((ObjectResult)iActionResult).Value += result; } }}

تمام نفهم الكود: 
عملنا إنشاء object من Stopwatch class لقياس الوقت. بحيث يبدأ في OnActionExecuting method والكود :
timer = Stopwatch.StartNew();
ويتوقف في  OnActionExecuted method.
timer.Stop();
بعد امر التوقف اضفنا Result property من نوع string لتخزين قيمة الوقت المستغرق للتنفيذ. ثم النتيجة context.Result خزناها في متغير من نوع IActionResult باسم iActionResult وتم اضافة قيمة الوقت المستغرق result للمتغير iActionResult وهذه القيمة من نوع string سيتم إرجاعها بواسطة action.

تمام الان نعرف كيف ممكن تطبيق هذا Filter على action 
بتم ذلك باستخدام باضافة الكود التالي فوق action:
[TimeElapsed]
وبكون شكل controller 

using Microsoft.AspNetCore.Mvc;
using StudentsAcademy.CustomFilters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace StudentsAcademy.Controllers
{
    public class FilterController : Controller
    {
        [TimeElapsed]
        public string Index()
        {
            return "This is the Index action on the filter controller";
        }
    }
}

الان وقت تشغيل التطبيق،واكيد بتكون النتيجة رسالة التي تخبرنا بعدد المللي ثانية الذي تستغرقه index action للتنفيذ ، كما هو موضح في الصورة أدناه:

تطبيق asynchronous على Action Filter 

لتطبيق هذا الشي يجب ان يتم اشتقاق Action Filter من IAsyncActionFilter وبناء علية خلونا نعمل class باسم TimeElapsedAsync ونضيف الكود التالي: 

using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StudentsAcademy.CustomFilters
{
    public class TimeElapsedAsync : Attribute, IAsyncActionFilter
    {
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            Stopwatch timer = Stopwatch.StartNew();
            await next();
            timer.Stop();
            string result = "<div>Elapsed time: " + $"{timer.Elapsed.TotalMilliseconds} ms</div>";
            byte[] bytes = Encoding.ASCII.GetBytes(result);
            await context.HttpContext.Response.Body.WriteAsync(bytes, 0, bytes.Length);
        }
    }
}