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

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

| Architecture - .Net 5.0

ยท

10 min read

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 officially released in the month of 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!

  • AutoMapper.Extensions.Microsoft.DependencyInjection

  • Microsoft.AspNetCore.Authentication.JwtBearer

  • Microsoft.AspNetCore.Mvc.Versioning

  • Microsoft.EntityFrameworkCore

  • Microsoft.EntityFrameworkCore.Design

  • Microsoft.EntityFrameworkCore.Relational

  • Microsoft.EntityFrameworkCore.SqlServer

  • Microsoft.EntityFrameworkCore.Tools

  • Newtonsoft.Json

  • Swashbuckle.AspNetCore

  • Swashbuckle.AspNetCore.Newtonsoft

Step 1

Create a Project in Visual Studio

C1.png

Step 2

C2.png

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 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 a 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 which 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

C3.png

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 as Services in which we are maintaining all the Business logic and Core Functionality.

Folder Structure

C4.png

Mapper : Automapper โ€” Obeject โ€” 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 System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using DataAccessLayer.Models;
using System.Linq;
using static Services.ViewModels.CommonModel;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Configuration;

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

        #region Create User
        /// <summary>
        /// Create user to Db
        /// </summary>
        /// <param name="userModel"></param>
        /// <returns></returns>
        public async Task<string> CreateUserAsync(UserModel userModel)
        {
            try
            {
                var userExists = await _cFCDbContext.users.Where(c => c.Email.Equals(userModel.Email)).AnyAsync();
                if (userExists is false)
                {
                    User res = new User
                    {
                        FirstName = userModel.FirstName,
                        LastName = userModel.LastName,
                        PhoneNumber = userModel.PhoneNumber,
                        Password = userModel.Password,
                        Email = userModel.Email,
                        RoleId = await _cFCDbContext.userRoles.Where(c => c.IsActive.Equals(true) && c.RoleName.Equals(userModel.RoleName)).Select(x => x.RoleId).FirstOrDefaultAsync(),
                        IsActive = true,
                        CreatedDate = DateTime.UtcNow,
                        ModifiedDate = DateTime.UtcNow
                    };
                    await _cFCDbContext.users.AddAsync(res);
                    await _cFCDbContext.SaveChangesAsync();
                    return "User Created Success";
                }
                else return "Email already exists";

            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
        #endregion

        #region User Login
        public async Task<bool> UserLoginAsync(LoginModel loginModel)
        {
            try
            {
                var response = await _cFCDbContext.users.Where(c => c.Email.Equals(loginModel.UserName) && c.Password.Equals(loginModel.Password) && c.IsActive.Equals(true)).AnyAsync();
                if (response is true) return true; else return false;
            }
            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 services, I have separated everything with regions

Startup.cs

using AutoMapper;  
using CFC_API.Versioning;  
using DataAccessLayer.ApplicationDbContext;  
using Services.RepositoryPattern.UserLogin;  
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.Extensions.Logging;  
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 connections strings, 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  
    }  
}

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

Git Hub Repo

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

bmc-button.png

Keep learning!!!