۱۳۹۱/۰۱/۲۶

ASP.NET MVC #15


فيلترها در ASP.NET MVC

پايه قسمت‌هاي بعدي مانند مباحث امنيت، اعتبار سنجي كاربران، caching و غيره، مبحثي است به نام فيلترها در ASP.NET MVC. تابحال با سه فيلتر به نام‌هاي ActionName، NonAction و AcceptVerbs آشنا شده‌ايم. به اين‌ها Action selector filters هم گفته مي‌شود. زمانيكه قرار است يك درخواست رسيده به متدي در يك كنترلر خاص نگاشت شود،‌ فريم ورك ابتدا به متاديتاي اعمالي به متدها توجه كرده و بر اين اساس درخواست را به متدي صحيح هدايت خواهد كرد. ActionName، نام پيش فرض يك متد را بازنويسي مي‌كند و توسط AcceptVerbs اجراي يك متد، به افعالي مانند POST، GET، DELETE و امثال آن محدود مي‌شود كه در قسمت‌هاي قبل در مورد آن‌ها بحث شد.
علاوه بر اين‌ها يك سري فيلتر ديگر نيز در ASP.NET MVC وجود دارند كه آن‌ها نيز به شكل متاديتا به متدهاي كنترلرها اعمال شده و كار نهايي‌اشان تزريق كدهايي است كه بايد پيش و پس از اجراي يك اكشن متد،‌ اجرا شوند. 4 نوع فيلتر در ASP.NET MVC وجود دارند:
الف) IAuthorizationFilter
اين نوع فيلترها پيش از اجراي هر متد يا فيلتر ديگري در كنترلر جاري اجرا شده و امكان لغو اجراي آن‌را فراهم مي‌كنند. پياده سازي پيش‌فرض آن توسط كلاس AuthorizeAttribute در فريم ورك وجود دارد.
بديهي است اين نوع اعمال را مستقيما داخل متدهاي كنترلرها نيز مي‌توان انجام داد (بدون نياز به هيچگونه فيلتري). اما به اين ترتيب حجم كدهاي تكراري در سراسر برنامه به شدت افزايش مي‌يابد و نگهداري آن‌را در طول زمان مشكل خواهد ساخت.

ب) IActionFilter
ActionFilterها پيش (OnActionExecuting) و پس از (OnActionExecuted) اجراي متدهاي كنترلر جاري اجرا مي‌شوند و همچنين پيش از ارائه خروجي نهايي متدها. به اين ترتيب براي مثال مي‌توان نحوه رندر يك View را تحت كنترل گرفت. اين اينترفيس توسط كلاس ActionFilterAttribute در فريم ورك پياده سازي شده است.

ج) IResultFilter
ResultFilter بسيار شبيه به ActionFilter است با اين تفاوت كه تنها پيش از (OnResultExecuting) بازگرداندن نتيجه متد و همچنين پس از (OnResultExecuted) اجراي متد، فراخواني مي‌گردد. كلاس ActionFilterAttribute موجود در فريم ورك، پياده سازي پيش فرضي از آن‌‌را ارائه مي‌دهد.

د) IExceptionFilter
ExceptionFilterها پس از اجراي تمامي فيلترهاي ديگر، همواره اجرا خواهند شد؛ صرفنظر از اينكه آيا در اين بين استثنايي رخ داده است يا خير. بنابراين يكي از كاربردهاي آن‌ها مي‌تواند ثبت وقايع مرتبط با استثناهاي رخ‌داده باشد. پياده سازي پيش فرض آن توسط كلاس HandleErrorAttribute در فريم ورك موجود است.

علت معرفي 4 نوع فيلتر متفاوت هم به مسايل امنيتي بر مي‌گردد. مي‌شد تنها موارد ب و ج معرفي شوند اما از آنجائيكه نياز است مورد الف همواره پيش از اجراي متدي و همچنين تمامي فيلترهاي ديگر فراخواني شود، احتمال بروز اشتباه در نحوه و ترتيب معرفي اين فيلترها وجود داشت. به همين دليل روش معرفي صريح مورد الف در پيش گرفته شد. براي مثال فرض كنيد كه اگر از روي اشتباه فيلتر كش شدن اطلاعات پيش از فيلتر اعتبار سنجي كاربر جاري اجرا مي‌شد چه مشكلات امنيتي ممكن بود بروز كند.

مثالي جهت درك بهتر ترتيب و نحوه اجراي فيلترها:

يك پروژه جديد خالي ASP.NET MVC را آغاز كنيد. سپس فيلتر سفارشي زير را به برنامه اضافه نمائيد:

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
    public class LogAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            Log("OnActionExecuting", filterContext);
        }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            Log("OnActionExecuted", filterContext);
        }

        public override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            Log("OnResultExecuting", filterContext);
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            Log("OnResultExecuted", filterContext);
        }

        private void Log(string stage, ControllerContext ctx)
        {
            ctx.HttpContext.Response.Write(
                string.Format("{0}:{1} - {2} < br/> ",
                ctx.RouteData.Values["controller"], ctx.RouteData.Values["action"], stage));
        }
    }
}

مرسوم است براي ايجاد فيلترهاي سفارشي، همانند مثال فوق با ارث بري از پياده سازي‌هاي توكار اينترفيس‌هاي چهارگانه ياد شده، كار شروع شود.
سپس يك كنترلر جديد را به همراه دو متد، به برنامه اضافه نمائيد. براي هر كدام از متدها هم يك View خالي را ايجاد كنيد. اكنون اين ويژگي جديد را به هر كدام از اين متدها اعمال نموده و برنامه را اجرا كنيد.

using System.Web.Mvc;
using MvcApplication12.CustomFilters;

namespace MvcApplication12.Controllers
{
    public class HomeController : Controller
    {
        [Log]
        public ActionResult Index()
        {
            return View();
        }

        [Log]
        public ActionResult Test()
        {
            return View();
        }
    }
}

سپس ويژگي Log را از متدها حذف كرده و به خود كنترلر اعمال كنيد:
[Log]
public class HomeController : Controller

در اين حالت ويژگي اعمالي، پيش از اجراي متد درخواستي جاري اجرا خواهد شد يا به عبارتي به تمام متدهاي قابل دسترسي كنترلر اعمال مي‌گردد.


تقدم و تاخر اجراي فيلترهاي هم‌خانواده

همانطور كه عنوان شد، هميشه ابتدا AuthorizationFilter اجرا مي‌شود و در آخر ExceptionFilter. سؤال: اگر در اين بين مثلا دو نوع ActionFilter متفاوت به يك متد اعمال شدند، كداميك ابتدا اجرا مي‌شود؟
تمام فيلترها از كلاسي به نام FilterAttribute مشتق مي‌شوند كه داراي خاصيتي است به نام Order. بنابراين جهت مشخص سازي ترتيب اجراي فيلترها تنها كافي است اين خاصيت مقدار دهي شود. براي مثال جهت اعمال دو فيلتر سفارشي زير:

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
    public class AuthorizationFilterA : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            Debug.WriteLine("OnAuthorization : AuthorizationFilterA");
        }
    }
}

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
    public class AuthorizationFilterB : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            Debug.WriteLine("OnAuthorization : AuthorizationFilterB");
        }
    }
}

خواهيم داشت:
using System.Web.Mvc;
using MvcApplication12.CustomFilters;

namespace MvcApplication12.Controllers
{
    public class HomeController : Controller
    {
        [AuthorizationFilterA(Order = 2)]
        [AuthorizationFilterB(Order = 1)]
        public ActionResult Index()
        {
            return View();
        }
    }
}

در اينجا با توجه به مقادير order، ابتدا AuthorizationFilterB اجرا مي‌گردد و سپس AuthorizationFilterA.
علاوه بر اين‌ها محدوده اجراي فيلترها نيز بر اين حق تقدم اجرايي تاثير گذار هستند. براي مثال در پشت صحنه زمانيكه قرار است يك فيلتر جديد اجرا شود، وهله سازي آن به نحوه زير است كه بر اساس مقادير order و FilterScope صورت مي‌گيرد:
var filter = new Filter(actionFilter, FilterScope, order);

مقادير FilterScope را در ادامه ملاحظه مي‌نمائيد:
namespace System.Web.Mvc { 
      public enum FilterScope {
            First = 0,
            Global = 10,
            Controller = 20,
            Action = 30,
            Last = 100,
        }
}

به صورت پيش فرض، ابتدا فيلتري با محدوده اجراي كمتر، اجرا خواهد شد. در اينجا Global به معناي اجراي شدن در تمام كنترلرها است.


تعريف فيلترهاي سراسري

براي اينكه فيلتري را عمومي و سراسري تعريف كنيم، تنها كافي است آن‌را در متد Application_Start فايل Global.asax.cs به نحو زير معرفي نمائيم:

GobalFilters.Filters.Add(new AuthorizationFilterA() { Order = 2});

به اين ترتيب AuthorizationFilterA، به تمام كنترلرها و متدهاي قابل دسترسي آن‌ها در برنامه به صورت خودكار اعمال خواهد شد.
يكي از كاربردهاي فيلترهاي سراسري، نوشتن برنامه‌هاي پروفايلر است. برنامه‌هايي كه براي مثال مدت زمان اجراي متدها را ثبت كرده و بر اين اساس بهتر مي‌توان كارآيي قسمت‌هاي مختلف برنامه را دقيقا زيرنظر قرار داد.


يك نكته
كلاس كنترلر در ASP.NET MVC نيز يك فيلتر است:
public abstract class Controller : ControllerBase, IActionFilter, IAuthorizationFilter, IDisposable, IExceptionFilter, IResultFilter

به همين دليل، امكان تحريف متدهاي OnActionExecuting، OnActionExecuted و امثال آن كه پيشتر ذكر شد، در يك كنترلر نيز وجود دارد.
كلاس كنترلر داراي محدوده اجرايي First و Order ايي مساوي Int32.MinValue است. به اين ترتيب كنترلرها پيش از اجراي هر فيلتر ديگري اجرا خواهند شد.


ASP.NET MVC داراي يك سري فيلتر و متاديتاي توكار مانند OutputCache، HandleError، RequireHttps، ValidateInpute و غيره است كه توضيحات بيشتر آن‌ها به قسمت‌هاي بعد موكول مي‌گردد.