مقدمه ای بر Meteor
در حین کار با WCF RIA Services اگر تمام موجودیتهای تعریف شده انتخاب شوند یک فایل طویل DomainService حاصل خواهد شد. کار کردن با این فایل نه زیبا است و نه ساده. بعد از یک مدت شاید بگوئیم، خوب! من به ازای هر جدول یک DomainService جدا تولید میکنم با نامی مختص به آن و این اطلاعات را در فایلی جداگانه نیز ذخیره خواهم کرد. پس از انجام اینکار با خطای زیر مواجه خواهیم شد:
به صورت خلاصه: مهم نیست سیستم شما از چند جدول تشکیل شده است؛ مهم این است که تنها یک فایل DomainService را باید تولید کنید و البته این یک محدودیت نیست؛ یک هدف محسوب میشود؛ از این دیدگاه که موجودیتهای مرتبط باید در یک Domain قرار گیرند و تنها در یک دومین هستند که روابط بین آنها معنا پیدا میکند.
- روش توصیه شده برای مدیریت این کلاس DomainService طویل، استفاده از واژهی کلیدی partial است (برای مثال public partial class MyDomainService). به این صورت هر موجودیت را میتوان در یک فایل جداگانه قرار داد و به این ترتیب مدیریت سادهتری را بر روی اطلاعات تولید شده داشت و همچنین تمامی این فایلها در نهایت یک کلاس واحد را تشکیل میدهند و اصل وجود یک DomainService واحد در برنامه زیر سؤال نخواهد رفت.
- کدهایی را که خودتان نیز به این مجموعه اضافه خواهید کرد، در لابلای کدهای تولید شده قرار ندهید. در صورت تغییری در جداول نیاز است تا این فایلها مجددا تولید شوند و اینجا است که تمام تغییرات خود را از دست خواهید داد. برای این منظور باز هم یک کلاس partial دیگر را تعریف کنید تا کدهای سفارشی خود را بتوان به صورت مجزایی از کدهای تولید شده به صورت خودکار، در آن قرار داد (برای مثال به نام MyServiceNameDomainService.extensions.cs).
- جایی که قرار است یک سری از فایلها مجددا تولید شوند استفاده از برنامههای سورس کنترل را فراموش نکنید؛ تا هر زمانی بتوان کدهای جدید را با کدهای نگارشهای قبل به سادگی مقایسه کرد؛ یا حتی به نگارشهای قبلی بازگشت نمود.
- این نکته را به خاطر داشته باشید که اگر اطلاعاتی به صورت خودکار از پیش تعریف شده و موجود است (کلاس دومین یا متادیتای آن)، این اطلاعات حین استفاده از امکانات تولید کد خودکار، مجددا تولید نخواهند شد. به همین جهت باید ابتدا آنها را به صورت comment در آورد (یا از پروژه خارج نمود؛ استفاده از امکانات include و exclude پوشهها یا فایلها در ویژوال استودیو) تا همواره کدهای آخرین اطلاعات موجود تولید گردند.
نوشتن آزمونهای واحد برای SQL Server
One SQL unit testing framework which I have been using lately is tSQLt, which is a simple open-source framework.
Here’s what r/javascript had to say:
- React.js with Flux (a view-only library and an eventing module)
- Ember.js (a full MVC framework)
- Knockout.js (view-only library)
- Backbone.js (full MVC framework)
- Meteor (full isomorphic framework)
- Mithril (full MVC framework)
- Ember (full MVC framework)
- ‘No framework; just lots of libraries’
- Vue.js (view-only library)
- Breeze.js (model-only library)
- Ractive (view-only library)
public bool AddOrUpdateOrDelete<TEntity>(TEntity newItem, bool updateIsNull) where TEntity : class { try { var dbMyNews = new dbMyNewsEntities(); if (updateIsNull) dbMyNews.Set<TEntity>().AddOrUpdate(newItem); else { dbMyNews.Set<TEntity>().Attach(newItem); var entry = dbMyNews.Entry(newItem); foreach ( var pri in newItem.GetType().GetProperties() .Where(pri => (pri.GetGetMethod(false).ReturnParameter.ParameterType.IsSerializable && pri.GetValue(newItem, null) != null))) { entry.Property(pri.Name).IsModified = true; } } dbMyNews.SaveChanges(); return true; } catch (Exception) { return false; } }
var news = new tblNews(); news.tblCategoryId = 2; news.tblNewsId = 1; MyNews.EditNews(news);
var news = MyNews.GetNews(1); news.tblCategoryId = 2; MyNews.EditNews(news)
dbMyNews.Entry(news).State = EntityState.Modified;
public int AddNews(tblNews News) { dbMyNews.tblNews.Add(News); dbMyNews.SaveChanges(); return News.tblNewsId; } public bool EditNews(tblNews News) { try { dbMyNews.Entry(News).State = EntityState.Modified; dbMyNews.SaveChanges(); return true; } catch (Exception exp) { return false; } } public bool DeleteNews(int tblNewsId) { try { tblNews News = dbMyNews.tblNews.FirstOrDefault(p => p.tblNewsId == tblNewsId); News.IsDeleted = true; dbMyNews.SaveChanges(); return true; } catch (Exception exp) { return false; } }
public bool AddOrEditNews(tblNews News) { return AddOrUpdateOrDelete(News, News.tblNewsId == 0); }
MyNews.AddOrEditNews (new tblNews { tblNewsId = 1, IsDeleted =true });
شرایطی را در نظر بگیرید که نیاز است از تغییرات یک Entity در سیستم آگاه شویم. برای مثلا در زمان ثبت سفارش جدید در فروشگاه، ایمیلی به مدیر فروشگاه ارسال شود، یک Business Rule نیز چک شود و همچنین بنابر نیاز مشتری، تعداد آنها روز به روز ممکن است افزایش یابد و چه بسا در اعمال این Ruleها، موجودیتهای مختلفی درگیر باشند. در این صورت است که خواسته یا ناخواسته اتصال بین کلاسها خیلی افزایش خواهد یافت. یکی از راه حلهای رهایی از این پیچیدگی و اتصال بالا، استفاده از Event میباشد.
هدف طراحی و پیاده سازی زیرساختی برای استفاده از DomainEventها میباشد. کدهای کامل این مطلب را میتوانید از اینجا دریافت کنید.
Domain Event چیست؟
چیزی که در یک Domain خاصی رخ داده است و هدف از آن آگاه کردن سایر بخشهای آن Domain میباشد تا بتوانند واکنش مناسبی را نشان دهند. با بهره گیری از این نوع رویدادها، میتوان Separation Of Concerns خوبی را بین کلاسهای موجود در آن Domain اعمال کرد و به طراحی ای با Coupling پایین رسید. این رویدادها عموما داخل پروسه Raise میشوند.
namespace DomainEventsSample.Framework.Eventing.DomainEvents { public interface IDomainEvent : ITransientDependency { } }
namespace DomainEventsSample.Framework.Eventing.DomainEvents { public interface IDomainEventHandler<in T> : ITransientDependency where T : IDomainEvent { bool IsAdvisable { get; } void Handle(T domainEvent); } }
- متد Raise مربوط به Engine برای رویداد خاصی فراخوانی میشود.
- با استفاده از یک IOC Container، تمام هندلرهای مربوط به رویداد جمع آوری میشود.
- متد Handle مربوط به تک تک هندلرها، فراخوانی خواهد شد.
namespace DomainEventsSample.Framework.Eventing.DomainEvents { public interface IDomainEventEngine : ISingletonDependency { void Raise<T>(T domainEvent) where T : IDomainEvent; } } namespace DomainEventsSample.Framework.Eventing.DomainEvents { public class DomainEventEngine : IDomainEventEngine { private readonly IContainer _container; public DomainEventEngine(IContainer container) { _container = container; } public void Raise<T>(T domainEvent) where T : IDomainEvent { foreach (var handler in _container.GetAllInstances<IDomainEventHandler<T>>()) try { handler.Handle(domainEvent); } catch (Exception) { if (domainEvent.IsAdvisable && handler.IsAdvisable) throw; } } } }
namespace DomainEventsSample.Framework.Domain.Events { public abstract class EntityDomainEvent<TEntity> : IDomainEvent where TEntity : Entity { protected EntityDomainEvent(TEntity entity) { Entity = entity; } public TEntity Entity { get; } } }
کلاس بالا به عنوان کلاس پایه یکسری رویداد مشترک مابین Entityهای سیستم در نظر گرفته شده است.
namespace DomainEventsSample.Framework.Domain.Events { public class EntityCreatingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityCreatingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntityCreatedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityCreatedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از ایجاد یک Entity میباشند.
namespace DomainEventsSample.Framework.Domain.Events { public class EntityEditingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityEditingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntityEditedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityEditedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از ویرایش یک Entity میباشند.
namespace DomainEventsSample.Framework.Domain.Events { public class EntityDeletingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityDeletingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntityDeletedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityDeletedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از حذف یک Entity میباشند.
namespace DomainEventsSample.Framework.Domain.Events { public class EntitySavingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntitySavingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntitySavedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntitySavedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از ذخیره (ایجاد و ویرایش) یک Entity میباشند.
نکته: برای اسکن کردن تمام هندلرها لازم است کد زیر را به تنظیمات StructureMap اضافه کنید:
Scan(scan => { scan.ConnectImplementationsToTypesClosing(typeof(IDomainEventHandler<>)); });
public class ProductCreatedEventHandler : IDomainEventHandler<EntityCreatedEvent<Product>> { public bool IsAdvisable => false; public void Handle(EntityCreatedEvent<Product> domainEvent) { //todo: notify users } }
در متد Create مربوط به ProductApplicationService و بعد از عملیات ذخیره سازی به شکل زیر میبایست عمل کرد:
public class ProductApplicationService : IProductApplicationService { private readonly IDomainEventEngine _eventEngine; private readonly IUnitOfWork _unitOfWork; private readonly IMapper _mapper; public ProductApplicationService(IDomainEventEngine eventEngine,IMapper mapper,IUnitOfWork unitOfWork) { _eventEngine = eventEngine; _mapper=mapper; _unitOfWork=unitOfWork; } [Transactional] public void Create(ProductCreateViewModel model) { var entity=_mapper.Map<Product>(model); _unitOfWork.Set<Product>().Add(entity); _unitOfWork.SaveChanges(); _eventEngine.Raise(new EntityCreatedEvent<Product>(entity)); } }
البته بهتر است برای Raise کردن این نوع رویدادها از مکانیزم Hook استفاده کرد و در زمان ذخیره سازی و فراخوانی متد SaveChange، این عملیات به صورت خودکار صورت گیرند.
در مقاله بعدی با استفاده از Hookها این عملیات را انجام خواهیم داد.
کدهای این قسمت را میتوانید از اینجا دریافت کنید.
Mini Course #1 Clean Architecture + CQRS - YouTube
00:00:00 - Intro
00:01:10 - Why do we record this course?
00:04:39 - Layered architecture: Why I don't like it anymore?
00:27:27 - Clean architecture
00:38:16 - Course prerequisites (still can't pronounce it correctly)
00:42:30 - What do we build?
00:45:18 - *Domain Layer*
00:49:57 - Entity
01:00:20 - Primitive obsession code smell
01:03:20 - Value Object
01:10:54 - Custom exceptions
01:33:25 - Domain model validation
01:38:55 - Aggregate
01:50:50 - Domain event
02:14:30 - Factory
02:28:50 - Policy
02:43:50 - Repository
02:48:55 - Domain Layer: Summary
02:56:45 - CQS: Command Query Separation
03:07:46 - CQRS: Command Query Responsibility Segregation
03:29:48 - *Application Layer*
03:30:25 - Command/Command Handler/Command Dispatcher definitions
03:40:45 - Automatic command handlers registration
03:44:12 - Application & Domain registration
03:50:52 - Command
03:55:31 - Command Handler
04:01:11 - Where to put methods related to reading?
04:07:00 - Read Service
04:14:14 - Weather Service
04:28:25 - Overview of the other commands & command handlers
04:34:35 - Query/Query Handler/Query Dispatcher definitions
04:44:37 - Automatic query handlers registration (BONUS: example of copy/paste pattern)
04:52:50 - How to tackle reading on Query side?
05:06:30 - *Infrastructure Layer*
05:07:56 - Configuration of Entity Framework Core
05:15:15 - Read Models
05:19:05 - ReadDbContext & WriteDbContext
05:24:13 - EF Entity Configuration
05:42:37 - DbContexts registration
05:55:51 - Query Handlers
06:12:18 - Repository implementation on top of EF Core
06:16:45 - Read Service implementation on top of EF Core
06:19:00 - Dummy Weather Service implementation
06:23:20 - EF Migration
06:29:50 - Applying EF migrations automatically
06:39:38 - *Presentation Layer*
06:41:35 - Controller
06:50:43 - How to return ID of resource in CQRS approach?
07:00:02 - Testing API
07:09:29 - *Cross Cutting Concerns*
07:09:30 - Error Handling
07:20:49 - Logging
07:34:57 - *Unit Testing*
07:42:34 - Unit Test on Domain Layer
08:09:45 - Unit Test on Application Layer
08:34:00 - Summary
در این نوشتار که به صورت آموزش تصویری ارائه میشود؛ یک سرویس WCF در Visual Studio 2013 ایجاد میکنم، سپس روش استفاده از آنرا در یک برنامه ویندوزی آموزش خواهم داد. در اینجا در نظرگرفته شده است که شما افزونهی Resharper را روی ویژوال استودیوی خود نصب دارید. پس در صورتیکه هنوز به سراغ آن نرفته اید درنگ نکنید و واپسین نگارش آن را دانلود کنید.
در این پروژهی ساده در نظر میگیریم که دو جدول یکی برای اخبار، شامل عنوان، متن خبر و تاریخ ثبت و دسته بندی و دیگری برای نگهداری دستهها در پایگاه داده داریم و میخواهیم سرویسهای مناسب با این دو جدول را بسازیم. با کد زیر، پایگاه دادهی dbTest و جدولهای tblNews و tblCategory در SQL Server 2012 ساخته میشود:
USE [master] GO /****** Object: Database [dbMyNews] Script Date: 2014/01/14 09:46:04 ب.ظ ******/ CREATE DATABASE [dbMyNews] CONTAINMENT = NONE ON PRIMARY ( NAME = N'dbMyNews', FILENAME = N'D:\dbMyNews.mdf' , SIZE = 5120KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB ) LOG ON ( NAME = N'dbMyNews_log', FILENAME = N'D:\dbMyNews_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%) GO USE [dbMyNews] GO /****** Object: Table [dbo].[tblCategory] Script Date: 2014/01/14 09:46:04 ب.ظ ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[tblCategory]( [tblCategoryId] [int] IDENTITY(1,1) NOT NULL, [CatName] [nvarchar](50) NOT NULL, [IsDeleted] [bit] NOT NULL, CONSTRAINT [PK_tblCategory] PRIMARY KEY CLUSTERED ( [tblCategoryId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO /****** Object: Table [dbo].[tblNews] Script Date: 2014/01/14 09:46:04 ب.ظ ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[tblNews]( [tblNewsId] [int] IDENTITY(1,1) NOT NULL, [tblCategoryId] [int] NOT NULL, [Title] [nvarchar](50) NOT NULL, [Description] [nvarchar](max) NOT NULL, [RegDate] [datetime] NOT NULL, [IsDeleted] [bit] NULL, CONSTRAINT [PK_tblNews] PRIMARY KEY CLUSTERED ( [tblNewsId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO ALTER TABLE [dbo].[tblNews] WITH CHECK ADD CONSTRAINT [FK_tblNews_tblCategory] FOREIGN KEY([tblCategoryId]) REFERENCES [dbo].[tblCategory] ([tblCategoryId]) GO ALTER TABLE [dbo].[tblNews] CHECK CONSTRAINT [FK_tblNews_tblCategory] GO USE [master] GO ALTER DATABASE [dbMyNews] SET READ_WRITE GO
اکنون Visual Studio 2013 را بازکنید سپس روی گزینه New Project کلیک کنید و برابر با نگارهی زیر عمل کنید:
پروژه MyNewsWCFLibrary در راه حل MyNews ساخته میشود. این پروژه به صورت پیشگزیده دارای یک کلاس به نام Service و یک interface به نام IService است. هر دو را حذف کنید و سپس روی نام پروژه راستکلیک کرده، از منوی بازشده گزینهی Add -> New Item را انتخاب کنید. سپس برابر با نگارهی زیر عمل کنید:
در لایهی Service Interface کلیهی روالهای مورد نیاز برای ارتباط با پایگاه داده را میسازیم. پیش از آن باید یک Model برای ارتباط با پایگاه داده ساخته باشیم. برای این کار از پنجره Add New Item و از زیرمجموعه Data، گزینه ADO.NET Entity Data Model را انتخاب کنید و بهسان زیر پیش روید:
در گام پسین روی دکمه New Connection کلیک کنید و رشتهی اتصال به پایگاه دادهی dbMyNews را بسازید. سپس همانند تنظیمات نگارهی زیر ادامه دهید:
در گام پسین گزینهی Entity Framework 6.0 را برگزینید و روی دکمهی Next کلیک کنید.
در پنجره نشان داده شده، جدولهای مورد نیاز را همانند نگارهی زیر انتخاب کرده و روی دکمه Finish کلیک کنید:
در پایان مدل ما همانند نگارهی زیر خواهد بود.
در بخش پسین دربارهی شیوهی دستکاری کلاسهای Entity خواهم نوشت.