ZemBoilerplate Technical Guide (ASP.NET Core)

This boilerplate provides a standard, production-ready starting point for building an ASP.NET Core web application with:

  • Clean architecture / layered structure — Clear separation of concerns across Presentation, Application, Domain, and Infrastructure layers for maintainability and testability.
  • Dependency Injection and modular services — Built-in DI with loosely coupled, feature-based services and interfaces for easy swapping, scaling, and unit testing.
  • EF Core data access and migrations — Entity Framework Core for data persistence with code-first migrations, versioned schema changes, and reliable deployment workflows.
  • Authentication/Authorization — Secure identity and access control using authentication (JWT/Cookies) and role/claim-based authorization policies.
  • Swagger/OpenAPI for API discovery — Interactive API documentation and testing via Swagger UI, enabling fast integration, validation, and client generation.
  • Centralized configuration & logging — Environment-based configuration (appsettings + secrets) with structured logging and centralized observability for diagnostics and auditing.

Backend Technologies

Technology Version Purpose
.NET .NET 10 Core application framework
ASP.NET Core Web API .NET 10 REST API layer
Entity Framework Core EF Core 10 ORM & database access
ASP.NET Core Identity .NET 10 Authentication & user management
FluentValidation v12+ Input validation
AutoMapper v14+ DTO ↔ Entity mapping
SQL Server 2019+ Primary relational database
xUnit / NUnit Latest Unit testing framework

Backend Technologies

Technology Version Purpose
Razor Views .NET 10 Server-side rendering of dynamic UI using C# and HTML
ASP.NET Core MVC .NET 10 Model-View-Controller pattern for structured web applications
HTML5 Latest Markup language for structuring web pages
CSS3 Latest Styling and responsive layout design
Bootstrap v5+ Responsive UI framework for modern layouts and components
JavaScript ES6+ Client-side interactivity and dynamic behavior
jQuery v3+ Simplified DOM manipulation and AJAX handling
AJAX Asynchronous server communication without full page reload

The ZemBoilerplate solution is built using a layered (Clean) architecture pattern in ASP.NET Core. The system is divided into multiple projects, each responsible for a specific concern, ensuring separation of responsibilities, scalability, and maintainability.

The architecture follows this layered structure:

Architecture Flow

Web
DTO / ViewModel
Application
Domain
Infrastructure

🔹 Project Overview

Project Layer Type Responsibility
ZemBoilerplate.Web Presentation Layer Handles user interaction, HTTP requests, controllers, Razor views, middleware, and security. Communicates with the Application layer and contains no core business logic.
ZemBoilerplate.Application Business Layer Implements application use cases and coordinates business operations. Contains service managers, feature modules, validation logic, and object mapping.
ZemBoilerplate.Domain Core Layer Contains core business entities, domain rules, interfaces, and business logic. Fully independent of frameworks and infrastructure concerns.
ZemBoilerplate.Infrastructure Data / Infrastructure Layer Manages database access using EF Core, DbContext configuration, repository implementations, and external service integrations.
ZemBoilerplate.Common Shared Utilities Provides reusable components such as constants, helper classes, encryption utilities, enums, and common request/response structures.
ZemBoilerplate.ViewModel Data Transfer Layer Contains ViewModels used for transferring data between the Web and Application layers, ensuring separation between domain entities and UI models.
ZemBoilerplate.LoggerService Logging Layer Implements centralized logging using NLog with abstraction, enabling consistent logging across all layers while maintaining clean architecture principles.
ZemBoilerplate.Domain (Domain Layer)

Base Entities

Base entities are foundational classes in a system that contain common properties shared across multiple domain models. These typically include fields such as Id, CreatedDate, UpdatedDate, CreatedBy, ModifiedBy, IsDeleted, or CompanyId (in multi-tenant systems).

  • Promote code reusability
  • Ensure consistency across database tables
  • Reduce duplication
  • Centralize shared behaviors (e.g., soft delete, auditing)

public interface IBaseEntities
{
    public Guid Id { get; set; }
    public bool IsActive { get; set; }
    public bool IsDeleted { get; set; }
    public string? CreatedBy { get; set; }
    public DateTime? CreatedDate { get; set; }
    public string? ModifiedBy { get; set; }
    public DateTime? ModifiedDate { get; set; }
    public byte[]? RowVersion { get; set; }
}
public class BaseEntities :IBaseEntities
{
    public Guid Id { get; set; }
    public bool IsActive { get; set; }
    public bool IsDeleted { get; set; }
    public string? CreatedBy { get; set; }
    public DateTime? CreatedDate { get; set; }
    public string? ModifiedBy { get; set; }
    public DateTime? ModifiedDate { get; set; }
    public byte[]? RowVersion { get; set; }
}

    

JWT Configuration

This class represents the JWT (JSON Web Token) settings loaded from appsettings.json. It centralizes all token-related configuration in one place.

  • Section – Specifies the configuration section name in appsettings.json (default: JwtSettings).
  • ValidIssuer – Identifies the application or server that generates the JWT.
  • ValidAudience – Defines the intended recipient of the token (which application can use it).
  • Expires – Determines how long the token remains valid before expiration.
  • SecretKey – The secure key used to sign and validate the JWT.

public class JwtConfiguration
{
    public string Section { get; set; } = "JwtSettings"; 
    public string? ValidIssuer { get; set; }
    public string? ValidAudience { get; set; }
    public string? Expires { get; set; }
    public string? SecretKey { get; set; }
}

    

appsettings.json


"JwtSettings": {
  "ValidIssuer": "ZemBoilerplate",
  "ValidAudience": "ZemBoilerplateUsers",
  "Expires": "120",
  "SecretKey": "THIS_IS_A_SUPER_SECRET_KEY_12345"
}

    
ZemBoilerplate.DataAccess (DataAccess Layer)

Global Usings

  • Definition – Global Usings allow you to define using directives once and make them available across the entire project.
  • Reduces Repetition – Eliminates the need to add the same using statements in every file.
  • Cleaner Code Files – Keeps individual class files shorter and more readable.
  • Centralized Imports – Common namespaces are managed in one file (usually GlobalUsings.cs).
  • Introduced in .NET 6 – Supported from C# 10 and .NET 6 onwards.

// Global using directives for ZeMBoilerPlate.DataAccess project
global using System.Text;
global using System.Linq.Expressions;

global using Microsoft.EntityFrameworkCore;
global using Microsoft.EntityFrameworkCore.Metadata.Builders;

global using ZeMBoilerPlate.DataAccess.DataContext;
global using ZeMBoilerPlate.DataAccess.Base;
global using ZeMBoilerPlate.DataAccess.BaseContract;
global using ZeMBoilerPlate.DataAccess.Contract;
global using ZeMBoilerPlate.DataAccess.Repositories.Utility;
global using ZeMBoilerPlate.DataAccess.Repositories;

global using ZeMBoilerPlate.CommonModel.Paging;
global using ZeMBoilerPlate.CommonModel.RequestFeatures;

global using ZeMBoilerPlate.Entities.Models.Admin;
global using ZeMBoilerPlate.Entities.Models.Application;
global using ZeMBoilerPlate.Entities.Models.Identity;
global using ZeMBoilerPlate.Entities.Models.LookUp;
global using ZeMBoilerPlate.Entities.Models.Base;

    

Dependency Injection (DI) DataAccess Layer

  • Definition – Dependency Injection is a design pattern used to provide objects (dependencies) to a class instead of the class creating them itself.
  • Loose Coupling – It reduces tight coupling between classes, making the application more flexible and maintainable.
  • Improved Testability – Dependencies can be mocked or replaced during unit testing.
  • Centralized Configuration – Services are registered in one place (e.g., Program.cs or Startup.cs).
  • Lifetime Management – Supports different lifetimes such as Transient, Scoped, and Singleton.

public static class DependencyInjection
{
    public static IServiceCollection AddSqlContext(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddDbContext<AccountsDbContext>(options =>
            options.UseSqlServer(configuration.GetConnectionString("sqlConnection")));
        
        return services;
    }
    public static IServiceCollection AddRepositories(this IServiceCollection services)
    {
       
        services.AddScoped<IRepositoryManager, RepositoryManager>();

        return services;
    }
}

    

Dependency Injection (DI) of DataAccess Layer in program.cs

  • Purpose – Registers Data Access services so they can be injected into controllers and other services.
  • DbContext Registration – Configures the database connection and Entity Framework Core context.
  • Repository Registration – Maps interfaces (e.g., IUserRepository) to concrete implementations.
  • Service Lifetime – Defines how long objects live (Scoped is recommended for DbContext).
  • Loose Coupling – Ensures controllers depend on abstractions, not concrete implementations.
            
builder.Services
    .AddSqlContext(builder.Configuration)
    .AddRepositories();
 

DataContext

DataContext is the class that allows your application to communicate with the database using Entity Framework Core.

  • Definition – DataContext (commonly called DbContext) is the primary class in Entity Framework Core that manages database connections and operations.
  • Database Bridge – Acts as a bridge between your application and the database.
  • Inherits IdentityDbContext – Extends IdentityDbContext<ApplicationUser, ApplicationRole, string> to integrate ASP.NET Core Identity with your database.
  • Database Tables (DbSet) – Defines application tables such as Company and Product using DbSet<T>.
  • OnModelCreating Override – Customizes EF Core model configuration and applies entity configurations automatically from the assembly.
  • ApplyConfigurationsFromAssembly – Loads all IEntityTypeConfiguration<T> classes to keep entity mapping clean and modular.
  • RenameIdentityTables – Customizes default Identity table names and moves them into the UserManagement schema.
  • Default Schema – Sets Identity tables under a dedicated schema for better organization and separation of concerns.

public class ApplicationDbContext 
    : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{

    public ApplicationDbContext(DbContextOptions options)
        : base(options)
    {
    }

    #region Admin Schema

    public virtual DbSet<Company> Company { get; set; } = null!;

    #endregion

    #region Application

    public virtual DbSet<Product> Product { get; set; } = null!;

    #endregion

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
        RenameIdentityTables(modelBuilder);
    }
		
    protected void RenameIdentityTables(ModelBuilder builder)
    {
        builder.HasDefaultSchema("UserManagement");

        builder.Entity<ApplicationUser>(entity =>
        {
            entity.ToTable(name: "Users");
        });

        builder.Entity<ApplicationRole>(entity =>
        {
            entity.ToTable(name: "Roles");
        });

        builder.Entity<IdentityUserRole<string>>(entity =>
        {
            entity.ToTable("UserRoles");
        });

        builder.Entity<IdentityUserClaim<string>>(entity =>
        {
            entity.ToTable("UserClaims");
        });

        builder.Entity<IdentityUserLogin<string>>(entity =>
        {
            entity.ToTable("UserLogins");
        });

        builder.Entity<IdentityRoleClaim<string>>(entity =>
        {
            entity.ToTable("RoleClaims");
        });

        builder.Entity<IdentityUserToken<string>>(entity =>
        {
            entity.ToTable("UserTokens");
        });
    }
}

EntityTypeConfiguration

DataContext is the class that allows your application to communicate with the database using Entity Framework Core.

  • Definition – EntityTypeConfiguration is used to configure how an entity maps to a database table in Entity Framework Core.
  • Separation of Concerns – Moves entity configuration out of DbContext to keep it clean and organized.
  • Implements Interface – Uses the interface IEntityTypeConfiguration<T> to define table rules.
  • Fluent API Configuration – Configures properties, relationships, keys, indexes, and constraints using the Fluent API.
  • Automatic Registration – Can be automatically applied using ApplyConfigurationsFromAssembly in DbContext.

public class CompanyConfiguration : IEntityTypeConfiguration<Company>
{
    public void Configure(EntityTypeBuilder<Company> entity)
    {
        entity.ToTable("Company", "Admin");

        entity.Property(e => e.Id)
            .HasDefaultValueSql("NEWID()")
            .HasColumnName("CompanyID");

        entity.Property(e => e.CompanyCode)
            .HasMaxLength(10)
            .IsUnicode(false);

        entity.Property(e => e.CompanyName)
            .HasMaxLength(50);

        entity.Property(e => e.ParentCompanyId)
            .HasColumnName("ParentCompanyID");

        entity.HasOne(d => d.ParentCompany)
            .WithMany(p => p.InverseParentCompany)
            .HasForeignKey(d => d.ParentCompanyId)
            .HasConstraintName("FK_Company_Company");
    }
}

Configuration in DataContext


    modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());

Generic Repository

The Generic Repository centralizes common CRUD operations and promotes reusable data access logic across the application.

  • Reusability – One repository works for all entity types.
  • Abstraction – Business layer depends on interfaces, not DbContext.
  • Track Changes Control – Improves performance using AsNoTracking().
  • Maintainability – Reduces duplicated CRUD logic.

Repository Interface


public interface IRepositoryBase<TEntity>
{
    IQueryable<TEntity> FindAll(bool trackChanges);

    IQueryable<TEntity> FindByCondition(
        Expression<Func<TEntity, bool>> expression,
        bool trackChanges);

    void Create(TEntity entity);

    void Update(TEntity entity);

    void Delete(TEntity entity);
}

            

RepositoryBase Implementation


public abstract class RepositoryBase<TEntity> 
    : IRepositoryBase<TEntity> where TEntity : class
{
    protected readonly ApplicationDbContext _context;

    protected RepositoryBase(ApplicationDbContext context)
    {
        _context = context;
    }

    public IQueryable<TEntity> FindAll(bool trackChanges) =>
        !trackChanges
            ? _context.Set<TEntity>().AsNoTracking()
            : _context.Set<TEntity>();

    public IQueryable<TEntity> FindByCondition(
        Expression<Func<TEntity, bool>> expression,
        bool trackChanges) =>
        !trackChanges
            ? _context.Set<TEntity>()
                      .Where(expression)
                      .AsNoTracking()
            : _context.Set<TEntity>()
                      .Where(expression);

    public void Create(TEntity entity) =>
        _context.Set<TEntity>().Add(entity);

    public void Update(TEntity entity) =>
        _context.Set<TEntity>().Update(entity);

    public void Delete(TEntity entity) =>
        _context.Set<TEntity>().Remove(entity);
}

            

Product Repository

ProductRepository extends RepositoryBase and implements entity-specific data access logic such as filtering, searching, sorting, and pagination.

  • Extends Generic Repository – Inherits common CRUD logic.
  • Pagination Support – Returns data using PagedList.
  • Dynamic Filtering – Supports price range, company, category.
  • Dynamic Searching – Uses LinqKit PredicateBuilder.
  • Dynamic Sorting – Uses System.Linq.Dynamic.Core.

ProductRepository


public class ProductRepository 
    : RepositoryBase<Product>, IProductRepository
{
    public ProductRepository(AccountsDbContext repositoryContext)
        : base(repositoryContext)
    {
    }

    public async Task<PagedList<Product>> 
        GetProductsByCompanyAsync(ProductParameters parameters, bool trackChanges)
    {
        var baseQuery = FindByCondition(
                c => !c.IsDeleted && 
                c.RefCompanyID.Equals(parameters.RefCompanyID), 
                trackChanges)
            .FilterByPrice(parameters.MinPrice, parameters.MaxPrice)
            .Search(parameters)
            .Sort(parameters.OrderBy);

        var count = await baseQuery.CountAsync();

        var products = await baseQuery
            .Skip((parameters.PageNumber - 1) * parameters.PageSize)
            .Take(parameters.PageSize)
            .ToListAsync();

        return new PagedList<Product>(
            products,
            count,
            parameters.PageNumber,
            parameters.PageSize
        );
    }
}

            

Repository Extensions (Filter, Search, Sort)

Extension methods are used to keep filtering, searching, and sorting logic modular and reusable.


public static class RepositoryProductExtensions
{
    public static IQueryable<Product> FilterByPrice(
        this IQueryable<Product> products,
        decimal? minPrice,
        decimal? maxPrice)
    {
        if (minPrice.HasValue)
            products = products.Where(
                p => p.SellPrice >= minPrice.Value);

        if (maxPrice.HasValue)
            products = products.Where(
                p => p.SellPrice <= maxPrice.Value);

        return products;
    }

    public static IQueryable<Product> Search(
        this IQueryable<Product> products,
        ProductParameters parameters)
    {
        var predicate = PredicateBuilder
            .New<Product>(true);

        if (!string.IsNullOrWhiteSpace(parameters.SearchTerm))
        {
            var search = parameters.SearchTerm.ToLower();

            predicate = predicate.And(p =>
                p.Description.ToLower().Contains(search) ||
                p.ProductCode.ToLower().Contains(search) ||
                p.ProductName.ToLower().Contains(search));
        }

        return products.AsExpandable().Where(predicate);
    }

    public static IQueryable<Product> Sort(
        this IQueryable<Product> products,
        string orderByQueryString)
    {
        if (string.IsNullOrWhiteSpace(orderByQueryString))
            return products.OrderBy(p => p.ProductName);

        var orderQuery = OrderQueryBuilder
            .CreateOrderQuery<Product>(orderByQueryString);

        if (string.IsNullOrWhiteSpace(orderQuery))
            return products.OrderBy(p => p.ProductName);

        return products.OrderBy(orderQuery);
    }
}

            

Repository Manager

The RepositoryManager acts as a centralized entry point for all repositories and implements the Unit of Work pattern to manage transactions and audit behavior.

  • Unit of Work Pattern – Coordinates multiple repositories under a single transaction.
  • Lazy Loading – Repositories are instantiated only when accessed.
  • Centralized Save – SaveAsync() commits all changes in one place.
  • Audit Automation – Automatically sets Created/Modified fields.
  • Identity Integration – Applies auditing for ApplicationUser and ApplicationRole.

IRepositoryManager Interface


public interface IRepositoryManager
{
    IUserManagementRepository UserManagement { get; }
    IRoleManagementRepository RoleManagement { get; }
    ICompanyRepository Company { get; }
    ICustomerRepository Customer { get; }
    IProductRepository Product { get; }
    ICategoryRepository Category { get; }

    Task SaveAsync();
    void ApplyAuditInformation();
}

            

RepositoryManager Implementation


public class RepositoryManager : IRepositoryManager
{
    private readonly AccountsDbContext _accountsDbContext;
    private readonly IDateTimeService _dateTimeService;
    private readonly IUserContext _userContext;

    private readonly Lazy<IUserManagementRepository> _userManagementRepository;
    private readonly Lazy<IProductRepository> _productRepository;

    public RepositoryManager(
        AccountsDbContext accountsDbContext,
        IDateTimeService dateTimeService,
        IUserContext userContext)
    {
        _accountsDbContext = accountsDbContext;
        _dateTimeService = dateTimeService;
        _userContext = userContext;

        _userManagementRepository =
            new Lazy<IUserManagementRepository>(() => new UserManagementRepository(accountsDbContext));

        _productRepository =
            new Lazy<IProductRepository>(() => new ProductRepository(accountsDbContext));
    }

    public IUserManagementRepository UserManagement =>
        _userManagementRepository.Value;

    public IProductRepository Product =>
        _productRepository.Value;

    public async Task SaveAsync()
    {
        ApplyAuditInformation();
        await _accountsDbContext.SaveChangesAsync();
    }
}

            

Audit Handling

Before saving changes, ApplyAuditInformation() automatically:

  • Sets CreatedDate and CreatedBy on Added entities
  • Sets ModifiedDate and ModifiedBy on Modified entities
  • Applies auditing to Identity entities (Users & Roles)
ZemBoilerplate.Dto/ViewModel (Dto/ViewModel Layer)

_DtoViewModelLayer.GlobalUsings

Global Usings centralize commonly used namespaces across the DTO and ViewModel layer to reduce repetition and keep files clean.

  • Reduces Boilerplate – Eliminates repeated using statements in every DTO file.
  • Improves Readability – Keeps DTO classes clean and focused.
  • Centralized Management – All shared namespaces are maintained in one file.
  • Introduced in .NET 6 – Uses global using feature from C# 10.

Example: GlobalUsings.cs


global using System;
global using System.Collections.Generic;
global using System.ComponentModel.DataAnnotations;
global using System.Linq;
global using System.Threading.Tasks;

            

Benefits in DTO Layer

  • Cleaner DTO and ViewModel classes
  • Consistent namespace usage
  • Better maintainability in large projects

Application Layer Dependency Injection

The DependencyInjection class centralizes registration of application-layer services such as AutoMapper and FluentValidation.

  • Centralized Registration – Keeps Program.cs clean and modular.
  • Assembly Scanning – Automatically discovers validators or profiles.
  • Clean Architecture Friendly – Application layer controls its own dependencies.
  • Extension Method Pattern – Improves readability of service registration.

DependencyInjection Class


public static class DependencyInjection
{
    public static IServiceCollection 
        AddApplicationValidation(this IServiceCollection services)
    {
        services.AddValidatorsFromAssemblyContaining<ApplicationAssemblyMarker>();
        return services;
    }
}

            

Assembly Marker Class


public sealed class ApplicationAssemblyMarker
{
}

            

Registering AutoMapper (Example)


public static IServiceCollection 
    AddApplicationMapping(this IServiceCollection services)
{
    services.AddAutoMapper(typeof(ApplicationAssemblyMarker));
    return services;
}

            

Usage in Program.cs


builder.Services
       .AddApplicationValidation()
       .AddApplicationMapping();

            

Data Transfer Object (DTO)

DTOs are used to transfer data between layers (e.g., Controller and Service) without exposing domain entities directly.

  • Encapsulation – Prevents exposing database entities to the presentation layer.
  • Security – Sensitive fields (e.g., PasswordHash) are hidden.
  • Validation Friendly – Supports data validation rules.
  • Immutability – Uses record type with init-only properties.
  • Separation of Concerns – Keeps domain models separate from API contracts.

UserForCreationDto


public record UserForCreationDto
{
    public string UserName { get; init; } = default!;
    public string Email { get; init; } = default!;
    public bool? EmailConfirmed { get; init; }
    public bool IsSuperAdmin { get; init; }
    public bool IsAdmin { get; init; }
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
    public string? Password { get; init; }
    public Guid? RefCompanyId { get; init; }
    public string? Role { get; init; }
    public string? PhoneNumber { get; init; }
    public DateTime? CreatedDate { get; init; }
}

            

Why Use record Instead of class?

  • Immutability – Properties use init-only setters.
  • Value-Based Equality – Useful for comparisons.
  • Cleaner Syntax – Ideal for data transport models.

UserForCreationDtoValidator

FluentValidation is used to enforce business rules and input validation for the UserForCreationDto before processing the request.

  • Separation of Concerns – Validation logic is separated from controllers.
  • Business Rule Enforcement – Validates role, company, and user flags.
  • Database Validation – Checks email uniqueness and role existence.
  • Async Validation – Uses MustAsync for database checks.
  • Security Enforcement – Ensures password complexity rules.

Validator Implementation


public class UserForCreationDtoValidator 
    : AbstractValidator<UserForCreationDto>
{
    private readonly IRepositoryManager _repositoryManager;

    public UserForCreationDtoValidator(
        IRepositoryManager repositoryManager)
    {
        _repositoryManager = repositoryManager;

        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .MaximumLength(150)
            .MustAsync(BeUniqueEmail)
            .WithMessage("Email already exists.");

        RuleFor(x => x.FirstName)
            .NotEmpty()
            .MaximumLength(100);

        RuleFor(x => x.LastName)
            .NotEmpty()
            .MaximumLength(100);

        RuleFor(x => x.Password)
            .NotEmpty()
            .MinimumLength(8)
            .Matches("[A-Z]")
            .Matches("[a-z]")
            .Matches("[0-9]");

        RuleFor(x => x.Role)
            .NotEmpty()
            .When(x => !x.IsSuperAdmin && !x.IsAdmin)
            .MustAsync(BeRoleExist);

        RuleFor(x => x.RefCompanyId)
            .NotNull()
            .When(x => !x.IsSuperAdmin);

        RuleFor(x => x)
            .Must(x => !(x.IsSuperAdmin && x.IsAdmin))
            .WithMessage("User cannot be both SuperAdmin and Admin.");
    }
}

            

Why Use FluentValidation?

  • Cleaner Controllers
  • Centralized Business Rules
  • Supports Async Database Checks
  • Clean Architecture compliant
ZemBoilerplate.Application (Application Layer)

Application Layer Global Usings

Global Usings centralize commonly used namespaces across the Application layer to reduce duplication and keep service classes clean.

  • Reduces repetitive using statements
  • Improves readability and maintainability
  • Ensures consistent namespace usage

global using System;
global using System.Collections.Generic;
global using System.Threading.Tasks;
global using AutoMapper;
global using FluentValidation;

            

Service Layer Configuration

The Service Layer centralizes application configuration including Identity, JWT authentication, authorization policies, session management, and cookie configuration.

  • Registers ServiceManager as the application service entry point
  • Configures AutoMapper for object mapping
  • Configures JWT authentication
  • Configures ASP.NET Core Identity
  • Defines dynamic permission-based authorization policies
  • Handles cookie and session configuration
Service Registration

public static IServiceCollection AddServices(
    this IServiceCollection services, 
    IConfiguration configuration)
{
    services.AddScoped<IServiceManager, ServiceManager>();
    return services;
}

            
JWT Configuration

public static void ConfigureJWT(
    this IServiceCollection services, 
    IConfiguration configuration)
{
    var jwtConfig = new JwtConfiguration();
    configuration.Bind(jwtConfig.Section, jwtConfig);

    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = 
            JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = 
            JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters =
            new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = jwtConfig.ValidIssuer,
                ValidAudience = jwtConfig.ValidAudience,
                IssuerSigningKey =
                    new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(jwtConfig.SecretKey))
            };
    });
}

            
Identity Configuration

services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
    options.Password.RequiredLength = 8;
    options.User.RequireUniqueEmail = true;
    options.Lockout.MaxFailedAccessAttempts = 5;
})
.AddEntityFrameworkStores<AccountsDbContext>()
.AddDefaultTokenProviders();

            
Authorization Policies

services.AddAuthorization(options =>
{
    foreach (var permission in Permissions.GetAll())
    {
        options.AddPolicy(permission, policy =>
            policy.RequireAssertion(context =>
                context.User.HasClaim("IsSuperAdmin", "true") ||
                context.User.HasClaim("IsAdmin", "true") ||
                context.User.HasClaim("permission", permission)
            ));
    }
});

            

ApplicationUser Mapping Profile

AutoMapper profiles define how domain entities are mapped to DTOs and vice versa, ensuring separation between persistence models and API contracts.

  • Prevents exposing domain entities directly
  • Simplifies object transformation
  • Reduces manual mapping code
  • Supports bidirectional mapping using ReverseMap()
ApplicationUserProfile

public class ApplicationUserProfile : Profile
{
    public ApplicationUserProfile()
    {
        CreateMap<ApplicationUser, UserForRegistrationDto>()
            .ReverseMap();

        CreateMap<ApplicationUser, UserDto>()
            .ReverseMap()
            .ForMember(x => x.Id, 
                opt => opt.Ignore()); // Prevent overwriting primary key

        CreateMap<ApplicationUser, UserForCreationDto>()
            .ReverseMap();

        CreateMap<ApplicationUser, UserForUpdateDto>()
            .ReverseMap();
    }
}

            
Why Ignore Id?
  • Prevents accidental primary key modification
  • Protects Identity system integrity
  • Ensures safe update operations
Best Practice

Avoid mapping sensitive Identity properties such as PasswordHash, SecurityStamp, and ConcurrencyStamp unless explicitly required.

Product Service

The ProductService implements business logic for managing products. It acts as a bridge between Controllers and the Data Access Layer.

  • Uses RepositoryManager for data access
  • Uses AutoMapper for DTO ↔ Entity transformation
  • Supports pagination and filtering
  • Implements soft delete
  • Wraps responses using ErrorSuccessResponse
  • Uses TransactionScope for atomic operations
IProductService Interface

public interface IProductService
{
    Task<(IEnumerable<ProductDto> items, MetaData metaData)>
        GetProductsAsync(ProductParameters parameters, bool trackChanges);

    Task<ErrorSuccessResponse<ProductDto>>
        GetProductByIdAsync(Guid id, Guid companyId, bool trackChanges);

    Task<ErrorSuccessResponse<ProductDto>>
        CreateProductAsync(ProductForCreationDto creationDto, 
                           Guid companyId, bool trackChanges);

    Task<ErrorSuccessResponse<ProductDto>>
        UpdateProductAsync(Guid id, 
                           ProductForUpdateDto updateDto,
                           Guid companyId, bool trackChanges);

    Task<ErrorSuccessResponse>
        DeleteProductAsync(Guid id, Guid companyId, bool trackChanges);
}

            
ProductService Implementation

internal sealed class ProductService : IProductService
{
    private readonly IRepositoryManager _repository;
    private readonly IMapper _mapper;

    public ProductService(IRepositoryManager repository, IMapper mapper)
    {
        _repository = repository;
        _mapper = mapper;
    }

    public async Task<(IEnumerable<ProductDto>, MetaData)>
        GetProductsAsync(ProductParameters parameters, bool trackChanges)
    {
        var productsWithMetaData =
            await _repository.Product
                .GetProductsByCompanyAsync(parameters, trackChanges);

        var mapped = _mapper
            .Map<IEnumerable<ProductDto>>(productsWithMetaData);

        return (mapped, productsWithMetaData.MetaData);
    }

    public async Task<ErrorSuccessResponse<ProductDto>>
        CreateProductAsync(ProductForCreationDto dto, 
                           Guid companyId, bool trackChanges)
    {
        using var scope =
            new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);

        var entity = _mapper.Map<Product>(dto);
        _repository.Product.Create(entity);

        await _repository.SaveAsync();
        scope.Complete();

        return new ErrorSuccessResponse<ProductDto>(
            true,
            "Product created successfully.",
            _mapper.Map<ProductDto>(entity));
    }
}

            
Key Architectural Responsibilities
  • Enforces company-level isolation
  • Ensures soft delete instead of hard delete
  • Coordinates repository + mapping + transaction
  • Maintains clean separation from Controllers

Service Manager

The ServiceManager acts as a centralized gateway to all application services. It follows the Facade Pattern, providing a single access point for controllers.

  • Aggregates all service interfaces
  • Uses Lazy initialization for performance optimization
  • Reduces constructor dependency overload in Controllers
  • Improves maintainability and scalability
IServiceManager Interface

public interface IServiceManager
{
    IUserAuthenticationService AuthenticationService { get; }
    IUserManagementService UserManagementService { get; }
    IRoleManagementService RoleManagementService { get; }

    ICompanyService CompanyService { get; }
    ICustomerService CustomerService { get; }
    IProductService ProductService { get; }
    ICategoryService CategoryService { get; }

    ICommonService CommonService { get; }
    IOptionListService OptionListService { get; }
}

            
ServiceManager Implementation

public class ServiceManager : IServiceManager
{
    private readonly Lazy<IProductService> _productService;
    private readonly Lazy<ICategoryService> _categoryService;

    public ServiceManager(
        IRepositoryManager repositoryManager,
        IMapper mapper)
    {
        _productService =
            new Lazy<IProductService>(() =>
                new ProductService(repositoryManager, mapper));

        _categoryService =
            new Lazy<ICategoryService>(() =>
                new CategoryService(repositoryManager, mapper));
    }

    public IProductService ProductService =>
        _productService.Value;

    public ICategoryService CategoryService =>
        _categoryService.Value;
}

            
Why Use Lazy<T>?
  • Services are created only when accessed
  • Reduces memory usage
  • Improves performance
Architecture Flow

Controller → IServiceManager → Specific Service → Repository → DbContext

ZemBoilerplate.Common (Common Layer)

Common Layer Global Usings

Global Usings centralize shared namespaces used across the entire solution such as base models, constants, pagination, and response wrappers.

  • Reduces duplication across projects
  • Improves readability
  • Keeps shared logic organized

global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;

            

Common Layer Dependency Injection

The Common Layer registers shared cross-cutting services such as DateTime handling and User Context management. These services are used across multiple layers.

  • Registers DateTime abstraction for testability
  • Provides UserContext for multi-tenant support
  • Improves decoupling from system dependencies
  • Supports clean architecture principles
DependencyInjection Class

public static class DependencyInjection
{
    public static IServiceCollection 
        AddDateTimeService(this IServiceCollection services)
    {
        services.AddScoped<IDateTimeService, DateTimeService>();
        services.AddScoped<IUserContext, UserContext>();
        return services;
    }

    public static IServiceCollection 
        AddUserContextService(this IServiceCollection services)
    {
        services.AddScoped<IUserContext, UserContext>();
        return services;
    }
}

            
Why Abstract DateTime?
  • Makes unit testing easier
  • Avoids direct dependency on DateTime.Now
  • Centralizes time handling logic
UserContext Responsibility

UserContext extracts current user information (UserId, CompanyId, Claims) from HttpContext, enabling multi-tenant data isolation.

IUserContext Interface

IUserContext provides access to the currently authenticated user's identity and company-related information. It enables multi-tenant data isolation and role-based logic.

  • Extracts user information from claims
  • Supports company-level isolation
  • Provides role flags (Admin / SuperAdmin)
  • Used by Repository and Service layers
Interface Definition

public interface IUserContext
{
    Guid UserId { get; }
    Guid CompanyId { get; }
    Guid UserCompanyId { get; }
    string Email { get; }
    string DisplayName { get; }
    string CompanyName { get; }
    bool IsSuperAdmin { get; }
    bool IsAdmin { get; }
}

            
Architecture Responsibility

IUserContext centralizes access to authenticated user data, preventing direct dependency on HttpContext across layers. This improves testability and enforces clean architecture boundaries.

Usage Flow

Controller → Claims → UserContext → Service/Repository → Database Filter

IDateTimeService

IDateTimeService abstracts system time handling to improve testability, time-zone support, and consistency across the application.

  • Avoids direct usage of DateTime.Now / DateTime.UtcNow
  • Supports multi-timezone applications
  • Improves unit testing by enabling time mocking
  • Ensures consistent audit timestamps
Interface Definition

public interface IDateTimeService
{
    DateTime UtcNow { get; }

    DateTime Now(string timeZoneId);

    DateTime ConvertFromUtc(
        DateTime utcDate,
        string timeZoneId);

    DateTime ConvertToUtc(
        DateTime localDate,
        string timeZoneId);

    DateTime ConvertLocalDateTimeFromUtc(
        DateTime utcDate);
}

            
Why Time Abstraction Matters

Using a time abstraction prevents tight coupling with the system clock, which is critical for:

  • Audit logging (CreatedDate / ModifiedDate)
  • Token expiration (JWT)
  • Scheduled jobs
  • Global multi-region deployments
Architecture Flow

Service Layer → IDateTimeService → Repository (Audit Fields) → Database

Custom Claim Types

CustomClaimTypes defines standardized claim keys used across authentication, authorization, and multi-tenant logic.

  • Centralizes claim naming
  • Prevents hardcoded string usage
  • Supports role and company isolation

public static class CustomClaimTypes
{
    public const string CompanyId = "CompanyId";
    public const string UserId = "UserId";
    public const string DisplayName = "DisplayName";
    public const string CompanyName = "CompanyName";
    public const string UserCompanyId = "UserCompanyId";
    public const string IsSuperAdmin = "IsSuperAdmin";
    public const string IsAdmin = "IsAdmin";
}

            

Permission Constants

Permissions define fine-grained, policy-based authorization rules. They are grouped by module and dynamically registered in the Authorization configuration.

  • Structured by module (Users, Products, Roles, etc.)
  • Used in policy-based authorization
  • Enables dynamic permission scanning
Example: Users Management

public static class UsersManagement
{
    public const string Read = "User.Read";
    public const string Create = "User.Create";
    public const string Update = "User.Update";
    public const string Delete = "User.Delete";
}

            
Dynamic Permission Retrieval

public static IEnumerable<string> GetAll()
{
    return typeof(Permissions)
        .GetNestedTypes()
        .SelectMany(t => t.GetFields(
            BindingFlags.Public | BindingFlags.Static)
            .Where(f => f.IsLiteral && !f.IsInitOnly)
            .Select(f => f.GetValue(null)!.ToString()!));
}

            
Architecture Usage

Claims → JWT Token → Authorization Policy → Controller Action

Permission-based policies are dynamically registered during application startup using Permissions.GetAll().

Shared Enumerations

Enums in the Common Layer define standardized constant values used across the application. They improve readability, type safety, and prevent magic strings.

  • Eliminates hardcoded string or numeric values
  • Improves type safety
  • Enhances maintainability
  • Shared across all layers
Example: Status Enum

public enum StatusType
{
    Active = 1,
    Inactive = 2,
    Deleted = 3
}

            
Example: Role Type Enum

public enum RoleType
{
    SuperAdmin = 1,
    Admin = 2,
    User = 3
}

            
Best Practices
  • Store enum values in database as integers
  • Use Enum.GetName() for display
  • Convert safely using Enum.TryParse()
Architecture Role

Common Layer Enums → Used in Entities → Referenced in Services → Returned in DTOs → Displayed in UI

RequestParameters (Base Class)

RequestParameters provides a standardized structure for pagination, searching, sorting, and filtering across the application. It is inherited by specific parameter classes (e.g., ProductParameters).

  • Centralized pagination logic
  • Supports dynamic sorting
  • Prevents excessive page sizes
  • Enables flexible search filtering
Base RequestParameters

public abstract class RequestParameters
{
    public string? SearchValue { get; set; }
    public string? SearchField { get; set; }

    const int maxPageSize = 1000;

    private int _pageNumber = 1;
    public int PageNumber
    {
        get => _pageNumber;
        set => _pageNumber = value <= 0 ? _pageNumber : value;
    }

    private int _pageSize = 10;
    public int PageSize
    {
        get => _pageSize;
        set => _pageSize = 
            (value > maxPageSize) ? maxPageSize : value;
    }

    public string? OrderBy { get; set; }
    public string? Fields { get; set; }
    public bool? IsActive { get; set; }
}

            

ProductParameters

ProductParameters extends RequestParameters to provide product-specific filtering such as price range, category, company isolation, and date filtering.

  • Inherits pagination & sorting
  • Adds price filtering
  • Supports company & category filtering
  • Enables soft-delete filtering
ProductParameters Example

public class ProductParameters : RequestParameters
{
    public ProductParameters() => OrderBy = "ProductName";

    public Guid? RefCompanyID { get; set; }
    public Guid? RefCategoryID { get; set; }

    public decimal? MinPrice { get; set; } = 0;
    public decimal? MaxPrice { get; set; } = decimal.MaxValue;

    public bool? IsDeleted { get; set; } = false;
}

            
Architecture Flow

Controller → ProductParameters → Service → Repository → IQueryable Extensions → Database

Why This Design Is Strong
  • Prevents duplicated pagination logic
  • Supports scalable API filtering
  • Clean separation between query parameters and entities
  • Enables reusable IQueryable extension methods

ErrorSuccessResponse

ResponseFeatures provide a standardized structure for API responses. Instead of returning raw data, the application wraps results inside a consistent success/error model.

  • Standardizes API responses
  • Simplifies frontend handling
  • Improves error management
  • Supports generic typed responses
Generic Response Wrapper

public class ErrorSuccessResponse<T>
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public T? Data { get; set; }

    public ErrorSuccessResponse(
        bool success,
        string message,
        T? data = default)
    {
        Success = success;
        Message = message;
        Data = data;
    }
}

            
Non-Generic Response

public class ErrorSuccessResponse
{
    public bool Success { get; set; }
    public string Message { get; set; }

    public ErrorSuccessResponse(
        bool success,
        string message)
    {
        Success = success;
        Message = message;
    }
}

            
Usage Example (Service Layer)

return new ErrorSuccessResponse<ProductDto>(
    true,
    "Product created successfully.",
    mappedProduct);

            
Architecture Flow

Service → ErrorSuccessResponse → Controller → JSON Output → Frontend

Why This Pattern Is Strong
  • Consistent response contract
  • Reduces frontend condition complexity
  • Avoids mixing data with HTTP logic
  • Easily extendable (e.g., add error codes)
ZemBoilerplate.Logging (Logging Layer)

ZeMBoilerPlate.LoggerService

The Logger Service Layer provides centralized logging functionality using NLog. It abstracts logging logic from other layers and ensures consistent log handling.

  • Centralized logging abstraction
  • Uses NLog as logging provider
  • Decouples logging from business logic
  • Supports structured logging

ILoggerManager Interface

public interface ILoggerManager
{
    void LogInfo(string message);
    void LogWarn(string message);
    void LogError(string message);
    void LogDebug(string message);
}

            

LoggerManager Implementation

public class LoggerManager : ILoggerManager
{
    private static NLog.Logger logger =
        LogManager.GetCurrentClassLogger();

    public void LogInfo(string message) =>
        logger.Info(message);

    public void LogWarn(string message) =>
        logger.Warn(message);

    public void LogError(string message) =>
        logger.Error(message);

    public void LogDebug(string message) =>
        logger.Debug(message);
}

            

Dependency Injection

public static class DependencyInjection
{
    public static IServiceCollection 
        AddLoggerService(this IServiceCollection services)
    {
        services.AddSingleton<ILoggerManager, LoggerManager>();
        return services;
    }
}

            

NLog Configuration (nlog.config)

nlog.config defines log targets (file, console, database) and logging rules.

  • File logging
  • Console logging
  • Log level control

Architecture Role

Service Layer → ILoggerManager → NLog → File/Console

The LoggerService is a cross-cutting infrastructure component used across all layers.

ZemBoilerplate.Web (Presentation Layer)

In modern ASP.NET applications, Custom Attributes and Filters provide a clean and declarative way to apply cross-cutting concerns at the controller or action level. They allow developers to enforce authorization policies, logging, validation, and other behaviors without cluttering the controller logic. By centralizing these responsibilities, attributes and filters help maintain a thin, maintainable presentation layer, improve code reusability, and ensure consistent enforcement of business and security rules across the application.

HasPermissionAttribute

This attribute is used to enforce permission-based access control:


public class HasPermissionAttribute : AuthorizeAttribute
{
    public HasPermissionAttribute(string permission)
    {
        Policy = $"Permission:{permission}";
    }
}

    

    [HasPermission(Permissions.CategoriesManagement.Read)]
    public IActionResult Index()
    {
        return View();
    }

    

Middlewares in the ZemBoilerplate.Web presentation layer serves as the backbone for handling cross-cutting concerns in the application. They operate on every HTTP request and response, allowing the application to enforce consistent behavior such as global exception handling, request logging, authentication/authorization, and request/response manipulation. Middlewares enable centralized control over these processes, reducing code duplication and ensuring that critical policies are uniformly applied across the application.

Purpose

Middlewares in Berks.Web handle cross-cutting HTTP concerns for the application, including:

  • Global Exception Handling – Catching unhandled exceptions and returning appropriate error responses.
  • Request Logging – Capturing request and response details for monitoring, auditing, and debugging purposes.
  • Authentication / Authorization – Validating incoming requests and ensuring users have the required permissions.
  • Request / Response Manipulation – Adding, modifying, or rejecting requests and responses dynamically as needed.
            
public sealed class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILoggerManager _logger;
    private readonly IHostEnvironment _env;

    public ExceptionMiddleware(RequestDelegate next,ILoggerManager logger,IHostEnvironment env)
    {
        _next = next;
        _logger = logger;
        _env = env;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        var response = new
        {
            statusCode = context.Response.StatusCode,
            message = _env.IsDevelopment()
                ? exception.Message
                : "An unexpected error occurred.",
            detail = _env.IsDevelopment()
                ? exception.StackTrace
                : null
        };
        _logger.LogError($"Something went wrong: {response.message}");
        await context.Response.WriteAsync(JsonSerializer.Serialize(response));
    }
}
 

Request-scoped contexts are used to store and access data that is relevant only to the current HTTP request. This typically includes information such as the currently authenticated user and the active tenant. Instead of passing user or tenant data through method parameters across multiple layers (controllers, services, repositories), request-scoped contexts provide a centralized and consistent way to access this data anywhere within the same request lifecycle.

Responsibilities

  • Expose authenticated user identity details
  • Provide company / tenant identifiers for multi-tenant processing
  • Read user-related data from claims
  • Maintain request-level scope (new instance per HTTP request)
  • Avoid tight coupling with HttpContext
            
public interface IUserContext
{
    Guid UserId { get; }
    public Guid CompanyId { get; }
    public string Email { get; }
    public string DisplayName { get; }
    public string CompanyName { get; }
    bool IsSuperAdmin { get; }
    bool IsAdmin { get; }
}
 
            
public sealed class UserContext : IUserContext
{
    private readonly IHttpContextAccessor _http;

    public UserContext(IHttpContextAccessor http)
    {
        _http = http;
    }

    private HttpContext HttpContext =>
        _http.HttpContext
        ?? throw new UnauthorizedAccessException("No active HTTP context.");

    private ClaimsPrincipal User => HttpContext.User;

    public Guid UserId
    {
        get
        {
            if (!Guid.TryParse(
                User.FindFirst(ClaimTypes.NameIdentifier)?.Value,
                out var userId))
            {
                throw new UnauthorizedAccessException("Invalid or missing UserId claim.");
            }

            return userId;
        }
    }

    public Guid CompanyId
    {
        get
        {
            if (!Guid.TryParse(
                User.FindFirst(CustomClaimTypes.CompanyId)?.Value,
                out var claimCompanyId))
            {
                throw new UnauthorizedAccessException("CompanyId not found in session or claims.");
            }

            return claimCompanyId;
        }
    }

    
    public string Email =>
        User.FindFirst(ClaimTypes.Email)?.Value ?? string.Empty;

    public string DisplayName =>
        User.FindFirst(CustomClaimTypes.DisplayName)?.Value
        ?? User.Identity?.Name
        ?? string.Empty;

    public string CompanyName =>
        User.FindFirst(CustomClaimTypes.CompanyName)?.Value ?? string.Empty;

    public bool IsSuperAdmin =>
        bool.TryParse(
            User.FindFirst(CustomClaimTypes.IsSuperAdmin)?.Value,
            out var isSuperAdmin
        ) && isSuperAdmin;

    public bool IsAdmin =>
        bool.TryParse(
            User.FindFirst(CustomClaimTypes.IsAdmin)?.Value,
            out var isAdmin
        ) && isAdmin;

  
}

 

The Security & Claims Helpers in Berks.Web provide reusable functionality for authentication, authorization, and user permission management. They centralize common security tasks such as checking user roles, validating claims, and supporting policy-based authorization, making the system easier to maintain and secure.

Key Responsibilities

  • Authentication Helpers
    Simplify authentication tasks such as reading tokens or cookies to validate user identity. These helpers provide consistent methods to ensure that only authenticated users can access protected resources.
  • Claims Handling
    Read and verify claims from the ClaimsPrincipal object. This ensures that user permissions are checked reliably across controllers, views, and middleware.
  • Role / Permission Helpers
    Determine whether a user has specific roles such as Admin, Tenant Admin, or Super Admin. Centralizing this logic prevents repetitive permission checks throughout the codebase.
  • Reusable Security Logic
    Encapsulate permission-checking logic in a single location, supporting maintainable and clean policy-based authorization in controllers and Razor views.

PermissionTagHelper

            
    [HtmlTargetElement(Attributes = "asp-permission")]
    [HtmlTargetElement(Attributes = "asp-admin")]
    public class PermissionTagHelper : TagHelper
    {
        private readonly IAuthorizationService _authorizationService;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public PermissionTagHelper(
            IAuthorizationService authorizationService,
            IHttpContextAccessor httpContextAccessor)
        {
            _authorizationService = authorizationService;
            _httpContextAccessor = httpContextAccessor;
        }

        [HtmlAttributeName("asp-permission")]
        public string? Permission { get; set; }

        [HtmlAttributeName("asp-admin")]
        public bool AllowAdmin { get; set; }

        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            var user = _httpContextAccessor.HttpContext?.User;

            if (user == null)
            {
                output.SuppressOutput();
                return;
            }

            bool hasPermission = !string.IsNullOrWhiteSpace(Permission);

            
            if (IsAllowedByRole(user, hasPermission))
                return;

            
            if (!hasPermission)
            {
                output.SuppressOutput();
                return;
            }

            // ✅ Permission-based authorization
            foreach (var permission in Permission!.Split(
                ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
            {
                if (await IsAuthorizedAsync(user, permission))
                    return;
            }

            output.SuppressOutput();
        }

        // ===========================
        // 🔐 Authorization helpers
        // ===========================

        private bool IsAllowedByRole(ClaimsPrincipal user, bool hasPermission)
        {
            if (AllowAdmin && IsAdmin(user))
                return true;
            return false;
        }

        private async Task<bool> IsAuthorizedAsync(ClaimsPrincipal user, string permission)
        {
            return (await _authorizationService.AuthorizeAsync(user, permission)).Succeeded;
        }

        // ===========================
        // 👤 Role checks
        // ===========================

        private static bool IsAdmin(ClaimsPrincipal user) =>
            HasTrueClaim(user, "IsAdmin");

        private static bool HasTrueClaim(ClaimsPrincipal user, string claimType) =>
            user.Claims.Any(c =>
                c.Type == claimType &&
                string.Equals(c.Value, "true", StringComparison.OrdinalIgnoreCase));
    }
 

Usages In Razor View


<button class="btn btn-primary js-edit-category"
        asp-admin="true"
        asp-super-admin="true"
        asp-permission="@Permissions.UsersManagement.Create"
        data-bs-toggle="modal"
        data-bs-target="#addNewUpdateModal"
        data-id="">
    Add New User
</button>

Usages In Javascript


if (UI_PERMISSIONS.users.update) {
    buttons += `
        <a href="javascript:;"
           data-value="\${row.id}"
           data-bs-toggle="modal"
           data-bs-target="#addNewUpdateModal"
           class="btn btn-icon btn-text-secondary rounded-pill add-new-update-user">
            <i class="icon-base ri ri-edit-2-line icon-md"></i>
        </a>
    `;
}

The wwwroot folder in ZemBoilerplate.Web contains all static, publicly accessible assets required by the application’s front-end. These assets are served directly by the web server without passing through controllers or business logic.

Responsibilities

  • UI Styling and Layout – Define the visual structure, design consistency, and responsive behavior of the application interface.
  • Client-Side Interactivity – Enable dynamic behaviors such as form validation, modal interactions, AJAX requests, and real-time updates.
  • Visual Branding and Media – Incorporate logos, images, icons, fonts, and other branding assets to enhance user experience and identity.
  • Performance Optimization Through Static File Serving – Improve application performance by efficiently serving static assets (CSS, JavaScript, images) with caching, compression, and CDN support.

JavaScript Assets

  • Client-Side Interactivity – Enable dynamic user interactions such as button actions, form submissions, and real-time UI updates without full page reloads.
  • AJAX Requests to MVC/API Controllers – Communicate asynchronously with backend controllers to fetch, submit, or update data without disrupting the user experience.
  • DataTables Integration – Implement advanced table features including pagination, sorting, searching, and filtering for improved data presentation and usability.
  • Modal Handling – Manage modal dialogs for opening, closing, and confirming user actions such as create, update, or delete operations.
  • Front-End Validation and UI Behavior – Enforce client-side validation rules and dynamic UI behavior to enhance usability and reduce unnecessary server requests.

var CategoryManagement = (function () {

    let dataTable;
    let searchTimer;
    const permissions = window.UI_PERMISSIONS?.categories || {};

    /* =========================================
       INIT
    ========================================= */
    function init() {
        initTable();
        bindSearch();
        bindDelete();
        bindAddEdit();
    }

    /* =========================================
       DATATABLE
    ========================================= */
    function initTable() {
        dataTable = initServerDataTable(
            '#categoryTable',
            categoryUrls.getAll,
            getColumns(),
            {},
            getFilters
        );
    }

    function getColumns() {
        return [
            {
                data: null,
                orderable: false,
                className: 'text-center',
                render: function (data, type, row, meta) {
                    return meta.row + meta.settings._iDisplayStart + 1;
                }
            },
            {
                data: 'categoryName',
                name: 'CategoryName',
                className: 'text-center'
            },
            {
                data: 'isActive',
                name: 'IsActive',
                className: 'text-center',
                render: renderStatus
            },
            {
                data: null,
                orderable: false,
                searchable: false,
                className: 'text-center',
                render: renderActions
            }
        ];
    }

    function getFilters() {
        return {
            searchString: $('#searchString').val()
        };
    }

    /* =========================================
       RENDER FUNCTIONS
    ========================================= */
    function renderStatus(data) {
        return data
            ? '<span class="text-success fw-semibold">' +
              '<i class="bi bi-toggle-on fs-5"></i> Active</span>'
            : '<span class="text-muted fw-semibold">' +
              '<i class="bi bi-toggle-off fs-5"></i> Inactive</span>';
    }

    function renderActions(data, type, row) {

        let buttons = '';

        if (permissions.update) {
            buttons += `
                <button class="btn btn-icon text-primary js-edit-category"
                        data-id="\${row.id}"
                        data-bs-toggle="modal"
                        data-bs-target="#addNewUpdateModal">
                    <i class="bi bi-pencil-square"></i>
                </button>
            `;
        }

        if (permissions.delete) {
            buttons += `
                <button class="btn btn-sm btn-outline-danger js-delete-category"
                        data-id="\${row.id}">
                    <i class="bi bi-trash-fill"></i>
                </button>
            `;
        }

        if (!buttons)
            return '<span class="text-muted">-</span>';

        return `<div class="d-flex justify-content-center gap-2">\${buttons}</div>`;
    }

    /* =========================================
       PUBLIC METHODS
    ========================================= */
    function reload() {
        if (dataTable) {
            dataTable.ajax.reload(null, false);
        }
    }

    return {
        init: init,
        reload: reload
    };

})();