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?
- Entity Framework Code First Approach
- Dependency Injection
- Automapper
- JWT Authentication
- Versioning of API's
- 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.
Step 2
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
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
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!!!