با معماری CQRS و Event Sourcing بصورت مختصر آشنا شدیم. برای درک بیشتر مطلب پیشین، احتیاج به پیاده سازی آن به صورت عملیاتی و نه فقط تئوری محض میباشد و در این مرحله قصد پیاده سازی این مدل را به سادهترین صورت ممکن داریم.
برای مطالعهی ادامهی این مقاله، نیاز به آشنایی با مباحث مطرح شده در قسمت قبل وجود دارد. پس از توضیحات اضافه بر روی قسمتهای زیر گذشته و فرض بر آن است که آشنایی با این قسمتها وجود دارد.
برای سریعتر شدن کار میخواهیم پیاده سازی این مدل را در یک پروژهی Console انجام دهیم و همچنین پس از ایجاد، پکیجهای زیر را نصب مینماییم:
Unity, SimpleCqrs, SimpleCqrs.Unity
میخواهیم طبق مراحل گفته شدهی در قسمت قبل، به پیاده سازی این مدل بپردازیم و هدف، اضافه کردن یک Account به سیستم خواهد بود.
ابتدا باید DomainObject مورد نظر نوشته شود:
using System;
using SimpleCqrs.Domain;
namespace CqrsPattern.Cqrs.Command
{
public class Account : AggregateRoot
{
public Account(Guid id)
{
Apply(new AccountCreatedEvent { AggregateRootId = id });
}
public void SetName(string firstName, string lastName)
{
Apply(new AccountNameSetEvent { FirstName = firstName, LastName = lastName });
}
public void OnAccountCreated(AccountCreatedEvent evt)
{
Id = evt.AggregateRootId;
}
}
}
نکته: میخواهیم عملیات اضافه کردن یک Account، با استفاده از دو event مربوطه به نام AccountCreatedEvent و مقدار دهی آن با استفاده از AccountNameSetEvent انجام شود.
eventهای فوق را در ادامه اضافه خواهیم داد (از توضیحات بیشتر صرفنظر شده و به مقالهی قسمت قبل رجوع شود).
حال احتیاج به پیاده سازی Command مربوطه برای انجام وظیفهی خود داریم که هدف آن، اضافه کردن یک Account به سیستم مورد نظر میباشد.
فرض کنید برای اضافه شدن Account، پراپرتیهای FirstName و LastName باید مقدار دهی شوند:
using SimpleCqrs.Commanding;
namespace CqrsPattern.Cqrs.Command
{
public class CreateAccountCommand : ICommand
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
حال CommandHandler که وظیفهی تفسیر کردن Command مربوطه را به عهده دارد، پیاده سازی خواهد شد:
using System;
using SimpleCqrs.Commanding;
using SimpleCqrs.Domain;
namespace CqrsPattern.Cqrs.Command
{
public class CreateAccountCommandHandler : CommandHandler<CreateAccountCommand>
{
private readonly IDomainRepository repository;
public CreateAccountCommandHandler(IDomainRepository repository)
{
this.repository = repository;
}
public override void Handle(CreateAccountCommand command)
{
var account = new Account(Guid.NewGuid());
account.SetName(command.FirstName, command.LastName);
repository.Save(account);
}
}
}
نکته: از طریق account.SetName فراخوانی Event مربوطه انجام شدهاست و همچنین repository.Save به raise کردن EventHandler میپردازد.
event مربوط به اضافه شدن Account را به صورت زیر پیاده سازی مینماییم:
using SimpleCqrs.Eventing;
namespace CqrsPattern.Cqrs.Command
{
public class AccountCreatedEvent : DomainEvent { }
}
و همچنین event مربوط به مقدار دهی پراپرتیها نیز به صورت زیر خواهد بود:
using SimpleCqrs.Eventing;
namespace CqrsPattern.Cqrs.Command
{
public class AccountNameSetEvent : DomainEvent
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
در این بخش، پیاده سازی EventHandler را خواهیم داشت. طبق مطلب پیشین هر Domain باید EventHnadler ی داشته باشد که از Event هایش ارث بری کرده و هر کدام از Eventها عملا در قسمت Handle مربوط به خودش پردازش خواهد شد.
using System.Linq;
using SimpleCqrs.Eventing;
using CqrsPattern.Cqrs.Db;
namespace CqrsPattern.Cqrs.Command
{
public class AccountEventHandler : IHandleDomainEvents<AccountCreatedEvent>,
IHandleDomainEvents<AccountNameSetEvent>
{
private readonly FakeAccountTable accountTable;
public AccountEventHandler(FakeAccountTable accountTable)
{
this.accountTable = accountTable;
}
public void Handle(AccountCreatedEvent domainEvent)
{
accountTable.Add(new FakeAccountTableRow { Id = domainEvent.AggregateRootId });
}
public void Handle(AccountNameSetEvent domainEvent)
{
var account = accountTable.Single(x => x.Id == domainEvent.AggregateRootId);
account.Name = domainEvent.FirstName + " " + domainEvent.LastName;
}
}
}
نکته: از آنجاییکه پیاده سازی ذخیره کردن Account با استفاده از دو event فوق انجام شده، بعد از Raise شدن EventHandler هر دو متد Handle، وظیفهی Command مربوطه را به عهده دارند (بنابراین وظیفهی هر Command میتواند با استفاده از eventهای مختلفی انجام شود).
برای اینکه نخواهیم وارد فازهای مربوط به دیتابیس شویم، موقتا یک db به صورت fake شده را پیاده سازی مینماییم؛ به صورت زیر:
using System.Collections.Generic;
namespace CqrsPattern.Cqrs.Db
{
public class FakeAccountTable : List<FakeAccountTableRow>
{ }
}
using System;
namespace CqrsPattern.Cqrs.Db
{
public class FakeAccountTableRow
{
public Guid Id { get; set; }
public string Name { get; set; }
}
}
و همچنین نیاز به ServiceLocator برای نمونه گرفتن از RunTime ی که از آن ارث بری کرده است داریم (برای سادگی کار از الگوی ServiceLocator استفاده میکنیم، ServiceLocator جز
Anti-Pattern ها محسوب میشود و معمولا در پروژههای واقعی از آن استفاده نمیشود)
using SimpleCqrs;
using SimpleCqrs.Unity;
namespace CqrsPattern
{
public class SampleRunTime : SimpleCqrsRuntime<UnityServiceLocator> { }
}
حال احتیاج به پیاده سازی قسمت Queryداریم به همراه ReadModel و سرویسی برای فراخوانی آن
using System;
namespace CqrsPattern.Cqrs.Query
{
public class AccountReadModel
{
public string Name { get; set; }
public Guid Id { get; set; }
}
}
using CqrsPattern.Cqrs.Db;
using System.Collections.Generic;
using System.Linq;
namespace CqrsPattern.Cqrs.Query
{
public class AccountReportReadService
{
private FakeAccountTable fakeAccountDb;
public AccountReportReadService(FakeAccountTable fakeAccountDb)
{
this.fakeAccountDb = fakeAccountDb;
}
public IEnumerable<AccountReadModel> GetAccounts()
{
return from a in fakeAccountDb
select new AccountReadModel { Id = a.Id, Name = a.Name };
}
}
}
در قسمت Main نرم افزار نیاز به register کردن FakeTable خود داریم و همانطور که ملاحظه میکنید Command مورد نظر را نمونه سازی کرده و آن را روی CommandBus قرار میدهیم تا مراحل پیاده سازی شده در قسمتهای فوق انجام شود و همچنین بعد از اتمام command ارسال شده از طریق Service مورد نظر اطلاعات ذخیره شده بازگردانی میشود
using System;
using SimpleCqrs.Commanding;
using CqrsPattern.Cqrs.Query;
using CqrsPattern.Cqrs.Command;
namespace CqrsPattern
{
class Program
{
static void Main(string[] args)
{
var runtime = new SampleRunTime();
runtime.Start();
var fakeAccountTable = new FakeAccountTable();
runtime.ServiceLocator.Register(fakeAccountTable);
runtime.ServiceLocator.Register(new AccountReportReadService(fakeAccountTable));
var commandBus = runtime.ServiceLocator.Resolve<ICommandBus>();
var cmd = new CreateAccountCommand { FirstName = "Ali", LastName = "Kh" };
commandBus.Send(cmd);
var accountReportReadModel = runtime.ServiceLocator.Resolve<AccountReportReadService>();
Console.WriteLine("Accounts in database");
Console.WriteLine("####################");
foreach (var account in accountReportReadModel.GetAccounts())
{
Console.WriteLine(" Id: {0} Name: {1}", account.Id, account.Name);
}
runtime.Shutdown();
Console.ReadLine();
}
}
}
اینگونه کل عملیاتهای لازم انجام خواهد شد.
خلاصه:
1) Command مربوطه را نمونه سازی کرده و روی CommandBus قرار میدهیم.
2) CommandHandler فراخوانی شده و فانکشن Handle آن باعث نمونه سازی از AggregateRoot میشود.
public override void Handle(CreateAccountCommand command)
{
var account = new Account(Guid.NewGuid()); //line 1
account.SetName(command.FirstName, command.LastName); //line 2
repository.Save(account); //line 3
}
در خط نخست Constructor کلاس Account باعث Apply شدن event مربوطه میشود.
public Account(Guid id)
{
Apply(new AccountCreatedEvent { AggregateRootId = id });
}
و در خط دوم
account.SetName برای Apply شدن event مربوط به مقدار دهی propertyها میباشد. public void SetName(string firstName, string lastName)
{
Apply(new AccountNameSetEvent { FirstName = firstName, LastName = lastName });
}
و همچنین در خط سوم و پس از
repository.Save باعث میشود eventهای pending شده Raise شده و توسط متد Handle مربوط به EventHandler پردازش شده و عملیاتهای زیر انجام شوند: public void Handle(AccountCreatedEvent domainEvent)
{
accountTable.Add(new FakeAccountTableRow { Id = domainEvent.AggregateRootId });
}
public void Handle(AccountNameSetEvent domainEvent)
{
var account = accountTable.Single(x => x.Id == domainEvent.AggregateRootId);
account.Name = domainEvent.FirstName + " " + domainEvent.LastName;
}
رکورد مورد نظر ثبت شده و event بعدی، پراپرتیهایش را مقدار دهی مینماید و بصورت InMemory درون FakeAccountTable ذخیره میشود (پر واضح است که در یک پروژهی واقعی به جای ذخیره شدن در یک Collection باید درون دیتایس واقعی ذخیره سازی شود).
و پس از اتمام عملیات انجام شده، بصورت زیر در Main برنامه اطلاعات ذخیره شده بازگردانده خواهد شد:
var accountReportReadModel = runtime.ServiceLocator.Resolve<AccountReportReadService>();
var accounts = accountReportReadModel.GetAccounts();
در ادامه برای مطالعه بیشتر میتوان به Scale out کردن این سیستم و استفاده از فریمورکهای messaging چون Redis یا Kafka پرداخت و همچنین اعمال Load Balancing را در اینگونه سیستمها انجام داد.
نکته: Cqrs-Pattern را میتوانید از
اینجا clone نمایید