The following patterns are known to describe business solutions.
- Value Object
- Entity
- Aggregate Root
- Repository
- Use Case
- Bounded Context
- Entity Factory
- Domain Service
- Application Service
Encapsulate the tiny domain business rules. Structures that are unique by their properties and the whole object is immutable, once it is created its state can not change.
public readonly struct Name
{
private readonly string _text;
public Name(string text)
{
if (string.IsNullOrWhiteSpace(text))
throw new NameShouldNotBeEmptyException("The 'Name' field is required");
_text = text;
}
public override string ToString()
{
return _text;
}
}
Rules of thumb:
- The developer should make the Value Object serializable and deserializable.
- A Value Object can not reference an Entity or another mutable object.
Highly abstract and mutable objects unique identified by its IDs.
Rules of Thumb:
- Entities are mutable.
- Entities are highly abstract.
- Entities do not need to be serializable.
- The entity state should be encapsulated to external access.
Similar to Entities with the addition that Aggregate Root are responsible to keep the graph of objects consistent.
- Owns entities object graph.
- Ensure the child entities state are always consistent.
- Define the transaction scope.
public class Task : IAggregateRoot
{
public TaskId TaskId { get; set; }
public Summary Summary { get; set; }
public Description Description { get; set; }
}
Rules of thumb:
- Protect business invariants inside Aggregate boundaries.
- Design small Aggregates.
- Reference other Aggregates by identity only.
- Update other Aggregates using eventual consistency. (Event Sourcing)
Provides persistence capabilities to Aggregate Roots.
public class TaskRepository : ITaskRepository
{
private readonly ITaskFactory _taskFactory;
public TaskRepository(ITaskFactory taskFactory)
{
_taskFactory = taskFactory;
}
public Task<Domain.Tasks.Task> Add(Domain.Tasks.Task taskEntity)
{
//Your database access implementation
}
public Task<List<Domain.Tasks.Task>> FindAll()
{
//Your database access implementation
return tasks;
}
public Task<Domain.Tasks.Task> FindById(Guid id)
{
//Your database access implementation
}
public System.Threading.Tasks.Task Remove(Guid id)
{
//Your database access implementation
}
}
Rules of thumb:
- The repository is designed around the aggregate root.
- A repository for every entity is a code smell.
It is the application entry point for an user interaction. It accepts an input message, executes the algorithm then it should give the output message to the Output port.
public class TaskService : ITaskService
{
private readonly ITaskRepository _taskRepository;
private readonly ITaskFactory _taskFactory;
private readonly TaskViewModelMapper _taskViewModelMapper;
private readonly ITracer _tracer;
private readonly IMediator _mediator;
public TaskService(ITaskRepository taskRepository, TaskViewModelMapper taskViewModelMapper, ITracer tracer, ITaskFactory taskFactory, IMediator mediator)
{
_taskRepository = taskRepository;
_taskViewModelMapper = taskViewModelMapper;
_tracer = tracer;
_taskFactory = taskFactory;
_mediator = mediator;
}
public async Task<TaskViewModel> Create(TaskViewModel taskViewModel)
{
using(var scope = _tracer.BuildSpan("Create_TaskService").StartActive(true))
{
var newTaskCommand = _taskViewModelMapper.ConvertToNewTaskCommand(taskViewModel);
var taskResult = await _mediator.SendAsync<Domain.Tasks.Task>(newTaskCommand);
return _taskViewModelMapper.ConstructFromEntity(taskResult);
}
}
public async System.Threading.Tasks.Task Delete(Guid id)
{
using (var scope = _tracer.BuildSpan("Delete_TaskService").StartActive(true))
{
var deleteTaskCommand = _taskViewModelMapper.ConvertToDeleteTaskCommand(id);
await _mediator.PublishAsync(deleteTaskCommand);
}
}
public async Task<IEnumerable<TaskViewModel>> GetAll()
{
using (var scope = _tracer.BuildSpan("GetAll_TaskService").StartActive(true))
{
var tasksEntities = await _taskRepository.FindAll();
return _taskViewModelMapper.ConstructFromListOfEntities(tasksEntities);
}
}
public async Task<TaskViewModel> GetById(Guid id)
{
using (var scope = _tracer.BuildSpan("GetById_TaskService").StartActive(true))
{
var taskEntity = await _taskRepository.FindById(id);
return _taskViewModelMapper.ConstructFromEntity(taskEntity);
}
}
}
Rules of thumb:
- The use case implementation are close to a human readable language. (Get all, get by ID, delete, create, etc)
- Invokes transaction operations (eg. Unit Of Work).
It is a logical boundary, similar to a module in a system. In this project the single Domain project is the single bounded context we designed.
Creates new instances of Entities and Aggregate Roots. Should be implemented by the Infrastructure layer.
public class EntityFactory : ITaskFactory
{
public Domain.Tasks.Task CreateTaskInstance(Summary summary, Description description)
{
return new TaskFactory(summary, description);
}
}