Thursday 29 November 2018

Entity Framework Core 2 for Enterprise - End to end code sample

Introduction

Design an enterpise architecture for applications it's a big challenge, there is a common question in this point: What is the the best way to solve this issue following the best practices according to selected technology in our company.
This guide uses .Net Core, so We'll work with Entity Framework Core, but these concepts apply for another technologies like Dapper or another ORM.
In fact, we'll take a look at the common requirements to design of enterprise architect in this article.
The sample database provided in this guide represents an online store.
Because we're working with Entity Framework Core and ASP.NET Core, unit tests using in memory database provider; integration tests using a Test Web Server.
All tests (units and integration) are writen with xUnit framework.

Background

According to my experience, enterprise architecture for applications should have the following levels:
  1. Entity Layer: Contains entities (POCOs)
  2. Data Layer: Contains objects related to database access
  3. Business Layer: Contains definitions and validations related to business
  4. External Services Layer (optional): Contains invocations for external services (ASMX, WCF, RESTful)
  5. Common: Contains common objects for layers (e.g. Loggers, Mappers, Extensions)
  6. Tests (QA): Contains tests for back-end (units and integration)
  7. Presentation Layer: This is the UI
  8. UI Tests (QA): Contains automated tests for front-end

Architecture: Big Picture

DATABASESQL ServerDATABASE
ENTITY LAYERPOCOsBACK-END
DATA LAYERDbContext, Configurations, Contracts, Data Contracts and Repositories
BUSINESS LAYERServices, Contracts, DataContracts, Exceptions and Loggers
EXTERNAL SERVICES LAYERASMX, WCF, RESTful
COMMONLoggers, Mappers, Extensions
PRESENTATION LAYERUI Frameworks (AngularJS | ReactJS | Vue.js | Others)FRONT-END
USER  

Prerequisites

Skills

Before to continuing, keep in mind we need to have the folllowing skills in order to understand this guide:
  • OOP (Object Oriented Programming)
  • AOP (Aspect Oriented Programming)
  • ORM (Object Relational Mapping)
  • Design Patterns: Domain Driven Design, Repository & Unit of Work and IoC

Software

  • .Net Core
  • Visual Studio 2017
  • SQL Server instance (local or remote)
  • SQL Server Management Studio

Table of Contents

  1. Using the Code
  2. Code improvements
  3. Points of Interest
  4. Related Links

Using the Code

Chapter 01 - Database

Take a look for sample database to understand each component in architecture. In this database there are 4 schemas: DboHumanResourcesWarehouse and Sales.
Each schema represents a division on store company, keep this in mind because all code is designed following this aspect; at this moment this code only implements features for Production and Sales schemas.
All tables have a primary key with one column and have columns for creation, last update and concurrency token.

Tables

SchemaName
dboChangeLog
dboChangeLogExclusion
dboCountry
dboCountryCurrency
dboCurrency
dboEventLog
HumanResourcesEmployee
HumanResourcesEmployeeAddress
HumanResourcesEmployeeEmail
SalesCustomer
SalesOrderDetail
SalesOrderHeader
SalesOrderStatus
SalesPaymentMethod
SalesShipper
WarehouseLocation
WarehouseProduct
WarehouseProductCategory
WarehouseProductInventory
You can found the scripts for database in this link: OnLine Store Database Scripts on GitHub.
Please remember: This is a sample database, only for demonstration of concepts.

Chapter 02 - Core Project

Core project represents the core for solution, in this guide Core project includes entity, data and business layers.
We're working with .NET Core, the naming convention is .NET naming convention, so it's very useful to define a naming convention table to show how to set names in code, something like this:
IdentifierCaseExample
NamespacePascalCaseStore
ClassPascalCaseProduct
InterfaceI prefix + PascalCaseISalesRepository
MethodVerb in PascalCase + Noun in PascalCaseGetProducts
Async MethodVerb in PascalCase + Noun in PascalCase + Async sufixGetOrdersAsync
PropertyPascalCaseDescription
ParametercamelCaseconnectionString
This convention is important because it defines the naming guidelines for architecture.
This is the structure for Store.Core project:
  1. EntityLayer
  2. DataLayer
  3. DataLayer\Configurations
  4. BusinessLayer
  5. BusinessLayer\Contracts
  6. BusinessLayer\Requests
  7. 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 because it's a common class for DataLayer.
For 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).
For DataLayer\DataContracts, we'll place all object definitions for returned values from Contractsnamespace, for now this directory contains OrderInfo class definition.
For DataLayer\Mapping, we'll place all object definitions related to mapping classes for database.
For DataLayer\Repositories, we'll place the implementations for Contracts definitons.
One repository includes operations related to one schema, so we have 4 repositories: DboRepositoryHumanResourcesRepositoryProductionRepository and SalesRepository.
Inside of EntityLayer and DataLayer\Mapping, we'll create one directory per schema.
Inside of BusinessLayer, we'll create the interfaces and implementations for services, in this case, the services will contain the methods according to use cases (or something similar) and those methods must perform validations and handle exceptions related to busines.
For BusinessLayer\Responses, we'll create the responses: single, list and paged to represent the result from services.
We'll inspect the code to understand these concepts but the review would be with one object per level because the remaining code is similar.

Entity Layer

OrderHeader class:
using System;
using System.Collections.ObjectModel;
using OnLineStore.Core.EntityLayer.Dbo;
using OnLineStore.Core.EntityLayer.HumanResources;

namespace OnLineStore.Core.EntityLayer.Sales
{
    public class OrderHeader : IAuditableEntity
    {
        public OrderHeader()
        {
        }

        public OrderHeader(long? orderHeaderID)
        {
            OrderHeaderID = orderHeaderID;
        }

        public long? OrderHeaderID { get; set; }

        public short? OrderStatusID { get; set; }

        public DateTime? OrderDate { get; set; }

        public int? CustomerID { get; set; }

        public int? EmployeeID { get; set; }

        public int? ShipperID { get; set; }

        public decimal? Total { get; set; }

        public short? CurrencyID { get; set; }

        public Guid? PaymentMethodID { get; set; }

        public int? DetailsCount { get; set; }

        public long? ReferenceOrderID { 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 byte[] Timestamp { get; set; }

        public virtual OrderStatus OrderStatusFk { get; set; }

        public virtual Customer CustomerFk { get; set; }

        public virtual Employee EmployeeFk { get; set; }

        public virtual Shipper ShipperFk { get; set; }

        public virtual Currency CurrencyFk { get; set; }

        public virtual PaymentMethod PaymentMethodFk { get; set; }

        public virtual Collection<OrderDetail> OrderDetails { get; set; } = new Collection<OrderDetail>();
    }
}
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 IAuditEntityIEntity 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

For this source code, the implementation for repositories are by feature instead of generic repositories; the generic repositories require to create derived repositories in case we need to implement specific operations. I prefer repositories by features because do not require to create derived objects (interfaces and classes) but a repository by feature will contains a lot of operations because is a placeholder for all operations in feature.
The sample database for this article contains 4 schemas in database, so we'll have 4 repositories, this implementation provides a separation of concepts.
We are working with EF Core in this guide, so we need to have a DbContext and objects that allow mapping classes and database objects (tables and views).
Repository versus DbHelper versus Data Access Object
This issue is related to naming objects, some years ago I used DataAccessObject as suffix to class that contain database operatios (select, insert, update, delete, etc). Other developers used DbHelper as suffix to represent this kind of objects, at my beggining in EF I learned about repository design pattern, so from my point of view I prefer to use Repository suffix to name the object that contains database operations.
OnLineStoreDbContext class:
using Microsoft.EntityFrameworkCore;
using OnLineStore.Core.DataLayer.Configurations;
using OnLineStore.Core.DataLayer.Configurations.Dbo;
using OnLineStore.Core.DataLayer.Configurations.HumanResources;
using OnLineStore.Core.DataLayer.Configurations.Warehouse;
using OnLineStore.Core.DataLayer.Configurations.Sales;
using OnLineStore.Core.EntityLayer.Dbo;
using OnLineStore.Core.EntityLayer.HumanResources;
using OnLineStore.Core.EntityLayer.Warehouse;
using OnLineStore.Core.EntityLayer.Sales;

namespace OnLineStore.Core.DataLayer
{
    public class OnLineStoreDbContext : DbContext
    {
        public OnLineStoreDbContext(DbContextOptions<OnLineStoreDbContext> options)
            : base(options)
        {
        }

        public DbSet<ChangeLog> ChangeLogs { get; set; }

        public DbSet<ChangeLogExclusion> ChangeLogExclusions { get; set; }

        public DbSet<CountryCurrency> CountryCurrencies { get; set; }

        public DbSet<Country> Countries { get; set; }

        public DbSet<Currency> Currencies { get; set; }

        public DbSet<EventLog> EventLogs { get; set; }

        public DbSet<Employee> Employees { get; set; }

        public DbSet<EmployeeAddress> EmployeeAddresses { get; set; }

        public DbSet<EmployeeEmail> EmployeeEmails { get; set; }

        public DbSet<ProductCategory> ProductCategories { get; set; }

        public DbSet<ProductInventory> ProductInventories { get; set; }

        public DbSet<Product> Products { get; set; }

        public DbSet<EntityLayer.Warehouse.Location> Warehouses { get; set; }

        public DbSet<Customer> Customers { get; set; }

        public DbSet<OrderDetail> OrderDetails { get; set; }

        public DbSet<OrderHeader> Orders { get; set; }

        public DbSet<OrderStatus> OrderStatuses { get; set; }

        public DbSet<OrderSummary> OrderSummaries { get; set; }

        public DbSet<PaymentMethod> PaymentMethods { get; set; }

        public DbSet<Shipper> Shippers { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Apply all configurations

            modelBuilder
                .ApplyConfiguration(new ChangeLogConfiguration())
                .ApplyConfiguration(new ChangeLogExclusionConfiguration())
                .ApplyConfiguration(new CountryCurrencyConfiguration())
                .ApplyConfiguration(new CountryConfiguration())
                .ApplyConfiguration(new CurrencyConfiguration())
                .ApplyConfiguration(new EventLogConfiguration())
                ;

            modelBuilder
                .ApplyConfiguration(new EmployeeConfiguration())
                .ApplyConfiguration(new EmployeeAddressConfiguration())
                .ApplyConfiguration(new EmployeeEmailConfiguration())
                ;

            modelBuilder
                .ApplyConfiguration(new ProductCategoryConfiguration())
                .ApplyConfiguration(new ProductInventoryConfiguration())
                .ApplyConfiguration(new ProductConfiguration())
                .ApplyConfiguration(new LocationConfiguration())
                ;

            modelBuilder
                .ApplyConfiguration(new CustomerConfiguration())
                .ApplyConfiguration(new OrderDetailConfiguration())
                .ApplyConfiguration(new OrderHeaderConfiguration())
                .ApplyConfiguration(new OrderStatusConfiguration())
                .ApplyConfiguration(new OrderSummaryConfiguration())
                .ApplyConfiguration(new PaymentMethodConfiguration())
                .ApplyConfiguration(new ShipperConfiguration())
                ;

            base.OnModelCreating(modelBuilder);
        }
    }
}
OrderHeaderConfiguration class:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OnLineStore.Core.EntityLayer.Sales;

namespace OnLineStore.Core.DataLayer.Configurations.Sales
{
    public class OrderHeaderConfiguration : IEntityTypeConfiguration<OrderHeader>
    {
        public void Configure(EntityTypeBuilder<OrderHeader> builder)
        {
            // Mapping for table
            builder.ToTable("OrderHeader", "Sales");

            // Set key for entity
            builder.HasKey(p => p.OrderHeaderID);

            // Set identity for entity (auto increment)
            builder.Property(p => p.OrderHeaderID).UseSqlServerIdentityColumn();

            // Set mapping for columns
            builder.Property(p => p.OrderStatusID).HasColumnType("smallint").IsRequired();
            builder.Property(p => p.OrderDate).HasColumnType("datetime").IsRequired();
            builder.Property(p => p.CustomerID).HasColumnType("int").IsRequired();
            builder.Property(p => p.EmployeeID).HasColumnType("int");
            builder.Property(p => p.ShipperID).HasColumnType("int");
            builder.Property(p => p.Total).HasColumnType("decimal(12, 4)").IsRequired();
            builder.Property(p => p.CurrencyID).HasColumnType("smallint");
            builder.Property(p => p.PaymentMethodID).HasColumnType("uniqueidentifier");
            builder.Property(p => p.DetailsCount).HasColumnType("int").IsRequired();
            builder.Property(p => p.ReferenceOrderID).HasColumnType("bigint");
            builder.Property(p => p.Comments).HasColumnType("varchar(max)");
            builder.Property(p => p.CreationUser).HasColumnType("varchar(25)").IsRequired();
            builder.Property(p => p.CreationDateTime).HasColumnType("datetime").IsRequired();
            builder.Property(p => p.LastUpdateUser).HasColumnType("varchar(25)");
            builder.Property(p => p.LastUpdateDateTime).HasColumnType("datetime");

            // Set concurrency token for entity
            builder.Property(p => p.Timestamp).ValueGeneratedOnAddOrUpdate().IsConcurrencyToken();

            // Add configuration for foreign keys
            builder
                .HasOne(p => p.OrderStatusFk)
                .WithMany(b => b.Orders)
                .HasForeignKey(p => p.OrderStatusID);

            builder
                .HasOne(p => p.CustomerFk)
                .WithMany(b => b.Orders)
                .HasForeignKey(p => p.CustomerID);

            builder
                .HasOne(p => p.ShipperFk)
                .WithMany(b => b.Orders)
                .HasForeignKey(p => p.ShipperID);
        }
    }
}
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).

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 stored procedures 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 Entity Framework Core doesn't allow to invoke stored procedures"
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.
Just to be clear: STAY AWAY FROM STORED PROCEDURES!
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   Key  OriginalValue          CurrentValue           UserName   ChangeDate
----------- ------------ -------------- ---- ---------------------- ---------------------- ---------- -----------------------
1           Employee     FirstName      1    John                   John III               admin      2017-02-19 21:49:51.347
2           Employee     MiddleName     1                           Smith III              admin      2017-02-19 21:49:51.347
3           Employee     LastName       1    Doe                    Doe III                admin      2017-02-19 21:49:51.347

(3 row(s) affected)
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

Controller versus Service versus Business Object
There is a common issue in this point, How we must to name the object that represents business operations: for first versions of this article I named this object as BusinessObject, that can be confusing for some developers, some developers do not name this as business object because the controller in Web API represents business logic, but Service is another name used by developers, so from my point of view is more clear to use Serviceas sufix for this object. If we have a Web API that implements business logic in controller we can ommit to have services, but if there is business layer it is more useful to have services, these classes must to implement logic business and controllers must invoke service's methods.

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. We have added logging with package Microsoft.Extensions.Logging, in this way we're using the default log system in .NET Core, we can use another log mechanism but at this moment we'll use this logger, inside of every method in controllers and business objects, there is a code line like this: Logger?.LogInformation("{0} has been invoked", nameof(GetOrdersAsync));, in this way we make sure invoke logger if is a valid instance and ths using of nameof operator to retrieve the name of member without use magic strings, after we'll add code to save all logs into database.
  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. Transactions: as we can see inside of Sales business object, we have implemented transaction to handle multiple changes in our database; inside of CreateOrderAsync method, we invoke methods from repositories, inside of repositories we don't have any transactions because the service is the responsible 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 CloneOrderAsync 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.
  5. GetCreateOrderRequestAsync method in SalesRepository provides the required information to create an order, information from foreign keys: products and anothers. With this method we are providing a model that contains the list for foreign keys and in that way we reduce the work from front-end to know how to create create order operation.
Service class:
using Microsoft.Extensions.Logging;
using OnLineStore.Core.BusinessLayer.Contracts;
using OnLineStore.Core.DataLayer;

namespace OnLineStore.Core.BusinessLayer
{
    public abstract class Service : IService
    {
        protected bool Disposed;
        protected ILogger Logger;
        protected IUserInfo UserInfo;

        public Service(ILogger logger, IUserInfo userInfo, OnLineStoreDbContext dbContext)
        {
            Logger = logger;
            UserInfo = userInfo;
            DbContext = dbContext;
        }

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

                Disposed = true;
            }
        }

        public OnLineStoreDbContext DbContext { get; }
    }
}
SalesService class:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using OnLineStore.Core.BusinessLayer.Contracts;
using OnLineStore.Core.BusinessLayer.Requests;
using OnLineStore.Core.BusinessLayer.Responses;
using OnLineStore.Core.DataLayer;
using OnLineStore.Core.DataLayer.Repositories;
using OnLineStore.Core.DataLayer.Sales;
using OnLineStore.Core.DataLayer.Warehouse;
using OnLineStore.Core.EntityLayer.Dbo;
using OnLineStore.Core.EntityLayer.Sales;
using OnLineStore.Core.EntityLayer.Warehouse;

namespace OnLineStore.Core.BusinessLayer
{
    public class SalesService : Service, ISalesService
    {
        public SalesService(ILogger<SalesService> logger, IUserInfo userInfo, OnLineStoreDbContext dbContext)
            : base(logger, userInfo, dbContext)
        {
        }

        public async Task<IPagedResponse<Customer>> GetCustomersAsync(int pageSize = 10, int pageNumber = 1)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetCustomersAsync));

            var response = new PagedResponse<Customer>();

            try
            {
                // Get query
                var query = DbContext.Customers;

                // Set information for paging
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items, set model for response
                response.Model = await query
                    .Paging(pageSize, pageNumber)
                    .ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetCustomersAsync), ex);
            }

            return response;
        }

        public async Task<IPagedResponse<Shipper>> GetShippersAsync(int pageSize = 10, int pageNumber = 1)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetShippersAsync));

            var response = new PagedResponse<Shipper>();

            try
            {
                // Get query
                var query = DbContext.Shippers;

                // Set information for paging
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items, set model for response
                response.Model = await query
                    .Paging(pageSize, pageNumber)
                    .ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetShippersAsync), ex);
            }

            return response;
        }

        public async Task<IPagedResponse<Currency>> GetCurrenciesAsync(int pageSize = 10, int pageNumber = 1)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetCurrenciesAsync));

            var response = new PagedResponse<Currency>();

            try
            {
                // Get query
                var query = DbContext.Currencies;

                // Set information for paging
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items, set model for response
                response.Model = await query
                    .Paging(pageSize, pageNumber)
                    .ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetCurrenciesAsync), ex);
            }

            return response;
        }

        public async Task<IPagedResponse<PaymentMethod>> GetPaymentMethodsAsync(int pageSize = 10, int pageNumber = 1)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetPaymentMethodsAsync));

            var response = new PagedResponse<PaymentMethod>();

            try
            {
                // Get query
                var query = DbContext.PaymentMethods;

                // Set information for paging
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items, set model for response
                response.Model = await query
                    .Paging(pageSize, pageNumber)
                    .ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetPaymentMethodsAsync), ex);
            }

            return response;
        }

        public async Task<IPagedResponse<OrderInfo>> GetOrdersAsync(int pageSize = 10, int pageNumber = 1, short? orderStatusID = null, int? customerID = null, int? employeeID = null, int? shipperID = null, short? currencyID = null, Guid? paymentMethodID = null)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetOrdersAsync));

            var response = new PagedResponse<OrderInfo>();

            try
            {
                // Get query
                var query = DbContext.GetOrders(orderStatusID, customerID, employeeID, shipperID, currencyID, paymentMethodID);

                // Set information for paging
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;
                response.ItemsCount = await query.CountAsync();

                // Retrieve items, set model for response
                response.Model = await query
                    .Paging(pageSize, pageNumber)
                    .ToListAsync();

                response.Message = string.Format("Page {0} of {1}, Total of rows: {2}", response.PageNumber, response.PageCount, response.ItemsCount);

                Logger?.LogInformation(response.Message);
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetOrdersAsync), ex);
            }

            return response;
        }

        public async Task<ISingleResponse<OrderHeader>> GetOrderAsync(long id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetOrderAsync));

            var response = new SingleResponse<OrderHeader>();

            try
            {
                // Retrieve order by id
                response.Model = await DbContext.GetOrderAsync(new OrderHeader(id));
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetOrderAsync), ex);
            }

            return response;
        }

        public async Task<ISingleResponse<CreateOrderRequest>> GetCreateOrderRequestAsync()
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetCreateOrderRequestAsync));

            var response = new SingleResponse<CreateOrderRequest>();

            try
            {
                // Retrieve products list
                response.Model.Products = await DbContext.GetProducts().ToListAsync();

                // Retrieve customers list
                response.Model.Customers = await DbContext.Customers.ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetCreateOrderRequestAsync), ex);
            }

            return response;
        }

        public async Task<ISingleResponse<OrderHeader>> CreateOrderAsync(OrderHeader header, OrderDetail[] details)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(CreateOrderAsync));

            var response = new SingleResponse<OrderHeader>();

            // Begin transaction
            using (var transaction = await DbContext.Database.BeginTransactionAsync())
            {
                try
                {
                    // todo: Retrieve available warehouse to dispatch products
                    var warehouses = await DbContext.Warehouses.ToListAsync();

                    foreach (var detail in details)
                    {
                        // Retrieve product by id
                        var product = await DbContext.GetProductAsync(new Product(detail.ProductID));

                        // Throw exception if product no exists
                        if (product == null)
                            throw new NonExistingProductException(string.Format(SalesDisplays.NonExistingProductExceptionMessage, detail.ProductID));

                        // Throw exception if product is discontinued
                        if (product.Discontinued == true)
                            throw new AddOrderWithDiscontinuedProductException(string.Format(SalesDisplays.AddOrderWithDiscontinuedProductExceptionMessage, product.ProductID));

                        // Throw exception if quantity for product is invalid
                        if (detail.Quantity <= 0)
                            throw new InvalidQuantityException(string.Format(SalesDisplays.InvalidQuantityExceptionMessage, product.ProductID));

                        // Set values for detail
                        detail.ProductName = product.ProductName;
                        detail.UnitPrice = product.UnitPrice;
                        detail.Total = product.UnitPrice * detail.Quantity;
                    }

                    // Set default values for order header
                    if (!header.OrderDate.HasValue)
                        header.OrderDate = DateTime.Now;

                    header.OrderStatusID = 100;

                    // Calculate total for order header from order's details
                    header.Total = details.Sum(item => item.Total);
                    header.DetailsCount = details.Count();

                    // Save order header
                    DbContext.Add(header, UserInfo);

                    await DbContext.SaveChangesAsync();

                    foreach (var detail in details)
                    {
                        // Set order id for order detail
                        detail.OrderHeaderID = header.OrderHeaderID;

                        // Add order detail
                        DbContext.Add(detail, UserInfo);

                        await DbContext.SaveChangesAsync();

                        // Create product inventory instance
                        var productInventory = new ProductInventory
                        {
                            ProductID = detail.ProductID,
                            LocationID = warehouses.First().LocationID,
                            OrderDetailID = detail.OrderDetailID,
                            Quantity = detail.Quantity * -1,
                            CreationDateTime = DateTime.Now,
                            CreationUser = header.CreationUser
                        };

                        // Save product inventory
                        DbContext.Add(productInventory);
                    }

                    await DbContext.SaveChangesAsync();

                    response.Model = header;

                    // Commit transaction
                    transaction.Commit();

                    Logger.LogInformation(SalesDisplays.CreateOrderMessage);
                }
                catch (Exception ex)
                {
                    response.SetError(Logger, nameof(CreateOrderAsync), ex);
                }
            }

            return response;
        }

        public async Task<ISingleResponse<OrderHeader>> CloneOrderAsync(long id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(CloneOrderAsync));

            var response = new SingleResponse<OrderHeader>();

            try
            {
                // Retrieve order by id
                var entity = await DbContext.GetOrderAsync(new OrderHeader(id));

                if (entity != null)
                {
                    // Create a new instance for order and set values from existing order
                    response.Model = new OrderHeader
                    {
                        OrderHeaderID = entity.OrderHeaderID,
                        OrderDate = entity.OrderDate,
                        CustomerID = entity.CustomerID,
                        EmployeeID = entity.EmployeeID,
                        ShipperID = entity.ShipperID,
                        Total = entity.Total,
                        Comments = entity.Comments
                    };

                    if (entity.OrderDetails != null && entity.OrderDetails.Count > 0)
                    {
                        foreach (var detail in entity.OrderDetails)
                        {
                            // Add order detail clone to collection
                            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(Logger, nameof(CloneOrderAsync), ex);
            }

            return response;
        }

        public async Task<IResponse> RemoveOrderAsync(long id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(RemoveOrderAsync));

            var response = new Response();

            try
            {
                // Retrieve order by id
                var entity = await DbContext.GetOrderAsync(new OrderHeader(id));

                if (entity != null)
                {
                    // Restrict remove operation for orders with details
                    if (entity.OrderDetails.Count > 0)
                        throw new ForeignKeyDependencyException(string.Format(SalesDisplays.RemoveOrderExceptionMessage, id));

                    // Delete order
                    DbContext.Remove(entity);

                    await DbContext.SaveChangesAsync();

                    Logger?.LogInformation(SalesDisplays.DeleteOrderMessage);
                }
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(RemoveOrderAsync), ex);
            }

            return response;
        }
    }
}
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 adding order with a discontinued product
ForeignKeyDependencyExceptionRepresents an exception deleting an order with detail rows
DuplicatedProductNameExceptionRepresents an exception adding product with existing name
NonExistingProductExceptionRepresents an exception adding order with non existing product

Chapter 03 - Putting All Code Together

We need to create a OnLineStoreDbContext instance, that instance works with SQL Server, in OnModelCreating method, all configurations are applied to ModelBuilder instance.
Later, there is an instance of SalesService created with a valid instance of OnLineStoreDbContext to get access for service's operations.

Get All

This is an example of how we can retrieve a list of orders list:
// Create logger instance
var logger = LoggerMocker.GetLogger<ISalesService>();

// Create application user
var userInfo = new UserInfo("admin");

// Create options for DbContext
var options = new DbContextOptionsBuilder<OnLineStoreDbContext>()
    .UseSqlServer("YourConnectionStringHere")
    .Options;

// Create instance of business object
// Set logger, application user and context for database
using (var service = new SalesService(logger, userInfo, new OnLineStoreDbContext(options)))
{
    // Declare parameters and set values for paging
 var pageSize = 10;
 var pageNumber = 1;

    // Get response from business object
 var response = await service.GetOrdersAsync(pageSize, pageNumber);

 // Validate if there was an error
 var valid = !response.DidError;
}
As we can see, GetOrdersAsync method in SalesService retrieves rows from Sales.Order table as a generic list.

Get by Key

This is an example of how we can retrieve an entity by key:
// Create logger instance
var logger = LoggerMocker.GetLogger<ISalesService>();

// Create application user
var userInfo = new UserInfo("admin");

// Create options for DbContext
var options = new DbContextOptionsBuilder<OnLineStoreDbContext>()
    .UseSqlServer("YourConnectionStringHere")
    .Options;

// Create instance of business object
// Set logger, application user and context for database
using (var service = new SalesService(logger, userInfo, new OnLineStoreDbContext(options)))
{
    // Declare parameters and set values for paging
 var id = 1;

    // Get response from business object
 var response = await service.GetOrderAsync(id);

 // Validate if there was an error
 var valid = !response.DidError;
 
 // Get entity
 var entity = response.Model;
}
For incoming versions of this article, there will be samples for another operations.

Chapter 04 - Mocker

Mocker it's a project that allows to create rows in Sales.OrderHeaderSales.OrderDetail and Warehouse.ProductInventory tables for a range of dates, by default Mocker creates rows for one year.
Program class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OnLineStore.Common;
using OnLineStore.Core.EntityLayer.Sales;

namespace OnLineStore.Mocker
{
    public class Program
    {
        private static readonly ILogger Logger;

        static Program()
        {
            Logger = LoggingHelper.GetLogger<Program>();
        }

        public static void Main(string[] args)
        {
            MainAsync(args).GetAwaiter().GetResult();
        }

        static async Task MainAsync(string[] args)
        {
            var year = DateTime.Now.AddYears(-1).Year;
            var ordersLimitPerDay = 3;

            foreach (var arg in args)
            {
                if (arg.StartsWith("/year:"))
                    year = Convert.ToInt32(arg.Replace("/year:", string.Empty));
                else if (arg.StartsWith("/ordersLimitPerDay:"))
                    ordersLimitPerDay = Convert.ToInt32(arg.Replace("/ordersLimitPerDay:", string.Empty));
            }

            var start = new DateTime(year, 1, 1);
            var end = new DateTime(year, 12, DateTime.DaysInMonth(year, 12));

            if (start.DayOfWeek == DayOfWeek.Sunday)
                start = start.AddDays(1);

            do
            {
                if (start.DayOfWeek != DayOfWeek.Sunday)
                {
                    await CreateDataAsync(start, ordersLimitPerDay);

                    Thread.Sleep(1000);
                }

                start = start.AddDays(1);
            }
            while (start <= end);
        }

        static async Task CreateDataAsync(DateTime date, int ordersLimitPerDay)
        {
            var random = new Random();

            var warehouseService = ServiceMocker.GetWarehouseService();
            var salesService = ServiceMocker.GetSalesService();

            var customers = (await salesService.GetCustomersAsync()).Model.ToList();
            var currencies = (await salesService.GetCurrenciesAsync()).Model.ToList();
            var paymentMethods = (await salesService.GetPaymentMethodsAsync()).Model.ToList();
            var products = (await warehouseService.GetProductsAsync()).Model.ToList();

            Logger.LogInformation("Creating orders for {0}", date);

            for (var i = 0; i < ordersLimitPerDay; i++)
            {
                var header = new OrderHeader
                {
                    OrderDate = date,
                    CreationDateTime = date
                };

                var selectedCustomer = random.Next(0, customers.Count - 1);
                var selectedCurrency = random.Next(0, currencies.Count - 1);
                var selectedPaymentMethod = random.Next(0, paymentMethods.Count - 1);

                header.CustomerID = customers[selectedCustomer].CustomerID;
                header.CurrencyID = currencies[selectedCurrency].CurrencyID;
                header.PaymentMethodID = paymentMethods[selectedPaymentMethod].PaymentMethodID;

                var details = new List<OrderDetail>();

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

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

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

                    details.Add(detail);
                }

                await salesService.CreateOrderAsync(header, details.ToArray());

                Logger.LogInformation("Date: {0}", date);
            }

            warehouseService.Dispose();
            salesService.Dispose();
        }
    }
}
Now in the same window terminal, we need to run the following command: dotnet run and if everything works fine, we can check in our database the data for OrderHeaderOrderDetail and ProductInventorytables.
How Mocker works? set a range for dates and a limit of orders per day, then iterates all days in date range except sundays beacuse we're assuming create order process is not allowed on sundays; then create the instance of DbContext and Services, arranges data using a random index to get elements from products, customers, currencies and payment methods; then invokes the CreateOrderAsync method.
You can adjust the range for dates and orders per day to mock data according to your requirements, once the Mocker has finished you can check the data on your database.

Chapter 05 - Web API

There is a project with name OnlineStore.WebAPI, this represents Web API for this solution, this project has references to OnLineStore.Core project.
We'll take a look on SalesController class:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using OnLineStore.Core.BusinessLayer.Contracts;
using OnLineStore.WebAPI.Requests;
using OnLineStore.WebAPI.Responses;

namespace OnLineStore.WebAPI.Controllers
{
#pragma warning disable CS1591
    [ApiController]
    [Route("api/v1/[controller]")]
    public class SalesController : ControllerBase
    {
        protected ILogger Logger;
        protected ISalesService SalesService;

        public SalesController(ILogger<SalesController> logger, ISalesService salesService)
        {
            Logger = logger;
            SalesService = salesService;
        }
#pragma warning restore CS1591

        /// <summary>
        /// Retrieves the orders generated by customers
        /// </summary>
        /// <param name="pageSize">Page size</param>
        /// <param name="pageNumber">Page number</param>
        /// <param name="orderStatusID">Order status</param>
        /// <param name="customerID">Customer</param>
        /// <param name="employeeID">Employee</param>
        /// <param name="shipperID">Shipper</param>
        /// <param name="currencyID">Currency</param>
        /// <param name="paymentMethodID">Payment method</param>
        /// <returns>A sequence of orders</returns>
        [HttpGet("Order")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> GetOrdersAsync(int? pageSize = 50, int? pageNumber = 1, short? orderStatusID = null, int? customerID = null, int? employeeID = null, int? shipperID = null, short? currencyID = null, Guid? paymentMethodID = null)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetOrdersAsync));

            // Get response from business logic
            var response = await SalesService.GetOrdersAsync((int)pageSize, (int)pageNumber, orderStatusID, customerID, employeeID, shipperID, currencyID, paymentMethodID);

            // Return as http response
            return response.ToHttpResponse();
        }

        /// <summary>
        /// Retrieves an existing order by id
        /// </summary>
        /// <param name="id">Order ID</param>
        /// <returns>An existing order</returns>
        [HttpGet("Order/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(404)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> GetOrderAsync(long id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetOrderAsync));

            // Get response from business logic
            var response = await SalesService.GetOrderAsync(id);

            // Return as http response
            return response.ToHttpResponse();
        }

        /// <summary>
        /// Retrieves the request model to create a new order
        /// </summary>
        /// <returns>A model that represents the request to create a new order</returns>
        [HttpGet("CreateOrderRequest")]
        public async Task<IActionResult> GetCreateOrderRequestAsync()
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetCreateOrderRequestAsync));

            // Get response from business logic
            var response = await SalesService.GetCreateOrderRequestAsync();

            // Return as http response
            return response.ToHttpResponse();
        }

        /// <summary>
        /// Creates a new order
        /// </summary>
        /// <param name="request">Model request</param>
        /// <returns>A result that contains the order ID generated by application</returns>
        [HttpPost]
        [Route("Order")]
        [ProducesResponseType(200)]
        [ProducesResponseType(400)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> CreateOrderAsync([FromBody] OrderHeaderRequest request)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(CreateOrderAsync));

            // Get response from business logic
            var response = await SalesService.CreateOrderAsync(request.GetOrder(), request.GetOrderDetails().ToArray());

            // Return as http response
            return response.ToHttpResponse();
        }

        /// <summary>
        /// Creates a new order from existing order
        /// </summary>
        /// <param name="id">Order ID</param>
        /// <returns>A model for a new order</returns>
        [HttpGet("CloneOrder/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> CloneOrderAsync(int id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(CloneOrderAsync));

            // Get response from business logic
            var response = await SalesService.CloneOrderAsync(id);

            // Return as http response
            return response.ToHttpResponse();
        }

        /// <summary>
        /// Deletes an existing order
        /// </summary>
        /// <param name="id">ID for order</param>
        /// <returns>A success response if order is deleted</returns>
        [HttpDelete("Order/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> DeleteOrderAsync(int id)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(DeleteOrderAsync));

            // Get response from business logic
            var response = await SalesService.RemoveOrderAsync(id);

            // Return as http response
            return response.ToHttpResponse();
        }
    }
}
ViewModel versus Request
ViewModel is an object that contains behavior, request is the action related to invoke a Web API method, this is the misunderstood: ViewModel is an object linked to a view, contains behavior to handle changes and sync up with view; usually the parameter for Web API method is an object with properties, so this definition is named Request; MVC is not MVVM, the life's cycle for model is different in those patterns, this definition doesn't keep state between UI and API, also the process to set properties values in request from query string is handled by a model binder.
Now take a look on Startup.cs class:
using System;
using System.IO;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using OnLineStore.Core;
using OnLineStore.Core.BusinessLayer;
using OnLineStore.Core.BusinessLayer.Contracts;
using OnLineStore.Core.DataLayer;
using Swashbuckle.AspNetCore.Swagger;

namespace OnLineStore.WebAPI
{
#pragma warning disable CS1591
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            services.AddMvc().AddJsonOptions(options =>
            {
                options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
            });

            // Setting dependency injection

            // For DbContext
            services.AddDbContext<OnLineStoreDbContext>(options => options.UseSqlServer(Configuration["AppSettings:ConnectionString"]));

            // User info
            services.AddScoped<IUserInfo, UserInfo>();

            // Logger for services
            services.AddScoped<ILogger, Logger<Service>>();

            // Services
            services.AddScoped<IHumanResourcesService, HumanResourcesService>();
            services.AddScoped<IWarehouseService, WarehouseService>();
            services.AddScoped<ISalesService, SalesService>();

            // Configuration for Help page
            services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new Info { Title = "OnLine Store API", Version = "v1" });

                // Get xml comments path
                var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);

                // Set xml path
                options.IncludeXmlComments(xmlPath);
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
                app.UseDeveloperExceptionPage();

            // todo: Set port number for client app
            app.UseCors(policy =>
            {
                // Add client origin in CORS policy
                policy.WithOrigins("http://localhost:4200");
                policy.AllowAnyHeader();
                policy.AllowAnyMethod();
            });

            // Configuration for Swagger
            app.UseSwagger();

            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "OnLine Store API");
            });

            app.UseMvc();
        }
    }
#pragma warning restore CS1591
}
This class it's the configuration point for Web API project, in this class there is the configuration for dependency injection, API's configuration and another settings.
For Web API project, these are the routes for controllers:
VerbRouteDescription
GETapi/v1/Sales/OrderGet orders
GETapi/v1/Sales/Order/1Get order by id
GETapi/v1/Sales/CreateOrderRequestGet model to create order
GETapi/v1/Sales/CloneOrder/3Clone an existing order
POSTapi/v1/Sales/OrderCreate a new order
DELETEapi/v1/Sales/OrderDelete an existing order
As we can see there is a v1 in each route, this is because the version for Web API is 1 and that value is defined in Route attribute for controllers in Web API project.

Chapter 06 - Help Page for Web API

Web API uses Swagger to show a help page.
The following package is required to show a help page with Swagger:
  • Swashbuckle.AspNetCore
The configuration for Swagger is located in Startup class, addition for Swagger is in ConfigureServicesmethod:
// Configuration for Help page
services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new Info { Title = "OnLine Store API", Version = "v1" });

    // Get xml comments path
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);

    // Set xml path
    options.IncludeXmlComments(xmlPath);
});
The configuration for endpoint is in Configure method:
// Configuration for Swagger
app.UseSwagger();

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "OnLine Store API");
});
Swagger allows to show description for actions in controllers, these descriptions are taken from xml comments.
Help Page:
Help Page For Web API
Models Section in Help Page:
Models Section In Help Page For Web API
Help Page for Web API it's a good practice, because provides information about API for clients.

Chapter 07 - Unit Tests for Web API

Now we proceed to add unit tests for Web API project, these tests work with in memory database, what is the difference between unit tests and integration tests? for unit tests we simulate all dependencies for Web API project and for integration tests we run a process that simulates Web API execution. I mean a simulation of Web API (accept Http requests), obviously there is more information about unit tests and integration tests but at this point this basic idea is enough.
What is TDD? Testing is important in these days, because with unit tests it's easy to performing tests for features before to publish, Test Driven Development (TDD) is the way to define unit tests and validate the behavior in 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, take a look for SalesControllerTests class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using OnLineStore.Common;
using OnLineStore.Core.BusinessLayer.Requests;
using OnLineStore.Core.BusinessLayer.Responses;
using OnLineStore.Core.DataLayer.Sales;
using OnLineStore.Core.EntityLayer.Sales;
using OnLineStore.WebAPI.Controllers;
using OnLineStore.WebAPI.Requests;
using OnLineStore.WebAPI.UnitTests.Mocks;
using Xunit;

namespace OnLineStore.WebAPI.UnitTests
{
    public class SalesControllerTests
    {
        [Fact]
        public async Task TestGetOrdersAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetOrdersAsync));
            var controller = new SalesController(logger, service);

            // Act
            var response = await controller.GetOrdersAsync() as ObjectResult;
            var value = response.Value as IPagedResponse<OrderInfo>;

            service.Dispose();

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

        [Fact]
        public async Task TestGetOrdersByCurrencyAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetOrdersByCurrencyAsync));
            var controller = new SalesController(logger, service);
            var currencyID = (short?)1000;

            // Act
            var response = await controller.GetOrdersAsync(currencyID: currencyID) as ObjectResult;
            var value = response.Value as IPagedResponse<OrderInfo>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.Count() > 0);
            Assert.True(value.Model.Count(item => item.CurrencyID == currencyID) == value.Model.Count());
        }

        [Fact]
        public async Task TestGetOrdersByCustomerAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetOrdersByCustomerAsync));
            var controller = new SalesController(logger, service);
            var customerID = 1;

            // Act
            var response = await controller.GetOrdersAsync(customerID: customerID) as ObjectResult;
            var value = response.Value as IPagedResponse<OrderInfo>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.Count(item => item.CustomerID == customerID) == value.Model.Count());
        }

        [Fact]
        public async Task TestGetOrdersByEmployeeAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetOrdersByEmployeeAsync));
            var controller = new SalesController(logger, service);
            var employeeID = 1;

            // Act
            var response = await controller.GetOrdersAsync(employeeID: employeeID) as ObjectResult;
            var value = response.Value as IPagedResponse<OrderInfo>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.Count(item => item.EmployeeID == employeeID) == value.Model.Count());
        }

        [Fact]
        public async Task TestGetOrderAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetOrderAsync));
            var controller = new SalesController(logger, service);
            var id = 1;

            // Act
            var response = await controller.GetOrderAsync(id) as ObjectResult;
            var value = response.Value as ISingleResponse<OrderHeader>;

            service.Dispose();

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

        [Fact]
        public async Task TestGetNonExistingOrderAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetNonExistingOrderAsync));
            var controller = new SalesController(logger, service);
            var id = 0;

            // Act
            var response = await controller.GetOrderAsync(id) as ObjectResult;
            var value = response.Value as ISingleResponse<OrderHeader>;

            service.Dispose();

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

        [Fact]
        public async Task TestGetCreateOrderRequestAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestGetCreateOrderRequestAsync));
            var controller = new SalesController(logger, service);

            // Act
            var response = await controller.GetCreateOrderRequestAsync() as ObjectResult;
            var value = response.Value as ISingleResponse<CreateOrderRequest>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.Products.Count() > 0);
            Assert.True(value.Model.Customers.Count() > 0);
        }

        [Fact]
        public async Task TestCreateOrderAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestCreateOrderAsync));
            var controller = new SalesController(logger, service);
            var model = new OrderHeaderRequest
            {
                CustomerID = 1,
                PaymentMethodID = new Guid("7671A4F7-A735-4CB7-AAB4-CF47AE20171D"),
                Comments = "Order from unit tests",
                CreationUser = "unitests",
                CreationDateTime = DateTime.Now,
                Details = new List<OrderDetailRequest>
                {
                    new OrderDetailRequest
                    {
                        ProductID = 1,
                        ProductName = "The King of Fighters XIV",
                        Quantity = 1,
                    }
                }
            };

            // Act
            var response = await controller.CreateOrderAsync(model) as ObjectResult;
            var value = response.Value as ISingleResponse<OrderHeader>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
            Assert.True(value.Model.OrderHeaderID.HasValue);
        }

        [Fact]
        public async Task TestCloneOrderAsync()
        {
            // Arrange
            var logger = LoggingHelper.GetLogger<SalesController>();
            var service = ServiceMocker.GetSalesService(nameof(TestCloneOrderAsync));
            var controller = new SalesController(logger, service);
            var id = 1;

            // Act
            var response = await controller.CloneOrderAsync(id) as ObjectResult;
            var value = response.Value as ISingleResponse<OrderHeader>;

            service.Dispose();

            // Assert
            Assert.False(value.DidError);
        }
    }
}
As we can see those methods perform tests for Urls in Web API project, please take care about the tests are async methods.

Chapter 08 - Integration Tests for Web API

In order to work with integration tests, we need to create a class to provide a Web Host to performing Http behavior, this class it will be TestFixture and to represent Http requests for Web API, there is a class with name SalesTests, this class will contains all requests for defined actions in SalesController class, but using a mocked Http client.
Code for TestFixture class:
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Store.API.IntegrationTests
{
    public class TestFixture<TStartup> : IDisposable
    {
        public static string GetProjectPath(string projectRelativePath, Assembly startupAssembly)
        {
            var projectName = startupAssembly.GetName().Name;

            var applicationBasePath = AppContext.BaseDirectory;

            var directoryInfo = new DirectoryInfo(applicationBasePath);

            do
            {
                directoryInfo = directoryInfo.Parent;

                var projectDirectoryInfo = new DirectoryInfo(Path.Combine(directoryInfo.FullName, projectRelativePath));

                if (projectDirectoryInfo.Exists)
                    if (new FileInfo(Path.Combine(projectDirectoryInfo.FullName, projectName, $"{projectName}.csproj")).Exists)
                        return Path.Combine(projectDirectoryInfo.FullName, projectName);
            }
            while (directoryInfo.Parent != null);

            throw new Exception($"Project root could not be located using the application root {applicationBasePath}.");
        }

        private TestServer Server;

        public TestFixture()
            : this(Path.Combine(""))
        {
        }

        protected TestFixture(string relativeTargetProjectParentDir)
        {
            var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
            var contentRoot = GetProjectPath(relativeTargetProjectParentDir, startupAssembly);

            var configurationBuilder = new ConfigurationBuilder()
                .SetBasePath(contentRoot)
                .AddJsonFile("appsettings.json");

            var webHostBuilder = new WebHostBuilder()
                .UseContentRoot(contentRoot)
                .ConfigureServices(InitializeServices)
                .UseConfiguration(configurationBuilder.Build())
                .UseEnvironment("Development")
                .UseStartup(typeof(TStartup));

            Server = new TestServer(webHostBuilder);

            Client = Server.CreateClient();
            Client.BaseAddress = new Uri("http://localhost:1234");
            Client.DefaultRequestHeaders.Accept.Clear();
            Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        }

        public void Dispose()
        {
            Client.Dispose();
            Server.Dispose();
        }

        public HttpClient Client { get; }

        protected virtual void InitializeServices(IServiceCollection services)
        {
            var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;

            var manager = new ApplicationPartManager();

            manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
            manager.FeatureProviders.Add(new ControllerFeatureProvider());
            manager.FeatureProviders.Add(new ViewComponentFeatureProvider());

            services.AddSingleton(manager);
        }
    }
}
Now, this is the code for SalesTests class:
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using OnLineStore.WebAPI.IntegrationTests.Helpers;
using Xunit;

namespace OnLineStore.WebAPI.IntegrationTests
{
    public class SalesTests : IClassFixture<TestFixture<Startup>>
    {
        private HttpClient Client;

        public SalesTests(TestFixture<Startup> fixture)
        {
            Client = fixture.Client;
        }

        [Fact]
        public async Task TestGetOrdersAsync()
        {
            // Arrange
            var request = "/api/v1/Sales/Order";

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrdersByCurrencyAsync()
        {
            // Arrange
            var currencyID = (short)1;
            var request = string.Format("/api/v1/Sales/Order?currencyID={0}", currencyID);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrdersByCustomerAsync()
        {
            // Arrange
            var customerID = 1;
            var request = string.Format("/api/v1/Sales/Order?customerID={0}", customerID);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrdersByEmployeeAsync()
        {
            // Arrange
            var employeeID = 1;
            var request = string.Format("/api/v1/Sales/Order?employeeID={0}", employeeID);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrderByIdAsync()
        {
            // Arrange
            var id = 1;
            var request = string.Format("/api/v1/Sales/Order/{0}", id);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrderByNonExistingIdAsync()
        {
            // Arrange
            var id = 0;
            var request = string.Format("/api/v1/Sales/Order/{0}", id);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            Assert.True(response.StatusCode == HttpStatusCode.NotFound);
        }

        [Fact]
        public async Task TestGetCreateOrderRequestAsync()
        {
            // Arrange
            var request = "/api/v1/Sales/CreateOrderRequest";

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestCreateOrderAsync()
        {
            // Arrange
            var request = "/api/v1/Sales/Order";
            var model = new
            {
                CustomerID = 1,
                PaymentMethodID = new Guid("7671A4F7-A735-4CB7-AAB4-CF47AE20171D"),
                Comments = "Order from integration tests",
                CreationUser = "integrationtests",
                Details = new[]
                {
                    new
                    {
                        ProductID = 1,
                        Quantity = 1
                    }
                }
            };

            // Act
            var response = await Client.PostAsync(request, ContentHelper.GetStringContent(model));

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestCloneOrderAsync()
        {
            // Arrange
            var id = 1;
            var request = string.Format("/api/v1/Sales/CloneOrder/{0}", id);

            // Act
            var response = await Client.GetAsync(request);

            // Assert
            response.EnsureSuccessStatusCode();
        }
    }
}
Don't forget we can have more tests, we have class with name ProductionTests to perform requests for ProductionController.

Code Improvements

  1. Add Security (e.g. IdentityServer4)
  2. Save logs to text file or database
  3. Implement Money Pattern to represent money in application
  4. Add section to explain why this code doesn't use Generic Repository and Unit of Work

No comments:

Post a Comment

Angular Tutorial (Update to Angular 7)

As Angular 7 has just been released a few days ago. This tutorial is updated to show you how to create an Angular 7 project and the new fe...