iTextSharp تا نگارش 4.6.1 آن با مجوز LGPL ارائه شد و پس از آن بود که مجوز آن به AGPL تجاری تغییر کرد. به همین جهت کسانیکه نمیخواهند از مجوز AGPL آن استفاده کنند، هنوز با نگارش 4.6.1 آن کار میکنند. کتابخانهی iTextSharp.LGPLv2.Core حاصل انتقال کدهای این نگارش ویژه به NET Core. است.
یک نکتهی تکمیلی
iTextSharp تا نگارش 4.6.1 آن با مجوز LGPL ارائه شد و پس از آن بود که مجوز آن به AGPL تجاری تغییر کرد. به همین جهت کسانیکه نمیخواهند از مجوز AGPL آن استفاده کنند، هنوز با نگارش 4.6.1 آن کار میکنند. کتابخانهی iTextSharp.LGPLv2.Core حاصل انتقال کدهای این نگارش ویژه به NET Core. است.
iTextSharp تا نگارش 4.6.1 آن با مجوز LGPL ارائه شد و پس از آن بود که مجوز آن به AGPL تجاری تغییر کرد. به همین جهت کسانیکه نمیخواهند از مجوز AGPL آن استفاده کنند، هنوز با نگارش 4.6.1 آن کار میکنند. کتابخانهی iTextSharp.LGPLv2.Core حاصل انتقال کدهای این نگارش ویژه به NET Core. است.
نظرات مطالب
آموزش سیلورلایت 4 - قسمتهای 1 تا 5
البته دو پروژه سورس باز در سایت codeplex برای نگارشهای قبل از 4 موجود است:
http://silverlightrtl.codeplex.com
http://persiansilverlight.codeplex.com/
http://silverlightrtl.codeplex.com
http://persiansilverlight.codeplex.com/
- AfsharM: git server با استفاده از ویندوز و داتنت | blog.afsharm.com
- آشنایی با مجوزهای نرم افزارهای آزاد/متن باز | libooks.ir
- شهروند دردمند - مستندسازی یکی از رموز پیشرفت غرب؛ در اهمیت مدیریت دانش | kaarmand.blogfa.com
- Dotnet4all Code Snippets: A cheat sheet for SQL Server developers | snippets.dotnet4all.com
- Download Details - Microsoft Download Center - Expression Encoder 4 with SP2 | www.microsoft.com
- Data quality services - demo | blogs.msdn.com
- پرسش و پاسخ با مدیر پروژه MonoDevelop | www.infoq.com
- کدهای خود را آنالیز کنید | channel9.msdn.com
معرفی کتابخانه stateless به عنوان جایگزین سبک وزنی برای Windows workflow foundation
کتابخانه سورس باز Stateless، برای طراحی و پیاده سازی «ماشینهای حالت گردش کاری مانند» تهیه شده و مزایای زیر را نسبت به Windows workflow foundation دارا است:
- جمعا 30 کیلوبایت است!
- تمام اجزای آن سورس باز است.
- دارای API روان و سادهای است.
- امکان تبدیل UML state diagrams، به نمونه معادل Stateless بسیار ساده و سریع است.
- به دلیل code first بودن، کار کردن با آن برای برنامه نویسها سادهتر بوده و افزودن یا تغییر اجزای آن با کدنویسی به سادگی میسر است.
دریافت کتابخانه Stateless از Google code و یا از NuGet
پیاده سازی مثال کلید برق با Stateless
در ادامه همان مثال ساده کلید برق قسمت قبل را با Stateless پیاده سازی خواهیم کرد:
کار با ایجاد یک وهله از ماشین حالت (new StateMachine) آغاز میشود. حالت آغازین آن (initialState) مطابق مثال قسمت قبل، مساوی off است.
امضای کلاس StateMachine را در ذیل مشاهده میکنید؛ جهت توضیح آرگومانهای جنریک string و char معرفی شده در مثال:
که اولی بیانگر نوع حالات قابل تعریف است و دومی نوع رویداد قابل دریافت را مشخص میکند.
برای مثال در اینجا حالات روشن و خاموش، با رشتههای on و off مشخص شدهاند و رویداد قابل قبول دریافتی، کاراکتر فاصله است.
سپس نیاز است این ماشین حالت را برای معرفی رویدادهایی (trigger در اینجا) که سبب تغییر حالت آن میشوند، تنظیم کنیم. اینکار توسط متدهای Configure و Permit انجام خواهد شد. متد Configure، یکی از حالات از پیش تعیین شده را جهت تنظیم، مشخص میکند و سپس در متد Permit تعیین خواهیم کرد که بر اساس رخدادی مشخص (برای مثال در اینجا فشرده شدن کلید space) وضعیت حالت جاری، به وضعیت جدیدی (destinationState) منتقل شود.
نهایتا این ماشین حالت در یک حلقه بینهایت مشغول به کار خواهد شد. برای نمونه یک Thread پس زمینه (BackgroundWorker) نیز میتواند همین کار را در برنامههای ویندوزی انجام دهد.
یک نکته
علاوه بر روشهای یاد شدهی تشخیص الگوی ماشین حالت که در قسمت قبل بررسی شدند، مورد refactoring انبوهی از if و elseها و یا switchهای بسیار طولانی را نیز میتوان به این لیست افزود.
استفاده از Stateless Designer برای تولید کدهای ماشین حالت
کتابخانه Stateless دارای یک طراح و Code generator بصری سورس باز است که آنرا به شکل افزونهای برای VS.NET میتوانید در سایت Codeplex دریافت کنید. این طراح از کتابخانه GLEE برای رسم گراف استفاده میکند.
کار مقدماتی با آن به نحو زیر است:
الف) فایل StatelessDesignerPackage.vsix را از سایت کدپلکس دریافت و نصب کنید. البته نگارش فعلی آن فقط با VS 2012 سازگار است.
ب) ارجاعی را به اسمبلی stateless به پروژه خود اضافه نمائید (به یک پروژه جدید یا از پیش موجود).
ج) از منوی پروژه، گزینه Add new item را انتخاب کرده و سپس در صفحه ظاهر شده، گزینه جدید Stateless state machine را انتخاب و به پروژه اضافه نمائید.
کار با این طراح، با ادیت XML آن شروع میشود. برای مثال گردش کاری ارسال و تائید یک مطلب جدید را در بلاگی فرضی، به نحو زیر وارد نمائید:
حاصل آن گراف زیر خواهد بود:
به علاوه کدهای زیر که به صورت خودکار تولید شدهاند:
توضیحات:
ماشین حالت فوق دارای چهار حالت شروع، در حال بررسی، منتشر شده و رد شده است. معمول است که این چهار حالت را به شکل یک enum معرفی کنند که در کدهای تولیدی فوق نیز به همین نحو عمل گردیده و public enum State معرف چهار حالت ذکر شده است. همچنین رویدادهای ذخیره، نیاز به ویرایش، ویرایش، تائید و رد نیز توسط public enum Trigger معرفی شدهاند.
در قسمت Transitions، بر اساس یک رویداد (Trigger در اینجا)، انتقال از یک حالت به حالتی دیگر را سبب خواهیم شد.
تعاریف اصلی تنظیمات ماشین حالت، در سازنده کلاس BlogPostStateMachine انجام شده است. این تعاریف نیز بسیار ساده هستند. به ازای هر حالت، یک Configure داریم. در متدهای OnEntry و OnExit هر حالت، یک سری callback function فراخوانی خواهند شد. برای مثال در حالت Rejected یا Approved میتوان ایمیلی را به ارسال کننده مطلب جهت یادآوری وضعیت رخ داده، ارسال نمود.
متدهای PermitIf سبب انتقال شرطی، به حالتی دیگر خواهند شد. برای مثال رد یا تائید یک مطلب نیاز به دسترسی مدیریتی خواهد داشت. این نوع موارد را توسط delgateهای Guard ایی که برای مدیریت شرطها ایجاد کرده است، میتوان تنظیم کرد. PermitReentryIf سبب بازگشت مجدد به همان حالت میگردد. برای مثال ویرایش و ذخیره یک مطلب در حال انتشار، سبب تائید یا رد آن نخواهد شد؛ صرفا عملیات ذخیره صورت گرفته و ماشین حالت مجددا در همان مرحله باقی خواهد ماند.
نحوه استفاده از ماشین حالت تولیدی:
همانطور که عنوان شد، حداقل استفاده از ماشینهای حالت، refactoing انبوهی از if و elseها است که در حالت مدیریت یک چنین گردشهای کاری باید تدارک دید.
در کلاس فوق، نحوه استفاده از ماشین حالت تولیدی را مشاهده میکنید. در delegateهای Guard، سطوح دسترسی انجام عملیات بررسی خواهند شد. برای مثال، از بانک اطلاعاتی بر اساس اطلاعات کاربر جاری وارد شده به سیستم اخذ میگردند. در متدهای Exit هر مرحله، کارهای ذخیره سازی اطلاعات در بانک اطلاعاتی، ذخیره سازی حالت (مثلا در یک فیلد که بعدا قابل بازیابی باشد) صورت میگیرد و در صورت نیاز ایمیلی به اشخاص مختلف ارسال خواهد شد.
برای به حرکت درآوردن این ماشین، نیاز به یک سری اکشن متد نیز میباشد. تعدادی از این موارد را در انتهای کلاس فوق ملاحظه میکنید. کد نویسی آنها در حد فراخوانی متد TryFireTrigger ماشین حالت است.
یک نکته:
ماشین حالت تولیدی به صورت پیش فرض در حالت State.Begin قرار دارد. میتوان این مورد را از بانک اطلاعاتی خواند و سپس مقدار دهی نمود تا با هر بار وهله سازی ماشین حالت دقیقا مشخص باشد که در چه مرحلهای قرار داریم و TryFireTrigger بتواند بر این اساس تصمیمگیری کند که آیا مجاز است عملیاتی را انجام دهد یا خیر.
کتابخانه سورس باز Stateless، برای طراحی و پیاده سازی «ماشینهای حالت گردش کاری مانند» تهیه شده و مزایای زیر را نسبت به Windows workflow foundation دارا است:
- جمعا 30 کیلوبایت است!
- تمام اجزای آن سورس باز است.
- دارای API روان و سادهای است.
- امکان تبدیل UML state diagrams، به نمونه معادل Stateless بسیار ساده و سریع است.
- به دلیل code first بودن، کار کردن با آن برای برنامه نویسها سادهتر بوده و افزودن یا تغییر اجزای آن با کدنویسی به سادگی میسر است.
دریافت کتابخانه Stateless از Google code و یا از NuGet
پیاده سازی مثال کلید برق با Stateless
در ادامه همان مثال ساده کلید برق قسمت قبل را با Stateless پیاده سازی خواهیم کرد:
using System; using Stateless; namespace StatelessTests { class Program { static void Main(string[] args) { try { string on = "On", off = "Off"; var space = ' '; var onOffSwitch = new StateMachine<string, char>(initialState: off); onOffSwitch.Configure(state: off).Permit(trigger: space, destinationState: on); onOffSwitch.Configure(state: on).Permit(trigger: space, destinationState: off); Console.WriteLine("Press <space> to toggle the switch. Any other key will raise an error."); while (true) { Console.WriteLine("Switch is in state: " + onOffSwitch.State); var pressed = Console.ReadKey(true).KeyChar; onOffSwitch.Fire(trigger: pressed); } } catch (Exception ex) { Console.WriteLine("Exception: " + ex.Message); Console.WriteLine("Press any key to continue..."); Console.ReadKey(true); } } } }
امضای کلاس StateMachine را در ذیل مشاهده میکنید؛ جهت توضیح آرگومانهای جنریک string و char معرفی شده در مثال:
public class StateMachine<TState, TTrigger>
برای مثال در اینجا حالات روشن و خاموش، با رشتههای on و off مشخص شدهاند و رویداد قابل قبول دریافتی، کاراکتر فاصله است.
سپس نیاز است این ماشین حالت را برای معرفی رویدادهایی (trigger در اینجا) که سبب تغییر حالت آن میشوند، تنظیم کنیم. اینکار توسط متدهای Configure و Permit انجام خواهد شد. متد Configure، یکی از حالات از پیش تعیین شده را جهت تنظیم، مشخص میکند و سپس در متد Permit تعیین خواهیم کرد که بر اساس رخدادی مشخص (برای مثال در اینجا فشرده شدن کلید space) وضعیت حالت جاری، به وضعیت جدیدی (destinationState) منتقل شود.
نهایتا این ماشین حالت در یک حلقه بینهایت مشغول به کار خواهد شد. برای نمونه یک Thread پس زمینه (BackgroundWorker) نیز میتواند همین کار را در برنامههای ویندوزی انجام دهد.
یک نکته
علاوه بر روشهای یاد شدهی تشخیص الگوی ماشین حالت که در قسمت قبل بررسی شدند، مورد refactoring انبوهی از if و elseها و یا switchهای بسیار طولانی را نیز میتوان به این لیست افزود.
استفاده از Stateless Designer برای تولید کدهای ماشین حالت
کتابخانه Stateless دارای یک طراح و Code generator بصری سورس باز است که آنرا به شکل افزونهای برای VS.NET میتوانید در سایت Codeplex دریافت کنید. این طراح از کتابخانه GLEE برای رسم گراف استفاده میکند.
کار مقدماتی با آن به نحو زیر است:
الف) فایل StatelessDesignerPackage.vsix را از سایت کدپلکس دریافت و نصب کنید. البته نگارش فعلی آن فقط با VS 2012 سازگار است.
ب) ارجاعی را به اسمبلی stateless به پروژه خود اضافه نمائید (به یک پروژه جدید یا از پیش موجود).
ج) از منوی پروژه، گزینه Add new item را انتخاب کرده و سپس در صفحه ظاهر شده، گزینه جدید Stateless state machine را انتخاب و به پروژه اضافه نمائید.
کار با این طراح، با ادیت XML آن شروع میشود. برای مثال گردش کاری ارسال و تائید یک مطلب جدید را در بلاگی فرضی، به نحو زیر وارد نمائید:
<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema"> <settings> <itemname>BlogPostStateMachine</itemname> <namespace>StatelessTests</namespace> <class>public</class> </settings> <triggers> <trigger>Save</trigger> <trigger>RequireEdit</trigger> <trigger>Accept</trigger> <trigger>Reject</trigger> </triggers> <states> <state start="yes">Begin</state> <state>InProgress</state> <state>Published</state> <state>Rejected</state> </states> <transitions> <transition trigger="Save" from="Begin" to="InProgress" /> <transition trigger="Accept" from="InProgress" to="Published" /> <transition trigger="Reject" from="InProgress" to="Rejected" /> <transition trigger="Save" from="InProgress" to="InProgress" /> <transition trigger="RequireEdit" from="Published" to="InProgress" /> <transition trigger="RequireEdit" from="Rejected" to="InProgress" /> </transitions> </statemachine>
به علاوه کدهای زیر که به صورت خودکار تولید شدهاند:
using Stateless; namespace StatelessTests { public class BlogPostStateMachine { public delegate void UnhandledTriggerDelegate(State state, Trigger trigger); public delegate void EntryExitDelegate(); public delegate bool GuardClauseDelegate(); public enum Trigger { Save, RequireEdit, Accept, Reject, } public enum State { Begin, InProgress, Published, Rejected, } private readonly StateMachine<State, Trigger> stateMachine = null; public EntryExitDelegate OnBeginEntry = null; public EntryExitDelegate OnBeginExit = null; public EntryExitDelegate OnInProgressEntry = null; public EntryExitDelegate OnInProgressExit = null; public EntryExitDelegate OnPublishedEntry = null; public EntryExitDelegate OnPublishedExit = null; public EntryExitDelegate OnRejectedEntry = null; public EntryExitDelegate OnRejectedExit = null; public GuardClauseDelegate GuardClauseFromBeginToInProgressUsingTriggerSave = null; public GuardClauseDelegate GuardClauseFromInProgressToPublishedUsingTriggerAccept = null; public GuardClauseDelegate GuardClauseFromInProgressToRejectedUsingTriggerReject = null; public GuardClauseDelegate GuardClauseFromInProgressToInProgressUsingTriggerSave = null; public GuardClauseDelegate GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = null; public GuardClauseDelegate GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = null; public UnhandledTriggerDelegate OnUnhandledTrigger = null; public BlogPost() { stateMachine = new StateMachine<State, Trigger>(State.Begin); stateMachine.Configure(State.Begin) .OnEntry(() => { if (OnBeginEntry != null) OnBeginEntry(); }) .OnExit(() => { if (OnBeginExit != null) OnBeginExit(); }) .PermitIf(Trigger.Save, State.InProgress , () => { if (GuardClauseFromBeginToInProgressUsingTriggerSave != null) return GuardClauseFromBeginToInProgressUsingTriggerSave(); return true; } ) ; stateMachine.Configure(State.InProgress) .OnEntry(() => { if (OnInProgressEntry != null) OnInProgressEntry(); }) .OnExit(() => { if (OnInProgressExit != null) OnInProgressExit(); }) .PermitIf(Trigger.Accept, State.Published , () => { if (GuardClauseFromInProgressToPublishedUsingTriggerAccept != null) return GuardClauseFromInProgressToPublishedUsingTriggerAccept(); return true; } ) .PermitIf(Trigger.Reject, State.Rejected , () => { if (GuardClauseFromInProgressToRejectedUsingTriggerReject != null) return GuardClauseFromInProgressToRejectedUsingTriggerReject(); return true; } ) .PermitReentryIf(Trigger.Save , () => { if (GuardClauseFromInProgressToInProgressUsingTriggerSave != null) return GuardClauseFromInProgressToInProgressUsingTriggerSave(); return true; } ) ; stateMachine.Configure(State.Published) .OnEntry(() => { if (OnPublishedEntry != null) OnPublishedEntry(); }) .OnExit(() => { if (OnPublishedExit != null) OnPublishedExit(); }) .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit(); return true; } ) ; stateMachine.Configure(State.Rejected) .OnEntry(() => { if (OnRejectedEntry != null) OnRejectedEntry(); }) .OnExit(() => { if (OnRejectedExit != null) OnRejectedExit(); }) .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit(); return true; } ) ; stateMachine.OnUnhandledTrigger((state, trigger) => { if (OnUnhandledTrigger != null) OnUnhandledTrigger(state, trigger); }); } public bool TryFireTrigger(Trigger trigger) { if (!stateMachine.CanFire(trigger)) { return false; } stateMachine.Fire(trigger); return true; } public State GetState { get { return stateMachine.State; } } } }
ماشین حالت فوق دارای چهار حالت شروع، در حال بررسی، منتشر شده و رد شده است. معمول است که این چهار حالت را به شکل یک enum معرفی کنند که در کدهای تولیدی فوق نیز به همین نحو عمل گردیده و public enum State معرف چهار حالت ذکر شده است. همچنین رویدادهای ذخیره، نیاز به ویرایش، ویرایش، تائید و رد نیز توسط public enum Trigger معرفی شدهاند.
در قسمت Transitions، بر اساس یک رویداد (Trigger در اینجا)، انتقال از یک حالت به حالتی دیگر را سبب خواهیم شد.
تعاریف اصلی تنظیمات ماشین حالت، در سازنده کلاس BlogPostStateMachine انجام شده است. این تعاریف نیز بسیار ساده هستند. به ازای هر حالت، یک Configure داریم. در متدهای OnEntry و OnExit هر حالت، یک سری callback function فراخوانی خواهند شد. برای مثال در حالت Rejected یا Approved میتوان ایمیلی را به ارسال کننده مطلب جهت یادآوری وضعیت رخ داده، ارسال نمود.
متدهای PermitIf سبب انتقال شرطی، به حالتی دیگر خواهند شد. برای مثال رد یا تائید یک مطلب نیاز به دسترسی مدیریتی خواهد داشت. این نوع موارد را توسط delgateهای Guard ایی که برای مدیریت شرطها ایجاد کرده است، میتوان تنظیم کرد. PermitReentryIf سبب بازگشت مجدد به همان حالت میگردد. برای مثال ویرایش و ذخیره یک مطلب در حال انتشار، سبب تائید یا رد آن نخواهد شد؛ صرفا عملیات ذخیره صورت گرفته و ماشین حالت مجددا در همان مرحله باقی خواهد ماند.
نحوه استفاده از ماشین حالت تولیدی:
همانطور که عنوان شد، حداقل استفاده از ماشینهای حالت، refactoing انبوهی از if و elseها است که در حالت مدیریت یک چنین گردشهای کاری باید تدارک دید.
namespace StatelessTests { public class BlogPostManager { private BlogPostStateMachine _stateMachine; public BlogPostManager() { configureWorkflow(); } private void configureWorkflow() { _stateMachine = new BlogPostStateMachine(); _stateMachine.GuardClauseFromBeginToInProgressUsingTriggerSave = () => { return UserCanPost; }; _stateMachine.OnBeginExit = () => { /* save data + save state + send an email to admin */ }; _stateMachine.GuardClauseFromInProgressToPublishedUsingTriggerAccept = () => { return UserIsAdmin; }; _stateMachine.GuardClauseFromInProgressToRejectedUsingTriggerReject = () => { return UserIsAdmin; }; _stateMachine.GuardClauseFromInProgressToInProgressUsingTriggerSave = () => { return UserHasEditRights; }; _stateMachine.OnInProgressExit = () => { /* save data + save state + send an email to user */ }; _stateMachine.OnPublishedExit = () => { /* save data + save state + send an email to admin */ }; _stateMachine.GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; }; _stateMachine.OnRejectedExit = () => { /* save data + save state + send an email to admin */ }; _stateMachine.GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; }; } public bool UserIsAdmin { get { return true; // TODO: Evaluate if user is an admin. } } public bool UserCanPost { get { return true; // TODO: Evaluate if user is authenticated } } public bool UserHasEditRights { get { return true; // TODO: Evaluate if user is owner or admin } } // User actions public void Save() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Save); } public void RequireEdit() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.RequireEdit); } // Admin actions public void Accept() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Accept); } public void Reject() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Reject); } } }
برای به حرکت درآوردن این ماشین، نیاز به یک سری اکشن متد نیز میباشد. تعدادی از این موارد را در انتهای کلاس فوق ملاحظه میکنید. کد نویسی آنها در حد فراخوانی متد TryFireTrigger ماشین حالت است.
یک نکته:
ماشین حالت تولیدی به صورت پیش فرض در حالت State.Begin قرار دارد. میتوان این مورد را از بانک اطلاعاتی خواند و سپس مقدار دهی نمود تا با هر بار وهله سازی ماشین حالت دقیقا مشخص باشد که در چه مرحلهای قرار داریم و TryFireTrigger بتواند بر این اساس تصمیمگیری کند که آیا مجاز است عملیاتی را انجام دهد یا خیر.
در ادامهی سری «ارتقاء به ASP.NET Core 1.0» اگر بخواهیم مباحث اعتبارسنجی کاربران و ASP.NET Identity مخصوص آنرا بررسی کنیم، نیاز است ابتدا مباحث Entity framework Core 1.0 را بررسی کنیم. به همین جهت در طی چند قسمت مباحث پایهای کار با EF Core 1.0 را در ASP.NET Core 1.0، بررسی خواهیم کرد. بنابراین پیشنیاز ضروری این مباحث، مطالعهی سری «ارتقاء به ASP.NET Core 1.0» است و در آن از مباحثی مانند چگونگی کار با فایلهای کانفیگ جدید، تزریق وابستگیها و سرویسها، فعال سازی سرویس Logging، فعال سازی صفحات مخصوص توسعه دهندهها و ... در ASP.NET Core 1.0 استفاده خواهد شد.
EF Core چیست؟
EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانکهای اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر میکند. توسط آن قسمت عمدهی کدهای مستقیم کار با بانکهای اطلاعاتی حذف شده و تبدیل به کدهای دات نتی میشوند. مزیت این لایهی Abstraction اضافی (لایهای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامهاست؛ بدون نیاز به تغییری در سایر قسمتهای برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر میکنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر میرسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئریها، رفتار بسیاری از بانکهای اطلاعاتی با آنها همانند رویههای ذخیره شده است که به عدم تولید Query planهای مجزایی به ازای هر کوئری رسیده منجر میشود که در نهایت سبب بالا رفتن سرعت اجرای کوئریها و مصرف حافظهی کمتری در سمت سرور بانک اطلاعاتی میگردد.
تفاوت EF Core با نگارشهای دیگر Entity framework در چیست؟
سورس باز بودن
EF از نگارشهای آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارشهای پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.
چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفادهی از آن در انواع و اقسام برنامههای دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامههای مختلف به شرح زیر است:
افزایش تعداد بانکهای اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانکهای اطلاعاتی رابطهای بود و همچنین مایکروسافت صرفا نگارشهای مختلف SQL Server را به صورت رسمی پشتیبانی میکرد و برای سایر بانکهای اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانکهای اطلاعاتی رابطهای، پشتیبانی از بانکهای اطلاعاتی NoSQL هم اضافه شدهاست؛ به همراه پروایدر ویژهای به نام In Memory جهت انجام سادهتر Unit testing. کاری که با نگارشهای پیشین EF به سادگی و از روز اول پشتیبانی نمیشد.
حذف و یا عدم پیاده سازی تعدادی از قابلیتهای EF 6.x
اگر موارد فوق جزو مهمترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگیها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامههای UWP نیاز دارد تمام نوعها را همانند زبانهای C و ++C، در زمان کامپایل بداند)، یک سری از قابلیتهای EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمیشوند. لیست کامل و به روز شدهی این موارد را در اینجا میتوانید مطالعه کنید.
بنابراین امکان انتقال برنامههای EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.
برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0
برای نصب تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بستهی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
البته در این قسمت صرفا از بستهی اول که جهت اتصال به SQL Server است استفاده میکنیم. بستههای دیگر را در قسمتهای بعد، برای به روز رسانی اسکیمای بانک اطلاعاتی (مباحث Migrations) و مباحث scaffolding استفاده خواهیم کرد.
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
این مداخلی که توسط نیوگت اضافه شدهاند، نیاز به اصلاح دارند؛ به این صورت:
نیاز است در قسمت dependencies مشخص کنیم که ابزارهای اضافه شده مخصوص build هستند و نه اجرای برنامه. همچنین قسمت tools را باید با Microsoft.EntityFrameworkCore.Tools مقدار دهی کرد تا بتوان از این ابزار در خط فرمان، جهت اجرای فرامین migrations استفاده کرد.
بنابراین از همین ابتدای کار، بدون مراجعهی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آنرا ذخیره کنید؛ تا کار به روز رسانی و نصب بستهها، به صورت خودکار و همچنین صحیحی انجام شود.
فعال سازی صفحات مخصوص توسعه دهندههای EF Core 1.0
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهندهها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بستهی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحهی نمایش خطاهای بانک اطلاعاتی را میسر میکند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
سپس میتوان متد جدید UseDatabaseErrorPage را در متد Configure کلاس آغازین برنامه، فراخوانی کرد:
با فعال سازی این صفحه، اگر در حین توسعهی برنامه و اتصال به بانک اطلاعاتی، خطایی رخ دهد، بجای مشاهدهی یک صفحهی خطای عمومی (اگر UseDeveloperExceptionPage را فعال کرده باشید)، اینبار ریز جزئیات بیشتری را به همراه توصیههایی از طرف تیم EF مشاهده خواهید کرد.
تعریف اولین Context برنامه و مشخص سازی رشتهی اتصالی آن
در این تصویر، زیر ساخت نگاشتهای EF Core را مشاهده میکنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرندهی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل اینها را مشاهده میکنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که اینها نیز به فیلدها و ستونهای آن جدول در سمت بانک اطلاعاتی نگاشت میشوند.
بنابراین برای شروع کار، پوشهای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه میکنیم:
کلاس Person بیانگر ساختار جدول اشخاص بانک اطلاعاتی است. برای اینکه این کلاس را تبدیل و نگاشت به یک جدول کنیم، نیاز است آنرا به صورت یک DbSet در معرض دید EF Core قرار دهیم و اینکار در کلاسی که از DbContex مشتق میشود، صورت خواهد گرفت:
بنابراین در ادامه کلاس جدید ApplicationDbContext را که از کلاس پایه DbContext مشتق میشود تعریف کرده و سپس کلاس Person را به صورت یک DbSet در معرض دید EF Core قرار میدهیم.
سازندهی این کلاس نیز به نحو خاصی تعریف شدهاست. اگر به سورسهای EF Core مراجعه کنیم، کلاس پایهی DbContext دارای دو سازندهی با و بدون پارامتر است:
اگر از سازندهی بدون پارامتر استفاده کنیم و برای مثال در کلاس ApplicationDbContext فوق، به طور کامل سازندهی تعریف شده را حذف کنیم، باید به نحو ذیل تنظیمات بانک اطلاعاتی را مشخص کنیم:
در این حالت باید متد OnConfiguring را override و یا بازنویسی کنیم، تا بتوان از اول مشخص کرد که قرار است از پروایدر SQL Server استفاده کنیم و ثانیا رشتهی اتصالی به آن چیست.
اما چون در یک برنامهی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام میشود و در آنجا به سادگی میتوان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشتهی اتصالی را دریافت کرد، مرسوم است از سازندهی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعهی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» به فایل appsettings.json مراجعه کرده و تنظیمات رشتهی اتصالی برنامه را به صورت ذیل در آن مشخص میکنیم:
باید دقت داشت که نام این مداخل کاملا اختیاری هستند و در نهایت باید در کلاس آغازین برنامه به صورت صریحی مشخص شوند.
در اینجا به وهلهی پیش فرض SQL Server اشاره شدهاست؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده میشود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیدهاست.
پس از تعریف رشتهی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازندهی با پارامتر آن استفاده میکنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویسهای ASP.NET Core معرفی میکنیم:
در اینجا جهت یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» نحوهی وهله سازی خاصیت Configuration که در متد UseSqlServer مورد استفاده قرار گرفتهاست، نیز ذکر شدهاست.
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی میکنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.
یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد
با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگیها نیز در آن وجود دارد. یعنی بجای روش فوق، میتوان IConfigurationRoot را به سازندهی کلاس Context برنامه نیز تزریق کرد:
با توجه به اینکه IConfigurationRoot در کلاس ConfigureServices به صورت Singleton، به مجموعهی سرویسهای برنامه معرفی شدهاست، از آن در تمام کلاسهای برنامه که تحت نظر سیستم تزریق وابستگیهای توکار ASP.NET Core هستند، میتوان استفاده کرد.
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا میکند و ساده میشود:
یک نکته: امکان تزریق ApplicationDbContext به تمام کلاسهای برنامه وجود دارد
همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامهی ASP.NET Core نیز وجود دارد:
در اینجا نحوهی تزریق DB Context برنامه را به یک کنترلر مشاهده میکنید. البته هرچند تزریق یک کلاس مشخص به این شکل، تزریق وابستگیها نام ندارد و هنوز این کنترلر دقیقا وابستهاست به پیاده سازی خاص کلاس ApplicationDbContext، اما ... در کل امکان آن هست.
در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:
علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشدهاست و همچنین ساختار جداول را به آن منتقل نکردهایم که این موارد را در قسمتهای بعدی مرور خواهیم کرد.
EF Core چیست؟
EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانکهای اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر میکند. توسط آن قسمت عمدهی کدهای مستقیم کار با بانکهای اطلاعاتی حذف شده و تبدیل به کدهای دات نتی میشوند. مزیت این لایهی Abstraction اضافی (لایهای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامهاست؛ بدون نیاز به تغییری در سایر قسمتهای برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر میکنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر میرسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئریها، رفتار بسیاری از بانکهای اطلاعاتی با آنها همانند رویههای ذخیره شده است که به عدم تولید Query planهای مجزایی به ازای هر کوئری رسیده منجر میشود که در نهایت سبب بالا رفتن سرعت اجرای کوئریها و مصرف حافظهی کمتری در سمت سرور بانک اطلاعاتی میگردد.
تفاوت EF Core با نگارشهای دیگر Entity framework در چیست؟
سورس باز بودن
EF از نگارشهای آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارشهای پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.
چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفادهی از آن در انواع و اقسام برنامههای دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامههای مختلف به شرح زیر است:
• All .NET application (Console, ASP.NET 4, WinForms, WPF) • Mac and Linux applications (Mono) • UWP (Universal Windows Platform) • ASP.NET Core applications • Can use EF Core in Windows phone and Windows store app
افزایش تعداد بانکهای اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانکهای اطلاعاتی رابطهای بود و همچنین مایکروسافت صرفا نگارشهای مختلف SQL Server را به صورت رسمی پشتیبانی میکرد و برای سایر بانکهای اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانکهای اطلاعاتی رابطهای، پشتیبانی از بانکهای اطلاعاتی NoSQL هم اضافه شدهاست؛ به همراه پروایدر ویژهای به نام In Memory جهت انجام سادهتر Unit testing. کاری که با نگارشهای پیشین EF به سادگی و از روز اول پشتیبانی نمیشد.
حذف و یا عدم پیاده سازی تعدادی از قابلیتهای EF 6.x
اگر موارد فوق جزو مهمترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگیها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامههای UWP نیاز دارد تمام نوعها را همانند زبانهای C و ++C، در زمان کامپایل بداند)، یک سری از قابلیتهای EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمیشوند. لیست کامل و به روز شدهی این موارد را در اینجا میتوانید مطالعه کنید.
بنابراین امکان انتقال برنامههای EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.
برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0
برای نصب تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بستهی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final", "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0" } }
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.EntityFrameworkCore.SqlServer.Design": { "version": "1.0.0", "type": "build" } }, "tools": { // same as before "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "imports": [ "portable-net45+win8" ] } } }
بنابراین از همین ابتدای کار، بدون مراجعهی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آنرا ذخیره کنید؛ تا کار به روز رسانی و نصب بستهها، به صورت خودکار و همچنین صحیحی انجام شود.
فعال سازی صفحات مخصوص توسعه دهندههای EF Core 1.0
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهندهها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بستهی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحهی نمایش خطاهای بانک اطلاعاتی را میسر میکند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
{ "dependencies": { // same as before "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0" } }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDatabaseErrorPage(); }
تعریف اولین Context برنامه و مشخص سازی رشتهی اتصالی آن
در این تصویر، زیر ساخت نگاشتهای EF Core را مشاهده میکنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرندهی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل اینها را مشاهده میکنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که اینها نیز به فیلدها و ستونهای آن جدول در سمت بانک اطلاعاتی نگاشت میشوند.
بنابراین برای شروع کار، پوشهای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه میکنیم:
namespace Core1RtmEmptyTest.Entities { public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Person> Persons { get; set; } } }
سازندهی این کلاس نیز به نحو خاصی تعریف شدهاست. اگر به سورسهای EF Core مراجعه کنیم، کلاس پایهی DbContext دارای دو سازندهی با و بدون پارامتر است:
protected DbContext() : this((DbContextOptions) new DbContextOptions<DbContext>()) { } public DbContext([NotNull] DbContextOptions options) { // … }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"... connection string ..."); } } }
اما چون در یک برنامهی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام میشود و در آنجا به سادگی میتوان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشتهی اتصالی را دریافت کرد، مرسوم است از سازندهی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعهی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» به فایل appsettings.json مراجعه کرده و تنظیمات رشتهی اتصالی برنامه را به صورت ذیل در آن مشخص میکنیم:
{ "ConnectionStrings": { "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true" } }
در اینجا به وهلهی پیش فرض SQL Server اشاره شدهاست؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده میشود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیدهاست.
پس از تعریف رشتهی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازندهی با پارامتر آن استفاده میکنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویسهای ASP.NET Core معرفی میکنیم:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false) .AddJsonFile($"appsettings.{env}.json", optional: true); Configuration = builder.Build(); } public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; }); services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]); });
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی میکنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.
یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد
با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگیها نیز در آن وجود دارد. یعنی بجای روش فوق، میتوان IConfigurationRoot را به سازندهی کلاس Context برنامه نیز تزریق کرد:
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { private readonly IConfigurationRoot _configuration; public ApplicationDbContext(IConfigurationRoot configuration) { _configuration = configuration; } //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) //{ //} public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]); } } }
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا میکند و ساده میشود:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>();
یک نکته: امکان تزریق ApplicationDbContext به تمام کلاسهای برنامه وجود دارد
همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامهی ASP.NET Core نیز وجود دارد:
using System.Linq; using Core1RtmEmptyTest.Entities; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class TestDBController : Controller { private readonly ApplicationDbContext _ctx; public TestDBController(ApplicationDbContext ctx) { _ctx = ctx; } public IActionResult Index() { var name = _ctx.Persons.First().FirstName; return Json(new { firstName = name }); } } }
در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:
علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشدهاست و همچنین ساختار جداول را به آن منتقل نکردهایم که این موارد را در قسمتهای بعدی مرور خواهیم کرد.
نظرات مطالب
آشنایی با الگوی Adapter
در زبان انگلیسی derived به معنای مشتق شده هست و به نظر در اینجا منظور اصلی همین بوده؛ چون derived هم برای اینترفیس و هم در ارث بری، هر دو بکار برده میشه. به کلاسی که یک اینترفیس رو پیاده سازی میکنه، derived class هم میگن. مثلا: Because interfaces must be implemented by derived classes and structs, they define a contract
net framework. شامل Framework
Class Library یا به اختصار FCL است. FCL مجموعهای از dll اسمبلیهایی است که صدها و هزاران نوع در آن تعریف شدهاند و هر نوع تعدادی کار انجام
میدهد. همچنین مایکروسافت کتابخانههای اضافهتری را چون azure و Directx نیز ارائه کرده است که باز هر کدام شامل نوعهای زیادی میشوند. این
کتابخانه به طور شگفت آوری باعث سرعت و راحتی توسعه دهندگان در زمینه
فناوریهای مایکروسافت گشته است.
تعدادی از فناوریهایی که توسط این کتابخانه پشتیبانی میشوند در زیر آمده است:
Web Service: این فناوری اجازهی ارسال و دریافت پیامهای تحت شبکه را به خصوص بر روی اینترنت، فراهم میکند و باعث ارتباط جامعتر بین برنامهها و فناوریهای مختلف میگردد. در انواع جدیدتر WCF و Web Api نیز به بازار ارائه شدهاند.
webform و MVC : فناوریهای تحت وب که باعث سهولت در ساخت وب سایتها میشوند که وب فرم رفته رفته به سمت منسوخ شدن پیش میرود و در صورتی که قصد دارید طراحی وب را آغاز کنید توصیه میکنم از همان اول به سمت MVC بروید.
Rich Windows GUI Application : برای سهولت در ایجاد برنامههای تحت وب حالا چه با فناوری WPF یا فناوری قدیمی و البته منسوخ شده Windows Form.
Windows Console Application: برای ایجاد برنامههای ساده و بدون رابط گرافیکی.
Windows Services: شما میتوانید یک یا چند سرویس تحت ویندوز را که توسط Service Control Manager یا به اختصار SCM کنترل میشوند، تولید کنید.
Database stored Procedure: نوشتن stored procedure بر روی دیتابیسهایی چون sql server و اوراکل و ... توسط فریم ورک دات نت مهیاست.
Component Libraray: ساخت اسمبلیهای واحدی که میتوانند با انواع مختلفی از موارد بالا ارتباط برقرار کنند.
Portable Class Libary : این نوع پروژهها شما را قادر میسازد تا کلاسهایی با قابلیت انتقال پذیری برای استفاده در سیلور لایت، ویندوز فون و ایکس باکس و فروشگاه ویندوز و ... تولید کنید.
ازآنجا که یک کتابخانه شامل زیادی نوع میگردد سعی شده است گروه بندیهای مختلفی از آن در قالبی به اسم فضای نام namespace تقسیم بندی گردند که شما آشنایی با آنها دارید. به همین جهت فقط تصویر زیر را که نمایشی از فضای نامهای اساسی و مشترک و پرکاربرد هستند، قرار میدهم.
تعدادی از فناوریهایی که توسط این کتابخانه پشتیبانی میشوند در زیر آمده است:
Web Service: این فناوری اجازهی ارسال و دریافت پیامهای تحت شبکه را به خصوص بر روی اینترنت، فراهم میکند و باعث ارتباط جامعتر بین برنامهها و فناوریهای مختلف میگردد. در انواع جدیدتر WCF و Web Api نیز به بازار ارائه شدهاند.
webform و MVC : فناوریهای تحت وب که باعث سهولت در ساخت وب سایتها میشوند که وب فرم رفته رفته به سمت منسوخ شدن پیش میرود و در صورتی که قصد دارید طراحی وب را آغاز کنید توصیه میکنم از همان اول به سمت MVC بروید.
Rich Windows GUI Application : برای سهولت در ایجاد برنامههای تحت وب حالا چه با فناوری WPF یا فناوری قدیمی و البته منسوخ شده Windows Form.
Windows Console Application: برای ایجاد برنامههای ساده و بدون رابط گرافیکی.
Windows Services: شما میتوانید یک یا چند سرویس تحت ویندوز را که توسط Service Control Manager یا به اختصار SCM کنترل میشوند، تولید کنید.
Database stored Procedure: نوشتن stored procedure بر روی دیتابیسهایی چون sql server و اوراکل و ... توسط فریم ورک دات نت مهیاست.
Component Libraray: ساخت اسمبلیهای واحدی که میتوانند با انواع مختلفی از موارد بالا ارتباط برقرار کنند.
Portable Class Libary : این نوع پروژهها شما را قادر میسازد تا کلاسهایی با قابلیت انتقال پذیری برای استفاده در سیلور لایت، ویندوز فون و ایکس باکس و فروشگاه ویندوز و ... تولید کنید.
ازآنجا که یک کتابخانه شامل زیادی نوع میگردد سعی شده است گروه بندیهای مختلفی از آن در قالبی به اسم فضای نام namespace تقسیم بندی گردند که شما آشنایی با آنها دارید. به همین جهت فقط تصویر زیر را که نمایشی از فضای نامهای اساسی و مشترک و پرکاربرد هستند، قرار میدهم.
در CLR مفهومی به نام Common Type System یا CTS وجود دارد که توضیح میدهد نوعها باید چگونه تعریف شوند و چگونه باید رفتار کنند که این قوانین از آنجایی که در ریشهی CLR نهفته است، بین تمامی زبانهای دات نت مشترک میباشد. تعدادی از مشخصات این CTS در زیر آورده شده است ولی در آینده بررسی بیشتری روی آنان خواهیم داشت:
- فیلد
- متد
- پراپرتی
- رویدادها
CTS همچنین شامل قوانین زیادی در مورد وضعیت کپسوله سازی برای اعضای یک نوع دارد:
- private
- public
- Family یا در زبانهایی مثل سی ++ و سی شارپ با نام protected شناخته میشود.
- family and assembly: این هم مثل بالایی است ولی کلاس مشتق شده باید در همان اسمبلی باشد. در زبانهایی چون سی شارپ و ویژوال بیسیک، چنین امکانی پیاده سازی نشدهاست و دسترسی به آن ممکن نیست ولی در IL Assembly چنین قابلیتی وجود دارد.
- Assembly یا در بعضی زبانها به نام internal شناخته میشود.
- Family Or Assembly: که در سی شارپ با نوع Protected internal شناخته میشود. در این وضعیت هر عضوی در هر اسمبلی قابل ارث بری است و یک عضو فقط میتواند در همان اسمبلی مورد استفاده قرار بگیرد.
موارد دیگری که تحت قوانین CTS هستند مفاهیم ارث بری، متدهای مجازی، عمر اشیاء و .. است.
یکی دیگر از ویژگیهای CTS این است که همهی نوعها از نوع شیء Object که در فضای نام system قرار دارد ارث بری کردهاند. به همین دلیل همهی نوعها حداقل قابلیتهایی را که یک نوع object ارئه میدهد، دارند که به شرح زیر هستند:
- مقایسهی دو شیء از لحاظ برابری.
- به دست آوردن هش کد برای هر نمونه از یک شیء
- ارائهای از وضعیت شیء به صورت رشته ای
- دریافت نوع شیء جاری
وجود COMها به دلیل ایجاد اشیاء در یک زبان متفاوت بود تا با زبان دیگر ارتباط برقرار کنند. در طرف دیگر CLR هم بین زبانهای برنامه نویسی یکپارچگی ایجاد کرده است. یکپارچگی زبانهای برنامه نویسی علل زیادی دارند. اول اینکه رسیدن به هدف یا یک الگوریتم خاص در زبان دیگر راحتتر از زبان پایه پروژه است. دوم در یک کار تیمی که افراد مختلف با دانش متفاوتی حضور دارند و ممکن است زیان هر یک متفاوت باشند.
برای ایجاد این یکپارچگی، مایکروسافت سیستم CLS یا Common Language Specification را راه اندازی کرد. این سیستم برای تولیدکنندگان کامپایلرها جزئیاتی را تعریف میکند که کامپایلر آنها را باید با حداقل ویژگیهای تعریف شدهی CLR، پشتیبانی کند.
CLR/CTS مجموعهای از ویژگیها را شامل میشود و گفتیم که هر زبانی بسیاری از این ویژگیها را پشتیبانی میکند ولی نه کامل. به عنوان مثال برنامه نویسی که قصد کرده از IL Assembly استفاده کند، قادر است از تمامی این ویژگیهایی که CLR/CTS ارائه میدهند، استفاده کند ولی تعدادی دیگر از زبانها مثل سی شارپ و فورترن و ویژوال بیسیک تنها بخشی از آن را استفاده میکنند و CLS حداقل ویژگی که بین همه این زبانها مشترک است را ارائه میکند.
شکل زیر را نگاه کنید:
یعنی
اگر شما دارید نوع جدیدی را در یک زبان ایجاد میکنید که قصد دارید در یک
زبان دیگر استفاده شود، نباید از امتیازات ویژهای که آن زبان در اختیار شما میگذارد و به بیان بهتر CLS آنها را پشتیبانی نمیکند، استفاده کنید؛ چرا
که کد شما ممکن است در زبان دیگر مورد استفاده قرار نگیرد.
به کد زیر دقت کنید. تعدادی از کدها سازگاری کامل با CLS دارند که به آنها CLS Compliant گویند و تعدادی از آنها non-CLS-Compliant هستند یعنی با
CLS سازگاری ندارند ولی استفاده از خاصیت [(assembly: CLSCompliant(true] باعث میشود که تا کامپایلر از پشتیبانی و سازگاری این کدها اطمینان کسب کند و در صورت وجود، از اجرای آن جلوگیری کند. با کمپایل کد زیر دو اخطار به ما میرسد.
using System; // Tell compiler to check for CLS compliance [assembly: CLSCompliant(true)] namespace SomeLibrary { // Warnings appear because the class is public public sealed class SomeLibraryType { // Warning: Return type of 'SomeLibrary.SomeLibraryType.Abc()' // is not CLScompliant public UInt32 Abc() { return 0; } // Warning: Identifier 'SomeLibrary.SomeLibraryType.abc()' // differing only in case is not CLScompliant public void abc() { } // No warning: this method is private private UInt32 ABC() { return 0; } } }
اولین اخطار اینکه
یکی از متدها یک عدد صحیح بدون علامت unsigned integer را بر میگرداند که همهی زبانها آن را پشتیبانی نمیکنند و خاص بعضی از زبان هاست.
دومین اخطار اینکه دو متد یکسان وجود دارند که در حروف بزرگ و کوچک تفاوت دارند. ولی زبان هایی چون ویژوال بیسیک نمیتوانند تفاوتی بین دو متد abc و ABC بیابند.
نکتهی جالب اینکه اگر شما کلمه public را از جلوی نام کلاس بردارید تمامی این اخطارها لغو میشود. به این خاطر که اینها اشیای داخلی آن اسمبلی شناخته شده و قرار نیست از بیرون به آن دسترسی صورت بگیرد. عضو خصوصی کد بالا را ببینید؛ کامنت بالای آن میگوید که چون خصوصی است هشداری نمیگیرد، چون قرار نیست در زبان مقصد از آن به طور مستقیم استفاده کند.
برای دیدن قوانین CLS به این صفحه مراجعه فرمایید.
دومین اخطار اینکه دو متد یکسان وجود دارند که در حروف بزرگ و کوچک تفاوت دارند. ولی زبان هایی چون ویژوال بیسیک نمیتوانند تفاوتی بین دو متد abc و ABC بیابند.
نکتهی جالب اینکه اگر شما کلمه public را از جلوی نام کلاس بردارید تمامی این اخطارها لغو میشود. به این خاطر که اینها اشیای داخلی آن اسمبلی شناخته شده و قرار نیست از بیرون به آن دسترسی صورت بگیرد. عضو خصوصی کد بالا را ببینید؛ کامنت بالای آن میگوید که چون خصوصی است هشداری نمیگیرد، چون قرار نیست در زبان مقصد از آن به طور مستقیم استفاده کند.
برای دیدن قوانین CLS به این صفحه مراجعه فرمایید.
سازگاری با کدهای مدیریت نشده
در بالا در مورد یکپارچگی و سازگاری کدهای مدیریت شده توسط CLS صحبت کردیم ولی در مورد ارتباط با کدهای مدیریت نشده چطور؟
مایکروسافت موقعیکه CLR را ارئه کرد، متوجه این قضیه بود که بسیاری از شرکتها توانایی اینکه کدهای خودشون را مجددا طراحی و پیاده سازی کنند، ندارند و خوب، سورسهای مدیریت نشدهی زیادی هم موجود هست که توسعه دهندگان علاقه زیادی به استفاده از آنها دارند. در نتیجه مایکروسافت طرحی را ریخت که CLR هر دو قسمت کدهای مدیریت شده و نشده را پشتیبانی کند. دو نمونه از این پشتیبانی را در زیر بیان میکنیم:
یک. کدهای مدیریت شده میتوانند توابع مدیریت شده را در قالب یک dll صدا زده و از آنها استفاده کنند.
دو. کدهای مدیریت شده میتوانند از کامپوننتهای COM استفاده کنند: بسیاری از شرکتها از قبل بسیاری از کامپوننتهای COM را ایجاد کرده بودند که کدهای مدیریت شده با راحتی با آنها ارتباط برقرار میکنند. ولی اگر دوست دارید روی آنها کنترل بیشتری داشته باشید و آن کدها را به معادل CLR تبدیل کنید؛ میتوانید از ابزار کمکی که مایکروسافت همراه فریم ورک دات نت ارائه کرده است استفاده کنید. نام این ابزار TLBIMP.exe میباشد که از Type Library Importer گرفته شده است.
سه. اگر کدهای مدیریت نشدهی زیادتری دارید شاید راحتتر باشد که برعکس کار کنید و کدهای مدیریت شده را در در یک برنامهی مدیریت نشده اجرا کنید. این کدها میتوانند برای مثال به یک Activex یا shell Extension تبدیل شده و مورد استفاده قرار گیرند. ابزارهای TLBEXP .exe و RegAsm .exe برای این منظور به همراه فریم ورک دات نت عرضه شده اند.
سورس کد Type Library Importer را میتوانید در کدپلکس بیابید.
در ویندوز 8 به بعد مایکروسافت API جدید را تحت عنوان WinsowsRuntime یا winRT ارائه کرده است . این api یک سیستم داخلی را از طریق کامپوننتهای com ایجاد کرده و به جای استفاده از فایلهای کتابخانهای، کامپوننتها api هایشان را از طریق متادیتاهایی بر اساس استاندارد ECMA که توسط تیم دات نت طراحی شده است معرفی میکنند.
زیبایی این روش اینست که کد نوشته شده در زبانهای دات نت میتواند به طور مداوم با apiهای winrt ارتباط برقرار کند. یعنی همهی کارها توسط CLR انجام میگیرد بدون اینکه لازم باشد از ابزار اضافی استفاده کنید. در آینده در مورد winRT بیشتر صحبت میکنیم.
در بالا در مورد یکپارچگی و سازگاری کدهای مدیریت شده توسط CLS صحبت کردیم ولی در مورد ارتباط با کدهای مدیریت نشده چطور؟
مایکروسافت موقعیکه CLR را ارئه کرد، متوجه این قضیه بود که بسیاری از شرکتها توانایی اینکه کدهای خودشون را مجددا طراحی و پیاده سازی کنند، ندارند و خوب، سورسهای مدیریت نشدهی زیادی هم موجود هست که توسعه دهندگان علاقه زیادی به استفاده از آنها دارند. در نتیجه مایکروسافت طرحی را ریخت که CLR هر دو قسمت کدهای مدیریت شده و نشده را پشتیبانی کند. دو نمونه از این پشتیبانی را در زیر بیان میکنیم:
یک. کدهای مدیریت شده میتوانند توابع مدیریت شده را در قالب یک dll صدا زده و از آنها استفاده کنند.
دو. کدهای مدیریت شده میتوانند از کامپوننتهای COM استفاده کنند: بسیاری از شرکتها از قبل بسیاری از کامپوننتهای COM را ایجاد کرده بودند که کدهای مدیریت شده با راحتی با آنها ارتباط برقرار میکنند. ولی اگر دوست دارید روی آنها کنترل بیشتری داشته باشید و آن کدها را به معادل CLR تبدیل کنید؛ میتوانید از ابزار کمکی که مایکروسافت همراه فریم ورک دات نت ارائه کرده است استفاده کنید. نام این ابزار TLBIMP.exe میباشد که از Type Library Importer گرفته شده است.
سه. اگر کدهای مدیریت نشدهی زیادتری دارید شاید راحتتر باشد که برعکس کار کنید و کدهای مدیریت شده را در در یک برنامهی مدیریت نشده اجرا کنید. این کدها میتوانند برای مثال به یک Activex یا shell Extension تبدیل شده و مورد استفاده قرار گیرند. ابزارهای TLBEXP .exe و RegAsm .exe برای این منظور به همراه فریم ورک دات نت عرضه شده اند.
سورس کد Type Library Importer را میتوانید در کدپلکس بیابید.
در ویندوز 8 به بعد مایکروسافت API جدید را تحت عنوان WinsowsRuntime یا winRT ارائه کرده است . این api یک سیستم داخلی را از طریق کامپوننتهای com ایجاد کرده و به جای استفاده از فایلهای کتابخانهای، کامپوننتها api هایشان را از طریق متادیتاهایی بر اساس استاندارد ECMA که توسط تیم دات نت طراحی شده است معرفی میکنند.
زیبایی این روش اینست که کد نوشته شده در زبانهای دات نت میتواند به طور مداوم با apiهای winrt ارتباط برقرار کند. یعنی همهی کارها توسط CLR انجام میگیرد بدون اینکه لازم باشد از ابزار اضافی استفاده کنید. در آینده در مورد winRT بیشتر صحبت میکنیم.
سخن پایانی: ممنون از دوستان عزیز بابت پیگیری مطالب تا بدینجا. تا این قسمت فصل اول کتاب با عنوان اصول اولیه CLR بخش اول مدل اجرای CLR به پایان رسید.
ادامهی مطالب بعد از تکمیل هر بخش در دسترس دوستان قرار خواهد گرفت.
مطالب
ASP.NET MVC #11
بررسی نکات تکمیلی Model binder در ASP.NET MVC
یک برنامه خالی جدید ASP.NET MVC را شروع کنید و سپس مدل زیر را به پوشه Models آن اضافه نمائید:
using System;
namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}
از این مدل چند مقصود ذیل دنبال میشوند:
استفاده از Id به عنوان primary key برای edit و update رکوردها. استفاده از DateTime برای اینکه اگر کاربری اطلاعات بی ربطی را وارد کرد چگونه باید این مشکل را در حالت model binding خودکار تشخیص داد و استفاده از IsAdmin برای یادآوری یک نکته امنیتی بسیار مهم که اگر حین model binding خودکار به آن توجه نشود، سایت را با مشکلات حاد امنیتی مواجه خواهد کرد. سیستم پیشرفته است. میتواند به صورت خودکار ورودیهای کاربر را تبدیل به یک شیء حاضر و آماده کند ... اما باید حین استفاده از این قابلیت دلپذیر به یک سری نکات امنیتی هم دقت داشت تا سایت ما به نحو دلپذیری هک نشود!
در ادامه یک کنترلر جدید به نام UserController را به پوشه کنترلرهای پروژه اضافه نمائید. همچنین نام کنترلر پیش فرض تعریف شده در قسمت مسیریابی فایل Global.asax.cs را هم به User تغییر دهید تا در هربار اجرای برنامه در VS.NET، نیازی به تایپ آدرسهای مرتبط با UserController نداشته باشیم.
یک منبع داده تشکیل شده در حافظه را هم برای نمایش لیستی از کاربران، به نحو زیر به پروژه اضافه خواهیم کرد:
using System;
using System.Collections.Generic;
namespace MvcApplication7.Models
{
public class Users
{
public IList<User> CreateInMemoryDataSource()
{
return new[]
{
new User { Id = 1, Name = "User1", Password = "123", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 2, Name = "User2", Password = "456", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 3, Name = "User3", Password = "789", IsAdmin = true, AddDate = DateTime.Now }
};
}
}
}
در اینجا فعلا هدف آشنایی با زیر ساختهای ASP.NET MVC است و درک صحیح نحوه کارکرد آن. مهم نیست از EF استفاده میکنید یا NH یا حتی ADO.NET کلاسیک و یا از Micro ORMهایی که پس از ارائه دات نت 4 مرسوم شدهاند. تهیه یک ToList یا Insert و Update با این فریم ورکها خارج از بحث جاری هستند.
سورس کامل کنترلر User به شرح زیر است:
using System;
using System.Linq;
using System.Web.Mvc;
using MvcApplication7.Models;
namespace MvcApplication7.Controllers
{
public class UserController : Controller
{
[HttpGet]
public ActionResult Index()
{
var usersList = new Users().CreateInMemoryDataSource();
return View(usersList); // Shows the Index view.
}
[HttpGet]
public ActionResult Details(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Details view.
}
[HttpGet]
public ActionResult Create()
{
var user = new User { AddDate = DateTime.Now };
return View(user); // Shows the Create view.
}
[HttpPost]
public ActionResult Create(User user)
{
if (this.ModelState.IsValid)
{
// todo: Add record
return RedirectToAction("Index");
}
return View(user); // Shows the Create view again.
}
[HttpGet]
public ActionResult Edit(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Edit view.
}
[HttpPost]
public ActionResult Edit(User user)
{
if (this.ModelState.IsValid)
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}
[HttpPost]
public ActionResult Delete(int id)
{
// todo: Delete record
return RedirectToAction("Index");
}
}
}
توضیحات:
ایجاد خودکار فرمهای ورود اطلاعات
در قسمت قبل برای توضیح دادن نحوه ایجاد فرمها در ASP.NET MVC و همچنین نحوه نگاشت اطلاعات آنها به اکشن متدهای کنترلرها، فرمهای مورد نظر را دستی ایجاد کردیم.
اما باید درنظر داشت که برای ایجاد Viewها میتوان از ابزار توکار خود VS.NET نیز استفاده کرد و سپس اطلاعات و فرمهای تولیدی را سفارشی نمود. این سریعترین راه ممکن است زمانیکه مدل مورد استفاده کاملا مشخص است و میخواهیم Strongly typed views را ایجاد کنیم.
برای نمونه بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. در اینجا گزینهی create a strongly typed view را انتخاب کرده و سپس از لیست مدلها، User را انتخاب نمائید. Scaffold template را هم بر روی حالت List قرار دهید.
برای متد Details هم به همین نحو عمل نمائید.
برای ایجاد View متناظر با متد Create در حالت HttpGet، تمام مراحل یکی است. فقط Scaffold template انتخابی را بر روی Create قرار دهید تا فرم ورود اطلاعات، به صورت خودکار تولید شود.
متد Create در حالت HttpPost نیازی به View اضافی ندارد. چون صرفا قرار است اطلاعاتی را از سرور دریافت و ثبت کند.
برای ایجاد View متناظر با متد Edit در حالت HttpGet، باز هم مراحل مانند قبل است با این تفاوت که Scaffold template انتخابی را بر روی گزینه Edit قرار دهید تا فرم ویرایش اطلاعات کاربر به صورت خودکار به پروژه اضافه شود.
متد Edit در حالت HttpPost نیازی به View اضافی ندارد و کارش تنها دریافت اطلاعات از سرور و به روز رسانی بانک اطلاعاتی است.
به همین ترتیب متد Delete نیز، نیازی به View خاصی ندارد. در اینجا بر اساس primary key دریافتی، میتوان یک کاربر را یافته و حذف کرد.
سفارشی سازی Viewهای خودکار تولیدی
با کمک امکانات Scaffolding نامبرده شده، حجم قابل توجهی کد را در اندک زمانی میتوان تولید کرد. بدیهی است حتما نیاز به سفارشی سازی کدهای تولیدی وجود خواهد داشت. مثلا شاید نیازی نباشد فیلد پسود کاربر، در حین نمایش لیست کاربران، نمایش داده شود. میشود کلا این ستون را حذف کرد و از این نوع مسایل.
یک مورد دیگر را هم در Viewهای تولیدی حتما نیاز است که ویرایش کنیم. آن هم مرتبط است به لینک حذف اطلاعات یک کاربر در صفحه Index.cshtml:
@Html.ActionLink("Delete", "Delete", new { id=item.Id }
در قسمت قبل هم عنوان شد که اعمال حذف باید بر اساس HttpPost محدود شوند تا بتوان میزان امنیت برنامه را بهبود داد. متد Delete هم در کنترلر فوق تنها به حالت HttpPost محدود شده است. بنابراین ActionLink پیش فرض را حذف کرده و بجای آن فرم و دکمه زیر را قرار میدهیم تا اطلاعات به سرور Post شوند:
@using (Html.BeginForm(actionName: "Delete", controllerName: "User", routeValues: new { id = item.Id }))
{
<input type="submit" value="Delete"
onclick="return confirm ('Do you want to delete this record?');" />
}
در اینجا نحوه ایجاد یک فرم، که id رکورد متناظر را به سرور ارسال میکند، مشاهده میکنید.
علت وجود دو متد، به ازای هر Edit یا Create
به ازای هر کدام از متدهای Edit و Create دو متد HttpGet و HttpPost را ایجاد کردهایم. کار متدهای HttpGet نمایش Viewهای متناظر به کاربر هستند. بنابراین وجود آنها ضروری است. در این حالت چون از دو Verb متفاوت استفاده شده، میتوان متدهای هم نامی را بدون مشکل استفاده کرد. به هر کدام از افعال Get و Post و امثال آن، یک Http Verb گفته میشود.
بررسی معتبر بودن اطلاعات دریافتی
کلاس پایه Controller که کنترلرهای برنامه از آن مشتق میشوند، شامل یک سری خواص و متدهای توکار نیز هست. برای مثال توسط خاصیت this.ModelState.IsValid میتوان بررسی کرد که آیا Model دریافتی معتبر است یا خیر. برای بررسی این مورد، یک breakpoint را بر روی سطر this.ModelState.IsValid در متد Create قرار دهید. سپس به صفحه ایجاد کاربر جدید مراجعه کرده و مثلا بجای تاریخ روز، abcd را وارد کنید. سپس فرم را به سرور ارسال نمائید. در این حالت مقدار خاصیت this.ModelState.IsValid مساوی false میباشد که حتما باید به آن پیش از ثبت اطلاعات دقت داشت.
شبیه سازی عملکرد ViewState در ASP.NET MVC
در متدهای Create و Edit در حالت Post، اگر اطلاعات Model معتبر نباشند، مجددا شیء User دریافتی، به View بازگشت داده میشود. چرا؟
صفحات وب، زمانیکه به سرور ارسال میشوند، تمام اطلاعات کنترلهای خود را از دست خواهد داد (صفحه پاک میشود، چون مجددا یک صفحه خالی از سرور دریافت خواهد شد). برای رفع این مشکل در ASP.NET Web forms، از مفهومی به نام ViewState کمک میگیرند. کار ViewState ذخیره موقت اطلاعات فرم جاری است برای استفاده مجدد پس از Postback. به این معنا که پس از ارسال فرم به سرور، اگر کاربری در textbox اول مقدار abc را وارد کرده بود، پس از نمایش مجدد فرم، مقدار abc را در همان textbox مشاهده خواهد کرد (شبیه سازی برنامههای دسکتاپ در محیط وب). بدیهی است وجود ViewState برای ذخیره سازی این نوع اطلاعات، حجم صفحه را بالا میبرد (بسته به پیچیدگی صفحه ممکن است به چند صد کیلوبایت هم برسد).
در ASP.NET MVC بجای استفاده از ترفندی به نام ViewState، مجددا اطلاعات همان مدل متناظر با View را بازگشت میدهند. در این حالت پس از ارسال صفحه به سرور و نمایش مجدد صفحه ورود اطلاعات، تمام کنترلها با همان مقادیر قبلی وارد شده توسط کاربر قابل مشاهده خواهند بود (مدل مشخص است، View ما هم از نوع strongly typed میباشد. در این حالت فریم ورک میداند که اطلاعات را چگونه به کنترلهای قرار گرفته در صفحه نگاشت کند).
در مثال فوق، اگر اطلاعات وارد شده صحیح باشند، کاربر به صفحه Index هدایت خواهد شد. در غیراینصورت مجددا همان View جاری با همان اطلاعات model قبلی که کاربر تکمیل کرده است به او برای تصحیح، نمایش داده میشود. این مساله هم جهت بالا بردن سهولت کاربری برنامه بسیار مهم است. تصور کنید که یک فرم خالی با پیغام «تاریخ وارد شده معتبر نیست» مجدا به کاربر نمایش داده شود و از او درخواست کنیم که تمام اطلاعات دیگر را نیز از صفر وارد کند چون اطلاعات صفحه پس از ارسال به سرور پاک شدهاند؛ که ... اصلا قابل قبول نیست و فوقالعاده برنامه را غیرحرفهای نمایش میدهد.
خطاهای نمایش داده شده به کاربر
به صورت پیش فرض خطایی که به کاربر نمایش داده میشود، استثنایی است که توسط فریم ورک صادر شده است. برای مثال نتوانسته است abcd را به یک تاریخ معتبر تبدیل کند. میتوان توسط this.ModelState.AddModelError خطایی را نیز در اینجا اضافه کرد و پیغام بهتری را به کاربر نمایش داد. یا توسط یک سری data annotations هم کار اعتبار سنجی را سفارشی کرد که بحث آن به صورت جداگانه در یک قسمت مستقل بررسی خواهد شد.
ولی به صورت خلاصه اگر به فرمهای تولید شده توسط VS.NET دقت کنید، در ابتدای هر فرم داریم:
@Html.ValidationSummary(true)
در اینجا خطاهای عمومی در سطح مدل نمایش داده میشوند. برای اضافه کردن این نوع خطاها، در متد AddModelError، مقدار key را خالی وارد کنید:
ModelState.AddModelError(string.Empty, "There is something wrong with model.");
همچنین در این فرمها داریم:
@Html.EditorFor(model => model.AddDate)
@Html.ValidationMessageFor(model => model.AddDate)
EditorFor سعی میکند اندکی هوش به خرج دهد. یعنی اگر خاصیت دریافتی مثلا از نوع bool بود، خودش یک checkbox را در صفحه نمایش میدهد. همچنین بر اساس متادیتا یک خاصیت نیز میتواند تصمیم گیری را انجام دهد. این متادیتا منظور attributes و data annotations ایی است که به خواص یک مدل اعمال میشود. مثلا اگر ویژگی HiddenInput را به یک خاصیت اعمال کنیم، به شکل یک فیلد مخفی در صفحه ظاهر خواهد شد.
یا متد Html.DisplayFor، اطلاعات را به صورت فقط خواندنی نمایش میدهد. اصطلاحا به این نوع متدها، Templated Helpers هم گفته میشود. بحث بیشتر دربارهای این موارد به قسمتی مجزا و مستقل موکول میگردد. برای نمونه کل فرم ادیت برنامه را حذف کنید و بجای آن بنویسید Html.EditorForModel و سپس برنامه را اجرا کنید. یک فرم کامل خودکار ویرایش اطلاعات را مشاهده خواهید کرد (و البته نکات سفارشی سازی آن به یک قسمت کامل نیاز دارند).
در اینجا متد ValidationMessageFor کار نمایش خطاهای اعتبارسنجی مرتبط با یک خاصیت مشخص را انجام میدهد. بنابراین اگر قصد ارائه خطایی سفارشی و مخصوص یک فیلد مشخص را داشتید، در متد AddModelError، مقدار پارامتر اول یا همان key را مساوی نام خاصیت مورد نظر قرار دهید.
مقابله با مشکل امنیتی Mass Assignment در حین کار با Model binders
استفاده از Model binders بسیار لذت بخش است. یک شیء را به عنوان پارامتر اکشن متد خود معرفی میکنیم. فریم ورک هم در ادامه سعی میکند تا اطلاعات فرم را به خواص این شیء نگاشت کند. بدیهی است این روش نسبت به روش ASP.NET Web forms که باید به ازای تک تک کنترلهای موجود در صفحه یکبار کار دریافت اطلاعات و مقدار دهی خواص یک شیء را انجام داد، بسیار سادهتر و سریعتر است.
اما اگر همین سیستم پیشرفته جدید ناآگاهانه مورد استفاده قرار گیرد میتواند منشاء حملات ناگواری شود که به نام «Mass Assignment» شهرت یافتهاند.
همان صفحه ویرایش اطلاعات را درنظر بگیرید. چک باکس IsAdmin قرار است در قسمت مدیریتی برنامه تنظیم شود. اگر کاربری نیاز داشته باشد اطلاعات خودش را ویرایش کند، مثلا پسوردش را تغییر دهد، با یک صفحه ساده کلمه عبور قبلی را وارد کنید و دوبار کلمه عبور جدید را نیز وارد نمائید، مواجه خواهد شد. خوب ... اگر همین کاربر صفحه را جعل کند و فیلد چک باکس IsAdmin را به صفحه اضافه کند چه اتفاقی خواهد افتاد؟ بله ... مشکل هم همینجا است. در اینصورت کاربر عادی میتواند دسترسی خودش را تا سطح ادمین بالا ببرد، چون model binder اطلاعات IsAdmin را از کاربر دریافت کرده و به صورت خودکار به model ارائه شده، نگاشت کرده است.
برای مقابله با این نوع حملات چندین روش وجود دارند:
الف) ایجاد لیست سفید
به کمک ویژگی Bind میتوان لیستی از خواص را جهت به روز رسانی به model binder معرفی کرد. مابقی ندید گرفته خواهند شد:
public ActionResult Edit([Bind(Include = "Name, Password")] User user)
در اینجا تنها خواص Name و Password توسط model binder به خواص شیء User نگاشت میشوند.
به علاوه همانطور که در قسمت قبل نیز ذکر شد، متد edit را به شکل زیر نیز میتوان بازنویسی کرد. در اینجا متدهای توکار UpdateModel و TryUpdateModel نیز لیست سفید خواص مورد نظر را میپذیرند (اعمال دستی model binding):
[HttpPost]
public ActionResult Edit()
{
var user = new User();
if(TryUpdateModel(user, includeProperties: new[] { "Name", "Password" }))
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}
ب) ایجاد لیست سیاه
به همین ترتیب میتوان تنها خواصی را معرفی کرد که باید صرفنظر شوند:
public ActionResult Edit([Bind(Exclude = "IsAdmin")] User user)
در اینجا از خاصیت IsAdmin صرف نظر گردیده و از مقدار ارسالی آن توسط کاربر استفاده نخواهد شد.
و یا میتوان پارامتر excludeProperties متد TryUpdateModel را نیز مقدار دهی کرد.
لازم به ذکر است که ویژگی Bind را به کل یک کلاس هم میتوان اعمال کرد. برای مثال:
using System;
using System.Web.Mvc;
namespace MvcApplication7.Models
{
[Bind(Exclude = "IsAdmin")]
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}
این مورد اثر سراسری داشته و قابل بازنویسی نیست. به عبارتی حتی اگر در متدی خاصیت IsAdmin را مجددا الحاق کنیم، تاثیری نخواهد داشت.
یا میتوان از ویژگی ReadOnly هم استفاده کرد:
using System;
using System.ComponentModel;
namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
[ReadOnly(true)]
public bool IsAdmin { set; get; }
}
}
در این حالت هم خاصیت IsAdmin هیچگاه توسط model binder به روز و مقدار دهی نخواهد شد.
ج) استفاده از ViewModels
این راه حلی است که بیشتر مورد توجه معماران نرم افزار است و البته کسانی که پیشتر با الگوی MVVM کار کرده باشند این نام برایشان آشنا است؛ اما در اینجا مفهوم متفاوتی دارد. در الگوی MVVM، کلاسهای ViewModel شبیه به کنترلرها در MVC هستند یا به عبارتی همانند رهبر یک اکستر عمل میکنند. اما در الگوی MVC خیر. در اینجا فقط مدل یک View هستند و نه بیشتر. هدف هم این است که بین Domain Model و View Model تفاوت قائل شد.
کار View model در الگوی MVC، شکل دادن به چندین domain model و همچنین اطلاعات اضافی دیگری که نیاز هستند، جهت استفاده نهایی توسط یک View میباشد. به این ترتیب View با یک شیء سر و کار خواهد داشت و همچنین منطق شکل دهی به اطلاعات مورد نیازش هم از داخل View حذف شده و به خواص View model در زمان تشکیل آن منتقل میشود.
مشخصات یک View model خوب به شرح زیر است:
الف) رابطه بین یک View و View model آن، رابطهای یک به یک است. به ازای هر View، بهتر است یک کلاس View model وجود داشته باشد.
ب) View ساختار View model را دیکته میکند و نه کنترلر.
ج) View modelها صرفا یک سری کلاس POCO (کلاسهایی تشکیل شده از خاصیت، خاصیت، خاصیت ....) هستند که هیچ منطقی در آنها قرار نمیگیرد.
د) View model باید حاوی تمام اطلاعاتی باشد که View جهت رندر نیاز دارد و نه بیشتر و الزامی هم ندارد که این اطلاعات مستقیما به domain models مرتبط شوند. برای مثال اگر قرار است firstName+LastName در View نمایش داده شود، کار این جمع زدن باید حین تهیه View Model انجام شود و نه داخل View. یا اگر قرار است اطلاعات عددی با سه رقم جدا کننده به کاربر نمایش داده شوند، وظیفه View Model است که یک خاصیت اضافی را برای تهیه این مورد تدارک ببیند. یا مثلا اگر یک فرم ثبت نام داریم و در این فرم لیستی وجود دارد که تنها Id عنصر انتخابی آن در Model اصلی مورد استفاده قرار میگیرد، تهیه اطلاعات این لیست هم کار ViewModel است و نه اینکه مدام به Model اصلی بخواهیم خاصیت اضافه کنیم.
ViewModel چگونه پیاده سازی میشود؟
اکثر مقالات را که مطالعه کنید، این روش را توصیه میکنند:
public class MyViewModel
{
public SomeDomainModel1 Model1 { get; set; }
public SomeDomainModel2 Model2 { get; set; }
...
}
یعنی اینکه View ما به اطلاعات مثلا دو Model نیاز دارد. اینها را به این شکل محصور و کپسوله میکنیم. اگر View، واقعا به تمام فیلدهای این کلاسها نیاز داشته باشد، این روش صحیح است. در غیر اینصورت، این روش نادرست است (و متاسفانه همه جا هم دقیقا به این شکل تبلیغ میشود).
ViewModel محصور کننده یک یا چند مدل نیست. در اینجا حس غلط کار کردن با یک ViewModel را داریم. ViewModel فقط باید ارائه کننده اطلاعاتی باشد که یک View نیاز دارد و نه بیشتر و نه تمام خواص تمام کلاسهای تعریف شده. به عبارتی این نوع تعریف صحیح است:
public class MyViewModel
{
public string SomeExtraField1 { get; set; }
public string SomeExtraField2 { get; set; }
public IEnumerable<SelectListItem> StateSelectList { get; set; }
// ...
public string PersonFullName { set; set; }
}
در اینجا، View متناظری، قرار است نام کامل یک شخص را به علاوه یک سری اطلاعات اضافی که در domain model نیست، نمایش دهد. مثلا نمایش نام استانها که نهایتا Id انتخابی آن قرار است در برنامه استفاده شود.
خلاصه علت وجودی ViewModel این موارد است:
الف) Model برنامه را مستقیما در معرض استفاده قرار ندهیم (عدم رعایت این نکته به مشکلات امنیتی حادی هم حین به روز رسانی اطلاعات ممکن است ختم شود که پیشتر توضیح داده شد).
ب) فیلدهای نمایشی اضافی مورد نیاز یک View را داخل Model برنامه تعریف نکنیم (مثلا تعاریف عناصر یک دراپ داون لیست، جایش اینجا نیست. مدل فقط نیاز به Id عنصر انتخابی آن دارد).
با این توضیحات، اگر View به روز رسانی اطلاعات کلمه عبور کاربر، تنها به اطلاعات id آن کاربر و کلمه عبور او نیاز دارد، فقط باید همین اطلاعات را در اختیار View قرار داد و نه بیشتر:
namespace MvcApplication7.Models
{
public class UserViewModel
{
public int Id { set; get; }
public string Password { set; get; }
}
}
به این ترتیب دیگر خاصیت IsAdming اضافهای وجود ندارد که بخواهد مورد حمله واقع شود.
استفاده از model binding برای آپلود فایل به سرور
برای آپلود فایل به سرور تنها کافی است یک اکشن متد به شکل زیر را تعریف کنیم. HttpPostedFileBase نیز یکی دیگر از model binderهای توکار ASP.NET MVC است:
[HttpGet]
public ActionResult Upload()
{
return View(); // Shows the upload page
}
[HttpPost]
public ActionResult Upload(System.Web.HttpPostedFileBase file)
{
string filename = Server.MapPath("~/files/somename.ext");
file.SaveAs(filename);
return RedirectToAction("Index");
}
View متناظر هم میتواند به شکل زیر باشد:
@{
ViewBag.Title = "Upload";
}
<h2>
Upload</h2>
@using (Html.BeginForm(actionName: "Upload", controllerName: "User",
method: FormMethod.Post,
htmlAttributes: new { enctype = "multipart/form-data" }))
{
<text>Upload a photo:</text> <input type="file" name="photo" />
<input type="submit" value="Upload" />
}
اگر دقت کرده باشید در طراحی ASP.NET MVC از anonymously typed objects زیاد استفاده میشود. در اینجا هم برای معرفی enctype فرم آپلود، مورد استفاده قرار گرفته است. به عبارتی هر جایی که مشخص نبوده چه تعداد ویژگی یا کلا چه ویژگیها و خاصیتهایی را میتوان تنظیم کرد، اجازه تعریف آنها را به صورت anonymously typed objects میسر کردهاند. یک نمونه دیگر آن در متد routes.MapRoute فایل Global.asax.cs است که پارامتر سوم دریافت مقدار پیش فرضها نیز anonymously typed object است. یا نمونه دیگر آنرا در همین قسمت در جایی که لینک delete را به فرم تبدیل کردیم مشاهده نمودید. مقدار routeValues هم یک anonymously typed object معرفی شد.
سفارشی سازی model binder پیش فرض ASP.NET MVC
در همین مثال فرض کنید تاریخ را به صورت شمسی از کاربر دریافت میکنیم. خاصیت تعریف شده هم DateTime میلادی است. به عبارتی model binder حین تبدیل رشته تاریخ شمسی دریافتی به تاریخ میلادی با شکست مواجه شده و نهایتا خاصیت this.ModelState.IsValid مقدارش false خواهد بود. برای حل این مشکل چکار باید کرد؟
برای این منظور باید نحوه پردازش یک نوع خاص را سفارشی کرد. ابتدا با پیاده سازی اینترفیس IModelBinder شروع میکنیم. توسط bindingContext.ValueProvider میتوان به مقداری که کاربر وارد کرده در میانه راه دسترسی یافت. آنرا تبدیل کرده و نمونه صحیح را بازگشت داد.
نمونهای از این پیاده سازی را در ادامه ملاحظه میکنید:
using System;
using System.Globalization;
using System.Web.Mvc;
namespace MvcApplication7.Binders
{
public class PersianDateModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
var parts = valueResult.AttemptedValue.Split('/'); //ex. 1391/1/19
if (parts.Length != 3) return null;
int year = int.Parse(parts[0]);
int month = int.Parse(parts[1]);
int day = int.Parse(parts[2]);
actualValue = new DateTime(year, month, day, new PersianCalendar());
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
}
سپس برای معرفی PersianDateModelBinder جدید تنها کافی است سطر زیر را
ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder());
به متد Application_Start قرار گرفته در فایل Global.asax.cs برنامه اضافه کرد. از این پس کاربران میتوانند تاریخها را در برنامه شمسی وارد کنند و model binder بدون مشکل خواهد توانست اطلاعات ورودی را به معادل DateTime میلادی آن تبدیل کند و استفاده نماید.
تعریف مدل بایندر سفارشی در فایل Global.asax.cs آنرا به صورت سراسری در تمام مدلها و اکشنمتدها فعال خواهد کرد. اگر نیاز بود تنها یک اکشن متد خاص از این مدل بایندر سفارشی استفاده کند میتوان به روش زیر عمل کرد:
public ActionResult Create([ModelBinder(typeof(PersianDateModelBinder))] User user)
همچنین ویژگی ModelBinder را به یک کلاس هم میتوان اعمال کرد:
[ModelBinder(typeof(PersianDateModelBinder))]
public class User
{
مطالب دورهها
بایدها و نبایدهای استفاده از IoC Containers
طوری با IoC Containers کار کنید که انگار وجود خارجی ندارند
تفاوت پایهای که بین یک فریم ورک IoC و سایر فریم ورکها وجود دارد، در معکوس شدن مسئولیتها است. در اینجا لایههای مختلف برنامه شما نیستند که فریم ورک IoC را فراخوانی میکنند؛ بلکه این فریم ورک IoC است که از جزئیات ارتباطات و وابستگیهای سیستم شما آگاه است و نهایتا کار کنترل وهله سازی اشیاء مختلف را عهده دار خواهد شد. طول عمر آنها را تنظیم کرده یا حتی در بعضی از موارد مانند برنامه نویسی جنبهگرا یا AOP، نسبت به تزئین این اشیاء یا دخالت در مراحل مختلف فراخوانی متدهای آنها نیز نقش خواهد داشت. نکتهی مهم در اینجا، نا آگاهی برنامه از حضور آنها است.
بنابراین در پروژه شما اگر ماژولها و لایههای مختلفی حضور دارند، تنها برنامه اصلی است که باید ارجاعی را به فریم ورک IoC داشته باشد و نه سایر لایههای سیستم. علت حضور آن در ریشه سیستم نیز تنها باید به اصطلاحا bootstrapping و اعمال تنظیمات مرتبط با آن خلاصه شود.
به عبارتی استفاده صحیح از یک فریم ورک IoC نباید به شکل الگوی Service Locator باشد؛ حالتی که در تمام قسمتهای برنامه مدام مشاهده میکنید resolver.Resolve، resolver.Resolve و الی آخر. باید از این نوع استفاده از فریم ورکهای IoC تا حد ممکن حذر شود و کدهای برنامه نباید وابستگی مستقیم ثانویهای را به نام خود فریم ورک IoC پیدا کنند.
نمونهای از نحوه صحیح استفاده از یک IoC Container را مشاهده میکنید. تنها در سه نقطه است که یک IoC container باید حضور پیدا کند:
الف) در آغاز برنامه برای اعمال تنظیمات اولیه و bootstrapping
ب) پیش از اجرای عملی جهت وهله سازی وابستگیهای مورد نیاز
ج) پس از اجرای عمل مورد نظر جهت آزاد سازی منابع
نکته مهم اینجا است که در حین اجرای فرآیند، این فرآیند باید تا حد ممکن از حضور IoC container بیخبر باشد و کار تشکیل اشیاء باید خارج از منطق تجاری برنامه انجام شود: IoC container خود را صدا نزنید؛ او شما را صدا خواهد زد.
عنوان شد تا «حد ممکن». این تا حد ممکن به چه معنایی است؟ اگر کار وهله سازی اشیاء را میتوانید تحت کنترل قرار دهید، مثلا آیا میتوانید در نحوه وهله سازی کنترلرها در ASP.NET MVC دخل و تصرف کرده و در زمان وهله سازی، اینکار را به یک IoC Container واگذار کنید؟ اگر بلی، دیگر به هیچ عنوانی نباید داخل کلاسهای فراخوانی شده و تزریق شده به کنترلرهای برنامه اثری از IoC Container شما مشاهده شود. زیرا این فریم ورکها اینقدر توانمند هستند که بتوانند تا چندین لایه از سیستم را واکاوی کرده و وابستگیهای لازم را وهله سازی کنند.
اگر خیر (نمیتوانید کار وهله سازی اشیاء را مستقیما تحت کنترل قرار دهید)؛ مانند تهیه یک Role Provider سفارشی در ASP.NET MVC که کار وهله سازی این Role Provider راسا توسط موتور ASP.NET انجام میشود و در این بین امکان دخل و تصرفی هم در آن ممکن نیست، آنگاه مجاز است داخل این کلاس ویژه از متدهای container.Resolve استفاده کرد؛ چون چارهی دیگری وجود ندارد و IoC Container نیست که کار وهله سازی ابتدایی آنرا عهده دار شده است. باید دقت داشت به این حالت خاص دیگر تزریق وابستگیها گفته نمیشود؛ بلکه نام الگوی آن Service locator است. در Service locator یک کامپوننت خودش به دنبال وابستگیهای مورد نیازش میگردد. در حالت تزریق وابستگیها، یک کامپوننت وابستگیهای مورد نیاز را درخواست میکند.
یک مثال:
کاری که در اینجا انجام شده است نمونه اشتباهی از استفاده از یک IoC Container میباشد. به صرف اینکه مشغول به استفاده از یک IoC Container هستیم به این معنا نیست که واقعا الگوی معکوس سازی وابستگیها را درست درک کردهایم. در اینجا الگوی Service locator مورد استفاده است و نه الگوی تزریق وابستگیها. به عبارتی در مثال فوق، کلاس ExampleClass وابسته است به یک وابستگی جدیدی به نام Container، علاوه بر وابستگی IService ایی که به او قرار است خدماتی را ارائه دهد.
نمونه اصلاح شده کلاس فوق، تزریق وابستگیها در سازنده کلاس به نحو زیر است:
در اینجا این کلاس است که وابستگیهای خود را درخواست میکند و نه اینکه خودش به دنبال آنها بگردد.
نمونه دیگری از کلاسی که خودش به دنبال یافتن و وهله سازی وابستگیهای مورد نیازش است مثال زیر میباشد:
به این کار poor man's dependency injection هم گفته میشود؛ اولین سازنده از طریق یک default constructor سعی کرده است وابستگیهای کلاس را، خودش تامین کند. باز هم کلاس میداند که به چه وابستگی خاصی نیاز دارد و عملا معکوس سازی وابستگیها رخ نداده است. همچنین استفاده از این حالت زمانیکه کلاس Dinner خودش وابستگی به کلاسهای دیگر داشته باشد، بسیار به هم ریخته و مشکل خواهد بود. مزیت استفاده از IoC Containers وهله سازی یک large object graph کامل است. به علاوه توسط IoC Containers مدیریت طول عمر اشیاء را نیز میتوان تحت نظر قرار داد. برای مثال میتوان به یک IoC Container گفت تنها یک وهله از DbContext را در طول یک درخواست ایجاد و آنرا در اختیار لایههای مختلف برنامه قرار بده؛ چون نیاز داریم کاری که در طی یک درخواست انجام میشود، در داخل یک تراکنش انجام شده و همچنین بیجهت به ازای هر new DbConetxt جدید، یکبار اتصالی به بانک اطلاعاتی باز و بسته نشود (سرعت بیشتر، سربار کمتر).
تفاوت پایهای که بین یک فریم ورک IoC و سایر فریم ورکها وجود دارد، در معکوس شدن مسئولیتها است. در اینجا لایههای مختلف برنامه شما نیستند که فریم ورک IoC را فراخوانی میکنند؛ بلکه این فریم ورک IoC است که از جزئیات ارتباطات و وابستگیهای سیستم شما آگاه است و نهایتا کار کنترل وهله سازی اشیاء مختلف را عهده دار خواهد شد. طول عمر آنها را تنظیم کرده یا حتی در بعضی از موارد مانند برنامه نویسی جنبهگرا یا AOP، نسبت به تزئین این اشیاء یا دخالت در مراحل مختلف فراخوانی متدهای آنها نیز نقش خواهد داشت. نکتهی مهم در اینجا، نا آگاهی برنامه از حضور آنها است.
بنابراین در پروژه شما اگر ماژولها و لایههای مختلفی حضور دارند، تنها برنامه اصلی است که باید ارجاعی را به فریم ورک IoC داشته باشد و نه سایر لایههای سیستم. علت حضور آن در ریشه سیستم نیز تنها باید به اصطلاحا bootstrapping و اعمال تنظیمات مرتبط با آن خلاصه شود.
به عبارتی استفاده صحیح از یک فریم ورک IoC نباید به شکل الگوی Service Locator باشد؛ حالتی که در تمام قسمتهای برنامه مدام مشاهده میکنید resolver.Resolve، resolver.Resolve و الی آخر. باید از این نوع استفاده از فریم ورکهای IoC تا حد ممکن حذر شود و کدهای برنامه نباید وابستگی مستقیم ثانویهای را به نام خود فریم ورک IoC پیدا کنند.
var container = BootstrapContainer(); var finder = container.Resolve<IDuplicateFinder>(); var processor = container.Resolve<IArgumentsParser>(); Execute( args, processor, finder ); container.Dispose();
الف) در آغاز برنامه برای اعمال تنظیمات اولیه و bootstrapping
ب) پیش از اجرای عملی جهت وهله سازی وابستگیهای مورد نیاز
ج) پس از اجرای عمل مورد نظر جهت آزاد سازی منابع
نکته مهم اینجا است که در حین اجرای فرآیند، این فرآیند باید تا حد ممکن از حضور IoC container بیخبر باشد و کار تشکیل اشیاء باید خارج از منطق تجاری برنامه انجام شود: IoC container خود را صدا نزنید؛ او شما را صدا خواهد زد.
عنوان شد تا «حد ممکن». این تا حد ممکن به چه معنایی است؟ اگر کار وهله سازی اشیاء را میتوانید تحت کنترل قرار دهید، مثلا آیا میتوانید در نحوه وهله سازی کنترلرها در ASP.NET MVC دخل و تصرف کرده و در زمان وهله سازی، اینکار را به یک IoC Container واگذار کنید؟ اگر بلی، دیگر به هیچ عنوانی نباید داخل کلاسهای فراخوانی شده و تزریق شده به کنترلرهای برنامه اثری از IoC Container شما مشاهده شود. زیرا این فریم ورکها اینقدر توانمند هستند که بتوانند تا چندین لایه از سیستم را واکاوی کرده و وابستگیهای لازم را وهله سازی کنند.
اگر خیر (نمیتوانید کار وهله سازی اشیاء را مستقیما تحت کنترل قرار دهید)؛ مانند تهیه یک Role Provider سفارشی در ASP.NET MVC که کار وهله سازی این Role Provider راسا توسط موتور ASP.NET انجام میشود و در این بین امکان دخل و تصرفی هم در آن ممکن نیست، آنگاه مجاز است داخل این کلاس ویژه از متدهای container.Resolve استفاده کرد؛ چون چارهی دیگری وجود ندارد و IoC Container نیست که کار وهله سازی ابتدایی آنرا عهده دار شده است. باید دقت داشت به این حالت خاص دیگر تزریق وابستگیها گفته نمیشود؛ بلکه نام الگوی آن Service locator است. در Service locator یک کامپوننت خودش به دنبال وابستگیهای مورد نیازش میگردد. در حالت تزریق وابستگیها، یک کامپوننت وابستگیهای مورد نیاز را درخواست میکند.
یک مثال:
public class ExampleClass { private readonly IService _service; public ExampleClass() { _service = Container.Resolve<IService>(); } public void DoSomething(int id) { _service.DoSomething(id); } }
نمونه اصلاح شده کلاس فوق، تزریق وابستگیها در سازنده کلاس به نحو زیر است:
public class ExampleClass { private IService _service; public ExampleClass(IService service) { _service = service; } public void DoSomething(int id) { _service.DoSomething(id); } }
نمونه دیگری از کلاسی که خودش به دنبال یافتن و وهله سازی وابستگیهای مورد نیازش است مثال زیر میباشد:
public class Search { IDinner _dinner; public Search(): this(new Dinner()) { } public Search(IDinner dinner) { _dinner = dinner; } }