public static RouteValueDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes) { RouteValueDictionary result = new RouteValueDictionary(); if (htmlAttributes != null) { foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(htmlAttributes)) { result.Add(property.Name.Replace('_', '-'), property.GetValue(htmlAttributes)); } } return result; }
تاریخ شمسی با Extension Method برای DateTime
به کمک آن میتوان عبارات پیچیده T-SQL را Parse و آنالیز کرد. البته باید در نظر داشت هرچند این بسته جهت SQL Server 2012 ارائه شده اما این اسمبلی با نگارشهای 2005 به بعد اس کیوال سرور کاملا سازگار است و اساسا نیازی هم به SQL Server ندارد. در ادامه مروری خواهیم داشت بر نحوه استفاده از آن.
یافتن کوئریهای * Select در بین انبوهی از اسکریپتها به کمک SQLDom
در مورد مضرات کوئریهای * select پیشتر مطلبی را در این سایت خواندهاید. در ادامه قصد داریم به کمک امکانات اسمبلی Microsoft.SqlServer.TransactSql.ScriptDom.dll، تعدادی عبارت T-SQL را آنالیز کرده و مشخص کنیم که آیا حاوی * select هستند یا خیر. کد کامل آنرا در ذیل مشاهده میکنید:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Microsoft.SqlServer.TransactSql.ScriptDom; namespace DbCop { // Microsoft® SQL Server® 2012 Transact-SQL ScriptDom // SQL Server 2012 managed parser, Supports SQL Server 2005+ // SQLDom.msi (redist x86/x64) // http://www.microsoft.com/en-us/download/details.aspx?id=29065 // X86: http://go.microsoft.com/fwlink/?LinkID=239634&clcid=0x409 // X64: http://go.microsoft.com/fwlink/?LinkID=239635&clcid=0x409 // Program Files\Microsoft SQL Server\110\SDK\Assemblies\Microsoft.SqlServer.TransactSql.ScriptDom.dll class Program { static void Main() { const string tSql = @" -- select * in PROCEDURE CREATE PROCEDURE dbo.SelectStarTest AS SELECT * FROM dbo.tbl1 go -- select * in PROCEDURE with TableVar Create PRocedure SelectAll AS Declare @X table(Id integer) Select * from @x go -- select * in PROCEDURE with ctex CREATE PROCEDURE dbo.SelectAllCte AS WITH ctex AS ( SELECT * FROM sys.objects ) SELECT * FROM ctex go -- normal select * select * from tbl1; select * from dbo.tbl2; "; IList<ParseError> errors; TSqlScript sqlFragment; using (var reader = new StringReader(tSql)) { var parser = new TSql110Parser(initialQuotedIdentifiers: true); sqlFragment = (TSqlScript)parser.Parse(reader, out errors); } if (errors != null && errors.Any()) { var sb = new StringBuilder(); foreach (var error in errors) sb.AppendLine(error.Message); throw new InvalidOperationException(sb.ToString()); } var i = 0; foreach (var batch in sqlFragment.Batches) { Console.WriteLine("Batch: {0}, Statement(s): {1}", ++i, batch.Statements.Count); foreach (var statement in batch.Statements) { processStatement(statement); } Console.WriteLine(); } Console.WriteLine("\nPress a key..."); Console.Read(); } private static void processStatement(TSqlStatement statement) { var createProcedureStatement = statement as CreateProcedureStatement; if (createProcedureStatement != null) { var statementList = createProcedureStatement.StatementList; foreach (var procedureStatement in statementList.Statements) { processStatement(procedureStatement); } } var selectStatement = statement as SelectStatement; if (selectStatement != null) { var query = selectStatement.QueryExpression; var selectElements = ((QuerySpecification)query).SelectElements; foreach (var selectElement in selectElements) { var expression = selectElement as SelectStarExpression; if (expression == null) continue; Console.WriteLine( "`Select *` detected @StartOffset:{0}, Line:{1}, T-SQL: {2}", expression.StartOffset, expression.StartLine, statementToString(selectStatement)); } } } private static string statementToString(TSqlFragment selectStatement) { var text = new StringBuilder(); for (var i = selectStatement.FirstTokenIndex; i <= selectStatement.LastTokenIndex; i++) { text.Append(selectStatement.ScriptTokenStream[i].Text); } return text.ToString(); } } }
توضیحات:
پس از نصب SQLDom.msi، ارجاعی را به اسمبلی زیر اضافه نمائید تا بتوانید کد فوق را کامپایل کنید:
Program Files\Microsoft SQL Server\110\SDK\Assemblies\Microsoft.SqlServer.TransactSql.ScriptDom.dll
کار با ایجاد وهلهای از TSql110Parser شروع میشود. متد Parse آن، آرگومانی از نوع TextReader را قبول میکند. برای مثال با استفاده از StringReader میتوان محتوای یک متغیر رشتهای را به آن ارسال کرد و یا توسط StreamReader یک فایل sql را.
پس از فراخوانی متد Parse، بهتر است بررسی شود که آیا عبارت T-SQL دریافتی معتبر بوده است یا خیر. اینکار را توسط لیستی از ParseErrorهای دریافتی میتوان انجام داد.
خروجی متد Parse، حاوی یک سری Batch آنالیز شده است. هر عبارت Go در اینجا یک Batch را تشکیل میدهد. سپس در داخل هر batch به دنبال batch.Statements خواهیم گشت تا بتوان به عبارات T-SQL آنها دسترسی یافت.
در ادامه کار اصلی توسط متد processStatement صورت میگیرد. عبارات دریافتی، در حالت کلی از نوع TSqlStatement هستند اما در اصل میتوانند یکی از مشتقات آن نیز باشند. در اینجا فقط دو مورد CreateProcedureStatement و SelectStatement بررسی شدهاند (مطابق رشته tSql ابتدای مثال). هر دو عبارت، از کلاس TSqlStatement مشتق شدهاند.
در متد processStatement عبارات select معمولی و همچنین آنهایی که داخل رویههای ذخیره شده تعریف شدهاند، استخراج شده و در نهایت بررسی میشوند که آیا از نوع SelectStarExpression هستند یا خیر (همان * select صورت مساله).
خروجی مثال فوق به شرح زیر است:
Batch: 1, Statement(s): 1 `Select *` detected @StartOffset:140, Line:5, T-SQL: SELECT * FROM dbo.tbl1 Batch: 2, Statement(s): 1 `Select *` detected @StartOffset:368, Line:12, T-SQL: Select * from @x Batch: 3, Statement(s): 1 `Select *` detected @StartOffset:659, Line:22, T-SQL: WITH ctex AS ( SELECT * FROM sys.objects ) SELECT * FROM ctex Batch: 4, Statement(s): 2 `Select *` detected @StartOffset:753, Line:26, T-SQL: select * from tbl1; `Select *` detected @StartOffset:791, Line:27, T-SQL: select * from dbo.tbl2;
سؤال: چطور میشود برنامهای را که تنظیمات پروکسی ندارد، پروکسی خور کرد؟!
روشی که با سطح دسترسی معمولی و بدون نیاز به درایورهای خاص بررسی پکتهای TCP و UDP سیستم و همچنین توسط دات نت فریم ورک قابل استفاده باشد، استفاده از کتابخانهی معظم FiddlerCore است. برنامهی Fiddler توسط یکی از کارکنان سابق مایکروسافت و عضو پیشین تیم IE تهیه شدهاست. کار اصلی این برنامه، دیباگ درخواستهای HTTP/HTTPS، FTP و امثال آن است. هستهی اصلی آن نیز به صورت یک کتابخانهی مجزا به نام FiddlerCore در اختیار برنامه نویسهای دات نت است. این برنامه اخیرا توسط شرکت تلریک پشتیبانی و تملک شدهاست.
کتابخانهی FiddlerCore و برنامهی Fiddler را از اینجا میتوانید دریافت کنید. (اگر سایت آن باز نمیشود به این علت است که هاستینگ شرکت تلریک IPهای ایرانی را بسته است)
اسکلت اصلی یک برنامهی مبتنی بر FiddlerCore
using System; using System.Net; using System.Threading; using Fiddler; using System.Net.Security; namespace FiddlerTest { class Program { static void beforeRequest(Session oSession) { } static void Main(string[] args) { try { startFiddlerApplication(); Console.WriteLine("FiddlerCore started on port " + FiddlerApplication.oProxy.ListenPort); Console.WriteLine("Press any key to exit"); Console.ReadKey(); } finally { shutdownFiddlerApplication(); } } static void onLogString(object sender, LogEventArgs e) { Console.WriteLine("** LogString: " + e.LogString); } static void onNotification(object sender, NotificationEventArgs e) { Console.WriteLine("** NotifyUser: " + e.NotifyString); } static void onValidateServerCertificate(object sender, ValidateServerCertificateEventArgs e) { if (SslPolicyErrors.None == e.CertificatePolicyErrors) return; Console.WriteLine("invalid certificate: {0}", e.ServerCertificate.Subject); e.ValidityState = CertificateValidity.ForceValid; } static void shutdownFiddlerApplication() { FiddlerApplication.OnNotification -= onNotification; FiddlerApplication.Log.OnLogString -= onLogString; FiddlerApplication.BeforeRequest -= beforeRequest; FiddlerApplication.OnValidateServerCertificate -= onValidateServerCertificate; FiddlerApplication.oProxy.Detach(); FiddlerApplication.Shutdown(); Thread.Sleep(500); } private static void startFiddlerApplication() { FiddlerApplication.OnNotification += onNotification; FiddlerApplication.Log.OnLogString += onLogString; FiddlerApplication.BeforeRequest += beforeRequest; FiddlerApplication.OnValidateServerCertificate += onValidateServerCertificate; FiddlerApplication.Startup(5656, FiddlerCoreStartupFlags.RegisterAsSystemProxy | FiddlerCoreStartupFlags.MonitorAllConnections | FiddlerCoreStartupFlags.CaptureFTP); // proxy server on 5656 } } }
در متد FiddlerApplication.Startup روی پورتی مشخص، کار تنظیم پروکسی فیدلر انجام میشود. سپس مشخص میکنیم که چه مواردی را باید تحت نظر قرار دهد. با تنظیمات RegisterAsSystemProxy و MonitorAllConnections فیدلر قادر خواهد بود ترافیک وب اکثر برنامههای ویندوزی را مونیتور و دیباگ کند.
در متد shutdownFiddlerApplication نیز روالهای رخدادگردان، آزاد شده و پروکسی آن خاموش میشود.
هدایت درخواستهای وب کلیهی برنامهها به یک پروکسی مشخص
static void beforeRequest(Session oSession) { //send each request to the next proxy oSession["X-OverrideGateway"] = "socks=" + IPAddress.Loopback + ":" + 2002; //socks on 2002 }
هدایت درخواستهای تنها یک برنامهی خاص به یک پروکسی مشخص
در متد beforeRequest، متغیر oSession.LocalProcessID مشخص کنندهی مقدار PID پروسهای است که درخواست وب آن در حال بررسی است. برای بدست آوردن این PIDها در دات نت میتوان از متد Process.GetProcesses استفاده کرد. Id هر پروسه، همان LocalProcessID فیدلر است. بر این اساس میتوان تنها یک پروسهی مشخص را تحت نظر قرار داد و نه کل سیستم را.
کاربردها
- فرض کنید برنامهای تنظیمات پروکسی ندارد. با استفاده از روش فوق میتوان برای آن پروکسی تعریف کرد.
- فرض کنید برنامهای تنظیمات HTTP پروکسی دارد، اما پروکسی سرور شما از نوع ساکس است و نمیتوان از این پروکسی سرور در برنامهی مورد نظر استفاده کرد. X-OverrideGateway ذکر شده با هر دو نوع پروکسیهای HTTP و Socks کار میکند.
اگر علاقمند به مطالعهی اطلاعات بیشتری در مورد این کتابخانه هستید، کتاب 316 صفحهای Debugging with Fiddler نویسندهی اصلی آن، Eric Lawrence توصیه میشود.
معرفی برنامهی Process Proxifier
اگر اطلاعات فوق را کنار هم قرار دهیم و یک GUI نیز برای آن طراحی کنیم، به برنامهی Process Proxifier خواهیم رسید:
کار کردن با آن نیز بسیار سادهاست. در قسمت تنظیمات پیش فرض برنامه، آدرس IP و پورت پروکسی سرور خود را وارد کنید. نوع آنرا نیز مشخص نمائید که Socks است یا از نوع HTTP Proxy.
سپس در لیست پروسهها، مواردی را که لازم است از این پروکسی عبور کنند تیک بزنید. در اینجا میشود یا از تنظیمات پیش فرض استفاده کرد، یا میتوان به ازای هر پروسه، از یک پروکسی مجزا با تنظیماتی که ذکر میکنید، کمک گرفت. اگر صرفا یک پروسه را انتخاب کنید و اطلاعاتی را وارد ننمائید، از اطلاعات پروکسی پیش فرض استفاده خواهد شد.
دریافت سورس + باینری
ProcessProxifier_V1.0.rar
مثال دوم) به استفاده کننده از API کتابخانه خود، اجازه فرمول نویسی بدهید
برای نمونه مثال ساده زیر را درنظر بگیرید که در آن قرار است یک سری عدد که از منبع دادهای دریافت شدهاند، بر روی صفحه نمایش داده شوند:
public static void PrintNumbers() { var numbers = new[] { 1,2,3,5,7,90 }; // from a data source foreach(var item in numbers) { Console.WriteLine(item); } }
روال کار اکثر ابزارهای گزارشسازی موجود، ارائه یک زبان اسکریپتی جدید برای حل این نوع مسایل است. اما با استفاده از Func و ... روشهای Code first (بجای روشهای Wizard first)، خیلی از این رنج و دردها را میتوان سادهتر و بدون نیاز به اختراع و یا آموزش زبان جدیدی حل کرد:
public static void PrintNumbers(Func<int,string> formula) { var numbers = new[] { 1,2,3,5,7,90 }; // from a data source foreach(var item in numbers) { var data = formula(item); Console.WriteLine(data); } }
PrintNumbers(number => string.Format("{0:n0}",number));
PrintNumbers((number) =>{ return string.Format("{0:n0}",number); });
از این نوع طراحی، در ابزارها و کتابخانههای جدید گزارش سازی مخصوص ASP.NET MVC زیاد مشاهده میشوند.
مثال سوم) حذف کدهای تکراری برنامه
فرض کنید قصد دارید در برنامه وب خود مباحث caching را پیاده سازی کنید:
using System; using System.Web; using System.Web.Caching; using System.Collections.Generic; namespace WebToolkit { public static class CacheManager { public static void CacheInsert(this HttpContextBase httpContext, string key, object data, int durationMinutes) { if (data == null) return; httpContext.Cache.Add( key, data, null, DateTime.Now.AddMinutes(durationMinutes), TimeSpan.Zero, CacheItemPriority.AboveNormal, null); } } }
var item = httpContext.Cache[key]; if (item == null) { item = ReadDataFromDataSource(); if (item == null) return null; CacheInsert(httpContext, key, item, durationMinutes); }
میتوان در این الگوی تکراری، خواندن اطلاعات را از منبع داده، به یک Func واگذار کرد و به این صورت کدهای ما به نحو زیر بازسازی خواهند شد:
using System; using System.Web; using System.Web.Caching; using System.Collections.Generic; namespace WebToolkit { public static class CacheManager { public static void CacheInsert(this HttpContextBase httpContext, string key, object data, int durationMinutes) { if (data == null) return; httpContext.Cache.Add( key, data, null, DateTime.Now.AddMinutes(durationMinutes), TimeSpan.Zero, CacheItemPriority.AboveNormal, null); } public static T CacheRead<T>(this HttpContextBase httpContext, string key, int durationMinutes, Func<T> ifNullRetrievalMethod) { var item = httpContext.Cache[key]; if (item == null) { item = ifNullRetrievalMethod(); if (item == null) return default(T); CacheInsert(httpContext, key, item, durationMinutes); } return (T)item; } } }
var user = HttpContext.CacheRead( "Key1", 15, () => _usersService.FindUser(userId));
@Html.ActionLink("text", "Index", "Home")
@Html.ActionLink("text", result: MVC.Home.Index())
پروژهی T4MVC توسط یکی از اعضای تیم ASP.NET تهیه شدهاست. همچنین مدتی است مایکروسافت پروژهی دیگری را نیز به نام Microsoft.AspNet.Mvc.Futures در حال تهیه و آزمایش دارد که از آن نیز میتوان برای تولید لینکهای Strongly typed استفاده کرد.
نصب کتابخانهی Microsoft ASP.NET MVC Futures
برای نصب کتابخانهی آیندهی ASP.NET MVC، تنها کافی است دستور ذیل را در کنسول پاورشل نیوگت صادر کنید:
PM> Install-Package Microsoft.AspNet.Mvc.Futures
نحوهی تعریف مسیرهای Strongly typed، توسط کتابخانهی آیندهی ASP.NET MVC
پس از نصب بستهی Microsoft.AspNet.Mvc.Futures، جهت سهولت کار نیاز است اسمبلی آنرا که Microsoft.Web.Mvc.dll نام دارد، به تمام صفحات سایت معرفی کنیم. برای این منظور فایل web.config پوشهی views را گشوده و یک سطر تعریف فضای نام Microsoft.Web.Mvc را به آن اضافه کنید:
<system.web.webPages.razor> <host /> <pages pageBaseType="System.Web.Mvc.WebViewPage"> <namespaces> <!-- سایر تعاریف --> <add namespace="Microsoft.Web.Mvc"/> <!-- این سطر اضافه شود --> </namespaces> </pages> </system.web.webPages.razor>
یک نکتهی مهم
این بسته در حال حاضر هرچند دارای پوشهی دات نت 4 است، اما عملا برای دات نت 4.5 یا به عبارتی ASP.NET MVC 5 کامپایل شدهاست و در ASP.NET MVC 4 قابل استفاده نیست:
The primary reference "Microsoft.Web.Mvc" could not be resolved because it was built against the ".NETFramework,Version=v4.5" framework. This is a higher version than the currently targeted framework ".NETFramework,Version=v4.0"
ActionLinkهای جدید بستهی Microsoft.AspNet.Mvc.Futures
به این ترتیب برای مثال در مورد ActionLink، دو overload جدید را میتوان در Viewها استفاده کرد:
public static System.Web.Mvc.MvcHtmlString ActionLink<TController>(this System.Web.Mvc.HtmlHelper helper, System.Linq.Expressions.Expression<Action<TController>> action, string linkText) public static System.Web.Mvc.MvcHtmlString ActionLink<TController>(this System.Web.Mvc.HtmlHelper helper, System.Linq.Expressions.Expression<Action<TController>> action, string linkText, object htmlAttributes)
@(Html.ActionLink<HomeController>(action => action.Index(id: 1), "Test"))
همچنین اگر اکشن متد Index کنترلر HomeController دارای پارامتر نیز باشد، در همینجا قابل مقدار دهی است.
RenderAction و BeginFormهای جدید بستهی Microsoft.AspNet.Mvc.Futures
از این نوع متدهای الحاقی Expression Action دار، برای RenderAction و BeginForm نیز طراحی شدهاند:
public static void RenderAction<TController>(this System.Web.Mvc.HtmlHelper helper, System.Linq.Expressions.Expression<Action<TController>> action) public static System.Web.Mvc.Html.MvcForm BeginForm<TController>(this System.Web.Mvc.HtmlHelper helper, System.Linq.Expressions.Expression<Action<TController>> action, System.Web.Mvc.FormMethod method, System.Collections.Generic.IDictionary<string,object> htmlAttributes)
@{ Html.RenderAction<HomeController>(action => action.Index(id: 1)); }
@using (Html.BeginForm<HomeController>(action => action.Index(null))) { }
RedirectToAction جدید بستهی Microsoft.AspNet.Mvc.Futures
به همراه دو متد کمکی Expression Action دار، جهت استفاده در متدهای کنترلرهای سایت؛ برای ساخت Url و همچنین redirect به یک اکشن متد دیگر:
public static string BuildUrlFromExpression<TController>(System.Web.Routing.RequestContext context, System.Web.Routing.RouteCollection routeCollection, System.Linq.Expressions.Expression<Action<TController>> action) public static System.Web.Mvc.RedirectToRouteResult RedirectToAction<TController>(this System.Web.Mvc.Controller controller, System.Linq.Expressions.Expression<Action<TController>> action)
using System.Web.Mvc; using Microsoft.Web.Mvc; namespace MVC5Basic.Controllers { public class HomeController : Controller { public ActionResult Index() { var link = LinkBuilder.BuildUrlFromExpression<HomeController>( this.Request.RequestContext, null, action => action.About()); this.RedirectToAction<HomeController>(action => action.About()); return View(); } public ActionResult About() { ViewBag.Message = "Your application description page."; return View(); } } }
کدامیک بهتر است؟ T4MVC یا ASP.NET MVC Futures ؟
T4MVC موارد بیشتری را پوشش میدهد؛ حتی مسیرهای تصاویر ثابت و فایلهای js را نیز میتوان توسط آن تعریف کرد. فقط نگهداری آن هر بار نیاز به اجرای فایل t4 مرتبط با آن دارد و در اینجا کار با ASP.NET MVC Futures سادهتر است.
برای مطالعه بیشتر
بررسی که در اینجا صورت گرفت صرفا در مورد امکانات تولید مسیرهای strongly typed این کتابخانه است. سایر امکانات آنرا در مطلب ذیل میتوانید پیگیری کنید:
Using the Features of ASP.NET MVC 3 Futures
ASP.NET MVC #14
آشنایی با نحوه معرفی تعاریف طرحبندی سایت به کمک Razor
ممکن است یک سری از اصطلاحات را در قسمتهای قبل مانند master page در لابلای توضیحات ارائه شده، مشاهده کرده باشید. این نوع مفاهیم برای برنامه نویسهای ASP.NET Web forms آشنا است (و اگر با Web forms view engine در ASP.NET MVC کار کنید، دقیقا یکی است؛ البته با این تفاوت که فایل code behind آنها حذف شده است). به همین جهت در این قسمت برای تکمیل بحث، مروری خواهیم داشت بر نحوهی معرفی جدید آنها توسط Razor.
در یک پروژه جدید ASP.NET MVC و در پوشه Views\Shared\_Layout.cshtml آن، فایل Layout آن، مفهوم master page را دارد. در این نوع فایلها، زیر ساخت مشترک تمام صفحات سایت قرار میگیرند:
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>
<body>
@RenderBody()
</body>
</html>
اگر دقت کرده باشید، در هیچکدام از فایلهای Viewایی که تا این قسمت به پروژههای مختلف اضافه کردیم، تگهایی مانند body، title و امثال آن وجود نداشتند. در ASP.NET مرسوم است کلیه اطلاعات تکراری صفحات مختلف سایت را مانند تگهای یاد شده به همراه منویی که باید در تمام صفحات قرار گیرد یا footer مشترک بین تمام صفحات سایت، به یک فایل اصلی به نام master page که در اینجا layout نام گرفته، Refactor کنند. به این ترتیب حجم کدها و markup تکراری که باید در تمام Viewهای سایت قرار گیرند به حداقل خواهد رسید.
برای مثال محل قرار گیری تعاریف Content-Type تمام صفحات و همچنین favicon سایت، بهتر است در فایل layout باشد و نه در تک تک Viewهای تعریف شده:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="shortcut icon" href="@Url.Content("~/favicon.ico")" type="image/x-icon" />
در کدهای فوق یک نمونه پیش فرض فایل layout را مشاهده میکنید. در اینجا توسط متد RenderBody، محتوای رندر شده یک View درخواستی، به داخل تگ body تزریق خواهد شد.
تا اینجا در تمام مثالهای قبلی این سری، فایل layout در Viewهای اضافه شده معرفی نشد. اما اگر برنامه را اجرا کنیم باز هم به نظر میرسد که فایل layout اعمال شده است. علت این است که در صورت عدم تعریف صریح layout در یک View، این تعریف از فایل Views\_ViewStart.cshtml دریافت میگردد:
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
فایل ViewStart، محل تعریف کدهای تکراری است که باید پیش از اجرای هر View مقدار دهی یا اجرا شوند. برای مثال در اینجا میشود بر اساس نوع مرورگر، layout خاصی را به تمام Viewها اعمال کرد. مثلا یک layout ویژه برای مرورگرهای موبایلها و layout ایی دیگر برای مرورگرهای معمولی. امکان دسترسی به متغیرهای تعریف شده در یک View در فایل ViewStart از طریق ViewContext.ViewData میسر است.
ضمن اینکه باید درنظر داشت که میتوان فایل ViewStart را در زیر پوشههای پوشه اصلی View نیز قرار داد. مثلا اگر فایل ViewStart ایی در پوشه Views/Home قرار گرفت، این فایل محتوای ViewStart اصلی قرار گرفته در ریشه پوشه Views را بازنویسی خواهد کرد.
برای معرفی صریح فایل layout، تنها کافی است مسیر کامل فایل layout را در یک View مشخص کنیم:
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Index</h2>
اهمیت این مساله هم در اینجا است که یک سایت میتواند چندین layout یا master page داشته باشد. برای نمونه یک layout برای صفحات ورود اطلاعات؛ یک layout خاص هم مثلا برای صفحات گزارش گیری نهایی سایت.
همانطور که پیشتر نیز ذکر شد، در ASP.NET حرف ~ به معنای ریشه سایت است که در اینجا ابتدای محل جستجوی فایل layout را مشخص میکند.
به این ترتیب زمانیکه یک کنترلر، View خاصی را فراخوانی میکند، کار از فایل Views\Shared\_Layout.cshtml شروع خواهد شد. سپس View درخواستی پردازش شده و محتوای نهایی آن، جایی که متد RenderBody قرار دارد، تزریق خواهد شد.
همچنین مقدار ViewBag.Title ایی که در فایل View تعریف شده، در فایل layout جهت رندر مقدار تگ title استفاده میشود (انتقال یک متغیر از View به یک فایل master page؛ کلاس layout، مدل View ایی را که قرار است رندر کند به ارث میبرد).
یک نکته:
در نگارش سوم ASP.NET MVC امکان بکارگیری حرف ~ به صورت مستقیم در حین تعریف یک فایل js یا css وجود ندارد و حتما باید از متد سمت سرور Url.Content کمک گرفت. در نگارش چهارم ASP.NET MVC، این محدودیت برطرف شده و دقیقا همانند متغیر Layout ایی که در بالا مشاهده میکنید، میتوان بدون نیاز به متد Url.Content، مستقیما از حرف ~ کمک گرفت و به صورت خودکار پردازش خواهد شد.
تزریق نواحی ویژه یک View در فایل layout
توسط متد RenderBody، کل محتوای View درخواستی در موقعیت تعریف شده آن در فایل Layout، رندر میشود. این ویژگی به نحو یکسانی به تمام Viewها اعمال میشود. اما اگر نیاز باشد تا view بتواند محتوای markup قسمت ویژهای از master page را مقدار دهی کند، میتوان از مفهومی به نام Sections استفاده کرد:
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>
<body>
<div id="menu">
@if (IsSectionDefined("Menu"))
{
RenderSection("Menu", required: false);
}
else
{
<span>This is the default ...!</span>
}
</div>
<div id="body">
@RenderBody()
</div>
</body>
</html>
در اینجا ابتدا بررسی میشود که آیا قسمتی به نام Menu در View جاری که باید رندر شود وجود دارد یا خیر. اگر بله، توسط متد RenderSection، آن قسمت نمایش داده خواهد شد. در غیراینصورت، محتوای پیش فرضی را در صفحه قرار میدهد. البته اگر از متد RenderSection با آرگومان required: false استفاده شود، درصورتیکه View جاری حاوی قسمتی به نام مثلا menu نباشد، تنها چیزی نمایش داده نخواهد شد. اگر این آرگومان را حذف کنیم، یک استثنای عدم یافت شدن ناحیه یا قسمت مورد نظر صادر میگردد.
نحوهی تعریف یک Section در Viewهای برنامه به شکل زیر است:
@{
ViewBag.Title = "Index";
//Layout = null;
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>
Index</h2>
@section Menu{
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
}
برای مثال فرض کنید که یکی از Viewهای شما نیاز به دو فایل اضافی جاوا اسکریپت برای اجرای صحیح خود دارد. میتوان تعاریف الحاق این دو فایل را در قسمت header فایل layout قرار داد تا در تمام Viewها به صورت خودکار لحاظ شوند. یا اینکه یک section سفارشی را به نحو زیر در آن View خاص تعریف میکنیم:
@section JavaScript
{
<script type="text/javascript" src="@Url.Content("~/Scripts/SomeScript.js")" />;
<script type="text/javascript" src="@Url.Content("~/Scripts/AnotherScript.js")" />;
}
سپس کافی است جهت تزریق این کدها به header تعریف شده در master page مورد نظر، یک سطر زیر را اضافه کرد:
@RenderSection("JavaScript", required: false)
به این ترتیب، اگر view ایی حاوی تعریف قسمت JavaScript نبود، به صورت خودکار شامل تعاریف الحاق اسکریپتهای یاد شده نیز نخواهد گردید. در نتیجه دارای حجمی کمتر و سرعت بارگذاری بالاتری نیز خواهد بود.
مدیریت بهتر فایلها و پوشههای یک برنامه ASP.NET MVC به کمک Areas
به کمک قابلیتی به نام Areas میتوان یک برنامه بزرگ را به چندین قسمت کوچکتر تقسیم کرد. هر کدام از این نواحی، دارای تعاریف مسیریابی، کنترلرها و Viewهای خاص خودشان هستند. به این ترتیب دیگر به یک برنامهی از کنترل خارج شده ASP.NET MVC که دارای یک پوشه Views به همراه صدها زیر پوشه است، نخواهیم رسید و کنترل این نوع برنامههای بزرگ سادهتر خواهد شد.
برای مثال یک برنامه بزرگ را درنظر بگیرید که به کمک قابلیت Areas، به نواحی ویژهای مانند گزارشگیری، قسمت ویژه مدیریتی، قسمت کاربران، ناحیه بلاگ سایت، ناحیه انجمن سایت و غیره، تقسیم شده است. به علاوه هر کدام از این نواحی نیز هنوز میتوانند از اطلاعات ناحیه اصلی برنامه مانند master page آن استفاده کنند. البته باید درنظر داشت که فایل viewStart به پوشه جاری و زیر پوشههای آن اعمال میشود. اگر نیاز باشد تا اطلاعات این فایل به کل برنامه اعمال شود، فقط کافی است آنرا به یک سطح بالاتر، یعنی ریشه سایت منتقل کرد.
نحوه افزودن نواحی جدید
افزودن یک Area جدید هم بسیار ساده است. بر روی نام پروژه در VS.NET کلیک راست کرده و سپس گزینه Add|Area را انتخاب کنید. سپس در صفحه باز شده، نام دلخواهی را وارد نمائید. مثلا نام Reporting را وارد نمائید تا ناحیه گزارشگیری برنامه از قسمتهای دیگر آن مستقل شود. پس از افزودن یک Area جدید، به صورت خودکار پوشه جدیدی به نام Areas به ریشه سایت اضافه میشود. سپس داخل آن، پوشهی دیگری به نام Reporting اضافه خواهد شد. پوشه reporting اضافه شده هم دارای پوشههای Model، Views و Controllers خاص خود میباشد.
اکنون که پوشه Areas به ریشه سایت اضافه شده است، با کلیک راست بر روی این پوشه نیز گزینهی Add|Area در دسترس میباشد. برای نمونه یک ناحیه جدید دیگر را به نام Admin به سایت اضافه کنید تا بتوان امکانات مدیریتی سایت را از سایر قسمتهای آن مستقل کرد.
نحوه معرفی تعاریف مسیریابی نواحی تعریف شده
پس از اینکه کار با Areas را آغاز کردیم، نیاز است تا با نحوهی مسیریابی آنها نیز آشنا شویم. برای این منظور فایل Global.asax.cs قرار گرفته در ریشه اصلی برنامه را باز کنید. در متد Application_Start، متدی به نام AreaRegistration.RegisterAllAreas، کار ثبت و معرفی تمام نواحی ثبت شده را به فریم ورک، به عهده دارد. کاری که در پشت صحنه انجام خواهد شد این است که به کمک Reflection تمام کلاسهای مشتق شده از کلاس پایه AreaRegistration به صورت خودکار یافت شده و پردازش خواهند شد. این کلاسها هم به صورت پیش فرض به نام SomeNameAreaRegistration.cs در ریشه اصلی هر Area توسط VS.NET تولید میشوند. برای نمونه فایل ReportingAreaRegistration.cs تولید شده، حاوی اطلاعات زیر است:
using System.Web.Mvc;
namespace MvcApplication11.Areas.Reporting
{
public class ReportingAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Reporting";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Reporting_default",
"Reporting/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}
توسط AreaName، یک نام منحصربفرد در اختیار فریم ورک قرار خواهد گرفت. همچنین از این نام برای ایجاد پیوند بین نواحی مختلف نیز استفاده میشود.
سپس در قسمت RegisterArea، یک مسیریابی ویژه خاص ناحیه جاری مشخص گردیده است. برای مثال تمام آدرسهای ناحیه گزارشگیری سایت باید با http://localhost/reporting آغاز شوند تا مورد پردازش قرارگیرند. سایر مباحث آن هم مانند قبل است. برای مثال در اینجا نام اکشن متد پیش فرض، index تعریف شده و همچنین ذکر قسمت id نیز اختیاری است.
همانطور که ملاحظه میکنید، تعاریف مسیریابی و اطلاعات پیش فرض آن منطقی هستند و آنچنان نیازی به دستکاری و تغییر ندارند. البته اگر دقت کرده باشید مقدار نام controller پیش فرض، مشخص نشده است. بنابراین بد نیست که مثلا نام Home یا هر نام مورد نظر دیگری را به عنوان نام کنترلر پیش فرض در اینجا اضافه کرد.
تعاریف کنترلرهای هم نام در نواحی مختلف
در ادامه مثال جاری که دو ناحیه Admin و Reporting به آن اضافه شده، به پوشههای Controllers هر کدام، یک کنترلر جدید را به نام HomeController اضافه کنید. همچنین این HomeController را در ناحیه اصلی و ریشه سایت نیز اضافه نمائید. سپس برای متد پیش فرض Index هر کدام هم یک View جدید را با کلیک راست بر روی نام متد و انتخاب گزینه Add view، اضافه کنید. اکنون برنامه را به همین نحو اجرا نمائید. اجرای برنامه با خطای زیر متوقف خواهد شد:
Multiple types were found that match the controller named 'Home'. This can happen if the route that services this
request ('{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request.
If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
The request for 'Home' has found the following matching controllers:
MvcApplication11.Areas.Admin.Controllers.HomeController
MvcApplication11.Controllers.HomeController
فوق العاده خطای کاملی است و راه حل را هم ارائه داده است! برای اینکه مشکل ابهام یافتن HomeController برطرف شود، باید این جستجو را به فضاهای نام هر قسمت از نواحی برنامه محدود کرد (چون به صورت پیش فرض فضای نامی برای آن مشخص نشده، کل ناحیه ریشه سایت و زیر مجموعههای آنرا جستجو خواهد کرد). به همین جهت فایل Global.asax.cs را گشوده و متد RegisterRoutes آنرا مثلا به نحو زیر اصلاح نمائید:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
, namespaces: new[] { "MvcApplication11.Controllers" }
);
}
آرگومان چهارم معرفی شده، آرایهای از نامهای فضاهای نام مورد نظر را جهت یافتن کنترلرهایی که باید توسط این مسیریابی یافت شوند، تعریف میکند.
اکنون اگر مجددا برنامه را اجرا کنیم، بدون مشکل View متناظر با متد Index کنترلر Home نمایش داده خواهد شد.
البته این مشکل با نواحی ویژه و غیر اصلی سایت وجود ندارد؛ چون جستجوی پیش فرض کنترلرها بر اساس ناحیه است.
در ادامه مسیر http://localhost/Admin/Home را نیز در مرورگر وارد کنید. سپس بر روی صفحه در مروگر کلیک راست کرده و سورس صفحه را بررسی کنید. مشاهده خواهید کرد که master page یا فایل layout ایی به آن اعمال نشده است. علت را هم در ابتدای بحث Areas مطالعه کردید. فایل Views\_ViewStart.cshtml در سطحی که قرار دارد به ناحیه Admin اعمال نمیشود. آنرا به ریشه سایت منتقل کنید تا layout اصلی سایت نیز به این قسمت اعمال گردد. البته بدیهی است که هر ناحیه میتواند layout خاص خودش را داشته باشد یا حتی میتوان با مقدار دهی خاصیت Layout نیز در هر view، فایل master page ویژهای را انتخاب و معرفی کرد.
نحوه ایجاد پیوند بین نواحی مختلف سایت
زمانیکه پیوندی را به شکل زیر تعریف میکنیم:
@Html.ActionLink(linkText: "Home", actionName: "Index", controllerName: "Home")
یعنی ایجاد لینکی در ناحیه جاری. برای اینکه پیوند تعریف شده به ناحیهای خارج از ناحیه جاری اشاره کند باید نام Area را صریحا ذکر کرد:
@Html.ActionLink(linkText: "Home", actionName: "Index", controllerName: "Home",
routeValues: new { Area = "Admin" } , htmlAttributes: null)
همین نکته را باید حین کار با متد RedirectToAction نیز درنظر داشت:
public ActionResult Index()
{
return RedirectToAction("Index", "Home", new { Area = "Admin" });
}
public static class TestDependencyScope { private static IContainer _currentNestedContainer; public static void Begin() { if (_currentNestedContainer != null) throw new Exception("Cannot begin test dependency scope. Another dependency scope is still in effect."); _currentNestedContainer = IoC.Container.GetNestedContainer(); } public static IContainer CurrentNestedContainer { get { if (_currentNestedContainer == null) throw new Exception($"Cannot access the {nameof(CurrentNestedContainer)}. There is no dependency scope in effect."); return _currentNestedContainer; } } public static void End() { if (_currentNestedContainer == null) throw new Exception("Cannot end test dependency scope. There is no dependency scope in effect."); _currentNestedContainer.Dispose(); _currentNestedContainer = null; } }
public class ContainerPerTestCaseAttribute : Attribute, ITestAction { public void BeforeTest(ITest test) { TestDependencyScope.Begin(); } public void AfterTest(ITest test) { TestDependencyScope.End(); } public ActionTargets Targets => ActionTargets.Test; }
[PopulateHttpContext] [ContainerPerTestCase] [Transactional] [TestFixture] public class IntegratedTestBase { [SetUp] public void EachTestSetUp() { BeforeEachTest(); } [TearDown] public void EachTestTearDown() { AfterEachTest(); } protected virtual void BeforeEachTest() { } protected virtual void AfterEachTest() { } protected void UsingUnitOfWork(Action<IUnitOfWork> action) { IoC.Container.Using((IUnitOfWork uow)=> { uow.DisableAllFilters(); action(uow); }); } protected T UsingUnitOfWork<T>(Func<IUnitOfWork, T> func) { var uow = IoC.Resolve<IUnitOfWork>(); uow.DisableAllFilters(); using (uow) { var result = func(uow); uow.SaveChanges(); return result; } } protected async Task<T> UsingUnitOfWorkAsync<T>(Func<IUnitOfWork, Task<T>> func) { var uow = IoC.Resolve<IUnitOfWork>(); uow.DisableAllFilters(); using (uow) { var result = await func(uow).ConfigureAwait(false); await uow.SaveChangesAsync().ConfigureAwait(false); return result; } } }
namespace ProjectName.ServiceLayer.IntegrationTests { public static class Testing { private static IContainer Container => TestDependencyScope.CurrentNestedContainer; public static T Resolve<T>() { return Container.GetInstance<T>(); } public static object Resolve(Type type) { return Container.GetInstance(type); } public static void Inject<T>(T instance) where T : class { Container.Inject(instance); } } } //in test classes using static ProjectName.ServiceLayer.IntegrationTests.Testing; namespace ProjectName.ServiceLayer.IntegrationTests { public class RoleServiceTests : IntegratedTestBase { private IRoleService _service; protected override void BeforeEachTest() { _service = Resolve<IRoleService>(); } } }
خلاص شدن از شر deep null check
- استفاده از روشهای AOP مانند Minimizing the null ref with dynamic proxies
- استفاده از expression trees مانند Avoiding nulls with expression trees
namespace Microsoft.Extensions.DependencyInjection { public interface IServiceCollection : ICollection<ServiceDescriptor>, IEnumerable<ServiceDescriptor>, IEnumerable, IList<ServiceDescriptor> { } }
ServiceProvider و مؤلفههای درونی آن، از یک مجموعه از ServiceDescriptorها برای برنامهی شما بر اساس سرویسهای ثبت شدهی توسط IServiceCollection استفاده میکنند. ServiceDescriptor حاوی اطلاعاتی در مورد سرویسهای ثبت شدهاست. اگر به کد منبع این کلاس برویم، میبینیم پنج Property اصلی دارد که با استفاده از آنها اطلاعات یک سرویس ثبت و نگهداری میشوند. با استفاده از این اطلاعات در هنگام اجرا ، DI Container به واکشی و ساخت نمونههایی از سرویس درخواستی اقدام میکند:
public Type ImplementationType { get; } public object ImplementationInstance { get; } public Func<IServiceProvider, object> ImplementationFactory { get; } public ServiceLifetime Lifetime { get; } public Type ServiceType { get; }
هر کدام از این Property ها کاربرد خاص خود را دارند:
- · ServiceType : نوع سرویسی را که میخواهیم ثبت شود، مشخص میکنیم ( مثلا اینترفیس IMessageService ) .
- · ImplementionType : نوع پیاده سازی سرویس مورد نظرمان را مشخص میکند ( مثلا کلاس MessageService ).
- · LifeTime : طول حیات سرویس را مشخص میکند. DI Container بر اساس این ویژگی، اقدام به ساخت و از بین بردن نمونههایی از سرویس میکند.
- · ImplementionInstance : نمونهی ساخته شدهی از سرویس است.
- · ImplementionFactory : یک Delegate است که چگونگی ساخته شدن یک نمونه از پیاده سازی سرویس را در خود نگه میدارد. این Delegate یک IServiceProvider را به عنوان ورودی دریافت میکند و یک object را بازگشت میدهد.
به صورت عادی، در سناریوهای معمول ثبت سرویسها درون IServiceCollection، نیازی به استفاده از ServiceDescriptor نیست؛ ولی اگر بخواهیم سرویسها را به روشهای پیشرفتهتری ثبت کنیم، مجبوریم که به صورت مستقیم با این کلاس کار کنیم.
می توانیم یک ServiceDesciriptor را به روشهای زیر تعریف کنیم:
var serviceDescriptor1 = new ServiceDescriptor( typeof(IMessageServiceB), typeof(MessageServiceBB), ServiceLifetime.Scoped); var serviceDescriptor2 = ServiceDescriptor.Describe( typeof(IMessageServiceB), typeof(MessageServiceBB), ServiceLifetime.Scoped); var serviceDescriptor3 = ServiceDescriptor.Singleton(typeof(IMessageServiceB), typeof(MessageServiceBB)); var serviceDescriptor4 = ServiceDescriptor.Singleton<IMessageServiceB, MessageServiceBB>();
همانطور که دیدیم، IServiceCollection در
واقع لیست و مجموعهای از اشیاء است که از نمونههای جنریک IServiceCollection ، IList ، IEnumerable و Ienumberabl ارث بری میکند؛ بنابراین میتوان از متدهای تعریف شدهی در این
اینترفیسها برای IServiceCollection نیز استفاده کرد. حالا ما برای اضافه کردن این سرویسهای جدید،
بدین طریق عمل میکنیم:
Services.Add(serviceDescriptor1);
استفاده از متدهای TryAdd()
به کد زیر نگاه کنید :
services.AddScoped<IMessageServiceB, MessageServiceBA>(); services.AddScoped<IMessageServiceB, MessageServiceBB>();
برای جلوگیری از این خطا میتوانیم از متدهای TryAddSingleton() ، TryAddScoped() و TryAddTransient() استفاده کنیم. این متدها درون فضای نام Microsoft.Extionsion.DependencyInjection.Extension قرار دارند.
عملکرد کلی این
متدها درست مثل متدهای Add() است؛ با این تفاوت که این متد ابتدا IServiceCollection را جستجو میکند و اگر برای type مورد نظر سرویسی ثبت نشده بود،
آن را ثبت میکند:
services.TryAddScoped<IMessageServiceB, MessageServiceBA>(); services.TryAddScoped<IMessageServiceB, MessageServiceBB>();
جایگذاری یک سرویس با نمونهای دیگر
گاهی اوقات میخواهیم یک پیاده سازی دیگر را بجای پیاده سازی فعلی، در DI Container ثبت کنیم. در این حالت از متد Replace() بر روی IServiceCollection برای این کار استفاده میکنیم. این متد فقط یک ServiceDescriptor را به عنوان پارامتر ورودی میگیرد:
services.Replace(serviceDescriptor3);
services.RemoveAll<IMessageServiceB>();
معمولا در پروژههای معمول خودمان نیازی به استفاده از Replace() و RemoveAll() نداریم؛ مگر اینکه بخواهیم پیاده سازی اختصاصی خودمان را برای سرویسهای درونی فریم ورک یا کتابخانههای شخص ثالث، بجای پیاده سازی پیش فرض، ثبت و استفاده کنیم.
AddEnumerable()
فرض کنید دارید برنامهی نوبت دهی یک کلینیک را مینویسید و به صورت پیش فرض از شما خواستهاند که هنگام صدور نوبت، این قوانین را بررسی کنید:
- هر شخص در هفته نتواند بیش از 2 نوبت برای یک تخصص بگیرد.
- اگر شخص در ماه بیش از 3 نوبت رزرو شده داشته باشد ولی مراجعه نکرده باشد، تا پایان ماه، امکان رزرو نوبت را نداشته باشد .
- تعداد نوبتهای ثبت شدهی برای پزشک در آن روز نباید بیش از تعدادی باشد که پزشک پذیرش میکند.
- و ...
یک روش معمول برای پیاده سازی این قابلیت، ساخت سرویسی برای ثبت نوبت است که درون آن متدی برای بررسی کردن قوانین ثبت نام وجود دارد. خب، ما این کار را انجام میدهیم. تستهای واحد و تستهای جامع را هم مینویسیم و بعد برنامه را انتشار میدهیم و همه چیز خوب است؛ تا اینکه مالک محصول یک نیازمندی جدید را میخواهد که در آن ما باید قانون زیر را در هنگام ثبت نوبت بررسی کنیم:
- نوبتهای ثبت شده برای یک شخص نباید دارای تداخل باشند.
در این حالت ما باید دوباره سرویس Register را باز کنیم و به متد بررسی کردن قوانین برویم و دوباره کدهایی را برای بررسی کردن قانون جدید بنویسیم و احتمالا کد ما به این صورت خواهد شد:
public class RegisterAppointmentService : RegisterAppointmentService { public Task<Result> RegisterAsync( PatientInfoDTO patientIfno , DateTimeOffset requestedDateTime , PhysicianId phusicianId ) { CheckRegisterantionRule(patientInfo); // code here } private Task CheckRegisterationRule(PatientInfoDTO patientInfo) { CheckRule1(patientInfo); CheckRule2(patientInfo); CheckRule3(patientInfo); } }
در این حالت باید به ازای هر قانون جدید، به متد CheckRegisterationRule برویم و به ازای هر قانون، یک متد private جدید را بسازیم. مشکل این روش این است که در این حالت ما مجبوریم با هر کم و زیاد شدن قانون، این کلاس را باز کنیم و آن را تغییر دهیم و با هر تغییر دوباره، تستهای واحد آن را دوباره نویسی کنیم. در یک کلام در کد بالا اصول Separation of Concern و Open/Closed Principle را رعایت نمیشود.
یک راهکار این است که یک
سرویس جداگانه را برای بررسی کردن قوانین بنویسیم و آن را به سرویس ثبت نوبت تزریق کنیم:
public class ICheckRegisterationRuleForAppointmentService : ICheckRegisterationRuleForAppointmentService { public Task CheckRegisterantionRule(PatientInfoDTO patientInfo) { CheckRule1(patientInfo); CheckRule2(patientInfo); CheckRule3(patientInfo); } } public class RegisterAppointmentService : IRegisterAppointmentService { private ICheckRegisterationRuleForAppointmentService _ruleChecker; public RegisterAppointmentService (RegisterAppointmentService ruleChecker) { _ruleChecker = ruleChecker; } public Task<Result> RegisterAsync( PatientInfoDTO patientIfno , DateTimeOffset requestedDateTime , PhysicianId phusicianId ) { _ruleChecker.CheckRegisterantionRule(patientInfo); // code here } }
با این کار وظیفهی چک کردن قوانین و وظیفهی ثبت و ذخیره سازی قوانین را از یکدیگر جدا کردیم؛ ولی همچنان در سرویس بررسی کردن قوانین، اصل Open/Closed رعایت نشدهاست. خب راه حل چیست !؟
یکی از راه حلهای موجود، استفاده از الگوی قوانین یا Rule Pattern است. برای اجرای این الگو، میتوانیم با تعریف یک اینترفیس کلی برای بررسی کردن قانون، به ازای هر قانون یک پیاده سازی اختصاصی را داشته باشیم:
interface IAppointmentRegisterationRule { Task CheckRule(PatientInfo patientIfno); } public class AppointmentRegisterationRule1 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) { Console.WriteLine("Rule 1 is checked"); return Task.CompletedTask; } } public class AppointmentRegisterationRule2 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) {
Console.WriteLine("Rule 2 is checked"); return Task.CompletedTask; } } public class AppointmentRegisterationRule3 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) { Console.WriteLine("Rule 3 is checked"); return Task.CompletedTask; } } public class AppointmentRegisterationRule4 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) { Console.WriteLine("Rule 4 is checked"); return Task.CompletedTask; } }
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule1>(); services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule2>(); services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule3>(); services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule4>();
public class CheckRegisterationRuleForAppointmentService : ICheckRegisterationRuleForAppointmentService { private IEnumerable<IAppointmentRegisterationRule> _rules ; public CheckRegisterationRuleForAppointmentService(IEnumerable<IAppointmentRegisterationRule> rules) { _rules = rules; } public Task CheckRegisterantionRule(PatientInfoDTO patientInfo) { foreach(var rule in rules) { rule.CheckRule(patientInfo); } } }
کد بالا به
نظر کامل میآید ولی مشکلی دارد! اگر در DI Container برای IAppointmentRegisterationRule یک قانون را دو یا چند بار ثبت کنیم، در هر بار بررسی کردن قوانین، آن را به همان تعداد بررسی میکند و اگر این فرآیند منابع زیادی را به
کار میگیرد، میتواند عملکرد برنامهی ما را به هم بریزد. برای جلوگیری از این مشکل، از متد TryAddEnumerabl()
استفاده میکنیم که لیستی از ServiceDescriptor ها را میگیرد و هر serviceDescriptor را فقط یکبار ثبت میکند:
services.TryAddEnumerable(new[] { ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule1)), ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule2)), ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule3)), ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule4)), });