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
🔹 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. |
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"
}
Global Usings
-
Definition – Global Usings allow you to define
usingdirectives once and make them available across the entire project. -
Reduces Repetition – Eliminates the need to add the same
usingstatements 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.csorStartup.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
CompanyandProductusingDbSet<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
UserManagementschema. - 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)
_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
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
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.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.
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 theClaimsPrincipalobject. 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
};
})();