61 KiB
Implementing Domain Driven Design
Introduction
This is a practical guide for implementing the Domain Driven Design (DDD). While the implementation details relies on the ABP Framework infrastructure, core concepts, principles and patterns are applicable in any kind of solution, even if it is not a .NET solution.
Goals
The goals of this document are;
- Introduce and explain the DDD architecture, concepts, principles, patterns and building blocks.
- Explain the layered architecture & solution structure offered by the ABP Framework.
- Introduce explicit rules to implement DDD patterns and best practices by giving concrete examples.
- Show what ABP Framework provides you as the infrastructure for implementing DDD in a proper way.
Simple Code!
Playing football is very simple, but playing simple football is the hardest thing there is. — Johan Cruyff
If we take this famous quote for programming, we can say;
Writing code is very simple, but writing simple code is the hardest thing there is. — ???
In this document, we will introduce simple rules those are easy to implement.
Once your application grows, it will be hard to follow these rules. Sometimes you find breaking rules will save your time in a short term. However, the saved time in the short term will bring much more time loss in the middle and long term. Your code base becomes complicated and hard to maintain. Most of the business applications are re-written just because you can't maintain it anymore.
If you follow the rules and best practices, your code base will be simpler and easier to maintain. Your application react to changes faster.
What is the Domain Driven Design?
Domain-driven design (DDD) is an approach to software development for complex needs by connecting the implementation to an evolving model;
DDD is suitable for complex domains and large-scale applications rather than simple CRUD applications. It focuses on the core domain logic rather than the infrastructure details. It helps to build a flexible, modular and maintainable code base.
OOP & SOLID
Implementing DDD highly relies on the Object Oriented Programming (OOP) and SOLID principles. Actually, it implements and extends these principles. So, a good understanding of OOP & SOLID helps you a lot while truly implementing the DDD.
DDD Layers & Clean Architecture
There are four fundamental layers of a Domain Driven Based Solution;
Business Logic places into two layers, the Domain layer and the Application Layer, while they contains different kinds of business logic;
- Domain Layer implements the core, use-case independent business logic of the domain/system.
- Application Layer implements the use cases of the application based on the domain. A use case can be thought as a user interaction on the User Interface (UI).
- Presentation Layer contains the UI elements (pages, components) of the application.
- Infrastructure Layer supports other layer by implementing the abstractions and integrations to 3rd-party library and systems.
The same layering can be shown as the diagram below and known as the Clean Architecture, or sometimes the Onion Architecture:
In the Clean Architecture, each layer only depends on the layer directly inside it. The most independent layer is shown in the most inner circle and it is the Domain Layer.
Core Building Blocks
DDD mostly focuses on the Domain & Application Layers and ignores the Presentation and Infrastructure. They are seen as details and the business layers should not depend on them.
That doesn't mean the Presentation and Infrastructure layers are not important. They are very important. UI frameworks and database providers have their own rules and best practices that you need to know and apply. However these are not in the topics of DDD.
This section introduces the essential building blocks of the Domain & Application Layers.
Domain Layer Building Blocks
- Entity: An Entity is an object with its own properties (state, data) and methods that implements the business logic that is executed on these properties. An entity is represented by its unique identifier (Id). Two entity object with different Ids are considered as different entities.
- Value Object: A Value Object is another kind of domain object that is identified by its properties rather than a unique Id. That means two Value Objects with same properties are considered as the same object. Value objects are generally implemented as immutable and mostly are much simpler than the Entities.
- Aggregate & Aggregate Root: An Aggregate is a cluster of objects (entities and value objects) bound together by an Aggregate Root object. The Aggregate Root is a specific type of an entity with some additional responsibilities.
- Repository (interface): 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). It hides the complexity of the DBMS from the business code. Domain Layer contains the
interfaces of the repositories. - Domain Service: A Domain Service is a stateless service that implements core business rules of the domain. It is useful to implement domain logic that depends on multiple aggregate (entity) type or some external services.
- Specification: A Specification is used to define named, reusable and combinable filters for entities and other business objects.
- Domain Event: A Domain Event is a way of informing other services in a loosely coupled manner, when a domain specific event occurs.
Application Layer Building Blocks
- Application Service: An Application Service is a stateless service that implements use cases of the application. An application service typically gets and returns DTOs. It is used by the Presentation Layer. It uses and coordinates the domain objects to implement the use cases. A use case is typically considered as a Unit Of Work.
- Data Transfer Object (DTO): A DTO is a simple object without any business logic that is used to transfer state (data) between the Application and Presentation Layers.
- Unit of Work (UOW): A Unit of Work is an atomic work that should be done as a transaction unit. All the operations inside a UOW should be committed on success or rolled back on a failure.
Implementation: The Big Picture
Layering of a .NET Solution
The picture below shows a Visual Studio Solution created using the ABP's application startup template:
The solution name is IssueTracking and it consists of multiple projects. The solution is layered by considering DDD principles as well as development and deployment practicals. The sub sections below explains the projects in the solution;
Your solution structure may be slightly different if you choose a different UI or Database provider. However, the Domain and Application layers will be same and this is the essential point for the DDD perspective. See the Application Startup Template document if you want to know more about the solution structure.
The Domain Layer
The Domain Layer is splitted into two projects;
IssueTracking.Domainis the essential domain layer that contains all the building blocks (entities, value objects, domain services, specifications, repository interfaces, etc.) introduced before.IssueTracking.Domain.Sharedis a thin project that contains some types those belong to the Domain Layer, but shared with all other layers. For example, it may contain some constants andenums related to the Domain Objects but need to be reused by other layers.
The Application Layer
The Application Layer is also splitted into two projects;
IssueTracking.Application.Contractscontains the application service interfaces and the DTOs used by these interfaces. This project can be shared by the client applications (including the UI).IssueTracking.Applicationis the essential application layer that implements the interfaces defined in the Contracts project.
The Presentation Layer
IssueTracking.Webis an ASP.NET Core MVC / Razor Pages application for this example. This is the only executable application that serves the application and the APIs.
ABP Framework also supports different kind of UI frameworks including Angular and Blazor. In these cases, the
IssueTracking.Webdoesn't exist in the solution. Instead, anIssueTracking.HttpApi.Hostapplication will be in the solution to serve the HTTP APIs as a standalone endpoint to be consumed by the UI applications via HTTP API calls.
The Remote Service Layer
IssueTracking.HttpApiproject contains HTTP APIs defined by the solution. It typically contains MVCControllers and related models, if available. So, you write your HTTP APIs in this project.
Most of the time, API Controllers are just wrappers around the Application Services to expose them to the remote clients. Since ABP Framework's Automatic API Controller System automatically configures and exposes your Application Services as API Controllers, you typically don't create Controllers in this project. However, the startup solution includes it for the cases you need to manually create API controllers.
IssueTracking.HttpApi.Clientproject is useful when you have a C# application that needs to consume your HTTP APIs. Once the client application references this project, it can directly inject & use the Application Services. This is possible by the help of the ABP Framework's Dynamic C# Client API Proxies System.
There is a Console Application in
testfolder the solution, namedIssueTracking.HttpApi.Client.ConsoleTestApp. It simply uses theIssueTracking.HttpApi.Clientproject to consume the APIs exposed by the application. It is just a demo application and you can safely delete it. You can even delete theIssueTracking.HttpApi.Clientproject if you think that you don't need to them.
The Infrastructure Layer
In a DDD implementation, you may have a single Infrastructure project to implement all the abstractions and integrations, or you may have different projects for each dependency.
We suggest a balanced approach; Create separate projects for main infrastructure dependencies (like Entity Framework Core) and a common infrastructure project for other infrastructure.
ABP's startup solution has two projects for the Entity Framework Core integration;
IssueTracking.EntityFrameworkCoreis the essential integration package for the EF Core. Your application'sDbContext, database mappings, implementations of the repositories and other EF Core related stuff are located here.IssueTracking.EntityFrameworkCore.DbMigrationsis a special project to manage the Code First database migrations. There is a separateDbContextin this project to track the migrations. You typically don't touch this project much except you need to create a new database migration or add an application module that has some database tables and naturally requires to create a new database migration.
You may wonder why there are two projects for the EF Core. It is mostly related to modularity. Each module has its own independent
DbContextand your application has also oneDbContext.DbMigrationsproject contains a union of the modules to track and apply a single migration path. While most of the times you don't need to know it, you can see the EF Core migrations document for more information.
Other Projects
There is one more project, IssueTracking.DbMigrator, that is a simple Console Application that migrates the database schema and seeds the initial data when you execute it. It is a useful utility application that you can use it in development as well as in production environment.
Dependencies of the Projects in the Solution
The diagram below shows the essential dependencies (project references) between the projects in the solution (IssueTracking. part is not shown to be simple)
The projects have been explained before. Now, we can explain the reasons of the dependencies;
Domain.Sharedis the project all other projects directly or indirectly depend on. So, all the types in this project are available to all projects.Domainonly depends on theDomain.Sharedbecause it is already a (shared) part of the domain. For example, anIssueTypeenum in theDomain.Sharedcan be used by anIssueentity in theDomainproject.Application.Contractsdepends on theDomain.Shared. In this way, you can reuse these types in the DTOs. For example, the sameIssueTypeenum in theDomain.Sharedcan be used by aCreateIssueDtoas a property.Applicationdepends on theApplication.Contractssince it implements the Application Service interfaces and uses the DTOs inside it. It also depends on theDomainsince the Application Services are implemented using the Domain Objects defined inside it.EntityFrameworkCoredepends on theDomainsince it maps the Domain Objects (entities and value types) to database tables (as it is an ORM) and implements the repository interfaces defined in theDomain.HttpApidepends on theApplication.Contactssince the Controllers inside it inject and use the Application Service interfaces as explained before.HttpApi.Clientdepends on theApplication.Contactssince it can consume the Application Services as explained before.Webdepends on theHttpApisince it serves the HTTP APIs defined inside it. Also, in this way, it indirectly depends on theApplication.Contactsproject to consume the Application Services in the Pages/Components.
Dashed Dependencies
When you investigate the solution, you will see two more dependencies shown with the dashed lines in the figure above. Web project depends on the Application and EntityFrameworkCore projects which theoretically should not be like that but actually it is.
This is because the Web is the final project that runs and hosts the application and the application needs to the implementations of the Application Services and the Repositories while running.
This design decision potentially allows you to use Entities and EF Core objects in the Presentation Layer which is should be strictly avoided. However, we find the alternative designs over complicated. Here, two of the alternatives if you want to remove this dependency;
- Convert
Webproject to a razor class library and create a new project, likeWeb.Host, that depends on theWeb,ApplicationandEntityFrameworkCoreprojects and hosts the application. You don't write any UI code here, but use only for hosting. - Remove
ApplicationandEntityFrameworkCoredependencies from theWebproject and load their assemblies on application initialization. You can use ABP's Plug-In Modules system for that purpose.
Execution Flow a DDD Based Application
The figure below shows a typical request flow for a web application that has been developed based on DDD patterns.
- The request typically begins by a user interaction on the UI (a use case) that causes an HTTP request to the server.
- An MVC Controller or a Razor Page Handler in the Presentation Layer (or in the Distributed Services Layer) handles the request and can perform some cross cutting concerns in this stage (Authorization, Validation, Exception Handling, etc.). A Controller/Page injects the related Application Service interface and calls its method(s) by sending and receiving DTOs.
- The Application Service use the Domain Objects (Entities, Repository interfaces, Domain Services, etc.) to implement the use case. Application Layer implements some cross cutting concerns (Authorization, Validation, etc.). An Application Service method should be a Unit Of Work. That means it should be atomic.
Most of the cross cutting concerns are automatically and conventionally implemented by the ABP Framework and you typically don't need to write code for them.
Common Principles
Before going into details, let's see some overall DDD principles;
Database Provider / ORM Independence
The domain and application layers should be ORM / Database Provider agnostic. They only depends on the Repository interfaces and the Repository interfaces doesn't use any ORM specific objects.
Here, the main reasons of this principle;
- To make your domain/application infrastructure independent since the infrastructure may change in the future or you may need to support a second database type later.
- To make your domain/application focus on the business code by hiding the infrastructure details behind the repositories.
- To make your automated tests easier since you can mock the repositories in this case.
As a respect to this principle, none of the projects in the solution has reference to the
EntityFrameworkCoreproject, except the startup application.
Discussion About the Database Independence Principle
Especially, the reason 1 deeply effects your domain object design (especially, the entity relations) and application code. Assume that you are using Entity Framework Core with a relational database. If you try to make your application so that it is possible to switch to MongoDB later, you can't use some very useful EF Core features. Examples;
- You can't assume Change Tracking since MongoDB provider can't do it. So, you always need to explicitly update the changed entities.
- You can't use Navigation Properties (or Collections) to other Aggregates in your entities since this is not possible for a Document Database. See the "Rule: Reference Other Aggregates Only By Id" section for more info.
If you think such features are important for you and you will never move away from the EF Core, we believe that it is worth relaxing this principle. We still suggest to use the repository pattern to hide the infrastructure details. But you can assume that you are using EF Core while designing your entity relations and writing your application code. You can even reference to the EF Core NuGet Package from your application layer to be able to directly use the asynchronous LINQ extension methods, like ToListAsync() (see the IQueryable & Async Operations section in the Repositories document for more info).
Presentation Technology Agnostic
The presentation technology (UI Framework) is one of the most changed parts of a real world application. It is very important to design the Domain and Application Layers to be completely unaware of the presentation technology/framework. This principle is relatively easy to implement and ABP's startup template makes it even easier.
In some cases, you may need to have duplicate logic in the application and presentation layers. For example, you may need to duplicate the validation and authorization checks in both layers. The checks in the UI layer is mostly for user experience while checks in the application and domain layers are for security and data integrity. That's perfectly normal and necessary.
Focus on the State Changes, Not Reporting
DDD focuses on how the domain objects changes and interactions; How to create an entity and change its properties by preserving the data integrity/validity and implementing the business rules.
DDD ignores reporting and mass querying. That doesn't mean they are not important. If your application doesn't have fancy dashboards and reports, who would use it? However, reporting is another topic. You typically want to use the full power of the SQL Server or even use a separate data source (like ElasticSearch) for reporting purpose. You will write optimized queries, create indexes and even stored procedures(!). You are free to do all as long as you don't mix all these into your business logic.
Implementation: The Building Blocks
This is the essential part of this guide. We will introduce and explain some explicit rules with examples. You can follow these rules and apply in your solutions while implementing the Domain Driven Design.
The Example Domain
The examples will use some concepts those are used by GitHub, like Issue, Repository, Label and User, you already familiar with. The figure below shows some of the aggregates, aggregate roots, entities, value object and the relations between them:
Issue Aggregate consists of an Issue Aggregate Root that contains Comment and IssueLabel collections. Other aggregates are shown as simple since we will focus on the Issue Aggregate:
Aggregates
As said before, an Aggregate is a cluster of objects (entities and value objects) bound together by an Aggregate Root object. This section will introduce the principles and rules related to the Aggregates.
We refer the term Entity both for Aggregate Root and sub-collection entities unless we explicitly write Aggregate Root or sub-collection entity.
Aggregate / Aggregate Root Principles
Business Rules
Entities are responsible to implement the business rules related to the properties of their own. The Aggregate Root Entities are also responsible for their sub-collection entities.
An aggregate should maintain its self integrity and validity by implementing domain rules and constraints. That means, unlike the DTOs, Entities have methods to implement some business logic. Actually, we should try to implement business rules in the entities wherever possible.
Single Unit
An aggregate is retrieved and saved as a single unit, with all the sub-collections and properties. For example, if you want to add a Comment to an Issue, you need to;
- Get the
Issuefrom database with including all the sub-collections (Comments andIssueLabels). - Use methods on the
Issueclass to add a new comment, likeIssue.AddComment(...);. - Save the
Issue(with all sub-collections) to the database as a single database operation (update).
That may seem strange to the developers used to work with EF Core & Relational Databases before. Getting the Issue with all details seems unnecessary and inefficient. Why don't we just execute an SQL Insert command to database without querying any data?
The answer is that we should implement the business rules and preserve the data consistency and integrity in the code. If we have a business rule like "Users can not comment on the locked issues", how can we check the Issue's lock state without retrieving it from the database? So, we can execute the business rules only if the related objects available in the application code.
On the other hand, MongoDB developers will find this rule very natural. In MongoDB, an aggregate object (with sub-collections) is saved in a single collection in the database (while it is distributed into several tables in a relational database). So, when you get an aggregate, all the sub-collections are already retrieved as a part of the query, without any additional configuration.
ABP Framework helps to implement this principle in your applications.
Example: Add a comment to an issue
public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IRepository<Issue, Guid> _issueRepository;
public IssueAppService(IRepository<Issue, Guid> issueRepository)
{
_issueRepository = issueRepository;
}
[Authorize]
public async Task CreateCommentAsync(CreateCommentDto input)
{
var issue = await _issueRepository.GetAsync(input.IssueId);
issue.AddComment(CurrentUser.GetId(), input.Text);
await _issueRepository.UpdateAsync(issue);
}
}
_issueRepository.GetAsync method retrieves the Issue with all details (sub-collections) as a single unit by default. While this works out of the box for MongoDB, you need to configure your aggregate details for the EF Core. But, once you configure, repositories automatically handle it. _issueRepository.GetAsync method gets an optional parameter, includeDetails, that you can pass false to disable this behavior when you need it.
See the Loading Related Entities section of the EF Core document for the configuration and alternative scenarios.
Issue.AddComment gets a userId and comment text, implements the necessary business rules and adds the comment to the Comments collection of the Issue.
Finally, we use _issueRepository.UpdateAsync to save changes to the database.
EF Core has a change tracking feature. So, you actually don't need to call
_issueRepository.UpdateAsync. It will be automatically saved thanks to ABP's Unit Of Work system that automatically callsDbContext.SaveChanges()at the end of the method. However, for MongoDB, you need to explicitly update the changed entity.So, if you want to write your code Database Provider independent, you should always call the
UpdateAsyncmethod for the changed entities.
Transaction Boundary
An aggregate is generally considered as a transaction boundary. If a use case works with a single aggregate, reads and saves it as a single unit, all the changes made to the aggregate objects are saved together as an atomic operation and you don't need to an explicit database transaction.
However, in real life, you may need to change more than one aggregate instances in a single use case and you need to use database transactions to ensure atomic update and data consistency. Because of that, ABP Framework uses an explicit database transaction for a use case (an application service method boundary). See the Unit Of Work documentation for more info.
Serializability
An aggregate (with the root entity and sub-collections) should be serializable and transferrable on the wire as a single unit. For example, MongoDB serializes the aggregate to JSON document while saving to the database and deserializes from JSON while reading from the database.
This requirement is not necessary when you use relational databases and ORMs. However, it is an important practice of Domain Driven Design.
The following rules will already bring the serializability.
Aggregate / Aggregate Root Rules & Best Practices
The following rules ensures implementing the principles introduced above.
Reference Other Aggregates Only By Id
The first rule says an Aggregate should reference to other aggregates only by their Id. That means you can not add navigation properties to other aggregates.
- This rule make possible to implement the serializability principle.
- It also prevents different aggregates manipulate each other and leaking business logic of an aggregate to one another.
You see two aggregate roots, GitRepository and Issue in the example below;
GitRepositoryshould not have a collection ofIssues since they are different aggregates.Issueshould not have a navigation property for the relatedGitRepositorysince it is a different aggregate.Issuecan haveRepositoryId(as aGuid).
So, when you have an Issue and need to have GitRepository related to this issue, you need to explicitly query it from database by the RepositoryId.
For EF Core & Relational Databases
In MongoDB, it is naturally not suitable to have such navigation properties/collections. If you do that, you find a copy of the destination aggregate object in the database collection of the source aggregate since it is being serialized to JSON on save.
However, EF Core & relational database developers may find this restrictive rule unnecessary since EF Core can handle it on database read and write. We see this an important rule that helps to reduce the complexity of the domain prevents potential problems and we strongly suggest to implement this rule. However, if you think it is practical to ignore this rule, see the Discussion About the Database Independence Principle section above.
Keep Aggregates Small
One good practice is to keep an aggregate simple and small. This is because an aggregate will be loaded and saved as a single unit and reading/writing a big object has performance problems. See the example below:
Role aggregate has a collection of UserRole value objects to track the users assigned for this role. Notice that UserRole is not another aggregate and it is not a problem for the rule Reference Other Aggregates Only By Id. However, it is a problem in practical. A role may be assigned to thousands (even millions) of users in a real life scenario and it is a significant performance problem to load thousands of items whenever you query a Role from database (remember: Aggregates are loaded by their sub-collections as a single unit).
On the other hand, User may have such a Roles collection since a user doesn't have much roles in practical and it can be useful to have a list of roles while you are working with a User Aggregate.
If you think careful, there is one more problem when Role and User both have the list of relation if you use a non-relational database, like MongoDB. In this case, the same information is duplicated in different collections and it will be hard to maintain data consistency (whenever you add an item to User.Roles, you need to add it to Role.Users too).
So, determine your aggregate boundaries and size based on the following considerations;
- Objects used together.
- Query (load/save) performance and memory consumption.
- Data integrity, validity and consistency.
In practical;
- Most of the aggregate roots will not have sub-collections.
- A sub-collection should not have more than 100-150 items inside it at the most case. If you think a collection potentially can have more items, don't define the collection as a part of the aggregate and consider to extract another aggregate root for the entity inside the collection.
Primary Keys of the Aggregate Roots / Entities
- An aggregate root typically has a single
Idproperty for its identifier (Primark Key: PK). We preferGuidas the PK of an aggregate root entity (see the Guid Genertation document to learn why). - An entity (that's not the aggregate root) in an aggregate can use a composite primary key.
For example, see the Aggregate root and the Entity below:
Organizationhas aGuididentifier (Id).OrganizationUseris a sub-collection of anOrganizationand has a composite primary key consists of theOrganizationIdandUserId.
That doesn't mean sub-collection entities should always have composite PKs. They may have single Id properties when it's needed.
Composite PKs are actually a concept of relational databases since the sub-collection entities have their own tables and needs to a PK. On the other hand, for example, in MongoDB you don't need to define PK for the sub-collection entities at all since they are stored as a part of the aggregate root.
Constructors of the Aggregate Roots / Entities
The constructor is where the lifecycle of an entity begins. There are a some responsibilities of a well designed constructor:
- Gets the required entity properties as parameters to create a valid entity. Should force to pass only for the required parameters and may get non-required properties as optional parameters.
- Checks validity of the parameters.
- Initializes sub-collections.
Example: Issue (Aggregate Root) constructor
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Volo.Abp;
using Volo.Abp.Domain.Entities;
namespace IssueTracking.Issues
{
public class Issue : AggregateRoot<Guid>
{
public Guid RepositoryId { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public Guid? AssignedUserId { get; set; }
public bool IsClosed { get; set; }
public IssueCloseReason? CloseReason { get; set; } //enum
public ICollection<IssueLabel> Labels { get; set; }
public Issue(
Guid id,
Guid repositoryId,
string title,
string text = null,
Guid? assignedUserId = null
) : base(id)
{
RepositoryId = repositoryId;
Title = Check.NotNullOrWhiteSpace(title, nameof(title));
Text = text;
AssignedUserId = assignedUserId;
Labels = new Collection<IssueLabel>();
}
private Issue() { /* for deserialization & ORMs */ }
}
}
Issueclass properly forces to create a valid entity by taking minimum necessary properties in its constructor as parameters.- The constructor validates the inputs (
Check.NotNullOrWhiteSpace(...)throwsArgumentExceptionif the given value is empty). - It initializes the sub-collections, so you don't get a null reference exception when you try to use the
Labelscollection after creating theIssue. - The constructor also takes the
idand passes to thebaseclass. We don't generateGuids inside the constructor to be able to delegate this responsibility to another service (see Guid Generation). - Private empty constructor is necessary for ORMs. We made it
privateto prevent accidently using it in our own code.
See the Entities document to learn more about creating entities with the ABP Framework.
Entity Property Accessors & Methods
The example above seems strange to you. For example, we force to pass a non-null Title in the constructor. However, the developer may then set the Title property to null without any control. This is because the example code above just focuses on the constructor.
If we declare all the properties with public setters (like the example Issue class above), we can't force validity and integrity of the entity in its lifecycle. So;
- Use private setter for a property when you need to perform any logic while setting that property.
- Define public methods to manipulate such properties.
Example: Methods to change the properties in a controlled way
using System;
using Volo.Abp;
using Volo.Abp.Domain.Entities;
namespace IssueTracking.Issues
{
public class Issue : AggregateRoot<Guid>
{
public Guid RepositoryId { get; private set; } //Never changes
public string Title { get; private set; } //Needs validation
public string Text { get; set; } //No validation
public Guid? AssignedUserId { get; set; } //No validation
public bool IsClosed { get; private set; } //Should change with CloseReason
public IssueCloseReason? CloseReason { get; private set; } //Should change with IsClosed
//...
public void SetTitle(string title)
{
Title = Check.NotNullOrWhiteSpace(title, nameof(title));
}
public void Close(IssueCloseReason reason)
{
IsClosed = true;
CloseReason = reason;
}
public void ReOpen()
{
IsClosed = false;
CloseReason = null;
}
}
}
RepositoryIdsetter made private and there is no way to change it after creating anIssuebecause this is what we want for this domain: An issue can't be moved to another repository.Titlesetter made private andSetTitlemethod has been created if you want to change it later in a controlled way.TextandAssignedUserIdhas public setter since there is no restriction on them. They can be null or any other value. We think it is unnecessary to define separate methods to set them. If we need later, we can add methods and make the setters private. Breaking changes are not problem in the domain layer since the domain layer is an internal project, it is not exposed to clients.IsClosedandIssueCloseReasonare pair properties. DefinedCloseandReOpenmethods to change them together. In this way, we prevent to close an issue without any reason.
Business Logic & Exceptions in the Entities
When you implement validation and business logic in the entities, you frequently need to manage exceptional cases. In these cases;
- Create domain specific exceptions.
- Throw these exceptions in the entity methods when necessary.
Example
public class Issue : AggregateRoot<Guid>
{
//...
public bool IsLocked { get; private set; }
public bool IsClosed { get; private set; }
public IssueCloseReason? CloseReason { get; private set; }
public void Close(IssueCloseReason reason)
{
IsClosed = true;
CloseReason = reason;
}
public void ReOpen()
{
if (IsLocked)
{
throw new IssueStateException(
"Can not open a locked issue! Unlock it first."
);
}
IsClosed = false;
CloseReason = null;
}
public void Lock()
{
if (!IsClosed)
{
throw new IssueStateException(
"Can not open a locked issue! Unlock it first."
);
}
IsLocked = true;
}
public void Unlock()
{
IsLocked = false;
}
}
There are two business rules here;
- A locked issue can not be re-opened.
- You can not lock an open issue.
Issue class throws a IssueStateException in these cases to force business rules:
using System;
namespace IssueTracking.Issues
{
public class IssueStateException : Exception
{
public IssueStateException(string message)
: base(message)
{
}
}
}
There are two potential problems of throwing such exceptions;
- In case of such an exception, should the end user see the exception (error) message? If so, how do you localize the exception message? You can not use the localization system, because you can't inject and use
IStringLocalizerin the entities. - For a web application or HTTP API, what HTTP Status Code should return to the client?
ABP's Exception Handling system solves these problems (and more).
Example: Throwing a business exception with code
using Volo.Abp;
namespace IssueTracking.Issues
{
public class IssueStateException : BusinessException
{
public IssueStateException(string code)
: base(code)
{
}
}
}
IssueStateExceptionclass inherits theBusinessExceptionclass. ABP returns 403 (forbidden) HTTP Status code by default (instead of 500 - Internal Server Error) for the exceptions derived from theBusinessException.- The
codeis used as a key in the localization resource file to find the localized message.
Now, we can change the ReOpen method as shown below:
public void ReOpen()
{
if (IsLocked)
{
throw new IssueStateException("IssueTracking:CanNotOpenLockedIssue");
}
IsClosed = false;
CloseReason = null;
}
Use constants instead of magic strings.
And add an entry to the localization resource. Example entry for the English language:
"IssueTracking:CanNotOpenLockedIssue": "Can not open a locked issue! Unlock it first."
- ABP automatically uses this localized message (based on the current language) to show to the end user when you throw the exception.
- The exception code (
IssueTracking:CanNotOpenLockedIssuehere) is also sent to the client, so it may handle the error case programmatically.
For this example, you could directly throw
BusinessExceptioninstead of defining a specializedIssueStateException. The result will be same. See the exception handling document for all the details.
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 to 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
public class Issue : AggregateRoot<Guid>
{
//...
public Guid? AssignedUserId { get; private set; }
public async Task AssignToAsync(AppUser user, IUserIssueService userIssueService)
{
var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id);
if (openIssueCount >= 3)
{
throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit");
}
AssignedUserId = user.Id;
}
public void CleanAssignment()
{
AssignedUserId = null;
}
}
AssignedUserIdproperty setter made private. So, the only way to change it to use theAssignToAsyncandCleanAssignmentmethods.AssignToAsyncgets anAppUserentity. Actually, it only uses theuser.Id, so you could get aGuidvalue, likeuserId. However, this way ensures that theGuidvalue isIdof an existing user and not a randomGuidvalue.IUserIssueServiceis an arbitrary service that is used to get open issue count for a user. It's the responsibility of the code part (that calls theAssignToAsync) to resolve theIUserIssueServiceand pass here.AssignToAsyncthrows exception if the business rule doesn't meet.- Finally, if everything is correct,
AssignedUserIdproperty is set.
This method perfectly guarantee 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 depends 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
IUserIssueServiceand pass to theAssignToAsyncmethod.
An alternative way of implementing this business logic is to introduce a Domain Service, which will be explained later.
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
DbSetfrom a repository method.DbSetis 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
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
namespace IssueTracking.Issues
{
public interface IIssueRepository : IRepository<Issue, Guid>
{
Task<List<Issue>> GetInActiveIssuesAsync();
}
}
IIssueRepository extends the standard IRepository<...> interface by adding a GetInActiveIssuesAsync method. This repository works with such an Issue class:
public class Issue : AggregateRoot<Guid>, IHasCreationTime
{
public bool IsClosed { get; private set; }
public Guid? AssignedUserId { get; private set; }
public DateTime CreationTime { get; private set; }
public DateTime? LastCommentTime { get; private set; }
//...
}
(the code shows only the properties we need for this example)
The rule says the repository shouldn't know the business rules. The question here is "What is an inactive issue? Is it a business rule definition?"
Let's see the implementation to understand it:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IssueTracking.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
namespace IssueTracking.Issues
{
public class EfCoreIssueRepository :
EfCoreRepository<IssueTrackingDbContext, Issue, Guid>,
IIssueRepository
{
public EfCoreIssueRepository(
IDbContextProvider<IssueTrackingDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<List<Issue>> GetInActiveIssuesAsync()
{
var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30));
return await DbSet.Where(i =>
//Open
!i.IsClosed &&
//Assigned to nobody
i.AssignedUserId == null &&
//Created 30+ days ago
i.CreationTime < daysAgo30 &&
//No comment or the last comment was 30+ days ago
(i.LastCommentTime == null || i.LastCommentTime < daysAgo30)
).ToListAsync();
}
}
}
(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 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:
public class Issue : AggregateRoot<Guid>, IHasCreationTime
{
public bool IsClosed { get; private set; }
public Guid? AssignedUserId { get; private set; }
public DateTime CreationTime { get; private set; }
public DateTime? LastCommentTime { get; private set; }
//...
public bool IsInActive()
{
var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30));
return
//Open
!IsClosed &&
//Assigned to nobody
AssignedUserId == null &&
//Created 30+ days ago
CreationTime < daysAgo30 &&
//No comment or the last comment was 30+ days ago
(LastCommentTime == null || LastCommentTime < daysAgo30);
}
}
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:
using System;
using System.Linq.Expressions;
using Volo.Abp.Specifications;
namespace IssueTracking.Issues
{
public class InActiveIssueSpecification : Specification<Issue>
{
public override Expression<Func<Issue, bool>> ToExpression()
{
var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30));
return i =>
//Open
!i.IsClosed &&
//Assigned to nobody
i.AssignedUserId == null &&
//Created 30+ days ago
i.CreationTime < daysAgo30 &&
//No comment or the last comment was 30+ days ago
(i.LastCommentTime == null || i.LastCommentTime < daysAgo30);
}
}
}
Specification<T> 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:
public class Issue : AggregateRoot<Guid>, IHasCreationTime
{
public bool IsClosed { get; private set; }
public Guid? AssignedUserId { get; private set; }
public DateTime CreationTime { get; private set; }
public DateTime? LastCommentTime { get; private set; }
//...
public bool IsInActive()
{
return new InActiveIssueSpecification().IsSatisfiedBy(this);
}
}
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:
public interface IIssueRepository : IRepository<Issue, Guid>
{
Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec);
}
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:
public class EfCoreIssueRepository :
EfCoreRepository<IssueTrackingDbContext, Issue, Guid>,
IIssueRepository
{
public EfCoreIssueRepository(
IDbContextProvider<IssueTrackingDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec)
{
return await DbSet
.Where(spec.ToExpression())
.ToListAsync();
}
}
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:
public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IIssueRepository _issueRepository;
public IssueAppService(IIssueRepository issueRepository)
{
_issueRepository = issueRepository;
}
public async Task DoItAsync()
{
var issues = await _issueRepository.GetIssuesAsync(
new InActiveIssueSpecification()
);
}
}
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:
public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IRepository<Issue, Guid> _issueRepository;
public IssueAppService(IRepository<Issue, Guid> issueRepository)
{
_issueRepository = issueRepository;
}
public async Task DoItAsync()
{
var issues = AsyncExecuter.ToListAsync(
_issueRepository.Where(new InActiveIssueSpecification())
);
}
}
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. See the Repositories document for more information.
Combining the Specifications
One powerful side of the Specifications is they are combinable. Assume that we've another specification that returns true only if the Issue is in a Milestone:
public class MilestoneSpecification : Specification<Issue>
{
public Guid MilestoneId { get; }
public MilestoneSpecification(Guid milestoneId)
{
MilestoneId = milestoneId;
}
public override Expression<Func<Issue, bool>> ToExpression()
{
return i => i.MilestoneId == MilestoneId;
}
}
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:
public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IRepository<Issue, Guid> _issueRepository;
public IssueAppService(IRepository<Issue, Guid> issueRepository)
{
_issueRepository = issueRepository;
}
public async Task DoItAsync(Guid milestoneId)
{
var issues = AsyncExecuter.ToListAsync(
_issueRepository
.Where(
new InActiveIssueSpecification()
.And(new MilestoneSpecification(milestoneId))
.ToExpression()
)
);
}
}
The example above uses the And extension method to combine the specifications. There are more combining methods are available, like Or(...) and AndNot(...).
See the Specifications document for more details about the specification infrastructure provided by the ABP Framework.
Domain Services
Domain Services implement domain logic that;
- 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 issue assignment has been implemented in the Issue entity:
public class Issue : AggregateRoot<Guid>
{
//...
public Guid? AssignedUserId { get; private set; }
public async Task AssignToAsync(AppUser user, IUserIssueService userIssueService)
{
var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id);
if (openIssueCount >= 3)
{
throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit");
}
AssignedUserId = user.Id;
}
public void CleanAssignment()
{
AssignedUserId = null;
}
}
Here, we will move this logic into a Domain Service.
First, changing the Issue class:
public class Issue : AggregateRoot<Guid>
{
//...
public Guid? AssignedUserId { get; internal set; }
}
- Removed the assign-related methods.
- Changed
AssignedUserIdproperty's setter fromprivatetointernal, 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.
public class IssueManager : DomainService
{
private readonly IRepository<Issue, Guid> _issueRepository;
public IssueManager(IRepository<Issue, Guid> issueRepository)
{
_issueRepository = issueRepository;
}
public async Task AssignToAsync(Issue issue, AppUser user)
{
var openIssueCount = await _issueRepository.CountAsync(
i => i.AssignedUserId == user.Id && !i.IsClosed
);
if (openIssueCount >= 3)
{
throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit");
}
issue.AssignedUserId = user.Id;
}
}
IssueManager can inject any service dependency and use to query open issue count on the user.
We prefer and suggest to use the
Managersuffix for the Domain Services.
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
IssueManagersince 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.
Application Services
An Application Service is a stateless service that implements use cases of the application. An application service typically gets and returns DTOs. It is used by the Presentation Layer. It uses and coordinates the domain objects (entities, repositories, etc.) to implement use cases.
Common principles of an application service are;
- Implement the application logic that is specific to the current use case. Do not implement the core domain logic inside the application services. We will come back to differences between Application Domain logics.
- Never get or return entities for an application service method. This breaks the encapsulation of the Domain Layer. Always get and return DTOs.
Example: Assign an Issue to a User
using System;
using System.Threading.Tasks;
using IssueTracking.Users;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace IssueTracking.Issues
{
public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IssueManager _issueManager;
private readonly IRepository<Issue, Guid> _issueRepository;
private readonly IRepository<AppUser, Guid> _userRepository;
public IssueAppService(
IssueManager issueManager,
IRepository<Issue, Guid> issueRepository,
IRepository<AppUser, Guid> userRepository)
{
_issueManager = issueManager;
_issueRepository = issueRepository;
_userRepository = userRepository;
}
[Authorize]
public async Task AssignAsync(IssueAssignDto input)
{
var issue = await _issueRepository.GetAsync(input.IssueId);
var user = await _userRepository.GetAsync(input.UserId);
await _issueManager.AssignToAsync(issue, user);
await _issueRepository.UpdateAsync(issue);
}
}
}
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.
The last Update is not necessary if your are using EF Core since it has a Change Tracking system. If you want to take advantage of this EF Core feature, please see the Discussion About the Database Independence Principle section above.
IssueAssignDto in this example is a simple DTO class:
using System;
namespace IssueTracking.Issues
{
public class IssueAssignDto
{
public Guid IssueId { get; set; }
public Guid UserId { get; set; }
}
}
Data Transfer Objects
TODO









