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.
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.
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.
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.
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:
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
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.
In the NuGet form, click on Browse tab, and enter text “EntityFrameworkCore
” in search criteria filter text box, as shown in the image below:
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:
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
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
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:
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:
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.
In the NuGet form, click on Browse tab, and enter text “EntityFrameworkCore
” in search criteria filter text box, as shown in the image below:
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
.
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
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
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
using EShoppingTutorial.Core.Domain.Enums;
using SharedKernel.Exceptions;
using System.ComponentModel.DataAnnotations.Schema;
namespace EShoppingTutorial.Core.Domain.ValueObjects
{
[ComplexType]
public class Price
{
protected Price()
{
}
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
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()
{
}
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 OrderItem
s data in application will be via this aggregate root rich entity model.
As you see below, the OrderItem
s 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 OrderItem
s 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
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()
{
_orderItems = new List<OrderItem>();
}
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);
}
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:
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:
OrderShould
class will contain unit tests for order entity.
OrderShould.cs
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()
{
TestDelegate testDelegate = () => new Order("IRAN", new OrderItem[] { });
var ex = Assert.Throws<BusinessRuleBrokenException>(testDelegate);
}
[Test]
public void Test_OrderItemsProperty_AddingOrderItemToReadOnlyCollection_ExpectsNotSupportedException()
{
var order = new Order("IRAN", new OrderItem[] { new OrderItem(1, new Price(1, MoneyUnit.Dollar)) });
TestDelegate testDelegate = () => order.OrderItems.Add(new OrderItem(1, new Price(1, MoneyUnit.Dollar)));
var ex = Assert.Throws<NotSupportedException>(testDelegate);
}
[Test]
public void Test_InstantiateOrder_WithOrderItems_ThatExccedsTotalPriceOf_10000_Dollar_ExpectsBusinessRuleBrokenException()
{
var orderItem1 = new OrderItem(1, new Price (5000, MoneyUnit.Dollar));
var orderItem2 = new OrderItem(2, new Price(6000, MoneyUnit.Dollar));
TestDelegate testDelegate = () =>
{
new Order("IRAN",new OrderItem[] { orderItem1, orderItem2 });
};
var ex = Assert.Throws<BusinessRuleBrokenException>(testDelegate);
Assert.That(ex.Message.ToLower().Contains("maximum price"));
}
[Test]
public void Test_InstantiateOrder_WithOrderItems_ThatExccedsTotalPriceOf_9000_Euro_ExpectsBusinessRuleBrokenException()
{
var orderItem1 = new OrderItem(1, new Price(5000, MoneyUnit.Dollar));
var orderItem2 = new OrderItem(2, new Price(6000, MoneyUnit.Dollar));
TestDelegate testDelegate = () =>
{
new Order("IRAN", new OrderItem[] { orderItem1, orderItem2 });
};
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
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
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
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)
{
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:
In the next form, select API template, and remove tick for Configure for Https as shown in the image below:
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
{
"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
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:
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.
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:
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
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.
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.
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
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)
{
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
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()
{
var order = new Order("IRAN", new OrderItem[]
{
new OrderItem (3, new Price(2000, MoneyUnit.Euro))
});
_orderRepository.Add(order);
var actualOrder = await _orderRepository.GetByIdAsync(1);
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
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
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);
}
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.
- Add
AutoMapper
and AutoMapper.Extensions.Microsoft.DependencyInjection
NuGet Packages, for mapping DTO models. - Add
FluentValidation.AspNetCore
NuGet Package, for validating DTO models. - Configure
IUnitOfWork
and AutoMapper
for injection, in Startup.cs class, like below:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvcCore()
.AddApiExplorer()
.AddFluentValidation(s =>
{
s.RegisterValidatorsFromAssemblyContaining<Startup>();
s.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
s.AutomaticValidationEnabled = true;
s.ImplicitlyValidateChildProperties = true;
});
services.AddSwaggerDocument();
services.AddDbContext<EShoppingTutorialDbContext>(opts => opts.UseSqlServer(Configuration["ConnectionStrings:EShoppingTutorialDB"]));
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddAutoMapper(typeof(Startup));
}}
- 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
using EShoppingTutorial.Core.Domain.Enums;
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
public class PriceSaveRequestModel
{
public int? Amount { get; set; }
public MoneyUnit? Unit { get; set; } = MoneyUnit.UnSpecified;
}
}
PriceSaveRequestModelValidator.cs
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
using System.ComponentModel.DataAnnotations;
using EShoppingTutorial.Core.Domain.ValueObjects;
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
public class OrderItemSaveRequestModel
{
public int? ProductId { get; set; }
public PriceSaveRequestModel Price { get; set; }
}
}
PriceSaveRequestModelValidator.cs
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
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace EShoppingTutorialWebAPI.Models.OrderModels
{
public class OrderSaveRequestModel
{
public string ShippingAdress { get; set; }
public IEnumerable<OrderItemSaveRequestModel> OrderItemsDtoModel { get; set; }
}
}
OrderSaveRequestModelValidator.cs
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
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
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
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));
}
}
}
- 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
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.
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