Skip to content

Domain Services

  • Do create an domain service for each aggregate root.

Domain Service Interface

  • Do define an interface for each aggregate in the domain layer.
  • Do inherit from the IDomainService interface.
  • Do use the Manager postfix for the interface name (ex: IProjectManager).
  • Do order methods according to C / R / U / D
  • Do create or return entities.
  • Do not avoid to generate domain services for only create, read, update and delete operations.
public interface IProjectManager : IDomainService
{
    Task<Project> CreateAsync(
        string name,
        string description,
        Guid baseModelId,
        Priority priority,
        DateTime startDate
    );
}

Domain Service Implementation

The core business logic is generally implemented in aggregates (entities) and the Domain Services. Creating a Domain Service is especially needed when;

  • You implement a core domain logic that depends on some services (like repositories or other external services).

  • The logic you need to implement is related to more than one aggregate/entity, so it doesn't properly fit in any of the aggregates.

  • Do use virtual access modifier business rules in domain layer.

Creating A New Entity

  • Do use the CreateAsync method name.
  • Do not have id parameter at CreateAsync method.
    • Exception: If it is desired to establish a correlation with an external entity as per the business rule, id can be added as a parameter.
  • Do get and return the domain objects(entities, value objects)
  • Do use domain services at application services
  • Do not inject base services like ILogger and IGuidGenerator.
  • Do not implement the use cases of the application (user interactions in a typical web application).
public class ProjectManager : DomainService, IProjectManager
{
    protected IProjectRepository ProjectRepository { get; }

    protected IBaseModelRepository BaseModelRepository { get; }

    public ProjectManager(
        IProjectRepository projectRepository,
        IBaseModelRepository baseModelRepository)
    {
        ProjectRepository = projectRepository;
        BaseModelRepository = baseModelRepository;
    }

    public virtual async Task<Project> CreateAsync(
        string name,
        string description,
        Guid baseModelId,  // This is required value while creating the project object.
        Priority priority,
        DateTime startDate)
    {
        ProjectValidator.CheckEmptyGuid(baseModelId);
        ProjectValidator.CheckEdges(edges);

        var baseModel = await BaseModelRepository.FindAsync( // related with another aggregate named `BaseModel`
            id: baseModelId,
            includeDetails: false
        );

        if (baseModel == null)
        {
            throw new BaseModelDoesNotExistException();
        }

        var project = new Project(
            id: GuidGenerator.Create(),
            name: name,
            description: description,
            baseModelId: baseModelId,
            priority: priority,
            startDate: startDate,
            tenantId: CurrentTenant.Id
        );

        return project;
    }
  • Do use the Check{EntityName}Async method name for existance checks.
protected virtual async Task CheckBaseModelAsync(
    Guid modelId,
    [NotNull] string name,
    [NotNull] string providerName)
{
    using (DataFilter.Disable<IRequireOrganizationUnitAccessControl>())
    {
        if (await BaseModelRepository.ExistsAsync(modelId: modelId, name: name, providerName: providerName))
        {
            throw new BaseModelExistException();
        }
    }
}

Updating An Existing Entity

  • Do not created dedicated the UpdateAsync method, most of the cases this should be handled at application layer.

Business Methods

  • Do use entity always as first parameter.
  • Do make only the described change in method name

Example method:

public virtual async Task<Project> LinkPredecessorAsync(
    [NotNull] Project project,
    List<Guid> predecessorIds)
{
    foreach (var predecessorId in predecessorIds)
    {
        // Predecessor project should be part of existing the project
        if (!(project.SuccessorIds.Contains(predecessorId) & (project.PredecessorIds.Contains(predecessorId))))
        {
            var predecessorProject = await GetProjectInternalAsync(predecessorId);
            if (predecessorProject.BaseModelId == project.BaseModelId)
            {
                project.AddPredecessorRecord(predecessorId); // Rich entity has logic to handle add operation of predecessorId
            }
        }
    }

    return project;
}

Deleting An Existing Entity

  • Do not create dedicated DeleteAsync method, most of the cases this should be handled at application layer.

Distributed Caching

  • Do use the CacheItem postfix for the class name (ex: ProjectCacheItem).
  • Do inherit from the ExtensibleObject class if entity is derived from AggregateRoot.
  • Do decorate with [Serializable] attribute.
[Serializable]
public class ProjectCacheItem : ExtensibleObject
{
    public string Name { get; set; }

    public string Description { get; set; }

    public Guid BaseModelId { get; set; }

    public Priority Priority { get; set; }

    public string CurrentState { get; set; }

    public DateTime StartDate { get; set; }
}