본문 바로가기

.NET/ASP.NET

엔터 프라이즈 엔터 프라이즈 프레임 워크 코어

출처: https://www.codeproject.com/Articles/1160586/Entity-Framework-Core-for-Enterprise


소개

엔터프라이즈 애플리케이션을위한 설계는 소프트웨어 개발에서 공통적 인 문제이며 우리가 직무에 대해 선택한 기술에 따라 최선의 방법으로이 문제를 해결할 수있는 방법입니다.

이 가이드에서는 엔터프라이즈 설계자의 공통 요구 사항을 살펴 보겠습니다.

이 가이드에서는 EntityFramework를 사용 하겠지만이 개념은 Dapper 또는 다른 ORM에 적용됩니다.

배경

엔터프라이즈 응용 프로그램의 아키텍처에는 최소한 다음 수준이 있어야합니다.

  1. 엔티티 계층 : 엔티티 (POCO)
  2. 데이터 영역 : 데이터베이스 액세스와 관련된 모든 코드 포함
  3. 비즈니스 계층 : 비즈니스 와 관련된 정의 및 유효성 검사 포함
  4. 외부 서비스 계층 (선택 사항) : 외부 서비스 (ASMX, WCF, RESTful)에 대한 호출을 포함합니다.
  5. Common : 모든 레이어 (예 : 로거, 맵퍼, 확장 프로그램)의 공통 클래스 및 인터페이스를 포함합니다.
  6. 테스트 (QA) : 백엔드 코드에 대한 자동화 된 테스트 포함
  7. 프레젠테이션 레이어 : UI입니다.
  8. UI 테스트 (QA) : 프런트 엔드 코드에 대한 자동화 된 테스트 포함

기술 전제 조건

  • OOP (객체 지향 프로그래밍)
  • AOP (Aspect Oriented Programming)
  • ORM (객체 관계형 매핑)
  • 디자인 패턴 : 도메인 기반 디자인, 저장소 및 작업 단위 및 IoC

소프트웨어 필수 구성 요소

  • 마지막 업데이트가 포함 된 Visual Studio 2015 또는 2017
  • SQL Server 인스턴스 (로컬 또는 원격)

목차

  1. 코드 사용
  2. 코드 개선
  3. 가볼만한 곳
  4. 관련된 링크들

코드 사용

1 단계 - 데이터베이스 생성

이 가이드에서는 샘플 데이터베이스를 사용하여 아키텍처의 각 구성 요소를 이해합니다. 이것은 데이터베이스를위한 스크립트입니다 :

use master
go

drop database Store
go

create database Store
go

use Store
go

/* Definition for schemas */
create schema HumanResources
go

create schema Production
go

create schema Sales
go

/* Definition for tables */
create table [EventLog]
(
	[EventLogID] uniqueidentifier not null,
	[EventType] int not null,
	[Key] varchar(255) not null,
	[Message] varchar(max) not null,
	[EntryDate] datetime not null
)

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

create table [ChangeLogExclusion]
(
	[ChangeLogExclusionID] int not null identity(1, 1),
	[EntityName] varchar(128) not null,
	[PropertyName] varchar(128) not null
)

create table [dbo].[Country]
(
	[CountryID] int not null,
	[CountryName] varchar(100) not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

create table [dbo].[Currency]
(
	[CurrencyID] smallint not null identity(1000, 1000),
	[CurrencyName] varchar(50) not null,
	[CurrencySymbol] varchar(1) not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

create table [dbo].[CountryCurrency]
(
	[CountryCurrencyID] int not null identity(1, 1),
	[CountryID] int not null,
	[CurrencyID] smallint not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

create table [HumanResources].[Employee]
(
	[EmployeeID] int not null identity(1, 1),
	[FirstName] varchar(25) not null,
	[MiddleName] varchar(25) null,
	[LastName] varchar(25) not null,
	[BirthDate] datetime not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

create table [Production].[ProductCategory]
(
	[ProductCategoryID] int not null identity(1, 1),
	[ProductCategoryName] varchar(100) not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

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

create table [Production].[Warehouse]
(
	[WarehouseID] varchar(5) not null,
	[WarehouseName] varchar(100) not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

create table [Production].[ProductInventory]
(
	[ProductInventoryID] int not null identity(1, 1),
	[ProductID] int not null,
	[WarehouseID] varchar(5) not null,
	[Quantity] int not null,
	[Stocks] int not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

create table [Sales].[OrderStatus]
(
	[OrderStatusID] smallint not null,
	[Description] varchar(100) not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

create table [Sales].[PaymentMethod]
(
	[PaymentMethodID] uniqueidentifier not null,
	[PaymentMethodName] varchar(50) not null,
	[PaymentMethodDescription] varchar(255) not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

create table [Sales].[Customer]
(
	[CustomerID] int not null identity(1, 1),
	[CompanyName] varchar(100) null,
	[ContactName] varchar(100) null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

create table [Sales].[Shipper]
(
	[ShipperID] int not null identity(1, 1),
	[CompanyName] varchar(100) null,
	[ContactName] varchar(100) null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)

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

create table [Sales].[OrderDetail]
(
	[OrderDetailID] bigint not null identity(1, 1),
	[OrderID] bigint not null,
	[ProductID] int not null,
	[ProductName] varchar(255) not null,
	[UnitPrice] decimal(8, 4) not null,
	[Quantity] int not null,
	[Total] decimal(8, 4) not null,
	[CreationUser] varchar(25) not null,
	[CreationDateTime] datetime not null,
	[LastUpdateUser] varchar(25) null,
	[LastUpdateDateTime] datetime null,
	[Timestamp] rowversion null
)
go

/* Definition for constraints (Primary key and unique) */
alter table [EventLog]
	add constraint [PK_EventLog] primary key (EventLogID)
go

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

alter table [ChangeLogExclusion]
	add constraint [PK_ChangeLogExclusion] primary key(ChangeLogExclusionID)

alter table [dbo].[Country]
	add constraint [PK_Country] primary key([CountryID])
go

alter table [dbo].[Currency]
	add constraint [PK_Currency] primary key([CurrencyID])
go

alter table [dbo].[CountryCurrency]
	add constraint [PK_CountryCurrency] primary key([CountryCurrencyID])
go

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

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

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

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

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

alter table [Production].[Warehouse]
	add constraint [PK_Production_Warehouse] primary key (WarehouseID)
go

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

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

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

alter table [Sales].[PaymentMethod]
	add constraint [PK_Sales_PaymentMethod] primary key (PaymentMethodID)
go

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

alter table [Sales].[OrderDetail]
	add constraint [PK_Sales_OrderDetail] primary key (OrderDetailID)
go

alter table [dbo].[CountryCurrency]
	add constraint [U_CountryCurrency] unique (CountryID, CurrencyID)
go

alter table [Sales].[OrderDetail]
	add constraint [U_Sales_OrderDetail] unique (OrderID, ProductID)
go

alter table [ChangeLogExclusion]
	add constraint [U_ChangeLogExclusion] unique(EntityName, PropertyName)
go

/* Definition for Relations */
alter table [dbo].[CountryCurrency]
	add constraint [FK_CountryCurrency_Country] foreign key (CountryID)
		references [dbo].[Country]
go

alter table [dbo].[CountryCurrency]
	add constraint [FK_CountryCurrency_Currency] foreign key (CurrencyID)
		references [dbo].[Currency]
go

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

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

alter table [Production].[ProductInventory]
	add constraint [FK_Production_ProductInventory_Warehouse] foreign key (WarehouseID)
		references [Production].[Warehouse]
go

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

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

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

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

alter table [Sales].[Order]
	add constraint [FK_Sales_Order_Currency] foreign key (CurrencyID)
		references [dbo].[Currency]
go

alter table [Sales].[Order]
	add constraint [FK_Sales_Order_PaymentMethod] foreign key (PaymentMethodID)
		references [Sales].[PaymentMethod]
go

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

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

SQL Server 인스턴스에서 스크립트를 실행하면 다음과 같은 데이터베이스 다이어그램을 생성 할 수 있습니다.

Dbo 스키마

Dbo 스키마

인적 자원 스키마

HumanResources 스키마

생산 스키마

생산 스키마

영업 스키마

영업 스키마

이것은 개념의 데모를위한 간단한 데이터베이스입니다.

02 단계 - 핵심 프로젝트

데이터베이스를 추가하고 코드를 추가하기 전에 다음과 같이 코드에 대한 명명 규칙을 정의해야합니다.

식별자케이스
네임 스페이스파스칼 케이스AdventureWorks
수업 이름파스칼 케이스ProductViewModel
인터페이스 이름접두사 + PascalCaseIDatabaseValidator
메서드 이름파스칼 케이스GetOrders
속성 이름파스칼 케이스기술
매개 변수 이름낙타 사례connectionString

이 협약은 아키텍처에 대한 명명 지침을 정의하기 때문에 매우 중요합니다.

Store.CoreDotNet Core의 이름으로 프로젝트를 생성하고 폴더를 추가하십시오 :

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

Entitylayer이 엔티티 안에는 모든 엔티티를 배치 할 것입니다. 엔티티는 데이터베이스의 테이블이나 뷰를 나타내는 클래스를 의미하며, 메소드가 아닌 속성 만 가진 클래스를 의미하는 POCO (Plain Old Common language runtime Object)라고도합니다. 다른 것들 (사건); wkempf 피드백 에 따르면 POCO에 대해 명확해야 할 필요가 있지만 POCO 에는 메서드 및 이벤트와 다른 멤버가있을 수 있지만 이러한 멤버를 POCO에 추가하는 것은 일반적이지 않습니다.

내부에 DataLayer, 우리는 DbContext그것이 공통 클래스이기 때문에 놓을 것 입니다 DataLayer.

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

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

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

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

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

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

For BusinessLayer\Responses, we'll create the responses: single, list and paged to represent the result from services.

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

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

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

Architecture: Big Picture

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

Entity Layer

Order class code:

using System;
using System.Collections.ObjectModel;
using Store.Core.EntityLayer.Dbo;
using Store.Core.EntityLayer.HumanResources;

namespace Store.Core.EntityLayer.Sales
{
    public class Order : IAuditEntity
    {
        public Order()
        {
        }

        public Order(Int64 orderID)
        {
            OrderID = orderID;
        }

        public Int64? OrderID { get; set; }

        public Int16? OrderStatusID { get; set; }

        public DateTime? OrderDate { get; set; }

        public Int32? CustomerID { get; set; }

        public Int32? EmployeeID { get; set; }

        public Int32? ShipperID { get; set; }

        public Decimal? Total { get; set; }

        public Int16? CurrencyID { get; set; }

        public Guid? PaymentMethodID { get; set; }

        public String Comments { get; set; }

        public String CreationUser { get; set; }

        public DateTime? CreationDateTime { get; set; }

        public String LastUpdateUser { get; set; }

        public DateTime? LastUpdateDateTime { get; set; }

        public Byte[] Timestamp { get; set; }

        public virtual OrderStatus OrderStatusFk { get; set; }

        public virtual Customer CustomerFk { get; set; }

        public virtual Employee EmployeeFk { get; set; }

        public virtual Shipper ShipperFk { get; set; }

        public virtual Currency CurrencyFk { get; set; }

        public virtual PaymentMethod PaymentMethodFk { get; set; }

        public virtual Collection<OrderDetail> OrderDetails { get; set; } = new Collection<OrderDetail>();
    }
}

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

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

Data Layer

For this source code, the implementation for repositories are by feature instead of generic repositories; the generic repositories require to create derived repositories in case we need to implement specific operations. I prefer repositories by features because do not require to create derived objects (interfaces and classes) but a repository by feature will contains a lot of operations because is a placeholder for all operations in feature.

The sample database for this article contains 4 schemas in database, so we'll have 4 repositories, this implementation provides a separation of concepts.

We are working with EF Core in this guide, so we need to have a DbContext and objects that allow mapping classes and database objects (tables and views).

Repository versus DbHelper versus Data Access Object

This issue is related to naming objects, some years ago I used DataAccessObject as suffix to class that contain database operatios (select, insert, update, delete, etc). Other developers used DbHelper as suffix to represent this kind of objects, at my beggining in EF I learned about repository design pattern, so from my point of view I prefer to use Repository suffix to name the object that contains database operations.

StoreDbContext class code:

using Microsoft.EntityFrameworkCore;
using Store.Core.DataLayer.Mapping;

namespace Store.Core.DataLayer
{
    public class StoreDbContext : DbContext
    {
        public StoreDbContext(DbContextOptions<StoreDbContext> options, IEntityMapper entityMapper)
            : base(options)
        {
            EntityMapper = entityMapper;
        }

        public IEntityMapper EntityMapper { get; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Load all mappings for entities
            EntityMapper.MapEntities(modelBuilder);

            base.OnModelCreating(modelBuilder);
        }
    }
}

OrderMap class code:

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

namespace Store.Core.DataLayer.Mapping.Sales
{
    [Export(typeof(IEntityMap))]
    public class OrderMap : IEntityMap
    {
        public void Map(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Order>(entity =>
            {
                // Mapping for table
                entity.ToTable("Order", "Sales");

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

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

                // Set mapping for columns
                entity.Property(p => p.OrderStatusID).HasColumnType("smallint").IsRequired();
                entity.Property(p => p.OrderDate).HasColumnType("datetime").IsRequired();
                entity.Property(p => p.CustomerID).HasColumnType("int").IsRequired();
                entity.Property(p => p.EmployeeID).HasColumnType("int");
                entity.Property(p => p.ShipperID).HasColumnType("int");
                entity.Property(p => p.Total).HasColumnType("decimal(12, 4)").IsRequired();
                entity.Property(p => p.Comments).HasColumnType("varchar(255)");
                entity.Property(p => p.CreationUser).HasColumnType("varchar(25)").IsRequired();
                entity.Property(p => p.CreationDateTime).HasColumnType("datetime").IsRequired();
                entity.Property(p => p.LastUpdateUser).HasColumnType("varchar(25)");
                entity.Property(p => p.LastUpdateDateTime).HasColumnType("datetime");

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

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

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

                entity
                    .HasOne(p => p.ShipperFk)
                    .WithMany(b => b.Orders)
                    .HasForeignKey(p => p.ShipperID);
            });
        }
    }
}

Repository 클래스 코드 :

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

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

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

        protected virtual void Add<TEntity>(TEntity entity) where TEntity : class, IEntity
        {
            var cast = entity as IAuditEntity;

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

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

            DbContext.Set<TEntity>().Add(entity);
        }

        protected virtual void Update<TEntity>(TEntity entity) where TEntity : class, IEntity
        {
            var cast = entity as IAuditEntity;

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

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

        protected virtual void Remove<TEntity>(TEntity entity) where TEntity : class, IEntity
            => DbContext.Set<TEntity>().Remove(entity);

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

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

                        if (String.Concat(originalValue) != String.Concat(currentValue))
                        {
                            // todo: improve the way to retrieve primary key value from entity instance
                            var key = entry.Entity.GetType().GetProperties()[0].GetValue(entry.Entity, null).ToString();

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

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

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

            return DbContext.SaveChanges();
        }

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

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

            return DbContext.SaveChangesAsync();
        }
    }
}

SalesRepository 클래스 코드 :

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Store.Core.DataLayer.Contracts;
using Store.Core.DataLayer.DataContracts;
using Store.Core.EntityLayer.Dbo;
using Store.Core.EntityLayer.HumanResources;
using Store.Core.EntityLayer.Sales;

namespace Store.Core.DataLayer.Repositories
{
    public class SalesRepository : Repository, ISalesRepository
    {
        public SalesRepository(IUserInfo userInfo, StoreDbContext dbContext)
            : base(userInfo, dbContext)
        {
        }

        public IQueryable<Customer> GetCustomers()
            => DbContext.Set<Customer>();

        public async Task<Customer> GetCustomerAsync(Customer entity)
            => await DbContext.Set<Customer>().FirstOrDefaultAsync(item => item.CustomerID == entity.CustomerID);

        public async Task<Int32> AddCustomerAsync(Customer entity)
        {
            Add(entity);

            return await CommitChangesAsync();
        }

        public async Task<Int32> UpdateCustomerAsync(Customer changes)
        {
            Update(changes);

            return await CommitChangesAsync();
        }

        public async Task<Int32> DeleteCustomerAsync(Customer entity)
        {
            Remove(entity);

            return await CommitChangesAsync();
        }

        public IQueryable<OrderInfo> GetOrders(Int16? currencyID = null, Int32? customerID = null, Int32? employeeID = null, Int16? orderStatusID = null, Guid? paymentMethodID = null, Int32? shipperID = null)
        {
            var query =
                from order in DbContext.Set<Order>()
                join currencyJoin in DbContext.Set<Currency>() on order.CurrencyID equals currencyJoin.CurrencyID into currencyTemp
                from currency in currencyTemp.Where(relation => relation.CurrencyID == order.CurrencyID).DefaultIfEmpty()
                join customer in DbContext.Set<Customer>() on order.CustomerID equals customer.CustomerID
                join employeeJoin in DbContext.Set<Employee>() on order.EmployeeID equals employeeJoin.EmployeeID into employeeTemp
                from employee in employeeTemp.Where(relation => relation.EmployeeID == order.EmployeeID).DefaultIfEmpty()
                join orderStatus in DbContext.Set<OrderStatus>() on order.OrderStatusID equals orderStatus.OrderStatusID
                join paymentMethodJoin in DbContext.Set<PaymentMethod>() on order.PaymentMethodID equals paymentMethodJoin.PaymentMethodID into paymentMethodTemp
                from paymentMethod in paymentMethodTemp.Where(relation => relation.PaymentMethodID == order.PaymentMethodID).DefaultIfEmpty()
                join shipperJoin in DbContext.Set<Shipper>() on order.ShipperID equals shipperJoin.ShipperID into shipperTemp
                from shipper in shipperTemp.Where(relation => relation.ShipperID == order.ShipperID).DefaultIfEmpty()
                select new OrderInfo
                {
                    OrderID = order.OrderID,
                    OrderStatusID = order.OrderStatusID,
                    CustomerID = order.CustomerID,
                    EmployeeID = order.EmployeeID,
                    ShipperID = order.ShipperID,
                    OrderDate = order.OrderDate,
                    Total = order.Total,
                    CurrencyID = order.CurrencyID,
                    PaymentMethodID = order.PaymentMethodID,
                    Comments = order.Comments,
                    CreationUser = order.CreationUser,
                    CreationDateTime = order.CreationDateTime,
                    LastUpdateUser = order.LastUpdateUser,
                    LastUpdateDateTime = order.LastUpdateDateTime,
                    Timestamp = order.Timestamp,
                    CurrencyCurrencyName = currency == null ? String.Empty : currency.CurrencyName,
                    CurrencyCurrencySymbol = currency == null ? String.Empty : currency.CurrencySymbol,
                    CustomerCompanyName = customer == null ? String.Empty : customer.CompanyName,
                    CustomerContactName = customer == null ? String.Empty : customer.ContactName,
                    EmployeeFirstName = employee.FirstName,
                    EmployeeMiddleName = employee == null ? String.Empty : employee.MiddleName,
                    EmployeeLastName = employee.LastName,
                    EmployeeBirthDate = employee.BirthDate,
                    OrderStatusDescription = orderStatus.Description,
                    PaymentMethodPaymentMethodName = paymentMethod == null ? String.Empty : paymentMethod.PaymentMethodName,
                    PaymentMethodPaymentMethodDescription = paymentMethod == null ? String.Empty : paymentMethod.PaymentMethodDescription,
                    ShipperCompanyName = shipper == null ? String.Empty : shipper.CompanyName,
                    ShipperContactName = shipper == null ? String.Empty : shipper.ContactName,
                };

            if (currencyID.HasValue)
            {
                query = query.Where(item => item.CurrencyID == currencyID);
            }

            if (customerID.HasValue)
            {
                query = query.Where(item => item.CustomerID == customerID);
            }

            if (employeeID.HasValue)
            {
                query = query.Where(item => item.EmployeeID == employeeID);
            }

            if (orderStatusID.HasValue)
            {
                query = query.Where(item => item.OrderStatusID == orderStatusID);
            }

            if (paymentMethodID.HasValue)
            {
                query = query.Where(item => item.PaymentMethodID == paymentMethodID);
            }

            if (shipperID.HasValue)
            {
                query = query.Where(item => item.ShipperID == shipperID);
            }

            return query;
        }

        public async Task<Order> GetOrderAsync(Order entity)
            => await DbContext.Set<Order>().Include(p => p.OrderDetails).FirstOrDefaultAsync(item => item.OrderID == entity.OrderID);

        public Task<Int32> AddOrderAsync(Order entity)
        {
            Add(entity);

            return CommitChangesAsync();
        }

        public async Task<Int32> UpdateOrderAsync(Order changes)
        {
            Update(changes);

            return await CommitChangesAsync();
        }

        public async Task<Int32> DeleteOrderAsync(Order entity)
        {
            Remove(entity);

            return await CommitChangesAsync();
        }

        public async Task<OrderDetail> GetOrderDetailAsync(OrderDetail entity)
            => await DbContext.Set<OrderDetail>().FirstOrDefaultAsync(item => item.OrderID == entity.OrderID && item.ProductID == entity.ProductID);

        public Task<Int32> AddOrderDetailAsync(OrderDetail entity)
        {
            Add(entity);

            return CommitChangesAsync();
        }

        public async Task<Int32> UpdateOrderDetailAsync(OrderDetail changes)
        {
            Update(changes);

            return await CommitChangesAsync();
        }

        public async Task<Int32> DeleteOrderDetailAsync(OrderDetail entity)
        {
            Remove(entity);

            return await CommitChangesAsync();
        }

        public IQueryable<Shipper> GetShippers()
            => DbContext.Set<Shipper>();

        public async Task<Shipper> GetShipperAsync(Shipper entity)
            => await DbContext.Set<Shipper>().FirstOrDefaultAsync(item => item.ShipperID == entity.ShipperID);

        public async Task<Int32> AddShipperAsync(Shipper entity)
        {
            Add(entity);

            return await CommitChangesAsync();
        }

        public async Task<Int32> UpdateShipperAsync(Shipper changes)
        {
            Update(changes);

            return await CommitChangesAsync();
        }

        public async Task<Int32> DeleteShipperAsync(Shipper entity)
        {
            Remove(entity);

            return await CommitChangesAsync();
        }

        public IQueryable<OrderStatus> GetOrderStatus()
            => DbContext.Set<OrderStatus>();

        public async Task<OrderStatus> GetOrderStatusAsync(OrderStatus entity)
            => await DbContext.Set<OrderStatus>().FirstOrDefaultAsync(item => item.OrderStatusID == entity.OrderStatusID);

        public async Task<Int32> AddOrderStatusAsync(OrderStatus entity)
        {
            Add(entity);

            return await CommitChangesAsync();
        }

        public async Task<Int32> UpdateOrderStatusAsync(OrderStatus changes)
        {
            Update(changes);

            return await CommitChangesAsync();
        }

        public async Task<Int32> RemoveOrderStatusAsync(OrderStatus entity)
        {
            Remove(entity);

            return await CommitChangesAsync();
        }

        public IQueryable<Currency> GetCurrencies()
            => DbContext.Set<Currency>();

        public IQueryable<PaymentMethod> GetPaymentMethods()
            => DbContext.Set<PaymentMethod>();
    }
}

Unit of Work는 어떻습니까? EF 6.x에서는 일반적으로 저장소 클래스와 작업 단위 클래스를 작성했습니다. 데이터베이스 액세스 및 작업 단위 (UOW) 작업을위한 저장소 제공 작업은 데이터베이스의 변경 사항을 저장하기위한 조작을 제공합니다. 그러나 EF Core에서는 단지 저장소와 작업 단위가없는 것이 일반적입니다. 어쨌든이 코드를 우리는 두 가지의 방법을 추가 한 Repository클래스 : CommitChanges와 CommitChangesAsync, 그래서 그냥 내부 저장소에서 mehotds를 작성하는 모든 데이터의 전화 있는지 확인 CommitChanges하거나 CommitChangesAsync그 디자인으로 우리는 우리의 구조 작업이 정의를 가지고있다.

에 DbContext이 버전, 우리가 사용하고있는 DbSet비행을 대신 선언 DbSet에서 속성을 DbContext나는 DbSet모든 DbSet것을 추가하는 것에 대해 걱정하지 않기 때문에 내가 사용하는 것을 선호하는 아키텍트 환경 설정에 더 가깝다고 생각합니다. DbContext하지만 선언 된 DbSet속성 을 사용하는 것이 더 정확하다고 생각한다면이 스타일은 변경 될 것 입니다 DbContext.

비동기 작업은 어떻습니까? 이 게시물의 이전 버전에서는 REST API를 사용하여 마지막 단계에서 비동기 작업을 구현할 것이라고 말했지만 .NET 코어는 비동기 프로그래밍에 대한 문제이므로 잘못된 것이므로 모든 데이터베이스 작업을 비동기 방식으로 처리하는 것이 가장 좋습니다. EF 코어가 제공하는 비동기 메소드를 사용합니다.

우리가 좀 걸릴 수 있습니다 Repository: 클래스,이 두 가지 방법이 있습니다 Add및 Update이 예를 들어, Order클래스는 감사 속성이 있습니다 CreationUser, CreationDateTime, LastUpdateUser and LastUpdateDateTime또한 Order클래스를 구현 IAuditEntity하는 인터페이스가 감사 속성에 대한 값을 설정하는 데 사용되는 인터페이스를

이 기사의 최신 버전에서는 서비스 계층을 생략하되 경우에 따라 외부 서비스 (ASMX, WCF 및 RESTful)에 대한 연결을 포함하는 계층이 있습니다.

저장 프로 시저와 LINQ 쿼리 비교

데이터 레이어에는 매우 흥미로운 점이 있습니다. 저장 프로 시저를 어떻게 사용할 수 있습니까? EF 코어의 현재 버전에서는 저장 프로 시저가 지원되지 않으므로 내부적으로는 사용할 수 없습니다 DbSet. 쿼리를 실행하는 메소드가 있지만 저장 프로 시저에서 결과를 반환하지는 않습니다. set (columns)을 사용하면 몇 가지 확장 메서드를 추가하고 기존 ADO.NET을 사용하는 패키지를 추가 할 수 있으므로이 경우 저장 프로 시저 결과를 나타내는 개체의 동적 생성을 처리해야합니다. 그게 말이 되니? 우리가 이름이있는 프로 시저를 사용 GetOrdersByMonth하고 그 프로 시저가 7 개의 열을 가진 select를 반환하면 모든 결과를 같은 방식으로 처리하기 위해 우리는 그 결과를 나타내는 객체를 정의해야하며 객체는 우리의 이름에 따라 DataLayerDataContractsnamespace 내부를 정의해야합니다 협약.

엔터프라이즈 환경에서 LINQ 쿼리 또는 저장 프로 시저에 대한 일반적인 설명이 있습니다. 내 경험에 비추어 볼 때,이 질문을 해결하는 가장 좋은 방법은 건축가 및 데이터베이스 관리자와의 검토 디자인 관례입니다. 요즘 저장 프로 시저 대신 비동기 모드에서 LINQ 쿼리를 사용하는 것이 일반적이지만 때로는 일부 회사에서 규칙을 제한하고 LINQ 쿼리를 사용할 수 없기 때문에 저장 프로 시저를 사용해야하며 아키텍처를 융통성있게 만들 필요가 있습니다. 개발자 관리자에게 "EF 코어가 저장 프로 시저를 호출 할 수 없기 때문에 비즈니스 논리가 다시 작성됩니다"라고 말하지 않습니다.

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

Just to be clear: STAY AWAY FROM STORED PROCEDURES!

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

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

ChangeLogID ClassName    PropertyName   Key  OriginalValue          CurrentValue           UserName   ChangeDate
----------- ------------ -------------- ---- ---------------------- ---------------------- ---------- -----------------------
1           Employee     FirstName      1    John                   John III               admin      2017-02-19 21:49:51.347
2           Employee     MiddleName     1                           Smith III              admin      2017-02-19 21:49:51.347
3           Employee     LastName       1    Doe                    Doe III                admin      2017-02-19 21:49:51.347
4           Employee     BirthDate      1    2/19/2017 8:01:45 PM   1/6/2017 12:00:00 AM   admin      2017-02-19 21:49:51.350

(4 row(s) affected)

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

Business Layer

Controller versus Service versus Business Object

There is a common issue in this point, How we must to name the object that represents business operations: I named this object as BusinessObject, that can be confusing for some developers, some developers do not implement business object because the controller in API represents business logic, but the most used name is Service, so from my point of view is more clear to use Service as sufix for this object. If we have an API we'll implement business logic in controller but if there is a business layer is more useful to have a service class, this class must to implement logic business.

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 CreateOrder method, we invoke methods from repositories, inside of repositories we don't have any transactions because the business object is the responsibility for transactional process, also we added logic to handle exceptions related to business with custom messages because we need to provide a friendly message to the end-user.
  4. There is a 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, shippers, customers and employees. 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 code:

using System;
using Microsoft.Extensions.Logging;
using Store.Core.BusinessLayer.Contracts;
using Store.Core.DataLayer;
using Store.Core.DataLayer.Contracts;
using Store.Core.DataLayer.Repositories;

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

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

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

                Disposed = true;
            }
        }

        protected IHumanResourcesRepository HumanResourcesRepository
            => m_humanResourcesRepository ?? (m_humanResourcesRepository = new HumanResourcesRepository(UserInfo, DbContext));

        protected IProductionRepository ProductionRepository
            => m_productionRepository ?? (m_productionRepository = new ProductionRepository(UserInfo, DbContext));

        protected ISalesRepository SalesRepository
            => m_salesRepository ?? (m_salesRepository = new SalesRepository(UserInfo, DbContext));
    }
}

SalesService class code:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Store.Core.BusinessLayer.Contracts;
using Store.Core.BusinessLayer.Requests;
using Store.Core.BusinessLayer.Responses;
using Store.Core.DataLayer;
using Store.Core.DataLayer.DataContracts;
using Store.Core.DataLayer.Repositories;
using Store.Core.EntityLayer.Dbo;
using Store.Core.EntityLayer.Production;
using Store.Core.EntityLayer.Sales;

namespace Store.Core.BusinessLayer
{
    public class SalesService : Service, ISalesService
    {
        public SalesService(ILogger logger, IUserInfo userInfo, StoreDbContext dbContext)
            : base(logger, userInfo, dbContext)
        {
        }

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

            var response = new PagedResponse<Customer>();

            try
            {
                // Get query
                var query = SalesRepository.GetCustomers();

                // Set information for paging
                response.PageSize = (Int32)pageSize;
                response.PageNumber = (Int32)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(ex, Logger);
            }

            return response;
        }

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

            var response = new PagedResponse<Shipper>();

            try
            {
                // Get query
                var query = SalesRepository.GetShippers();

                // Set information for paging
                response.PageSize = (Int32)pageSize;
                response.PageNumber = (Int32)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(ex, Logger);
            }

            return response;
        }

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

            var response = new PagedResponse<Currency>();

            try
            {
                // Get query
                var query = SalesRepository.GetCurrencies();

                // Set information for paging
                response.PageSize = (Int32)pageSize;
                response.PageNumber = (Int32)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(ex, Logger);
            }

            return response;
        }

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

            var response = new PagedResponse<PaymentMethod>();

            try
            {
                // Get query
                var query = SalesRepository.GetPaymentMethods();

                // Set information for paging
                response.PageSize = (Int32)pageSize;
                response.PageNumber = (Int32)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(ex, Logger);
            }

            return response;
        }

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

            var response = new PagedResponse<OrderInfo>();

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

                // Set information for paging
                response.PageSize = (Int32)pageSize;
                response.PageNumber = (Int32)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(ex, Logger);
            }

            return response;
        }

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

            var response = new SingleResponse<Order>();

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

            return response;
        }

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

            var response = new SingleResponse<CreateOrderRequest>();

            try
            {
                // Retrieve customers list
                response.Model.Customers = await SalesRepository.GetCustomers().ToListAsync();

                // Retrieve employees list
                response.Model.Employees = await HumanResourcesRepository.GetEmployees().ToListAsync();

                // Retrieve shippers list
                response.Model.Shippers = await SalesRepository.GetShippers().ToListAsync();

                // Retrieve products list
                response.Model.Products = await ProductionRepository.GetProducts().ToListAsync();
            }
            catch (Exception ex)
            {
                response.SetError(ex, Logger);
            }

            return response;
        }

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

            var response = new SingleResponse<Order>();

            try
            {
                // Begin transaction
                using (var transaction = await DbContext.Database.BeginTransactionAsync())
                {
                    // Retrieve warehouses
                    var warehouses = await ProductionRepository
                        .GetWarehouses()
                        .ToListAsync();

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

                            if (product == null)
                            {
                                // Throw exception if product no exists
                                throw new NonExistingProductException(String.Format(SalesDisplays.NonExistingProductExceptionMessage, detail.ProductID));
                            }
                            else
                            {
                                // Set product name from existing entity
                                detail.ProductName = product.ProductName;
                            }

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

                            // Set unit price and total for product detail
                            detail.UnitPrice = product.UnitPrice;
                            detail.Total = product.UnitPrice * detail.Quantity;
                        }

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

                        // Save order header
                        await SalesRepository.AddOrderAsync(header);

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

                            // Add order detail
                            await SalesRepository.AddOrderDetailAsync(detail);

                            // Get last inventory for product
                            var lastInventory = ProductionRepository
                                .GetProductInventories()
                                .Where(item => item.ProductID == detail.ProductID)
                                .OrderByDescending(item => item.CreationDateTime)
                                .FirstOrDefault();

                            // Calculate stocks for product
                            var stocks = lastInventory == null ? 0 : lastInventory.Stocks - detail.Quantity;

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

                            // Save product inventory
                            await ProductionRepository.AddProductInventoryAsync(productInventory);
                        }

                        response.Model = header;

                        // Commit transaction
                        transaction.Commit();

                        Logger.LogInformation(SalesDisplays.CreateOrderMessage);
                    }
                    catch (Exception ex)
                    {
                        transaction.Rollback();

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

            return response;
        }

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

            var response = new SingleResponse<Order>();

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

                if (entity != null)
                {
                    // Init a new instance for order
                    response.Model = new Order();

                    // Set values from existing order
                    response.Model.OrderID = entity.OrderID;
                    response.Model.OrderDate = entity.OrderDate;
                    response.Model.CustomerID = entity.CustomerID;
                    response.Model.EmployeeID = entity.EmployeeID;
                    response.Model.ShipperID = entity.ShipperID;
                    response.Model.Total = entity.Total;
                    response.Model.Comments = entity.Comments;

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

            return response;
        }

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

            var response = new SingleResponse<Order>();

            try
            {
                // Retrieve order by id
                response.Model = await SalesRepository
                    .GetOrderAsync(new Order(id));

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

                    // Delete order
                    await SalesRepository.DeleteOrderAsync(response.Model);

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

            return response;
        }
    }
}

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

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

Step 03 - Putting All Code Together

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

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

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

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

// Create logger instance
var logger = LoggerMocker.GetLogger<ISalesBusinessObject>();

// Create application user
var userInfo = new UserInfo { Name = "admin" };

// Create options for DbContext
var options = new DbContextOptionsBuilder<StoreDbContext>()
    .UseSqlServer(ConnectionString)
    .Options;

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

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

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

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

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

Step 04 - Add Unit Tests

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

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

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

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

namespace Store.Core.Tests
{
    public class SalesServiceTests
    {
        [Fact]
        public async Task TestGetCustomers()
        {
            // Arrange
            using (var service = ServiceMocker.GetSalesService())
            {
                var pageSize = 10;
                var pageNumber = 1;

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

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

        [Fact]
        public async Task TestGetShippers()
        {
            // Arrange
            using (var service = ServiceMocker.GetSalesService())
            {
                var pageSize = 10;
                var pageNumber = 1;

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

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

        [Fact]
        public async Task TestGetOrders()
        {
            // Arrange
            using (var service = ServiceMocker.GetSalesService())
            {
                var pageSize = 10;
                var pageNumber = 1;

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

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

        [Fact]
        public async Task TestCreateOrder()
        {
            // Arrange
            using (var service = ServiceMocker.GetSalesService())
            {
                var header = new Order();

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

                var details = new List<OrderDetail>();

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

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

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

        [Fact]
        public async Task TestUpdateOrder()
        {
            // Arrange
            using (var service = ServiceMocker.GetSalesService())
            {
                var id = 1;

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

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

        [Fact]
        public async Task TestRemoveOrder()
        {
            // Arrange
            using (var service = ServiceMocker.GetSalesService())
            {
                var id = 601;

                // Act
                var response = await service.RemoveOrderAsync(id);

                // Assert
                Assert.True(response.DidError);
                Assert.True(response.ErrorMessage == String.Format(SalesDisplays.RemoveOrderExceptionMessage, id));
            }
        }
    }
}

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

Step 05 - Add Mocks

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

  1. Go to source code directory in Store.Core.
  2. Create a directory with name Store.Mocker.
  3. Change to Store.Mocker directory
  4. Run this command: dotnet new
  5. Run this command: dotnet restore
  6. Later, add console project to current solution.
  7. Add reference to Store.Core project and save changes to rebuild.

Now, we proceed to change the code on Program code file:

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

namespace Store.Mocker
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var task = new Task(MockAsync);

            task.Start();

            task.Wait();

            Console.ReadLine();
        }

        static async void MockAsync()
        {
            var year = DateTime.Now.Year;
            var ordersLimitPerDay = 10;

            var args = Environment.GetCommandLineArgs();

            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, DateTime.Now.Month, DateTime.DaysInMonth(year, DateTime.Now.Month));

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

            while (start <= end)
            {
                Console.WriteLine("Date: {0}", start);

                if (start.DayOfWeek != DayOfWeek.Sunday)
                {
                    await Task.Factory.StartNew(async () =>
                    {
                        await CreateDataAsync(start, ordersLimitPerDay);
                    });
                }

                start = start.AddDays(1);
            }
        }

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

            var humanResourcesService = ServiceMocker.GetHumanResourcesService();
            var productionService = ServiceMocker.GetProductionService();
            var salesService = ServiceMocker.GetSalesService();

            var employees = (await humanResourcesService.GetEmployeesAsync()).Model.ToList();
            var products = (await productionService.GetProductsAsync()).Model.ToList();
            var customers = (await salesService.GetCustomersAsync()).Model.ToList();
            var shippers = (await salesService.GetShippersAsync()).Model.ToList();
            var currencies = (await salesService.GetCurrenciesAsync()).Model.ToList();
            var paymentMethods = (await salesService.GetPaymentMethodsAsync()).Model.ToList();

            Console.WriteLine("Creating orders for {0}", date);
            Console.WriteLine();

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

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

                header.OrderDate = date;
                header.OrderStatusID = 100;

                header.CustomerID = customers[selectedCustomer].CustomerID;
                header.EmployeeID = employees[selectedEmployee].EmployeeID;
                header.ShipperID = shippers[selectedShipper].ShipperID;
                header.CurrencyID = currencies[selectedCurrency].CurrencyID;
                header.PaymentMethodID = paymentMethods[selectedPaymentMethod].PaymentMethodID;

                header.CreationDateTime = date;

                var details = new List<OrderDetail>();

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

                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, 2)
                    };

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

                    details.Add(detail);
                }

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

            humanResourcesService.Dispose();
            productionService.Dispose();
            salesService.Dispose();
        }
    }
}

지금 같은 창 터미널에서, 우리는 다음과 같은 명령을 실행해야 dotnet run 모든 것이 잘 작동하는 경우에, 우리는 우리의 데이터베이스에 대한 데이터를 확인하실 수 있습니다 OrderOrderDetail그리고 ProductInventory테이블을.

데이터 마커는 어떻게 작동합니까? 하루 범위의 날짜 및 한도를 설정 한 다음 일요일에 제외하고 범위 날짜의 모든 요일을 반복합니다. 일요일에 작성 명령을 내릴 수 없다고 가정합니다. 그런 다음 DbContext 및 비즈니스 개체의 인스턴스를 만들고, 무작위로 데이터를 정렬하여 고객, 화주, 직원 및 제품 목록에서 데이터를 가져옵니다. 그런 다음 CreateOrder매개 변수로 메소드 를 호출하십시오 .

요구 사항에 따라 더 많은 데이터를 생성하기 위해 날짜와 주문의 범위를 조정할 수 있습니다. 일단 사용자가 이미 완료했다면 Management Studio로 데이터베이스의 데이터를 확인할 수 있습니다

Step 06 - 웹 API 추가

이제 솔루션 탐색기에서 이름이 Store.API 인 웹 API 프로젝트를 추가하고 Store.Core프로젝트 에 대한 참조를 추가하고 이름 Sales이 있는 컨트롤러를 추가하고 다음 코드를 추가합니다.

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

namespace Store.API.Controllers
{
    [Route("api/[controller]")]
    public class SalesController : Controller
    {
        protected ILogger Logger;
        protected ISalesService SalesService;

        public SalesController(ILogger<SalesController> logger, ISalesService salesService)
        {
            Logger = logger;
            SalesService = salesService;
        }

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

            base.Dispose(disposing);
        }

        [HttpGet("Order")]
        public async Task<IActionResult> GetOrdersAsync(Int32? pageSize = 10, Int32? pageNumber = 1, Int16? currencyID = null, Int32? customerID = null, Int32? employeeID = null, Int16? orderStatusID = null, Guid? paymentMethodID = null, Int32? shipperID = null)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(GetOrdersAsync));

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

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

        [HttpGet("Order/{id}")]
        public async Task<IActionResult> GetOrderAsync(Int64 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();
        }

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

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

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

        [HttpPost]
        [Route("Order")]
        public async Task<IActionResult> CreateOrderAsync([FromBody] OrderViewModel value)
        {
            Logger?.LogDebug("{0} has been invoked", nameof(CreateOrderAsync));

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

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

        [HttpGet("CloneOrder/{id}")]
        public async Task<IActionResult> CloneOrderAsync(Int32 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();
        }

        [HttpDelete("Order/{id}")]
        public async Task<IActionResult> DeleteOrderAsync(Int32 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 대 요청

뷰 모델은 동작을 포함하는 객체이며, 요청은 웹 API 메소드를 호출하는 것과 관련된 동작입니다. 오해입니다. 뷰 모델은 뷰에 링크 된 객체이며 변경 사항을 처리하고 뷰와 동기화하는 동작을 포함합니다. 일반적으로 웹 API 메소드의 매개 변수는 속성이있는 객체이므로이 정의의 이름은 Request입니다. MVC는 MVVM이 아니며 모델의 수명주기가 다른 패턴인데,이 정의는 UI와 API 사이의 상태를 유지하지 않으며 요청 문자열에서 요청에 따라 속성 값을 설정하는 프로세스는 모델 바인더에 의해 처리됩니다.

Startup.cs파일에 모든 의존성을 설정하는 것을 잊지 마십시오 :

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

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

        public IConfigurationRoot Configuration { get; }

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

            services.AddDbContext<StoreDbContext>(options => options.UseSqlServer(Configuration["AppSettings:ConnectionString"]));

            services.AddScoped<IEntityMapper, StoreEntityMapper>();

            services.AddScoped<IUserInfo, UserInfo>();

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

            services.AddScoped<IHumanResourcesService, HumanResourcesService>();
            services.AddScoped<IProductionService, ProductionService>();
            services.AddScoped<ISalesService, SalesService>();

            services.AddOptions();

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

            services.AddSingleton<IConfiguration>(Configuration);
        }

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

            app.UseMvc();
        }
    }
}

이제 프로젝트를 빌드하고 브라우저에서 URL을 테스트 할 수 있습니다.

동사URL기술
도망API / 영업 / 주문주문 받기
도망API / 판매 / 주문 / 1ID로 주문 받기
도망API / 판매 / 주문 / 0기존 주문 없음
도망API / Sales / CreateOrderViewModel주문을 생성하는 뷰 모델 가져 오기
도망api / Sales / CloneOrder / 3기존 주문 복제
게시하다API / 영업 / 주문새 주문 만들기
지우다API / 영업 / 주문기존 주문 삭제

특별한 점으로, 우리는 다른 테스트 블록을 실행할 수 있습니다.

07 단계 - 웹 API 단위 테스트 추가

이제 우리는 API 프로젝트를위한 단위 테스트를 추가하기 시작합니다.이 테스트는 모의 테스트입니다. 나중에 통합 테스트를 추가 할 것입니다. 차이점은 무엇입니까? 모의 테스트에서 우리는 API 프로젝트를위한 모든 의존 객체를 시뮬레이션하고 통합 테스트에서 우리는 API 실행을 시뮬레이트하는 프로세스를 실행한다. API의 시뮬레이션 (Http 요청을 받아 들인다)을 의미합니다. 분명히 모의 (mock)와 통합에 대한 더 많은 정보가 있지만이 시점에서이 기본 아이디어로 충분합니다.

TDD 란 무엇입니까? 단위 테스트에서는 게시하기 전에 기능을 테스트하기 쉽기 때문에 TDD (Test Driven Development)는 단위 테스트를 정의하고 코드에서 동작을 확인하는 방법이기 때문에 테스팅이 요즘 필요합니다. TDD의 또 다른 개념은 AAA : Arrange, Act 및 Assert입니다. arrange는 객체 생성을위한 블록이고, act는 메소드에 대한 모든 호출을 배치하는 블록이고 assert는 메소드 호출의 결과를 검증하는 블록입니다.

이제 작업 디렉토리에서 터미널 창을 열고 다음 단계에 따라 API 프로젝트의 단위 테스트를 만듭니다.

  1. 에있는 테스트 디렉토리로 이동하십시오 Store.
  2. 이름이있는 디렉토리를 만듭니다 Store.API.Tests.
  3. Store.API.Tests 디렉토리로 변경하십시오.
  4. 이 명령을 실행하십시오 : dotnet new -t xunittest
  5. 이 명령을 실행하십시오 : dotnet restore
  6. 나중에 테스트 프로젝트를 현재 솔루션에 추가하고 이름 테스트 및 해당 솔루션 항목 내부에 기존 프로젝트를 추가하는 새 솔루션 항목을 만듭니다.
  7. Store.API프로젝트에 대한 참조를 추가 하고 변경 사항을 저장하여 다시 빌드하십시오.

이제 이름 SalesControllerTests이 있는 파일을 추가하고이 코드를 새 파일에 추가하십시오.

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Store.API.Controllers;
using Store.Core.BusinessLayer.Requests;
using Store.Core.BusinessLayer.Responses;
using Store.Core.DataLayer.DataContracts;
using Store.Core.EntityLayer.Sales;
using Xunit;

namespace Store.API.Tests
{
    public class SalesControllerTests
    {
        [Fact]
        public async Task TestGetOrdersAsync()
        {
            // Arrange
            var logger = LoggerMocker.GetLogger<SalesController>();
            var salesService = ServiceMocker.GetSalesService();

            using (var controller = new SalesController(logger, salesService))
            {
                // 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 logger = LoggerMocker.GetLogger<SalesController>();
            var salesService = ServiceMocker.GetSalesService();
            var currencyID = (Int16?)1;

            using (var controller = new SalesController(logger, salesService))
            {
                // 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.Where(item => item.CurrencyID == currencyID).Count() == value.Model.Count());
            }
        }

        [Fact]
        public async Task TestGetOrdersByCustomerAsync()
        {
            // Arrange
            var logger = LoggerMocker.GetLogger<SalesController>();
            var salesService = ServiceMocker.GetSalesService();
            var customerID = 1;

            using (var controller = new SalesController(logger, salesService))
            {
                // 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.Where(item => item.CustomerID == customerID).Count() == value.Model.Count());
            }
        }

        [Fact]
        public async Task TestGetOrdersByEmployeeAsync()
        {
            // Arrange
            var logger = LoggerMocker.GetLogger<SalesController>();
            var salesService = ServiceMocker.GetSalesService();
            var employeeID = 1;

            using (var controller = new SalesController(logger, salesService))
            {
                // 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.Where(item => item.EmployeeID == employeeID).Count() == value.Model.Count());
            }
        }

        [Fact]
        public async Task TestGetOrderAsync()
        {
            // Arrange
            var logger = LoggerMocker.GetLogger<SalesController>();
            var salesService = ServiceMocker.GetSalesService();
            var id = 1;

            using (var controller = new SalesController(logger, salesService))
            {
                // Act
                var response = await controller.GetOrderAsync(id) as ObjectResult;
                var value = response.Value as ISingleResponse<Order>;

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

        [Fact]
        public async Task TestGetNonExistingOrderAsync()
        {
            // Arrange
            var logger = LoggerMocker.GetLogger<SalesController>();
            var salesService = ServiceMocker.GetSalesService();
            var id = 0;

            using (var controller = new SalesController(logger, salesService))
            {
                // Act
                var response = await controller.GetOrderAsync(id) as ObjectResult;
                var value = response.Value as ISingleResponse<Order>;

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

        [Fact]
        public async Task TestGetCreateOrderRequestAsync()
        {
            // Arrange
            var logger = LoggerMocker.GetLogger<SalesController>();
            var salesService = ServiceMocker.GetSalesService();

            using (var controller = new SalesController(logger, salesService))
            {
                // Act
                var response = await controller.GetCreateOrderRequestAsync() as ObjectResult;
                var value = response.Value as ISingleResponse<CreateOrderRequest>;

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

        [Fact]
        public async Task TestCloneOrderAsync()
        {
            // Arrange
            var logger = LoggerMocker.GetLogger<SalesController>();
            var salesService = ServiceMocker.GetSalesService();
            var id = 1;

            using (var controller = new SalesController(logger, salesService))
            {
                // Act
                var response = await controller.CloneOrderAsync(id) as ObjectResult;
                var value = response.Value as ISingleResponse<Order>;

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

우리가 볼 수 있듯이 이러한 메소드는 API 프로젝트에서 Url에 대한 테스트입니다. 테스트는 비동기 메소드이므로주의하십시오.

모든 변경 사항을 저장하고 명령 줄 또는 Visual Studio에서 테스트를 실행합니다.

코드 개선

  1. Entity Framework Core 2로 업그레이드
  2. API 단위 테스트를 위해 메모리 데이터베이스에 사용
  3. 통합 테스트 추가
  4. 로그를 데이터베이스에 저장
  5. 돈을 나타내는 속성을 십진수로 나타내는 대신 응용 프로그램에서 돈을 나타 내기 위해 돈 패턴을 추가하십시오.
  6. 저장 프로 시저 호출을위한 코드 추가
  7. 인증 API 추가