public type this [type identifier] { get{ ... } set{ ... } }
- از کلمه کلیدی this برای نعریف Indexer استفاده میشود و یک Indexer نام ندارد.
- از دستیاب get برای برگرداندن مقدار و از دستیاب set جهت مقدار دهی و انتساب استفاده میشود.
- الزامی به پیاده سازی Indexer با مقادیر صحیح عددی نیست و شما میتوانید Indexer ی با پارامترهایی از نوع string یا double داشته باشید.
- Indexerها قابلیت سربارگذاری (overloaded) دارند.
- Indexerها میتوانند چندین پارامتری باشند مانند آرایههای دو بعدی که دو پارامتری هستند.
public class Matrix { // فیلدها private int _row, _col; private readonly double[,] _values; // تعداد ردیفهای ماتریس public int Row { get { return _row; } set { _row = value > 0 ? value : 3; } } // تعداد ستونهای ماتریس public int Col { get { return _col; } set { _col = value > 0 ? value : 3; } } // نعریف یک ایندکسر public double this[int r, int c] { get { return Math.Round(_values[r, c], 3); } set { _values[r, c] = value; } } // سازنده 1 public Matrix() { _values = new double[_row,_col]; } // سازنده 2 public Matrix(int row, int col) { _row = row; _col = col; _values = new double[_row,_col]; } }
public class UseMatrixIndexer { // ایجاد نمونه از شیء ماتریس private readonly Matrix m = new Matrix(5, 5); private double item; public UseMatrixIndexer() { // دسترسی به عنصر واقع در ردیف چهار و ستون سه item = m[4, 3]; } }
EF Code First #14
تو عمل ما از Unit of Work استفاده میکنیم،حالا در اینصورت نحوهی استفاده از این روش چجوره؟
1- باید اینترفیس IUnitOfWork رو توی کلاس MyDbContextBase پیاده سازی کنیم و پیاده سازی متد SaveChanges اینترفیس IUnitOfWork توی کلاس MyDbContextBase باید به شکل زیر باشه؟
applyCorrectYeKe(); auditFields(); return base.SaveChanges();
public class EmployeeDB { public void Insert(Employee e) { //Database Logic written here } public Employee Select() { //Database Logic written here } }
public class EmployeeDB { public virtual Employee Select() { //Old Select Method } } public class EmployeeManagerDB : EmployeeDB { public override Employee Select() { //Select method as per Manager //UI requirement } }
//Normal Screen EmployeeDB objEmpDb = new EmployeeDB(); Employee objEmp = objEmpDb.Select(); //Manager Screen EmployeeDB objEmpDb = new EmployeeManagerDB(); Employee objEmp = objEmpDb.Select();
Public static class MyExtensionMethod{ public static Employee managerSelect(this EmployeeDB employeeDB) { //Select method as per Manager } } //Manager Screen Employee objEmp = EmployeeDB.managerSelect();
رفع مشکلات:
در قسمت قبل با ذکر یک مثال و بیان مشکلات آن از دیدگاه اصول Defensive Code قصد داشتیم که مساله را روشنتر کنیم. مواردی که در
قسمت قبل ذکر شدند، به سادهترین شکل ممکن بیان
شدند و شما به راحتی با بررسی این موارد و تفکر در کدهای خود، میتوانید
این موارد را در کدی که خودتان مینویسید رعایت کنید. حل پیچیدگیهای موجود در کد قبل، با در نظر گرفتن اصول مذکور و اصولهای طراحی مختلف میتواند به روشهای مختلفی انجام گیرد. برای مثال میتوان برای هر یک از کارهایی که کد مثال قبل انجام میدهد، یک کلاس مجزا ایجاد نمود و اصول مذکور را در آن رعایت کرد. درنهایت این
کلاسها را در قالب یک Class Library دسته بندی کرد.
Predictability:
در ادامه قصد داریم در مورد قابلیت پیش بینی و فواید رعایت اصول آن در کد، بحث کنیم. به صورت کلی یک متد، یکسری پارامترها را به عنوان ورودی دریافت میکند؛ یک عملیات خاص را بر روی پارامترهای ورودی انجام میدهد و در نهایت، در صورت لزوم یک مقدار را بر میگرداند. پارامترهای ورودی میتوانند از سمت کاربر و از یک سورس خارجی وارد شوند و یا میتوانند از یک متد دیگر به این متد ارسال شوند. اولین مرحله برای ایجاد قابلیت Predictability این است که یکسری گاردها را به کد خود اضافه کنید تا به وسیلهی آنها پارامترهای ورودی را بررسی کنید و فقط اجازهی ورود، ورودیهای معتبر را بدهیم. برای مثال کد ذیل را درنظر بگیرد.
پارامترهای ورودی این کد با مستطیل قرمز رنگ مشخص شده اند. حال ما سعی داریم با قرار دادن یکسری گارد برای پارامترهای ورودی، از ورود مقادیر ناخواسته جلوگیری کنیم.
بر اساس اصول (GIGO (Garbage in-Garbage out در برنامه نویسی متدی که ورودیهای نامعتبر به آن پاس داده شوند، خروجیهای نامعتبری هم پس خواهد داد. بنابراین برای جلوگیری از این مسئله باید از ورود ورودیهای نامعتبر به متدها جلوگیری کرد. گاردها از ورود مقادیر نامعتبر به متدها جلوگیری خواهند کرد و در نتیجه خروجی مناسب و قابل پیش بینی از متد گرفته خواهد شد. برای جلوگیری از ورود دادههای نامعتبر، باید با استفاده از این دستورات که در ابتدای متد قرار داده میشوند، از ورود دادههای نامعتبر جلوگیری کرد. به این دستورات Guard Clauses گفته میشود. غیر از این مساله، کاهش دادن تعداد پارامترها و قراردادن قانونی برای تعیین اولویت پارامترهای متدها (برای مثال با توجه به اهمیت) میتواند به افزایش Predictability متدها بسیار کمک کند. با پیروی کردن از این اصول ساده شما میتوانید میزان خطاهایی که از پارامترهای ورودی منشاء میگیرند را کاهش دهید.
اجازه دهید با یک مثال؛ مسالهی بالا را تشریح کنیم. برای مثال یک برنامهی کوچک نوشتهایم؛ برای شمردن گام ها. در این برنامه تعداد قدمهای هدف و تعداد قدمهای برداشته شدهی امروز تعیین میشوند و سپس هدف، بر حسب درصد بیان خواهد شد.
با استفاده از این Application میخواهیم مفاهیمی را که بیان کردیم، به صورت کاربردی
نمایش دهیم. کدی این محاسبه را برای ما انجام میدهد، در ذیل نمایش داده شده و
در قالب یک متد تعیین شده است.
private decimal CalculatePercentOfGoalSteps (string goalSteps, string actualSteps) { return (Convert.ToDecimal(actualSteps) / Convert.ToDecimal(goalSteps)) * 100; }
این متد دارای دو پارامتر از نوع string می باشد و نتیجه هم در قالب یک مقدار decimal بازگشت داده خواهد شد. این جمله کلیتی از متد را بیان خواهد کرد. نحوهی فراخوانی این متد هم در کد ذیل آورده شد است.
private void Calculate_Click(object sender, EventArgs e) { var result =CalculatePercentOfGoalSteps (stepGoalForTodayTxt.Text, numberOfStepsForToday.Text); lblResult.Text = "شما به" + result + "% از هدف تان رسیده اید"; }
حال Application را اجرا کرده و نتیجه کار را مشاهده میکنیم. برای مثال شکل ذیل:
در این مثال با توجه به مقادیر وارد شده، به 40 درصد از هدف مورد نظر رسیدهایم. اما هدف از بیان این مثال، این نیست که مشخص گردد که ما چقدر به هدفمان نزدیک شدهایم. بلکه هدف مسایل دیگری است. در نظر بگیرید که بجای 5000، صفر را وارد کنید. در این حالت با یک Exception روبرو میشویم:
همانطور که در شکل بالا مشاهده میکنید، خطای Divide by zero رخ داده است. برای رفع این خطا و جلوگیری از رخداد این خطا، میتوان کد
ذیل را پیشنهاد داد.
private decimal CalculatePercentOfGoalSteps(string goalSteps, string actualSteps) { decimal result =0; var goalStepsCount = Convert.ToDecimal(goalSteps); if (goalStepsCount>0) { result = (Convert.ToDecimal(actualSteps) / goalStepsCount) * 100; } return result; }
با تغییر کد به این صورت مشکل Exception بالا حل میشود، اما باز هم مشکل دیگری وجود دارد. فرض کنید همانند شکل ذیل textbox اول را خالی کنیم و بعد از آن سعی در محاسبه داشته باشیم،
باز هم یک Exception دیگر
علت بوجود آمدن این مشکل این است که ما در کد امکان خالی بودن پارامترهای متد را در نظر نگرفتهایم و پیش بینیهای لازم صورت نگرفته است بنابراین دستور Convert .با مشکل مواجه شد. برای حل این مشکل میتوان به جای Convert از decimal.Tryparse استفاده کرد.
private decimal CalculatePercentOfGoalSteps(string goalSteps, string actualSteps) { decimal result = 0; decimal goalStepsCount = 0; decimal.TryParse(goalSteps, out goalStepsCount); decimal actualStepsCount = 0; decimal.TryParse(actualSteps, out actualStepsCount); if (goalStepsCount>0) { result = (actualStepsCount / goalStepsCount) * 100; } return result; }
با انجام دادن این کارها از بروز خطاهایی که ناشی از ورودیهای نامعتبر در کد هستند، جلوگیری کردیم. اما آیا این پایان کار است؟ خیر با استفاده کردن از این روش ما توانستهایم که از بروز خطا در برنامه جلوگیری کنیم. اما مشکلی که این روش دارد این است که کاربر متوجه نمیشود که چه زمانی برنامه دچار مشکل شده است. کاری که ما انجام میدهیم این است که برای تمامی حالات خطا، مقدار صفر را بر میگردانیم.
برای اینکه بتوانیم این کد به راحتی debug کنیم باید از مفهوم Fail Fast استفاده کنیم . این مفهوم قابلیتی را در کد ایجاد میکند که در صورتی که کد، دادههای نامعتبری را دریافت کرد، سریعا اجرای آن متوقف میشود و همزمان نیز اطلاعاتی در مورد خطا در اختیار کاربر قرار میدهد. برای این منظور با قرار دادن یکسری Guard Clauses، کد بالا را همانند شکل ذیل تغییر خواهیم داد.
private decimal CalculatePercentOfGoalSteps(string goalSteps, string actualSteps) { decimal goalStepsCount = 0; decimal actualStepsCount = 0; /// اطمینان حاصل میکنند که پارامترهای ورودی دارای مقدار هستند if (string.IsNullOrWhiteSpace(goalSteps)) throw new ArgumentException("مقدار هدف باید وارد شود", "goalSteps"); if (string.IsNullOrWhiteSpace(actualSteps)) throw new ArgumentException("مقدار واقعی باید وارد شود", "goalSteps"); ///اطمینان حاصل میکنند که مقادیر وارد شده حتما عددی هستند if (!decimal.TryParse(goalSteps, out goalStepsCount)) throw new ArgumentException("مقدار هدف باید عددی باشد", goalSteps); if(!decimal.TryParse(actualSteps, out actualStepsCount)) throw new ArgumentException("مقدار واقعی باید عددی باشد", actualSteps); ///اطمینان حاصل میکند که مقدار متغیر نباید صفر باشد if (goalStepsCount <= 0) throw new ArgumentException("مقدار هدف نباید صفر و یا کمتر از صفر باشد", "goalStepsCount"); return (actualStepsCount / goalStepsCount) * 100; }
ایجاد کردن این تغییرات در متد باعث افزایش خوانایی
کد میشود و هدف متد را روشنتر بیان خواهد کرد. اضافه کردن این کدها به دلیل اینکه
تمامی شرایط تست را تعیین خواهیم کرد Test-ability کد را بالا میبرد. اضافه کردن کدهای بالا به برنامه کمک خواهد کرد که
شرایط خطا در برنامه به درستی هندل شود و به طبع آن تصمیمات مناسبی گرفته شود و در
نهایت Predictability متدها و کل برنامه را افزایش میهد.
دریافت آخرین نگارش Froala WYSIWYG Editor
برای دریافت فایلهای آخرین نگارش این ادیتور وب میتوانید به سایت آن، قسمت دریافت فایلها مراجعه نمائید.
http://editor.froala.com/download
و یا به این آدرس مراجعه کنید:
https://github.com/froala/wysiwyg-editor/releases
ساختار پروژه و نحوهی کپی فایلهای آن
در هر دو مثالی که فایلهای آنرا از انتهای بحث میتوانید دریافت کنید، این ساختار رعایت شده است:
فایلهای CSS و فونتهای آن، در پوشهی Content قرار گرفتهاند.
فایلهای اسکریپت و زبان آن (که دارای زبان فارسی هم هست) در پوشهی Scripts کپی شدهاند.
یک نکته
فایل font-awesome.css را نیاز است کمی اصلاح کنید. مسیر پوشهی فونتهای آن اکنون با fonts شروع میشود.
تنظیمات اولیه
تفاوتی نمیکند که از وب فرمها استفاده میکنید یا MVC، نحوهی تعریف و افزودن پیش نیازهای این ادیتور به نحو ذیل است:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <link href="Content/font-awesome.css" rel="stylesheet" /> <link href="Content/froala_editor.css" rel="stylesheet" /> <script src="Scripts/jquery-1.10.2.min.js"></script> <script src="Scripts/froala_editor.min.js"></script> <script src="Scripts/langs/fa.js"></script> </head> <body> <form id="form1" runat="server"> </form> </body> </html>
استفاده از Froala WYSIWYG Editor در ASP.NET MVC
در ادامه نحوهی فعال سازی ادیتور وب Froala را در یک View برنامههای ASP.NET MVC ملاحظه میکنید:
@{ ViewBag.Title = "Index"; } <style type="text/css"> /*تنظیم فونت پیش فرض ادیتور*/ .froala-element { } </style> @using (Html.BeginForm(actionName: "Index", controllerName: "Home")) { @Html.TextArea(name: "Editor1") <input type="submit" value="ارسال" /> } @section Scripts { <script type="text/javascript"> $(function () { $('#Editor1').editable({ buttons: ["bold", "italic", "underline", "strikeThrough", "fontFamily", "fontSize", "color", "formatBlock", "align", "insertOrderedList", "insertUnorderedList", "outdent", "indent", "selectAll", "createLink", "insertImage", "insertVideo", "undo", "redo", "html", "save", "inserthorizontalrule"], inlineMode: false, inverseSkin: true, preloaderSrc: '@Url.Content("~/Content/img/preloader.gif")', allowedImageTypes: ["jpeg", "jpg", "png"], height: 300, language: "fa", direction: "rtl", fontList: ["Tahoma, Geneva", "Arial, Helvetica", "Impact, Charcoal"], autosave: true, autosaveInterval: 2500, saveURL: '@Url.Action("FroalaAutoSave", "Home")', saveParams: { postId: "123" }, spellcheck: true, plainPaste: true, imageButtons: ["removeImage", "replaceImage", "linkImage"], borderColor: '#00008b', imageUploadURL: '@Url.Action("FroalaUploadImage", "Home")', imageParams: { postId: "123" }, enableScript: false }); }); </script> }
سپس این ادیتور را بر روی المان TextArea قرار گرفته در صفحه، فعال میکنیم.
در قسمت مقادیر buttons، تمام حالات ممکن پیش بینی شدهاند. هر کدام را که نیاز ندارید، حذف کنید.
نحوهی تعریف زبان و راست به چپ بودن این ادیتور را با مقدار دهی پارامترهای language و direction ملاحظه میکنید.
پارامترهای autosave، saveURL و saveParams کار تنظیم ارسال خودکار محتوای ادیتور را جهت ذخیرهی آن در سرور به عهده دارند. بر اساس مقدار autosaveInterval میتوان مشخص کرد که هر چند میلی ثانیه یکبار اینکار باید انجام شود.
/// <summary> /// ذخیره سازی خودکار /// </summary> [HttpPost] [ValidateInput(false)] public ActionResult FroalaAutoSave(string body, int? postId) // نام پارامتر بادی را تغییر ندهید { //todo: save body ... return new EmptyResult(); }
چون قرار است تگهای HTML به سرور ارسال شوند، ویژگی ValidateInput به false تنظیم شدهاست.
saveParams آن، برای مقدار دهی پارامترهای اضافی است که نیاز میباشند تا به سرور ارسال شوند. مثلا شماره مطلب جاری نیز به سرور ارسال گردد.
در اینجا نام پارامتری که ارسال میگردد، دقیقا مساوی body است. بنابراین آنرا تغییر ندهید.
پارامترهای imageUploadURL و imageParams برای فعال سازی ذخیره تصاویر آن در سرور کاربرد دارند.
اکشن متد مدیریت کنندهی آن به نحو ذیل میتواند تعریف شود:
// todo: مسایل امنیتی آپلود را فراموش نکنید /// <summary> /// ذخیره سازی تصاویر ارسالی /// </summary> [HttpPost] public ActionResult FroalaUploadImage(HttpPostedFileBase file, int? postId) // نام پارامتر فایل را تغییر ندهید { var fileName = Path.GetFileName(file.FileName); var rootPath = Server.MapPath("~/images/"); file.SaveAs(Path.Combine(rootPath, fileName)); return Json(new { link = "images/" + fileName }, JsonRequestBehavior.AllowGet); }
خروجی آن برای مشخص سازی محل ذخیره سازی تصویر در سرور باید یک خروجی JSON دارای خاصیت و پارامتر link به نحو فوق باشد (این مسیر، یک مسیر نسبی است؛ نسبت به ریشه سایت).
imageParams آن برای مقدار دهی پارامترهای اضافی است که نیاز میباشند تا به سرور ارسال شوند. مثلا شماره مطلب جاری نیز به سرور ارسال گردد.
استفاده از Froala WYSIWYG Editor در ASP.NET Web forms
تمام نکاتی که در قسمت تنظیمات ASP.NET MVC در مورد ویژگیهای سمت کلاینت این ادیتور ذکر شد، در مورد وب فرمها نیز صادق است. فقط قسمت مدیریت سمت سرور آن اندکی تفاوت دارد.
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" ValidateRequest="false" EnableEventValidation="false" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="FroalaWebFormsTest.Default" %> <%--اعتبارسنجی ورودی غیرفعال شده چون باید تگ ارسال شود--%> <%--همچنین در وب کانفیگ هم تنظیم دیگری نیاز دارد--%> <asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server"> <%--حالت کلاینت آی دی بهتر است تنظیم شود در اینجا--%> <asp:TextBox ID="txtEditor" ClientIDMode="Static" runat="server" Height="199px" TextMode="MultiLine" Width="447px"></asp:TextBox> <br /> <asp:Button ID="btnSave" runat="server" OnClick="btnSave_Click" Text="ارسال" /> <style type="text/css"> /*تنظیم فونت پیش فرض ادیتور*/ .froala-element { } </style> <script type="text/javascript"> $(function () { $('#txtEditor').editable({ buttons: ["bold", "italic", "underline", "strikeThrough", "fontFamily", "fontSize", "color", "formatBlock", "align", "insertOrderedList", "insertUnorderedList", "outdent", "indent", "selectAll", "createLink", "insertImage", "insertVideo", "undo", "redo", "html", "save", "inserthorizontalrule"], inlineMode: false, inverseSkin: true, preloaderSrc: 'Content/img/preloader.gif', allowedImageTypes: ["jpeg", "jpg", "png"], height: 300, language: "fa", direction: "rtl", fontList: ["Tahoma, Geneva", "Arial, Helvetica", "Impact, Charcoal"], autosave: true, autosaveInterval: 2500, saveURL: 'FroalaHandler.ashx', saveParams: { postId: "123" }, spellcheck: true, plainPaste: true, imageButtons: ["removeImage", "replaceImage", "linkImage"], borderColor: '#00008b', imageUploadURL: 'FroalaHandler.ashx', imageParams: { postId: "123" }, enableScript: false }); }); </script> </asp:Content>
به علاوه ClientIDMode=Static نیز تنظیم شدهاست، تا بتوان از ID تکست باکس قرار گرفته در صفحه، به سادگی در کدهای سمت کاربر جیکوئری استفاده کرد.
اگر دقت کرده باشید، save urlها اینبار به فایل FroalaHandler.ashx اشاره میکنند. محتوای این Genric handler را ذیل مشاهده میکنید:
using System.IO; using System.Web; using System.Web.Script.Serialization; namespace FroalaWebFormsTest { public class FroalaHandler : IHttpHandler { //todo: برای اینکارها بهتر است از وب ای پی آی استفاده شود //todo: یا دو هندلر مجزا یکی برای تصاویر و دیگری برای ذخیره سازی متن public void ProcessRequest(HttpContext context) { var body = context.Request.Form["body"]; var postId = context.Request.Form["postId"]; if (!string.IsNullOrWhiteSpace(body) && !string.IsNullOrWhiteSpace(postId)) { //todo: save changes context.Response.ContentType = "text/plain"; context.Response.Write(""); context.Response.End(); } var files = context.Request.Files; if (files.Keys.Count > 0) { foreach (string fileKey in files) { var file = context.Request.Files[fileKey]; if (file == null || file.ContentLength == 0) continue; //todo: در اینجا مسایل امنیتی آپلود فراموش نشود var fileName = Path.GetFileName(file.FileName); var rootPath = context.Server.MapPath("~/images/"); file.SaveAs(Path.Combine(rootPath, fileName)); var json = new JavaScriptSerializer().Serialize(new { link = "images/" + fileName }); // البته اینجا یک فایل بیشتر ارسال نمیشود context.Response.ContentType = "text/plain"; context.Response.Write(json); context.Response.End(); } } context.Response.ContentType = "text/plain"; context.Response.Write(""); context.Response.End(); } public bool IsReusable { get { return false; } } } }
یک نکتهی امنیتی مهم
<location path="upload"> <system.webServer> <handlers accessPolicy="Read" /> </system.webServer> </location>
کدهای کامل این مطلب را در ادامه میتوانید دریافت کنید
Froala-Sample
مشاهده یا دانلود کدهای مقاله
تزریق وابستگی چیست؟
تزریق وابستگی (DI) تکنیکی برای دستیابی به اتصال شل بین اشیاء و همکاران اشیاء و وابستگیهای بین آنها میباشد. یک شیء برای انجام وظایف خود، بجای اینکه اشیاء همکار خود را به صورت مستقیم نمونه سازی کند، یا از ارجاعات استاتیک استفاده نماید، میتواند از اشیائی که برایش تامین شدهاست، استفاده کند. در اغلب موارد کلاسها، وابستگیهای خود را از طریق سازندهی خود درخواست میکنند، که به آنها اجازه میدهد اصل وابستگی صریح را رعایت کنند (Explicit Dependencies Principle). این روش را «تزریق در سازنده» مینامند.
از آنجا که در طراحی کلاسها با استفاده از DI، نمونه سازی مستقیم، توسط کلاسها و به صورت Hard-coded انجام نمیگیرد، وابستگی بین اشیاء کم شده و پروژهای با اتصالات شل به دست میآید. با این کار اصل وابستگی معکوس (Dependency Inversion Principle) رعایت میشود. بر اساس این اصل، ماژولهای سطح بالا نباید به ماژولهای سطح پایین خود وابسته باشند؛ بلکه هر دو باید به کلاسهایی انتزاعی وابسته باشند. اشیاء بجای ارجاع به پیاده سازیهای خاص کلاسهای همکار خود، کلاسهای انتزاعی، معمولاٌ اینترفیس آنها را درخواست میکنند و هنگام نمونه سازی از آنها (داخل متد سازنده) کلاس پیاده سازی شده برایشان تامین میشود. خارج کردن وابستگیهای مستقیم از کلاسها و تامین پیاده سازیهای این اینترفیسها به صورت پارامترهایی برای کلاسها، یک مثال از الگوی طراحی استراتژی (Strategy design pattern) میباشد.
در حالتیکه کلاسها به تعداد زیادی کلاس وابستگی داشته باشند و برای اجرا شدن، نیاز به تامین وابستگیهایشان داشته باشند، بهتر است یک کلاس اختصاصی، برای نمونه سازی این کلاسها با وابستگیهای مورد نیاز آنها، در سیستم وجود داشته باشد. این کلاس نمونه ساز را کانتینرIoC، یا کانتینر DI یا به طور خلاصه کانتینر مینامند ( Inversion of Control (IoC) ). کانتینر در اصل یک کارخانه میباشد که وظیفهی تامین نمونههایی از کلاسهایی را که از آن درخواست میشود، انجام میدهد. اگر یک کلاس تعریف شده، وابستگی به کلاسهای دیگر داشته باشد و کانتینر برای ارائه وابستگیهای کلاس تعریف شده تنظیم شده باشد، هر موقع نیاز به یک نمونه از این کلاس وجود داشته باشد، به عنوان بخشی از کار نمونه سازی از کلاس مورد نظر، کلاسهای وابستهی آن نیز ایجاد میشوند (همهی کارهای مربوط به نمونه سازی کلاس خاص و کلاسهای وابسته به آن توسط کانتینر انجام میگیرد). به این ترتیب، میتوان وابستگیهای بسیار پیچیده و تو در توی موجود در سیستم را بدون نیاز به هیچگونه نمونه سازی hard-code شده، برای کلاسها فراهم کرد. کانتینرها علاوه بر ایجاد اشیاء و وابستگیهای موجود در آنها، معمولا طول عمر اشیاء در اپلیکیشن را نیز مدیریت میکنند.
ASP.NET Core یک کانتینر بسیار ساده را به نام اینترفیس IServiceProvider ارائه داده است که به صورت پیش فرض از تزریق وابستگی در سازندهی کلاسها پشتیبانی میکند و همچنین ASP.NET برخی از سرویسهای خود را از طریق DI در دسترس قرار داده است. کانتینرASP.NET، یک اشارهگر به کلاسهایی است که به عنوان سرویس عمل میکنند. در ادامهی این مقاله، سرویسها به کلاسهایی گفته میشود که به وسیلهی کانتینر ASP.NET Core مدیریت میشوند. شما میتوانید سرویس ConfigureServices کانتینر را در داخل کلاس Startup پروژه خود پیکربندی کنید.
تزریق وابستگی از طریق متد سازندهی کلاس
تزریق وابستگی از طریق متد سازنده، مستلزم آن است که سازندهی کلاس مورد نظر عمومی باشد. در غیر این صورت، اپلیکیشن شما استثنای InvalidOperationException را با پیام زیر نشان میدهد:
A suitable constructor for type 'YourType' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
تزریق از طریق متد سازنده مستلزم آن است که تنها یک سازندهی مناسب وجود داشته باشد. البته Overload سازنده امکان پذیر است؛ ولی باید تنها یک متد سازنده وجود داشته باشد که آرگومانهای آن توسط DI قابل ارائه باشند. اگر بیش از یکی وجود داشته باشد، سیستم استثنای InvalidOperationException را با پیام زیر نشان میدهد:
Multiple constructors accepting all given argument types have been found in type 'YourType'. There should only be one applicable constructor.
سازندگان میتوانند آرگومانهایی را از طریق DI دریافت کنند. برای این منظور آرگومانهای این سازندهها باید مقدار پیش فرضی را داشته باشند. به مثال زیر توجه نمایید:
// throws InvalidOperationException: Unable to resolve service for type 'System.String'... public CharactersController(ICharacterRepository characterRepository, string title) { _characterRepository = characterRepository; _title = title; } // runs without error public CharactersController(ICharacterRepository characterRepository, string title = "Characters") { _characterRepository = characterRepository; _title = title; }
استفاده از سرویس ارائه شده توسط فریم ورک
متد ConfigureServices در کلاس Startup، مسئول تعریف سرویسهایی است که سیستم از آن استفاده میکند. از جملهی این سرویسها میتوان به ویژگیهای پلتفرم مانند EF Core و ASP.NET Core MVC اشاره کرد. IServiceCollection که به ConfigureServices ارائه میشود، سرویسهای زیر را تعریف میکند (که البته بستگی به نوع پیکربندی هاست دارد):
نوع سرویس | طول زندگی |
Microsoft.AspNetCore.Hosting.IHostingEnvironment | Singleton |
Microsoft.AspNetCore.Hosting.IApplicationLifetime | Singleton |
Microsoft.AspNetCore.Hosting.IStartup | Singleton |
Microsoft.AspNetCore.Hosting.Server.IServer | Singleton |
Microsoft.Extensions.Options.IConfigureOptions | Transient |
Microsoft.Extensions.ObjectPool.ObjectPoolProvider | Singleton |
Microsoft.AspNetCore.Hosting.IStartupFilter | Transient |
System.Diagnostics.DiagnosticListener | Singleton |
System.Diagnostics.DiagnosticSource | Singleton |
Microsoft.Extensions.Options.IOptions | Singleton |
Microsoft.AspNetCore.Http.IHttpContextFactory | Transient |
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory | Transient |
Microsoft.Extensions.Logging.ILogger | Singleton |
Microsoft.Extensions.Logging.ILoggerFactory | Singleton |
در زیر نمونه ای از نحوهی اضافه کردن سرویسهای مختلف را به کانتینر، با استفاده از متدهای الحاقی مانند AddDbContext، AddIdentity و AddMvc، مشاهده میکنید:
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // Add framework services. services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); services.AddMvc(); // Add application services. services.AddTransient<IEmailSender, AuthMessageSender>(); services.AddTransient<ISmsSender, AuthMessageSender>(); }
ثبت سرویسهای اختصاصی
شما میتوانید سرویسهای اپلیکیشن خودتان را به ترتیبی که در تکه کد زیر مشاهده میکنید، ثبت نمایید. اولین نوع جنریک، نوعی است که از کانتینر درخواست خواهد شد و معمولا به شکل اینترفیس میباشد. نوع دوم، نوع پیاده سازی شدهای است که به وسیلهی کانتینر، نمونه سازی خواهد شد و کانتینر برای درخواستهای از نوع اول، این نمونه از تایپ را ارائه خواهد کرد:
services.AddTransient<IEmailSender, AuthMessageSender>(); services.AddTransient<ISmsSender, AuthMessageSender>();
نکته:
هر متد الحاقی <services.Add<ServiceName، سرویسهایی را اضافه و پیکربندی میکند. به عنوان مثال services.AddMvc نیازمندیهای سرویس MVC را اضافه میکند. توصیه میشود شما هم با افزودن متدهای الحاقی در فضای نام Microsoft.Extensions.DependencyInjection این قرارداد را رعایت نمائید. این کار باعث کپسوله شدن ثبت گروهی سرویسها میشود.
متد AddTransient، برای نگاشت نوعهای انتزاعی به سرویسهای واقعی که نیاز به نمونه سازی به ازای هر درخواست دارند، استفاده میشود. در اصطلاح، طول عمر سرویسها در اینجا مشخص میشوند. در ادامه گزینههای دیگری هم برای طول عمر سرویسها تعریف خواهند شد. خیلی مهم است که برای هر یک از سرویسهای ثبت شده، طول عمر مناسبی را انتخاب نمایید. آیا برای هر کلاس که سرویسی را درخواست میکند، باید یک نمونهی جدید ساخته شود؟ آیا فقط یک نمونه در طول یک درخواست وب مورد استفاده قرار میگیرد؟ یا باید از یک نمونهی واحد برای طول عمر کل اپلیکیشن استفاده شود؟
در مثال ارائه شدهی در این مقاله، یک کنترلر ساده به نام CharactersController وجود دارد که نام کاراکتری را نشان میدهد. متد Index، لیست کنونی کاراکترهایی را که در اپلیکیشن ذخیره شدهاند، نشان میدهد. در صورتیکه این لیست خالی باشد، تعدادی به آن اضافه میکند. توجه داشته باشید، اگرچه این اپلیکیشن از Entity Framework Core و ClassDataContext برای دادههای مانا استفاده میکند، هیچیکدام از آنها در کنترلر ظاهر نمیشوند. در عوض، مکانیزم دسترسی به دادههای خاص، در پشت یک اینترفیس (ICharacterRepository) مخفی شده است (طبق الگوی طراحی ریپازیتوری). یک نمونه از ICharacterRepository از طریق سازنده درخواست میشود و به یک فیلد خصوصی اختصاص داده میشود، سپس برای دسترسی به کاراکترها در صورت لزوم استفاده میشود:
public class CharactersController : Controller { private readonly ICharacterRepository _characterRepository; public CharactersController(ICharacterRepository characterRepository) { _characterRepository = characterRepository; } // GET: /characters/ public IActionResult Index() { PopulateCharactersIfNoneExist(); var characters = _characterRepository.ListAll(); return View(characters); } private void PopulateCharactersIfNoneExist() { if (!_characterRepository.ListAll().Any()) { _characterRepository.Add(new Character("Darth Maul")); _characterRepository.Add(new Character("Darth Vader")); _characterRepository.Add(new Character("Yoda")); _characterRepository.Add(new Character("Mace Windu")); } } }
ICharacterRepository دو متد مورد نیاز کنترلر برای کار با نمونههای Character را تعریف میکند:
using System.Collections.Generic; using DependencyInjectionSample.Models; namespace DependencyInjectionSample.Interfaces { public interface ICharacterRepository { IEnumerable<Character> ListAll(); void Add(Character character); } }
using System.Collections.Generic; using System.Linq; using DependencyInjectionSample.Interfaces; namespace DependencyInjectionSample.Models { public class CharacterRepository : ICharacterRepository { private readonly ApplicationDbContext _dbContext; public CharacterRepository(ApplicationDbContext dbContext) { _dbContext = dbContext; } public IEnumerable<Character> ListAll() { return _dbContext.Characters.AsEnumerable(); } public void Add(Character character) { _dbContext.Characters.Add(character); _dbContext.SaveChanges(); } } }
نکته
ایجاد شیء درخواست شده و تمامی اشیاء مورد نیاز شیء درخواست شده را گراف شیء مینامند. به همین ترتیب مجموعهای از وابستگیهایی را که باید resolve شوند، به طور معمول، درخت وابستگی یا گراف وابستگی مینامند.
در مورد مثال مطرح شده، ICharacterRepository و به نوبه خود ApplicationDbContext باید با سرویسهای خود در کانتینر ConfigureServices و کلاس Startup ثبت شوند. ApplicationDbContext با فراخوانی متد <AddDbContext<T پیکربندی میشود. کد زیر ثبت کردن نوع CharacterRepository را نشان میدهد:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase() ); // Add framework services. services.AddMvc(); // Register application services. services.AddScoped<ICharacterRepository, CharacterRepository>(); services.AddTransient<IOperationTransient, Operation>(); services.AddScoped<IOperationScoped, Operation>(); services.AddSingleton<IOperationSingleton, Operation>(); services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty)); services.AddTransient<OperationService, OperationService>(); }
هشدار
خطر بزرگی را که باید در نظر گرفت، resolve کردن سرویس Scoped از طول عمر singleton میباشد. در صورت انجام این کار، احتمال دارد که سرویسها وارد حالت نادرستی شوند.
سرویسهایی که وابستگیهای دیگری هم دارند، باید آنها را در کانتینر ثبت کنند. اگر سازندهی سرویس نیاز به یک primitive به عنوان ورودی داشته باشد، میتوان با استفاده از الگوی گزینهها و پیکربندی (options pattern and configuration)، ورودیهای مناسبی را به سازندهها منتقل کرد.
طول عمر سرویسها و گزینههای ثبت
سرویسهای ASP.NET را میتوان با طول عمرهای زیر پیکربندی کرد:
Transient: سرویسهایی با طول عمر Transient، در هر زمان که درخواست میشوند، مجددا ایجاد میشوند. این طول عمر برای سرویسهای سبک و بدون حالت مناسب میباشند.
Scoped: سرویسهایی با طول عمر Scoped، تنها یکبار در طی هر درخواست ایجاد میشوند.
Singleton: سرویسهایی با طول عمر Singleton، برای اولین باری که درخواست میشوند (یا اگر در ConfigureServices نمونهای را مشخص کرده باشید) ایجاد میشوند و درخواستهای آتی برای این سرویسها از همان نمونهی ایجاد شده استفاده میکنند. اگر اپلیکیشن شما درخواست رفتار singleton را داشته باشد، پیشنهاد میشود که سرویس کانتینر را برای مدیریت طول عمر سرویس مورد نیاز پیکربندی کنید و خودتان الگوی طراحی singleton را پیاده سازی نکنید.
سرویسها به چندین روش میتوانند در کانتینر ثبت شوند. چگونگی ثبت کردن یک سرویس پیاده سازی شده برای یک نوع، در بخشهای پیشین توضیح داده شده است. علاوه بر این، یک کارخانه را میتوان مشخص کرد، که برای ایجاد نمونه بر اساس تقاضا استفاده شود. رویکرد سوم، ایجاد مستقیم نمونهای از نوع مورد نظر است که در این حالت کانتینر اقدام به ایجاد یا نابود کردن نمونه نمیکند.
به منظور مشخص کردن تفاوت بین این طول عمرها و گزینههای ثبت کردن، یک اینترفیس ساده را در نظر بگیرید که نشان دهندهی یک یا چند operation است و یک شناسهی منحصر به فرد operation را از طریق OperationId نشان میدهد. برای مشخص شدن انواع طول عمرهای درخواست شده، بسته به نحوهی پیکربندی طول عمر سرویس مثال زده شده، کانتینر، نمونهی یکسان یا متفاوتی را از سرویس، به کلاس درخواست کننده ارائه میدهد. ما برای هر طول عمر، یک نوع را ایجاد میکنیم:
using System; namespace DependencyInjectionSample.Interfaces { public interface IOperation { Guid OperationId { get; } } public interface IOperationTransient : IOperation { } public interface IOperationScoped : IOperation { } public interface IOperationSingleton : IOperation { } public interface IOperationSingletonInstance : IOperation { } }
سپس در ConfigureServices، هر نوع با توجه به طول عمر مورد نظر، به کانتینر افزوده میشود:
services.AddScoped<ICharacterRepository, CharacterRepository>(); services.AddTransient<IOperationTransient, Operation>(); services.AddScoped<IOperationScoped, Operation>(); services.AddSingleton<IOperationSingleton, Operation>(); services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty)); services.AddTransient<OperationService, OperationService>();
using DependencyInjectionSample.Interfaces; namespace DependencyInjectionSample.Services { public class OperationService { public IOperationTransient TransientOperation { get; } public IOperationScoped ScopedOperation { get; } public IOperationSingleton SingletonOperation { get; } public IOperationSingletonInstance SingletonInstanceOperation { get; } public OperationService(IOperationTransient transientOperation, IOperationScoped scopedOperation, IOperationSingleton singletonOperation, IOperationSingletonInstance instanceOperation) { TransientOperation = transientOperation; ScopedOperation = scopedOperation; SingletonOperation = singletonOperation; SingletonInstanceOperation = instanceOperation; } } }
using DependencyInjectionSample.Interfaces; using DependencyInjectionSample.Services; using Microsoft.AspNetCore.Mvc; namespace DependencyInjectionSample.Controllers { public class OperationsController : Controller { private readonly OperationService _operationService; private readonly IOperationTransient _transientOperation; private readonly IOperationScoped _scopedOperation; private readonly IOperationSingleton _singletonOperation; private readonly IOperationSingletonInstance _singletonInstanceOperation; public OperationsController(OperationService operationService, IOperationTransient transientOperation, IOperationScoped scopedOperation, IOperationSingleton singletonOperation, IOperationSingletonInstance singletonInstanceOperation) { _operationService = operationService; _transientOperation = transientOperation; _scopedOperation = scopedOperation; _singletonOperation = singletonOperation; _singletonInstanceOperation = singletonInstanceOperation; } public IActionResult Index() { // viewbag contains controller-requested services ViewBag.Transient = _transientOperation; ViewBag.Scoped = _scopedOperation; ViewBag.Singleton = _singletonOperation; ViewBag.SingletonInstance = _singletonInstanceOperation; // operation service has its own requested services ViewBag.Service = _operationService; return View(); } } }
حالا دو درخواست جداگانه برای این کنترلر ساخته شده است:
به تفاوتهای موجود در مقادیر OperationId در یک درخواست و بین درخواستها توجه کنید:
- OperationId اشیاء Transient همیشه متفاوت میباشند. چون یک نمونه جدید برای هر کنترلر و هر سرویس ایجاد شدهاست.
- اشیاء Scoped در یک درخواست، یکسان هستند؛ اما در درخواستهای مختلف متفاوت میباشند.
- اشیاء Singleton برای هر شیء و هر درخواست (صرف نظر از اینکه یک نمونه در ConfigureServices ارائه شده است) یکسان میباشند.
درخواست سرویس
در ASP.NET سرویسهای موجود در یک درخواست HttpContext از طریق مجموعه RequestServices قابل مشاهده میباشد.
RequestServices نشان دهندهی سرویسهایی است که شما به عنوان بخشی از اپلیکیشن خود، آنها را پیکربندی و درخواست میکنید. هنگامیکه اشیاء اپلیکیشن شما وابستگیهای خود را مشخص میکنند، این وابستگیها با استفاده از نوعهای موجود در RequestServices برآورده میشوند و نوعهای موجود در ApplicationServices در این مرحله مورد استفاده قرار نمیگیرد.
به طور کلی، شما نباید مستقیما از این خواص استفاده کنید و بجای آن، نوعهای کلاس خود را توسط سازندهی کلاس، درخواست کنید و اجازه دهید فریم ورک این وابستگیها را تزریق کند. این کار باعث بهوجود آمدن کلاسهایی با قابلیت آزمونپذیری بالاتر و اتصالات شلتر بین آنها میشود.
نکته
درخواست وابستگیها با استفاده از پارامترهای کلاس سازنده، بر روش کار با مجموعهی RequestServices ارجحیت دارد.
طراحی سرویسها برای تزریق وابستگیها
شما باید سرویسهای خود را طوری طراحی کنید که از تزریق وابستگیها برای ارتباطات خود استفاده نمایند. این کار باعث کاهش استفاده از فراخوانیهای متدهای استاتیک (متدهای استاتیک، حالت دار میباشند و استفادهی زیاد از آنها باعث به وجود آمدن بوی بد کدی به نام static cling، میشود) و همچنین از بین رفتن نیاز به نمونه سازی مستقیم کلاسهای وابسته داخل سرویسها، میشود. هر موقع بخواهید بین new کردن یک کلاس، یا درخواست دادن آن از طریق تزریق وابستگی، یکی را انتخاب کنید، این اصطلاح را به یاد بیاورید، New is Glue. با پیروی از اصول SOLID طراحی شیء گرا، به طور طبیعی کلاسهای شما تمایل به کوچک بودن، کارا و قابل تست بودن را دارند.
اگر متوجه شدید که کلاسهای شما تمایل دارند تا تعداد وابستگیهای زیادی به آنها تزریق شود، چه باید بکنید؟ به طور کلی این مشکل نشانهای است از نقض Single Responsibility Principle یا SRP است و احتمالا کلاسهای شما وظایف بیش از اندازهای را دارند. در این گونه موارد تلاش کنید مقداری از وظایف کلاس را به یک کلاس جدید منتقل کنید. در نظر داشته باشید که کلاسهای کنترلر باید به مسائل UI تمرکز کنند و قوانین کسب و کار و جزئیات دسترسی به دادهها باید در کلاسهایی جداگانه و مرتبط با خود قرار داشته باشند.
به طور خاص برای دسترسی به داده ، شما میتوانید DbContext را به کنترلرهای خود تزریق کنید (با فرض اینکه شما EF را به کانتینر سرویس ConfigureServices اضافه کردهاید). بعضی از توسعه دهندگان به جای تزریق مستقیم DbContext از یک اینترفیس ریپازیتوری استفاده مینمایند. میتوانید با استفاده از یک اینترفیس برای کپسوله کردن منطق دسترسی به دادهها در یک مکان، تعداد تغییرات مورد نیاز را در صورت تغییر دیتابیس، به حداقل برسانید.
تخریب سرویس ها
سرویس کانتینر برای نوعهای IDisposable که خودش ایجاد کردهاست، متد Dispose را فراخوانی خواهد کرد. با این حال، اگر شما خودتان نمونهای را به صورت دستی نمونه سازی و به کانتینر اضافه کرده باشید، سرویس کانتینر آنرا dispose نخواهد کرد.
مثال:
// Services implement IDisposable: public class Service1 : IDisposable {} public class Service2 : IDisposable {} public class Service3 : IDisposable {} public void ConfigureServices(IServiceCollection services) { // container will create the instance(s) of these types and will dispose them services.AddScoped<Service1>(); services.AddSingleton<Service2>(); // container did not create instance so it will NOT dispose it services.AddSingleton<Service3>(new Service3()); services.AddSingleton(new Service3()); }
نکته:
در نسخه 1.0، کانتینر برای تمام اشیاء از نوع IDisposable از جمله اشیائی که خودش ایجاد نکرده بود، متد dispose را فراخوانی میکرد.
سرویسهای کانتینر جانشین
کانتینر موجود در net core. به منظور تامین نیازهای اساسی فریم ورک ایجاد شدهاست و تعداد زیادی از اپلیکیشنها از آن استفاده میکنند. با این حال، توسعه دهندگان میتوانند کانتینرهای مورد نظر خود را جایگزین آن کنند. متد ConfigureServices به طور معمول مقدار void را بر میگرداند. اما با تغییر امضای آن به نوع بازگشتیIServiceProvider، میتوان سرویس کانتینر متفاوتی را در اپلیکیشن پیکربندی کرد. سرویسهای کانتینر IOC مختلفی برای NET. وجود دارند؛ در مثال زیر، Autofac استفاده شده است.
در ابتدا بستههای زیر را نصب کنید:
Autofac Autofac.Extensions.DependencyInjection
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); // Add other framework services // Add Autofac var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterModule<DefaultModule>(); containerBuilder.Populate(services); var container = containerBuilder.Build(); return new AutofacServiceProvider(container); }
توصیه ها
هنگام کار با تزریق وابستگیها، توصیههای ذیر را در نظر داشته باشید:
- DI برای اشیایی که دارای وابستگی پیچیده هستند، مناسب میباشد. کنترلرها، سرویسها، آداپتورها و ریپازیتوریها، نمونههایی از این اشیاء هستند که میتوانند به DI اضافه شوند.
- از ذخیرهی دادهها و پیکربندی مستقیم در DI اجتناب کنید. به عنوان مثال، معمولا سبد خرید کاربر نباید به سرویس کانتینر اضافه شود. پیکربندی باید از مدل گزینهها استفاده کند. همچنین از اشیاء "data holder"، که فقط برای دسترسی دادن به اشیاء دیگر ایجاد شدهاند، نیز اجتناب کنید. در صورت امکان بهتر است شیء واقعی مورد نیاز DI درخواست شود.
- از دسترسی استاتیک به سرویسها اجتناب شود.
- از نمونه سازی مستقیم سرویسها در کد برنامه خود اجتناب کنید.
- از دسترسی استاتیک به HttpContext اجتناب کنید.
توجه
مانند هر توصیهی دیگری، ممکن است شما با شرایطی مواجه شوید که مجبور به نقض هر یک از این توصیهها شوید. اما این موارد استثناء بسیار نادر میباشند و رعایت این نکات یک عادت برنامه نویسی خوب محسوب میشود.
مرجع: Introduction to Dependency Injection in ASP.NET Core
تزریق خودکار وابستگیها در برنامههای ASP.NET MVC
@inject ISecurityService SecurityService <h3>@ViewBag.Message</h3> @if (!SecurityService.IsAuthenticated) { <li>@Html.ActionLink("ورود", "Login", "Main")</li> }
بررسی تغییرات Reflection در NET Core.
آشنایی با Fluent interfaces
پ.ن.
بلاگر در ارسال یک سری کاراکترها حساسیت دارد حتی در کامنتها. همان کاراکترهای غیرمجاز در XML که باید به صورت escape شده معرفی شوند و گرنه خیلی ساده یا آنها را نمایش نمیدهد یا حذف میکند (مشکلی که به نظر با تعریف جنریکها داشتید و کاراکترها حدف شده بود؛ علتش این مبحث است).