.NET 7 minimal API from scratch | FULL COURSE | clean architecture, repository pattern, CQRS MediatR
In this course I want to provide you a project structure and code organization to get you started with real .NET 7 minimal API projects. It's a full course on this topic where I start from creating and explaining the project structure, setting up different layers using EF Core, repository pattern, CQRS and MediatR. The biggest part of the video is however around the .NET 7 minimal API, taking you from the initial setup, explaining route handlers, implementing all CRUD operations and so on. Last but not least, this course walks you through the process of refactoring the .NET 7 minimal API so that it becomes readable, maintainable and scalable. At the end, you'll have a full project structure organized according to modern architectural patterns that you can take as a template for your own projects.
Contents
1. Intro: 00:00
2. Structuring the solution: 01:00
3. Coding the domain layer: 05:25
4. Coding the data access layer: 08:22
5. Creating repositories: 11:17
6. Adding migrations and database update: 22:30
7. CQRS with MediatR: 29:07
8. Route and rout handlers: 52:06
9. Dependency injection: 55:52
10. Implementing GET by ID : 57:40
11. Implementing POST route: 01:00:26
12. Implementing GET all route: 01:03:41
13. Implement PUT and DELETE: 01:04:57
14. Testing with Postman: 01:09:01
15. Is there a problem? 01:12:41
16. Refactoring service registrations: 01:15:49
17. Refactoring route registrations: 01:20:01
18. Automatic registration of endpoints: 01:26:28
19. Introducing route groups: 01:31:43
20. Extract lambdas into instance methods: 01:34:31
21: Model validation with endpoint filters: 01:45:58
22. Global exception handling: 01:55:10
23. Conclusions: 01:59:49
یکی از قابلیتهای جالب NHibernate امکان تعریف فیلدها به صورت پویا هستند. به این معنا که زیرساخت طراحی یک برنامه "فرم ساز" هم اکنون در اختیار شما است! سیستمی که امکان افزودن فیلدهای سفارشی را دارا است که توسط برنامه نویس در زمان طراحی اولیه آن ایجاد نشدهاند. در ادامه نحوهی تعریف و استفاده از این قابلیت را توسط Fluent NHibernate بررسی خواهیم کرد.
در اینجا کلاسی که قرار است توانایی افزودن فیلدهای سفارشی را داشته باشد به صورت زیر تعریف میشود:
using System.Collections;
namespace TestModel
{
public class DynamicEntity
{
public virtual int Id { get; set; }
public virtual IDictionary Attributes { set; get; }
}
}
Attributes در عمل همان فیلدهای سفارشی مورد نظر خواهند بود. جهت معرفی صحیح این قابلیت نیاز است تا نگاشت آنرا از نوع dynamic component تعریف کنیم:
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;
namespace TestModel
{
public class DynamicEntityMapping : IAutoMappingOverride<DynamicEntity>
{
public void Override(AutoMapping<DynamicEntity> mapping)
{
mapping.Table("tblDynamicEntity");
mapping.Id(x => x.Id);
mapping.IgnoreProperty(x => x.Attributes);
mapping.DynamicComponent(x => x.Attributes,
c =>
{
c.Map(x => (string)x["field1"]);
c.Map(x => (string)x["field2"]).Length(300);
c.Map(x => (int)x["field3"]);
c.Map(x => (double)x["field4"]);
});
}
}
}
ابتدا از IgnoreProperty جهت ندید گرفتن Attributes استفاده کردیم. زیرا درغیراینصورت در حالت Auto mapping ، یک رابطه چند به یک به علت وجود IDictionary به صورت خودکار ایجاد خواهد شد که نیازی به آن نیست (یافتن این نکته نصف روز کار برد ....! چون مرتبا خطای An association from the table DynamicEntity refers to an unmapped class: System.Collections.Idictionary ظاهر میشد و مشخص نبود که مشکل از کجاست).
سپس Attributes به عنوان یک DynamicComponent معرفی شده است. در اینجا چهار فیلد سفارشی را اضافه کردهایم. به این معنا که اگر نیاز باشد تا فیلد سفارشی دیگری به سیستم اضافه شود باید یکبار Session factory ساخته شود و SchemaUpdate فراخوانی گردد و خوشبختانه با وجود Fluent NHibernate ، تمام این تغییرات در کدهای برنامه قابل انجام است (بدون نیاز به سر و کار داشتن با فایلهای XML نگاشتها و ویرایش دستی آنها). از تغییر نام جدول که برای مثال در اینجا tblDynamicEntity در نظر گرفته شده تا افزودن فیلدهای دیگر در قسمت DynamicComponent فوق.
همچنین باتوجه به اینکه این نوع تغییرات (ساخت دوبار سشن فکتوری) مواردی نیستند که قرار باشد هر ساعت انجام شوند، بنابراین سربار آنچنانی را به سیستم تحمیل نمیکنند.
drop table tblDynamicEntity
create table tblDynamicEntity (
Id INT IDENTITY NOT NULL,
field1 NVARCHAR(255) null,
field2 NVARCHAR(300) null,
field3 INT null,
field4 FLOAT null,
primary key (Id)
)
اگر با SchemaExport، اسکریپت خروجی معادل با نگاشت فوق را تهیه کنیم به جدول فوق خواهیم رسید. نوع و طول این فیلدهای سفارشی بر اساس نوعی که برای اشیاء دیکشنری مشخص میکنید، تعیین خواهند شد.
چند مثال جهت کار با این فیلدهای سفارشی یا پویا :
نحوهی افزودن رکوردهای جدید بر اساس خاصیتهای سفارشی:
//insert
object savedId = 0;
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var obj = new DynamicEntity();
obj.Attributes = new Hashtable();
obj.Attributes["field1"] = "test1";
obj.Attributes["field2"] = "test2";
obj.Attributes["field3"] = 1;
obj.Attributes["field4"] = 1.1;
savedId = session.Save(obj);
tx.Commit();
}
}
با خروجی
INSERT
INTO
tblDynamicEntity
(field1, field2, field3, field4)
VALUES
(@p0, @p1, @p2, @p3);
@p0 = 'test1' [Type: String (0)], @p1 = 'test2' [Type: String (0)], @p2 = 1
[Type: Int32 (0)], @p3 = 1.1 [Type: Double (0)]
نحوهی کوئری گرفتن از این اطلاعات (فعلا پایدارترین روشی را که برای آن یافتهام استفاده از HQL میباشد ...):
//query
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
//using HQL
var list = session
.CreateQuery("from DynamicEntity d where d.Attributes.field1=:p0")
.SetString("p0", "test1")
.List<DynamicEntity>();
if (list != null && list.Any())
{
Console.WriteLine(list[0].Attributes["field2"]);
}
tx.Commit();
}
}
select
dynamicent0_.Id as Id1_,
dynamicent0_.field1 as field2_1_,
dynamicent0_.field2 as field3_1_,
dynamicent0_.field3 as field4_1_,
dynamicent0_.field4 as field5_1_
from
tblDynamicEntity dynamicent0_
where
dynamicent0_.field1=@p0;
@p0 = 'test1' [Type: String (0)]
استفاده از HQL هم یک مزیت مهم دارد: چون به صورت رشته قابل تعریف است، به سادگی میتوان آنرا داخل دیتابیس ذخیره کرد. برای مثال یک سیستم گزارش ساز پویا هم در این کنار طراحی کرد ....
نحوهی به روز رسانی و حذف اطلاعات بر اساس فیلدهای پویا:
//update
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var entity = session.Get<DynamicEntity>(savedId);
if (entity != null)
{
entity.Attributes["field2"] = "new-val";
tx.Commit(); // Persist modification
}
}
}
//delete
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var entity = session.Get<DynamicEntity>(savedId);
if (entity != null)
{
session.Delete(entity);
tx.Commit();
}
}
}
کتابخانه emergence.js
Emergence.js is a lightweight, high-performance JS plugin for detecting and manipulating elements in the browser. Demo
- Dependancy-free
- IE8+ and all modern browsers
- 1KB minified and gzipped
npm install emergence.js
bower install emergence.js
در قسمت اول با مفاهیم اولیه Class و Object آشنا شدیم.
Messages and Methods
Objectها باید مانند ماشینهایی تلقی شوند که عملیات موجود در واسط عمومی خود را برای افرادی که درخواست مناسبی ارسال کنند، اجرا خواهند کرد. با توجه به اینکه یک object از استفاده کننده خود مستقل است و وابستگی به او ندارد و همچنین توجه به ساختار نحوی (syntax) برخی از زبانهای شیء گرای جدید، عبارت «sending a message» برای توصیف اجرای رفتاری از مجموعه رفتارهای object، استفاده میشود.
به محض اینکه پیغامی (Message) به سمت object ارسال شود، ابتدا باید تصمیم بگیرد که این پیغام ارسالی را درک میکند. فرض کنیم این پیغام قابل درک است. در این صورت object مورد نظر، همزمان با نگاشت پیغام به یک فراخوانی تابع (function call)، خود را به صورت ضمنی به عنوان اولین آرگومان ارسال میکند. تصمیم گرفتن در رابطه با قابل درک بودن یک پیغام، در زبانهای مفسری در زمان اجرا و در زبانهای کامپایلری در زمان کامپایل، انجام میگرد.
نام (یا prototype) رفتار یک وهله، Message (پیغام) نامیده میشود. بسیاری از زبانهای شیء گرا مفهموم Overloaded Functions Or Operators را پشتیبانی میکنند. در این صورت میتوان در سیستم دو تابعی داشت که با نام یکسان، یا انواع مختلف آرگومان (intraclass overloading) داشته باشند یا در کلاسهای مختلفی (interclass overloading) قرار گیرند.
ممکن است کلاس ساعت زنگدار، دو پیغام set_time که یکی از آنها با دو آرگومان از نوع عدد صحیح و دیگری یک آرگومان رشتهای است داشته باشد.
void AlarmClock::set_time(int hours, int minutes); void AlarmClock::set_time(String time);
در مقابل، کلاس ساعت زنگدار و کلاس ساعت مچی هر دو messageای به نام set_time با دو آرگومان از نوع عدد صحیح دارند.
void AlarmClock::set_time(int hours, int minutes); void Watch::set_time(int hours, int minutes);
باید توجه کنید که یک پیغام، شامل نام تابع، انواع آرگومان، نوع بازگشتی و کلاسی که پیغام به آن متصل است، میباشد. این اطلاعاتی که مطرح شد، بخش اصلی چیزی است که کاربر یک کلاس نیاز دارد در مورد آنها آگاهی داشته باشد.
در برخی از زبانها و یا سیستمها، اطلاعات دیگری مانند: انواع استثناءهایی که از سمت پیغام پرتاب میشوند تا اطلاعات همزمانی (پیغام به صورت همزمان است یا ناهمزمان) را برای استفاده کننده مهیا کنند. از طرفی پیاده سازی کنندگان یک کلاس باید از پیاده سازی پیغام آگاه باشند. جزئیات پیاده سازی یک پیغام -کدی که پیغام را پیاده سازی میکند- Method (متد) نامیده میشود. آنگاه که نخ (thread) کنترل درون متد باشد، برای مشخص کردن اینکه پیغام رسیده برای کدام وهله ارسال شدهاست، ارجاعی به وهله مورد نظر و به عنوان اولین آرگومان، به صورت ضمنی ارسال میشود. این آرگومان ضمنی، در بیشتر زبانها Self Object نامیده میشود (در سی پلاس پلاس this object نام دارد). در نهایت، مجموعه پیغامهایی که یک وهله میتواند به آنها پاسخ دهد، Protocol (قرارداد) نام دارد.
دو پیغام خاصی که کلاسها یا وهلهها میتوانند به آنها پاسخ دهند، اولی که استفاده کنندگان کلاس برای ساخت وهلهها از آن استفاده میکنند، Constructor (سازنده) نام دارد. هر کلاسی میتواند سازندههای متعددی داشته باشد که هر کدام مجموعه پارامترهای مختلفی را برای مقدار دهی اولیه میپذیرند. دومین پیغام، عملیاتی است که وهله را قبل از حذف از سیستم، پاک سازی میکند. این عملیات، Destructor (تخریب کننده) نام دارد. بیشتر زبانهای شیء گرا، برای هر کلاس تنها یک تخریب کننده دارند. این پیغامها را به عنوان مکانیزم مقدار دهی اولیه و پاک سازی در پارادایم شیء گرا در نظر بگیرید.
قاعده شهودی 2.2
استفاده کنندگان از کلاس باید به واسط عمومی آن وابسته باشند، اما یک کلاس نباید به استفاده کنندگان خود، وابسته باشد. (Users of a class must be dependent on its public interface, but a class should not be dependent on its users)
منطق پشت این قاعده، یکی از شکلهای قابلیت استفاده مجدد (resuability) میباشد. یک ساعت زنگدار ممکن است توسط شخصی در اتاق خواب او استفاده شود. واضح است که شخص مورد نظر به واسط عمومی ساعت زنگدار وابسته میباشد. به هر حال، ساعت زنگدار نباید به شخصی وابسته باشد. اگر ساعت زنگدار به شخصی وابسته باشد، بدون مهیا کردن یک شخص، نمیتوان از آن برای ساخت یک TimeLockSafe استفاده کرد. این وابستگیها برای مواقعیکه میخواهیم امکان این را داشته باشیم تا کلاس ساعت زنگدار را از دامین (domain) خود خارج کرده و در دامین دیگری، بدون وابستگی هایش مورد استفاده قرار دهیم، نامطلوب هستند.
شکل 2.4 The Use Of Alarm Clocks
قاعده شهودی 2.3
تعداد پیغامهای موجود در قرارداد یک کلاس را کمینه سازید. (Minimize the number of messages in the protocol of a class)
چندین سال قبل، مطلبی منتشر شد که کاملا متضاد این قاعده شهودی میباشد. طبق آن، پیاده سازی کننده یک کلاس میتواند یکسری عملیات را با فرض اینکه در آینده مورد استفاده قرار گیرند، برای آن در نظر بگیرد. ایراد این کار چیست؟ اگر شما از این قاعده پیروی کنید، حتما کلاس LinkedList من، توجه شما را جلب خواهد کرد؛ این کلاس در واسط عمومی خود 4000 عملیات را دارد. فرض کنید قصد ادغام دو وهله از این کلاس را داشته باشید. در این صورت حتما فرض شما این است که عملیاتی تحت عنوان merge در این کلاس تعبیه شده است. بعد از جستجوی بین این تعداد عملیات، در نهایت این عملیات خاص را پیدا نخواهید کرد. چراکه این عملیات متأسفانه به صورت یک overloaded plus operator پیاده سازی شده است. مشکل اصلی واسط عمومی با تعداد زیادی عملیات این است که فرآیند یافتن عملیات مورد نظرمان را خیلی سخت یا حتی ناممکن خواهد کرد و مشکلی جدی برای قابلیت استفاده مجدد تلقی میشود.
با کمینه نگه داشتن تعداد عملیات واسط عمومی، سیستم، قابل فهمتر و همچنین مولفههای (components) آن به راحتی قابل استفاده مجدد خواهند بود.
قاعده شهودی 2.4
پیاده سازی یک واسط عمومی یکسان کمینه برای همه کلاسها (Implement a minimal public interface that all classes understand [e.g., operations such as copy (deep versus shallow), equality testing, pretty printing, parsing from an ASCII description, etc.].)
مهیا کردن یک واسط عمومی مشترک کمینه برای کلاسهایی که توسط یک توسعه دهنده پیاده سازی شده و قرار است توسط توسعه دهندگان دیگر مورد استفاده قرار گیرد، خیلی مفید خواهد بود. این واسط عمومی، حداقل عاملیتی را که به طور منطقی از هر کلاس میشود انتظار داشت، مهیا خواهد ساخت. واسطی که میتواند از آن به عنوان مبنای یادگیری درباره رفتارهای کلاسها در پایه نرم افزاری با قابلیت استفاده مجدد، بهره برد.
به عنوان مثال کلاس Object در دات نت به عنوان کلاس پایه ضمنی با یکسری از متدهای عمومی (برای مثال ToString)، نشان دهنده تعریف یک واسط عمومی مشترک برای همه کلاسها در این فریمورک، میباشد.
public class Object { public Object(); public static bool Equals(Object objA, Object objB){...} public static bool ReferenceEquals(Object objA, Object objB){...} public virtual bool Equals(Object obj){...} public virtual int GetHashCode(){...} public Type GetType(){...} public virtual string ToString(){...} protected Object MemberwiseClone(){...} }
قاعده شهودی 2.5
جزئیات پیاده سازی، مانند توابع خصوصی common-code ( توابعی که کد مشترک سایر متدهای کلاس را در بدنه خود دارند) را در واسط عمومی یک کلاس قرار ندهید. (Do not put implementation details such as common-code private functions into the public interface of a class)
این قاعده برای کاهش پیچیدگی واسط عمومی کلاس برای استفاده کنندگان آن، طراحی شده است. ایده اصلی این است که استفاده کنندگان کلاس تمایلی ندارند به اعضایی دسترسی داشته باشند که از آنها استفاده نخواهند کرد؛ این اعضا باید به صورت خصوصی در کلاس قرار داده شوند. این توابع خصوصی common-code، زمانیکه متدهای یک کلاس، کد مشترکی را داشته باشند، ایجاد خواهند شد. قرار دادن این کد مشترک در یک متد، معمولا روش مناسبی میباشد. نکته قابل توجه این است که این متد، عملیات جدیدی نمیباشد؛ بله جزئیات پیاده سازی دو عملیات دیگر از کلاس را ساده کرده است.
شکل 2.5 Example of a common-code private function
مثال واقعی
فرض کنید در شکل بالا، کلاس X معادل یک LinkedList کلاس، f1و f2 به عنوان توابع insert و remove و تابع f به عنوان تابع common-code که عملیات یافتن آدرس را برای درج و حذف انجام میدهد، میباشند.
قاعده شهودی 2.6
واسط عمومی کلاس را با اقلامی که یا استفاده کنندگان از کلاس توانایی استفاده از آن را نداشته و یا تمایلی به استفاده از آنها ندارند، آمیخته نکنید. (Do not clutter the public interface of a class with items that users of that class are not able to use or are not interested in using )
این قاعده شهودی با قاعده قبلی که با قرار دادن تابع common-code در واسط عمومی کلاس، فقط باعث در هم ریختن واسط عمومی شده بود، مرتبط میباشد. در برخی از زبانها مانند C++، برای مثال این امکان وجود دارد که سازنده یک کلاس انتزاعی (abstract) را در واسط عمومی آن قرار دهید؛ حتی با وجود اینکه در زمان استفاده از آن سازنده با خطای نحوی روبرو خواهید شد. این قاعده شهودی کلی، برای کاهش این مشکلات در نظر گرفته شده است.
مشاهده یا دانلود کدهای مقاله
تزریق وابستگی چیست؟
تزریق وابستگی (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
آزمون واحد کلاس نگاشت تهیه شده
در مورد آشنایی با آزمونهای واحد لطفا به برچسب مربوطه در سمت راست سایت مراجعه بفرمائید. همچنین در مورد اینکه چرا به این نوع API کلمه Fluent اطلاق میشود، میتوان به تعریف آن جهت مطالعه بیشتر مراجعه نمود.
در این قسمت قصد داریم برای بررسی وضعیت کلاس نگاشت تهیه شده یک آزمون واحد تهیه کنیم. برای این منظور ارجاعی را به اسمبلی nunit.framework.dll به پروژه UnitTests که در ابتدای کار به solution جاری در VS.Net افزوده بودیم، اضافه نمائید (همچنین ارجاعهایی به اسمبلیهای پروژه NHSample1 ، FluentNHibernate ، System.Data.SQLite ، NHibernate.ByteCode.Castle و Nhibernate نیز نیاز هستند). تمام اسمبلیهای این فریم ورکها از پروژه FluentNHibernate قابل استخراج هستند.
سپس سه کلاس زیر را به پروژه آزمون واحد اضافه خواهیم کرد.
کلاس TestModel : (جهت مشخص سازی محل دریافت اطلاعات نگاشت)
using FluentNHibernate;
using NHSample1.Domain;
namespace UnitTests
{
public class TestModel : PersistenceModel
{
public TestModel()
{
AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
}
}
}
کلاس FixtureBase : (جهت ایجاد سشن NHibernate در ابتدای آزمون واحد و سپس پاکسازی اشیاء در پایان کار)
using NUnit.Framework;
using NHibernate;
using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
namespace UnitTests
{
public class FixtureBase
{
protected SessionSource SessionSource { get; set; }
protected ISession Session { get; private set; }
[SetUp]
public void SetupContext()
{
var cfg = Fluently.Configure().Database(SQLiteConfiguration.Standard.InMemory);
SessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
new TestModel());
Session = SessionSource.CreateSession();
SessionSource.BuildSchema(Session);
}
[TearDown]
public void TearDownContext()
{
Session.Close();
Session.Dispose();
}
}
}
و کلاس CustomerMapping_Fixture.cs : (جهت بررسی صحت نگاشت تهیه شده با کمک دو کلاس قبل)
using NUnit.Framework;
using FluentNHibernate.Testing;
using NHSample1.Domain;
namespace UnitTests
{
[TestFixture]
public class CustomerMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_customer()
{
new PersistenceSpecification<Customer>(Session)
.CheckProperty(c => c.Id, 1001)
.CheckProperty(c => c.FirstName, "Vahid")
.CheckProperty(c => c.LastName, "Nasiri")
.CheckProperty(c => c.AddressLine1, "Addr1")
.CheckProperty(c => c.AddressLine2, "Addr2")
.CheckProperty(c => c.PostalCode, "1234")
.CheckProperty(c => c.City, "Tehran")
.CheckProperty(c => c.CountryCode, "IR")
.VerifyTheMappings();
}
}
}
توضیحات:
اکنون به عنوان یک برنامه نویس متعهد نیاز است تا کار صورت گرفته در قسمت قبل را آزمایش کنیم.
کار بررسی صحت نگاشت تعریف شده در قسمت قبل توسط کلاس استاندارد PersistenceSpecification فریم ورک FluentNHibernate انجام خواهد شد (در کلاس CustomerMapping_Fixture). این کلاس برای انجام عملیات آزمون واحد نیاز به کلاس پایه دیگری به نام FixtureBase دارد که در آن کار ایجاد سشن NHibernate (در قسمت استاندارد SetUp آزمون واحد) و سپس آزاد سازی آن را در هنگام خاتمه کار ، انجام میدهد (در قسمت TearDown آزمون واحد).
این ویژگیها که در مباحث آزمون واحد نیز به آنها اشاره شده است، سبب اجرای متدهایی پیش از اجرا و بررسی هر آزمون واحد و سپس آزاد سازی خودکار منابع خواهند شد.
برای ایجاد یک سشن NHibernate نیاز است تا نوع دیتابیس و همچنین رشته اتصالی به آن (کانکشن استرینگ) مشخص شوند. فریم ورک Fluent NHibernate با ایجاد کلاسهای کمکی برای این امر، به شدت سبب ساده سازی انجام آن شده است. در این مثال، نوع دیتابیس به SQLite و در حالت دیتابیس در حافظه (in memory)، تنظیم شده است (برای انجام امور آزمون واحد با سرعت بالا).
جهت اجرای هر دستوری در NHibernate نیاز به یک سشن میباشد. برای تعریف شیء سشن، نه تنها نیاز به مشخص سازی نوع و حالت دیتابیس مورد استفاده داریم، بلکه نیاز است تا وهلهای از کلاس استاندارد PersistanceModel را نیز جهت مشخص سازی کلاس نگاشت مورد استفاده مشخص نمائیم. برای این منظور کلاس TestModel فوق تعریف شده است تا این نگاشت را از اسمبلی مربوطه بخواند و مورد استفاده قرار دهد (بر پایی اولیه این مراحل شاید در ابتدای امر کمی زمانبر باشد اما در نهایت یک پروسه استاندارد است). توسط این کلاس به سیستم اعلام خواهیم کرد که اطلاعات نگاشت را باید از کدام کلاس دریافت کند.
تا اینجای کار شیء SessionSource را با معرفی نوع دیتابیس و همچنین محل دریافت اطلاعات نگاشت اشیاء معرفی کردیم. در دو سطر بعدی متد SetupContext کلاس FixtureBase ، ابتدا یک سشن را از این منبع سشن تهیه میکنیم. شیء منبع سشن در این فریم ورک در حقیقت یک factory object است (الگوهای طراحی برنامه نویسی شیءگرا) که امکان دسترسی به انواع و اقسام دیتابیسها را فراهم میسازد. برای مثال اگر روزی نیاز بود از دیتابیس اس کیوال سرور استفاده شود، میتوان از کلاس MsSqlConfiguration بجای SQLiteConfiguration استفاده کرد و همینطور الی آخر.
در ادامه توسط شیء SessionSource کار ساخت database schema را نیز به صورت پویا انجام خواهیم داد. بله، همانطور که متوجه شدهاید، کار ساخت database schema نیز به صورت پویا توسط فریم ورک NHibernate با توجه به اطلاعات کلاسهای نگاشت، صورت خواهد گرفت.
این مراحل، نحوه ایجاد و بر پایی یک آزمایشگاه آزمون واحد فریم ورک Fluent NHibernate را مشخص ساخته و در پروژههای شما میتوانند به کرات مورد استفاده قرار گیرند.
در ادامه اگر آزمون واحد را اجرا نمائیم (متد can_correctly_map_customer در کلاس CustomerMapping_Fixture)، نتیجه باید شبیه به شکل زیر باشد:
توسط متد CheckProperty کلاس PersistenceSpecification ، امکان بررسی نگاشت تهیه شده میسر است. اولین پارامتر آن، یک lambda expression خاصیت مورد نظر جهت بررسی است و دومین آرگومان آن، مقداری است که در حین آزمون به خاصیت تعریف شده انتساب داده میشود.
نکته:
شاید سؤال بپرسید که در تابع can_correctly_map_customer عملا چه اتفاقاتی رخ داده است؟ برای بررسی آن در متد SetupContext کلاس FixtureBase ، اولین سطر آنرا به صورت زیر تغییر دهید تا عبارات SQL نهایی تولید شده را نیز بتوانیم در حین عملیات تست مشاهده نمائیم:
var cfg = Fluently.Configure().Database(SQLiteConfiguration.Standard.ShowSql().InMemory);
مطابق متد تست فوق، عبارات تولید شده به شرح زیر هستند:
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Customer" (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 1001
NHibernate: SELECT customer0_.Id as Id0_0_, customer0_.FirstName as FirstName0_0_, customer0_.LastName as LastName0_0_, customer0_.AddressLine1 as AddressL4_0_0_, customer0_.AddressLine2 as AddressL5_0_0_, customer0_.PostalCode as PostalCode0_0_, customer0_.City as City0_0_, customer0_.CountryCode as CountryC8_0_0_ FROM "Customer" customer0_ WHERE customer0_.Id=@p0;@p0 = 1001
نکته جالب این عبارات، استفاده از کوئریهای پارامتری است به صورت پیش فرض که در نهایت سبب بالا رفتن امنیت بیشتر برنامه (یکی از راههای جلوگیری از تزریق اس کیوال در ADO.Net که در نهایت توسط تمامی این فریم ورکها در پشت صحنه مورد استفاده قرار خواهند گرفت) و همچنین سبب بکار افتادن سیستمهای کش دیتابیسهای پیشرفته مانند اس کیوال سرور میشوند (execution plan کوئریهای پارامتری در اس کیوال سرور جهت بالا رفتن کارآیی سیستم کش میشوند و اهمیتی هم ندارد که حتما رویه ذخیره شده باشند یا خیر).
ادامه دارد ...
var postCounts = await context.Database.SqlQuery<BlogPostsCount>(@$"SELECT * FROM View_BlogPostCounts").ToListAsync();
var postCounts = await context.Database .SqlQuery<BlogPostsCount>(@$"SELECT * FROM View_BlogPostCounts") .Where(x => x.PostCount > 1) .ToListAsync();
- مدلی که در اینجا تعریف میشود، باید ساده بوده و چندسطحی و یا به همراه روابطی نباشد.
- نگاشتها، بر اساس نام ستونهای بازگشت داده شده، انجام میشوند و حتی بکارگیری mapping attributes هم مجاز هستند.
- مدلها، بدون کلید اصلی هستند.
- متد SqlQuery، برای بار اول در EF 7x اضافه شد که توسط آن، تنها امکان داشتن خروجیهای scalar (یا غیر موجودیتی)، مانند اعداد و رشتهها وجود داشت (<SqlQuery<int).