I recently started a new project using DDD Architecture. I have 2 separate database for same bounded context. I know that different bounded context speak to each other via message queuing. But as I have query and command functionality in same bounded context I don't know how to sync both read model and write model (In same bounded context).
I used NH for write (Command) side and EF for read (Query) side. I Used Unit Of Work for NH and decorated all my commands with Unit Of Work and made them transactional per aggregate:
public class NhUnitOfWork : IUnitOfWork
{
private readonly ISession _session;
public NhUnitOfWork(ISession session)
{
_session = session;
}
public void Begin()
{
_session.Transaction.Begin(IsolationLevel.ReadCommitted);
}
public void Commit()
{
_session.Transaction.Commit();
}
public void Rollback()
{
_session.Transaction.Rollback();
}
}
and transaction decorator:
public class TransactionalCommandHandlerDecorator<T>:ICommandHandler<T>
{
private ICommandHandler<T> _commandHandler;
private IUnitOfWork _unitOfWork;
public TransactionalCommandHandlerDecorator(ICommandHandler<T> commandHandler, IUnitOfWork unitOfWork)
{
_commandHandler = commandHandler;
_unitOfWork = unitOfWork;
}
public void Handle(T command)
{
_unitOfWork.Begin();
try
{
_commandHandler.Handle(command);
_unitOfWork.Commit();
}
catch (Exception exp)
{
_unitOfWork.Rollback();
throw;
}
}
}
So in application layer, in creation command I must orchestrate flow of business:
public class CategoryCommandHandlers:ICommandHandler<CreateCategoryCommand>
{
private readonly ICategoryRepository _repository;
private readonly ICategoryQueryService _queryService;
public CategoryCommandHandlers(ICategoryRepository repository, ICategoryQueryService queryService)
{
_repository = repository;
_queryService = queryService;
}
public void Handle(CreateCategoryCommand command)
{
var categoryId = new CategoryId(Guid.NewGuid());
var parentId=new CategoryId(command.ParentId);
var category=new Category(categoryId,command.Name,parentId);
var parent = _repository.GetById(parentId);
if (parent==null)
throw new ParentCategoryNotFoundException();
_repository.Create(category);
var queryModel = new CategoryQuery(categoryId.Value, category.Name, parentId.Value);
_queryService.Create(queryModel);
}
}
But I don't know what is the problem. everything runs without a single error but I have read model saved into Query database and write model dosent.
If i should have separate transactional decorator for read model how can I be sure about running both transactions correctly without error? or rollback if one caught an error?
Or maybe the command is completely wrong and I don't know how to handle and sync both read and write!
I don't know enough about NH, EF, and transaction management, so this will be a partial answer, trying to address your question on having separate transactions for write and read.
Typically, you would want to separate the query model construction mechanism from the command transaction for a couple of reasons:
a. Query models tend to be bulky
b. There may be more than one query model constructed based on requirements, often dictated by different UI screens or view models
Second, to ensure that the transaction does not fail during query model persistence, you do all the validation up front within the command model transaction. Query Model construction should be an elementary gather-and-serialize type of activity, that produces a document in the format you want. It should not involve validations.
There is nothing wrong with using the same message queuing mechanism to run the query model construction as an async job. Often, Query models are one of the many things generated after a command has been processed. A good mechanism is to raise an Event at the end of command processing and construct the read model in response to the event.
If the query model construction does fail because of a system error (disk full, deadlock, etc.), you can depend on your message queuing mechanisms to track and handle exceptions and failed transactions. (If you have not implemented a mechanism to retry/handle message processing failures, you should preferably add it).
This mechanism assumes that you are not packaging the query model in response to the command in the application layer. If you are doing that, you should avoid it. Assume and act as if commands never return any data after processing.