TRUNGTQ

Think Big, Act Small, Fail Fast and Learn Rapidly

NAVIGATION - SEARCH

EF Core for Enterprise

Source Code: https://github.com/hherzl/EfCoreForEnterprise

Introduction

The design for enterprise architect is a common question in software development and how we can solve this issue in the best way following best practices.

In this guide, we'll take a look at the common requirements for design of enterprise architect.

Background

The architecture for enterprise application should have the following levels:

  1. Entity Layer: Contains entities (POCOs)
  2. Data Layer: Contains all code related to database access
  3. Business Layer: Contains definitions and validations related to business
  4. Services Layer (optional): Contains invocations for external services (ASMX, WCF, RESTful)
  5. Common: Contains common classes and interfaces for all layers (e.g. Loggers, Mappers, Extensions)
  6. Tests (QA): Contains automated tests
  7. Presentation Layer: This is the UI

Skills Prerequisites

  • OOP (Object Oriented Programming)
  • AOP (Aspect Oriented Programming)
  • ORM (Object Relational Mapping)
  • Design Patterns: Domain Driven Design, Repository & Unit of Work and IoC

Software Prerequisites

  • Visual Studio 2015 with Update 3
  • Local SQL Server instance

Using the Code

Step 01 - Create Database

In this guide, we'll use a sample database to understand each component in our architecture. This is the script for database:

use master
go
create database Store
go

use Store
go

create schema HumanResources
go

create schema Production
go

create schema Sales
go

create table [EventLog]
(
	[EventLogID] int not null identity(1, 1),
	[EventType] int not null,
	[Key] varchar(255) not null,
	[Message] varchar(max) not null,
	[EntryDate] datetime not null
)

create table [ChangeLog]
(
	[ChangeLogID] int not null identity(1, 1),
	[ClassName] varchar(255) not null,
	[PropertyName] varchar(255) not null,
	[OriginalValue] varchar(max) null,
	[CurrentValue] varchar(max) null,
	[UserName] varchar(25) not null,
	[ChangeDate] datetime not null
)

create table [HumanResources].[Employee]
(
	[EmployeeID] int not null identity(1, 1),
	[FirstName] varchar(25) not null,
	[MiddleName] varchar(25) null,
	[LastName] varchar(25) not null,
	[BirthDate] datetime not null
)

create table [Production].[ProductCategory]
(
	[ProductCategoryID] int not null identity(1, 1),
	[ProductCategoryName] varchar(100) not null
)

create table [Production].[Product]
(
	[ProductID] int not null identity(1, 1),
	[ProductName] varchar(100) not null,
	[ProductCategoryID] int not null,
	[UnitPrice] decimal(8, 4) not null,
	[Description] varchar(255) null,
	[Discontinued] bit not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null
)

create table [Production].[ProductInventory]
(
	[ProductInventoryID] int not null identity(1, 1),
	[ProductID] int not null,
	[EntryDate] datetime not null,
	[Quantity] int not null,
	[Stocks] int not null
)

create table [Sales].[Customer]
(
	[CustomerID] int not null identity(1, 1),
	[CompanyName] varchar(100) null,
	[ContactName] varchar(100) null
)

create table [Sales].[Shipper]
(
	[ShipperID] int not null identity(1, 1),
	[CompanyName] varchar(100) null,
	[ContactName] varchar(100) null
)

create table [Sales].[OrderStatus]
(
	[OrderStatusID] smallint not null identity(100, 100),
	[Description] varchar(100) not null
)

create table [Sales].[Order]
(
	[OrderID] int not null identity(1, 1),
	[OrderStatusID] smallint not null,
	[OrderDate] datetime not null,
	[CustomerID] int not null,
	[EmployeeID] int not null,
	[ShipperID] int not null,
	[Total] decimal(12, 4) not null,
	[Comments] varchar(255) null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null
)

create table [Sales].[OrderDetail]
(
	[OrderID] int not null,
	[ProductID] int not null,
	[ProductName] varchar(255) not null,
	[UnitPrice] decimal(8, 4) not null,
	[Quantity] int not null,
	[Total] decimal(8, 4) not null
)
go

alter table [EventLog]
	add constraint EventLog_PK primary key (EventLogID)
go

alter table [ChangeLog]
	add constraint ChangeLog_PK primary key (ChangeLogID)
go

alter table [HumanResources].[Employee]
	add constraint HumanResources_Employee_PK primary key (EmployeeID)
go

alter table [Production].[ProductCategory]
	add constraint Production_ProductCategory_PK primary key (ProductCategoryID)
go

alter table [Production].[Product]
	add constraint Production_Product_PK primary key (ProductID)
go

alter table [Production].[Product]
	add constraint Production_Product_ProductName unique (ProductName)
go

alter table [Production].[ProductInventory]
	add constraint Production_ProductInventory_PK primary key (ProductInventoryID)
go

alter table [Sales].[Customer]
	add constraint Sales_Customer_PK primary key (CustomerID)
go

alter table [Sales].[Shipper]
	add constraint Sales_Shipper_PK primary key (ShipperID)
go

alter table [Sales].[OrderStatus]
	add constraint Sales_OrderStatus_PK primary key (OrderStatusID)
go

alter table [Sales].[Order]
	add constraint Sales_Order_PK primary key (OrderID)
go

alter table [Sales].[OrderDetail]
	add constraint Sales_OrderDetail_PK primary key (OrderID, ProductID)
go

alter table [Production].[Product]
	add constraint Production_Product_ProductCategory foreign key (ProductCategoryID)
		references [Production].[ProductCategory]
go

alter table [Production].[ProductInventory]
	add constraint Production_ProductInventory_Product foreign key (ProductID)
		references [Production].[Product]
go

alter table [Sales].[Order]
	add constraint Sales_Order_OrderStatus foreign key (OrderStatusID)
		references [Sales].[OrderStatus]
go

alter table [Sales].[Order]
	add constraint Sales_Order_Customer foreign key (CustomerID)
		references [Sales].[Customer]
go

alter table [Sales].[Order]
	add constraint Sales_Order_Employee foreign key (EmployeeID)
		references [HumanResources].[Employee]
go

alter table [Sales].[Order]
	add constraint Sales_Order_Shipper foreign key (ShipperID)
		references [Sales].[Shipper]
go

alter table [Sales].[OrderDetail]
	add constraint Sales_OrderDetail_Order foreign key (OrderID)
		references [Sales].[Order]
go

alter table [Sales].[OrderDetail]
	add constraint Sales_OrderDetail_Product foreign key (ProductID)
		references [Production].[Product]
go

create view OrderSummary
as
	select
		OrderHeader.OrderID,
		OrderHeader.OrderDate,
		Customer.CompanyName as CustomerName,
		Employee.FirstName + ' ' +
		isnull(Employee.MiddleName, '') + ' ' +
		Employee.LastName as EmployeeName,
		Shipper.CompanyName as ShipperName
	from
		Sales.[Order] OrderHeader
		inner join Sales.Customer Customer
		    on OrderHeader.CustomerID = Customer.CustomerID
		inner join HumanResources.Employee Employee
		    on OrderHeader.EmployeeID = Employee.EmployeeID
		inner join Sales.Shipper Shipper
		    on OrderHeader.ShipperID = Shipper.ShipperID
go

insert into [HumanResources].[Employee] values ('John', null, 'Doe', getdate())
go

insert [Production].[ProductCategory] values ('PS4 Games')
go

declare @userName varchar(25)
select @userName = 'seed'

insert into [Production].[Product] values
    ('King of Fighters XIV', 1, 59.99, 'KOF XIV', 0, @userName, getdate(), null, null)
insert into [Production].[Product] values
    ('Street Fighter V', 1, 49.99, 'SF V', 0, @userName, getdate(), null, null)
insert into [Production].[Product] values
    ('Guilty Gear', 1, 39.99, 'GG', 0, @userName, getdate(), null, null)
go

insert into [Production].[ProductInventory] values (1, getdate(), 100000, 100000)
insert into [Production].[ProductInventory] values (2, getdate(), 100000, 100000)
go

insert into [Sales].[Customer] values ('Best Buy', 'Colleen Dunn')
insert into [Sales].[Customer] values ('Circuit City', 'Bill McCorey')
insert into [Sales].[Customer] values ('Game Stop', 'Michael Cooper')
go

insert into [Sales].[Shipper] values ('DHL', 'Ricardo A. Bartra')
insert into [Sales].[Shipper] values ('FedEx', 'Rob Carter')
insert into [Sales].[Shipper] values ('UPS', 'Juan R. Perez')
go

insert into [Sales].[OrderStatus] values ('Created')
go

Run script on SQL Server instance, now we can generate a database diagram like this:

Database diagram

This is a simple database, only to demonstrate concepts.

Once we have the database, we proceed to define a naming convention:

IdentifierCaseExample
NamespacePascalCaseAdventureWorks
Class namePascalCaseProductViewModel
Interface nameI prefix + PascalCaseIDatabaseValidator
Method namePascalCaseGetOrders
Property namePascalCaseDescription
Parameter namecamelCaseconnectionString

This convention is very important because it defines the naming guidelines for our architecture.

Step 02 - Core Project

Create a project with name Store.Core for DotNet Core and add the folloging directories:

  1. EntityLayer
  2. DataLayer
  3. DataLayer\Contracts
  4. DataLayer\DataContracts
  5. DataLayer\Mapping
  6. DataLayer\Repositories
  7. BusinessLayer
  8. BusinessLayer\Responses

Inside of Entitylayer, we'll place all entities, in this context, entity means a class that represents a table or view from database, sometimes entity is named POCO (Plain Old Common language runtime Object) than means a class with only properties not methods nor other things (events); according to wkempf feedback it's necessary to be clear about POCOs, POCOs can have methods and events and other members but it's not common to add those members in POCOs.

Inside of DataLayer, we'll place DbContext and AppSettings because they're common classes for DataLayer.

Inside of DataLayer\Contracts, we'll place all interfaces that represent operations catalog, we're focusing on schemas and we'll create one interface per schema and Store contract for default schema (dbo).

Inside of DataLayer\DataContracts, we'll place all object definitions for returned values from Contractsnamespace, for now this directory would be empty.

Inside of DataLayer\Mapping, we'll place all object definition related to mapping a class for database access.

Inside of DataLayer\Repositories, we'll place the implementations for Contracts definitons.

Inside of EntityLayer and DataLayer\Mapping, we'll create one directory per schema without including the default schema.

Inside of BusinessLayer, we'll create the interfaces and implementations for business objects, in this case, the business objects will contain the methods according to use cases (or something similar) and that methods must handle exceptions and other validations related to business.

EntityLayer structure:

Entity layer structure

DataLayer structure:

Data layer structure

One repository includes operations related to that schema, so we have 4 repositories: HumanResources, Production, Sales and Store.

There is only one class for mapping, so there is a class with name StoreEntityMapper because there aren't Focused DbContexts.

BusinessLayer structure:

Business layer structure

We'll inspect the code for understanding the concepts but the inspection would be with one object per level because the remaining code is similar.

Architecture: Big Picture

STORAGE (DATABASE)SQL Server 
ENTITY LAYERPOCOsBACK-END
DATA LAYERDbContext, Mappings, Contracts, Data Contracts
BUSINESS LAYERContracts, DataContracts, Exceptions and Loggers
SERVICES LAYERASMX, WCF, RESTful
COMMONLoggers, Mappers, Extensions
PRESENTATION LAYERUI Frameworks (AngularJS | ReactJS)FRONT-END
USER  

Entity Layer

Order class code:

using System;
using System.Collections.ObjectModel;

namespace Store.Core.EntityLayer.Sales
{
    public class Order : IAuditEntity
    {
        public Order()
        {
            OrderDetails = new Collection<OrderDetail>();
        }

        public Int32? OrderID { get; set; }

        public Int16? OrderStatusID { get; set; }

        public DateTime? OrderDate { get; set; }

        public Int32? CustomerID { get; set; }

        public Int32? EmployeeID { get; set; }

        public Int32? ShipperID { get; set; }

        public Decimal? Total { get; set; }

        public String Comments { get; set; }

        public String CreationUser { get; set; }

        public DateTime? CreationDateTime { get; set; }

        public String LastUpdateUser { get; set; }

        public DateTime? LastUpdateDateTime { get; set; }

        public virtual OrderStatus OrderStatusFk { get; set; }

        public virtual Collection<OrderDetail> OrderDetails { get; set; }
    }
}

Please take a look at POCOs, we're using nullable types instead of native types because nullable are easy to evaluate if property has value or not, that's more similar to database model.

In EntityLayer there are two interfaces: IEntity and IAuditEntity, IEntity represents all entities in our application and IAuditEntity represents all entities that allows to save audit information: create and last update; as special point if we have mapping for views, those classes do not implement IAuditEntity because a view doesn't allow insert, update and elete operations.

Data Layer

StoreDbContext class code:

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Store.Core.DataLayer.Mapping;

namespace Store.Core.DataLayer
{
	public class StoreDbContext : Microsoft.EntityFrameworkCore.DbContext
	{
		public StoreDbContext(IOptions<AppSettings> appSettings, IEntityMapper entityMapper)
		{
			ConnectionString = appSettings.Value.ConnectionString;
			EntityMapper = entityMapper;
		}

		public String ConnectionString { get; }

		public IEntityMapper EntityMapper { get; }

		protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
		{
			optionsBuilder.UseSqlServer(ConnectionString);
			
			base.OnConfiguring(optionsBuilder);
		}

		protected override void OnModelCreating(ModelBuilder modelBuilder)
		{
			EntityMapper.MapEntities(modelBuilder);
			
			base.OnModelCreating(modelBuilder);
		}
	}
}

OrderMap class code:

using Microsoft.EntityFrameworkCore;
using Store.Core.EntityLayer.Sales;

namespace Store.Core.DataLayer.Mapping.Sales
{
    public class OrderMap : IEntityMap
    {
        public void Map(ModelBuilder modelBuilder)
        {
            var entity = modelBuilder.Entity<Order>();

            entity.ToTable("Order", "Sales");

            entity.HasKey(p > p.OrderID);

            entity.Property(p => p.OrderID).UseSqlServerIdentityColumn();
        }
    }
}

Repository class code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Store.Core.EntityLayer;

namespace Store.Core.DataLayer.Repositories
{
    public abstract class Repository
    {
        protected IUserInfo UserInfo;
        protected StoreDbContext DbContext;

        public Repository(IUserInfo userInfo, StoreDbContext dbContext)
        {
            UserInfo = userInfo;
            DbContext = dbContext;
        }

        protected IQueryable<TEntity> Paging<TEntity>(Int32 pageSize = 0, Int32 pageNumber = 0) where TEntity : class
        {
            var query = DbContext.Set<TEntity>().AsQueryable();

            return pageSize > 0 && pageNumber > 0 ? query.Skip((pageNumber - 1) * pageSize).Take(pageSize) : query;
        }

        protected Task<IQueryable<TEntity>> PagingAsync<TEntity>(Int32 pageSize = 0, Int32 pageNumber = 0) where TEntity : class
        {
            var query = DbContext.Set<TEntity>().AsQueryable();
            
            return Task.FromResult<IQueryable<TEntity>>(pageSize > 0 && pageNumber > 0 ? query.Skip((pageNumber - 1) * pageSize).Take(pageSize) : query);
        }

        protected virtual void Add(IEntity entity)
        {
            var cast = entity as IAuditEntity;

            if (cast != null)
            {
                cast.CreationUser = UserInfo.Name;

                if (!cast.CreationDateTime.HasValue)
                {
                    cast.CreationDateTime = DateTime.Now;
                }
            }
        }

        protected virtual void Update(IEntity entity)
        {
            var cast = entity as IAuditEntity;

            if (cast != null)
            {
                cast.LastUpdateUser = UserInfo.Name;

                if (!cast.LastUpdateDateTime.HasValue)
                {
                    cast.LastUpdateDateTime = DateTime.Now;
                }
            }
        }

        protected virtual IEnumerable<ChangeLog> GetChanges()
        {
            foreach (var entry in DbContext.ChangeTracker.Entries())
            {
                if (entry.State == EntityState.Modified)
                {
                    var entityType = entry.Entity.GetType();

                    foreach (var property in entityType.GetTypeInfo().DeclaredProperties)
                    {
                        var originalValue = entry.Property(property.Name).OriginalValue;
                        var currentValue = entry.Property(property.Name).CurrentValue;

                        if (String.Concat(originalValue) != String.Concat(currentValue))
                        {
                            yield return new ChangeLog
                            {
                                ClassName = entityType.Name,
                                PropertyName = property.Name,
                                OriginalValue = originalValue == null ? String.Empty : originalValue.ToString(),
                                CurrentValue = currentValue == null ? String.Empty : currentValue.ToString(),
                                UserName = UserInfo.Name,
                                ChangeDate = DateTime.Now
                            };
                        }
                    }
                }
            }
        }

        public Int32 CommitChanges()
        {
            var dbSet = DbContext.Set<ChangeLog>();

            foreach (var change in GetChanges().ToList())
            {
                dbSet.Add(change);
            }

            return DbContext.SaveChanges();
        }

        public Task<Int32> CommitChangesAsync()
        {
            var dbSet = DbContext.Set<ChangeLog>();

            foreach (var change in GetChanges().ToList())
            {
                dbSet.Add(change);
            }

            return DbContext.SaveChangesAsync();
        }
    }
}

How about Unit of Work? in EF 6.x was usually create a repository class and unit of work class: repository provided operations for database access and unit of work provided operations to save changes in database; but in EF Core it's a common practice to have only repositories and no unit of work; anyway for this code we have added two methods in Repository class: CommitChanges and CommitChangesAsync, so just to make sure that inside of all data writing mehotds in repositories call CommitChanges or CommitChangesAsync and with that design we have two definitions working on our architecture.

On DbContext for this version, we're using DbSet on the fly instead of declaring DbSet properties in DbContext. I think that it's more about architect preferences I prefer to use on the fly DbSet because I don't worry about adding all DbSets to DbContext but this style would be changed if you considered it's more accurate to use declarated DbSet properties in DbContext.

How about async operations? In previous versions of this post I said we'll implement async operations in the last level: REST API, but I was wrong about that because .NET Core it's more about async programming, so the best decision is handle all database operations in async way using the Async methods that EF Core provides.

We can take a look on Repository class, there are two methods: Add and Update, for this example Order class has audit properties: CreationUser, CreationDateTime, LastUpdateUser and LastUpdateDateTimealso Order class implements IAuditEntity interface, that interface is used to set values for audit properties

For the current version of this article, we going to omit the services layer but in some cases, there is a layer that includes the connection for external services (ASMX, WCF and RESTful).

Data Layer: Stored Procedures versus LINQ Queries

In data layer, there is a very interesting point: How we can use stored procedures? For the current version of EF Core, there isn't support for stored procedures, so we can't use them in a native way, inside of DbSet, there is a method to execute a query but that works for stored procedures not return a result set (columns), we can add some extension methods and add packages to use classic ADO.NET, so in that case we need to handle the dynamic creation of objects to represent the stored procedure result; that makes sense? if we consume a procedure with name GetOrdersByMonth and that procedure returns a select with 7 columns, to handle all results in the same way, we'll need to define objects to represent those results, that objects must define inside of DataLayer\DataContracts namespace according to our naming convention.

Inside of enterprise environment, a common discussion is about LINQ queries or stored procedures. According to my experience, I think the best way to solve that question is: review design conventions with architect and database administrator; nowadays, it's more common to use LINQ queries in async mode instead of storedprocedures but sometimes some companies have restrict conventions and do not allow to use LINQ queries, so it's required to use stored procedure and we need to make our architecture flexible because we don't say to developer manager "the business logic will be rewrite because EF Core doesn't allow to invoke storedprocedures"

As we can see until now, assuming we have the extension methods for EF Core to invoke stored procedures and data contracts to represent results from stored procedures invocations, Where do we place those methods? It's preferable to use the same convention so we'll add those methods inside of contracts and repositories; just to be clear if we have procedures named Sales.GetCustomerOrdersHistory and HumanResources.DisableEmployee; we must to place methods inside of Sales and HumanResourcesrepositories.

The previous concept applies in the same way for views in database. In addition, we only need to check that repositories do not allow add, update and delete operations for views.

Change Tracking: inside of Repository class there is a method with name GetChanges, that method get all changes from DbContext through ChangeTracker and returns all changes, so those values are saved in ChangeLog table in CommitChanges method. You can update one existing entity with business object, later you can check your ChangeLog table:

ChangeLogID ClassName   PropertyName  OriginalValue  CurrentValue  UserName   ChangeDate
----------- ----------- ------------- -------------- ------------- ---------- -----------------------
1           Employee    MiddleName                   Someone       admin      2017-01-09 02:00:42.810
2           Employee    MiddleName    Someone        Smith         admin      2017-01-11 02:32:52.447

As we can see all changes made in entities will be saved on this table, as a future improvement we'll need to add exclusions for this change log. In this guide we're working with SQL Server, as I know there is a way to enable change tracking from database side but in this post I'm showing to you how you can implement this feature from back-end; if this feature is on back-end or database side will be a decision from your leader. In the timeline we can check on this table all changes in entities, some entities have audit properties but those properties only reflect the user and date for creation and last update but do not provide full details about how data change.

Business Layer

BusinessObject class code:

using System;
using Store.Core.BusinessLayer.Contracts;
using Store.Core.Common;
using Store.Core.DataLayer;
using Store.Core.DataLayer.Contracts;
using Store.Core.DataLayer.Repositories;

namespace Store.Core.BusinessLayer
{
    public abstract class BusinessObject : IBusinessObject
    {
        protected ILog Logger;
        protected IUserInfo UserInfo;
        protected Boolean Disposed;
        protected StoreDbContext DbContext;
        protected IHumanResourcesRepository m_humanResourcesRepository;
        protected IProductionRepository m_productionRepository;
        protected ISalesRepository m_salesRepository;

        public BusinessObject(IUserInfo userInfo, StoreDbContext dbContext)
        {
            Logger = new Log();
            UserInfo = userInfo;
            DbContext = dbContext;
        }

        public void Dispose()
        {
            if (!Disposed)
            {
                if (DbContext != null)
                {
                    DbContext.Dispose();

                    Disposed = true;
                }
            }
        }

        protected IHumanResourcesRepository HumanResourcesRepository
        {
            get
            {
                return m_humanResourcesRepository ?? (m_humanResourcesRepository = new HumanResourcesRepository(UserInfo, DbContext));
            }
        }

        protected IProductionRepository ProductionRepository
        {
            get
            {
                return m_productionRepository ?? (m_productionRepository = new ProductionRepository(UserInfo, DbContext));
            }
        }

        protected ISalesRepository SalesRepository
        {
            get
            {
                return m_salesRepository ?? (m_salesRepository = new SalesRepository(UserInfo, DbContext));
            }
        }
    }
}

SalesBusinessObject class code:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Store.Core.BusinessLayer.Contracts;
using Store.Core.BusinessLayer.Responses;
using Store.Core.DataLayer;
using Store.Core.EntityLayer.Production;
using Store.Core.EntityLayer.Sales;

namespace Store.Core.BusinessLayer
{
    public class SalesBusinessObject : BusinessObject, ISalesBusinessObject
    {
        public SalesBusinessObject(IUserInfo userInfo, StoreDbContext dbContext)
            : base(userInfo, dbContext)
        {
        }

        public async Task<IListModelResponse<Customer>> GetCustomersAsync(Int32 pageSize, Int32 pageNumber)
        {
            var response = new ListModelResponse<Customer>() as IListModelResponse<Customer>;

            try
            {
                response.Model = await SalesRepository.GetCustomers(pageSize, pageNumber).ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(ex, Logger);
            }

            return response;
        }

        public async Task<IListModelResponse<Shipper>> GetShippersAsync(Int32 pageSize, Int32 pageNumber)
        {
            var response = new ListModelResponse<Shipper>() as IListModelResponse<Shipper>;

            try
            {
                response.Model = await SalesRepository.GetShippers(pageSize, pageNumber).ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(ex, Logger);
            }

            return response;
        }

        public async Task<IListModelResponse<Order>> GetOrdersAsync(Int32 pageSize, Int32 pageNumber)
        {
            var response = new ListModelResponse<Order>() as IListModelResponse<Order>;

            try
            {
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;

                response.Model = await SalesRepository.GetOrders(pageSize, pageNumber).ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(ex, Logger);
            }

            return response;
        }

        public async Task<ISingleModelResponse<Order>> GetOrderAsync(Int32 id)
        {
            var response = new SingleModelResponse<Order>() as ISingleModelResponse<Order>;

            try
            {
                response.Model = await SalesRepository.GetOrderAsync(new Order { OrderID = id });
            }
            catch (Exception ex)
            {
                response.SetError(ex, Logger);
            }

            return response;
        }

        public async Task <ISingleModelResponse<Order>> CreateOrderAsync(Order header, OrderDetail[] details)
        {
            var response = new SingleModelResponse<Order>() as ISingleModelResponse<Order>;

            try
            {
                using (var transaction = await DbContext.Database.BeginTransactionAsync())
                {
                    try
                    {
                        foreach (var detail in details)
                        {
                            var product = await ProductionRepository.GetProductAsync(new Product { ProductID = detail.ProductID });

                            if (product == null)
                            {
                                throw new NonExistingProductException(
                                    String.Format("Sent order has a non existing product with ID: '{0}', order has been cancelled.", detail.ProductID)
                                );
                            }
                            else
                            {
                                detail.ProductName = product.ProductName;
                            }

                            if (product.Discontinued == true)
                            {
                                throw new AddOrderWithDiscontinuedProductException(
                                    String.Format("Product with ID: '{0}' is discontinued, order has been cancelled.", product.ProductID)
                                );
                            }

                            detail.UnitPrice = product.UnitPrice;
                            detail.Total = product.UnitPrice * detail.Quantity;
                        }

                        header.Total = details.Sum(item => item.Total);

                        await SalesRepository.AddOrderAsync(header);

                        foreach (var detail in details)
                        {
                            detail.OrderID = header.OrderID;

                            await SalesRepository.AddOrderDetailAsync(detail);

                            var lastInventory = ProductionRepository
                                .GetProductInventories()
                                .Where(item => item.ProductID == detail.ProductID)
                                .OrderByDescending(item => item.EntryDate)
                                .FirstOrDefault();

                            var stocks = lastInventory == null ? 0 : lastInventory.Stocks - detail.Quantity;

                            var productInventory = new ProductInventory
                            {
                                ProductID = detail.ProductID,
                                EntryDate = DateTime.Now,
                                Quantity = detail.Quantity * -1,
                                Stocks = stocks
                            };

                            await ProductionRepository.AddProductInventoryAsync(productInventory);
                        }

                        response.Model = header;

                        transaction.Commit();
                    }
                    catch (Exception ex)
                    {
                        transaction.Rollback();

                        throw ex;
                    }
                }
            }
            catch (Exception ex)
            {
                response.SetError(ex, Logger);
            }

            return response;
        }

        public async Task<ISingleModelResponse<Order>> CloneOrderAsync(Int32 id)
        {
            var response = new SingleModelResponse<Order>() as ISingleModelResponse<Order>;

            try
            {
                var entity = await SalesRepository.GetOrderAsync(new Order { OrderID = id });

                if (entity != null)
                {
                    response.Model = new Order();

                    response.Model.OrderID = entity.OrderID;
                    response.Model.OrderDate = entity.OrderDate;
                    response.Model.CustomerID = entity.CustomerID;
                    response.Model.EmployeeID = entity.EmployeeID;
                    response.Model.ShipperID = entity.ShipperID;
                    response.Model.Total = entity.Total;
                    response.Model.Comments = entity.Comments;

                    if (entity.OrderDetails != null && entity.OrderDetails.Count > 0)
                    {
                        foreach (var detail in entity.OrderDetails)
                        {
                            response.Model.OrderDetails.Add(new OrderDetail
                            {
                                ProductID = detail.ProductID,
                                ProductName = detail.ProductName,
                                UnitPrice = detail.UnitPrice,
                                Quantity = detail.Quantity,
                                Total = detail.Total
                            });
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                response.SetError(ex, Logger);
            }

            return response;
        }
    }
}

Business Layer: Handle Related Aspects To Business

  1. Logging: we need to have a logger object, that means an object that logs on text file, database, email, etc. all events in our architecture; we can create our own logger implementation or choose an existing log.
  2. Business exceptions: The best way to handle messaging to user is with custom exceptions, inside of business layer, we'll add definitions for exceptions to represent all handle errors in architecture.
  3. As we can see inside of Sales business object, we have implemented transaction to handle multiple changes in our database; inside of CreateOrder method, we invoke methods from repositories, inside of repositories we don't have any transactions because the business object is the responsibility for transactional process, also we added logic to handle exceptions related to business with custom messages because we need to provide a friendly message to the end-user.
  4. There is a CloneOrder method, this method provides a copy from existing order, this is a common requirement on ERP because it's more easy create a new order but adding some modifications instead of create the whole order there are cases where the sales agent create a new order but removing 1 or 2 lines from details or adding 1 or 2 details, anyway never let to front-end developer to add this logic in UI, the API must to provide this feature.

In BusinessLayer it's better to have custom exceptions for represent errors instead of send simple string messages to client, obviously the custom exception must have a message but in logger there will be a reference about custom exception. For this architecture these are the custom exceptions:

Business Exceptions
NameDescription
AddOrderWithDiscontinuedProductExceptionRepresents an exception in add order with a discontinued product
DuplicatedProductNameExceptionRepresents an exception in add product name with existing name
NonExistingProductExceptionRepresents an exception in add order with non existing product

Step 03 - Putting All Code Together

We create a StoreDbContext instance, that instance uses the connection string from AppSettings and inside of OnModelCreating method, there is a call of MapEntities method for EntityMapper instance, this is code in that way because it's more a stylish way to mapping entities instead of adding a lot of lines inside of OnModelCreating.

Later, for example, we create an instance of SalesBusinessObject passing a valid instance of StoreDbContext and then we can access business object's operations.

For this architecture implementation, we are using the DotNet naming conventions: PascalCase for classes, interfaces and methods; camelCase for parameters.

This is an example of how we can retrieve a list of orders list:

var userInfo = new UserInfo { Name = "admin" } as IUserInfo;

var appSettings = new AppSettings
{
	ConnectionString = "server=(local);database=Store;integrated security=yes; "
};

var entityMapper = new StoreEntityMapper() as IEntityMapper;

using (var businessObject = new SalesBusinessObject(userInfo, new StoreDbContext(appSettings, entityMapper)) as ISalesBusinessObject)
{
	var pageSize = 10;
	var pageNumber = 1;

	var response = businessObject.GetOrders(pageSize, pageNumber);

	// Validate if there was an error
	var valid = !response.DidError;
}

As we can see, the CreateOrder method in SalesBusinessObject handles all changes inside of a transaction, if there is an error, the transaction is rollback, otherwise is commit.

This code is the minimum requirements for an enterprise architect, for incoming versions of this tutorial, I'll include BusinessLayer, integration with Web API and unit tests.

Step 04 - Add Unit Tests

Open a terminal window in your working directory and follow these steps to create unit tests for current project:

  1. Create a directory in Store.Core with name test.
  2. Change to test directory.
  3. Create a directory with name Store.Core.Tests.
  4. Change to Store.Core.Tests directory
  5. Run this command: dotnet new -t xunittest
  6. Run this command: dotnet restore
  7. Later, add tests project to current solution, creating a new solution item with name test and inside of that solution item, add an existing project.
  8. Add reference to Store.Core project and save changes to rebuild.

Now, add a file with name SalesBusinessObjectTests and add this code to new file:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Store.Core.EntityLayer.Sales;
using Xunit;

namespace Store.Core.Tests
{
    public class SalesBusinessObjectTests
    {
        [Fact]
        public async Task TestGetCustomers()
        {
            // Arrange
            using (var businessObject = BusinessObjectMocker.GetSalesBusinessObject())
            {
                var pageSize = 10;
                var pageNumber = 1;

                // Act
                var response = await businessObject.GetCustomersAsync(pageSize, pageNumber);

                // Assert
                Assert.False(response.DidError);
            }
        }

        [Fact]
        public async Task TestGetShippers()
        {
            // Arrange
            using (var businessObject = BusinessObjectMocker.GetSalesBusinessObject())
            {
                var pageSize = 10;
                var pageNumber = 1;

                // Act
                var response = await businessObject.GetShippersAsync(pageSize, pageNumber);

                // Assert
                Assert.False(response.DidError);
            }
        }

        [Fact]
        public async Task TestGetOrders()
        {
            // Arrange
            using (var businessObject = BusinessObjectMocker.GetSalesBusinessObject())
            {
                var pageSize = 10;
                var pageNumber = 1;

                // Act
                var response = await businessObject.GetOrdersAsync(pageSize, pageNumber);

                // Assert
                Assert.False(response.DidError);
            }
        }

        [Fact]
        public async Task TestCreateOrder()
        {
            // Arrange
            using (var businessObject = BusinessObjectMocker.GetSalesBusinessObject())
            {
                var header = new Order();

                header.OrderDate = DateTime.Now;
                header.OrderStatusID = 100;
                header.CustomerID = 1;
                header.EmployeeID = 1;
                header.ShipperID = 1;

                var details = new List<OrderDetail>();

                details.Add(new OrderDetail { ProductID = 1, Quantity = 1 });

                // Act
                var response = await businessObject.CreateOrderAsync(header, details.ToArray());

                // Assert
                Assert.False(response.DidError);
            }
        }

        [Fact]
        public async Task TestUpdateOrder()
        {
            // Arrange
            using (var businessObject = BusinessObjectMocker.GetSalesBusinessObject())
            {
                var id = 1;

                // Act
                var response = await businessObject.GetOrderAsync(id);

                // Assert
                Assert.False(response.DidError);
            }
        }
    }
}

Now in the same window terminal, we need to run the following command: dotnet test and if everything works fine, we have done a good work at this point :)

Step 05 - Add Mocks

Open a terminal window in your working directory and follow these steps to create unit tests for current project:

  1. Go to test directory in Store.Core.
  2. Create a directory with name Store.Core.Mocks.
  3. Change to Store.Core.Mocks directory
  4. Run this command: dotnet new -t xunittest
  5. Run this command: dotnet restore
  6. Later, add tests project to current solution, creating a new solution item with name test and inside of that solution item, add an existing project.
  7. Add reference to Store.Core project and save changes to rebuild.

Now, add a file with name OrderMockingTests and add this code to new file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Store.Core.EntityLayer.Sales;
using Xunit;

namespace Store.Core.Mocks
{
    public class OrderMockingTests
    {
        private async Task CreateData(DateTime startDate, DateTime endDate, Int32 ordersLimitPerDay)
        {
            var date = new DateTime(startDate.Year, startDate.Month, startDate.Day);

            while (date <= endDate)
            {
                if (date.DayOfWeek != DayOfWeek.Sunday)
                {
                    var random = new Random();

                    var salesBusinessObject = BusinessObjectMocker.GetSalesBusinessObject();
                    var humanResourcesBusinessObject = BusinessObjectMocker.GetHumanResourcesBusinessObject();
                    var productionBusinessObject = BusinessObjectMocker.GetProductionBusinessObject();

                    var pageSize = 10;
                    var pageNumber = 1;

                    var customerResponse = await salesBusinessObject.GetCustomersAsync(pageSize, pageNumber);
                    var employeesResponse = await humanResourcesBusinessObject.GetEmployeesAsync(pageSize, pageNumber);
                    var shippersResponse = await salesBusinessObject.GetShippersAsync(pageSize, pageNumber);
                    var productsResponse = await productionBusinessObject.GetProductsAsync(pageSize, pageNumber);

                    var customers = customerResponse.Model.ToList();
                    var employees = employeesResponse.Model.ToList();
                    var shippers = shippersResponse.Model.ToList();
                    var products = productsResponse.Model.ToList();

                    for (var i = 0; i < ordersLimitPerDay; i++)
                    {
                        var header = new Order();

                        var selectedCustomer = random.Next(0, customers.Count - 1);
                        var selectedEmployee = random.Next(0, employees.Count - 1);
                        var selectedShipper = random.Next(0, shippers.Count - 1);

                        header.OrderDate = date;
                        header.OrderStatusID = 100;
                        header.CustomerID = customers[selectedCustomer].CustomerID;
                        header.EmployeeID = employees[selectedEmployee].EmployeeID;
                        header.ShipperID = shippers[selectedShipper].ShipperID;
                        header.CreationDateTime = date;

                        var details = new List<OrderDetail>();

                        var detailsCount = random.Next(1, 3);

                        for (var j = 0; j < detailsCount; j++)
                        {
                            var detail = new OrderDetail
                            {
                                ProductID = products[random.Next(0, products.Count - 1)].ProductID,
                                Quantity = (Int16)random.Next(1, 3)
                            };

                            if (details.Count > 0 && details.Where(item => item.ProductID == detail.ProductID).Count() == 1)
                            {
                                continue;
                            }

                            details.Add(detail);
                        }

                        await salesBusinessObject.CreateOrderAsync(header, details.ToArray());
                    }

                    salesBusinessObject.Dispose();
                    humanResourcesBusinessObject.Dispose();
                    productionBusinessObject.Dispose();
                }

                date = date.AddDays(1);
            }
        }

        [Fact]
        public async Task CreateOrders()
        {
            await CreateData(
                startDate: new DateTime(DateTime.Now.Year, 1, 1),
                endDate: new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month)),
                ordersLimitPerDay: 10
            );
        }
    }
}

Now in the same window terminal, we need to run the following command: dotnet test and if everything works fine, we can check in our database the data for Order, OrderDetail and ProductInventory tables.

How data mocker works? set a range for dates and a limit of orders per day, then iterate all days in range date except on sunday beacuse we're assuming create order is not allowed on sundays; then create the instance of DbContext and Business Object, arrange the data using a random to get data from customers, shippers, employees and products lists; then invoke the CreateOrder method with parameters.

You can adjust the range for dates and orders per day to generate more data according to your requirements, once the mocker it's already finish you can check the data on your database with Management Studio

Step 06 - Add Web API

Now in solution explorer add a Web API project with name Store.API and add references to Store.Coreproject, add a controller with name Sales and add this code:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Store.API.Extensions;
using Store.API.ViewModels;
using Store.Core.BusinessLayer.Contracts;
using Store.Core.BusinessLayer.Responses;

namespace Store.API.Controllers
{
    [Route("api/[controller]")]
    public class SalesController : Controller
    {
        protected IHumanResourcesBusinessObject HumanResourcesBusinessObject;
        protected IProductionBusinessObject ProductionBusinessObject;
        protected ISalesBusinessObject SalesBusinessObject;

        public SalesController(IHumanResourcesBusinessObject humanResourcesBusinessObject, IProductionBusinessObject productionBusinessObject, ISalesBusinessObject salesBusinessObject)
        {
            HumanResourcesBusinessObject = humanResourcesBusinessObject;
            ProductionBusinessObject = productionBusinessObject;
            SalesBusinessObject = salesBusinessObject;
        }

        protected override void Dispose(Boolean disposing)
        {
            SalesBusinessObject?.Dispose();

            base.Dispose(disposing);
        }

        [HttpGet]
        [Route("Order")]
        public async Task<IActionResult> GetOrders(Int32? pageSize = 10, Int32? pageNumber = 1)
        {
            var response = await SalesBusinessObject.GetOrdersAsync((Int32)pageSize, (Int32)pageNumber);

            return response.ToHttpResponse();
        }

        [HttpGet]
        [Route("Order/{id}")]
        public async Task<IActionResult> GetOrder(Int32 id)
        {
            var response = await SalesBusinessObject.GetOrderAsync(id);

            return response.ToHttpResponse();
        }

        [HttpGet]
        [Route("CreateOrderViewModel")]
        public async Task<IActionResult> GetCreateOrderViewModel()
        {
            var response = new SingleModelResponse<CreateOrderViewModel>() as ISingleModelResponse<CreateOrderViewModel>;

            var customersResponse = await SalesBusinessObject.GetCustomersAsync(0, 0);

            response.Model.Customers = customersResponse.Model.Select(item => new CustomerViewModel(item));

            var employeesResponse = await HumanResourcesBusinessObject.GetEmployeesAsync(0, 0);

            response.Model.Employees = employeesResponse.Model.Select(item => new EmployeeViewModel(item));

            var shippersResponse = await SalesBusinessObject.GetShippersAsync(0, 0);

            response.Model.Shippers = shippersResponse.Model.Select(item => new ShipperViewModel(item));

            var productsResponse = await ProductionBusinessObject.GetProductsAsync(0, 0);

            response.Model.Products = productsResponse.Model.Select(item => new ProductViewModel(item));

            return response.ToHttpResponse();
        }

        [HttpPost]
        [Route("Order")]
        public async Task<IActionResult> CreateOrder([FromBody] OrderViewModel value)
        {
            var response = await SalesBusinessObject.CreateOrderAsync(value.GetOrder(), value.GetOrderDetails().ToArray());

            return response.ToHttpResponse();
        }

        [HttpGet]
        [Route("CloneOrder/{id}")]
        public async Task<IActionResult> CloneOrder(Int32 id)
        {
            var response = await SalesBusinessObject.CloneOrderAsync(id);

            return response.ToHttpResponse();
        }
    }
}

Don't forget to set up all dependencies in Startup.cs file:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;
using Store.Core;
using Store.Core.BusinessLayer;
using Store.Core.BusinessLayer.Contracts;
using Store.Core.DataLayer;
using Store.Core.DataLayer.Mapping;

namespace Store.API
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services
                .AddMvc()
                .AddJsonOptions(a => a.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());

            services.AddEntityFrameworkSqlServer().AddDbContext<StoreDbContext>();

            services.AddScoped<IEntityMapper, StoreEntityMapper>();

            services.AddScoped<IUserInfo, UserInfo>();

            services.AddScoped<IHumanResourcesBusinessObject, HumanResourcesBusinessObject>();
            services.AddScoped<IProductionBusinessObject, ProductionBusinessObject>();
            services.AddScoped<ISalesBusinessObject, SalesBusinessObject>();

            services.AddOptions();

            services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

            services.AddSingleton<IConfiguration>(Configuration);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseMvc();
        }
    }
}

Now we can build our project and test the urls in browser.

VerbUrlDescription
GETapi/Sales/OrderGet orders
GETapi/Sales/Order/1Get order by id
GETapi/Sales/Order/0Get non existing order
GETapi/Sales/CreateOrderViewModelGet view model to create order
GETapi/Sales/CloneOrder/3Clone an existing order
POSTapi/Sales/OrderCreate a new order

Step 07 - Add Unit Tests for Web API

Now we proceed to add unit tests for API project, these tests are mock tests, later we'll add integration tests; what is the difference? in mock tests we simulate all dependency objects for API project and in the integration tests we run a process that simulates API execution. I mean a simulation of API (accepts Http requests), obviously there is more information about mock and integration but at this point with this basic idea is enough.

What is TDD? Testing is required in these days, because with unit tests it's easy to test a feature before publishing, Test Driven Development (TDD) is the way to define unit tests and validate the behavior in our code. Another concept in TDD is AAA: Arrange, Act and Assert; arrange is the block for creation of objects, act is the block to place all invocations for methods and assert is the block to validate the results from methods invocation.

Now, open a terminal window in your working directory and follow these steps to create unit tests for API project:

  1. Go to test directory in Store.
  2. Create a directory with name Store.API.Tests.
  3. Change to Store.API.Tests directory
  4. Run this command: dotnet new -t xunittest
  5. Run this command: dotnet restore
  6. Later, add tests project to current solution, creating a new solution item with name test and inside of that solution item, add an existing project.
  7. Add reference to Store.API project and save changes to rebuild.

Now, add a file with name SalesControllerTests and add this code to new file:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Store.API.Controllers;
using Store.API.ViewModels;
using Store.Core.BusinessLayer.Responses;
using Store.Core.EntityLayer.Sales;
using Xunit;

namespace Store.API.Tests
{
    public class SalesControllerTests
    {
        [Fact]
        public async Task GetOrdersTestAsync()
        {
            // Arrange
            var humanResourcesBusinessObject = BusinessObjectMocker.GetHumanResourcesBusinessObject();
            var productionBusinessObject = BusinessObjectMocker.GetProductionBusinessObject();
            var salesBusinessObject = BusinessObjectMocker.GetSalesBusinessObject();

            using (var controller = new SalesController(humanResourcesBusinessObject, productionBusinessObject, salesBusinessObject))
            {
                // Act
                var response = await controller.GetOrders() as ObjectResult;

                // Assert
                var value = response.Value as IListModelResponse<Order>;

                Assert.False(value.DidError);
            }
        }

        [Fact]
        public async Task GetOrderTestAsync()
        {
            // Arrange
            var humanResourcesBusinessObject = BusinessObjectMocker.GetHumanResourcesBusinessObject();
            var productionBusinessObject = BusinessObjectMocker.GetProductionBusinessObject();
            var salesBusinessObject = BusinessObjectMocker.GetSalesBusinessObject();
            var id = 1;

            using (var controller = new SalesController(humanResourcesBusinessObject, productionBusinessObject, salesBusinessObject))
            {
                // Act
                var response = await controller.GetOrder(id) as ObjectResult;

                // Assert
                var value = response.Value as ISingleModelResponse<Order>;

                Assert.False(value.DidError);
            }
        }

        [Fact]
        public async Task GetNonExistingOrderTestAsync()
        {
            // Arrange
            var humanResourcesBusinessObject = BusinessObjectMocker.GetHumanResourcesBusinessObject();
            var productionBusinessObject = BusinessObjectMocker.GetProductionBusinessObject();
            var salesBusinessObject = BusinessObjectMocker.GetSalesBusinessObject();
            var id = 0;

            using (var controller = new SalesController(humanResourcesBusinessObject, productionBusinessObject, salesBusinessObject))
            {
                // Act
                var response = await controller.GetOrder(id) as ObjectResult;

                // Assert
                var value = response.Value as ISingleModelResponse<Order>;

                Assert.False(value.DidError);
            }
        }

        [Fact]
        public async Task GetCreateOrderViewModelTestAsync()
        {
            // Arrange
            var humanResourcesBusinessObject = BusinessObjectMocker.GetHumanResourcesBusinessObject();
            var productionBusinessObject = BusinessObjectMocker.GetProductionBusinessObject();
            var salesBusinessObject = BusinessObjectMocker.GetSalesBusinessObject();

            using (var controller = new SalesController(humanResourcesBusinessObject, productionBusinessObject, salesBusinessObject))
            {
                // Act
                var response = await controller.GetCreateOrderViewModel() as ObjectResult;

                // Assert
                var value = response.Value as ISingleModelResponse<CreateOrderViewModel>;

                Assert.False(value.DidError);
            }
        }

        [Fact]
        public async Task GetCloneOrderTestAsync()
        {
            // Arrange
            var humanResourcesBusinessObject = BusinessObjectMocker.GetHumanResourcesBusinessObject();
            var productionBusinessObject = BusinessObjectMocker.GetProductionBusinessObject();
            var salesBusinessObject = BusinessObjectMocker.GetSalesBusinessObject();
            var id = 1;

            using (var controller = new SalesController(humanResourcesBusinessObject, productionBusinessObject, salesBusinessObject))
            {
                // Act
                var response = await controller.CloneOrder(id) as ObjectResult;

                // Assert
                var value = response.Value as ISingleModelResponse<Order>;

                Assert.False(value.DidError);
            }
        }
    }
}

As we can see these methods are the tests for Urls in API project, please take care bout the tests are async methods.

Save all changes and run the tests from command line or Visual Studio.

Code Improvements

  1. Add code for store procedures invocation
  2. Add log for operations
  3. Add integration tests
  4. Add authentication API
  5. Dynamic loading for entity mappings
  6. Add concurrency token
  7. Add logic for delete

Points of Interest

  1. In this article, we're working with EF Core but these concepts can apply to another ORM or without ORM.
  2. We can adjust all repositories to expose required operations, I mean in some cases we don't want to get all, add, update or delete but those operations will depend on product owner requirements.

LINK: https://www.codeproject.com/Articles/1160586/EF-Core-for-Enterprise

 

Add comment