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.

 

 

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.

 

 

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.

 

 

 

Code examples

BaseService.cs


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

    protected internal IUnitOfWork UnitOfWork { get; set; }
}

UserService.cs


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

        public async Task<AddUserResponse> AddNewAsync(AddUserRequest model)
        {
            // You can you some mapping tools as such as AutoMapper
            var user = new User(model.UserName
                , model.FirstName
                , modelmentId.Value);
.LastName
                , model.Address
                , model.BirthDate
                , model.Depart
            var repository = UnitOfWork.AsyncRepository<User>();
            await repository.AddAsync(user);
            await UnitOfWork.SaveChangesAsync();

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

            return response;
        }

        public async Task<AddPayslipResponse> AddUserPayslipAsync(AddPayslipRequest model)
        {
            var repository = UnitOfWork.AsyncRepository<User>();
            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<UserInfoDTO>> SearchAsync(GetUserRequest request)
        {
            var repository = UnitOfWork.AsyncRepository<User>();
            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;
        }
    }

 

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.

 

 

Code examples

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

User.cs


    public partial class User : BaseEntity<int>
    {
        public User()
        {
            PaySlips = new HashSet<Payslip>();
        }

        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<Payslip> PaySlips { get; private set; }
    }

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;
        }
    }

Payslip.cs


public class Payslip : BaseEntity<int>
    {
        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;
        }
    }

Department.cs


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

    public virtual ICollection<User> Users { get; internal set; }
}

Department.Aggregate.cs


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

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

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

 

b. The Generic repositories

IAsyncRepository.cs


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

    Task<T> UpdateAsync(T entity);

    Task<bool> DeleteAsync(T entity);

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

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

IUnitOfWork.cs


public interface IUnitOfWork
{
    {
        Task<int> SaveChangesAsync();

        IAsyncRepository<T> Repository<T>() where T : BaseEntity;
    }
}

 

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.

 

 

Code examples

UnitOfWork.cs


    public class UnitOfWork : IUnitOfWork
{
    private readonly EFContext _dbContext;

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

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

    public Task<int> SaveChangesAsync()
    {
        return _dbContext.SaveChangesAsync();
    }
}

RepositoryBase.cs


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

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

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

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

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

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

    public Task<T> UpdateAsync(T entity)
    {
        _dbSet.Update(entity);
        return Task.FromResult(entity);
    }
}

UserRepository.cs


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

DepartmentRepository.cs


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

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!

 

 

References

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.
Frequently Asked Questions (FAQs)
What is Domain-Driven Design (DDD), and why do we need it?

Domain-Driven Design (DDD) is an approach to software development introduced by Eric Evans in 2003. It focuses on modeling the problem domain of your application and aligning your software with the real-world business processes. DDD helps in creating software that is more maintainable, adaptable, and closely connected to the business model’s evolution. It emphasizes the use of bounded contexts, rich domain models, value objects, aggregates, and other patterns to achieve these goals.

What are the key layers in Domain-Driven Design (DDD)?

In Domain-Driven Design, the architecture typically consists of three main layers:

Domain Layer: This layer is at the core of the system and handles most of the business logic. It defines concepts, behaviors, and rules, including entities, aggregates, value objects, interfaces, and repository interfaces.

Application Layer: The application layer acts as a gateway for interactions between the application (presentation layer) and the system. It receives and validates requests from users or external services, then forwards them to the domain layer for processing.

Infrastructure Layer: The infrastructure layer deals with database interactions, logging, and exceptions. It interacts with the database based on the behaviors and rules defined in the domain layer.

How is Domain-Driven Design (DDD) implemented in a .NET Core application?

Implementing Domain-Driven Design in a .NET Core application involves organizing your code into the three main layers (Domain, Application, and Infrastructure) and following DDD principles. The Domain layer defines entities, aggregates, value objects, and repository interfaces. The Application layer handles user interactions and forwards requests to the Domain layer. The Infrastructure layer interacts with the database and performs other technical tasks.

Additionally, your code examples demonstrate how various components like services, repositories, and unit of work are implemented within these layers.

Up Next

July 05, 2024 by Dat Le
In the rapidly evolving world of software development, Big Data stands out as a transformative force....
June 27, 2024 by Dat Le
In today's rapidly evolving digital landscape, secure coding practices are paramount to safeguarding applications from a...
June 20, 2024 by Dat Le
In the rapidly evolving digital landscape, the role of User Interface (UI) and User Experience (UX)...
June 17, 2024 by Dat Le
In the dynamic world of software development, one element has emerged as crucial to success: User...

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

Subscribe