Think with Enlab

Diving deep into the ocean of technology

Stay Connected. No spam!

Domain-Driven Design in ASP.NET Core applications

 

The technology industry has been thriving for the second half of the last century. It's still a critical industry in most countries worldwide, from developed to developing ones. In many cases, it’s the main criteria to assess the development and dynamism of a nation. Yet, leaving aside the flashiness of this industry, let’s dig deeper into its reality. When newer technologies are being developed daily, it produces plenty of young and immature systems. The question is, how do software engineers thrive in this fast-changing environment and create systems that withstand time as much as possible?

So far, many architectures have been invented to reduce software costs for clients and increase the software lifespan itself. It spans from multi-layered to multi-tiered ones, followed by a Domain Oriented Architecture. All that said, I want to mention a concept that is not new, but it is considered an effective approach for building most web applications these days. It is Domain-Driven Design (a.k.a DDD). Its combination, improvement, and outstanding development overcome shortcomings of popular old models, such as N-Tiers, MVC, MVP, MVVM.

In this blog post, I will explore some basic concepts of the DDD pattern and provide a practical example of how to apply DDD effectively in a typical .NET Core project.

 

What is Domain-Driven Design, and why do we need it?

In 2003, Eric Evan published the first book entitled "Tackling Complexity in the Heart of Software", which brought the first concepts to DDD. The pattern's popularity has exponentially increased since then. Many software development teams, businesses, or organizations have applied this model and achieved great success in software development. Several tech giants like Microsoft is not out of this trend, and they mentioned Domain-Driven Design as follows:

“Domain-driven design (DDD) advocates modeling based on the reality of business as relevant to your use cases. In the context of building applications, DDD talks about problems as domains. It describes independent problem areas as Bounded Contexts (each Bounded Context correlates to a microservice) and emphasizes a common language to talk about these problems. It also suggests many technical concepts and patterns, like domain entities with rich models (no anemic-domain model), value objects, aggregates, and aggregate root (or root entity) rules to support the internal implementation….”

DDD is an approach to business-focused software development. Problems and challenges that happen during software development and maintenance mostly come from the constant growth of the business. Thus, DDD helps closely connect the software development and the business model's evolution.

DDD helps to solve the problem of building complex systems. This pattern requires architects, developers, and domain experts to understand precisely the requirements first. Then, they define behaviors, understand rules, apply principles and business logic into the set of clauses (Abstractions, Interfaces, and so on). Next, engineers will implement them in other layers (e.g., Application Layer, Infrastructure layer). Nowadays, DDD is set as a standard to develop different popular architectures, such as Onion Architecture, Clean Architecture, Hexagonal Architecture, etc.

Before diving into the details, we will explain some advantages and disadvantages of Domain-Driven Design to assist you in understanding whether this model fits your project well or not.

 

Advantages and disadvantages of Domain-Driven Design

Advantages of DDD

  • Loose coupling: The parts of the system will interact with each other through the definitions and principles laid down in the Core layer (interfaces, abstract classes, base classes, etc.). Implementations will be completed in the remaining layers. Setting up the implementation will be through DI (IoC, AutoFac) libraries. Therefore, teams can develop independently at the same time.
  • Flexibility: The loose links and high-level definitions allow the team to enhance and adapt to new functional requirements more flexibly without considerable impact on the overall system.
  • Testability: As mentioned above, separating the implementation from the interfaces defined in the Core layer, testing with mock data in a separate environment is allowed.
  • Maintenance: DDD clearly divides functions among layers/tiers. Specifically, the Domain implements business logic, Infrastructure is in charge of data persistence, and the Application handles API and integration logic. Following this approach ultimately gives you chances to write cleaner and more reliable codes. Plus, your team can easily find code, limit its duplication and reduce maintenance time.

Disadvantages of DDD

  • Domain expertise: DDD requires extensive domain expertise. It means that your team needs to have at least one domain expert. They will help you define all of the processes, procedures, and terminology of that domain.
  • Low interactions: The loose connection among different parts requires the team to communicate and exchange regularly. So before applying the DDD approach, the team needs to discuss its principles in detail first.
  • Development costs: Domain experts and the team have to implement a great deal of isolation and encapsulation within the domain model. This often results in a more extended development and duration that can come at a relatively high cost. Therefore, it is not well-suited for short-term projects or projects without a high domain complexity.

Layers in DDD

The architecture of DDD projects usually includes three main parts: Domain, Infrastructure, Application. Depending on the size of each project, we can arrange these parts in a project or separate them into different layers.

  • Domain: A place to define logic concepts, principles, patterns, and behaviors of data, including domain validation, calculations, and expressions for system operations.
    • Entities: POCO classes, construction, and model validation.
    • Aggregate: The rules, computation, logic of domains, and related objects when updating the domain. According to Martin Fowler, an aggregate is a cluster of domain objects that can be treated as a single unit.
    • Value objects: The value of an object related to Domain entities. In principle, ValueObjects have no identity, and once been initialized, will not be modified. They can be understood as immutable classes.
    • Interfaces: They help define business behaviors, etc. Other layers will be responsible for implementing these definitions.
    • Repository Interfaces/ServiceBase: The Interfaces of generic repositories, domain repositories, and services. Other layers will inherit and develop them.
    • ILogger/DTOs/Exceptions: Notifications and information are transferred to other services.
    • Others
  • Application
    • Mobile application
    • Web MVC/API application
    • Desktop application
    • IoT
    • Others services
  • Infrastructure
    • Repositories: Repositories will be implemented here, including GenericRepository and <Entity> Repository. 
    • Data access: Contexts and the API connections link to databases. 
      • SQL: ADO.NET, EntityFramework, Dapper, and ORM, etc.
      • In-Memory stores.
      • Caching, NoSQL, and so on.
      • Data seeding
    • Others:
      • Logging.
      • Cryptography.
      • Etc.

 

Layers in DDD

DDD implementation in a .NET Core application with code examples

Let me illustrate a basic example that helps you understand this architecture clearly and know how to implement it effectively.

As mentioned earlier, a system with DDD pattern implementation usually consists of three main layers: Application, Domain, and Infrastructure and can be organized in a solution like below.

 

DDD implementation in a .NET Core application

 

API Layer

This is the Application Layer and works as a gateway where applications (AL or Presentation Layer) interact with the system. This layer processes collected information from interactions between the application and end-users or third-party services. It receives requests and validates the input before sending them to the Domain for processing. API also provides responses to the client.

The below screenshot explains the detailed structure of the API layer.

 

API Layer in DDD

 

 

Code examples

BaseService.cs



public class BaseService
{
    public BaseService(IUnitOfWork unitOfWork)
    {
        UnitOfWork = unitOfWork;
    }

    protected internal IUnitOfWork UnitOfWork { get; set; }
}

Code language: C# (cs)

 

UserService.cs

 public class UserService : BaseService
    {
        public UserService(IUnitOfWork unitOfWork) : base(unitOfWork)
        {
        }

        public async Task AddNewAsync(AddUserRequest model)
        {
            // You can you some mapping tools as such as AutoMapper
            var user = new User(model.UserName
                , model.FirstName
                , model.LastName
                , model.Address
                , model.BirthDate
                , model.DepartmentId.Value);

            var repository = UnitOfWork.AsyncRepository();
            await repository.AddAsync(user);
            await UnitOfWork.SaveChangesAsync();

            var response = new AddUserResponse()
            {
                Id = user.Id,
                UserName = user.UserName
            };

            return response;
        }

        public async Task AddUserPayslipAsync(AddPayslipRequest model)
        {
            var repository = UnitOfWork.AsyncRepository();
            var user = await repository.GetAsync(_ => _.Id == model.UserId);
            if (user != null)
            {
                var payslip = user.AddPayslip(model.Date.Value
                    , model.WorkingDays.Value
                    , model.Bonus
                    , model.IsPaid.Value);

                await repository.UpdateAsync(user);
                await UnitOfWork.SaveChangesAsync();

                return new AddPayslipResponse()
                {
                    UserId = user.Id,
                    TotalSalary = payslip.TotalSalary
                };
            }

            throw new Exception("User not found.");
        }

        public async Task<List> SearchAsync(GetUserRequest request)
        {
            var repository = UnitOfWork.AsyncRepository();
            var users = await repository
                .ListAsync(_ => _.UserName.Contains(request.Search));

            var userDTOs = users.Select(_ => new UserInfoDTO()
            {
                Address = _.Address,
                BirthDate = _.BirthDate,
                DepartmentId = _.DepartmentId,
                FirstName = _.FirstName,
                Id = _.Id,
                LastName = _.LastName,
                UserName = _.UserName
            })
            .ToList();

            return userDTOs;
        }
    }

Code language: C# (cs)

 

Domain Layer

This is the center of the system. The Domain handles most of the business logic of the system. This layer is also responsible for defining the concepts, behaviors, and rules. The remaining layers implement them.

 

Domain Layer in DDD

 

Code examples

a. The Entities
Now, let's create two classes named User and Department in Domain.

User.cs


 public partial class User : BaseEntity
    {
        public User()
        {
            PaySlips = new HashSet();
        }

        public string UserName { get; private set; }
        public string FirstName { get; private set; }
        public string LastName { get; private set; }
        public string Address { get; private set; }
        public DateTime? BirthDate { get; private set; }
        public int DepartmentId { get; private set; }
        public float CoefficientsSalary { get; private set; }

        public virtual Department Department { get; private set; }
        public virtual ICollection PaySlips { get; private set; }
    }

Code language: C# (cs)

 

User.Aggregate.cs


public partial class User: IAggregateRoot
    {
        public User(string userName
            , string firstName
            , string lastName
            , string address
            , DateTime? birthDate
            , int departmentId)
        {
            UserName = userName;

            this.Update(
                firstName
                , lastName
                , address
                , birthDate
                , departmentId
            );
        }

        public void Update(string firstName
            , string lastName
            , string address
            , DateTime? birthDate
            , int departmentId)
        {
            FirstName = firstName;
            LastName = lastName;
            Address = address;
            BirthDate = birthDate;
            DepartmentId = departmentId;
        }

        public void AddDepartment(int departmentId)
        {
            DepartmentId = departmentId;
        }

        public Payslip AddPayslip(DateTime date
            , float workingDays
            , decimal bonus
            , bool isPaid
            )
        {
            // Make sure there's only one payslip  per month
            var exist = PaySlips.Any(_ => _.Date.Month == date.Month && _.Date.Year == date.Year);
            if (exist)
                throw new Exception("Payslip for this month already exist.");

            var payslip = new Payslip(this.Id, date, workingDays, bonus);
            if (isPaid)
            {
                payslip.Pay(this.CoefficientsSalary);
            }

            PaySlips.Add(payslip);

            var addEvent = new OnPayslipAddedDomainEvent()
            {
                Payslip = payslip
            };

            AddEvent(addEvent);

            return payslip;
        }
    }


Code language: C# (cs)

 

Payslip.cs


public class Payslip : BaseEntity
    {
        public Payslip(int userId
            , DateTime date
            , float workingDays
            , decimal bonus)
        {
            UserId = userId;
            Date = date;
            WorkingDays = workingDays;
            Bonus = bonus;
        }

        public PayslipValueObject Value;

        public DateTime Date { get; private set; }
        public float WorkingDays { get; private set; }
        public bool IsPaid { get; private set; }
        public DateTime? PaymentDate { get; private set; }
        public int UserId { get; private set; }
        public decimal TotalSalary { get; private set; }
        public decimal Bonus { get; private set; }

        public virtual User User { get; private set; }

        public void Pay(
            float coefficientsSalary
            )
        {
            if (IsPaid)
                throw new Exception("This Payslip has been paid.");

            IsPaid = true;
            Value = new PayslipValueObject(WorkingDays, coefficientsSalary, Bonus);
            TotalSalary = Value.TotalSalary;
            PaymentDate = DateTime.Now;
        }
    }

Code language: C# (cs)

Department.cs

public partial class Department : BaseEntity
{
    public string Name { get; internal set; }
    public string Description { get; internal set; }

    public virtual ICollection Users { get; internal set; }
}

Code language: C# (cs)

 

Department.Aggregate.cs


public partial class Department: IAggregateRoot
{
    public Department()
    {
        Users = new HashSet();
    }

    public Department(string name
        , string description) : this()
    {
        this.Update(name, description);
    }

    public void Update(string name
        , string description)
    {
        Name = name;
        Description = description;
    }
}

Code language: C# (cs)

 

b. The Generic repositories

IAsyncRepository.cs


public interface IAsyncRepository where T : BaseEntity
{
    Task AddAsync(T entity);

    Task UpdateAsync(T entity);

    Task DeleteAsync(T entity);

    Task GetAsync(Expression<Func<T, bool>> expression);

    Task<List> ListAsync(Expression<Func<T, bool>> expression);
}

Code language: C# (cs)

 

IUnitOfWork.cs


public interface IUnitOfWork
{
    {
        Task SaveChangesAsync();

        IAsyncRepository Repository() where T : BaseEntity;
    }
}


Code language: C# (cs)

 

Infrastructure Layer

The Infrastructure of the system includes database, logging, and exceptions. This layer is the place to interact with the database. Through behaviors and rules, POCO classes have been defined in the Domain. This layer undertakes all operations related to the information storage of the system.

 

Infrastructure Layer in DDD

 

Code examples

UnitOfWork.cs


public class UnitOfWork : IUnitOfWork
{
    private readonly EFContext _dbContext;

    public UnitOfWork(EFContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IAsyncRepository AsyncRepository() where T : BaseEntity
    {
        return new RepositoryBase(_dbContext);
    }

    public Task SaveChangesAsync()
    {
        return _dbContext.SaveChangesAsync();
    }
}

Code language: C# (cs)


RepositoryBase.cs


public class RepositoryBase : IAsyncRepository where T : BaseEntity
{
    private readonly DbSet _dbSet;

    public RepositoryBase(EFContext dbContext)
    {
        _dbSet = dbContext.Set();
    }

    public async Task AddAsync(T entity)
    {
        await _dbSet.AddAsync(entity);
        return entity;
    }

    public Task DeleteAsync(T entity)
    {
        _dbSet.Remove(entity);
        return Task.FromResult(true);
    }

    public Task GetAsync(Expression<Func<T, bool>> expression)
    {
        return _dbSet.FirstOrDefaultAsync(expression);
    }

    public Task<List> ListAsync(Expression<Func<T, bool>> expression)
    {
        return _dbSet.Where(expression).ToListAsync();
    }

    public Task UpdateAsync(T entity)
    {
        _dbSet.Update(entity);
        return Task.FromResult(entity);
    }
}
Code language: C# (cs)


UserRepository.cs


public class UserRepository : RepositoryBase
        , IUserRepository
    {
        public UserRepository(EFContext dbContext) : base(dbContext)
        {
        }
    }

Code language: C# (cs)


DepartmentRepository.cs


public class DepartmentRepository : RepositoryBase
        , IDepartmentRepository
    {
        public DepartmentRepository(EFContext dbContext) : base(dbContext)
        {
        }
    }

Code language: C# (cs)

 

Now let’s move on to configure DI in startup.cs class and finish some other settings (e.g., connectionStrings, Db Migrations, and Services injection, etc.) to run the application.

 

Conclusion
In summary, DDD is a great pattern for systems with complex business logic, systems that require future maintenance and enhancement. It is not necessary to fully apply this pattern to the project, we need to make tradeoffs sometimes depending on particular situations in each project. Onion Architecture, Clean Architecture, and Hexagon Architecture are also good examples.

You can also refer to the source code here:

I hope this article inspires you to architect your project with DDD.

Happy coding and have a nice day!

 

CTA Enlab Software

 

References
1. David Laribee, Best Practice - An Introduction To Domain-Driven Design, www.docs.microsoft.com, 2009.
2. Design a DDD-oriented microservice, www.docs.microsoft.com, 2021.
3. Implementing Domain-Driven Design, www.docs.abp.io, 2021.
4. Domain-driven design, https://en.wikipedia.org.

About the author

Loc Nguyen

Loc’s a senior backend developer with a laser focus on Microsoft technologies (ASP .NET, Xamarin, WPF), DevOps (GitLab CI/CD, AWS DevOps, Docker), Design Patterns (DDD design, Clean Architect, Microservices, N-Tiers). With his +6-year experience, Loc’s happy to discuss topics like OOP, Data structures & Algorithms, and more.

Up Next

How to create real time chat applications using WebSocket APIs in API Gateway
March 26,2021 by Trong Pham
My experience developing real-time messaging applications leveraging AWS services inspires me to share with the...
How to build and deploy a 3-tier architecture application with C#
March 11,2021 by Uyen Luu
Hi, back to the architecture patterns, in the last article I explained what the 3-layer...
How to build and deploy a 3-layer architecture application with C#
February 27,2021 by Uyen Luu
What’s 3-layer architecture? Layer indicates the logical separation of components. Layered architecture concentrates on...
Repository and Unit of Work design patterns in .NET core
February 03,2021 by Loc Nguyen
Hi, back to the topic Repository & Unit of Work pattern, last time I explained...
Roll to Top

Can we send you our next blog posts? Only the best stuffs.

Subscribe