TRUNGTQ

Think Big, Act Small, Fail Fast and Learn Rapidly

NAVIGATION - SEARCH

ASP.NET Core Entity Framework Core Code First: CRUD Operations

Introduction


This article introduces how to perform the Create, Read, Update, and Delete (CRUD) operations in ASP.NET Core, using Entity Framework Core. This walk through will use the "Code First" development approach and create a database from model using migration. We can view this article's sample Jumpon TechNet Gallery. I would like to recommend the following wiki article Overview Of ASP.NET Coreso that development environment be prepare for this sample application in ASP.NET Core. We will create a single entity Customer to perform the CRUD operations.

Create Database


First, let's install the Entity Framework Core in our application. As we use SQL Server, install the package for SQL Server database provider. To install database provider, follow the below steps.

  • Tools - NuGet Package Manager - Package Manager Console
  • Run PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer

When we install a package in ASP.NET Core application, then the package installation occurs in the background. We see "(Restoring...)" appeared next to References in Solution Explorer while the installation occurs.

We also use Entity Framework Core as an Object Relationship Mapper (ORM) to perform the CRUD operations. Let's install the Entity Framework Core Tool to maintain the database, using the following procedure.

  • Run PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre.
  • Open project.json file.
  • Locate the tools section and add the ef command as shown below.
"tools": {
   "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final",
   "BundlerMinifier.Core": "2.0.238",
   "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final",
   "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"
 }

Now, create two entities - the BaseEntity class that has common properties that will be inherited by each entity, and the Customer. Let's see each entity. The following is a code snippet for the BaseEntity class.

using System;
  
namespace CRUDApplication.DbEntities
{
    public class BaseEntity
    {
        public Int64 Id { get; set; }
        public DateTime AddedDate { get; set; }
        public DateTime ModifiedDate { get; set; }
        public string IPAddress { get; set; }
    }
}
Now, create a Customer entity under the DbEntities folder which inherits from the BaseEntity class. The following is a code for Customer entity.

namespace CRUDApplication.DbEntities
{
    public class Customer:BaseEntity
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string MobileNo { get; set; }
    }
}

Now, let's define the configuration for the Customer entity that will be used when the database table will be created by the entity. The following is the code for Customer mapping entity (CustomerMap.cs).

using Microsoft.EntityFrameworkCore.Metadata.Builders;
  
namespace CRUDApplication.DbEntities
{
    public class CustomerMap
    {
        public CustomerMap(EntityTypeBuilder<Customer> entityBuilder)
        {
            entityBuilder.HasKey(t => t.Id);          
            entityBuilder.Property(t => t.FirstName).IsRequired();
            entityBuilder.Property(t => t.LastName).IsRequired();
            entityBuilder.Property(t => t.Email).IsRequired();
            entityBuilder.Property(t => t.MobileNo).IsRequired();         
        }
    }
}

The EntityTypeBuilder is an important class that allows configuration to be performed for an entity type in a model. This is done using the modelbuilder in an override of the OnModelCreating method. The Constructor of the CustomerMap class uses the Fluent API to map and configure properties in the table. So let's see each method used in the constructor one-by-one.

  1. HasKey(): The Haskey() method configures a primary key on the table.
  2. Property(): The Property method configures attributes for each property belonging to an entity or complex type. It is used to obtain a configuration object for a given property. The options on the configuration object are specific to the type being configured.

Now, it's time to define context class. The ADO.NET Entity Framework Code First development approach requires us to create a data access context class that inherits from the DbContext class so we create a context class CRUDContext (CRUDContext.cs) class. In this class, we override the OnModelCreating() method. This method is called when the model for a context class (CRUDContext) has been initialized, but before the model has been locked down and used to initialize the context such that the model can be further configured before it is locked down. The following is the code snippet for the context class.

using Microsoft.EntityFrameworkCore;
  
namespace CRUDApplication.DbEntities
{
    public class CRUDContext:DbContext
    {
        public CRUDContext(DbContextOptions<CRUDContext> options) : base(options)
        {
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            new CustomerMap(modelBuilder.Entity<Customer>());
        }
    }
}

As the concept of dependency injection is central to the ASP.NET Core application, so we register our context to dependency injection during the application start up. Once we register CRUDContext context as a service to dependency injection, then provide it via constructor to MVC controller. In order for our MVC controllers to make use of CRUDContext, we are going to register it as a service.

  1. Open the appsettings.json file and define connection string here.
    {
      "ConnectionStrings": {
        "DefaultConnection": "Data Source=ADMIN\\SQLEXPRESS;Initial Catalog=ECommerceDb;User ID=sa; Password=sa"
      },
      "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
          "Default": "Debug",
          "System": "Information",
          "Microsoft": "Information"
        }
      }
    }

    It stores application level settings such as connection string, SMTP etc. It's similar to web.config file in ASP.NET.

  2. Open the Startup.cs file and following using statements at the start of the file.
    using Microsoft.EntityFrameworkCore;
    using CRUDApplication.DbEntities;
  3. Now, we can use the AddDbContext method to register it as a service. Locate the ConfigureServices method and add the lines of code for register it to dependency injection as per following code snippet.
    public void ConfigureServices(IServiceCollection services)
            {
                services.AddMvc();
                services.AddDbContext<CRUDContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
            }

Now we have created a model so time to create a database using migration.

  • Tools -> NuGet Package Manager -> Package Manager Console
  • Run PM> Add-Migration MyFirstMigration to scaffold a migration to create the initial set of tables for our model. If we receive an error stating the term `add-migration' is not recognized as the name of a cmdlet, then close and reopen Visual Studio
  • Run PM> Update-Database to apply the new migration to the database. Because our database doesn't exist yet, it will be created for us before the migration is applied.

Create Application User Interface


Now we proceed to the controller. Create a CustomerController under the Controllers folder of the application. This controller has all ActionResult methods for each user interface of a CRUD operation. We first create a CRUDContext class instance then we inject it in the controller's constructor to get its object. The following is a code snippet for the CustomerController.

using CRUDApplication.DbEntities;
using CRUDApplication.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
  
namespace CRUDApplication.Controllers
{
    public class CustomerController : Controller
    {
        private CRUDContext context;
  
        public CustomerController(CRUDContext context)
        {
            this.context = context;
        }
        [HttpGet]
        public IActionResult Index()
        {
            IEnumerable<CustomerViewModel> model = context.Set<Customer>().ToList().Select(s => new CustomerViewModel
            {
                Id= s.Id,
                Name = $"{s.FirstName} {s.LastName}",
                MobileNo = s.MobileNo,
                Email = s.Email
            });
            return View("Index", model);
        }
  
        [HttpGet]
        public IActionResult AddEditCustomer(long? id)
        {
            CustomerViewModel model = new CustomerViewModel();
            if (id.HasValue)
            {
                Customer customer = context.Set<Customer>().SingleOrDefault(c => c.Id == id.Value);
                if (customer != null)
                {
                    model.Id = customer.Id;
                    model.FirstName = customer.FirstName;
                    model.LastName = customer.LastName;
                    model.MobileNo = customer.MobileNo;
                    model.Email = customer.Email;
                }
            }
            return PartialView("~/Views/Customer/_AddEditCustomer.cshtml", model);
        }
  
        [HttpPost]
        public ActionResult AddEditCustomer(long? id, CustomerViewModel model)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    bool isNew = !id.HasValue;
                    Customer customer = isNew ? new Customer
                    {
                        AddedDate = DateTime.UtcNow
                    } : context.Set<Customer>().SingleOrDefault(s => s.Id == id.Value);
                    customer.FirstName = model.FirstName;
                    customer.LastName = model.LastName;
                    customer.MobileNo = model.MobileNo;
                    customer.Email = model.Email;
                    customer.IPAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString();
                    customer.ModifiedDate = DateTime.UtcNow;
                    if (isNew)
                    {
                        context.Add(customer);
                    }
                    context.SaveChanges();
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return RedirectToAction("Index");
        }
  
        [HttpGet]
        public IActionResult DeleteCustomer(long id)
        {
            Customer customer = context.Set<Customer>().SingleOrDefault(c => c.Id == id);
            CustomerViewModel model = new CustomerViewModel
            {
                Name = $"{customer.FirstName} {customer.LastName}"
            };
            return PartialView("~/Views/Customer/_DeleteCustomer.cshtml", model);
        }
        [HttpPost]
        public IActionResult DeleteCustomer(long id, FormCollection form)
        {
            Customer customer = context.Set<Customer>().SingleOrDefault(c => c.Id == id);
            context.Entry(customer).State = Microsoft.EntityFrameworkCore.EntityState.Deleted;
            context.SaveChanges();
            return RedirectToAction("Index");
        }
    }
}

 

We can notice that the Controller takes a CRUDContext as a constructor parameter. ASP.NET dependency injection will take care of passing an instance of CRUDContext into our controller. The controller is developed to handle CURD operation requests for a Customer entity. Now, let's develop the user interface for the CRUD operations. We develop it for the views for adding and editing a customer, a customer listing, customer deletion. Let's see each one by one.

Customer List View


This is the first view when the application is accessed or the entry point of the application is executed. It shows the customer listing as in Figure 1. We display customer data in tabular format and on this view we create links to add a new customer, edit a customer and delete a customer. This view is an index view and the following is a code snippet for index.cshtml under the Customer folder of Views.

@model IEnumerable<CRUDApplication.Models.CustomerViewModel>
@using CRUDApplication.Models
@using CRUDApplication.Code
  
<div class="top-buffer"></div>
<div class="panel panel-primary">
    <div class="panel-heading panel-head">Customers</div>
    <div class="panel-body">
        <div class="btn-group">
            <a id="createEditCustomerModal" data-toggle="modal" asp-action="AddEditCustomer" data-target="#modal-action-customer" class="btn btn-primary">
                <i class="glyphicon glyphicon-plus"></i>  Add Customer
            </a>
        </div>
        <div class="top-buffer"></div>
        <table class="table table-bordered table-striped table-condensed">
            <thead>
                <tr>
                    <th>Name</th>                  
                    <th>Email</th>
                    <th>Mobile No</th>
                    <th>Action</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var item in Model)
                {
                    <tr>
                        <td>@Html.DisplayFor(modelItem => item.Name)</td>
                        <td>@Html.DisplayFor(modelItem => item.Email)</td>
                        <td>@Html.DisplayFor(modelItem => item.MobileNo)</td>                     
                        <td>
                            <a id="editCustomerModal" data-toggle="modal" asp-action="AddEditCustomer" asp-route-id= "@item.Id" data-target="#modal-action-customer"
                               class="btn btn-info">
                                <i class="glyphicon glyphicon-pencil"></i>  Edit
                            </a>
                            <a id="deleteCustomerModal" data-toggle="modal" asp-action="DeleteCustomer" asp-route-id= "@item.Id" data-target="#modal-action-customer" class="btn btn-danger">
                                <i class="glyphicon glyphicon-trash"></i>  Delete
                            </a>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
</div>
@Html.Partial("_Modal", new BootstrapModel { ID = "modal-action-customer", AreaLabeledId = "modal-action-customer-label", Size = ModalSize.Small })
@section scripts
{
    <script src="~/js/customer-index.js" asp-append-version="true"></script>
}

When we run the application and call the index() action with a HttpGet request, then we get all the customers listed in the UI as in Figure 1. This UI has options for CRUD operations.

Figure 1: Customer Listing UI

Create / Edit Customer View


We create a common view to create and edit a customer so we create a single customer view model. The following code snippet for CustomerViewModel.cs.

using System.ComponentModel.DataAnnotations;
  
namespace CRUDApplication.Models
{
    public class CustomerViewModel
    {
        public long Id { get; set; }
        [Display(Name="First Name")]
        public string FirstName { get; set; }
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        [Display(Name = "Mobile No")]
        public string MobileNo { get; set; }
    }
}

We show form in the bootstrap modal popup and submit using ajax post; that's why we create a javascript file which contains a method for removing loaded data.

(function ($) {
    function Customer() {
        var $this = this;
  
        function initilizeModel() {         
            $("#modal-action-customer").on('loaded.bs.modal', function (e) {             
                }).on('hidden.bs.modal', function (e) {                 
                    $(this).removeData('bs.modal');
                });          
        }     
        $this.init = function () {
            initilizeModel();
        }
    }
    $(function () {
        var self = new Customer();
        self.init();      
    })
}(jQuery))

Now, define a create/edit customer partial view. The following is the code snippet for _AddEditCustomer.cshtml.

@model CRUDApplication.Models.CustomerViewModel
@using CRUDApplication.Models
  
<form asp-action="AddEditCustomer" role="form">
    @await Html.PartialAsync("_ModalHeader", new ModalHeader { Heading = String.Format("{0} Customer", @Model.Id == 0 ? "Add" : "Edit") })
     
    <div class="modal-body form-horizontal">
        <div class="form-group">
            <label asp-for="FirstName" class="col-lg-3 col-sm-3 control-label"></label>         
            <div class="col-lg-6">
                <input asp-for="FirstName" class="form-control" />              
            </div>
        </div>
        <div class="form-group">
            <label asp-for="LastName" class="col-lg-3 col-sm-3 control-label"></label>
            <div class="col-lg-6">
                <input asp-for="LastName" class="form-control" />                             
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Email" class="col-lg-3 col-sm-3 control-label"></label>
            <div class="col-lg-6">
                <input asp-for="Email" class="form-control" />
            </div>
        </div>
        <div class="form-group">
            <label asp-for="MobileNo" class="col-lg-3 col-sm-3 control-label"></label>
            <div class="col-lg-6">
                <input asp-for="MobileNo" class="form-control" />
            </div>
        </div>      
    </div>
  @await Html.PartialAsync("_ModalFooter", new ModalFooter { })
</form>

Now, run the application and click on Edit button of listing which calls AddEditCustomer action method, then we get the UI as in Figure 2 to edit a customer.

Figure 2: Edit a Customer UI

Delete A Customer


To delete a customer, we follow the process of clicking on the Delete button that exists in the Customer listing data then a modal popup shows to ask "Are you want to delete xxx?" after clicking on the Delete button that exists in the popup view such as in Figure 3 then it makes a HttpPost request that calls the DeleteCustomer() action method and deletes the customer. The following is a code snippet for _DeleteCustomer.cshtml.

@model CRUDApplication.Models.CustomerViewModel
@using CRUDApplication.Models
  
@using (Html.BeginForm())
{
    @Html.Partial("_ModalHeader", new ModalHeader { Heading = "Delete Customer" })
  
    <div class="modal-body form-horizontal">
        Are you want to delete @Model.Name?
    </div>
    @Html.Partial("_ModalFooter", new ModalFooter { SubmitButtonText = "Delete" })
}

Now, run the application and click on Delete button of listing which calls DeleteCustomer action method, then we get the UI as in Figure 3 to delete a customer.

Figure 3: Delete Confirmation

Conclusion

This article introduced CRUD operations in ASP.NET Core using Entity Framework Core with "code first" development approach. We used bootstrap CSS and JavaScript for the user interface design in this application.


 

LINK: https://social.technet.microsoft.com/wiki/contents/articles/36046.asp-net-core-entity-framework-core-code-first-crud-operations.aspx

Building REST APIs using ASP.NET Core and Entity Framework Core

 

SourceCode: dotnetcore-entityframework-api

ASP.NET Core and Entity Framework Core are getting more and more attractive nowadays and this post will show you how to get the most of them in order to get started with building scalable and robust APIs. We have seen them in action on a previous post but now we have all the required tools and knowledge to explain things in more detail. One of the most key points we are going to show on this post is how to structure a cross platform API solution properly. On the previous post we used a single ASP.NET Core Web Application project to host all the different components of our application (Models, Data Repositories, API, Front-end) since cross-platform .NET Core libraries weren’t supported yet. This time though we will follow the Separation of Concerns design principle by spliting the application in different layers.

What this post is all about

The purpose of this post is to build the API infrastructure for an SPA Angular application that holds and manipulates schedule information. We will configure the database using Entity Framework Core (Code First – Migrations), create the Models, Repositories and the REST – MVC API as well. Despite the fact that we ‘ll build the application using VS 2015, the project will be able to run in and outside of it. Let’s denote the most important sections of this post.

  • Create a cross platform solution using the Separation of Concerns principle
  • Create the Models and Data Repositories
  • Apply EF Core migrations from a different assembly that the DbContext belongs
  • Build the API using REST architecture principles
  • Apply ViewModel validations using the FluentValidation Nuget Package
  • Apply a global Exception Handler for the API controllers

In the next post will build the associated Angular SPA that will make use of the API. The SPA will use the latest version of Angular, TypeScript and much more. More over, it’s going to apply several interesting features such as custom Modal popups, DateTime pickers, Form validations and animations. Just to keep you waiting for it let me show you some screenshots of the final SPA.
Are you ready? Let’s start!

Create a cross platform solution

Assuming you have already .NET Core installed on your machine, open VS 2015 and create a blank solution named Scheduler. Right click the solution and add two new projects of type Class Library (.NET Core). Name the first one Scheduler.Model and the second one Scheduler.Data.
You can remove the default Class1 classes, you won’t need them. Continue by adding a new ASP.NET Core Web Application (.NET Core) project named Scheduler.API by selecting the Empty template.

Create the Models and Data Repositories

Scheduler.Model and Scheduler.Data libraries are cross-platform projects and could be created outside VS as well. The most important file that this type of project has is the project.json. Let’s create first our models. Switch to Scheduler.Model and change the project.json file as follow:

project.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "version":"1.0.0-*",
 
  "dependencies": {
    "NETStandard.Library":"1.6.0"
  },
 
  "frameworks": {
    "netstandard1.6": {
      "imports": [
        "dnxcore50",
        "portable-net452+win81"
      ]
    }
  }
}

Add a .cs file named IEntityBase which will hold the base interface for our Entities.

IEntityBase.cs
1
2
3
4
public interface IEntityBase
{
    int Id {get;set; }
}

Create a folder named Entities and add the following classes:

Schedule.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Schedule : IEntityBase
{
    public Schedule()
    {
        Attendees =new List<Attendee>();
    }
 
    public int Id {get;set; }
    public string Title {get;set; }
    public string Description {get;set; }
    public DateTime TimeStart {get;set; }
    public DateTime TimeEnd {get;set; }
    public string Location {get;set; }
    public ScheduleType Type {get;set; }
 
    public ScheduleStatus Status {get;set; }
    public DateTime DateCreated {get;set; }
    public DateTime DateUpdated {get;set; }
    public User Creator {get;set; }
    public int CreatorId {get;set; }
    public ICollection<Attendee> Attendees {get;set; }
}
User.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User : IEntityBase
{
    public User()
    {
        SchedulesCreated =new List<Schedule>();
        SchedulesAttended =new List<Attendee>();
    }
    public int Id {get;set; }
    public string Name {get;set; }
    public string Avatar {get;set; }
    public string Profession {get;set; }
    public ICollection<Schedule> SchedulesCreated {get;set; }
    public ICollection<Attendee> SchedulesAttended {get;set; }
}
Attendee.cs
1
2
3
4
5
6
7
8
9
public class Attendee : IEntityBase
{
    public int Id {get;set; }
    public int UserId {get;set; }
    public User User {get;set; }
 
    public int ScheduleId {get;set; }
    public Schedule Schedule {get;set; }
}
ScheduleEnums.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum ScheduleType
{
    Work = 1,
    Coffee = 2,
    Doctor = 3,
    Shopping = 4,
    Other = 5
}
 
public enum ScheduleStatus
{
    Valid = 1,
    Cancelled = 2
}

As you can see there are only three basic classes, Schedule, User and Attendee. Our SPA will display schedule information where a user may create many schedules One – Many relationship and attend many others Many – Many relationship. We will bootstrap the database later on using EF migrations but here’s the schema for your reference.
Switch to Scheduler.Data project and change the project.json file as follow:

project.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "version":"1.0.0-*",
 
  "dependencies": {
    "Microsoft.EntityFrameworkCore":"1.0.0",
    "Microsoft.EntityFrameworkCore.Relational":"1.0.0",
    "NETStandard.Library":"1.6.0",
    "Scheduler.Model":"1.0.0-*",
    "System.Linq.Expressions":"4.1.0"
  },
 
  "frameworks": {
    "netstandard1.6": {
      "imports": [
        "dnxcore50",
        "portable-net452+win81"
      ]
    }
  }
}

We need Entity Framework Core on this project to set the DbContext class and a reference to the Scheduler.Model project. Add a folder named Abstract and create the following interfaces:

IEntityBaseRepository.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IEntityBaseRepository<T>where T :class, IEntityBase,new()
{
    IEnumerable<T> AllIncluding(params Expression<Func<T,object>>[] includeProperties);
    IEnumerable<T> GetAll();
    int Count();
    T GetSingle(int id);
    T GetSingle(Expression<Func<T,bool>> predicate);
    T GetSingle(Expression<Func<T,bool>> predicate,params Expression<Func<T,object>>[] includeProperties);
    IEnumerable<T> FindBy(Expression<Func<T,bool>> predicate);
    void Add(T entity);
    void Update(T entity);
    void Delete(T entity);
    void DeleteWhere(Expression<Func<T,bool>> predicate);
    void Commit();
}
IRepositories.cs
1
2
3
4
5
public interface IScheduleRepository : IEntityBaseRepository<Schedule> { }
 
public interface IUserRepository : IEntityBaseRepository<User> { }
 
public interface IAttendeeRepository : IEntityBaseRepository<Attendee> { }

Continue by creating the repositories in a new folder named Repositories.

EntityBaseRepository.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class EntityBaseRepository<T> : IEntityBaseRepository<T>
        where T :class, IEntityBase,new()
{
 
    private SchedulerContext _context;
 
    #region Properties
    public EntityBaseRepository(SchedulerContext context)
    {
        _context = context;
    }
    #endregion
    public virtual IEnumerable<T> GetAll()
    {
        return _context.Set<T>().AsEnumerable();
    }
 
    public virtual int Count()
    {
        return _context.Set<T>().Count();
    }
    public virtual IEnumerable<T> AllIncluding(params Expression<Func<T,object>>[] includeProperties)
    {
        IQueryable<T> query = _context.Set<T>();
        foreach (var includePropertyin includeProperties)
        {
            query = query.Include(includeProperty);
        }
        return query.AsEnumerable();
    }
 
    public T GetSingle(int id)
    {
        return _context.Set<T>().FirstOrDefault(x => x.Id == id);
    }
 
    public T GetSingle(Expression<Func<T,bool>> predicate)
    {
        return _context.Set<T>().FirstOrDefault(predicate);
    }
 
    public T GetSingle(Expression<Func<T,bool>> predicate,params Expression<Func<T,object>>[] includeProperties)
    {
        IQueryable<T> query = _context.Set<T>();
        foreach (var includePropertyin includeProperties)
        {
            query = query.Include(includeProperty);
        }
 
        return query.Where(predicate).FirstOrDefault();
    }
 
    public virtual IEnumerable<T> FindBy(Expression<Func<T,bool>> predicate)
    {
        return _context.Set<T>().Where(predicate);
    }
 
    public virtual void Add(T entity)
    {
        EntityEntry dbEntityEntry = _context.Entry<T>(entity);
        _context.Set<T>().Add(entity);
    }
 
    public virtual void Update(T entity)
    {
        EntityEntry dbEntityEntry = _context.Entry<T>(entity);
        dbEntityEntry.State = EntityState.Modified;
    }
    public virtual void Delete(T entity)
    {
        EntityEntry dbEntityEntry = _context.Entry<T>(entity);
        dbEntityEntry.State = EntityState.Deleted;
    }
 
    public virtual void DeleteWhere(Expression<Func<T,bool>> predicate)
    {
        IEnumerable<T> entities = _context.Set<T>().Where(predicate);
 
        foreach(var entityin entities)
        {
            _context.Entry<T>(entity).State = EntityState.Deleted;
        }
    }
 
    public virtual void Commit()
    {
        _context.SaveChanges();
    }
}
ScheduleRepository.cs
1
2
3
4
5
6
public class ScheduleRepository : EntityBaseRepository<Schedule>, IScheduleRepository
{
    public ScheduleRepository(SchedulerContext context)
        :base(context)
    { }
}
UserRepository.cs
1
2
3
4
5
6
public class UserRepository : EntityBaseRepository<User>, IUserRepository
{
    public UserRepository(SchedulerContext context)
        :base(context)
    { }
}
AttendeeRepository.cs
1
2
3
4
5
6
public class AttendeeRepository : EntityBaseRepository<Attendee>, IAttendeeRepository
{
    public AttendeeRepository(SchedulerContext context)
        :base(context)
    { }
}

Since we want to use Entity Framework to access our database we need to create a respective DbContext class. Add the SchedulerContext class under the root of the Scheduler.Data project.

SchedulerContext.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class SchedulerContext : DbContext
{
    public DbSet<Schedule> Schedules {get;set; }
    public DbSet<User> Users {get;set; }
    public DbSet<Attendee> Attendees {get;set; }
 
    public SchedulerContext(DbContextOptions options) :base(options) { }
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var relationshipin modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys()))
        {
            relationship.DeleteBehavior = DeleteBehavior.Restrict;
        }
 
 
        modelBuilder.Entity<Schedule>()
            .ToTable("Schedule");
 
        modelBuilder.Entity<Schedule>()
            .Property(s => s.CreatorId)
            .IsRequired();
 
        modelBuilder.Entity<Schedule>()
            .Property(s => s.DateCreated)
            .HasDefaultValue(DateTime.Now);
 
        modelBuilder.Entity<Schedule>()
            .Property(s => s.DateUpdated)
            .HasDefaultValue(DateTime.Now);
 
        modelBuilder.Entity<Schedule>()
            .Property(s => s.Type)
            .HasDefaultValue(ScheduleType.Work);
 
        modelBuilder.Entity<Schedule>()
            .Property(s => s.Status)
            .HasDefaultValue(ScheduleStatus.Valid);
 
        modelBuilder.Entity<Schedule>()
            .HasOne(s => s.Creator)
            .WithMany(c => c.SchedulesCreated);
 
        modelBuilder.Entity<User>()
            .ToTable("User");
 
        modelBuilder.Entity<User>()
            .Property(u => u.Name)
            .HasMaxLength(100)
            .IsRequired();
 
        modelBuilder.Entity<Attendee>()
            .ToTable("Attendee");
 
        modelBuilder.Entity<Attendee>()
            .HasOne(a => a.User)
            .WithMany(u => u.SchedulesAttended)
            .HasForeignKey(a => a.UserId);
 
        modelBuilder.Entity<Attendee>()
            .HasOne(a => a.Schedule)
            .WithMany(s => s.Attendees)
            .HasForeignKey(a => a.ScheduleId);
 
    }
}

Before moving to the Scheduler.API and create the API Controllers let’s add a Database Initializer class that will init some mock data when the application fires for the first time. You can find the SchedulerDbInitializer class here.

Build the API using REST architecture principles

Switch to the Scheduler.API ASP.NET Core Web Application project and modify the project.json file as follow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
{
  "userSecretsId":"Scheduler",
 
  "dependencies": {
    "AutoMapper.Data":"1.0.0-beta1",
    "FluentValidation":"6.2.1-beta1",
    "Microsoft.NETCore.App": {
      "version":"1.0.0",
      "type":"platform"
    },
    "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore":"1.0.0",
    "Microsoft.EntityFrameworkCore":"1.0.0",
    "Microsoft.EntityFrameworkCore.SqlServer":"1.0.0",
    "Microsoft.EntityFrameworkCore.Tools": {
      "version":"1.0.0-preview2-final",
      "type":"build"
    },
    "Microsoft.AspNetCore.Server.IISIntegration":"1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel":"1.0.0",
    "Microsoft.Extensions.Configuration":"1.0.0",
    "Microsoft.Extensions.Configuration.FileExtensions":"1.0.0",
    "Microsoft.Extensions.Configuration.Json":"1.0.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables":"1.0.0",
    "Scheduler.Data":"1.0.0-*",
    "Scheduler.Model":"1.0.0-*",
    "Microsoft.Extensions.Configuration.UserSecrets":"1.0.0",
    "Microsoft.AspNetCore.Mvc":"1.0.0",
    "Newtonsoft.Json":"9.0.1",
    "Microsoft.AspNetCore.StaticFiles":"1.0.0",
    "Microsoft.Extensions.FileProviders.Physical":"1.0.0",
    "Microsoft.AspNetCore.Diagnostics":"1.0.0"
  },
 
  "tools": {
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": {
      "version":"1.0.0-preview2-final",
      "imports":"portable-net45+win8+dnxcore50"
    },
    "Microsoft.EntityFrameworkCore.Tools": {
      "version":"1.0.0-preview2-final",
      "imports": [
        "portable-net45+win8+dnxcore50",
        "portable-net45+win8"
      ]
    }
  },
 
  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6",
        "dnxcore50",
        "portable-net45+win8"
      ]
    }
  },
 
  "buildOptions": {
    "emitEntryPoint":true,
    "preserveCompilationContext":true
  },
 
  "runtimeOptions": {
    "gcServer":true,
    "gcConcurrent":true
  },
 
  "publishOptions": {
    "include": [
      "wwwroot",
      "web.config"
    ]
  },
 
  "scripts": {
    "postpublish": ["dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

We referenced the previous two projects and some tools related to Entity Framework cause we are going to use EF migrations to create the database. Of course we also referenced MVC Nuget Packages in order to incorporate the MVC services into the pipeline. Modify the Startup class..

Startup.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public class Startup
    {
        private static string _applicationPath =string.Empty;
        private static string _contentRootPath =string.Empty;
        public IConfigurationRoot Configuration {get;set; }
        public Startup(IHostingEnvironment env)
        {
            _applicationPath = env.WebRootPath;
            _contentRootPath = env.ContentRootPath;
            // Setup configuration sources.
 
            var builder =new ConfigurationBuilder()
                .SetBasePath(_contentRootPath)
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional:true);
 
            if (env.IsDevelopment())
            {
                // This reads the configuration keys from the secret store.
                // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
                builder.AddUserSecrets();
            }
 
            builder.AddEnvironmentVariables();
            Configuration = builder.Build();
        }
        // 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 http://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<SchedulerContext>(options =>
                options.UseSqlServer(Configuration["Data:SchedulerConnection:ConnectionString"],
                b => b.MigrationsAssembly("Scheduler.API")));
 
            // Repositories
            services.AddScoped<IScheduleRepository, ScheduleRepository>();
            services.AddScoped<IUserRepository, UserRepository>();
            services.AddScoped<IAttendeeRepository, AttendeeRepository>();
 
            // Automapper Configuration
            AutoMapperConfiguration.Configure();
 
            // Enable Cors
            services.AddCors();
 
            // Add MVC services to the services container.
            services.AddMvc()
                .AddJsonOptions(opts =>
                {
                    // Force Camel Case to JSON
                    opts.SerializerSettings.ContractResolver =new CamelCasePropertyNamesContractResolver();
                });
        }
 
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app)
        {
            app.UseStaticFiles();
            // Add MVC to the request pipeline.
            app.UseCors(builder =>
                builder.AllowAnyOrigin()
                .AllowAnyHeader()
                .AllowAnyMethod());
 
            app.UseExceptionHandler(
              builder =>
              {
                  builder.Run(
                    async context =>
                    {
                        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                        context.Response.Headers.Add("Access-Control-Allow-Origin","*");
 
                        var error = context.Features.Get<IExceptionHandlerFeature>();
                        if (error !=null)
                        {
                            context.Response.AddApplicationError(error.Error.Message);
                            await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false);
                        }
                    });
              });
 
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name:"default",
                    template:"{controller=Home}/{action=Index}/{id?}");
 
                // Uncomment the following line to add a route for porting Web API 2 controllers.
                //routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}");
            });
 
            SchedulerDbInitializer.Initialize(app.ApplicationServices);
        }
    }

We may haven’t created all the required classes (dont’ worry we will) for this to be compiled yet, but let’s point the most important parts. There is a mismatch between the project that the configuration file (appsettings.json) which holds the database connection string and the respective SchedulerDbContext class leaves. The appsettings.json file which we will create a little bit later is inside the API project while the DbContext class belongs to the Scheduler.Data. If we were to init EF migrations using the following command, we would fail because of the mismatch.

1
dotnet ef migrations add"initial"

What we need to do is to inform EF the assembly to be used for migrations..

1
2
3
services.AddDbContext<SchedulerContext>(options =>
     options.UseSqlServer(Configuration["Data:SchedulerConnection:ConnectionString"],
        b => b.MigrationsAssembly("Scheduler.API")));

We have added Cors services allowing all headers for all origins just for simplicity. Normally, you would allow only a few origins and headers as well. We need this cause the SPA we are going to create is going to be an entire different Web application built in Visual Studio Code.

1
2
3
4
app.UseCors(builder =>
    builder.AllowAnyOrigin()
    .AllowAnyHeader()
    .AllowAnyMethod());

One thing I always try to avoid is polluting my code with try/catch blocks. This is easy to accomplish in ASP.NET Core by adding a global Exception Handler into the pipeline.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.UseExceptionHandler(
    builder =>
    {
        builder.Run(
        async context =>
        {
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            context.Response.Headers.Add("Access-Control-Allow-Origin","*");
 
            var error = context.Features.Get<IExceptionHandlerFeature>();
            if (error !=null)
            {
                context.Response.AddApplicationError(error.Error.Message);
                await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false);
            }
        });
    });

Create an appsettings.json file at the root of the API application to hold you database connection string. Make sure you change it and reflect your environment.

appsettings.json
1
2
3
4
5
6
7
{
  "Data": {
    "SchedulerConnection": {
      "ConnectionString":"Server=(localdb)\\v11.0;Database=SchedulerDb;Trusted_Connection=True;MultipleActiveResultSets=true"
    }
  }
}

Apply ViewModel validations and mappings

It’s good practise to send a parsed information to the front-end instead of using the database schema information. Add a new folder named ViewModels with the following three classes.

ScheduleViewModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ScheduleViewModel : IValidatableObject
{
    public int Id {get;set; }
    public string Title {get;set; }
    public string Description {get;set; }
    public DateTime TimeStart {get;set; }
    public DateTime TimeEnd {get;set; }
    public string Location {get;set; }
    public string Type {get;set; }
    public string Status {get;set; }
    public DateTime DateCreated {get;set; }
    public DateTime DateUpdated {get;set; }
    public string Creator {get;set; }
    public int CreatorId {get;set; }
    public int[] Attendees {get;set; }
 
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var validator =new ScheduleViewModelValidator();
        var result = validator.Validate(this);
        return result.Errors.Select(item =>new ValidationResult(item.ErrorMessage,new[] { item.PropertyName }));
    }
}
UserViewModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserViewModel : IValidatableObject
{
    public int Id {get;set; }
    public string Name {get;set; }
    public string Avatar {get;set; }
    public string Profession {get;set; }
    public int SchedulesCreated {get;set; }
 
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var validator =new UserViewModelValidator();
        var result = validator.Validate(this);
        return result.Errors.Select(item =>new ValidationResult(item.ErrorMessage,new[] { item.PropertyName }));
    }
}
ScheduleDetailsViewModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ScheduleDetailsViewModel
{
    public int Id {get;set; }
    public string Title {get;set; }
    public string Description {get;set; }
    public DateTime TimeStart {get;set; }
    public DateTime TimeEnd {get;set; }
    public string Location {get;set; }
    public string Type {get;set; }
    public string Status {get;set; }
    public DateTime DateCreated {get;set; }
    public DateTime DateUpdated {get;set; }
    public string Creator {get;set; }
    public int CreatorId {get;set; }
    public ICollection<UserViewModel> Attendees {get;set; }
    // Lookups
    public string[] Statuses {get;set; }
    public string[] Types {get;set; }
}

When posting or updating ViewModels through HTTP POST / UPDATE requests to our API we want posted ViewModel data to pass through validations first. For this reason we will configure custom validations using FluentValidation. Add a folder named Validations inside the ViewModels one and create the following two validators.

UserViewModelValidator.cs
1
2
3
4
5
6
7
8
9
public class UserViewModelValidator : AbstractValidator<UserViewModel>
{
    public UserViewModelValidator()
    {
        RuleFor(user => user.Name).NotEmpty().WithMessage("Name cannot be empty");
        RuleFor(user => user.Profession).NotEmpty().WithMessage("Profession cannot be empty");
        RuleFor(user => user.Avatar).NotEmpty().WithMessage("Profession cannot be empty");
    }
}
ScheduleViewModelValidator.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ScheduleViewModelValidator : AbstractValidator<ScheduleViewModel>
{
    public ScheduleViewModelValidator()
    {
        RuleFor(s => s.TimeEnd).Must((start, end) =>
        {
            return DateTimeIsGreater(start.TimeStart, end);
        }).WithMessage("Schedule's End time must be greater than Start time");
    }
 
    private bool DateTimeIsGreater(DateTime start, DateTime end)
    {
        return end > start;
    }
}

We will set front-end side validations using Angular but you should always run validations on the server as well. The ScheduleViewModelValidator ensures that the schedule’s end time is always greater than start time. The custom errors will be returned through the ModelState like this:

1
2
3
4
if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

Add a new folder named Mappings inside the ViewModels and set the Domain to ViewModel mappings.

DomainToViewModelMappingProfile.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DomainToViewModelMappingProfile : Profile
{
    protected override void Configure()
    {
        Mapper.CreateMap<Schedule, ScheduleViewModel>()
            .ForMember(vm => vm.Creator,
                map => map.MapFrom(s => s.Creator.Name))
            .ForMember(vm => vm.Attendees, map =>
                map.MapFrom(s => s.Attendees.Select(a => a.UserId)));
 
        Mapper.CreateMap<Schedule, ScheduleDetailsViewModel>()
            .ForMember(vm => vm.Creator,
                map => map.MapFrom(s => s.Creator.Name))
            .ForMember(vm => vm.Attendees, map =>
                map.UseValue(new List<UserViewModel>()))
            .ForMember(vm => vm.Status, map =>
                map.MapFrom(s => ((ScheduleStatus)s.Status).ToString()))
            .ForMember(vm => vm.Type, map =>
                map.MapFrom(s => ((ScheduleType)s.Type).ToString()))
            .ForMember(vm => vm.Statuses, map =>
                map.UseValue(Enum.GetNames(typeof(ScheduleStatus)).ToArray()))
            .ForMember(vm => vm.Types, map =>
                map.UseValue(Enum.GetNames(typeof(ScheduleType)).ToArray()));
 
        Mapper.CreateMap<User, UserViewModel>()
            .ForMember(vm => vm.SchedulesCreated,
                map => map.MapFrom(u => u.SchedulesCreated.Count()));
    }
}
AutoMapperConfiguration.cs
1
2
3
4
5
6
7
8
9
10
public class AutoMapperConfiguration
{
    public static void Configure()
    {
        Mapper.Initialize(x =>
        {
            x.AddProfile<DomainToViewModelMappingProfile>();
        });
    }
}

Add a new folder named Core at the root of the API application and create a helper class for supporting pagination in our SPA.

PaginationHeader.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PaginationHeader
{
    public int CurrentPage {get;set; }
    public int ItemsPerPage {get;set; }
    public int TotalItems {get;set; }
    public int TotalPages {get;set; }
 
    public PaginationHeader(int currentPage,int itemsPerPage,int totalItems,int totalPages)
    {
        this.CurrentPage = currentPage;
        this.ItemsPerPage = itemsPerPage;
        this.TotalItems = totalItems;
        this.TotalPages = totalPages;
    }
}

I decided on this app to encapsulate pagination information in the request/response header and only. If the client wants to retrieve the 5 schedules of the second page, the request must have a “Pagination” header equal to “2,5”. All the required information the client needs to build a pagination bar will be contained inside a corresponding response header. The same applies for custom error messages that the server returns to the client e.g. if an exception occurs.. through the global exception handler. Add an Extensions class inside the Core folder to support the previous functionalities.

Extensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static class Extensions
{
    /// <summary>
    /// Extension method to add pagination info to Response headers
    /// </summary>
    /// <param name="response"></param>
    /// <param name="currentPage"></param>
    /// <param name="itemsPerPage"></param>
    /// <param name="totalItems"></param>
    /// <param name="totalPages"></param>
    public static void AddPagination(this HttpResponse response,int currentPage,int itemsPerPage,int totalItems,int totalPages)
    {
        var paginationHeader =new PaginationHeader(currentPage, itemsPerPage, totalItems, totalPages);
 
        response.Headers.Add("Pagination",
            Newtonsoft.Json.JsonConvert.SerializeObject(paginationHeader));
        // CORS
        response.Headers.Add("access-control-expose-headers","Pagination");
    }
 
    public static void AddApplicationError(this HttpResponse response,string message)
    {
        response.Headers.Add("Application-Error", message);
        // CORS
        response.Headers.Add("access-control-expose-headers","Application-Error");
    }
}

The SPA that we ‘ll build on the next post will render images too so if you want to follow with me add an images folder inside the wwwroot folder and copy the images from here. The only thing remained is to create the API MVC Controller classes. Add them inside a new folder named Controllers.

SchedulesController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
[Route("api/[controller]")]
public class SchedulesController : Controller
{
    private IScheduleRepository _scheduleRepository;
    private IAttendeeRepository _attendeeRepository;
    private IUserRepository _userRepository;
    int page = 1;
    int pageSize = 4;
    public SchedulesController(IScheduleRepository scheduleRepository,
                                IAttendeeRepository attendeeRepository,
                                IUserRepository userRepository)
    {
        _scheduleRepository = scheduleRepository;
        _attendeeRepository = attendeeRepository;
        _userRepository = userRepository;
    }
 
    public IActionResult Get()
    {
        var pagination = Request.Headers["Pagination"];
 
        if (!string.IsNullOrEmpty(pagination))
        {
            string[] vals = pagination.ToString().Split(',');
            int.TryParse(vals[0],out page);
            int.TryParse(vals[1],out pageSize);
        }
 
        int currentPage = page;
        int currentPageSize = pageSize;
        var totalSchedules = _scheduleRepository.Count();
        var totalPages = (int)Math.Ceiling((double)totalSchedules / pageSize);
 
        IEnumerable<Schedule> _schedules = _scheduleRepository
            .AllIncluding(s => s.Creator, s => s.Attendees)
            .OrderBy(s => s.Id)
            .Skip((currentPage - 1) * currentPageSize)
            .Take(currentPageSize)
            .ToList();
 
        Response.AddPagination(page, pageSize, totalSchedules, totalPages);
 
        IEnumerable<ScheduleViewModel> _schedulesVM = Mapper.Map<IEnumerable<Schedule>, IEnumerable<ScheduleViewModel>>(_schedules);
 
        return new OkObjectResult(_schedulesVM);
    }
 
    [HttpGet("{id}", Name ="GetSchedule")]
    public IActionResult Get(int id)
    {
        Schedule _schedule = _scheduleRepository
            .GetSingle(s => s.Id == id, s => s.Creator, s => s.Attendees);
 
        if (_schedule !=null)
        {
            ScheduleViewModel _scheduleVM = Mapper.Map<Schedule, ScheduleViewModel>(_schedule);
            return new OkObjectResult(_scheduleVM);
        }
        else
        {
            return NotFound();
        }
    }
 
    [HttpGet("{id}/details", Name ="GetScheduleDetails")]
    public IActionResult GetScheduleDetails(int id)
    {
        Schedule _schedule = _scheduleRepository
            .GetSingle(s => s.Id == id, s => s.Creator, s => s.Attendees);
 
        if (_schedule !=null)
        {
 
 
            ScheduleDetailsViewModel _scheduleDetailsVM = Mapper.Map<Schedule, ScheduleDetailsViewModel>(_schedule);
 
            foreach (var attendeein _schedule.Attendees)
            {
                User _userDb = _userRepository.GetSingle(attendee.UserId);
                _scheduleDetailsVM.Attendees.Add(Mapper.Map<User, UserViewModel>(_userDb));
            }
 
 
            return new OkObjectResult(_scheduleDetailsVM);
        }
        else
        {
            return NotFound();
        }
    }
 
    [HttpPost]
    public IActionResult Create([FromBody]ScheduleViewModel schedule)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
 
        Schedule _newSchedule = Mapper.Map<ScheduleViewModel, Schedule>(schedule);
        _newSchedule.DateCreated = DateTime.Now;
 
        _scheduleRepository.Add(_newSchedule);
        _scheduleRepository.Commit();
 
        foreach (var userIdin schedule.Attendees)
        {
            _newSchedule.Attendees.Add(new Attendee { UserId = userId });
        }
        _scheduleRepository.Commit();
 
        schedule = Mapper.Map<Schedule, ScheduleViewModel>(_newSchedule);
 
        CreatedAtRouteResult result = CreatedAtRoute("GetSchedule",new { controller ="Schedules", id = schedule.Id }, schedule);
        return result;
    }
 
    [HttpPut("{id}")]
    public IActionResult Put(int id, [FromBody]ScheduleViewModel schedule)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
 
        Schedule _scheduleDb = _scheduleRepository.GetSingle(id);
 
        if (_scheduleDb ==null)
        {
            return NotFound();
        }
        else
        {
            _scheduleDb.Title = schedule.Title;
            _scheduleDb.Location = schedule.Location;
            _scheduleDb.Description = schedule.Description;
            _scheduleDb.Status = (ScheduleStatus)Enum.Parse(typeof(ScheduleStatus), schedule.Status);
            _scheduleDb.Type = (ScheduleType)Enum.Parse(typeof(ScheduleType), schedule.Type);
            _scheduleDb.TimeStart = schedule.TimeStart;
            _scheduleDb.TimeEnd = schedule.TimeEnd;
 
            // Remove current attendees
            _attendeeRepository.DeleteWhere(a => a.ScheduleId == id);
 
            foreach (var userIdin schedule.Attendees)
            {
                _scheduleDb.Attendees.Add(new Attendee { ScheduleId = id, UserId = userId });
            }
 
            _scheduleRepository.Commit();
        }
 
        schedule = Mapper.Map<Schedule, ScheduleViewModel>(_scheduleDb);
 
        return new NoContentResult();
    }
 
    [HttpDelete("{id}", Name ="RemoveSchedule")]
    public IActionResult Delete(int id)
    {
        Schedule _scheduleDb = _scheduleRepository.GetSingle(id);
 
        if (_scheduleDb ==null)
        {
            return new NotFoundResult();
        }
        else
        {
            _attendeeRepository.DeleteWhere(a => a.ScheduleId == id);
            _scheduleRepository.Delete(_scheduleDb);
 
            _scheduleRepository.Commit();
 
            return new NoContentResult();
        }
    }
 
    [HttpDelete("{id}/removeattendee/{attendee}")]
    public IActionResult Delete(int id,int attendee)
    {
        Schedule _scheduleDb = _scheduleRepository.GetSingle(id);
 
        if (_scheduleDb ==null)
        {
            return new NotFoundResult();
        }
        else
        {
            _attendeeRepository.DeleteWhere(a => a.ScheduleId == id && a.UserId == attendee);
 
            _attendeeRepository.Commit();
 
            return new NoContentResult();
        }
    }
}
UsersController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
[Route("api/[controller]")]
public class UsersController : Controller
{
    private IUserRepository _userRepository;
    private IScheduleRepository _scheduleRepository;
    private IAttendeeRepository _attendeeRepository;
 
    int page = 1;
    int pageSize = 10;
    public UsersController(IUserRepository userRepository,
                            IScheduleRepository scheduleRepository,
                            IAttendeeRepository attendeeRepository)
    {
        _userRepository = userRepository;
        _scheduleRepository = scheduleRepository;
        _attendeeRepository = attendeeRepository;
    }
 
    public IActionResult Get()
    {
        var pagination = Request.Headers["Pagination"];
 
        if (!string.IsNullOrEmpty(pagination))
        {
            string[] vals = pagination.ToString().Split(',');
            int.TryParse(vals[0],out page);
            int.TryParse(vals[1],out pageSize);
        }
 
        int currentPage = page;
        int currentPageSize = pageSize;
        var totalUsers = _userRepository.Count();
        var totalPages = (int)Math.Ceiling((double)totalUsers / pageSize);
 
        IEnumerable<User> _users = _userRepository
            .AllIncluding(u => u.SchedulesCreated)
            .OrderBy(u => u.Id)
            .Skip((currentPage - 1) * currentPageSize)
            .Take(currentPageSize)
            .ToList();
 
        IEnumerable<UserViewModel> _usersVM = Mapper.Map<IEnumerable<User>, IEnumerable<UserViewModel>>(_users);
 
        Response.AddPagination(page, pageSize, totalUsers, totalPages);
 
        return new OkObjectResult(_usersVM);
    }
 
    [HttpGet("{id}", Name ="GetUser")]
    public IActionResult Get(int id)
    {
        User _user = _userRepository.GetSingle(u => u.Id == id, u => u.SchedulesCreated);
 
        if (_user !=null)
        {
            UserViewModel _userVM = Mapper.Map<User, UserViewModel>(_user);
            return new OkObjectResult(_userVM);
        }
        else
        {
            return NotFound();
        }
    }
 
    [HttpGet("{id}/schedules", Name ="GetUserSchedules")]
    public IActionResult GetSchedules(int id)
    {
        IEnumerable<Schedule> _userSchedules = _scheduleRepository.FindBy(s => s.CreatorId == id);
 
        if (_userSchedules !=null)
        {
            IEnumerable<ScheduleViewModel> _userSchedulesVM = Mapper.Map<IEnumerable<Schedule>, IEnumerable<ScheduleViewModel>>(_userSchedules);
            return new OkObjectResult(_userSchedulesVM);
        }
        else
        {
            return NotFound();
        }
    }
 
    [HttpPost]
    public IActionResult Create([FromBody]UserViewModel user)
    {
 
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
 
        User _newUser =new User { Name = user.Name, Profession = user.Profession, Avatar = user.Avatar };
 
        _userRepository.Add(_newUser);
        _userRepository.Commit();
 
        user = Mapper.Map<User, UserViewModel>(_newUser);
 
        CreatedAtRouteResult result = CreatedAtRoute("GetUser",new { controller ="Users", id = user.Id }, user);
        return result;
    }
 
    [HttpPut("{id}")]
    public IActionResult Put(int id, [FromBody]UserViewModel user)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
 
        User _userDb = _userRepository.GetSingle(id);
 
        if (_userDb ==null)
        {
            return NotFound();
        }
        else
        {
            _userDb.Name = user.Name;
            _userDb.Profession = user.Profession;
            _userDb.Avatar = user.Avatar;
            _userRepository.Commit();
        }
 
        user = Mapper.Map<User, UserViewModel>(_userDb);
 
        return new NoContentResult();
    }
 
    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        User _userDb = _userRepository.GetSingle(id);
 
        if (_userDb ==null)
        {
            return new NotFoundResult();
        }
        else
        {
            IEnumerable<Attendee> _attendees = _attendeeRepository.FindBy(a => a.UserId == id);
            IEnumerable<Schedule> _schedules = _scheduleRepository.FindBy(s => s.CreatorId == id);
 
            foreach (var attendeein _attendees)
            {
                _attendeeRepository.Delete(attendee);
            }
 
            foreach (var schedulein _schedules)
            {
                _attendeeRepository.DeleteWhere(a => a.ScheduleId == schedule.Id);
                _scheduleRepository.Delete(schedule);
            }
 
            _userRepository.Delete(_userDb);
 
            _userRepository.Commit();
 
            return new NoContentResult();
        }
    }
 
}

At this point your application should compile without any errors. Before testing the API with HTTP requests we need to initialize the database. In order to accomplish this add migrations with the following command.

add migrations
1
dotnet ef migrations add"initial"

For this command to run successfully you have two options. Either open a terminal/cmd, and navigate to the root of the Scheduler.API project or open Package Manager Console in Visual Studio. In case you choose the latter, you still need to navigate at the root of the API project by typing cd path_to_scheduler_api first..
Next run the command that creates the database.

add migrations
1
dotnet ef database update

Testing the API

Fire the Web application either through Visual Studio or running dotnet run command from a command line. The database initializer we wrote before will init some mock data in the SchedulerDb database. Sending a simple GET request to http://localhost:your_port/api/users will fetch the first 6 users (if no pagination header the 10 is the pageSize). The response will also contain information for pagination.

You can request the first two schedules by sending a request to http://localhost:your_port/api/schedules with a “Pagination” header equal to 1,2.

Two of the most important features our API has are the validation and error messages returned. This way, the client can display related messages to the user. Let’s try to create a user with an empty name by sending a POST request to api/users.

As you can see the controller returned the ModelState errors in the body of the request. I will cause an exception intentionally in order to check the error returned from the API in the response header. The global exception handler will catch the exception and add the error message in the configured header.

Conclusion

We have finally finished building an API using ASP.NET Core and Entity Framework Core. We separated models, data repositories and API in different .NET Core projects that are able to run outside of IIS and on different platforms. Keep in mind that this project will be used as the backend infrastructure of an interesting SPA built with the latest Angular version. We will build the SPA in the next post so stay tuned!

Source Code: You can find the source code for this project here where you will also find instructions on how to run the application in or outside Visual Studio.

In case you find my blog’s content interesting, register your email to receive notifications of new posts and follow chsakell’s Blog on its Facebook or Twitter accounts.

LINK: https://chsakell.com/2016/06/23/rest-apis-using-asp-net-core-and-entity-framework-core/

EF Core for Enterprise

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

Introduction

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

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

Background

The architecture for enterprise application should have the following levels:

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

Skills Prerequisites

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

Software Prerequisites

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

Using the Code

Step 01 - Create Database

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

use master
go
create database Store
go

use Store
go

create schema HumanResources
go

create schema Production
go

create schema Sales
go

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Database diagram

This is a simple database, only to demonstrate concepts.

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

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

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

Step 02 - Core Project

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

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

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

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

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

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

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

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

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

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

EntityLayer structure:

Entity layer structure

DataLayer structure:

Data layer structure

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

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

BusinessLayer structure:

Business layer structure

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

Architecture: Big Picture

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

Entity Layer

Order class code:

using System;
using System.Collections.ObjectModel;

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

        public Int32? OrderID { get; set; }

        public Int16? OrderStatusID { get; set; }

        public DateTime? OrderDate { get; set; }

        public Int32? CustomerID { get; set; }

        public Int32? EmployeeID { get; set; }

        public Int32? ShipperID { get; set; }

        public Decimal? Total { get; set; }

        public String Comments { get; set; }

        public String CreationUser { get; set; }

        public DateTime? CreationDateTime { get; set; }

        public String LastUpdateUser { get; set; }

        public DateTime? LastUpdateDateTime { get; set; }

        public virtual OrderStatus OrderStatusFk { get; set; }

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

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

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

Data Layer

StoreDbContext class code:

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

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

		public String ConnectionString { get; }

		public IEntityMapper EntityMapper { get; }

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

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

OrderMap class code:

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

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

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

            entity.HasKey(p > p.OrderID);

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

Repository class code:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            return DbContext.SaveChanges();
        }

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

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

            return DbContext.SaveChangesAsync();
        }
    }
}

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

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

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

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

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

Data Layer: Stored Procedures versus LINQ Queries

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

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

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

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

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

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

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

Business Layer

BusinessObject class code:

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

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

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

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

                    Disposed = true;
                }
            }
        }

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

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

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

SalesBusinessObject class code:

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

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

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

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

            return response;
        }

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

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

            return response;
        }

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

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

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

            return response;
        }

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

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

            return response;
        }

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

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

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

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

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

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

                        await SalesRepository.AddOrderAsync(header);

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

                            await SalesRepository.AddOrderDetailAsync(detail);

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

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

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

                            await ProductionRepository.AddProductInventoryAsync(productInventory);
                        }

                        response.Model = header;

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

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

            return response;
        }

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

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

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

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

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

            return response;
        }
    }
}

Business Layer: Handle Related Aspects To Business

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

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

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

Step 03 - Putting All Code Together

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

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

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

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

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

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

var entityMapper = new StoreEntityMapper() as IEntityMapper;

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

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

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

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

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

Step 04 - Add Unit Tests

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                var details = new List<OrderDetail>();

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

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

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

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

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

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

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

Step 05 - Add Mocks

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

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

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

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

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

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

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

                    var pageSize = 10;
                    var pageNumber = 1;

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

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

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

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

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

                        var details = new List<OrderDetail>();

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

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

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

                            details.Add(detail);
                        }

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

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

                date = date.AddDays(1);
            }
        }

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

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

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

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

Step 06 - Add Web API

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

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

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

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

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

            base.Dispose(disposing);
        }

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

            return response.ToHttpResponse();
        }

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

            return response.ToHttpResponse();
        }

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

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

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

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

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

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

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

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

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

            return response.ToHttpResponse();
        }

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

            return response.ToHttpResponse();
        }

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

            return response.ToHttpResponse();
        }
    }
}

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

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

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

        public IConfigurationRoot Configuration { get; }

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

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

            services.AddScoped<IEntityMapper, StoreEntityMapper>();

            services.AddScoped<IUserInfo, UserInfo>();

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

            services.AddOptions();

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

            services.AddSingleton<IConfiguration>(Configuration);
        }

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

            app.UseMvc();
        }
    }
}

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

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

Step 07 - Add Unit Tests for Web API

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

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

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

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

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

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

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

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

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

                Assert.False(value.DidError);
            }
        }

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

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

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

                Assert.False(value.DidError);
            }
        }

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

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

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

                Assert.False(value.DidError);
            }
        }

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

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

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

                Assert.False(value.DidError);
            }
        }

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

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

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

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

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

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

Code Improvements

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

Points of Interest

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

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