DEV Community

Salah Elhossiny
Salah Elhossiny

Posted on

Implementing Domain Driven Design: Part III

Business Logic in Entities Requiring External Services

It is simple to implement a business rule in an entity method when the business logic only uses the properties of that entity.

What if the business logic requires to query database or use any external services that should be resolved from the dependency injection system.

Remember; Entities can not inject services!

There are two common ways of implementing such a business logic:

  • Implement the business logic on an entity method and get external dependencies as parameters of the method.

  • Create a Domain Service.

Domain Services will be explained later. But, now let's see how it can be implemented in the entity class.

Example: Business Rule: Can not assign more than 3 open issues to a user concurrently

image

  • AssignedUserId property setter made private. So, the only way to change it to use the AssignToAsync and CleanAssignment methods.

  • AssignToAsync gets an AppUser entity. Actually, it only uses the user.Id, so you could get a Guid value, like userId. However, this way ensures that the Guid value is Id of an existing user and not a random Guid value.

  • IUserIssueService is an arbitrary service that is used to get open issue count for a user. It's the responsibility of the code part (that calls the AssignToAsync) to resolve the IUserIssueService and pass here.

  • AssignToAsync throws exception if the business rule doesn't meet.

  • Finally, if everything is correct, AssignedUserId property is set.

This method perfectly guarantees to apply the business logic when you want to assign an issue to a user. However, it has some problems:

  • It makes the entity class depending on an external service which makes the entity complicated.

  • It makes hard to use the entity. The code that uses the entity now needs to inject IUserIssueService and pass to the AssignToAsync method.

Repositories

A Repository is a collection-like interface that is used by the Domain and Application Layers to access to the data persistence system (the database) to read and write the Business Objects, generally the Aggregates.

Common Repository principles are:

  • Define a repository interface in the Domain Layer (because it is used in the Domain and Application Layers), implement in the Infrastructure Layer (EntityFrameworkCore project in the startup template).

  • Do not include business logic inside the repositories.

  • Repository interface should be database provider / ORM independent. For example, do not return a DbSet from a repository method. DbSet is an object provided by the EF Core.

  • Create repositories for aggregate roots, not for all entities. Because, sub-collection entities (of an aggregate) should be accessed over the aggregate root.

Do Not Include Domain Logic in Repositories

While this rule seems obvious at the beginning, it is easy to leak business logic into repositories.

Example: Get inactive issues from a repository

image

IIssueRepository extends the standard IRepository<...> interface by adding a GetInActiveIssuesAsync method. This repository works with such an Issue class:

Let's see the implementation to understand it:

image

(Used EF Core for the implementation. See the EF Core integration document to learn how to create custom repositories with the EF Core.)

When we check the GetInActiveIssuesAsync implementation, we see a business rule that defines an in-active issue: The issue should be open, assigned to nobody, created 30+ days ago and has no comment in the last 30 days.

This is an implicit definition of a business rule that is hidden inside a repository method. The problem occurs when we need to reuse this business logic.

For example, let's say that we want to add an bool IsInActive() method on the Issue entity. In this way, we can check activeness when we have an issue entity.

Let's see the implementation:

image

We had to copy/paste/modify the code. What if the definition of the activeness changes? We should not forget to update both
places. This is a duplication of a business logic, which is pretty dangerous.

A good solution to this problem is the Specification Pattern!

Specifications

A specification is a named, reusable, combinable and testable class to filter the Domain Objects based on the business rules. ABP Framework provides necessary infrastructure to easily create specification classes and use them inside your application code.

Let's implement the in-active issue filter as a specification class:

image

Specification base class simplifies to create a specification class by defining an expression. Just moved the expression here, from the repository. Now, we can re-use the InActiveIssueSpecification in the Issue entity and EfCoreIssueRepository classes.

Using within the Entity

Specification class provides an IsSatisfiedBy method that returns true if the given object (entity) satisfies the specification. We can re-write the Issue.IsInActive method as shown below:

image

Just created a new instance of the InActiveIssueSpecification and used its IsSatisfiedBy method to re-use the expression defined by the specification.

Using with the Repositories

First, starting from the repository interface:

image

Renamed GetInActiveIssuesAsync to simple GetIssuesAsync by taking a specification object. Since the specification (the filter) has been moved out of the repository, we no longer need to create different methods to get issues with different conditions (like GetAssignedIssues(...), GetLockedIssues(...), etc.)

Updated implementation of the repository can be like that:

image

Since ToExpression() method returns an expression, it can be directly passed to the Where method to filter the entities.

Finally, we can pass any Specification instance to the GetIssuesAsync method:

image

With Default Repositories

Actually, you don't have to create custom repositories to be able to use specifications.

The standard IRepository already extends the IQueryable, so you can use the standard LINQ extension methods over it:

image

AsyncExecuter is a utility provided by the ABP Framework to use asynchronous LINQ extension methods (like ToListAsync here) without depending on the EF Core NuGet package.

Combining the Specifications

One powerful side of the Specifications is they are combinable.

Assume that we have another specification that returns true only if the Issue is in a Milestone:

image

This Specification is parametric as a difference from the InActiveIssueSpecification. We can combine both specifications to get a list of inactive issues in a specific milestone:

image

The example above uses the And extension method to combine the specifications. There are more combining methods are available, like Or(...) and AndNot(...).

Domain Services

Domain Services implement domain logic which:

  • Depends on services and repositories.
  • Needs to work with multiple aggregates, so the logic doesn't properly fit in any of the aggregates.

Domain Services work with Domain Objects. Their methods can get and return entities, value objects, primitive types... etc.

However, they don't get/return DTOs. DTOs is a part of the Application Layer.

Example: Assigning an issue to a user

Remember how an issue assignment has been implemented in the Issue entity:

image

Here, we will move this logic into a Domain Service.

First, changing the Issue class:

image

  • Removed the assign-related methods.

  • Changed AssignedUserId property's setter from private to internal, to allow to set it from the Domain Service.

The next step is to create a domain service, named IssueManager, that has AssignToAsync to assign the given issue to the given user.

image

IssueManager can inject any service dependency and use to query open issue count on the user.

The only problem of this design is that Issue.AssignedUserId is now open to set out of the class. However, it is not public.

It is internal and changing it is possible only inside the same Assembly, the IssueTracking.Domain project for this example solution. We think this is reasonable

  • Domain Layer developers are already aware of domain rules and they use the IssueManager.

  • Application Layer developers are already forces to use the IssueManager since they don't directly set it

While there is a tradeoff between two approaches, we prefer to create Domain Services when the business logic requires to work with external services.

Example: Assigning an issue to a user

image

An application service method typically has three steps those
are implemented here;

  • Get the related domain objects from database to implement the use case.
  • Use domain objects (domain services, entities, etc.) to perform the actual operation.

  • Update the changed entities in the database.

Data transfer Objects

A DTO is a simple object that is used to transfer state (data) between the Application and Presentation Layers.

So, Application Service methods gets and returns DTOs.

Common DTO Principles & Best Practices

  • A DTO should be serializable, by its nature. Because, most of the time it is transferred over network. So, it should have a parameterless (empty) constructor.

  • Should not contain any business logic.

  • Never inherit from or reference to entities.

Input DTOs (those are passed to the Application Service methods) have different natures than Output DTOs (those are returned from the Application Service methods). So, they will be treated differently.

Input DTO Best Practices

Do not Define Unused Properties for Input DTOs

Define only the properties needed for the use case! Otherwise, it will be confusing for the clients to use the Application Service method. You can surely define optional properties, but they should effect how the use case is working, when the client provides them.

This rule seems unnecessary first. Who would define unused parameters (input DTO properties) for a method? But it happens, especially when you try to reuse input DTOs.

Do not Re-Use Input DTOs

Define a specialized input DTO for each use case (Application Service method). Otherwise, some properties are not used in some cases and this violates the rule defined above: Do not

Define Unused Properties for Input DTOs.

Sometimes, it seems appealing to reuse the same DTO class for two use cases, because they are almost same. Even if they are same now, they will probably become different by the time and you will come to the same problem. Code duplication is a better practice than coupling use cases.

Another way of reusing input DTOs is inheriting DTOs from
each other. While this can be useful in some rare cases, most of the time it brings you to the same point.

image

IUserAppService uses UserDto as the input DTO in all methods (use cases). UserDto is defined below:

image

For this example:

  • Id is not used in Create since the server determines it.

  • Password is not used in Update since we have another method for it.

  • CreationTime is never used since we can't allow client to send the Creation Time. It should be set in the server.

A true implementation can be like that:

image

With the given input DTO classes:

image

Input DTO Validation Logic

  • Implement only formal validation inside the DTO. Use Data Annotation Validation Attributes or implement IValidatableObject for formal validation.

  • Do not perform domain validation. For example, don't try to check unique username constraint in the DTOs.

Example: Using Data Annotation Attributes

image

ABP Framework automatically validates input DTOs, throws AbpValidationException and returns HTTP Status 400 to the client in case of an invalid input.

Output DTO Best Practices

  • Keep output DTO count minimum. Reuse where possible (exception: Do not reuse input DTOs as output DTOs).

  • Output DTOs can contain more properties than used in the client code.

  • Return entity DTO from Create and Update methods.

Example: Returning Different DTOs from different methods

image

The example code above returns different DTO types for each
method. As you can guess, there will be a lot of code
duplications for querying data, mapping entities to DTOs.

The IUserAppService service above can be simplified:

image

With a single output DTO:

image

  • Removed GetUserNameAndEmail and GetRoles since Get method already returns the necessary information.

  • GetList now returns the same with Get.

  • Create and Update also returns the same UserDto.

Discussion (0)