πŸ’  Clean Architecture End To End In .NET 5

Introduction

Hello everyone, in this article we are going to cover clean architecture with end-to-end support in ASP.NET 5.0. As we all know, its newly launched Framework was officially released in November. Here I am sharing the link to install the SDK for .NET 5

What we are going to cover in this .NET 5 Clean Architecture?

  1. Entity Framework Code First Approach
  2. Dependency Injection
  3. Automapper
  4. JWT Authentication
  5. Versioning of API's
  6. Swagger (Versioning)

Packages used in this Project!

  1. AutoMapper.Extensions.Microsoft.DependencyInjection
  2. Microsoft.AspNetCore.Authentication.JwtBearer
  3. Microsoft.AspNetCore.Mvc.Versioning
  4. Microsoft.EntityFrameworkCore
  5. Microsoft.EntityFrameworkCore.Design
  6. Microsoft.EntityFrameworkCore.Relational
  7. Microsoft.EntityFrameworkCore.SqlServer
  8. Microsoft.EntityFrameworkCore.Tools
  9. Newtonsoft.Json
  10. Swashbuckle.AspNetCore
  11. Swashbuckle.AspNetCore.Newtonsoft

Step 1. Create a Project in Visual Studio.

Create a new project

Step 2

ASP dot NET project

Make Sure to Select the ASP.NET Core 5.0 and enabling the OpenAPI support helps to add the swagger by default in our project without installing it manually again.

Step 3. Entity Framework Code First Approach

Create a Class Library (.NET Core) named DataAccessLayer, which contains:

ApplicationDbContext

Wrapping all the classes using conventions.

using DataAccessLayer.EntityMappers;
using DataAccessLayer.Models;
using DataAccessLayer.SeedData;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace DataAccessLayer.ApplicationDbContext
{
    public partial class CFCDbContext : DbContext
    {
        public CFCDbContext(DbContextOptions options) : base(options)
        {
        }
        public DbSet<User> users { get; set; }
        public DbSet<UserRoles> userRoles { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new UserMap());
            modelBuilder.ApplyConfiguration(new UserRoleMap());
            modelBuilder.ApplyConfiguration(new BranchMap());
            base.OnModelCreating(modelBuilder);
            modelBuilder.Seed();
        }
    }
}

Entity mappers

Creating Tables with relations using Model Objects

using DataAccessLayer.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace DataAccessLayer.EntityMappers
{
    public class BranchMap : IEntityTypeConfiguration<Branches>
    {
        public void Configure(EntityTypeBuilder<Branches> builder)
        {
            builder.ToTable("branches");
            builder.HasKey(x => x.BranchId)
                   .HasName("pk_branch_id");
            builder.Property(x => x.BranchId)
                   .ValueGeneratedOnAdd()
                   .HasColumnName("branch_id")
                   .HasColumnType("INT");
            builder.Property(x => x.BranchName)
                   .HasColumnName("branch_name")
                   .HasColumnType("NVARCHAR(100)")
                   .IsRequired();
            builder.Property(x => x.BranchManager)
                   .HasColumnName("branch_manager")
                   .HasColumnType("NVARCHAR(100)")
                   .IsRequired();
            builder.Property(x => x.BranchLocation)
                   .HasColumnName("branch_location")
                   .HasColumnType("NVARCHAR(100)")
                   .IsRequired();
            builder.Property(x => x.BranchNumber)
                   .HasColumnName("branch_number")
                   .HasColumnType("BIGINT")
                   .IsRequired();
            builder.Property(x => x.CreatedDate)
                   .HasColumnName("created_date")
                   .HasColumnType("DATETIME");
            builder.Property(x => x.ModifiedDate)
                   .HasColumnName("modified_date")
                   .HasColumnType("DATETIME");
            builder.Property(x => x.IsActive)
                   .HasColumnName("is_active")
                   .HasColumnType("BIT");
        }
    }
}

Migrations

Includes all our Migrations respective to tables that we are consuming

Models

Defining the Table Models Using Classes

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace DataAccessLayer.Models
{
    public class Branches : BaseModel
    {
        [JsonProperty(PropertyName = "branch_id")]
        public int BranchId { get; set; }    
        [JsonProperty(PropertyName = "branch_name")]
        public string BranchName { get; set; }    
        [JsonProperty(PropertyName = "branch_manager")]
        public string BranchManager { get; set; }    
        [JsonProperty(PropertyName = "branch_number")]
        public long BranchNumber { get; set; }    
        [JsonProperty(PropertyName = "branch_location")]
        public string BranchLocation { get; set; }
    }
}

Seed Data

Static Data for Tables

using DataAccessLayer.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace DataAccessLayer.SeedData
{
    public static class ModelBuilderExtension
    {
        public static void Seed(this ModelBuilder modelBuilder)
        {
            // Seed Data for Admin Roles
            modelBuilder.Entity<UserRoles>().HasData(
                new UserRoles { RoleId = 1, RoleName = "SuperAdmin", IsActive = true },
                new UserRoles { RoleId = 2, RoleName = "Admin", IsActive = true }
            );
        }
    }
}

Folder structure

Folder structure

Here I am maintaining the Folder Structure to have a deeper understanding and naming conventions as per my standard.

Dependency injection

Create a Class Library(.Net Core) named Services in which we are maintaining all the Business logic and Core Functionality.

Folder Structure

Folder Structure

Mapper

Automapper - Object - Object Mapping.

using AutoMapper;
using DataAccessLayer.Models;
using System;
using System.Collections.Generic;
using System.Text;
using static Services.ViewModels.CommonModel;
namespace Services.Mapper
{
    public class Mapper : Profile
    {
        public Mapper()
        {
            AllowNullDestinationValues = true;            
            // Source -> Destination
            CreateMap<UserRoles, RolesModel>()
                .ForMember(dto => dto.RoleId, opt => opt.MapFrom(src => src.RoleId))
                .ForMember(dto => dto.RoleName, opt => opt.MapFrom(src => src.RoleName));            
            CreateMap<User, LoginModel>()
                .ForMember(dto => dto.UserName, opt => opt.MapFrom(src => src.Email));
        }
    }
}

Repository Pattern using Interfaces

using DataAccessLayer.ApplicationDbContext;
using DataAccessLayer.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using AutoMapper;
using static Services.ViewModels.CommonModel;
using Microsoft.IdentityModel.Tokens;
namespace Services.RepositoryPattern.UserLogin
{
    public class UserService : IUserService
    {
        #region Property
        private readonly CFCDbContext _cFCDbContext;
        private readonly IMapper _mapper;
        #endregion
        #region Constructor
        public UserService(CFCDbContext cFCDbContext, IMapper mapper)
        {
            _cFCDbContext = cFCDbContext;
            _mapper = mapper;
               
        }
        #endregion
        #region Get User Roles
        /// <summary>
        /// Get User Roles from Db
        /// </summary>
        /// <returns></returns>
        public async Task<List<RolesModel>> GetUserRolesAsync()
        {
            try
            {
                var userRoles = await _cFCDbContext.userRoles.Where(c => c.IsActive.Equals(true)).ToListAsync();
                return _mapper.Map<List<RolesModel>>(userRoles);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
        #endregion
    }
}

Common model

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace Services.ViewModels
{
    public class CommonModel
    {
        public class UserModel
        {
            [JsonProperty(PropertyName = "firstname")]
            [Required]
            public string FirstName { get; set; }
            [JsonProperty(PropertyName = "lastname")]
            [Required]
            public string LastName { get; set; }
            [JsonProperty(PropertyName = "phonenumber")]
            [Required]
            public long PhoneNumber { get; set; }
            [JsonProperty(PropertyName = "password")]
            [Required]
            public string Password { get; set; }
            [JsonProperty(PropertyName = "email")]
            [Required]
            public string Email { get; set; }
            [JsonProperty(PropertyName = "rolename")]
            [Required]
            public string RoleName { get; set; }
        }
        public class RolesModel
        {
            [JsonProperty(PropertyName = "role_id")]
            public int RoleId { get; set; }
            [JsonProperty(PropertyName = "role_name")]
            public string RoleName { get; set; }
        }
        public class LoginModel
        {
            [JsonProperty(PropertyName = "username")]
            [Required]
            public string UserName { get; set; }
            [JsonProperty(PropertyName = "password")]
            [Required]
            public string Password { get; set; }
        }
    }
}

JWT Authentication, Swagger & Versioning

I followed C# Regions to improve the code readability, so in this configure service, I have separated everything with regions

Startup.cs

using AutoMapper;
using CFC_API.Versioning;
using DataAccessLayer.ApplicationDbContext;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Services.RepositoryPattern.UserLogin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace CleanArchitecture
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            #region API Versioning
            services.AddApiVersioning(options =>
            {
                options.ReportApiVersions = true;
                options.DefaultApiVersion = new ApiVersion(1, 0);
                options.AssumeDefaultVersionWhenUnspecified = true;
                options.ApiVersionReader =
                  new HeaderApiVersionReader("X-API-Version");
            });
            #endregion
            #region Connection String
            services.AddDbContext<CFCDbContext>(item => item.UseSqlServer(Configuration.GetConnectionString("myconn")));
            #endregion
            #region Enable Cors
            services.AddCors();
            #endregion
            #region Swagger
            services.AddSwaggerGen(swagger =>
            {
                swagger.SwaggerDoc("v1", new OpenApiInfo
                {
                    Version = "v1",
                    Title = " Clean Architecture v1 API's",
                    Description = $"Clean Architecture API's for integration with UI \r\n\r\n Β© Copyright {DateTime.Now.Year} JK. All rights reserved."
                });
                swagger.SwaggerDoc("v2", new OpenApiInfo
                {
                    Version = "v2",
                    Title = "Clean Architecture v2 API's",
                    Description = $"Clean Architecture API's for integration with UI \r\n\r\n Β© Copyright {DateTime.Now.Year} JK. All rights reserved."
                });
                swagger.ResolveConflictingActions(a => a.First());
                swagger.OperationFilter<RemoveVersionFromParameterv>();
                swagger.DocumentFilter<ReplaceVersionWithExactValueInPath>();
                #region Enable Authorization using Swagger (JWT)
                swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
                {
                    Name = "Authorization",
                    Type = SecuritySchemeType.ApiKey,
                    Scheme = "Bearer",
                    BearerFormat = "JWT",
                    In = ParameterLocation.Header,
                    Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"",
                });
                swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                          new OpenApiSecurityScheme
                            {
                                Reference = new OpenApiReference
                                {
                                    Type = ReferenceType.SecurityScheme,
                                    Id = "Bearer"
                                }
                            },
                            new string[] {}
                    }
                });
                #endregion
            });
            #endregion
            #region Swagger Json property Support
            services.AddSwaggerGenNewtonsoftSupport();
            #endregion
            #region JWT
            // Adding Authentication    
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            // Adding Jwt Bearer    
            .AddJwtBearer(options =>
            {
                options.SaveToken = true;
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidAudience = Configuration["Jwt:Issuer"],
                    ValidIssuer = Configuration["Jwt:Issuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
                };
            });
            #endregion
            #region Dependency Injection
            services.AddTransient<IUserService, UserService>();
            #endregion
            #region Automapper
            services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
            #endregion
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c =>
                {
                    c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
                    c.SwaggerEndpoint("/swagger/v2/swagger.json", "API v2");
                });
            }
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
            #region Global Cors Policy
            app.UseCors(x => x
                .AllowAnyMethod()
                .AllowAnyHeader()
                .SetIsOriginAllowed(origin => true) // allow any origin
                .AllowCredentials()); // allow credentials
            #endregion
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Key": "BB698DAF-6E3F-45FF-8493-06ECCF2F60D0",
    "Issuer": "https://localhost:44393"
  },
  "ConnectionStrings": {
    "myconn": "server= Your Connection String; database=CFCDb;Trusted_Connection=True;"
  }
}

Used to store configuration settings such as database connection strings, and any application scope global variables.

UserController

using Services.RepositoryPattern.UserLogin;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using static Services.ViewModels.CommonModel;
namespace CleanArchitecture.Controllers
{
    [ApiVersion("1.0")]
    [ApiExplorerSettings(GroupName = "v1")]
    public class UserController : BaseController
    {
        #region Property
        private readonly IUserService _userService;
        private readonly IConfiguration _configuration;
        #endregion
        #region Constructor
        public UserController(IUserService userService, IConfiguration configuration)
        {
            _userService = userService;
            _configuration = configuration;
        }
        #endregion
        #region Create User
        /// <summary>
        /// To Create a User
        /// </summary>
        /// <param name="userModel"></param>
        /// <returns></returns>
        [HttpPost(nameof(CreateUser))]
        public async Task<IActionResult> CreateUser([FromBody] UserModel userModel)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    var result = await _userService.CreateUserAsync(userModel);
                    return Ok(result);
                }
                else
                {
                    return BadRequest("Please fill all the required parameters");
                }
            }
            catch (Exception ex)
            {
                return BadRequest(ex);
                throw;
            }
        }
        #endregion
        #region User Login
        /// <summary>
        /// Login Authentication
        /// </summary>
        /// <param name="loginModel"></param>
        /// <returns></returns>
        [HttpPost(nameof(Login)), AllowAnonymous]
        public async Task<IActionResult> Login([FromBody] LoginModel loginModel)
        {
            try
            {
                var response = await _userService.UserLoginAsync(loginModel);
                if (response is true)
                {
                    var userRoles = await _userService.GetUserRolesAsync();
                    var authClaims = new List<Claim>
                    {
                        new Claim(ClaimTypes.Name, loginModel.UserName),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    };
                    foreach (var userRole in userRoles)
                    {
                        authClaims.Add(new Claim(ClaimTypes.Role, userRole.RoleName));
                    }
                    var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
                    var token = new JwtSecurityToken(
                        issuer: _configuration["Jwt:Issuer"],
                        audience: _configuration["Jwt:Issuer"],
                        expires: DateTime.Now.AddHours(3),
                        claims: authClaims,
                        signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                        );
                    return Ok(new
                    {
                        token = new JwtSecurityTokenHandler().WriteToken(token),
                        expiration = token.ValidTo
                    });
                }
                return Unauthorized();
            }
            catch (Exception ex)
            {
                return BadRequest(ex);
                throw;
            }
        }
        #endregion
    }
}

BaseController

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using static CleanArchitecture.ViewModels.Common.ResultModel;
namespace CleanArchitecture.Controllers
{
    [Route("api/v{version:apiversion}/[controller]")]
    [Authorize(AuthenticationSchemes = Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)]
    public class BaseController : ControllerBase
    {
        #region Protected Members
        /// <summary>
        /// Detailed Exception
        /// </summary>
        /// <param name="ex"></param>
        /// <returns></returns>
        protected object DetailedException(Exception ex)
        {
            var errormessage = ex.Message;
            if (ex.InnerException != null)
            {
                errormessage = "\n\nException: " + GetInnerException(ex);
            }
            var result = new Result
            {
                status = new Status
                {
                    code = (int)HttpStatusCode.InternalServerError,
                    message = errormessage
                }
            };
            return result;
        }
        /// <summary>
        /// Get Inner Exception
        /// </summary>
        /// <param name="ex"></param>
        /// <returns></returns>
        private string GetInnerException(Exception ex)
        {
            if (ex.InnerException != null)
            {
                return $"{ex.InnerException.Message + "( \n " + ex.Message + " \n )"} > {GetInnerException(ex.InnerException)} ";
            }
            return string.Empty;
        }
        #endregion
    }
}

RolesController

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Services.RepositoryPattern.UserLogin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace CleanArchitecture.Controllers
{
    [ApiVersion("2.0")]
    [ApiExplorerSettings(GroupName = "v2")]
    public class RoleController : BaseController
    {
        #region Property
        private readonly IUserService _userService;
        #endregion
        #region Constructor
        public RoleController(IUserService userService)
        {
            _userService = userService;
        }
        #endregion
        #region GetRoles
        /// <summary>
        /// Get the User Roles   
        /// </summary>
        /// <returns></returns>
        [HttpGet(nameof(GetUserRoles))]
        public async Task<IActionResult> GetUserRoles()
        {
            try
            {
                var result = await _userService.GetUserRolesAsync();
                if (result is not null) return Ok(result); else return BadRequest("No Data Found");
            }
            catch (Exception ex)
            {
                return BadRequest(ex);
                throw;
            }
        }
        #endregion
    }
}

I have set up everything in the BaseController and invoked this base to all the controllers to authorize and for routing. Below is the GitHub link to clone the project.

Github

This is the entire end-to-end clean architecture with the latest .NET 5, I hope this article helps you.

Keep learning!!!