<ABP framework> domain services, and domain services
Document directory
Content of this section:
- Introduction
- Example
- Create an Interface
- Implementation Service
- Use application services
- Discussion
- Why not just use application services?
- How to force you to use domain services?
Introduction
Domain services (or services) are used to perform domain operations and business rules. Eric Evans describes that a good service requires three features (in his DDD book ):
Unlike application services that obtain/return DTO (data transmission objects), domain services obtain/return domain objects (such as objects or value types ).
Domain services can be called by application services or other domain services, but are not directly used by the presentation layer (the application service is for it.
IDomainService interface and DomainService
ABP defines the IDomainService interface, which is implemented by all domain services as agreed. After implementation, domain services are automatically registered to the dependency injection system temporarily.
Similarly, domain services (at Will) can be inherited from the DomainService class, so they can use the inherited log, localization, and other attributes. Even if you do not inherit the DomainService class, you can inject it as needed.
Example
Suppose we have a task management system with business rules assigned to people.
Create an Interface
First, we define an interface for the service (not required, but a good practice ):
public interface ITaskManager : IDomainService{ void AssignTaskToPerson(Task task, Person person);}
As you can see, the TaskManager service uses a domain object: a Task and a Person. Some naming conventions for domain services can be named TaskManager, TaskService, or TaskDomainService...
Implementation Service
Let's take a look at the implementation:
public class TaskManager : DomainService, ITaskManager{ public const int MaxActiveTaskCountForAPerson = 3; private readonly ITaskRepository _taskRepository; public TaskManager(ITaskRepository taskRepository) { _taskRepository = taskRepository; } public void AssignTaskToPerson(Task task, Person person) { if (task.AssignedPersonId == person.Id) { return; } if (task.State != TaskState.Active) { throw new ApplicationException("Can not assign a task to a person when task is not active!"); } if (HasPersonMaximumAssignedTask(person)) { throw new UserFriendlyException(L("MaxPersonTaskLimitMessage", person.Name)); } task.AssignedPersonId = person.Id; } private bool HasPersonMaximumAssignedTask(Person person) { var assignedTaskCount = _taskRepository.Count(t => t.State == TaskState.Active && t.AssignedPersonId == person.Id); return assignedTaskCount >= MaxActiveTaskCountForAPerson; }}
Here we have two business rules:
- A Task must be active before it can be assigned to a new Person.
- One Person can be assigned up to three active tasks.
You may want to know why I threw an ApplicationException In the first check and UserFriendlyException In the second check (see Exception Handling), which is irrelevant to domain services, this is just an example. It depends on you. I think the user interface must check the status of a Task and should not be assigned to a Person. I think this is an application error and should be hidden from the user. The second is the check on the UI. We display a readable error message to the user.
Use application services
Now let's take a look at how the next Application Service uses the TaskManager service:
public class TaskAppService : ApplicationService, ITaskAppService{ private readonly IRepository<Task, long> _taskRepository; private readonly IRepository<Person> _personRepository; private readonly ITaskManager _taskManager; public TaskAppService(IRepository<Task, long> taskRepository, IRepository<Person> personRepository, ITaskManager taskManager) { _taskRepository = taskRepository; _personRepository = personRepository; _taskManager = taskManager; } public void AssignTaskToPerson(AssignTaskToPersonInput input) { var task = _taskRepository.Get(input.TaskId); var person = _personRepository.Get(input.PersonId); _taskManager.AssignTaskToPerson(task, person); }}
TaskApplicationService uses the given DTO (input) and warehouse to obtain related tasks and persons, and then passes them to TaskManager (domain service ).
Discussion
Based on the above example, you may want to ask some questions.
Why not only use application services??
That is to say, why does the application service not implement the business logic in the domain service?
We can simply say that it is not an application service task, because it is not a use case, but a business operation. In another use case, we may use the same domain logic of "assigning tasks to people". For example, we update tasks in some way on another interface, this update includes assigning tasks to another person. We may have two different UIS (one mobile application and one Web application) sharing the same domain; or a Web Api that provides tasks for remote clients.
If your domain is very simple, there is only one UI and only one place to use the operations assigned to the personnel, you may want to skip the domain service and directly implement the business logic in your application service, this is not the best practice of DDD, but it is not mandatory for you to use this design.
How to force you to use domain services?
The Application Service can simply do this:
public void AssignTaskToPerson(AssignTaskToPersonInput input){ var task = _taskRepository.Get(input.TaskId); task.AssignedPersonId = input.PersonId;}
Developers who write application services may not know the existence of TaskManager. They directly assign the given PersonId to AssignedPersonId. So how can they prevent this?
There are many discussions about this issue and some usage models in the DDD field. We are not prepared to go into depth, but we provide a simple method:
Modify the Task object as follows:
public class Task : Entity<long>{ public virtual int? AssignedPersonId { get; protected set; } //...other members and codes of Task entity public void AssignToPerson(Person person, ITaskPolicy taskPolicy) { taskPolicy.CheckIfCanAssignTaskToPerson(this, person); AssignedPersonId = person.Id; }}
Modify AssignedPersonId to protected, so it cannot be modified outside the object class. Add an AssignToPerson method and accept a Person and a TaskPolicy. The CheckIfCanAssignTaskToPerson method checks whether the resource can be allocated and throws a corresponding exception when the resource cannot be allocated (how to implement the exception here is not important ). Then the application service is shown as follows:
public void AssignTaskToPerson(AssignTaskToPersonInput input){ var task = _taskRepository.Get(input.TaskId); var person = _personRepository.Get(input.PersonId); task.AssignToPerson(person, _taskPolicy);}
We inject _ taskPolicy into ITaskPolicy and pass it to the AssignToPerson method. So far, there is no second way to assign a task to a single employee. We can only use AssignToPerson, but can no longer skip the business rules.