در این قسمت قصد داریم از امکانات جدید اعتبار سنجی تعریف شده در فضای نام استاندارد System.ComponentModel.DataAnnotations استفاده نمائیم. از سیلورلایت سه به بعد امکان استفاده از این فضای نام به سادگی در برنامههای سیلورلایت میسر است (همچنین در برنامههای ASP.Net MVC)؛ اما برای کار با آن در WPF نیاز به تعدادی متد کمکی میباشد...
فهرست مطالب:
فصل 5- تعیین اعتبار ورودی کاربر و الگوی MVVM
- مقدمه
- معرفی برنامه فصل
- مدل برنامهی فصل
- ViewModel برنامه فصل
- View برنامه فصل
دریافت قسمت پنجم
دریافت مثال قسمت پنجم
تعدادی از منابع و مآخذ مورد استفاده در این سری:
2. Model View ViewModel
3. DataModel-View-ViewModel pattern
4. 5 Minute Overview of MVVM in Silverlight
5. A Field Guide to WPF Presentation Patterns
6. An attempt at simple MVVM with WPF
7. WPF: If Heineken did MVVM Frameworks Part 1 of n
8. Modal dialogs with MVVM and Silverlight 4
9. How do I do… With the Model-View-ViewModel pattern
10. Intro to WPF MVVM
11. Introduction to MVVM pattern in WPF
12. Learning WPF M-V-VM
13. Binding Combo Boxes in WPF with MVVM
14. Model-View-ViewModel Pattern
15. Unit Testable WCF Web Services in MVVM and Silverlight 4
16. MVVM Part 1: Overview
17. Which came first, the View or the Model?
18. Stackoverflow's questions tagged with MVVM
19. WPF: MVVM (Model View View-Model) Simplified
20. WPF and MVVM tutorial 01, Introduction
21. WPF patterns : MVC, MVP or MVVM or…?
22. Silverlight, MVVM and Validation Part III
23. DotNetKicks.com - Stories recently tagged with 'MVVM'
24. DotNetShoutout - Stories tagged with MVVM
25. MVVM Light Toolkit
26. MVVM screen casts
27. What’s new in MVVM Light V3
28. Using RelayCommands in Silverlight 3 and WPF
29. WPF Apps With The Model-View-ViewModel Design Pattern
30. WPF MVVM and Showing Dialogs
آشنایی با الگوی طراحی Decorator
// ساخت کیک معمولی با روکش کاکائویی Cake c = new CommonCake(); c = new Chocolate(c); // ساخت کیک معمولی با روکش میوهای Cake c = new CommonCake(); c = new Fruity(c); // ساخت کیک معمولی مخلوط با روکش کاکائویی و روکش میوهای به صورت همزمان Cake c = new CommonCake(); c = new Chocolate(c); c = new Fruity(c); // ساخت کیک مخصوص مخلوط با روکش کاکائویی و روکش میوهای به صورت همزمان Cake c = new SpecialCake(); c = new Chocolate(c); c = new Fruity(c);
برای هر c میتوان متدهای اینترفیسش را اجرا کرد.
معرفی List Patterns Matching در C# 11
در C# 11 میتوان یک رشتهی ثابت را به صورت <ReadOnlySpan<char تعریف کرد و سپس بر روی آن عملیات patterns matching را انجام داد:
ReadOnlySpan<char> strSpan = "Vahid"; if (strSpan is "Vahid") { Console.WriteLine("Hey, Vahid"); }
در اینجا قابلیتهای List Patterns Matching ذکر شدهی در بحث جاری هم قابل استفاده هستند. برای مثال میتوان بررسی کرد که آیا رشتهی ثابت تعریف شده، با حرف خاصی شروع میشود (صرفنظر از مابقی حروف آن) و یا خیر:
if (strSpan is ['V', ..]) { Console.WriteLine("The name starts with V"); }
معرفی پروژه DNTFrameworkCore
پروژه DNTFrameworkCore که قصد پشتیبانی از آن را دارم، یک زیرساخت سبک وزن و توسعه پذیر با پشتیبانی از طراحی چند مستاجری با کمترین وابستگی به کتابخانههای ثالث میباشد که با تمرکز بر کاهش زمان و افزایش کیفیت توسعه بخش منطق تجاری پروژههای تحت وب، توسعه داده شده است. به مرور زمان مطالب و مستندات آن نیز کامل خواهد شد. برای برخی از امکانات از جمله اعتبارسنجی خودکار، مدیریت تراکنش ها، شماره گذاری خودکار و ... آزمون واحد نیز در نظر گرفته شده است که در آینده نزدیک با تکمیل آزمون واحد بخشهای دیگر، انتشار آنها نیز انجام خواهد شد.
برای نصب و استفاده از بستههای نیوگت آن، دستورات زیر را اجرا کنید:
PM>Install-Package DNTFrameworkCore PM>Install-Package DNTFrameworkCore.EntityFramework PM>Install-Package DNTFrameworkCore.Web PM>Install-Package DNTFrameworkCore.Web.EntityFramework
به منظور بررسی دقیقتر امکانات آن میتوانید پروژه TestAPI موجود در مخزن گیت هاب را بررسی کنید.
نمونه API پیاده سازی شده:
[Route("api/[controller]")] public class TasksController : CrudController<ITaskService, int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel> { public TasksController(ITaskService service) : base(service) { } protected override string CreatePermissionName => PermissionNames.Tasks_Create; protected override string EditPermissionName => PermissionNames.Tasks_Edit; protected override string ViewPermissionName => PermissionNames.Tasks_View; protected override string DeletePermissionName => PermissionNames.Tasks_Delete; }
PM> Install-Package DNTFrameworkCore -Version 1.0.0
مثال اول: یک موجودیت ساده بدون نیاز به مباحث ردیابی تغییرات
public class MeasurementUnit : Entity<int>, IAggregateRoot { public const int MaxTitleLength = 50; public const int MaxSymbolLength = 50; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Symbol { get; set; } public byte[] RowVersion { get; set; } }
کلاس جنریک Entity، در برگیرنده یکسری اعضای مشترک بین سایر موجودیتهای سیستم از جمله Id و TrackingState (به منظور سناریوهای Master-Detail)، میباشد.
نکته: در این زیرساخت برای پیاده سازی CrudService برای یک موجودیت خاص، نیاز است تا واسط IAggregateRoot را نیز پیاده سازی کرده باشد. برای پیاده سازی واسط مذکور نیاز است تا خصوصیت RowVersion را به منظور مدیریت Optimistic مباحث همزمانی، به کلاس بالا اضافه کنیم. این موضوع برای موجودیتهای وابسته به یک Aggregate ضروری نیست، چرا که آنها با AggregateRoot ذخیره خواهند شد و تراکنش جدایی برای ثبت، ویرایش و یا حذف آنها وجود ندارد.
مثال دوم: یک موجودیت به همراه مباحث ردیابی تغییرات ثبت و آخرین ویرایش
public class Blog : TrackableEntity<long>, IAggregateRoot { public const int MaxTitleLength = 50; public const int MaxUrlLength = 50; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Url { get; set; } public byte[] RowVersion { get; set; } }
کلاس جنریک TrackableEntity علاوه بر خصوصیات Id و TrackingState، یکسری خصوصیت دیگر از جمله زمان ثبت، زمان آخرین ویرایش، شناسه کاربر ثبت کننده، شناسه آخرین کاربر ویرایش کننده، اطلاعات مرورگرهای آنها و ... را نیز دارا میباشد. این خصوصیات به صورت خودکار توسط زیرساخت مقداردهی خواهند شد.
مثال سوم: یک موجودیت به همراه مباحث ردیابی تغییرات ثبت، آخرین ویرایش و حذف نرم
public class Blog : FullTrackableEntity<long>, IAggregateRoot { public const int MaxTitleLength = 50; public const int MaxUrlLength = 50; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Url { get; set; } public byte[] RowVersion { get; set; } }
کلاس جنریک FullTrackableEntity علاوه بر خصوصیات ذکر شده در مثال دوم، یکسری خصوصیت دیگر از جمله IsDeleted، شناسه کاربر حذف کننده، زمان حذف و ... را نیز دارا میباشد. همچنین مباحث فیلتر خودکار رکوردهای حذف شده، به صورت خودکار توسط زیرساخت انجام میگیرد که امکان غیرفعال کردن آن در شرایط مورد نیاز نیز وجود دارد.
مثال چهارم: یک موجودیت با پشتیبانی از چند مستاجری
public class Blog : Entity<long>, IAggregateRoot, ITenantEntity { public const int MaxTitleLength = 50; public const int MaxUrlLength = 50; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Url { get; set; } public byte[] RowVersion { get; set; } public long TenantId { get; set; } }
با پیاده سازی واسط ITenantEntity، به صورت خودکار خصوصیت TenantId آن با توجه به اطلاعات مستاجر جاری سیستم مقداردهی خواهد شد و همچنین فیلتر خودکار بر روی رکوردهای مستاجرهای مختلف، توسط زیرساخت انجام میشود که این مکانیزم هم قابلیت غیرفعال شدن در شرایط خاص را دارد.
مثال پنجم: یک موجودیت به همراه تعدادی موجودیت جزئی (سناریوهای Master-Detail)
public class Invoice : TrackableEntity<long>, IAggregateRoot { public InvoiceStatus Status { get; set; } public decimal TotalNet { get; set; } public decimal Total { get; set; } public decimal PayableTotal { get; set; } public decimal Debit { get; set; } public decimal Credit { get; set; } public decimal Gratuity { get; set; } public byte[] RowVersion { get; set; } public ICollection<InvoiceItem> Items { get; set; } } public class InvoiceItem : TrackableEntity { public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal Price { get; set; } public decimal UnitPriceDiscount { get; set; } public long ItemId { get; set; } public Item Item { get; set; } public long InvoiceId { get; set; } public Invoice Invoice { get; set; } }
همانطور که مشخص میباشد، موجودیت وابسته یا همان Detail، نیاز به پیاده سازی IAggregateRoot را نخواهد داشت. همانطور که اشاره شد، تراکنش مجزایی برای این موجودیتها نخواهیم داشت و درون تراکنش AggregateRoot، عملیات CRUD آنها انجام خواهد شد و برای انجام عملیات ویرایش، به همراه Root متناظر با خود، واکشی خواهند شد. این موضوع یکی از نقاط قوت زیرساخت محسوب میشود که در مقالات آینده و در قسمت طراحی سرویسهای متناظر با موجودیتهای سیستم، با جزئیات بیشتری بررسی خواهد شد.
مثال ششم: یک موجودیت با امکان شماره گذاری خودکار
public class Task : TrackableEntity, IAggregateRoot, INumberedEntity { public const int MaxTitleLength = 256; public const int MaxDescriptionLength = 1024; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Number { get; set; } public string Description { get; set; } public TaskState State { get; set; } = TaskState.Todo; public byte[] RowVersion { get; set; } }
همانطور که در مطلب «طراحی و پیاده سازی زیرساختی برای تولید خودکار کد منحصر به فرد در زمان ثبت رکورد جدید» ملاحظه کردید، نیاز است تا موجودیت مورد نظر، پیاده ساز واسط INumberedEntity نیز باشد. این واسط دارای خصوصیت رشتهای Number میباشد و همچنین زیرساخت به صورت خودکار در زمان ثبت، این خصوصیت را برای موجودیتهایی از این نوع، با رعایت مباحث همزمانی مقداردهی میکند.
مثال هفتم: یک موجودیت با امکان ذخیره سازی اطلاعات اضافی در قالب فیلد JSON
public class Task : TrackableEntity, IAggregateRoot, INumberedEntity, IExtendableEntity { public const int MaxTitleLength = 256; public const int MaxDescriptionLength = 1024; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Number { get; set; } public string Description { get; set; } public TaskState State { get; set; } = TaskState.Todo; public byte[] RowVersion { get; set; } public string ExtensionJson { get; set; } }
با پیاده سازی واسط IExtendableEntity، یکسری متد الحاقی برروی اشیاء موجودیت مورد نظر فعال خواهند شد که امکان مقداردهی یا خواندن این اطلاعات اضافی را خواهید داشت. به عنوان مثال:
var task = new Task(); task.SetExtensionValue("Name","Value"); var value = task.ReadExtensionValue("Name"); //or any complex object as string json
با دو متد الحاقی استفاده شده در بالا، امکان مقداردهی، تغییر و خواندن مقدار خصوصیتهای اضافی را خواهیم داشت که نیاز است موجودیت مورد نظر در دل خود نگهداری کند ولی ارزش و اهمیت زیادی در Domain ندارند.
مثال هشتم: طراحی یک نوع شمارشی (Enum)
public class OrderStatus : Enumeration { public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant()); public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant()); public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant()); public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant()); public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant()); public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant()); protected OrderStatus() { } public OrderStatus(int id, string name) : base(id, name) { } }
برای سناریوهایی که صرفا قصد انتخاب یک یا چند (حالت enum flags) مورد از بین یک لیست مشخص و سپس ذخیره سازی آنها را دارید، استفاده از نوع داده enum کفایت میکند؛ ولی اگر قصد استفاده از آنها برای flow control را دارید، در این صورت به طراحی شکنندهای خواهید رسید که پر شده است از if/else هایی که مقادیر مختلف enum مورد نظر را بررسی میکنند. با استفاده از کلاس Enumeration امکان مدل کردن انوع شمارشی که مرتبط هستند با منطق تجاری سیستم را با راه حل شیء گرا خواهید داشت. در این صورت رفتارهای متناظر با هریک از فیلدهای یک نوع شمارشی میتواند به عنوان رفتاری در دل خود کپسوله شده باشد و اینبار داده و رفتار کنار هم خواهند بود.
نکته: برای مطالعه بیشتر میتوانید به مطالب ^ و ^ مراجعه کنید.
در نهایت میتوانید برای سناریوهای خاص خودتان از سایر واسط های موجود در زیرساخت، نیز به شکل زیر استفاده کنید:
نیاز به حذف نرم بدون نگهداری اطلاعات ردیابی تغییرات
public interface ISoftDeleteEntity { bool IsDeleted { get; set; } }
.با پیاده سازی واسط بالا این امکان را خواهید داشت که صرفا از مکانیزم حذف نرم استفاده کنید؛ بدون نیاز به نگهداری سایر اطلاعات
نیاز به مقداردهی خودکار زمان ثبت یک موجودیت خاص
این امر با پیاده سازی واسط زیر امکان پذیر خواهد بود.
public interface IHasCreationDateTime { DateTimeOffset CreationDateTime { get; set; } }
با توجه به اعمال اصل ISP در مباحث مطرح شده در مطلب جاری، بنا به نیاز خود از این واسطها و کلاسهای پایه پیاده ساز آنها میتوانید استفاده کنید.
پیشنیاز
- نقشه راه «آزمون واحد در دات نت»
- مطلب «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها»
در این مطلب قصد داریم تست ServiceLayer را به جای تست درون حافظهای که با ابزارهای Mocking در قالب Unit Testing انجام میگیرد، به کمک یک دیتابیس واقعی سبک وزن در قالب Integration Testing انجام دهیم.
قدم اول
یک پروژه تست را ایجاد کنید؛ بهتر است برای نظم دهی به ساختار Solution، پروژههای تست را در پوشه ای به نام Tests نگهداری کنید.
قدم دوم
بستههای نیوگت زیر را نصب کنید:
PM> install-package NUnit PM> install-package Shouldly PM> install-package EntityFramework PM> install-package FakeHttpContext
قدم سوم
نسخه دیتابیس انتخابی برای تست خودکار، LocalDB می باشد. لازم است در ابتدای اجرای تستها دیتابیس مربوط به Integration Test ایجاد شده و بعد از اتمام نیز دیتابیس مورد نظر حذف شود؛ برای این منظور از کلاس TestSetup استفاده خواهیم کرد.
[SetUpFixture] public class TestSetup { [OneTimeSetUp] public void SetUpDatabase() { DestroyDatabase(); CreateDatabase(); } [OneTimeTearDown] public void TearDownDatabase() { DestroyDatabase(); } //... }
با توجه به اینکه کلاس TestSetup با [SetUpFixture] تزئین شده است، Nunit قبل از اجرای تستها سراغ این کلاس آمده و متد SetUpDatebase را به دلیل تزئین شدن با [OneTimeSetUp]، قبل از اجرای تستها و متد TearDownDatabase را بدلیل تزئین شدن با [OneTimeTearDown] بعد از اجرای تمام تستها، اجرا خواهد کرد.
متد CreateDatabase
private static void CreateDatabase() { ExecuteSqlCommand(Master, string.Format(SqlResource.DatabaseScript, FileName)); //Use T-Sql Scripts For Create Database //ExecuteSqlCommand(MyAppTest, SqlResources.V1_0_0); var migration = new MigrateDatabaseToLatestVersion<ApplicationDbContext, DataLayer.Migrations.Configuration>(); migration.InitializeDatabase(new ApplicationDbContext()); } private static SqlConnectionStringBuilder Master => new SqlConnectionStringBuilder { DataSource = @"(LocalDB)\MSSQLLocalDB", InitialCatalog = "master", IntegratedSecurity = true }; private static string FileName => Path.Combine( Path.GetDirectoryName( Assembly.GetExecutingAssembly().Location), "MyAppTest.mdf");
برای مدیریت محل ذخیره سازی فایلهای دیتابیس، ابتدا دستورات ایجاد «دیتابیس تست» را برروی دیتابیس master اجرا میکنیم و در ادامه برای ساخت جداول از مکانیزم Migration خود EF استفاده شده است.
لازم است رشته اتصال به این دیتابیس ایجاد شده را در فایل App.config پروژه تست قرار دهید:
<connectionStrings> <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="Data Source=(LocalDB)\MSSQLLocalDb;Initial Catalog=MyAppTest;Integrated Security=True;" /> </connectionStrings>
متد DestroyDatabase
private static void DestroyDatabase() { var fileNames = ExecuteSqlQuery(Master, SqlResource.SelecDatabaseFileNames, row => (string)row["physical_name"]); if (!fileNames.Any()) return; ExecuteSqlCommand(Master, SqlResource.DetachDatabase); fileNames.ForEach(File.Delete); }
در این متد ابتدا آدرس فایلهای مرتبط با «دیتابیس تست» واکشی شده و در ادامه دستورات Detach دیتابیس انجام شده و فایلهای مرتبط حذف خواهند شد. فایلهای دیتابیس در مسیری شبیه به آدرس نشان داده شدهی در شکل زیر ذخیره خواهند شد.
قدم چهارم
برای جلوگیری از تداخل بین تستها لازم است تک تک تستها از هم ایزوله باشند؛ یکی از راه حلهای موجود، استفاده از تراکنشها میباشد. برای این منظور امکان AutoRollback را به صورت خودکار به متدهای تست با استفاده از Attribute زیر اعمال خواهیم کرد:
public class AutoRollbackAttribute : Attribute, ITestAction { private TransactionScope _scope; public void BeforeTest(ITest test) { _scope = new TransactionScope(TransactionScopeOption.RequiresNew,new TransactionOptions {IsolationLevel = IsolationLevel.Snapshot}); } public void AfterTest(ITest test) { _scope?.Dispose(); _scope = null; } public ActionTargets Targets => ActionTargets.Test; }
متدهای BeforTest و AfterTest به ترتیب قبل و بعد از اجرای متدهای تست تزئین شده با این Attribute اجرا خواهند شد.
در مواقعی هم که به HttpConext نیاز دارید، میتوانید از کتابخانه FakeHttpContext بهره ببرید. برای این مورد هم میتوان Attributeای را به مانند AutoRollback در نظر گرفت.
public class HttpContextAttribute:Attribute,ITestAction { private FakeHttpContext.FakeHttpContext _httpContext; public void BeforeTest(ITest test) { _httpContext = new FakeHttpContext.FakeHttpContext(); } public void AfterTest(ITest test) { _httpContext?.Dispose(); _httpContext = null; } public ActionTargets Targets => ActionTargets.Test; }
کاری که FakeHttpContext انجام میدهد، مقدار دهی HttpContext.Current با یک پیاده سازی ساختگی میباشد.
قدم پنجم
به عنوان مثال اگر بخواهیم برای سرویس «گروه کاربری»، Integration Test بنویسیم، به شکل زیر عمل خواهیم کرد:
namespace MyApp.IntegrationTests.ServiceLayer { [TestFixture] [AutoRollback] [HttpContext] public class RoleServiceTests { private IRoleApplicationService _roleService; [SetUp] public void Init() { } [TearDown] public void Clean() { } [OneTimeSetUp] public void SetUp() { _roleService = IoC.Resolve<IRoleApplicationService>(); using (var uow = IoC.Resolve<IUnitOfWork>()) { RoleInitialDataBuilder.Build(uow); } } [OneTimeTearDown] public void TearDown() { } [Test] [TestCase("Role1")] public void Should_Create_New_Role(string role) { var viewModel = new RoleCreateViewModel { Name = role }; _roleService.Create(viewModel); using (var context = IoC.Resolve<IUnitOfWork>()) { var user = context.Set<Role>().FirstOrDefault(a => a.Name == role); user.ShouldNotBeNull(); } } [Test] public void Should_Not_Create_New_Role_With_Admin_Name() { var viewModel = new RoleCreateViewModel { Name = "Admin" }; Assert.Throws<DbUpdateException>(() => _roleService.Create(viewModel)); } [Test] public void Should_AdminRole_Exists() { using (var context = IoC.Resolve<IUnitOfWork>()) { var user = context.Set<Role>().FirstOrDefault(a => a.Name == "Admin"); user.ShouldNotBeNull(); } } [Test] public void Should_Not_Create_New_Role_Without_Name() { Assert.Throws<ValidationException>(() => _roleService.Create(new RoleCreateViewModel { Name = null })); } } }
با این خروجی:
EF Code First #11
استفاده از الگوی Repository اضافی در EF Code first؛ آری یا خیر؟!
اگر در ویژوال استودیو، اشارهگر ماوس را بر روی تعریف DbContext قرار دهیم، راهنمای زیر ظاهر میشود:
A DbContext instance represents a combination of the Unit Of Work and Repository patterns such that
it can be used to query from a database and group together changes that will then be written back to
the store as a unit. DbContext is conceptually similar to ObjectContext.
در اینجا تیم EF صراحتا عنوان میکند که DbContext در EF Code first همان الگوی Unit Of Work را پیاده سازی کرده و در داخل کلاس مشتق شده از آن، DbSetها همان Repositories هستند (فقط نامها تغییر کردهاند؛ اصول یکی است).
به عبارت دیگر با نام بردن صریح از این الگوها، مقصود زیر را دنبال میکنند:
لطفا بر روی این لایه Abstraction ایی که ما تهیه دیدهایم، یک لایه Abstraction دیگر را ایجاد نکنید!
«لایه Abstraction دیگر» یعنی پیاده سازی الگوهای Unit Of Work و Repository جدید، برفراز الگوهای Unit Of Work و Repository توکار موجود!
کار اضافهای که در بسیاری از سایتها مشاهده میشود و ... متاسفانه اکثر آنها هم اشتباه هستند! در ذیل روشهای تشخیص پیاده سازیهای نادرست الگوی Repository را بر خواهیم شمرد:
1) قرار دادن متد Save تغییرات نهایی انجام شده، در داخل کلاس Repository
متد Save باید داخل کلاس Unit of work تعریف شود نه داخل کلاس Repository. دقیقا همان کاری که در EF Code first به درستی انجام شده. متد SaveChanges توسط DbContext ارائه میشود. علت هم این است که در زمان Save ممکن است با چندین Entity و چندین جدول مشغول به کار باشیم. حاصل یک تراکنش، باید نهایتا ذخیره شود نه اینکه هر کدام از اینها، تراکنش خاص خودشان را داشته باشند.
2) نداشتن درکی از الگوی Unit of work
به Unit of work به شکل یک تراکنش نگاه کنید. در داخل آن با انواع و اقسام موجودیتها از کلاسها و جداول مختلف کار شده و حاصل عملیات، به بانک اطلاعاتی اعمال میگردد. پیاده سازیهای اشتباه الگوی Repository، تمام امکانات را در داخل همان کلاس Repository قرار میدهند؛ که اشتباه است. این نوع کلاسها فقط برای کار با یک Entity بهینه شدهاند؛ در حالیکه در دنیای واقعی، اطلاعات ممکن است از دو Entity مختلف دریافت و نتیجه محاسبات مفروضی به Entity سوم اعمال شود. تمام این عملیات یک تراکنش را تشکیل میدهد، نه اینکه هر کدام، تراکنش مجزای خود را داشته باشند.
3) وهله سازی از DbContext به صورت مستقیم داخل کلاس Repository
4) Dispose اشیاء DbContext داخل کلاس Repository
هر بار وهله سازی DbContext مساوی است با باز شدن یک اتصال به بانک اطلاعاتی و همچنین از آنجائیکه راهنمای ذکر شده فوق را در مورد DbContext مطالعه نکردهاند، زمانیکه در یک متد با سه وهله از سه Repository موجودیتهای مختلف کار میکنید، سه تراکنش و سه اتصال مختلف به بانک اطلاعاتی گشوده شده است. این مورد ذاتا اشتباه است و سربار بالایی را نیز به همراه دارد.
ضمن اینکه بستن DbContext در یک Repository، امکان اعمال کوئریهای بعدی LINQ را غیرممکن میکند. به ظاهر یک شیء IQueryable در اختیار داریم که میتوان بر روی آن انواع و اقسام کوئریهای LINQ را تعریف کرد اما ... در اینجا با LINQ to Objects که بر روی اطلاعات موجود در حافظه کار میکند سر و کار نداریم. اتصال به بانک اطلاعاتی با بستن DbContext قطع شده، بنابراین کوئری LINQ بعدی شما کار نخواهد کرد.
همچنین در EF نمیتوان یک Entity را از یک Context به Context دیگری ارسال کرد. در پیاده سازی صحیح الگوی Repository (دقیقا همان چیزی که در EF Code first به صورت توکار وجود دارد)، Context باید بین Repositories که در اینجا فقط نامش DbSet تعریف شده، به اشتراک گذاشته شود. علت هم این است که EF از Context برای ردیابی تغییرات انجام شده بر روی موجودیتها استفاده میکند (همان سطح اول کش که در قسمتهای قبل به آن اشاره شد). اگر به ازای هر Repository یکبار وهله سازی DbContext انجام شود، هر کدام کش جداگانه خاص خود را خواهند داشت.
5) عدم امکان استفاده از تنها یک DbConetext به ازای یک Http Request
هنگامیکه وهله سازی DbContext به داخل یک Repository منتقل میشود و الگوی واحد کار رعایت نمیگردد، امکان به اشتراک گذاری آن بین Repositoryهای تعریف شده وجود نخواهد داشت. این مساله در برنامههای وب سبب کاهش کارآیی میگردد (باز و بسته شدن بیش از حد اتصال به بانک اطلاعاتی در حالیکه میشد تمام این عملیات را با یک DbContext انجام داد).
نمونهای از این پیاده سازی اشتباه را در اینجا میتوانید پیدا کنید. متاسفانه شبیه به همین پیاده سازی، در پروژه MVC Scaffolding نیز بکارگرفته شده است.
چرا تعریف لایه دیگری بر روی لایه Abstraction موجود در EF Code first اشتباه است؟
یکی از دلایلی که حین تعریف الگوی Repository دوم بر روی لایه موجود عنوان میشود، این است:
«به این ترتیب به سادگی میتوان ORM مورد استفاده را تغییر داد» چون پیاده سازی استفاده از ORM، در پشت این لایه مخفی شده و ما هر زمان که بخواهیم به ORM دیگری کوچ کنیم، فقط کافی است این لایه را تغییر دهیم و نه کل برنامه را.
ولی سؤال این است که هرچند این مساله از هزار فرسنگ بالاتر درست است، اما واقعا تابحال دیدهاید که پروژهای را با یک ORM شروع کنند و بعد سوئیچ کنند به ORM دیگری؟!
ضمنا برای اینکه واقعا لایه اضافی پیاده سازی شده انتقال پذیر باشد، شما باید کاملا دست و پای ORM موجود را بریده و تواناییهای در دسترس آن را به سطح نازلی کاهش دهید تا پیاده سازی شما قابل انتقال باشد. برای مثال یک سری از قابلیتهای پیشرفته و بسیار جالب در NH هست که در EF نیست و برعکس. آیا واقعا میتوان به همین سادگی ORM مورد استفاده را تغییر داد؟ فقط در یک حالت این امر میسر است: از قابلیتهای پیشرفته ابزار موجود استفاده نکنیم و از آن در سطحی بسیار ساده و ابتدایی کمک بگیریم تا از قابلیتهای مشترک بین ORMهای موجود استفاده شود.
ضمن اینکه مباحث نگاشت کلاسها به جداول را چکار خواهید کرد؟ EF راه و روش خاص خودش را دارد، NH چندین و چند روش خاص خودش را دارد! اینها به این سادگی قابل انتقال نیستند که شخصی عنوان کند: «هر زمان که علاقمند بودیم، ORM مورد استفاده را میشود عوض کرد!»
دلیل دومی که برای تهیه لایه اضافهتری بر روی DbContext عنوان میکنند این است:
«با استفاده از الگوی Repository نوشتن آزمونهای واحد سادهتر میشود». زمانیکه برنامه بر اساس Interfaceها کار میکند میتوان آنها را بجای اشاره به بانک اطلاعاتی، به نمونهای موجود در حافظه، در زمان آزمون تغییر داد.
این مورد در حالت کلی درست است اما .... نه در مورد بانکهای اطلاعاتی!
زمانیکه در یک آزمون واحد، پیاده سازی جدیدی از الگوی Interface مخزن ما تهیه میشود و اینبار بجای بانک اطلاعاتی با یک سری شیء قرارگرفته در حافظه سروکار داریم، آیا موارد زیر را هم میتوان به سادگی آزمایش کرد؟
ارتباطات بین جداولرا، cascade delete، فیلدهای identity، فیلدهای unique، کلیدهای ترکیبی، نوعهای خاص تعریف شده در بانک اطلاعاتی و مسایلی از این دست.
پاسخ: خیر! تغییر انجام شده، سبب کار برنامه با اطلاعات موجود در حافظه خواهد شد، یعنی LINQ to Objects.
شما در حالت استفاده از LINQ to Objects آزادی عمل فوق العادهای دارید. میتوانید از انواع و اقسام متدها حین تهیه کوئریهای LINQ استفاده کنید که هیچکدام معادلی در بانک اطلاعاتی نداشته و ... به ظاهر آزمون واحد شما پاس میشود؛ اما در عمل بر روی یک بانک اطلاعاتی واقعی کار نخواهد کرد.
البته شاید شخصی عنوان که بله میشود تمام اینها نیازمندیها را در حالت کار با اشیاء درون حافظه هم پیاده سازی کرد ولی ... در نهایت پیاده سازی آن بسیار پیچیده و در حد پیاده سازی یک بانک اطلاعاتی واقعی خواهد شد که واقعا ضرورتی ندارد.
و پاسخ صحیح در اینجا و این مساله خاص این است:
لطفا در حین کار با بانکهای اطلاعاتی مباحث mocking را فراموش کنید. بجای SQL Server، رشته اتصالی و تنظیمات برنامه را به SQL Server CE تغییر داده و آزمایشات خود را انجام دهید. پس از پایان کار هم بانک اطلاعاتی را delete کنید. به این نوع آزمونها اصطلاحا integration tests گفته میشود. لازم است برنامه با یک بانک اطلاعاتی واقعی تست شود و نه یک سری شیء ساده قرار گرفته در حافظه که هیچ قیدی همانند شرایط کار با یک بانک اطلاعاتی واقعی، بر روی آنها اعمال نمیشود.
ضمنا باید درنظر داشت بانکهای اطلاعاتی که تنها در حافظه کار کنند نیز وجود دارند. برای مثال SQLite حالت کار کردن صرفا در حافظه را پشتیبانی میکند. زمانیکه آزمون واحد شروع میشود، یک بانک اطلاعاتی واقعی را در حافظه تشکیل داده و پس از پایان کار هم ... اثری از این بانک اطلاعاتی باقی نخواهد ماند و برای این نوع کارها بسیار سریع است.
نتیجه گیری:
حین استفاده از EF code first، الگوی واحد کار، همان DbContext است و الگوی مخزن، همان DbSetها. ضرورتی به ایجاد یک لایه محافظ اضافی بر روی اینها وجود ندارد.
در اینجا بهتر است یک لایه اضافی را به نام مثلا Service ایجاد کرد و تمام اعمال کار با EF را به آن منتقل نمود. سپس در قسمتهای مختلف برنامه میتوان از متدهای این لایه استفاده کرد. به عبارتی در فایلهای Code behind برنامه شما نباید کدهای EF مشاهده شوند. یا در کنترلرهای MVC نیز به همین ترتیب. اینها مصرف کننده نهایی لایه سرویس ایجاد شده خواهند بود.
همچنین بجای نوشتن آزمونهای واحد، به Integration tests سوئیچ کنید تا بتوان برنامه را در شرایط کار با یک بانک اطلاعاتی واقعی تست کرد.
برای مطالعه بیشتر: