Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET / ASP.NET-Core

Domain Driven Design Implementation Approach with Generic Repository and UoW Pattern in ASP.NET Core 3.1 Web API and EF Core 5.0

4.89/5 (16 votes)
5 Mar 2021CPOL20 min read 44K  
Understanding and implementing Domain Driven Design Implementation Approach
In this article, I'm going to guide you through creating an application using Domain Driven Design Implementation Approach, with Generic Repository Pattern and Unit of Work Pattern in ASP.NET Core 3.1 Web API and Entity Framework Core 5.0.

Introduction

Reading this article will give you a proper understanding of implementing an ASP.NET Core Web-API application using Domain Driven Design Implementation Approach, Generic Repository and Unit of Work Pattern in ASP.NET Core 3.1 Web API and Entity Framework Core 5.0.

I will introduce the Generic Repository and Unit of Work Pattern for data access layer, and then I will develop an ASP.NET web API application for registering customer orders and order items in clean, testable, and maintainable architecture, using Domain Driven Design (DDD) Implementation Approach.

Background

It will be necessary that you know the basics of object oriented programming, domain driven design (DDD) approach, Microsoft ORM Entity Framework, unit testing, and solid principles of object oriented design, introduced by Robert C. Martin.

Also, for a brief overview of the solid principles, you can just type few words in Google like: "solid principles of object oriented design".

Domain Driven Design

The term Domain-Driven Design (DDD) was coined by Eric Evans in his book in 2004. Domain Driven Design is a big topic, and in this article, we just want to have a glimpse of it and we want to focus on implementation details of domain driven design, and also we are going to write a simple shopping tutorial application, and our major goal will be keeping the application business logic safe in domain models (rich entities) and domain services. In this article on sample shopping tutorial application, we will focus on domain and domain logic, without worrying about data persistence.

As Martin Fowler says:

Domain-Driven Design is an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain”.

We will look at domain elements (building blocks) such as Entities, Value Objects, Aggregate Root Entity, Data Transfer Object (DTO), Services, and Repositories. We will look at the software design principles, frameworks and tools that we can use in the implementation effort. The most famous architecture for implementing DDD is Onion architecture, as shown in the image below: Application Core (domain models and domain services) where business logic lies in there, is not dependent on any data persistence tools or any technology, so we can say, that our application core domain is independent of the outside environment and external dependencies, so that lead us to (testable and agile architecture principles), because our application core business logic doesn't depend on other parts of the application like database, etc., so it can be easily tested and debugged in isolation, that’s so great.

Image 1

So we can change anything we want in future in any other parts of application like: repositories, databases, ORMs, UI, etc. and the whole application should probably work fine with minimum or no changes in inner layers (core domain).

I don't want to talk a lot about this approach, and will focus on implementation details of domain driven approach, because there is a lot of theoretical information about this topic in internet. So if you want to know more about this development approach, please type a few words in Google just like: "Domain Driven Design Implementation Approach".

Repository Pattern

The repository is one of Domain Driven Design (DDD) implementation elements or building blocks. Repository pattern gives us clean, testable and maintainable approach to access and manipulate data in our applications. As Martin Fowler in his book, Patterns of Enterprise Applications Architecture says: “a Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection, that isolates business entities from the underlying data infrastructure”. The repository pattern offers an interface and provides methods to add, remove, and retrieve domain entities, to enable the domain entities to remain agnostic of the underlying data persistence layer (Persistence Ignorance), encouraging loosely coupling programming and allowing to extend application layers independently, which leads us to test the business logic in isolation from external dependencies (testable and agile architecture principle). Also, Repository Pattern gives us the ability to centralize and encapsulate queries, in order to reuse it, in other parts of the application (DRY principle).

As you see in the image below, the application logic is dependent on Customer Repository Interface not on concrete implementation of repository, for more abstraction (Dependency Inversion Principle), this means that the application logic is completely ignorant of any implementation of customer repository and data access concerns, keeping the application logic codes intact and safe from data access changes in future, ensuring that it is less fragile and easy to extend.

Image 2

So it will be ok, if we want to mock the customer repository implementation and inject it in application logic classes (domain services or controllers) via DI (Dependency Injector) tools, in order to test application logics in isolation, giving us the opportunity of unit testing without having any concerns about data access logic and implementations (testable architecture principle).

Unit of Work Pattern

Unit of Work Design Pattern coordinates the data persistence operations of multiple business objects changes as one atomic transaction, which guarantees the entire transaction will be committed or rolled back. As you can see in the image below, Unit of Work design pattern encapsulates multiple repositories and shares single database context between them.

Image 3

For more information about Unit of Work design pattern, you can just type few words in Google like: "martin fowler unit of work".

Using the Code

Creating a Blank Solution and Solution Architecture

We start our application first by adding a Blank Solution, by opening Visual Studio 2019, selecting “Create a new project” on the right menu, selecting Blank Solution and naming it DomainDrivenTutorial, as shown in the image below. Then we add Framework.Core folder in Solution.

Image 4

Adding and Implementing Application Shared Kernel Library

Next we add .NET Core Class Library in Framework.Core folder and naming it Shared Kernel, as shown in the image below:

Image 5

Shared Kernel Library is where we want to put our public models and helper classes, which will be used among all libraries in the entire solution. We create a Models folder in it, then we add PageParam class to make data pagination request model, that will be used later between repository and application services layer.

PageParam.cs

C#
public class PageParam
{
        const int maxPageSize = 50;
 
        private int _pageSize = 10;
 
        public int PageSize
        {
          get
          {
                return _pageSize;
          }
          set
          {
            _pageSize = (value > maxPageSize) ? maxPageSize : value;
          }
        }
 
        public int PageNumber { get; set; } = 1;
}

Implementing Generic Repository Pattern on Entity Framework Core

The next step will be adding next .NET Core Class Library in Framework.Core folder, and naming it GenericRepositoryEntityFramework. This Library will contain only three .CS files, repository interface, repository implementation and SortingExtension static helper class in order to sort fetched records from database in ascending or descending order.

So before going further, we must add the NuGet packages for entity framework core, like image below. Right click on application solution name and click Manage NuGet Packages for Solution.

Image 6

In the NuGet form, click on Browse tab, and enter text “EntityFrameworkCore” in search criteria filter text box, as shown in the image below:

Image 7

Choose Microsoft.EntityFrameworkCore and on the right section, select Generic Repository Entity Framework project, and then click on Install button as shown in the image below:

Image 8

Repeat this scenario for NuGet packages: Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.Tools.

All right, adding nuget packages to the project is done. As you see below, there is a sample code for repository pattern implementation with the entity framework core 5.0.

In order to access the complete source code, you can download it from Github.

IRepository.cs

C#
using SharedKernel.Models;
 
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Collections.Generic; 
 
namespace GenericRepositoryEntityFramework
{
    public interface IRepository<TEntity> where TEntity : IAggregateRoot
    {
        void Add(TEntity entity);
 
        void Remove(TEntity entity);
 
        void Update(TEntity entity);
 
        Task<TEntity> GetByIdAsync(object id);
 
        Task<IEnumerable<TEntity>> GetAllAsync();
 
        Task<IEnumerable<TEntity>> GetAllAsync<TProperty>
        (Expression<Func<TEntity, TProperty>> include);
 
        Task<TEntity> SingleOrDefaultAsync(Expression<Func<TEntity, bool>> predicate);
 
        Task<QueryResult<TEntity>> GetPageAsync(QueryObjectParams queryObjectParams);
 
        Task<QueryResult<TEntity>> GetPageAsync(QueryObjectParams queryObjectParams, 
                                   Expression<Func<TEntity, bool>> predicate);
 
        Task<QueryResult<TEntity>> GetOrderedPageQueryResultAsync
        (QueryObjectParams queryObjectParams, IQueryable<TEntity> query);       
    } 
}

Please notice that I've defined deliberately the DbContext as a protected property in Repository implementation class, because I want to use it in derived repository classes, I want to let the derived repository classes to make flexible and rich queries, which definitely will be an "is-a" relationship between derived classes and base generic repository class.

Repository.cs

C#
using Microsoft.EntityFrameworkCore;
using SharedKernel.Models;
 
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Collections.Generic;
 
namespace GenericRepositoryEntityFramework
{
    public class Repository<TEntity> : IRepository<TEntity> where TEntity : class, IAggregateRoot
    {
        protected readonly DbContext Context; 
 
        private readonly DbSet<TEntity> _dbSet; 
 
        public Repository(DbContext context)
        {
            Context = context;
 
            if (context != null)
            {
                _dbSet = context.Set<TEntity>();
            }
        } 
 
        public virtual void Add(TEntity entity)
        {
            _dbSet.Add(entity);
        }
 
        public virtual void Remove(TEntity entity)
        {
            _dbSet.Remove(entity);
        }
 
        public virtual void Update(TEntity entity)
        {
            _dbSet.Update(entity);
        }
 
        public async Task<TEntity> GetByIdAsync(object id)
        {
            return await _dbSet.FindAsync(id).ConfigureAwait(false);
        }
 
        public async Task<IEnumerable<TEntity>> GetAllAsync()
        {
            return await _dbSet.ToListAsync().ConfigureAwait(false);
        }
 
        public async Task<IEnumerable<TEntity>> 
        GetAllAsync<TProperty>(Expression<Func<TEntity, TProperty>> include)
        {
            IQueryable<TEntity> query = _dbSet.Include(include);
 
            return await query.ToListAsync().ConfigureAwait(false);
        }
 
        public async Task<TEntity> 
        SingleOrDefaultAsync(Expression<Func<TEntity, bool>> predicate)
        {
            return await _dbSet.SingleOrDefaultAsync(predicate).ConfigureAwait(false);
        }
 
        public virtual async Task<QueryResult<TEntity>> 
        GetPageAsync(QueryObjectParams queryObjectParams)
        {
            return await GetOrderedPageQueryResultAsync
                   (queryObjectParams, _dbSet).ConfigureAwait(false);
        } 
 
        public virtual async Task<QueryResult<TEntity>> 
        GetPageAsync(QueryObjectParams queryObjectParams, 
        Expression<Func<TEntity, bool>> predicate)
        {
            IQueryable<TEntity> query = _dbSet;
 
            if (predicate != null)
                query = query.Where(predicate);
 
            return await GetOrderedPageQueryResultAsync
                         (queryObjectParams, query).ConfigureAwait(false);
        } 
 
        public async Task<QueryResult<TEntity>> 
        GetOrderedPageQueryResultAsync
        (QueryObjectParams queryObjectParams, IQueryable<TEntity> query)
        {
            IQueryable<TEntity> OrderedQuery = query;
 
            if (queryObjectParams.SortingParams != null && 
                queryObjectParams.SortingParams.Count > 0)
            {
                OrderedQuery = SortingExtension.GetOrdering
                               (query, queryObjectParams.SortingParams);
            }
 
            var totalCount = await query.CountAsync().ConfigureAwait(false);
 
            if (OrderedQuery != null)
            {
                var fecthedItems = 
                await GetPagePrivateQuery
                (OrderedQuery, queryObjectParams).ToListAsync().ConfigureAwait(false);
 
                return new QueryResult<TEntity>(fecthedItems, totalCount);
            }
 
            return new QueryResult<TEntity>(await GetPagePrivateQuery
            (_dbSet, queryObjectParams).ToListAsync().ConfigureAwait(false), totalCount);
        } 
 
        private IQueryable<TEntity> GetPagePrivateQuery
        (IQueryable<TEntity> query, QueryObjectParams queryObjectParams)
        {
            return query.Skip((queryObjectParams.PageNumber - 1) * 
            queryObjectParams.PageSize).Take(queryObjectParams.PageSize);
        }
    } 
}

Building a Simple Domain Driven ASP.NET Core Web API Application

Now let's go and build a simple ASP.NET Core Web API application in order to better understand the domain driven design implementation approach.

What we want to do is to create two domain entities, Order and Order Items entities by Entity Framework Core code first approach, and performing CRUD operations on them by use of domain driven design approaches, aggregate root, and repository pattern in order to encapsulate application logic in domain models and domain services and so on…

So at first, we will create a class library to separate application persistence and business logic from ASP.NET Core web API, and we will put our application logic in domain models and domain services, then we will add two domain entities, Order and Order Items entities. That's enough talk, let’s start the sample project.

Right click on solution name and create a new folder and name it EShoppingTutorial, then add a .NET Core class library project by going to File -> New -> Project, and name it EShoppingTutorial.Core, like the image below:

Image 9

Next, we want to define the application solution structure, to separate files in folders like: Domain, Entities, Services, Persistence, Repositories, etc., in order to separate application business logics of data persistence concerns for having a better and clean application architecture, like the image below:

Image 10

I want you to have a closer look at the application solution structure before coding, we have separated the folders, next we will add some classes to them, this can lead us to having better application architecture (Clean Architecture Principle), better refactoring and maintenance. At the beginning, we have decided to separate Domain Entities and Domain Services folders and boundaries from Persistence layer, our purpose is to separate things that can change in future (like: data persistence logic, ORM-versions, or databases) from things that almost don't or never change, or they are invariant or persistent ignorance like: domain invariants, domain services, or value objects, as I talked about it in domain driven design section at the beginning of this article.

So before going further, it is better to add the NuGet packages for entity framework core. Like the image below. Right click on application solution name and click Manage NuGet Packages for Solution.

Image 11

In the NuGet form, click on Browse tab, and enter text “EntityFrameworkCore” in search criteria filter text box, as shown in the image below:

Image 12

Choose Microsoft.EntityFrameworkCore and on the right section, select EShoppingTutorial.Core project, and then click on Install button as shown in the image below. Repeat this scenario for NuGet packages Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.Tools.

Image 13

Also, we must add project references of Generic Repository Entity Framework and Shared Kernel project libraries.

Adding Application Domain Models

So for the next step, we add two domain models (entity classes), called Order and Order Items in Entities folder, a Value Object called Price in ValueObjects folder, and an Enum called MoneyUnit in Enums folder, as shown in the code below:

MoneyUnit.cs

C#
namespace EShoppingTutorial.Core.Domain.Enums
{
    public enum MoneyUnit : int
    {
        UnSpecified = 0,
        Rial = 1,
        Dollar,
        Euro
    }
}

Adding Price Value Object

For the next step, we add a Value Object called Price in ValueObjects folder and a helper class called MoneySymbols, we should encapsulate business logic related to the Price value object.

MoneySymbols.cs

C#
using System.Collections.Generic;
using EShoppingTutorial.Core.Domain.Enums;
 
namespace EShoppingTutorial.Core.Domain.ValueObjects
{
    public static class MoneySymbols
    {
        private static Dictionary<MoneyUnit, string> _symbols;
 
        static MoneySymbols()
        {
            if (_symbols != null)
                return;
 
            _symbols = new Dictionary<MoneyUnit, string>
            {
                { MoneyUnit.UnSpecified, string.Empty },
 
                { MoneyUnit.Dollar, "$" },
 
                { MoneyUnit.Euro, "€" },
 
                { MoneyUnit.Rial, "Rial" },
            };
        }
 
        public static string GetSymbol(MoneyUnit moneyUnit)
        {
            return _symbols[moneyUnit].ToString();
        }
    }
}

Price.cs

C#
using EShoppingTutorial.Core.Domain.Enums;
using SharedKernel.Exceptions;
using System.ComponentModel.DataAnnotations.Schema;

namespace EShoppingTutorial.Core.Domain.ValueObjects
{
    [ComplexType]
    public class Price
    {
        protected Price() // For Entity Framework Core
        {

        }

        public Price(int amount, MoneyUnit unit)
        {
            if (MoneyUnit.UnSpecified == unit)
                throw new BusinessRuleBrokenException("You must supply a valid money unit!");

            Amount = amount;

            Unit = unit;
        }


        public int Amount { get; protected set; }


        public MoneyUnit Unit { get; protected set; } = MoneyUnit.UnSpecified;


        public bool HasValue
        {
            get
            {
                return (Unit != MoneyUnit.UnSpecified);
            }
        }


        public override string ToString()
        {
            return 
                Unit != MoneyUnit.UnSpecified ? 
                Amount + " " + MoneySymbols.GetSymbol(Unit) : 
                Amount.ToString();
        }
    }
}

Adding Order Item Entity Model

For the next step, we add a domain model called OrderItem in Entities folder, which will hold data of every order item.

OrderItem.cs

C#
using EShoppingTutorial.Core.Domain.ValueObjects;
using SharedKernel.Exceptions;

namespace EShoppingTutorial.Core.Domain.Entities
{
    public class OrderItem
    {
        public int Id { get; protected set; }

        public int ProductId { get; protected set; }

        public Price Price { get; protected set; }

        public int OrderId { get; protected set; }


        protected OrderItem() // For Entity Framework Core
        {
            
        }

        public OrderItem(int productId, Price price)
        {
            ProductId = productId;

            Price = price;

            CheckForBrokenRules();
        }

        private void CheckForBrokenRules()
        {
            if (ProductId == 0)
                throw new BusinessRuleBrokenException("You must supply valid Product!");

            if (Price is null)
                throw new BusinessRuleBrokenException("You must supply an Order Item!");
        }
    }
}

Adding Order Entity Model

So, at the end, we will add a domain model called Order in Entities folder, which also will act as an aggregate root entity. As you see in the code below, it is not an ordinary weak entity model that is used just for CRUD operations! It is a rich domain model, which combines data and logic together. It has properties and behaviors. It applies encapsulation and information hiding, as you see below, there is one-way relation to Order Items entity, and the only way to access the OrderItems data in application will be via this aggregate root rich entity model.

As you see below, the OrderItems property is a read-only collection, so we will not be able to add order items via this read-only property from outside, and the only way to add order items will be via Order model class’s constructor. So this class will hide and encapsulate the data of OrderItems and related business rules, and will perform its duty as an aggregate root entity. You can easily search in Google and read a lot about aggregate root entity.

Order.cs

C#
using System;
using System.Linq;
using System.Collections.Generic;
using SharedKernel.Exceptions;
using SharedKernel.Models;

namespace EShoppingTutorial.Core.Domain.Entities
{
    public class Order : IAggregateRoot
    {
        public int Id { get; protected set; }

        public Guid? TrackingNumber { get; protected set; }

        public string ShippingAdress { get; protected set; }

        public DateTime OrderDate { get; protected set; }


        private List<OrderItem> _orderItems;
        public ICollection<OrderItem> OrderItems { get { return _orderItems.AsReadOnly(); } }


        protected Order() // For Entity Framework Core
        {
            _orderItems = new List<OrderItem>();
        }


        /// <summary>
        /// Throws Exception if Maximum price has been reached, or if no Order Item has been added to this Order
        /// </summary>
        /// <param name="orderItems"></param>
        public Order(string shippingAdress, IEnumerable<OrderItem> orderItems) : this()
        {
            CheckForBrokenRules(shippingAdress, orderItems);

            AddOrderItems(orderItems);


            ShippingAdress = shippingAdress;

            TrackingNumber = Guid.NewGuid();

            OrderDate = DateTime.Now;
        }

        private void CheckForBrokenRules(string shippingAdress, IEnumerable<OrderItem> orderItems)
        {
            if (string.IsNullOrWhiteSpace(shippingAdress))
                throw new BusinessRuleBrokenException("You must supply ShippingAdress!");

            if (orderItems is null || (!orderItems.Any()))
                throw new BusinessRuleBrokenException("You must supply an Order Item!");
        }

        private void AddOrderItems(IEnumerable<OrderItem> orderItems)
        {
            var maximumPriceLimit = MaximumPriceLimits.GetMaximumPriceLimit(orderItems.First().Price.Unit);

            foreach (var orderItem in orderItems)
                AddOrderItem(orderItem, maximumPriceLimit);
        }



        /// <summary>
        /// Throws Exception if Maximum price has been reached
        /// </summary>
        /// <param name="orderItem"></param>
        private void AddOrderItem(OrderItem orderItem, int maximumPriceLimit)
        {
            var sumPriceOfOrderItems = _orderItems.Sum(en => en.Price.Amount);

            if (sumPriceOfOrderItems + orderItem.Price.Amount > maximumPriceLimit)
            {
                throw new BusinessRuleBrokenException("Maximum price has been reached !");
            }

            _orderItems.Add(orderItem);
        }

    }
}

The Order entity model checks some business rules and raises an BusinessRuleBrokenException (Custom exception defined in Shared Kernel Library) if any business rule has been broken.

Also, it’s a pure .NET object (POCO class) and persistence ignorance (PI). So, without connecting to any external service or database repository, we can easily write unit tests in order to check model’s behavior (business rules). It’s so great. So let’s write some unit-tests together for Order entity’s business rules.

Adding Unit Tests Project and Writing a Simple Unit Test for Order Entity

So, right click on solution Explorer, and choose Add --> New project.

Then select the template project that you want to use, for this app, you could select NUnit Test or Unit Test Project, and then choose Next.

So, enter a name like “EShoppingTutorial.UnitTests” for your project, and choose Create, as shown in the image below:

Image 14

Add project references for Generic Repository Entity Framework and Shared Kernel project libraries, to this project.

Create folders: Domain, Entities and Repositories, and add a new class naming OrderShould in Entities folder as shown in image below:

Image 15

OrderShould class will contain unit tests for order entity.

OrderShould.cs

C#
using NUnit.Framework;
using SharedKernel.Exceptions;
using EShoppingTutorial.Core.Domain.Entities;

using System;
using EShoppingTutorial.Core.Domain.ValueObjects;
using EShoppingTutorial.Core.Domain.Enums;

namespace EShoppingTutorial.UnitTests.Domain.Entities
{
    public class OrderShould
    {
        [Test]
        public void Test_InstantiatingOrder_WithEmptyOrderItems_ExpectsBusinessRuleBrokenException()
        {
            // act
            TestDelegate testDelegate = () => new Order("IRAN", new OrderItem[] { });


            // assert
            var ex = Assert.Throws<BusinessRuleBrokenException>(testDelegate);
        }


        [Test]
        public void Test_OrderItemsProperty_AddingOrderItemToReadOnlyCollection_ExpectsNotSupportedException()
        {
            // arrange
            var order = new Order("IRAN", new OrderItem[] { new OrderItem(1, new Price(1, MoneyUnit.Dollar)) });


            // act
            TestDelegate testDelegate = () => order.OrderItems.Add(new OrderItem(1, new Price(1, MoneyUnit.Dollar)));


            // assert
            var ex = Assert.Throws<NotSupportedException>(testDelegate);
        }


        [Test]
        public void Test_InstantiateOrder_WithOrderItems_ThatExccedsTotalPriceOf_10000_Dollar_ExpectsBusinessRuleBrokenException()
        {
            // arrange

            var orderItem1 = new OrderItem(1, new Price (5000, MoneyUnit.Dollar));

            var orderItem2 = new OrderItem(2, new Price(6000, MoneyUnit.Dollar));

            // act
            TestDelegate testDelegate = () =>
            {
                new Order("IRAN",new OrderItem[] { orderItem1, orderItem2 });
            };


            // assert
            var ex = Assert.Throws<BusinessRuleBrokenException>(testDelegate);

            Assert.That(ex.Message.ToLower().Contains("maximum price"));
        }


        [Test]
        public void Test_InstantiateOrder_WithOrderItems_ThatExccedsTotalPriceOf_9000_Euro_ExpectsBusinessRuleBrokenException()
        {
            // arrange

            var orderItem1 = new OrderItem(1, new Price(5000, MoneyUnit.Dollar));

            var orderItem2 = new OrderItem(2, new Price(6000, MoneyUnit.Dollar));

            // act
            TestDelegate testDelegate = () =>
            {
                new Order("IRAN", new OrderItem[] { orderItem1, orderItem2 });
            };


            // assert
            var ex = Assert.Throws<BusinessRuleBrokenException>(testDelegate);

            Assert.That(ex.Message.ToLower().Contains("maximum price"));
        }

    }
}

Adding Entity Framework DbContext and Database Migrations

It is so good; we were able to write easily unit tests for order entity business rules and invariants without connecting to any external database. So now, we will generate the database and tables creation scripts by using Microsoft Entity Framework Core (ORM) code first Fluent API approach and using add-migration command. Let’s do it.

At first, we should configure the mappings of C# entity classes to database tables, like code below. We add mapping class files called OrderMapConfig and OrderItemMapConfig in Mappings folder, and then we put entities mapping configurations.

OrderMapConfig.cs

C#
using EShoppingTutorial.Core.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
 
namespace EShoppingTutorial.Core.Persistence.Mappings
{
    public class OrderMapConfig : IEntityTypeConfiguration<Order>
    {
        public void Configure(EntityTypeBuilder<Order> builder)
        {
            builder.ToTable("Orders");
 
            builder.HasKey(o => o.Id);
 
            builder.Property(o => o.Id).ValueGeneratedOnAdd().HasColumnName("Id");
 
            builder.Property
            (en => en.TrackingNumber).HasColumnName("TrackingNumber").IsRequired(false);
 
            builder.HasIndex(en => en.TrackingNumber).IsUnique();
 
            builder.Property
            (en => en.ShippingAdress).HasColumnName
            ("ShippingAdress").HasMaxLength(100).IsUnicode().IsRequired();
 
            builder.Property
            (en => en.OrderDate).HasColumnName("OrderDate").HasMaxLength(10).IsRequired();
        }
    }
}

OrderItemMapConfig.cs

C#
using EShoppingTutorial.Core.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
 
namespace EShoppingTutorial.Core.Persistence.Mappings
{
    public class OrderItemMapConfig : IEntityTypeConfiguration<OrderItem>
    {
        public void Configure(EntityTypeBuilder<OrderItem> builder)
        {
            builder.ToTable("OrderItems");
 
            builder.HasKey(o => o.Id);
 
            builder.Property(o => o.Id).ValueGeneratedOnAdd().HasColumnName("Id");
 
            builder.Property(en => en.ProductId).HasColumnName("ProductId").IsRequired();
 
            builder.OwnsOne(en => en.Price, price =>
            {
                price.Property(x => x.Amount).HasColumnName("Amount");
 
                price.Property(x => x.Unit).HasColumnName("Unit");
            });
        }
    }
}

Then we add a DbContext class file in Persistence folder naming it “EShoppingTutorialDbContext”, and so we apply those mappings in our DbContext class, as shown in the code below. To shorten our work, I used the “ApplyConfigurationsFromAssembly” command to apply entity mappings.

EShoppingTutorialDbContext.cs

C#
using EShoppingTutorial.Core.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using System.Reflection;
 
namespace EShoppingTutorial.Core.Persistence
{
    public class EShoppingTutorialDbContext : DbContext
    {
        public virtual DbSet<Order> Orders { get; set; }
 
        public EShoppingTutorialDbContext
        (DbContextOptions<EShoppingTutorialDbContext> dbContextOptions) 
            : base(dbContextOptions)
        {
 
        }
 
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //Scans a given assembly for all types that implement 
            //IEntityTypeConfiguration, and registers each one automatically
            modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
 
            base.OnModelCreating(modelBuilder);
        }
    }
}

Microsoft Entity Framework Code-First will also create the database tables and fields with the name of DbSet properties in the DbContext class by convention, so why did we separate mapping files and do extra work? Good question, isn't it? Think we want to refactor and change entity names and properties, without having separated mappings files we have to run add-migration command, and apply those changes in the database too, otherwise .NET Framework will give us an error, and so what will happen if we don’t want to do that? Maybe we want to have different entity and property names with the database names, etc.

So, having separated entity mapping files gives us lots of opportunities, and it also leads us to clean architecture principles for better refactoring and debugging in the future. So in software designing always, don't forget to separate things that can change from things that doesn't change. But keep in mind also, that one size does not fit all. If you really have a simple data driven application, with minimum business logic, that you are sure it will rarely change in future, so don't do that, just keep it simple (kiss principle).

All right, the next step to generating database and tables via code first approach will be adding database connection strings, for this, at first we want to add an ASP.NET Core web API project to the solution in EShoppingTutorial folder, and then we add the connection string in a web API project as shown in the image below:

Image 16

In the next form, select API template, and remove tick for Configure for Https as shown in the image below:

Image 17

For the next step, we add Nuget packages for Microsoft Entity Framework Core, and Microsoft EntityFrameworkCore Design, and project references for EShoppingTutorial.Core and SharedKernel as we did before in this article.

Finally, we prepare proper database server by your own, and then add the connection string in web API project appsettings.json and finally add EShoppingTutorialDbContext in Startup.cs with the specified connection string in appsettings as shown in the code below.

appsettings.json

JavaScript
{
  "ConnectionStrings": {
    "EShoppingTutorialDB": "Data Source=localhost\\SQLEXPRESS;
    Initial Catalog=EShoppingTutorialDB;Integrated Security=True;Pooling=False"
  },
 
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Adding connection string and injecting EShoppingTutorialDbContext via AddDbContext command in ConfigureServices method in Startup class.

Startup.cs

C#
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
 
    services.AddDbContext<EShoppingTutorialDbContext>
    (opts => opts.UseSqlServer(Configuration["ConnectionStrings:EShoppingTutorialDB"]));
}

Alright, so far so good. So if we’ve done everything accurately, then we can open the package manager console and run add-migration command, but before that, make sure that you have set the EShoppingTutorial web API project as the startup project.

Open package manager console and in the default project menu, make sure that you have selected EShoppingTutorial.Core, and then run add-migration command. Entity framework will ask you to give a name, give it a meaningful name like: ”Adding Order and OrderItems tables” as shown in the image below:

Image 18

Alright, after getting the success message from entity framework, run update-database command as shown in the image below, otherwise you must read the errors and check what you have left behind.

Image 19

After getting the success message for update-database command, go and check the database, a database called EShoppingTutorialDB with two tables must be created as we configured before in entity mapping configurations as shown in the image below:

Image 20

Adding Other Parts of the Application

Alright, we have successfully created database and tables, so let’s go and complete the other parts of the application like: unit of work, order repository and web API controllers.

We add an IOrderRepository interface in Domain -> Repositories folder, which will inherit from the generic IRepository interface as the code below.

IOrderRepository.cs

C#
using EShoppingTutorial.Core.Domain.Entities;
using GenericRepositoryEntityFramework;
 
namespace EShoppingTutorial.Core.Domain.Repositories
{
    public interface IOrderRepository : IRepository<Order>
    {
 
    }
}

Notice that no repository implementation will be added to the application core domain. As I talked about it in the DDD section at the beginning of this article, application domain will remain pure and persistence agnostic, and we will only add the repository interfaces in application domain folder, as shown in the image below, the repository implementations are separated from the application core domain.

Image 21

As you see in the image below, the application core domain logic is dependent on repository Interface not on concrete implementation of repository, for more abstraction, and this means that the application logic is completely ignorant of any implementation of repository and data access concerns.

Image 22

Now we will add OrderRepository implementation in Persistence -> Repositories folder, which will inherit from the generic repository class as the code below. In order repository class, we can override base virtual methods or we can add new custom methods. For example, I have overridden the Add method of the base repository in order to change its behavior adds a new order. Also, we will have access to EShoppingTutorial DbContext.

OrderRepository.cs

C#
using System;
using GenericRepositoryEntityFramework;
using EShoppingTutorial.Core.Domain.Entities;
using EShoppingTutorial.Core.Domain.Repositories;
 
namespace EShoppingTutorial.Core.Persistence.Repositories
{
    public class OrderRepository : Repository<Order>, IOrderRepository
    {
        public OrderRepository(EShoppingTutorialDbContext context) : base(context)
        {
            
        }
 
        public EShoppingTutorialDbContext EShoppingTutorialDbContext
        {
            get { return Context as EShoppingTutorialDbContext; }
        }
 
        public override void Add(Order entity)
        {
            // We can override repository virtual methods in order to customize repository behavior, Template Method Pattern
            // Code here

            base.Add(entity);
        } 
    }
}

Writing Simple Unit Test for OrderRepository Add Method

All right, order repository implementation is done, so let’s go and write some simple unit tests for order repository add, and get methods.

So first, we have to mock Entity Framework's DbContext for Unit Testing (Mocking is an approach that we use in unit testing when our testing targets have external dependencies like: databases or others external services, for more information, you can search and read about it on the internet, there is lots of information about it.

So in order to mock EShoppingTutorial DbContext, we should add Entity Framework Core InMemory Database, and Microsoft Entity Framework Core NuGet Packages.

Right-click the EShoppingTutorial.UnitTests project and select Manage NuGet Packages, and add NuGet Packages.

All right, now it’s time to add a class called OrderRepositoryShould in EShoppingTutorial.UnitTests project -> Repositories folder, in order to test behaviors of order repository.

OrderRepositoryShould.cs

C#
using NUnit.Framework;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using EShoppingTutorial.Core.Persistence;
using EShoppingTutorial.Core.Domain.Entities;
using EShoppingTutorial.Core.Domain.ValueObjects;
using EShoppingTutorial.Core.Persistence.Repositories;
using EShoppingTutorial.Core.Domain.Enums;

namespace EShoppingTutorial.UnitTests.Repositories
{
    public class OrderRepositoryShould
    {
        private DbContextOptionsBuilder<EShoppingTutorialDbContext> _builder;

        private EShoppingTutorialDbContext _dbContext;

        private OrderRepository _orderRepository;


        [OneTimeSetUp]
        public void Setup()
        {
            _builder = new DbContextOptionsBuilder<EShoppingTutorialDbContext>()
                .UseInMemoryDatabase(databaseName: "Test_OrderRepository_Database");

            _dbContext = new EShoppingTutorialDbContext(_builder.Options);

            _orderRepository = new OrderRepository(_dbContext);
        }


        [Test]
        public async Task Test_MethodAdd_TrackingNumberMustNotBeNull_Ok()
        {
            // arrange
            var order = new Order("IRAN", new OrderItem[]
                                    {
                                        new OrderItem (3, new Price(2000, MoneyUnit.Euro))
                                    });

            // act

            _orderRepository.Add(order);

            var actualOrder = await _orderRepository.GetByIdAsync(1);

            // assert
            Assert.IsNotNull(actualOrder);

            Assert.IsNotNull(actualOrder.TrackingNumber);
        }


        [OneTimeTearDown]
        public void CleanUp()
        {
            _dbContext.Dispose();
        }
    }
}

Adding Unit of Work Interface and Implementation

After adding the repository, now it’s time to add a Unit of Work interface and implementation class. As before, we explained in this article, we only add interfaces in application domain, not the concrete implementations.

So we add an IUnitOfWork interface in Domain folder.

IUnitOfWork.cs

C#
using System.Threading;
using System.Threading.Tasks;
using EShoppingTutorial.Core.Domain.Repositories;
 
namespace EShoppingTutorial.Core.Domain
{
    public interface IUnitOfWork
    {
        IOrderRepository OrderRepository { get; }
 
        Task<int> CompleteAsync();
 
        Task<int> CompleteAsync(CancellationToken cancellationToken);
    }
}

Now we will add UnitOfWork implementation in Persistence folder. As you see in the code below, I’ve created a simple class that implements the IUnitOfWork and IAsyncDisposable, and at the time it only has one repository, if you in future need to add other repositories like: customer, or basket repository, just create and add them in this class.

As you see, this implementation of the Unit of Work pattern is not a too complex, it only contains the repositories, and a simple ComleteAsync method in order to save all application repository changes, and it also acts as a creational pattern and a placeholder, which will decrease the number of repository injections in the domain or application services, and will lead the application to stay simple and easy to maintain.

But I say again, one size does not fit all, if you have a simple application, or a micro service that in maximized state will only have eight or ten repositories, just keep it simple (kiss principle), and do the same, that we did.

UnitOfWork.cs

C#
using System;
using System.Threading;
using System.Threading.Tasks;
using EShoppingTutorial.Core.Domain;
using EShoppingTutorial.Core.Domain.Repositories;
using EShoppingTutorial.Core.Persistence.Repositories;
 
namespace EShoppingTutorial.Core.Persistence
{
    public class UnitOfWork : IUnitOfWork, IAsyncDisposable
    {
        private readonly EShoppingTutorialDbContext _context;
 
        public IOrderRepository OrderRepository { get; private set; }
 
 
        public UnitOfWork(EShoppingTutorialDbContext context)
        {
            _context = context;
 
            OrderRepository = new OrderRepository(_context);
        } 
 
        public async Task<int> CompleteAsync()
        {
            return await _context.SaveChangesAsync().ConfigureAwait(false);
        } 
 
        public async Task<int> CompleteAsync(CancellationToken cancellationToken)
        {
            return await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        } 
 
        /// <summary>
        /// No matter an exception has been raised or not, 
        /// this method always will dispose the DbContext 
        /// </summary>
        /// <returns></returns>
        public ValueTask DisposeAsync()
        {
            return _context.DisposeAsync();
        }
    }
}

Domain Services Layer

As Scott Millett in his book “Professional ASP.NET Design Patterns” says:

Methods that don’t really fit on a single entity or require access to the repository are contained within domain services. The domain service layer can also contain domain logic of its own and is as much part of the domain model as entities and value objects”.

So, maybe in real world scenarios, you will need to have domain services layer, but in this simple example, the domain services layer will be remaining empty.

Adding Application Services Layer (Web API Controllers)

In this simple example, we will inject IUnitOfWork interface directly in, application service layer, in ASP.NET web API controllers via the constructor, but before that, we must do some steps.

  1. Add AutoMapper and AutoMapper.Extensions.Microsoft.DependencyInjection NuGet Packages, for mapping DTO models.
  2. Add FluentValidation.AspNetCore NuGet Package, for validating DTO models.
  3. Configure IUnitOfWork and AutoMapper for injection, in Startup.cs class, like below:
    C#
    public void ConfigureServices(IServiceCollection services)
            {
                services
                    .AddMvcCore()
                    .AddApiExplorer()
                    .AddFluentValidation(s =>
                    {
                        s.RegisterValidatorsFromAssemblyContaining<Startup>();
                        s.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
                        s.AutomaticValidationEnabled = true;
                        s.ImplicitlyValidateChildProperties = true;
                    });
    
                // Register the Swagger services
                services.AddSwaggerDocument();
    
                services.AddDbContext<EShoppingTutorialDbContext>(opts => opts.UseSqlServer(Configuration["ConnectionStrings:EShoppingTutorialDB"]));
    
                services.AddScoped<IUnitOfWork, UnitOfWork>();
    
                services.AddAutoMapper(typeof(Startup));
            }}
  4. Create DTO models folder and name it "Models", then we add DTO models, and fluent validators, DTO mapping configurations, in order to config order entity to map order items via constructor, like code below:

     

PriceSaveRequestModel.cs

C#
using EShoppingTutorial.Core.Domain.Enums;

namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class PriceSaveRequestModel
    {
        /// <example>100</example>
        public int? Amount { get; set; }

        /// <example>MoneyUnit.Rial</example>
        public MoneyUnit? Unit { get; set; } = MoneyUnit.UnSpecified;
    }
}

 

PriceSaveRequestModelValidator.cs

C#
using FluentValidation;

namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class PriceSaveRequestModelValidator : AbstractValidator<PriceSaveRequestModel>
    {
        public PriceSaveRequestModelValidator()
        {
            RuleFor(x => x.Amount)
                .NotNull();

            RuleFor(x => x.Unit)
                .NotNull()
                .IsInEnum();
        }
    }
}

 

OrderItemSaveRequestModel.cs

C#
using System.ComponentModel.DataAnnotations;
using EShoppingTutorial.Core.Domain.ValueObjects;
 
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class OrderItemSaveRequestModel
    {
        /// <example>1</example>
        public int? ProductId { get; set; }
   
        public PriceSaveRequestModel Price { get; set; }
    }
}

 

PriceSaveRequestModelValidator.cs

C#
using FluentValidation;

namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class PriceSaveRequestModelValidator : AbstractValidator<PriceSaveRequestModel>
    {
        public PriceSaveRequestModelValidator()
        {
            RuleFor(x => x.Amount)
                .NotNull();

            RuleFor(x => x.Unit)
                .NotNull()
                .IsInEnum();
        }
    }
}

 

OrderSaveRequestModel.cs

C#
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
 
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class OrderSaveRequestModel
    {
        /// <example>IRAN Tehran Persia</example>
        public string ShippingAdress { get; set; }

        public IEnumerable<OrderItemSaveRequestModel> OrderItemsDtoModel { get; set; }
    }
}

 

OrderSaveRequestModelValidator.cs

C#
using FluentValidation;

namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class OrderSaveRequestModelValidator : AbstractValidator<OrderSaveRequestModel>
    {
        public OrderSaveRequestModelValidator()
        {
            RuleFor(x => x.ShippingAdress)
           .NotNull()
           .NotEmpty()
           .Length(2, 100);

            RuleFor(x => x.OrderItemsDtoModel)
           .NotNull().WithMessage("Please enter order items!");
        }
    }
}

 

OrderItemViewModel.cs

C#
using EShoppingTutorial.Core.Domain.ValueObjects;
 
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class OrderItemViewModel
    {
        public int Id { get; set; }
 
        public int ProductId { get; set; }
 
        public Price Price { get; set; }
    }
}

OrderViewModel.cs

C#
using System;
using System.Collections.Generic;
 
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
    public class OrderViewModel
    {
        public int Id { get; set; }
 
        public Guid? TrackingNumber { get; set; }
 
        public string ShippingAdress { get; set; }
 
        public DateTime OrderDate { get; set; }
 
        public IEnumerable<OrderItemViewModel> OrderItems { get; set; }
    }
}

So, we added a Order DTO models, now we must configure it, in order to config order entity to map order items via constructor, like code below:

OrderMappingProfile.cs

C#
using AutoMapper;
using EShoppingTutorial.Core.Domain.Entities;
using EShoppingTutorial.Core.Domain.ValueObjects;
using EShoppingTutorialWebAPI.Models.OrderModels;
using System.Collections.Generic;

namespace EShoppingTutorialWebAPI.Models.DtoMappingConfigs
{
    public class OrderMappingProfile : Profile
    {
        public OrderMappingProfile()
        {
            CreateMap<Order, OrderViewModel>();

            CreateMap<OrderSaveRequestModel, Order>()
            .ConstructUsing((src, res) =>
            {
                return new Order(src.ShippingAdress, orderItems: res.Mapper.Map<IEnumerable<OrderItem>>(src.OrderItemsDtoModel)
                );
            });

            CreateMap<OrderItem, OrderItemViewModel>();

            CreateMap<OrderItemSaveRequestModel, OrderItem>();

            CreateMap<PriceSaveRequestModel, Price>().ConvertUsing(x => new Price(x.Amount.Value, x.Unit.Value));
        }
    }
}
  1. Right click under controllers folder, and add a new web API empty controller, and name it OrderController, and add then add Get, GetPage, Post and Delete actions. Notice that in controller class, we injected only the IunitOfWork and Automapper, like the code below:

    OrderController.cs

    C#
    using AutoMapper;
    using Microsoft.AspNetCore.Mvc;
    using EShoppingTutorial.Core.Domain;
    using EShoppingTutorial.Core.Domain.Entities;
    using EShoppingTutorialWebAPI.Models.OrderModels;
    using SharedKernel.Models;
     
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using System.Linq;
     
    namespace EShoppingTutorialWebAPI.Controllers
    {
        [ApiController]
        [Produces("application/json")]
        [Route("api/[controller]")]
        public class OrderController : ControllerBase
        {
            private readonly IUnitOfWork _unitOfWork;
            private readonly IMapper _mapper;
     
            public OrderController(IUnitOfWork unitOfWork, IMapper mapper)
            {
                _unitOfWork = unitOfWork;
                _mapper = mapper;
            }
     
            [HttpGet]
            [Route("{id}")]
            public async Task<IActionResult> GetOrder(int id)
            {
                var order = 
                  await _unitOfWork.OrderRepository.GetByIdAsync(id).ConfigureAwait(false);
     
                if (order == null)
                    return NotFound();
     
                var mappedOrder = _mapper.Map<OrderViewModel>(order);
     
                return Ok(mappedOrder);
            } 
     
            [HttpGet]
            [Route("GetAll")]
            public async Task<IActionResult> GetAll()
            {
                var orders = await _unitOfWork.OrderRepository.GetAllAsync
                             (en => en.OrderItems).ConfigureAwait(false);
     
                if (orders is null)
                    return NotFound();
     
                var mappedOrders = _mapper.Map<IEnumerable<OrderViewModel>>(orders);
     
                return Ok(new QueryResult<OrderViewModel>
                         (mappedOrders, mappedOrders.Count()));
            } 
     
            [HttpPost]
            [Route("GetPaged")]
            public async Task<IActionResult> 
                   GetPaged([FromBody] QueryObjectParams queryObject)
            {
                var queryResult = await _unitOfWork.OrderRepository.GetPageAsync
                                  (queryObject).ConfigureAwait(false);
     
                if (queryResult is null)
                    return NotFound();
     
                var mappedOrders = 
                    _mapper.Map<IEnumerable<OrderViewModel>>(queryResult.Entities);
     
                return Ok(new QueryResult<OrderViewModel>
                         (mappedOrders, queryResult.TotalCount));
            } 
     
            [HttpPost]
            [Route("Add")]
            public async Task<IActionResult> Add([FromBody] 
                         OrderSaveRequestModel orderResource)
            {
                var order = _mapper.Map<OrderSaveRequestModel, Order>(orderResource);
     
                _unitOfWork.OrderRepository.Add(order);
     
                await _unitOfWork.CompleteAsync().ConfigureAwait(false);
     
                return Ok();
            }
     
            [HttpDelete]
            [Route("{id}")]
            public async Task<IActionResult> Delete(int id)
            {
                var order = await _unitOfWork.OrderRepository.
                            GetByIdAsync(id).ConfigureAwait(false);
     
                if (order is null)
                    return NotFound();
     
                _unitOfWork.OrderRepository.Remove(order);
     
                await _unitOfWork.CompleteAsync().ConfigureAwait(false);
     
                return Ok();
            }
        }
    }

Alright, we are almost done, now we can run and call the API services, with the postman or swagger. I've configured swagger and tested the API services, below is the demo. You can also download source code, review the codes and test the result.

Image 23

Conclusion

Alright, finally we're done! In this article, we've tried to focus and concentrate on application domain logic in domain models and domain services. We followed up lots of topics like: domain driven, test driven, loosely coupled programming, to have a clean, easy to understand, and maintainable application, that will demonstrate the application's behavior and business logic in its domain models in an object oriented manner (self intention reveals application). In complex applications that we would have tens or hundreds of entities, DDD will help us to tackle the complexities, and will lead us to have an object oriented, clean and easy understand application. But regardless of all the efforts that we put in in this article, my motto is that: "one size does not fit all". If you really have a simple data driven application, with minimum business logic, that you are sure it will rarely change in future, so don't cheat yourself and don't follow the DDD rules, just keep it simple (KISS principle).

History

  • 5th March, 2021: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)