Share via


Onion Architecture In ASP.NET Core MVC


Introduction

The Onion Architecture term was coined by Jeffrey Palermo in 2008. This architecture provides a better way to build applications for better testability, maintainability, and dependability on the infrastructures like databases and services. This architecture's main aim is to address the challenges faced with 3-tier architecture or n-tier architecture, and to provide a solution for common problems, like coupling and separation of concerns. There are two types of coupling - tight coupling and loose coupling.

Tight Coupling

When a class is dependent on a concrete dependency, it is said to be tightly coupled to that class. A tightly coupled object is dependent on another object; that means changing one object in a tightly coupled application, often requires changes to a number of other objects. It is not difficult when an application is small but in an enterprise level application, it is too difficult to make the changes.

Loose Coupling

It means two objects are independent and an object can use another object without being dependent on it. It is a design goal that seeks to reduce the inter-dependencies among components of a system with the goal of reducing the risk that changes in one component will require changes in any other component.


Advantages of Onion Architecture

There are several advantages of the Onion Architecture, as listed below.

  1. It provides better maintainability as all the codes depend on layers or the center.
  2. It provides better testability as the unit test can be created for separate layers without an effect of other modules of the application.
  3. It develops a loosely coupled application as the outer layer of the application always communicates with inner layer via interfaces.
  4. Any concrete implantation would be provided to the application at run time
  5. Domain entities are core and center part. It can have access to both database and UI layers.
  6. The internal layers never depend on external layer. The code that may have changed should be part of an external layer.

Why Onion Architecture

There are several traditional architectures, like 3-tier architecture and n-tier architecture, all having their own pros and cons. All these traditional architectures have some fundamental issues, such as - tight coupling and separation of concerns. The Model-View-Controller is the most commonly used web application architecture, these days. It solves the problem of separation of concern as there is a separation between UI, business logic, and data access logic. The View is used to design the user interface. The Model is used to pass the data between View and Controller on which the business logic performs any operations. The Controller is used to handle the web request by action methods and returns View accordingly. Hence, it solves the problem of separation of concern while the Controller is still used to database access logic. In essence, MVC solves the separation of concern issue but the tight coupling issue still remains.

On the other hand, Onion Architecture addresses both the separation of concern and tight coupling issues. The overall philosophy of the Onion Architecture is to keep the business logic, data access logic, and model in the middle of the application and push the dependencies as far outward as possible means all coupling towards to center.


Onion Architecture Layers

This architecture relies heavily on the Dependency Inversion Principle. The UI communicates to business logic through interfaces. It has four layers, as shown in figure 1.

  1. Domain Entities Layer
  2. Repository Layer
  3. Service Layer
  4. UI (Web/Unit Test) Layer

Figure 1: Onion Architecture Layers

These layers are towards to center. The center part is the Domain entities which represent the business and behavior objects. These layers can vary but the domain entities layer is always part of the center. The other layer defines more behavior of an object. Let’s see each layer one by one.

Domain Entities Layer

It is the center part of the architecture. It holds all application domain objects. If an application is developed with ORM entity framework then this layer holds POCO classes (Code First) or Edmx (Database First) with entities. These domain entities don't have any dependencies.

Repository Layer

The layer is intended to create an Abstraction layer between the Domain entities layer and Business Logic layer of an application. It is a data access pattern that prompts a more loosely coupled approach to data access. We create a generic repository, which queries the data source for the data, maps the data from the data source to a business entity and persists changes in the business entity to the data source.

Service Layer

The layer holds interfaces which are used to communicate between the UI layer and repository layer. It holds business logic for an entity so it’s called business logic layer as well.

UI Layer

It’s the most external layer. It could be the web application, Web API or Unit Test project. This layer has an implementation of the Dependency Inversion Principle so that application builds a loosely coupled application. It communicates to internal layer via interfaces.


Onion Architecture Project Structure

To implement the Onion architecture, we develop an ASP.NET Core application. This application performs CRUD operations on entities. The application holds four projects as per figure 2. Each project represents a layer in onion architecture.

Figure 2: Application projects structure

There are four projects in which three are class library projects and one is web application project. Let’s see each project mapping with onion architecture layers.

OA.Data

It is a class library project. It holds POCO classes along with configuration classes. It represents the Domain Entities layer of the onion architecture. These classes are used to create database tables. It’s a core and central part of the application.

OA.Repo

It is a second class library project. It holds generic repository class with its interface implementation. It also holds DbContext class. The Entity Framework Code First data access approach needs to create a data access context class that inherits from the DbContext class. This project represents the Repository layer of the onion architecture.

OA.Service

It is a third class library project. It holds business logic and interfaces. These interfaces communicate between UI and data access logic. As it communicates via interfaces, it builds applications that are loosely coupled. This project represents the Service layer of the onion architecture.

OA.Web

It is an ASP.NET Core Web application in this sample but it could be Unit Test or Web API project. It is the most external part of an application by which the end user can interact with the application. It builds loosely coupled applications with in-built dependency injection in ASP.NET Core. It represents the UI layer of the onion architecture.


Implement Onion Architecture

To implement the Onion Architecture in the ASP.NET Core application, create four projects as described in the above section. These four projects represent four layers of the onion architecture. Let’s see each one by one.

Domain Entities Layer

The Entities Domain layer is a core and central part of the architecture. So first, we create "OA.Data" project to implement this layer. This project holds POCO class and fluent API configuration for this POCO classes.

There is an unsupported issue of EF Core 1.0.0-preview2-final with "NETStandard.Library": "1.6.0". Thus, we have changed the target framework to netstandard1.6 > netcoreapp1.0. We modify the project.json file of OA.Data project to implement the Entity Framework Core in this class library project. Thus, the code snippet, mentioned below, is used for the project.json file after modification.

{
  "dependencies": {
    "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
    "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
  },
  "frameworks": {
    "netcoreapp1.0": {
      "imports": [ "dotnet5.6",  "portable-net45+win8"  ]
    }
  },
  "tools": {
    "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
  },
  "version": "1.0.0-*"
}

This Application uses the Entity Framework Code First approach, so the project OA.Data contains entities that are required in the application's database. The OA.Data project holds three entities, one is the BaseEntity class that has common properties that will be inherited by each entity. The code snippet, mentioned below is the BaseEntity class.

using System;
 
namespace OA.Data
{
    public class  BaseEntity
    {
        public Int64 Id { get; set; }
        public DateTime AddedDate { get; set; }
        public DateTime ModifiedDate { get; set; }
        public string  IPAddress { get; set; }
    }
}

There are two more entities, one is User and the another one is UserProfile. Both entities have a one to one relationship, as shown below.

Figure 3: One to One User-UserProfile relationship

Now, we create an User entity, which is inherited from BaseEntity class. The code snippet, mentioned below is for the User entity.

namespace OA.Data
{
    public class  User:BaseEntity
    {
        public string  UserName { get; set; }
        public string  Email { get; set; }
        public string  Password { get; set; }
        public virtual  UserProfile UserProfile { get; set; }
    }
}

Now, we define the configuration for the User entity that will be used when the database table will be created by the entity. The following is a code snippet for the User mapping entity (UserMap.cs).

using Microsoft.EntityFrameworkCore.Metadata.Builders;
 
namespace OA.Data
{
    public class  UserMap
    {
        public UserMap(EntityTypeBuilder<User> entityBuilder)
        {
            entityBuilder.HasKey(t => t.Id);
            entityBuilder.Property(t => t.Email).IsRequired();
            entityBuilder.Property(t => t.Password).IsRequired();
            entityBuilder.Property(t => t.Email).IsRequired();
            entityBuilder.HasOne(t => t.UserProfile).WithOne(u => u.User).HasForeignKey<UserProfile>(x => x.Id);
        }
    }
}

Now, we create a UserProfile entity, which inherits from the BaseEntity class. The code snippet, mentioned below is the UserProfile entity.

namespace OA.Data
{
    public class  UserProfile:BaseEntity
    {
        public string  FirstName { get; set; }
        public string  LastName { get; set; }
        public string  Address { get; set; }
        public virtual  User User { get; set; }
    }
}

Now, we define the configuration for the UserProfile entity that will be used when the database table will be created by the entity. The code snippet is mentioned below for the UserProfile mapping entity (UserProfileMap.cs).

using Microsoft.EntityFrameworkCore.Metadata.Builders;
 
namespace OA.Data
{
    public class  UserProfileMap
    {
        public UserProfileMap(EntityTypeBuilder<UserProfile> entityBuilder)
        {
            entityBuilder.HasKey(t => t.Id);
            entityBuilder.Property(t => t.FirstName).IsRequired();
            entityBuilder.Property(t => t.LastName).IsRequired();
            entityBuilder.Property(t => t.Address);  
        }
    }
}

Repository Layer

Now we create a second layer of the onion architecture which is repository layer. To build this layer, we create one more class library project named OA.Repo. This project holds both the repository and data context classes.

The OA.Repo project contains DataContext. The ADO.NET Entity Framework Code First data access approach needs to create a data access context class that inherits from the DbContext class, so we create a context class ApplicationContext (ApplicationContext.cs) class.

In this class, we override the OnModelCreating() method. This method is called when the model for a context class (ApplicationContext) 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;
using OA.Data;
 
namespace OA.Repo
{
    public class  ApplicationContext : DbContext
    {
        public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)
        {
        }
        protected override  void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            new UserMap(modelBuilder.Entity<User>());
            new UserProfileMap(modelBuilder.Entity<UserProfile>());
        }
    }
}

The DbContext must have an instance of DbContextOptions in order to execute. We will use dependency injection, so we pass options via constructor dependency injection. ASP.NET Core is designed from the ground to support and leverage dependency injection. Thus, we create generic repository interface for the entity operations, so that we can develop loosely coupled application. The code snippet, mentioned below is the IRepository interface.

using OA.Data;
using System.Collections.Generic;
 
namespace OA.Repo
{
    public interface  IRepository<T> where T : BaseEntity
    {
        IEnumerable<T> GetAll();
        T Get(long id);
        void Insert(T entity);
        void Update(T entity);
        void Delete(T entity);
        void Remove(T entity);
        void SaveChanges();
    }
}

Now, let's create a repository class to perform database operations on the entity, which implements IRepository. This repository contains a parameterized constructor with a parameter as Context, so when we create an instance of the repository, we pass a context so that the entity has the same context. The code snippet is mentioned below for the Repository class under OA.Repo project.

using Microsoft.EntityFrameworkCore;
using OA.Data;
using System;
using System.Collections.Generic;
using System.Linq;
 
namespace OA.Repo
{
    public class  Repository<T> : IRepository<T> where T : BaseEntity
    {
        private readonly  ApplicationContext context;
        private DbSet<T> entities;
        string errorMessage = string.Empty;
 
        public Repository(ApplicationContext context)
        {
            this.context = context;
            entities = context.Set<T>();
        }
        public IEnumerable<T> GetAll()
        {
            return entities.AsEnumerable();
        }
 
        public T Get(long id)
        {
            return entities.SingleOrDefault(s => s.Id == id);
        }
        public void  Insert(T entity)
        {
            if (entity == null)
            {
                throw new  ArgumentNullException("entity");
            }
            entities.Add(entity);
            context.SaveChanges();
        }
 
        public void  Update(T entity)
        {
            if (entity == null)
            {
                throw new  ArgumentNullException("entity");
            }
            context.SaveChanges();
        }
 
        public void  Delete(T entity)
        {
            if (entity == null)
            {
                throw new  ArgumentNullException("entity");
            }
            entities.Remove(entity);
            context.SaveChanges();
        }
        public void  Remove(T entity)
        {
            if (entity == null)
            {
                throw new  ArgumentNullException("entity");
            }
            entities.Remove(entity);            
        }
 
        public void  SaveChanges()
        {
            context.SaveChanges();
        }
    }
}

We developed entity and context which are required to create a database but we will come back to this after creating the two more projects.

Service Layer

Now we create the third layer of the onion architecture which is service layer. To build this layer, we create one more class library project named OA.Service. This project holds interfaces and classes which have an implementation of interfaces. This layer is intended to build loosely coupled applications. This layer communicates with both Web applications and repository projects.

We create an interface named IUserService. This interface holds all methods signature which access by external layer for the User entity. The following code snippet is for the same (IUserService.cs).

using OA.Data;
using System.Collections.Generic;
 
namespace OA.Service
{
    public  interface  IUserService
    {
        IEnumerable<User> GetUsers();
        User GetUser(long id);
        void InsertUser(User user);
        void UpdateUser(User user);
        void DeleteUser(long id);
    }
}

Now, this IUserService interface implements on a class named UserService. This UserService class holds all the operations for User entity. The following code snippet is for the same(UserService.cs).

using OA.Data;
using OA.Repo;
using System.Collections.Generic;
 
namespace OA.Service
{
    public class  UserService:IUserService
    {
        private IRepository<User> userRepository;
        private IRepository<UserProfile> userProfileRepository;
 
        public UserService(IRepository<User> userRepository, IRepository<UserProfile> userProfileRepository)
        {
            this.userRepository = userRepository;
            this.userProfileRepository = userProfileRepository;
        }
 
        public IEnumerable<User> GetUsers()
        {
            return userRepository.GetAll();
        }
 
        public User GetUser(long id)
        {
            return userRepository.Get(id);
        }
 
        public void  InsertUser(User user)
        {
            userRepository.Insert(user);
        }
        public void  UpdateUser(User user)
        {
            userRepository.Update(user);
        }
 
        public void  DeleteUser(long  id)
        {            
            UserProfile userProfile = userProfileRepository.Get(id);
            userProfileRepository.Remove(userProfile);
            User user = GetUser(id);
            userRepository.Remove(user);
            userRepository.SaveChanges();
        }
    }
}

We create one more interface named IUserProfileService. This interface holds method signature which is accessed by the external layer for the UserProfile entity. The following code snippet is for the same (IUserProfileService.cs).

using OA.Data;
 
namespace OA.Service
{
    public interface  IUserProfileService
    {
        UserProfile GetUserProfile(long id);
    }
}

Now, this IUserProfileService interface implements on a class named UserProfileService. This UserProfileService class holds the operation for the UserProfile entity. The following code snippet is for the same(UserProfileService.cs).

using OA.Data;
using OA.Repo;
 
namespace OA.Service
{
    public class  UserProfileService: IUserProfileService
    {
        private IRepository<UserProfile> userProfileRepository;
 
        public UserProfileService(IRepository<UserProfile> userProfileRepository)
        {           
            this.userProfileRepository = userProfileRepository;
        }
 
        public UserProfile GetUserProfile(long id)
        {
            return userProfileRepository.Get(id);
        }
    }
}

UI Layer

Now, we create the external layer of the onion architecture which is UI layer. The end user interacts with the application by this layer. To build this layer, we create an ASP.NET Core MVC web application named OA.Web. This layer communicates to service layer projects. This project contains the user interface for both user and user profile entities database operations and the controller to do these operations.

As the concept of dependency injection is central to the ASP.NET Core application, we register context, repository, and service to the dependency injection during the application start up. Thus, we register these as a Service in the ConfigureServices method in the StartUp class as per following code snippet.

public void  ConfigureServices(IServiceCollection services)
     {           
         services.AddMvc();
         services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
         services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
         services.AddTransient<IUserService, UserService>();
         services.AddTransient<IUserProfileService, UserProfileService>();
     }

Here, the DefaultConnection is connection string which defined in appsettings.json file as per following code snippet.

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=DESKTOP-RG33QHE;Initial Catalog=OADb;User ID=sa; Password="
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

Now, we have configured settings to create database, so we have time to create a database, using migration. We must choose the OA.Repo project in the Package Manager console during the performance of the steps, mentioned below.

  1. Tools -> NuGet Package Manager -> Package Manager Console
  2. 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.
  3. 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. We create controller named UserController under the Controllers folder of the application. It has all ActionResult methods for end user interface of operations. We create both IUserService and IUserProfile interface instances; then we inject these in the controller's constructor to get its object. The following is a partial code snippet for the UserController in which service interfaces are injected, using constructor dependency injection.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using OA.Service;
using OA.Web.Models;
using OA.Data;
using Microsoft.AspNetCore.Http;
 
namespace OA.Web.Controllers
{
    public class  UserController : Controller
    {
        private readonly  IUserService userService;
        private readonly  IUserProfileService userProfileService;
 
        public UserController(IUserService userService, IUserProfileService userProfileService)
        {
            this.userService = userService;
            this.userProfileService = userProfileService;
        }
    }
}

We can notice that Controller takes both IUserService and IUserProfileService as a constructor parameters. The ASP.NET Core dependency injection will take care of passing an instance of these services into UserController. The controller is developed to handle operations requests for both User and UserProfile entities. Now, let's develop the user interface for the User Listing, Add User, Edit User and Delete User. Let's see each one by one.

User List View

This is the first view when the application is accessed or the entry point of the application is executed. It shows the author listing as in Figure 4. The user data is displayed in a tabular format and on this view, it has linked to add a new user, edit a user and delete a user.

To pass data from controller to view, create named UserViewModel view model, as per the code snippet, mentioned below. This view model is also used for adding or editing a user.

using Microsoft.AspNetCore.Mvc;
using System;
using System.ComponentModel.DataAnnotations;
 
namespace OA.Web.Models
{
    public class  UserViewModel
    {
        [HiddenInput]
        public Int64 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  Address { get; set; }
        [Display(Name = "User Name")]
        public string  UserName { get; set; }
        public string  Email { get; set; }
        public string  Password { get; set; }
        [Display(Name = "Added Date")]
        public DateTime AddedDate { get; set; }
    }
}

Now, we create action method, which returns an index view with the data. The code snippet of Index action method in UserController is mentioned below.

[HttpGet]
        public IActionResult Index()
        {
            List<UserViewModel> model = new  List<UserViewModel>();
            userService.GetUsers().ToList().ForEach(u =>
            {
                UserProfile userProfile = userProfileService.GetUserProfile(u.Id);
                UserViewModel user = new  UserViewModel
                {
                    Id = u.Id,
                    Name = $"{userProfile.FirstName} {userProfile.LastName}",
                    Email = u.Email,
                    Address = userProfile.Address
                };
                model.Add(user);
            });
 
            return View(model);
        }

Now, we create an index view, as per the code snippet, mentioned below under the User folder of views.

@model IEnumerable<UserViewModel>
@using OA.Web.Models
@using OA.Web.Code
 
<div class="top-buffer"></div>
<div class="panel panel-primary">
    <div class="panel-heading panel-head">Users</div>
    <div class="panel-body">
        <div class="btn-group">
            <a id="createEditUserModal" data-toggle="modal" asp-action="AddUser" data-target="#modal-action-user" class="btn btn-primary">
                <i class="glyphicon glyphicon-plus"></i>  Add User
            </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>Address</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.Address)</td>
                        <td>
                            <a id="editUserModal" data-toggle="modal" asp-action="EditUser" asp-route-id="@item.Id" data-target="#modal-action-user"
                               class="btn btn-info">
                                <i class="glyphicon glyphicon-pencil"></i>  Edit
                            </a>                         
                            <a id="deleteUserModal" data-toggle="modal" asp-action="DeleteUser" asp-route-id="@item.Id" data-target="#modal-action-user" 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-user", AreaLabeledId = "modal-action-user-label", Size = ModalSize.Large })
 
@section scripts
{
    <script src="~/js/user-index.js" asp-append-version="true"></script>
}

It shows all forms in the Bootstrap model popup so create the user - index.js file as per the following code snippet.

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

When the application runs and calls the index() action method from UserController with a HttpGet request, it gets all the users listed in the UI, as shown in Figure 4.

Figure 4: User listing

Add User

To pass the data from UI to a controller to add a user, use same view model named UserViewModel. The AuthorController has an action method named AddUser which returns the view to add a user. The code snippet mentioned below is for same action method for both GET and Post requests.

[HttpGet]
        public ActionResult AddUser()
        {
            UserViewModel model = new  UserViewModel();
 
            return PartialView("_AddUser", model);
        }
 
        [HttpPost]
        public ActionResult AddUser(UserViewModel model)
        {
            User userEntity = new  User
            {
                UserName = model.UserName,
                Email = model.Email,
                Password = model.Password,
                AddedDate = DateTime.UtcNow,
                ModifiedDate = DateTime.UtcNow,
                IPAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString(),
                UserProfile = new  UserProfile
                {
                    FirstName = model.FirstName,
                    LastName = model.LastName,
                    Address = model.Address,
                    AddedDate = DateTime.UtcNow,
                    ModifiedDate = DateTime.UtcNow,
                    IPAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString()
                }
            };
            userService.InsertUser(userEntity);
            if (userEntity.Id > 0)
            {
                return RedirectToAction("index");
            }
            return View(model);
        }

The GET request for the AddUser action method returns _AddUser partial view; the code snippet follows under the User folder of views.

@model UserViewModel
@using OA.Web.Models
 
<form asp-action="AddUser" role="form">
    @await Html.PartialAsync("_ModalHeader", new ModalHeader { Heading = "Add User" })
    <div class="modal-body form-horizontal">
        <div class="row">
            <div class="col-lg-6">
                <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>
            <div class="col-lg-6">
                <div class="form-group">
                    <label asp-for="UserName" class="col-lg-3 col-sm-3 control-label"></label>
                    <div class="col-lg-6">
                        <input asp-for="UserName" class="form-control" />
                    </div>
                </div>
                <div class="form-group">
                    <label asp-for="Password" class="col-lg-3 col-sm-3 control-label"></label>
                    <div class="col-lg-6">
                        <input type="password" asp-for="Password" class="form-control" />
                    </div>
                </div>
                <div class="form-group">
                    <label asp-for="Address" class="col-lg-3 col-sm-3 control-label"></label>
                    <div class="col-lg-6">
                        <input asp-for="Address" class="form-control" />
                    </div>
                </div>
            </div>
        </div>
    </div>
    @await Html.PartialAsync("_ModalFooter", new ModalFooter { })
</form>

When the application runs and you click on the Add User button, it makes a GET request for the AddUser() action; add a user screen, as shown in Figure 5.

Figure 5: Add User screen

Edit User

To pass the data from UI to controller to edit a user, use same view model named UserViewModel. The UserController has an action method named EditUser, which returns view to edit a user. The code snippet mentioned below is for the same action method for both GET and Post requests.

public ActionResult EditUser(int? id)
        {
            UserViewModel model = new  UserViewModel();
            if (id.HasValue && id != 0)
            {
                User userEntity = userService.GetUser(id.Value);
                UserProfile userProfileEntity = userProfileService.GetUserProfile(id.Value);
                model.FirstName = userProfileEntity.FirstName;
                model.LastName = userProfileEntity.LastName;
                model.Address = userProfileEntity.Address;
                model.Email = userEntity.Email;
            }
            return PartialView("_EditUser", model);
        }
 
        [HttpPost]
        public ActionResult EditUser(UserViewModel model)
        {
            User userEntity = userService.GetUser(model.Id);
            userEntity.Email = model.Email;
            userEntity.ModifiedDate = DateTime.UtcNow;
            userEntity.IPAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString();
            UserProfile userProfileEntity = userProfileService.GetUserProfile(model.Id);
            userProfileEntity.FirstName = model.FirstName;
            userProfileEntity.LastName = model.LastName;
            userProfileEntity.Address = model.Address;
            userProfileEntity.ModifiedDate = DateTime.UtcNow;
            userProfileEntity.IPAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString();
            userEntity.UserProfile = userProfileEntity;
            userService.UpdateUser(userEntity);
            if (userEntity.Id > 0)
            {
                return RedirectToAction("index");
            }
            return View(model);
        }

The GET request for the EditUser action method returns _EditUser partial view, where code snippet follows under the User folder of views.

@model UserViewModel
@using OA.Web.Models
 
<form asp-action="EditUser" role="form">
    @await Html.PartialAsync("_ModalHeader", new ModalHeader { Heading = "Edit User" })
    <div class="modal-body form-horizontal">
        <div class="row">            
            <input asp-for="Id" />
                <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="Address" class="col-lg-3 col-sm-3 control-label"></label>
                    <div class="col-lg-6">
                        <input asp-for="Address" class="form-control" />
                    </div>
                </div>
             
        </div>
    </div>
    @await Html.PartialAsync("_ModalFooter", new ModalFooter { })
</form>

When the application runs and you click on the Edit button in the User listing, it makes a GET request for the EditUser() action, then the edit user screen is shown in Figure 6.

Figure 6: Edit User

Delete User

The UserController has an action method named DeleteUser, which returns the view to delete a user. The code snippet mentioned below is for the same action method for both GET and Post requests.

[HttpGet]
        public PartialViewResult DeleteUser(int id)
        {
            UserProfile userProfile = userProfileService.GetUserProfile(id);
            string name = $"{userProfile.FirstName} {userProfile.LastName}";
            return PartialView("_DeleteUser", name);
        }
 
        [HttpPost]
        public ActionResult DeleteUser(long id, FormCollection form)
        {
            userService.DeleteUser(id);          
            return RedirectToAction("Index");
        }

The GET request for the DeleteUser action method returns _DeleteUser partial View. The code snippet mentioned below is under the User folder of Views.

@model string
@using OA.Web.Models
 
<form asp-action="DeleteUser" role="form">
    @Html.Partial("_ModalHeader", new ModalHeader { Heading = "Delete User" })
 
    <div class="modal-body form-horizontal">
        Are you want to delete @Model?
    </div>
    @Html.Partial("_ModalFooter", new ModalFooter { SubmitButtonText = "Delete" })
</form>

When the application runs and a user clicks on the "Delete" button in the user listing, it makes a GET request for the DeleteUser() action, then the delete user screen is shown, as below.

Figure 7: Delete User


Download

You can download the complete source code from the MSDN Sample, using the links, mentioned below.

  1. Rating Star Application in ASP.NET Core
  2. CRUD Operations in ASP.NET Core and Entity Framework Core
  3. Repository Pattern In ASP.NET Core
  4. Generic Repository Pattern in ASP.NET Core
  5. Onion Architecture In ASP.NET Core MVC
  6. ASP.NET Core MVC: Authentication and Role Based Authorisation with Identity
  7. ASP.NET Core MVC: Authentication and Claim Based authorization with Identity

See Also

It's recommended to read more articles related to ASP.NET Core.

  1. ASP.NET Core: Overview
  2. ASP.NET Core With Visual Studio 2017 RC
  3. ASP.NET Core Entity Framework Core Code First: CRUD Operations
  4. Repository Pattern In ASP.NET Core
  5. ASP.NET Core: Generic Repository Pattern
  6. ASP.NET Core MVC: Authentication And Role Based Authorization With ASP.NET Core Identity
  7. ASP.NET Core MVC: Authentication And Claim Based Authorisation With ASP.NET Identity Core
  8. ASP.NET Core : Overview Of Dependency Injection
  9. ASP.NET Core: In-Memory Caching

Conclusion

This article introduced Onion Architecture in ASP.NET Core, using Entity Framework Core with the "code first" development approach. It’s widely accepted architecture these days. We used Bootstrap, CSS, and JavaScript for the user interface design in this application.