Click here to Skip to main content
13,863,229 members
Click here to Skip to main content
Add your own
alternative version

Stats

132K views
307 bookmarked
Posted 11 Dec 2016
Licenced CPOL

Entity Framework Core 2 for Enterprise

, 10 Feb 2019
Rate this:
Please Sign up or sign in to vote.
Entity Framework Core 2 for Enterprise

Introduction

Design and implement architecture for enterprise applications it's a big challenge, there is a common question in this point: What is the the best way 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 enterprise architect in this article.

The sample database provided in this guide represents an online store.

This is the stack for this solution:

  • Entity Framework Core
  • ASP.NET Core
  • xUnit for unit tests
  • xUnit for integration tests
  • Identity Server 4

Payment Gateway

Online Store needs to receive payments through payment service, for this case I created a payment gateway named Rothschild House, you can found source code in this link.

Rothschild House allows to receive payments with credit cards.

This payment gateway is for demonstration purposes only.

Running Payment Gateway

Run RothschildHouse solution, solution has these projects:

Project Runs on port
RothschildHouse 18000
RothschildHouse.IdentityServer 19000

The above output is the configuration for Identity Server, RothschildHouse.IdentityServer provides authentication and authorization to receive payments.

Credentials for Online Store

User name Password
administrator@onlinestore.com onlinestore1
These credentials are provided by Rothschild House to clients.

Payment Schema

RothschildHouse project contains PaymentDbContext class, this class provides the model to process payments.

Entities:

  • Person
  • CreditCard
  • PaymentMethod
  • PaymentTransaction

Payment Data

PaymentDbContext accepts these customers:

User name Password Card holder name Issuing network Card number Expiration Date CVV
jameslogan@walla.com wolverine James Logan Visa 4024007164051145 6/1/2024 987
ororo_munroe@yahoo.com storm Ororo Munroe MasterCard 5473913699329307 1/1/2023 147

Background

According to my experience, enterprise applications should have the following levels:

  1. Database: Is the relational database management system
  2. Common: Contains common objects for layers (e.g. Loggers, Mappers, Extensions)
  3. Domain Driven Design: Contains objects related to database access
  4. Business Layer: Contains definitions and validations related to business
  5. Tests (QA): Contains tests for back-end (units and integration)
  6. External Services Layer (optional): Contains invocations for external services (ASMX, WCF, RESTful)
  7. Security Layer: Is an API that provides authentication and authorization
  8. Presentation Layer: This is the UI
  9. UI Tests (QA): Contains automated tests for front-end

Architecture: Big Picture

DATABASE SQL Server DATABASE
COMMON Extensions, Helpers (Loggers and Mappers) BACK-END
DOMAIN DRIVEN DESIGN POCOs, DbContext, Configurations and Data Contracts
BUSINESS LAYER Services, Contracts, DataContracts, Exceptions
Tests (QA) LAYER Unit tests and Integration tests
EXTERNAL SERVICES LAYER ASMX, WCF, RESTful
SECURITY LAYER Authentication and Authorization (Identity Server | Others)
PRESENTATION LAYER UI Frameworks (Angular | ReactJS | Vue.js | Others) FRONT-END

Prerequisites

Skills

Before to continuing, keep in mind we need to have the folllowing skills in order to understand this guide:

  • Object Oriented Programming
  • Aspect Oriented Programming
  • Object Relational Mapping
  • Design Patterns

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:

  • Dbo
  • HumanResources
  • Sales
  • Warehouse

Each schema represents a division for online store company, keep this in mind because all code is designed following this aspect; at this moment this code only implements features for Warehouse and Sales schemas.

All tables have a primary key with one column and have columns for creation, last update and concurrency token.

Tables

Schema Name
dbo ChangeLog
dbo ChangeLogExclusion
dbo Country
dbo CountryCurrency
dbo Currency
dbo EventLog
HumanResources Employee
HumanResources EmployeeAddress
HumanResources EmployeeEmail
Sales Customer
Sales OrderDetail
Sales OrderHeader
Sales OrderStatus
Sales PaymentMethod
Sales Shipper
Warehouse Location
Warehouse Product
Warehouse ProductCategory
Warehouse ProductInventory

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.

Online Store works 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:

Identifier Case Example
Namespace PascalCase Store
Class PascalCase Product
Interface I prefix + PascalCase ISalesRepository
Method Verb in PascalCase + Noun in PascalCase GetProducts
Async Method Verb in PascalCase + Noun in PascalCase + Async sufix GetOrdersAsync
Property PascalCase Description
Parameter camelCase connectionString

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

This is the structure for OnlineStore.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 DataLayer\DataContracts, we'll place all object definitions for returned values from Contracts namespace, for now this directory contains OrderInfo class definition.

For DataLayer\Configurations, we'll place all object definitions related to mapping classes for database.

Inside of EntityLayer and DataLayer\Configurations, 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

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 delete operations.

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

Data Layer

This solution doesn't work with Repository pattern anymore, later I'll add an explanation why.

We're working with Entity Framework Core in this guide, so We need to have a DbContext and objects that allow mapping database objects like tables and views.

Naming Issue

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("varchar(10)");
            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.

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 Entity Framework Core provides.

For the last version of this article, We have a payment gateway named Rothschild House, this API provides payment authorization, this API below to external services layer.

Later, I'll add a section to explain about payment gateway.

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 HumanResources repositories.

You can also read more about this point in this link.

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 OnLineStoreDbContextExtensions 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

Naming Issue

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 Service as 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.Collections.ObjectModel;
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, string 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;
                        detail.CreationUser = header.CreationUser;

                        // 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,
                            CreationUser = header.CreationUser,
                            CreationDateTime = DateTime.Now
                        };

                        // 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?.Count > 0)
                    {
                        response.Model.OrderDetails = new Collection<OrderDetail>();

                        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)
                    return response;

                // 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
Name Description
AddOrderWithDiscontinuedProductException Represents an exception adding order with a discontinued product
ForeignKeyDependencyException Represents an exception deleting an order with detail rows
DuplicatedProductNameException Represents an exception adding product with existing name
NonExistingProductException Represents 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 = LoggingHelper.GetLogger<ISalesService>();

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

// 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.GetOrderHeadersAsync(pageSize, pageNumber);

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

As we can see, GetOrderHeadersAsync method in SalesService retrieves rows from Sales.OrderHeader 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 = LoggingHelper.GetLogger<ISalesService>();

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

// 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.GetOrderHeaderAsync(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.OrderHeader, Sales.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 OrderHeader, OrderDetail and ProductInventory tables.

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 - Payment Gateway

The payment gateway implements Identity Server as authentication and authorization API.

Payment Gateway has two projects:

  • RothschildHouse.IdentityServer
  • RothschildHouse

RothschildHouse.IdentityServer

Payment gateway implements in-memory configuration for Identity Server.

Identity Server API for Payment Gateway runs on port 18000.

In browser, open http://localhost:18000/.well-known/openid-configuration url:

{  
   "issuer":"http://localhost:18000",
   "jwks_uri":"http://localhost:18000/.well-known/openid-configuration/jwks",
   "authorization_endpoint":"http://localhost:18000/connect/authorize",
   "token_endpoint":"http://localhost:18000/connect/token",
   "userinfo_endpoint":"http://localhost:18000/connect/userinfo",
   "end_session_endpoint":"http://localhost:18000/connect/endsession",
   "check_session_iframe":"http://localhost:18000/connect/checksession",
   "revocation_endpoint":"http://localhost:18000/connect/revocation",
   "introspection_endpoint":"http://localhost:18000/connect/introspect",
   "device_authorization_endpoint":"http://localhost:18000/connect/deviceauthorization",
   "frontchannel_logout_supported":true,
   "frontchannel_logout_session_supported":true,
   "backchannel_logout_supported":true,
   "backchannel_logout_session_supported":true,
   "scopes_supported":[  
      "RothschildHouseApi",
      "offline_access"
   ],
   "claims_supported":[  

   ],
   "grant_types_supported":[  
      "authorization_code",
      "client_credentials",
      "refresh_token",
      "implicit",
      "password",
      "urn:ietf:params:oauth:grant-type:device_code"
   ],
   "response_types_supported":[  
      "code",
      "token",
      "id_token",
      "id_token token",
      "code id_token",
      "code token",
      "code id_token token"
   ],
   "response_modes_supported":[  
      "form_post",
      "query",
      "fragment"
   ],
   "token_endpoint_auth_methods_supported":[  
      "client_secret_basic",
      "client_secret_post"
   ],
   "subject_types_supported":[  
      "public"
   ],
   "id_token_signing_alg_values_supported":[  
      "RS256"
   ],
   "code_challenge_methods_supported":[  
      "plain",
      "S256"
   ]
}

In order to allow connections, We need to add configuration for API resources and clients, this configuration is in Config class:

using System.Collections.Generic;
using System.Security.Claims;
using IdentityModel;
using IdentityServer4.Models;

namespace RothschildHouse.IdentityServer
{
    public static class Config
    {
        public static IEnumerable<ApiResource> GetApiResources()
            => new List<ApiResource>
            {
                new ApiResource("RothschildHouseApi", "Rothschild House API")
            };

        public static IEnumerable<Client> GetClients()
            => new List<Client>
            {
                new Client
                {
                    ClientId = "onlinestoreclient",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    ClientSecrets =
                    {
                        new Secret("onlinestoreclientsecret1".Sha256())
                    },
                    AllowedScopes =
                    {
                        "RothschildHouseApi"
                    },
                    Claims =
                    {
                        new Claim(JwtClaimTypes.Role, "Customer")
                    }
                }
            };
    }
}

Let's take a look in Startup code:

using IdentityServer4.Services;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RothschildHouse.IdentityServer.Models;
using RothschildHouse.IdentityServer.Services;
using RothschildHouse.IdentityServer.Validation;

namespace RothschildHouse.IdentityServer
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            /* Setting up dependency injection */

            // For DbContext
            services.AddDbContext<AuthDbContext>(options => options.UseInMemoryDatabase("Auth"));

            // Password validator and profile
            services
                .AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>()
                .AddTransient<IProfileService, ProfileService>();

            /* Identity Server */

            // Use in-memory configurations

            services
                .AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients());

            // Add authentication
            services
                .AddAuthentication()
                .AddIdentityServerAuthentication();
        }

        // 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();

            /* Seed AuthDbContext in-memory */

            var authDbContext = app
                .ApplicationServices
                .CreateScope()
                .ServiceProvider
                .GetService<AuthDbContext>();

            authDbContext.SeedInMemory();

            app.UseIdentityServer();
        }
    }
}

RothschildHouse

RothschildHouse implements in-memory configuration for Identity Server.

RothschildHouse runs on port 19000.

Configuration for Startup 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.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RothschildHouse.Controllers;
using RothschildHouse.Models;
using Swashbuckle.AspNetCore.Swagger;

namespace RothschildHouse
{
#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)
        {
            /* Setting up dependency injection */

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            services.AddTransient<ILogger<TransactionController>, Logger<TransactionController>>();

            // In-memory DbContext
            services.AddDbContext<PaymentDbContext>(options =>
            {
                options
                    .UseInMemoryDatabase("Payment")
                    .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning));
            });

            // Identity Server
            services
                .AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    // todo: Set values from appsettings file

                    options.Authority = "http://localhost:18000";
                    options.RequireHttpsMetadata = false;
                    options.ApiName = "RothschildHouseApi";
                });

            // Register the Swagger generator, defining 1 or more Swagger documents
            services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new Info { Title = "RothschildHouse 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();

            // Seed initial data in-memory for DbContext
            var paymentDbContext = app
                .ApplicationServices
                .CreateScope()
                .ServiceProvider
                .GetService<PaymentDbContext>();

            paymentDbContext.SeedInMemory();

            app.UseAuthentication();

            // Enable middleware to serve generated Swagger as a JSON endpoint.
            app.UseSwagger();

            // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint.
            app.UseSwaggerUI(options =>
            {
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "RothschildHouse API V1");
            });

            app.UseMvc();
        }
    }
#pragma warning restore CS1591
}

Code for TransactionController class:

using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using RothschildHouse.Models;
using RothschildHouse.Requests;
using RothschildHouse.Responses;

namespace RothschildHouse.Controllers
{
#pragma warning disable CS1591
    [Route("api/v1/[controller]")]
    [ApiController]
    [Authorize]
    public class TransactionController : ControllerBase
    {
        private readonly ILogger<TransactionController> Logger;
        private readonly PaymentDbContext DbContext;

        public TransactionController(ILogger<TransactionController> logger, PaymentDbContext dbContext)
        {
            Logger = logger;
            DbContext = dbContext;
        }
#pragma warning restore CS1591

        /// <summary>
        /// Places a new payment
        /// </summary>
        /// <param name="request">Payment request</param>
        /// <returns>A payment response</returns>
        [HttpPost("Payment")]
        public async Task<IActionResult> PostPaymentAsync([FromBody]PostPaymentRequest request)
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(PostPaymentAsync));

            var creditCards = await DbContext.GetCreditCardByCardHolderName(request.CardHolderName).ToListAsync();

            var creditCard = default(CreditCard);

            var last4Digits = request.CardNumber.Substring(request.CardNumber.Length - 4);

            if (creditCards.Count > 1)
                creditCard = creditCards.FirstOrDefault(item => item.CardNumber == request.CardNumber);
            else if (creditCards.Count == 1)
                creditCard = creditCards.First();

            if (creditCard == null)
                return BadRequest(string.Format("There is not record for credit card with last 4 digits: {0}.", last4Digits));

            /* Check credit card information */

            if (!creditCard.IsValid(request))
                return BadRequest(string.Format("Invalid information for card payment."));

            /* Check if customer has available credit (limit) */

            if (!creditCard.HasFounds(request))
                return BadRequest(string.Format("There are no founds to approve the payment."));

            using (var txn = await DbContext.Database.BeginTransactionAsync())
            {
                try
                {
                    var paymentTxn = new PaymentTransaction
                    {
                        PaymentTransactionID = Guid.NewGuid(),
                        CreditCardID = creditCard.CreditCardID,
                        ConfirmationID = Guid.NewGuid(),
                        Amount = request.Amount,
                        PaymentDateTime = DateTime.Now
                    };

                    DbContext.PaymentTransactions.Add(paymentTxn);

                    creditCard.AvailableFounds -= request.Amount;

                    await DbContext.SaveChangesAsync();

                    txn.Commit();

                    Logger?.LogInformation("The payment for card with last 4 digits: '{0}' was successfully. Confirmation #: {1}", last4Digits, paymentTxn.ConfirmationID);

                    var response = new PaymentResponse
                    {
                        ConfirmationID = paymentTxn.ConfirmationID,
                        PaymentDateTime = paymentTxn.PaymentDateTime,
                        Last4Digits = creditCard.Last4Digits
                    };

                    return Ok(response);
                }
                catch (Exception ex)
                {
                    Logger?.LogCritical("There was an error on '{0}': {1}", nameof(PostPaymentAsync), ex);

                    txn.Rollback();

                    return new ObjectResult(ex.Message)
                    {
                        StatusCode = (int)HttpStatusCode.InternalServerError
                    };
                }
            }
        }
    }
}

Payment request:

using System;
using System.ComponentModel.DataAnnotations;

namespace RothschildHouse.Requests
{
    /// <summary>
    /// Represents the model for payment request
    /// </summary>
    public class PostPaymentRequest
    {
        /// <summary>
        /// Initializes a new instance of <see cref="PostPaymentRequest"/>
        /// </summary>
        public PostPaymentRequest()
        {
        }

        /// <summary>
        /// Gets or sets the card holder's name
        /// </summary>
        [Required]
        [StringLength(30)]
        public string CardHolderName { get; set; }

        /// <summary>
        /// Gets or sets the issuing network
        /// </summary>
        [Required]
        [StringLength(20)]
        public string IssuingNetwork { get; set; }

        /// <summary>
        /// Gets or sets the card number
        /// </summary>
        [Required]
        [StringLength(20)]
        public string CardNumber { get; set; }

        /// <summary>
        /// Gets or sets the expiration date
        /// </summary>
        [Required]
        public DateTime? ExpirationDate { get; set; }

        /// <summary>
        /// Gets or sets the CVV (Card Verification Value)
        /// </summary>
        [Required]
        [StringLength(4)]
        public string Cvv { get; set; }

        /// <summary>
        /// Gets or sets the amount
        /// </summary>
        [Required]
        [Range(0.0, 10000.0)]
        public decimal? Amount { get; set; }
    }
}

Chapter 06 - Online Store Identity Server

This solution implements Identity Server as authentication and authorization API.

This guide implements in-memory configuration for Identity Server.
This implementation for Identity Server doesn't have any value in real life, if do you want to apply Identity Server in real life, remove in-memory configuration and replace it with database store.

Identity Server API for Online Store runs on port 56000.

In order to allow connections, We need to add configuration for API resources and clients, this configuration is in Config class:

using System.Collections.Generic;
using System.Security.Claims;
using IdentityModel;
using IdentityServer4.Models;

namespace OnlineStore.IdentityServer
{
    public static class Config
    {
        public static IEnumerable<ApiResource> GetApiResources()
            => new List<ApiResource>
            {
                new ApiResource("OnlineStoreWebAPI", "Online Store Web API")
            };

        public static IEnumerable<Client> GetClients()
            => new List<Client>
            {
                new Client
                {
                    ClientId = "onlinestoreclient",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    ClientSecrets =
                    {
                        new Secret("onlinestoreclientsecret1".Sha256())
                    },
                    AllowedScopes =
                    {
                        "OnlineStoreWebAPI"
                    },
                    Claims =
                    {
                        new Claim(JwtClaimTypes.Role, "Administrator"),
                        new Claim(JwtClaimTypes.Role, "Customer")
                    }
                }
            };
    }
}

Also We need to add the configuration for services in Identity Server API Startup class:

using IdentityServer4.Services;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using OnlineStore.IdentityServer.Models;
using OnlineStore.IdentityServer.Services;
using OnlineStore.IdentityServer.Validation;

namespace OnlineStore.IdentityServer
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            /* Setting up dependency injection */

            // DbContext
            services.AddDbContext<AuthDbContext>(options => options.UseInMemoryDatabase("Auth"));

            // Password validator and profile
            services
                .AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>()
                .AddTransient<IProfileService, ProfileService>();

            /* Identity Server */

            services
                .AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients());

            services
                .AddAuthentication()
                .AddIdentityServerAuthentication();
        }

        // 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();

            /* Seed in-memory DbContext */

            var authDbContext = app
                .ApplicationServices
                .CreateScope()
                .ServiceProvider
                .GetService<AuthDbContext>();

            authDbContext.SeedInMemory();

            app.UseIdentityServer();
        }
    }
}

As We said before, We're working with in-memory configurations, these configurations apply for DbContext and Configuration.

To handle authentication and autorization, there are two entities: User and UserClaim.

using Microsoft.EntityFrameworkCore;

namespace OnlineStore.IdentityServer.Models
{
    public class AuthDbContext : DbContext
    {
        public AuthDbContext(DbContextOptions<AuthDbContext> options)
            : base(options)
        {
        }

        public DbSet<User> Users { get; set; }

        public DbSet<UserClaim> UserClaims { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Set key for entities

            modelBuilder
                .Entity<User>(builder => builder.HasKey(e => e.UserID));

            modelBuilder
                .Entity<UserClaim>(builder => builder.HasKey(e => e.UserClaimID));

            base.OnModelCreating(modelBuilder);
        }
    }
}

OnLineStore doesn't use Repository and Unit of Work design patterns anymore, the replacement for these design patterns is to have extension methods for DbContext class, later I'll explain this point with examples.

This is the code for AuthDbContextExtentions class:

using System;
using System.Collections.Generic;
using System.Linq;
using IdentityModel;

namespace OnlineStore.IdentityServer.Models
{
    public static class AuthDbContextExtentions
    {
        public static bool ValidatePassword(this AuthDbContext dbContext, string userName, string password)
        {
            var user = dbContext.Users.FirstOrDefault(item => item.Email == userName);

            if (user == null)
                return false;

            if (user.Password == password.ToSha256())
                return true;

            return false;
        }

        public static User GetUserByUserName(this AuthDbContext dbContext, string userName)
            => dbContext.Users.FirstOrDefault(item => item.Email == userName);

        public static User GetUserByID(this AuthDbContext dbContext, string id)
            => dbContext.Users.FirstOrDefault(item => item.UserID == id);

        public static IEnumerable<UserClaim> GetUserClaimsByUserID(this AuthDbContext dbContext, string userID)
            => dbContext.UserClaims.Where(item => item.UserID == userID);

        public static void SeedInMemory(this AuthDbContext dbContext)
        {
            dbContext.Users.Add(new User("10000", "jameslogan@walla.com", "wolverine".ToSha256(), true));

            dbContext.UserClaims.AddRange(
                new UserClaim(Guid.NewGuid(), "10000", JwtClaimTypes.Subject, "10000"),
                new UserClaim(Guid.NewGuid(), "10000", JwtClaimTypes.PreferredUserName, "jameslogan"),
                new UserClaim(Guid.NewGuid(), "10000", JwtClaimTypes.Email, "jameslogan@walla.com"),
                new UserClaim(Guid.NewGuid(), "10000", JwtClaimTypes.Role, "Customer"),
                new UserClaim(Guid.NewGuid(), "10000", JwtClaimTypes.GivenName, "James"),
                new UserClaim(Guid.NewGuid(), "10000", JwtClaimTypes.MiddleName, ""),
                new UserClaim(Guid.NewGuid(), "10000", JwtClaimTypes.FamilyName, "Logan")
            );

            dbContext.SaveChanges();
        }
    }
}

All actions in OnLineStore Web API need to get token from Identity Server API.

Chapter 07 - 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.

Clients for Payment Gateway

Payment Gateway provides two APIs, one for authentication and other for payment, in order to perform payment requests there are two clients for Rothschild House.

Code for RothschildHouseIdentityClient class:

using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.Extensions.Options;
using OnlineStore.WebAPI.Clients.Contracts;

namespace OnlineStore.WebAPI.Clients
{
#pragma warning disable CS1591
    public class RothschildHouseIdentityClient : IRothschildHouseIdentityClient
    {
        private readonly RothschildHouseIdentitySettings Settings;

        public RothschildHouseIdentityClient(IOptions<RothschildHouseIdentitySettings> settings)
        {
            Settings = settings.Value;
        }

        public async Task<TokenResponse> GetRothschildHouseTokenAsync()
        {
            using (var client = new HttpClient())
            {
                var disco = await client.GetDiscoveryDocumentAsync(Settings.Url);

                return await client.RequestPasswordTokenAsync(new PasswordTokenRequest
                {
                    Address = disco.TokenEndpoint,
                    ClientId = Settings.ClientId,
                    ClientSecret = Settings.ClientSecret,
                    UserName = Settings.UserName,
                    Password = Settings.Password
                });
            }
        }
    }
#pragma warning restore CS1591
}

Code for RothschildHousePaymentClient class:

using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.Extensions.Options;
using OnlineStore.WebAPI.Clients.Contracts;
using OnlineStore.WebAPI.Clients.Models;

namespace OnlineStore.WebAPI.Clients
{
#pragma warning disable CS1591
    public class RothschildHousePaymentClient : IRothschildHousePaymentClient
    {
        private readonly RothschildHousePaymentSettings Settings;
        private readonly ApiUrl apiUrl;

        public RothschildHousePaymentClient(IOptions<RothschildHousePaymentSettings> settings)
        {
            Settings = settings.Value;
            apiUrl = new ApiUrl(baseUrl: Settings.Url);
        }

        public async Task<HttpResponseMessage> PostPaymentAsync(TokenResponse token, PostPaymentRequest request)
        {
            using (var client = new HttpClient())
            {
                client.SetBearerToken(token.AccessToken);

                return await client.PostAsync(
                    apiUrl.Controller("Transaction").Action("Payment").ToString(),
                    request.GetStringContent()
                    );
            }
        }
    }
#pragma warning restore CS1591
}

Online Store Web API

We'll take a look on SalesController class:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using OnlineStore.Core.BusinessLayer.Contracts;
using OnlineStore.WebAPI.Clients.Contracts;
using OnlineStore.WebAPI.Clients.Models;
using OnlineStore.WebAPI.Filters;
using OnlineStore.WebAPI.Requests;
using OnlineStore.WebAPI.Responses;

namespace OnlineStore.WebAPI.Controllers
{
#pragma warning disable CS1591
    [Authorize]
    [ApiController]
    [Route("api/v1/[controller]")]
    public class SalesController : OnlineStoreController
    {
        protected readonly ILogger Logger;
        protected readonly IRothschildHouseIdentityClient RothschildHouseIdentityClient;
        protected readonly IRothschildHousePaymentClient RothschildHousePaymentClient;
        protected readonly ISalesService SalesService;

        public SalesController(
            ILogger<SalesController> logger,
            IRothschildHouseIdentityClient rothschildHouseIdentityClient,
            IRothschildHousePaymentClient rothschildHousePaymentClient,
            ISalesService salesService
            )
            : base()
        {
            Logger = logger;
            RothschildHouseIdentityClient = rothschildHouseIdentityClient;
            RothschildHousePaymentClient = rothschildHousePaymentClient;
            SalesService = salesService;
        }
#pragma warning restore CS1591

        /// <summary>
        /// Retrieves the orders placed 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>
        /// <response code="200">Returns a list of orders</response>
        /// <response code="401">If client is not authenticated</response>
        /// <response code="403">If client is not autorized</response>
        /// <response code="500">If there was an internal error</response>
        [HttpGet("Order")]
        [ProducesResponseType(200)]
        [ProducesResponseType(401)]
        [ProducesResponseType(403)]
        [ProducesResponseType(500)]
        [OnlineStoreActionFilter]
        public async Task<IActionResult> GetOrdersAsync(int? pageSize = 50, int? pageNumber = 1, short? orderStatusID = null, int? customerID = null, int? employeeID = null, int? shipperID = null, string 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>
        /// <response code="200">If id exists</response>
        /// <response code="401">If client is not authenticated</response>
        /// <response code="403">If client is not autorized</response>
        /// <response code="404">If id is not exists</response>
        /// <response code="500">If there was an internal error</response>
        [HttpGet("Order/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(401)]
        [ProducesResponseType(403)]
        [ProducesResponseType(404)]
        [ProducesResponseType(500)]
        [OnlineStoreActionFilter]
        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>
        /// <response code="200">Returns the model to create a new order</response>
        /// <response code="401">If client is not authenticated</response>
        /// <response code="403">If client is not autorized</response>
        /// <response code="500">If there was an internal error</response>
        [HttpGet("CreateOrderRequest")]
        [ProducesResponseType(200)]
        [ProducesResponseType(401)]
        [ProducesResponseType(403)]
        [ProducesResponseType(500)]
        [OnlineStoreActionFilter]
        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">Request</param>
        /// <returns>A result that contains the order ID generated by API</returns>
        /// <response code="200">If order was created successfully</response>
        /// <response code="400">If the request is invalid</response>
        /// <response code="401">If client is not authenticated</response>
        /// <response code="403">If client is not autorized</response>
        /// <response code="500">If there was an internal error</response>
        [HttpPost("Order")]
        [ProducesResponseType(200)]
        [ProducesResponseType(400)]
        [ProducesResponseType(401)]
        [ProducesResponseType(403)]
        [ProducesResponseType(500)]
        [OnlineStoreActionFilter]
        public async Task<IActionResult> PostOrderAsync([FromBody]PostOrderRequest request)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(PostOrderAsync));

            var token = await RothschildHouseIdentityClient.GetRothschildHouseTokenAsync();

            if (token.IsError)
                return Unauthorized();

            var paymentRequest = request.GetPostPaymentRequest();

            var paymentHttpResponse = await RothschildHousePaymentClient.PostPaymentAsync(token, paymentRequest);

            if (!paymentHttpResponse.IsSuccessStatusCode)
                return BadRequest();

            var paymentResponse = await paymentHttpResponse.GetPaymentResponseAsync();

            var entity = request.GetOrderHeader();

            entity.CreationUser = UserInfo.UserName;

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

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

        /// <summary>
        /// Creates a new order model from existing order
        /// </summary>
        /// <param name="id">Order ID</param>
        /// <returns>A model for a new order</returns>
        /// <response code="200">If order was cloned successfully</response>
        /// <response code="401">If client is not authenticated</response>
        /// <response code="403">If client is not autorized</response>
        /// <response code="404">If id is not exists</response>
        /// <response code="500">If there was an internal error</response>
        [HttpGet("CloneOrder/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(401)]
        [ProducesResponseType(403)]
        [ProducesResponseType(500)]
        [OnlineStoreActionFilter]
        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>
        /// <response code="200">If order was deleted successfully</response>
        /// <response code="401">If client is not authenticated</response>
        /// <response code="403">If client is not autorized</response>
        /// <response code="404">If id is not exists</response>
        /// <response code="500">If there was an internal error</response>
        [HttpDelete("Order/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(401)]
        [ProducesResponseType(403)]
        [ProducesResponseType(404)]
        [ProducesResponseType(500)]
        [OnlineStoreActionFilter]
        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.

Settings

To provide settings for Web API, first We need to define configurations in appsettings.json file:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "OnlineStore": "server=(local);database=OnlineStore;integrated security=yes;MultipleActiveResultSets=True;"
  },
  "IdentityServerSettings": {
    "Authority": "http://localhost:56000",
    "RequireHttpsMetadata": false,
    "ApiName": "OnlineStoreWebAPI"
  },
  "OnlineStoreIdentityClientSettings": {
    "Url": "http://localhost:56000",
    "ClientId": "onlinestoreclient",
    "ClientSecret": "onlinestoreclientsecret1",
    "UserName": "",
    "Password": ""
  },
  "RothschildHouseIdentitySettings": {
    "Url": "http://localhost:18000",
    "ClientId": "onlinestoreclient",
    "ClientSecret": "onlinestoreclientsecret1",
    "UserName": "administrator@onlinestore.com",
    "Password": "onlinestore1"
  },
  "RothschildHousePaymentSettings": {
    "Url": "http://localhost:19000"
  }
}

Then 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 OnlineStore.WebAPI.Clients;
using OnlineStore.WebAPI.Clients.Contracts;
using OnlineStore.WebAPI.PolicyRequirements;
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)
        {
            /* Setting dependency injection */

            // For DbContext
            services.AddDbContext<OnlineStoreDbContext>(builder =>
            {
                builder.UseSqlServer(Configuration["ConnectionStrings:OnlineStore"]);
            });

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

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

            services.Configure<OnlineStoreIdentityClientSettings>(Configuration.GetSection("OnlineStoreIdentityClientSettings"));
            services.AddSingleton<OnlineStoreIdentityClientSettings>();

            // Rothschild House Payment gateway
            services.Configure<RothschildHouseIdentitySettings>(Configuration.GetSection("RothschildHouseIdentitySettings"));
            services.AddSingleton<RothschildHouseIdentitySettings>();

            services.Configure<RothschildHousePaymentSettings>(Configuration.GetSection("RothschildHousePaymentSettings"));
            services.AddSingleton<RothschildHousePaymentSettings>();

            services.AddScoped<IRothschildHouseIdentityClient, RothschildHouseIdentityClient>();
            services.AddScoped<IRothschildHousePaymentClient, RothschildHousePaymentClient>();

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

            /* Configuration for MVC */

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

            /* Configuration for authorization */

            services
                .AddMvcCore()
                .AddAuthorization(options =>
                {
                    options.AddPolicy("AdministratorPolicy", builder =>
                    {
                        builder.Requirements.Add(new AdministratorPolicyRequirement());
                    });

                    options.AddPolicy("CustomerPolicy", builder =>
                    {
                        builder.Requirements.Add(new CustomerPolicyRequirement());
                    });
                });

            /* Configuration for IdentityServer authentication */

            services
                .AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    var settings = new IdentityServerSettings();

                    Configuration.Bind("IdentityServerSettings", settings);

                    options.Authority = settings.Authority;
                    options.RequireHttpsMetadata = settings.RequireHttpsMetadata;
                    options.ApiName = settings.ApiName;
                });

            /* 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();

            app.UseCors(builder =>
            {
                // Add client origin in CORS policy

                // todo: Set port number for client app from appsettings file

                builder.WithOrigins("http://localhost:4200");
                builder.AllowAnyHeader();
                builder.AllowAnyMethod();
            });

            /* Use authentication for Web API */

            app.UseAuthentication();

            /* Configuration for Swagger */

            app.UseSwagger();

            app.UseSwaggerUI(options =>
            {
                options.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:

Verb Route Description
GET api/v1/Sales/Order Get orders
GET api/v1/Sales/Order/1 Get order by id
GET api/v1/Sales/CreateOrderRequest Get model to create order
GET api/v1/Sales/CloneOrder/3 Clone an existing order
POST api/v1/Sales/Order Create a new order
DELETE api/v1/Sales/Order Delete 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 08 - 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 ConfigureServices method:

/* 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(options =>
{
    options.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 09 - Unit Tests for Web API

Now We proceed to explain 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 is the way to define tests and validate the behavior in code.

Another concept linked to TDD is AAA: Arrange, Act and Assert is a pattern for arranging and formatting code in test methods.

  1. Arrange: is the block for creation of objects
  2. Act: is the block to place all invocations for methods
  3. Assert: is the block to validate the results from methods invocation

Unit tests should be created according to Web API, so let's choose SalesController to explain the model for unit tests, these are the methods in controller:

  • GetOrdersAsync
  • GetOrderAsync
  • GetCreateOrderRequestAsync
  • PostOrderAsync
  • CloneOrderAsync
  • DeleteOrderAsync

Keeping this list in mind, We add the following methods to performing unit tests for SalesController:

Name Description
TestGetOrdersAsync Retrieves orders
TestGetOrdersByCurrencyAsync Retrieves orders by currency
TestGetOrdersByCustomerAsync Retrieves orders by customer
TestGetOrdersByEmployeeAsync Retrieves orders by employee
TestGetOrderAsync Retrieves order by id
TestGetNonExistingOrderAsync Retrieves order by non existing id
TestGetCreateOrderRequestAsync Retrieves model to create a new order
TestPostOrderAsync Creates a new order
TestCloneOrderAsync Clones an existing order

Code for class:

using System.Threading.Tasks;
using IdentityModel.Client;
using OnlineStore.WebAPI.Clients.Contracts;

namespace OnlineStore.WebAPI.UnitTests.Mocks.PaymentGateway
{
    public class MockedRothschildHouseIdentityClient : IRothschildHouseIdentityClient
    {
#pragma warning disable CS1998
        public async Task<TokenResponse> GetRothschildHouseTokenAsync()
            => new RothschildHouseClientMockedTokenResponse();
#pragma warning restore CS1998
    }
}

Code for MockedRothschildHousePaymentClient class:

using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.Client;
using Newtonsoft.Json;
using OnlineStore.WebAPI.Clients.Contracts;
using OnlineStore.WebAPI.Clients.Models;

namespace OnlineStore.WebAPI.UnitTests.Mocks.PaymentGateway
{
#pragma warning disable CS1998
    public class MockedRothschildHousePaymentClient : IRothschildHousePaymentClient
    {
        public async Task<HttpResponseMessage> PostPaymentAsync(TokenResponse token, PostPaymentRequest request)
            => new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(JsonConvert.SerializeObject(PaymentResponseMocks.SuccessPayment))
            };
    }
#pragma warning restore CS1998
}

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.Core.BusinessLayer.Responses;
using OnlineStore.Core.DataLayer.Sales;
using OnlineStore.Core.EntityLayer.Sales;
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 controller = ControllerMocker.GetSalesController(nameof(TestGetOrdersAsync));

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

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

        [Fact]
        public async Task TestGetOrdersByCurrencyAsync()
        {
            // Arrange
            var controller = ControllerMocker.GetSalesController(nameof(TestGetOrdersByCurrencyAsync));
            var currencyID = "USD";

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

            // 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 controller = ControllerMocker.GetSalesController(nameof(TestGetOrdersByCustomerAsync));
            var customerID = 1;

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

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

        [Fact]
        public async Task TestGetOrdersByEmployeeAsync()
        {
            // Arrange
            var controller = ControllerMocker.GetSalesController(nameof(TestGetOrdersByEmployeeAsync));
            var employeeID = 1;

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

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

        [Fact]
        public async Task TestGetOrderAsync()
        {
            // Arrange
            var controller = ControllerMocker.GetSalesController(nameof(TestGetOrderAsync));
            var id = 1;

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

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

        [Fact]
        public async Task TestGetNonExistingOrderAsync()
        {
            // Arrange
            var controller = ControllerMocker.GetSalesController(nameof(TestGetNonExistingOrderAsync));
            var id = 0;

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

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

        [Fact]
        public async Task TestGetCreateOrderRequestAsync()
        {
            // Arrange
            var controller = ControllerMocker.GetSalesController(nameof(TestGetCreateOrderRequestAsync));

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

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

        [Fact]
        public async Task TestPostOrderAsync()
        {
            // Arrange
            var controller = ControllerMocker.GetSalesController(nameof(TestPostOrderAsync));
            var request = new PostOrderRequest
            {
                CustomerID = 1,
                PaymentMethodID = new Guid("7671A4F7-A735-4CB7-AAB4-CF47AE20171D"),
                CurrencyID = "USD",
                Comments = "Order from unit tests",
                Details = new List<OrderDetailRequest>
                {
                    new OrderDetailRequest
                    {
                        ProductID = 1,
                        Quantity = 1
                    }
                }
            };

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

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

        [Fact]
        public async Task TestCloneOrderAsync()
        {
            // Arrange
            var controller = ControllerMocker.GetSalesController(nameof(TestCloneOrderAsync));
            var id = 1;

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

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

IdentityMocker class provides user identities, GetUserInfo extension method returns an implementation of IUserInfo interface that contains information for authenticated user.

Chapter 10 - Integration Tests for Web API

As We did with unit tests, integration tests should be created according to Web API, so let's choose SalesController to explain the model for integration tests, these are the methods in controller:

  • GetOrdersAsync
  • GetOrderAsync
  • GetCreateOrderRequestAsync
  • PostOrderAsync
  • CloneOrderAsync
  • DeleteOrderAsync

Keeping this list in mind, We add the following methods to performing integration tests for SalesController:

Verb Url Http Statuses can be returned Description
    OK Not Found Bad Request Unauthorized Forbidden Not Found Internal Server Error  
GET /api/v1/Sales/Order Yes     Yes Yes   Yes Retrieves orders
GET /api/v1/Sales/Order?currencyID={0} Yes     Yes Yes   Yes Retrieves orders by currency
GET /api/v1/Sales/Order?customerID={0} Yes     Yes Yes   Yes Retrieves orders by customer
GET /api/v1/Sales/Order?employeeID={0} Yes     Yes Yes   Yes Retrieves orders by employee
GET /api/v1/Sales/Order/{0} Yes Yes   Yes Yes Yes Yes Retrieves order by id
GET /api/v1/Sales/CreateOrderRequest Yes     Yes Yes   Yes Retrieves model to create new order
POST /api/v1/Sales/Order Yes   Yes Yes Yes   Yes Creates a new order
GET /api/v1/Sales/CloneOrder/{0} Yes Yes   Yes Yes Yes Yes Clones an existing order

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

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

        [Fact]
        public async Task TestGetOrdersAsCustomerAsync()
        {
            // Arrange
            var customerToken = await TokenHelper.GetOnlineStoreCustomerTokenForWolverineAsync();
            var request = new
            {
                Url = "/api/v1/Sales/Order"
            };

            // Act
            apiClient.SetBearerToken(customerToken.AccessToken);

            var response = await apiClient.GetAsync(request.Url);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrdersByCurrencyAsCustomerAsync()
        {
            // Arrange
            var customerToken = await TokenHelper.GetOnlineStoreCustomerTokenForWolverineAsync();
            var request = new
            {
                Url = string.Format("/api/v1/Sales/Order?currencyID={0}", 1)
            };

            // Act
            apiClient.SetBearerToken(customerToken.AccessToken);

            var response = await apiClient.GetAsync(request.Url);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrdersByCustomerAsCustomerAsync()
        {
            // Arrange
            var customerToken = await TokenHelper.GetOnlineStoreCustomerTokenForWolverineAsync();
            var request = new
            {
                Url = string.Format("/api/v1/Sales/Order?customerID={0}", 1)
            };

            // Act
            apiClient.SetBearerToken(customerToken.AccessToken);

            var response = await apiClient.GetAsync(request.Url);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrdersByEmployeeAsCustomerAsync()
        {
            // Arrange
            var customerToken = await TokenHelper.GetOnlineStoreCustomerTokenForWolverineAsync();
            var request = new
            {
                Url = string.Format("/api/v1/Sales/Order?employeeID={0}", 1)
            };

            // Act
            apiClient.SetBearerToken(customerToken.AccessToken);

            var response = await apiClient.GetAsync(request.Url);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrderByIdAsCustomerAsync()
        {
            // Arrange
            var customerToken = await TokenHelper.GetOnlineStoreCustomerTokenForWolverineAsync();
            var request = new
            {
                Url = string.Format("/api/v1/Sales/Order/{0}", 1)
            };

            // Act
            apiClient.SetBearerToken(customerToken.AccessToken);

            var response = await apiClient.GetAsync(request.Url);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestGetOrderByNonExistingIdAsCustomerAsync()
        {
            // Arrange
            var customerToken = await TokenHelper.GetOnlineStoreCustomerTokenForWolverineAsync();
            var request = new
            {
                Url = string.Format("/api/v1/Sales/Order/{0}", 0)
            };

            // Act
            apiClient.SetBearerToken(customerToken.AccessToken);

            var response = await apiClient.GetAsync(request.Url);

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

        [Fact]
        public async Task TestGetCreateOrderRequestAsCustomerAsync()
        {
            // Arrange
            var customerToken = await TokenHelper.GetOnlineStoreCustomerTokenForWolverineAsync();
            var request = new
            {
                Url = "/api/v1/Sales/CreateOrderRequest"
            };

            // Act
            apiClient.SetBearerToken(customerToken.AccessToken);

            var response = await apiClient.GetAsync(request.Url);

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestPostOrderAsCustomerAsync()
        {
            // Arrange
            var request = new
            {
                Url = "/api/v1/Sales/Order",
                Body = new
                {
                    UserName = "jameslogan@walla.com",
                    Password = "wolverine",
                    CardHolderName = "James Logan",
                    IssuingNetwork = "Visa",
                    CardNumber = "4024007164051145",
                    ExpirationDate = new DateTime(DateTime.Now.Year + 5, DateTime.Now.Month, 1),
                    Cvv = "987",
                    Total = 29.99m,
                    CustomerID = 1,
                    CurrencyID = "USD",
                    PaymentMethodID = new Guid("7671A4F7-A735-4CB7-AAB4-CF47AE20171D"),
                    Comments = "Order from integration tests",
                    Details = new[]
                    {
                        new
                        {
                            ProductID = 1,
                            Quantity = 1
                        }
                    }
                }
            };

            var customerToken = await TokenHelper.GetOnlineStoreCustomerTokenAsync(request.Body.UserName, request.Body.Password);

            // Act
            apiClient.SetBearerToken(customerToken.AccessToken);

            var response = await apiClient.PostAsync(request.Url, ContentHelper.GetStringContent(request.Body));

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task TestCloneOrderAsCustomerAsync()
        {
            // Arrange
            var customerToken = await TokenHelper.GetOnlineStoreCustomerTokenForWolverineAsync();
            var request = new
            {
                Url = string.Format("/api/v1/Sales/CloneOrder/{0}", 1)
            };

            // Act
            apiClient.SetBearerToken(customerToken.AccessToken);

            var response = await apiClient.GetAsync(request.Url);

            // Assert
            response.EnsureSuccessStatusCode();
        }
    }
}

IdentityServerHelper class provides static methods to retrieve valid tokens.

SetBearerToken is an extension method that allows to set an authorization header with a bearer token.

As We can see those methods perform tests for Urls in Web API project, please note that all tests are async methods.

Don't forget We can have more tests, We have a class with name WarehouseTests to perform requests for WarehouseController.
To run integration tests, We need to run payment gateway and Identity Server for OnlineStore 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 OnLineStore.WebAPI.IntegrationTests.Helpers
{
    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:57000");
            Client.DefaultRequestHeaders.Accept.Clear();
            Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        }

        public HttpClient Client { get; }

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

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

            var manager = new ApplicationPartManager
            {
                ApplicationParts =
                {
                    new AssemblyPart(startupAssembly)
                },
                FeatureProviders =
                {
                    new ControllerFeatureProvider(),
                    new ViewComponentFeatureProvider()
                }
            };

            services.AddSingleton(manager);
        }
    }
}

Code Improvements

  1. Save logs to text file
  2. Implement Money Pattern to represent money in application
  3. Add a section to explain why this solution doesn't implement Repository and Unit of Work

Related Links

Points of Interest

  1. In this article, We're working with Entity Framework Core.
  2. Entity Framework Core has in-memory database.
  3. Extension methods for OnLineStoreDbContext class allow to us expose specific operations, in some cases We don't want to have GetAll, Add, Update or Remove operations.
  4. Help page for Web API has been built with Swagger.
  5. Unit tests perform testing for Assemblies.
  6. Integration tests perform testing for Web Server.
  7. Unit and integration tests have been built with xUnit framework.
  8. Mocker is an object that creates an instance of object in testing.

History

  • 12th December, 2016: Initial version
  • 13th December, 2016: Addition of Business Layer
  • 15th December, 2016: Addition of Mocker
  • 31th December, 2016: Addition of Web API
  • 5th January, 2017: Addition of Unit Tests for Web API
  • 22th January, 2017: Addition of Change Log
  • 4th February, 2017: Addition of Async Operations
  • 15th May, 2017: Addition of Logs
  • 29th October, 2017: Code Refactor, using of Service in Business Layer
  • 10th February, 2018: Addition of exclusions for Change Log
  • 28th May, 2018: Addition of Integration Tests for Web API
  • 2nd October, 2018: Addition of in memory database for Unit Tests
  • 25th November, 2018: Addition of Help Page for Web API
  • 27th November, 2018: Addition of Related Links section
  • 16th January, 2019: Addition of Identity Server
  • 23th January, 2019: Addition of Payment Gateway
  • 10th February, 2019: Addition of Clients for Payment Gateway

License

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

Share

About the Author

HHerzl
Software Developer
El Salvador El Salvador
CatFactory Creator.

Full Stack Developer with Experience in C#, Entity Framework Core, ASP.NET Core and Angular.

You may also be interested in...

Pro

Comments and Discussions

 
GeneralMy vote of 5 Pin
sudhanshub11-Feb-19 9:14
membersudhanshub11-Feb-19 9:14 
GeneralRe: My vote of 5 Pin
HHerzl17-Feb-19 11:20
memberHHerzl17-Feb-19 11:20 
PraiseVery nicely stated Pin
Debashis 1043365619-Jan-19 5:04
memberDebashis 1043365619-Jan-19 5:04 
GeneralRe: Very nicely stated Pin
HHerzl19-Jan-19 21:18
memberHHerzl19-Jan-19 21:18 
QuestionEntity Framework Core 2 for Enterprise Pin
Member 1412084816-Jan-19 22:49
memberMember 1412084816-Jan-19 22:49 
AnswerRe: Entity Framework Core 2 for Enterprise Pin
HHerzl19-Jan-19 21:18
memberHHerzl19-Jan-19 21:18 
GeneralMessage Closed Pin
16-Jan-19 21:51
memberMember 1412077716-Jan-19 21:51 
QuestionIEntity Pin
DevForRent26-Nov-18 4:04
memberDevForRent26-Nov-18 4:04 
AnswerRe: IEntity Pin
HHerzl2-Dec-18 20:06
memberHHerzl2-Dec-18 20:06 
QuestionYesonlinePK Pin
Member 1406663125-Nov-18 10:23
memberMember 1406663125-Nov-18 10:23 
AnswerRe: YesonlinePK Pin
HHerzl25-Nov-18 11:29
memberHHerzl25-Nov-18 11:29 
QuestionI don't agree with ".....STAY AWAY FROM STORED PROCEDURES......" Pin
Arkady Geltzer10-Oct-18 10:45
memberArkady Geltzer10-Oct-18 10:45 
AnswerRe: I don't agree with ".....STAY AWAY FROM STORED PROCEDURES......" Pin
HHerzl12-Oct-18 21:57
memberHHerzl12-Oct-18 21:57 
GeneralRe: I don't agree with ".....STAY AWAY FROM STORED PROCEDURES......" Pin
Juba18-Jan-19 17:52
memberJuba18-Jan-19 17:52 
GeneralRe: I don't agree with ".....STAY AWAY FROM STORED PROCEDURES......" Pin
HHerzl19-Jan-19 21:22
memberHHerzl19-Jan-19 21:22 
GeneralMy vote of 5 Pin
dkurok9-Oct-18 1:10
memberdkurok9-Oct-18 1:10 
GeneralRe: My vote of 5 Pin
HHerzl12-Oct-18 14:29
memberHHerzl12-Oct-18 14:29 
QuestionIs this a code-first or database-first entity framework approach? Pin
Member 1057997130-May-18 17:48
professionalMember 1057997130-May-18 17:48 
AnswerRe: Is this a code-first or database-first entity framework approach? Pin
HHerzl30-May-18 23:55
memberHHerzl30-May-18 23:55 
GeneralRe: Is this a code-first or database-first entity framework approach? Pin
Member 1057997131-May-18 5:37
professionalMember 1057997131-May-18 5:37 
GeneralRe: Is this a code-first or database-first entity framework approach? Pin
HHerzl31-May-18 14:27
memberHHerzl31-May-18 14:27 
GeneralRe: Is this a code-first or database-first entity framework approach? Pin
Member 105799714-Jun-18 7:36
professionalMember 105799714-Jun-18 7:36 
GeneralRe: Is this a code-first or database-first entity framework approach? Pin
HHerzl17-Jun-18 17:40
memberHHerzl17-Jun-18 17:40 
GeneralRe: Is this a code-first or database-first entity framework approach? Pin
Member 1057997117-Jun-18 18:15
professionalMember 1057997117-Jun-18 18:15 
GeneralRe: Is this a code-first or database-first entity framework approach? Pin
HHerzl17-Jun-18 18:48
memberHHerzl17-Jun-18 18:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web03 | 2.8.190214.1 | Last Updated 10 Feb 2019
Article Copyright 2016 by HHerzl
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid