How to Build Architecture ASP.NET Core Web API and Angular 11

In this article, we are going to learn building a Clean Architecture application using ASP.NET Core Web API and Angular 11 Front End.

Clean Architecture
The architecture defines where the application performs its core functionality and how that functionality interacts with things like the database and the user interface. Clean architecture refers to organizing the project so that it’s easy to understand and easy to change as the project grows.

Command Query Responsibility Segregation is a design pattern to separate the read and write processes of your application. Read operations are called Queries and write operations are called Commands.

Project Structure

Swagger UI Setup
The swagger object model and middleware exposes the JSON objects as Endpoints

Installing Swagger packages using NuGet package console with the below command.

Install-Package Swashbuckle.AspNetCore -Version 5.6.3

Swagger Configuration with Bearer token Authentication

namespace CleanArchitectureApp.WebApi.Extensions
{
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.OpenApi.Models;
    using System;
    using System.Reflection;

    public static class SwaggerConfig
    {
        public static void AddSwaggerConfiguration(this IServiceCollection services)
        {
            if (services == null)
            {
               throw new ArgumentNullException(nameof(services));
            }

            services.AddSwaggerGen(s =>
            {
               s.SwaggerDoc("v1", new OpenApiInfo
               {
                   Version = "v1",
                   Title = "Clean Architecture Application",
                   Description = "Clean Architecture Application Web API",
               });
               s.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
               {
                   Description = "Input the JWT like: Bearer {your token}",
                   Name = "Authorization",
                   Scheme = "Bearer",
                   BearerFormat = "JWT",
                   In = ParameterLocation.Header,
                   Type = SecuritySchemeType.ApiKey,
               });
               s.AddSecurityRequirement(new OpenApiSecurityRequirement
               {
                   {
                      new OpenApiSecurityScheme
                      {
                          Reference = new OpenApiReference
                          {
                              Type = ReferenceType.SecurityScheme,
                              Id = "Bearer",
                          },
                      },
                      Array.Empty<string>()
                   },
               });
               var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
               var xmlPath = System.IO.Path.Combine(AppContext.BaseDirectory, xmlFile);
               s.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
           });
        }

        public static void UseSwaggerSetup(this IApplicationBuilder app)
        {
           if (app == null)
           {
               throw new ArgumentNullException(nameof(app));
           }

           app.UseSwagger();
           app.UseSwaggerUI(c =>
           {
               c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
               c.RoutePrefix = string.Empty;
               c.DefaultModelsExpandDepth(-1);
           });
        }

        public static void AddApiVersioningExtension(this IServiceCollection services)
        {
           services.AddApiVersioning(config =>
           {
               // Specify the default API Version as 1.0
               config.DefaultApiVersion = new ApiVersion(1, 0);

               // If the client hasn't specified the API version in the request, use the default API version number
               config.AssumeDefaultVersionWhenUnspecified = true;

               // Advertise the API versions supported for the particular endpoint
               config.ReportApiVersions = true;
           });
        }
    }
}

JWT Token Implementation

The JWT Bearer Token is used for accessing the WebApi endpoints securely. Below is the JWT configuration

namespace CleanArchitectureApp.WebApi.Extensions
{
    using CleanArchitectureApp.Application.Wrappers;
    using CleanArchitectureApp.Domain;
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.IdentityModel.Tokens;
    using Newtonsoft.Json;
    using System;
    using System.Text;

    public static class JwtTokenConfig
    {
        public static IServiceCollection AddJwtTokenAuthentication(this IServiceCollection services, IConfiguration configuration)
        {
            services.Configure<JwtConfig>(configuration.GetSection("JWTSettings"));
            services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
                                   .AddJwtBearer(x =>
                                   {
                                       x.TokenValidationParameters = new TokenValidationParameters()
                                       {
                                           ValidateIssuerSigningKey = true,
                                           IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWTSettings:Key"])),
                                           ValidateIssuer = false,
                                           ValidateAudience = false,
                                           ValidateLifetime = true,
                                           ValidIssuer = configuration["JWTSettings:Issuer"],
                                           ValidAudience = configuration["JWTSettings:Audience"],
                                           // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
                                           ClockSkew = TimeSpan.Zero,
                                       };
                                       x.RequireHttpsMetadata = false;
                                       x.SaveToken = true;
                                       x.Events = new JwtBearerEvents()
                                       {               
                                           OnAuthenticationFailed = c =>
                                           {
                                               c.NoResult();
                                               c.Response.StatusCode = 500;
                                               c.Response.ContentType = "text/plain";
                                               return c.Response.WriteAsync(c.Exception.ToString());
                                           },
                                           OnChallenge = context =>
                                           {
                                               if (!context.Response.HasStarted)
                                               {
                                                   context.Response.StatusCode = 401;
                                                   context.Response.ContentType = "application/json";
                                                   context.HandleResponse();
                                                   var result = JsonConvert.SerializeObject(new Response<string>("You are not Authorized"));
                                                   return context.Response.WriteAsync(result);
                                               }
                                               else
                                               {
                                                   var result = JsonConvert.SerializeObject(new Response<string>("Token Expired"));
                                                   return context.Response.WriteAsync(result);
                                               }
                                           },
                                           OnForbidden = context =>
                                           {
                                               context.Response.StatusCode = 403;
                                               context.Response.ContentType = "application/json";
                                               var result = JsonConvert.SerializeObject(new Response<string>("You are not authorized to access this resource"));
                                               return context.Response.WriteAsync(result);
                                           },
                                       };
                                   });
            return services;
        }
    }
}

Token Generation On Successful Login

private Task<JwtSecurityToken> GenerateJWToken(User user)
        {
            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.Email, user.UserEmail),
                new Claim("uid", user.UserId.ToString()),
            };

            var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key));
            var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256);

            var jwtSecurityToken = new JwtSecurityToken(
                issuer: _jwtSettings.Issuer,
                audience: _jwtSettings.Audience,
                claims: claims,
                expires: DateTime.UtcNow.AddMinutes(_jwtSettings.DurationInMinutes),
                signingCredentials: signingCredentials);
                return Task.FromResult(jwtSecurityToken);
            }

Global Error Middleware

Server Internal Error Exceptions and Business Validation Exceptions are handled and the response is returned for use of displaying these validations in UI.

namespace CleanArchitectureApp.WebApi.Middlewares
{
    using CleanArchitectureApp.Application.Exceptions;
    using CleanArchitectureApp.Application.Wrappers;
    using Microsoft.AspNetCore.Http;
    using System;
    using System.Collections.Generic;
    using System.Net;
    using System.Text.Json;
    using System.Threading.Tasks;

    public class ErrorHandlerMiddleware
    {
        private readonly RequestDelegate _next;

        public ErrorHandlerMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (Exception error)
            {
                var response = context.Response;
                response.ContentType = "application/json";
                var responseModel = new Response<string>() { Succeeded = false, Message = error?.Message };

                switch (error)
                {
                    case ApiException e:
                        // custom application error
                        response.StatusCode = (int)HttpStatusCode.BadRequest;
                        break;

                    case ValidationException e:
                        // custom application error
                        response.StatusCode = (int)HttpStatusCode.UnprocessableEntity;
                        responseModel.Errors = e.Errors;
                        break;

                    case KeyNotFoundException e:
                        // not found error
                        response.StatusCode = (int)HttpStatusCode.NotFound;
                        break;

                    default:
                        // unhandled error
                        response.StatusCode = (int)HttpStatusCode.InternalServerError;
                        break;
                 }

                 var result = JsonSerializer.Serialize(responseModel);

                 await response.WriteAsync(result);
            }
        }
    }
}

NHibernate ORM and Generic Repository Pattern

NHibernate is used to interact with the database. Singleton Session factory is implemented. Below is the Persistence Layer Service Extension.

namespace CleanArchitectureApp.Infrastructure.Persistence
{
    public static class ServiceRegistration
    {
        public static void AddPersistenceInfrastructure(this IServiceCollection services, IConfiguration configuration)
        {
            var mapper = new ModelMapper();
            mapper.AddMappings(typeof(UserMap).Assembly.ExportedTypes);

            HbmMapping domainMapping = mapper.CompileMappingForAllExplicitlyAddedEntities();

            var NHConfiguration = new Configuration();
            NHConfiguration.DataBaseIntegration(c =>
            {
                c.Dialect<MsSql2008Dialect>();
                c.ConnectionString = configuration.GetConnectionString("DefaultConnection");
                c.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;
                c.LogFormattedSql = true;
                c.LogSqlInConsole = true;
            });
            NHConfiguration.AddMapping(domainMapping);

            var sessionFactory = NHConfiguration.BuildSessionFactory();

            services.AddSingleton(sessionFactory);
            services.AddScoped(factory => sessionFactory.OpenSession());

            #region Repositories

            services.AddTransient(typeof(IGenericRepositoryAsync<>), typeof(GenericRepositoryAsync<>));
            services.AddTransient<IUserRepositoryAsync, UserRepositoryAsync>();
            services.AddTransient<IUserStatusRepositoryAsync, UserStatusRepositoryAsync>();
            services.AddTransient<ILoginLogRepositoryAsync, LoginLogRepositoryAsync>();
            services.AddTransient<ILoginLogTypeRepositoryAsync, LoginLogTypeRepositoryAsync>();
            services.AddTransient<IUserTokenRepositoryAsync, UserTokenRepositoryAsync>();
            services.AddTransient<IEmailTemplateRepositoryAsync, EmailTemplateRepositoryAsync>();
            services.AddTransient<IEmailRecipientRepositoryAsync, EmailRecipientRepositoryAsync>();

            #endregion Repositories
        }
    }
}

User Entity

namespace CleanArchitectureApp.Domain
{
    public class User : AuditableBaseEntity
    {
        public User()
        {
            UserTokens = new List<UserToken>();
        }

        public virtual Guid UserId { get; set; }
        public virtual UserStatuses UserStatuses { get; set; }
        public virtual string UserName { get; set; }
        public virtual string FirstName { get; set; }
        public virtual string LastName { get; set; }
        public virtual string UserEmail { get; set; }
        public virtual string PasswordHash { get; set; }
        public virtual string PasswordSalt { get; set; }
        public virtual IList<UserToken> UserTokens { get; set; }
    }
}

UserMap

namespace CleanArchitectureApp.Infrastructure.Persistence.Repositories
{
    public partial class UserMap : ClassMapping<Domain.User>
    {
        public UserMap()
        {
            Schema("dbo");
            Table("Users");
            Lazy(true);
            Id(x => x.UserId, map =>
            {
                map.Generator(Generators.Guid);
                map.Column("UserId");
                map.UnsavedValue(Guid.Empty);
            });
            Property(x => x.UserName, map => { map.NotNullable(true); map.Length(50); });
            Property(x => x.FirstName, map => { map.NotNullable(true); map.Length(50); });
            Property(x => x.LastName, map => map.Length(50));
            Property(x => x.UserEmail, map => { map.NotNullable(true); map.Length(50); });
            Property(x => x.PasswordHash, map => map.NotNullable(true));
            Property(x => x.PasswordSalt, map => map.NotNullable(true));
            Property(x => x.CreatedDate, map => map.NotNullable(true));
            Property(x => x.UpdatedDate);
            Property(x => x.CreatedBy, map => map.NotNullable(true));
            Property(x => x.UpdatedBy);
            ManyToOne(x => x.UserStatuses, map => { map.Column("UserStatusId"); map.Cascade(Cascade.All); map.Fetch(FetchKind.Select); });

            Bag(x => x.UserTokens, colmap => { colmap.Key(x => x.Column("UserId")); colmap.Inverse(true); colmap.Cascade(Cascade.Persist); }, map => { map.OneToMany(a => a.Class(typeof(UserToken))); });
       }
   }
}

Fluent Validation
Package Reference:

<PackageReference Include=”FluentValidation.DependencyInjectionExtensions” Version=”9.3.0" />

To define a set of validation rules for a particular object, you will need to create a class that inherits from AbstractValidator, where T is the type of class that you wish to validate.

Below is the sample Create User Validations.

namespace CleanArchitectureApp.Application.Features.Users.Commands
{
    public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
    {
        private readonly IUserRepositoryAsync userRepository;

        public CreateUserCommandValidator(IUserRepositoryAsync userRepository)
        {
            this.userRepository = userRepository;

            RuleFor(p => p.UserName)
                .NotEmpty().WithMessage("{PropertyName} is required.")
                .NotNull()
                .MaximumLength(50).WithMessage("{PropertyName} must not exceed 50 characters.")
                .MustAsync(IsUniqueUserName).WithMessage("{PropertyName} already exists.");

            RuleFor(p => p.Password)
                .NotEmpty().WithMessage("{PropertyName} is required.")
                .NotNull()
                .MaximumLength(50).WithMessage("{PropertyName} must not exceed 50 characters.");

            RuleFor(p => p.FirstName)
                .NotEmpty().WithMessage("{PropertyName} is required.")
                .NotNull()
                .MaximumLength(50).WithMessage("{PropertyName} must not exceed 50 characters.");

            RuleFor(p => p.UserEmail)
                .NotEmpty().WithMessage("{PropertyName} is required.")
                .NotNull()
                .MaximumLength(50).WithMessage("{PropertyName} must not exceed 50 characters.");

            RuleFor(p => p.CreatedBy)
                .NotEmpty().WithMessage("{PropertyName} is required.")
                .NotNull()
                .WithMessage("{PropertyName} must not exceed 50 characters.");
        }

        private async Task<bool> IsUniqueUserName(string username, CancellationToken cancellationToken)
        {
            var userObject = (await userRepository.FindByCondition(x => x.UserName.ToLower() == username.ToLower()).ConfigureAwait(false)).AsQueryable().FirstOrDefault();
            if (userObject != null)
            {
                return false;
            }
            return true;
        }
    }
}

Summary

Hope you enjoy above tutorial. If you enjoy it, please feel free to share it. Thank you

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *