Skip to content

joserobertoarias/CompanyEmployees

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

CompanyEmployees API

A modern, enterprise-grade REST API built with .NET 8 demonstrating advanced software architecture patterns including CQRS (Command Query Responsibility Segregation), MediatR for request/response handling, and comprehensive validation. This project exemplifies clean code principles and scalable web application design.

πŸ“‹ Project Overview

The CompanyEmployees API is a RESTful web service that manages company and employee data. It showcases professional software architecture with clear separation of concerns through:

  • CQRS Pattern - Separating read (queries) and write (commands) operations
  • MediatR Pipeline - Request/response mediation with built-in validation
  • Repository Pattern - Data access abstraction
  • Dependency Injection - Clean service registration and resolution
  • Centralized Exception Handling - Consistent error responses

πŸ—οΈ Architecture

The solution follows a multi-layer architecture with CQRS pattern and strict separation of concerns:

CompanyEmployees (API Entry Point & Configuration)
β”œβ”€β”€ CompanyEmployees.Presentation (Controllers & HTTP Routing)
β”œβ”€β”€ Application (CQRS & MediatR - Queries, Commands, Handlers, Validators, Behaviors)
β”œβ”€β”€ Repository (Data Access Layer & EF Core)
β”œβ”€β”€ Contracts (Interface Definitions)
β”œβ”€β”€ Entities (Domain Models, DTOs, Exceptions)
β”œβ”€β”€ LoggerService (NLog Integration)
└── Shared (Shared Data Transfer Objects)

Layer Responsibilities

Layer Purpose
CompanyEmployees API startup, middleware configuration, DI setup, exception handling, MediatR/AutoMapper registration
CompanyEmployees.Presentation HTTP controllers, route handling, MediatR request dispatching (ISender/IPublisher)
Application CQRS implementation - Queries, Commands, Handlers, Validators, Pipeline behaviors, Notifications
Repository EF Core context, repository implementations, database queries
Contracts Interface definitions for repositories and services
Entities Domain models, custom exceptions, error models, DTOs
LoggerService NLog logging abstraction and implementation
Shared Shared DTOs and data structures

πŸ”§ Technologies & Frameworks

  • Framework: .NET 8
  • ORM: Entity Framework Core 8.0
  • Database: SQL Server
  • CQRS & Mediation: MediatR 14.0.0
  • Validation: FluentValidation 12.1.1
  • Mapping: AutoMapper 12.0.1
  • Logging: NLog
  • Language: C# 12 (with nullable reference types enabled)
  • API Style: RESTful with JSON serialization

πŸ“ Design Patterns

1. CQRS (Command Query Responsibility Segregation)

The application separates read operations (Queries) from write operations (Commands), improving scalability and maintainability:

Queries (Read-only operations in Application/Queries/):

public sealed record GetCompaniesQuery(bool TrackChanges) : IRequest<IEnumerable<CompanyDto>>;

public sealed record GetCompanyQuery(Guid Id, bool TrackChanges) : IRequest<CompanyDto>;

Commands (Write operations in Application/Commands/):

public sealed record CreateCompanyCommand(CompanyForCreationDto Company) : IRequest<CompanyDto>;

public sealed record UpdateCompanyCommand(Guid Id, CompanyForUpdateDto Company) : IRequest<Unit>;

public sealed record DeleteCompanyCommand(Guid Id) : IRequest<Unit>;

2. MediatR Request/Response Handling

MediatR provides a mediator pattern implementation for decoupled request handling:

Query Handlers (in Application/Handlers/):

internal sealed class GetCompaniesHandler : IRequestHandler<GetCompaniesQuery, IEnumerable<CompanyDto>>
{
    private readonly IRepositoryManager _repository;
    private readonly IMapper _mapper;

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

    public async Task<IEnumerable<CompanyDto>> Handle(GetCompaniesQuery request, 
        CancellationToken cancellationToken)
    {
        var companies = await _repository.Company.GetAllCompaniesAsync(request.TrackChanges);
        return _mapper.Map<IEnumerable<CompanyDto>>(companies);
    }
}

Command Handlers:

internal sealed class CreateCompanyHandler : IRequestHandler<CreateCompanyCommand, CompanyDto>
{
    private readonly IRepositoryManager _repository;
    private readonly IMapper _mapper;

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

    public async Task<CompanyDto> Handle(CreateCompanyCommand request, CancellationToken cancellationToken)
    {
        var companyEntity = _mapper.Map<Company>(request.Company);
        _repository.Company.CreateCompany(companyEntity);
        await _repository.SaveAsync();
        return _mapper.Map<CompanyDto>(companyEntity);
    }
}

3. MediatR Pipeline Behaviors

Pipeline behaviors act as middleware for the MediatR request/response pipeline, enabling cross-cutting concerns like validation:

public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) =>
        _validators = validators;

    public async Task<TResponse> Handle(TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);
        var failures = _validators
            .Select(x => x.Validate(context))
            .SelectMany(x => x.Errors)
            .Where(x => x != null)
            .ToList();

        if (failures.Any())
            throw new ValidationAppException(/* error details */);

        return await next();
    }
}

4. FluentValidation

Validators are defined for commands using FluentValidation fluent API:

public sealed class CreateCompanyCommandValidator : AbstractValidator<CreateCompanyCommand>
{
    public CreateCompanyCommandValidator()
    {
        RuleFor(c => c.Company.Name).NotEmpty().MaximumLength(60);
        RuleFor(c => c.Company.Address).NotEmpty().MaximumLength(60);
    }

    public override ValidationResult Validate(ValidationContext<CreateCompanyCommand> context)
    {
        return context.InstanceToValidate.Company is null
            ? new ValidationResult(new[] { new ValidationFailure("CompanyForCreationDto", 
                "CompanyForCreationDto object is null") })
            : base.Validate(context);
    }
}

5. MediatR Notifications

Publish notifications for domain events without direct coupling:

public sealed record CompanyDeletedNotification(Guid Id, bool TrackChanges) : INotification;

// In controller:
await _publisher.Publish(new CompanyDeletedNotification(id, TrackChanges: false));

6. AutoMapper for DTO Mapping

MappingProfile defines transformations between domain entities and DTOs:

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<Company, CompanyDto>()
            .ForMember(c => c.FullAddress,
                opt => opt.MapFrom(x => string.Join(' ', x.Address, x.Country)));

        CreateMap<CompanyForCreationDto, Company>();
        CreateMap<CompanyForUpdateDto, Company>();
    }
}

7. Repository Pattern

Data access is abstracted through repository interfaces:

public interface IRepositoryBase<T>
{
    IQueryable<T> FindAll(bool trackChanges);
    IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression, bool trackChanges);
    void Create(T entity);
    void Update(T entity);
    void Delete(T entity);
}

8. Lazy Initialization

Child repositories in RepositoryManager are initialized lazily:

private readonly Lazy<ICompanyRepository> _companyRepository;
public ICompanyRepository Company => _companyRepository.Value;

9. Dependency Injection

Services are registered through extension methods:

public static void ConfigureRepositoryManager(this IServiceCollection services) =>
    services.AddScoped<IRepositoryManager, RepositoryManager>();

10. Centralized Exception Handling

A global exception handler middleware catches exceptions and returns appropriate HTTP status codes:

  • BadRequestException β†’ HTTP 400
  • NotFoundException β†’ HTTP 404
  • ValidationAppException β†’ HTTP 422 (Unprocessable Entity)
  • Generic exceptions β†’ HTTP 500

πŸš€ Getting Started

Prerequisites

  • .NET 8 SDK
  • SQL Server (local or remote)
  • Visual Studio, Rider, or VS Code with C# extension

Build & Run

# Clone/open the repository
cd CompanyEmployees

# Restore dependencies
dotnet restore

# Build the solution
dotnet build

# Run the API
dotnet run --project CompanyEmployees

# API starts on: https://localhost:5001 (HTTPS) or http://localhost:5000 (HTTP)

Database Setup

The API uses Entity Framework Core for database management:

# Create or update database with migrations
dotnet ef database update -p CompanyEmployees

# Create a new migration after model changes
dotnet ef migrations add MigrationName -p CompanyEmployees

The connection string is configured in appsettings.json:

{
  "ConnectionStrings": {
    "sqlConnection": "Server=.;Database=CompanyEmployees;Trusted_Connection=true;"
  }
}

πŸ“ Project Structure

CompanyEmployees/
β”œβ”€β”€ CompanyEmployees/                          # Main API project
β”‚   β”œβ”€β”€ Program.cs                            # Startup, MediatR, AutoMapper, validation registration
β”‚   β”œβ”€β”€ MappingProfile.cs                     # AutoMapper entity-to-DTO mappings
β”‚   β”œβ”€β”€ appsettings.json                      # Configuration (connection strings, etc.)
β”‚   β”œβ”€β”€ nlog.config                           # NLog configuration
β”‚   β”œβ”€β”€ Extensions/
β”‚   β”‚   β”œβ”€β”€ ServiceExtensions.cs              # DI service registrations
β”‚   β”‚   └── ExceptionMiddlewareExtensions.cs  # Global exception handler middleware
β”‚   β”œβ”€β”€ Migrations/                           # EF Core migrations
β”‚   └── ContextFactory/                       # Migration context factory
β”‚
β”œβ”€β”€ CompanyEmployees.Presentation/             # Controllers and routing
β”‚   β”œβ”€β”€ Controllers/
β”‚   β”‚   └── CompaniesController.cs            # HTTP endpoints using MediatR (ISender/IPublisher)
β”‚   └── AssemblyReference.cs                  # Assembly reference for DI
β”‚
β”œβ”€β”€ Application/                               # CQRS & MediatR implementation
β”‚   β”œβ”€β”€ Queries/                              # Read-only operations
β”‚   β”‚   β”œβ”€β”€ GetCompaniesQuery.cs
β”‚   β”‚   β”œβ”€β”€ GetCompanyQuery.cs
β”‚   β”‚   └── GetEmployeesQuery.cs
β”‚   β”œβ”€β”€ Commands/                             # Write operations
β”‚   β”‚   β”œβ”€β”€ CreateCompanyCommand.cs
β”‚   β”‚   β”œβ”€β”€ UpdateCompanyCommand.cs
β”‚   β”‚   └── DeleteCompanyCommand.cs
β”‚   β”œβ”€β”€ Handlers/                             # Query & Command handlers
β”‚   β”‚   β”œβ”€β”€ GetCompaniesHandler.cs
β”‚   β”‚   β”œβ”€β”€ GetCompanyHandler.cs
β”‚   β”‚   β”œβ”€β”€ CreateCompanyHandler.cs
β”‚   β”‚   β”œβ”€β”€ UpdateCompanyHandler.cs
β”‚   β”‚   β”œβ”€β”€ DeleteCompanyHandler.cs
β”‚   β”‚   └── EmailHandler.cs
β”‚   β”œβ”€β”€ Validators/                           # FluentValidation validators
β”‚   β”‚   └── CreateCompanyCommandValidator.cs
β”‚   β”œβ”€β”€ Behaviors/                            # MediatR pipeline behaviors
β”‚   β”‚   └── ValidationBehavior.cs
β”‚   β”œβ”€β”€ Notifications/                        # Domain events
β”‚   β”‚   └── CompanyDeletedNotification.cs
β”‚   └── Application.csproj                    # MediatR, FluentValidation, AutoMapper dependencies
β”‚
β”œβ”€β”€ Repository/                                # Data access layer
β”‚   β”œβ”€β”€ RepositoryContext.cs                  # EF Core DbContext
β”‚   β”œβ”€β”€ RepositoryManager.cs                  # Repository manager with Lazy initialization
β”‚   β”œβ”€β”€ RepositoryBase.cs                     # Base repository implementation
β”‚   β”œβ”€β”€ CompanyRepository.cs                  # Company-specific repository
β”‚   └── Configuration/                        # EF entity configurations
β”‚
β”œβ”€β”€ Contracts/                                 # Interface definitions
β”‚   β”œβ”€β”€ IRepositoryManager.cs
β”‚   β”œβ”€β”€ IRepositoryBase.cs
β”‚   β”œβ”€β”€ ICompanyRepository.cs
β”‚   └── ILoggerManager.cs
β”‚
β”œβ”€β”€ Entities/                                  # Domain models and exceptions
β”‚   β”œβ”€β”€ Models/                               # Domain entities
β”‚   β”‚   └── Company.cs
β”‚   β”œβ”€β”€ Exceptions/                           # Custom exception classes
β”‚   β”‚   β”œβ”€β”€ BadRequestException.cs
β”‚   β”‚   β”œβ”€β”€ NotFoundException.cs
β”‚   β”‚   β”œβ”€β”€ ValidationAppException.cs
β”‚   β”‚   β”œβ”€β”€ CompanyNotFoundException.cs
β”‚   β”‚   └── ...
β”‚   └── ErrorModel/                           # Error response models
β”‚       └── ErrorDetails.cs
β”‚
β”œβ”€β”€ LoggerService/                             # Logging abstraction
β”‚   └── LoggerManager.cs                      # NLog implementation
β”‚
└── Shared/                                    # Shared DTOs
    └── DataTransferObjects/                  # DTO classes
        β”œβ”€β”€ CompanyDto.cs
        β”œβ”€β”€ CompanyForCreationDto.cs
        └── CompanyForUpdateDto.cs

πŸ”Œ Configuration

MediatR & Dependency Injection Setup (Program.cs)

// Register MediatR with Application assembly
builder.Services.AddMediatR(cfg => 
    cfg.RegisterServicesFromAssembly(typeof(Application.AssemblyReference).Assembly));

// Register AutoMapper
builder.Services.AddAutoMapper(typeof(Program));

// Register pipeline behaviors (e.g., ValidationBehavior)
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

// Register all FluentValidation validators from Application assembly
builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);

Controller Usage with MediatR (ISender/IPublisher)

Controllers inject ISender (for queries/commands) and IPublisher (for notifications):

[ApiController]
[Route("api/companies")]
public class CompaniesController : ControllerBase
{
    private readonly ISender _sender;
    private readonly IPublisher _publisher;

    public CompaniesController(ISender sender, IPublisher publisher)
    {
        _sender = sender;
        _publisher = publisher;
    }

    [HttpGet]
    public async Task<IActionResult> GetCompanies()
    {
        // Send query through MediatR pipeline
        var companies = await _sender.Send(new GetCompaniesQuery(TrackChanges: false));
        return Ok(companies);
    }

    [HttpPost]
    public async Task<IActionResult> CreateCompany([FromBody] CompanyForCreationDto company)
    {
        // Send command through MediatR pipeline (includes validation behavior)
        var createdCompany = await _sender.Send(new CreateCompanyCommand(company));
        return CreatedAtRoute("CompanyById", new { id = createdCompany.Id }, createdCompany);
    }

    [HttpDelete("{id:guid}")]
    public async Task<IActionResult> DeleteCompany(Guid id)
    {
        await _sender.Send(new DeleteCompanyCommand(id));
        
        // Publish domain event
        await _publisher.Publish(new CompanyDeletedNotification(id, TrackChanges: false));
        
        return NoContent();
    }
}

CORS Policy

Configured in ServiceExtensions.ConfigureCors() to allow all origins, methods, and headers. Modify as needed for production:

options.AddPolicy("CorsPolicy", builder =>
    builder.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader());

IIS Integration

Configured for scenarios where the API runs behind IIS or a reverse proxy:

services.Configure<IISOptions>(options => { });
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.All
});

Logging

NLog is configured via nlog.config. Logs can be sent to files, console, or other targets. Access the logger in middleware:

var logger = app.Services.GetRequiredService<ILoggerManager>();
logger.LogError($"Error message: {exception}");

πŸ”„ Request Flow with CQRS & MediatR

Query Flow (GetCompanies)

  1. Controller β†’ Injects ISender, calls _sender.Send(new GetCompaniesQuery(...))
  2. MediatR Pipeline β†’ Routes to GetCompaniesHandler
  3. Handler β†’ Calls repository, maps result using AutoMapper, returns DTO
  4. Controller β†’ Returns 200 Ok with results

Command Flow (CreateCompany)

  1. Controller β†’ Receives CompanyForCreationDto from request body
  2. Controller β†’ Calls _sender.Send(new CreateCompanyCommand(dto))
  3. MediatR Pipeline β†’ Enters ValidationBehavior
  4. ValidationBehavior β†’ Runs CreateCompanyCommandValidator through FluentValidation
  5. If Valid β†’ Routes to CreateCompanyHandler
  6. If Invalid β†’ Throws ValidationAppException, caught by global exception handler
  7. Handler β†’ Maps DTO to entity, persists to database via repository, returns DTO
  8. Controller β†’ Returns 201 Created with location and DTO

Notification Flow (CompanyDeleted)

  1. Controller β†’ Calls _publisher.Publish(new CompanyDeletedNotification(...))
  2. MediatR β†’ Finds all INotificationHandler<CompanyDeletedNotification> implementations
  3. Handlers β†’ Execute asynchronously (fire-and-forget pattern)

πŸ“ Common Development Tasks

Adding a New Query

  1. Create the Query in Application/Queries/:
public sealed record GetDepartmentsQuery(bool TrackChanges) 
    : IRequest<IEnumerable<DepartmentDto>>;
  1. Create the Query Handler in Application/Handlers/:
internal sealed class GetDepartmentsHandler 
    : IRequestHandler<GetDepartmentsQuery, IEnumerable<DepartmentDto>>
{
    private readonly IRepositoryManager _repository;
    private readonly IMapper _mapper;

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

    public async Task<IEnumerable<DepartmentDto>> Handle(GetDepartmentsQuery request, 
        CancellationToken cancellationToken)
    {
        var departments = await _repository.Department.GetAllDepartmentsAsync(request.TrackChanges);
        return _mapper.Map<IEnumerable<DepartmentDto>>(departments);
    }
}
  1. Add to Controller:
[HttpGet]
public async Task<IActionResult> GetDepartments()
{
    var departments = await _sender.Send(new GetDepartmentsQuery(TrackChanges: false));
    return Ok(departments);
}

Adding a New Command with Validation

  1. Create the Command in Application/Commands/:
public sealed record CreateDepartmentCommand(DepartmentForCreationDto Department) 
    : IRequest<DepartmentDto>;
  1. Create the Validator in Application/Validators/:
public sealed class CreateDepartmentCommandValidator 
    : AbstractValidator<CreateDepartmentCommand>
{
    public CreateDepartmentCommandValidator()
    {
        RuleFor(c => c.Department.Name)
            .NotEmpty()
            .MaximumLength(100);
    }

    public override ValidationResult Validate(
        ValidationContext<CreateDepartmentCommand> context)
    {
        return context.InstanceToValidate.Department is null
            ? new ValidationResult(new[] { new ValidationFailure("DepartmentForCreationDto", 
                "DepartmentForCreationDto object is null") })
            : base.Validate(context);
    }
}
  1. Create the Command Handler in Application/Handlers/:
internal sealed class CreateDepartmentHandler 
    : IRequestHandler<CreateDepartmentCommand, DepartmentDto>
{
    private readonly IRepositoryManager _repository;
    private readonly IMapper _mapper;

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

    public async Task<DepartmentDto> Handle(CreateDepartmentCommand request, 
        CancellationToken cancellationToken)
    {
        var departmentEntity = _mapper.Map<Department>(request.Department);
        _repository.Department.CreateDepartment(departmentEntity);
        await _repository.SaveAsync();
        return _mapper.Map<DepartmentDto>(departmentEntity);
    }
}
  1. Add to Controller:
[HttpPost]
public async Task<IActionResult> CreateDepartment(
    [FromBody] DepartmentForCreationDto department)
{
    var createdDepartment = await _sender.Send(
        new CreateDepartmentCommand(department));
    return CreatedAtRoute("DepartmentById", 
        new { id = createdDepartment.Id }, createdDepartment);
}
  1. Add DTOs in Shared/DataTransferObjects/

  2. Add AutoMapper Mapping in MappingProfile.cs:

CreateMap<Department, DepartmentDto>();
CreateMap<DepartmentForCreationDto, Department>();
CreateMap<DepartmentForUpdateDto, Department>();
  1. Add Repository (see "Adding a New Entity" below)

Adding a New Entity with Repository

  1. Create the entity in Entities/Models/:
public class Department
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
}
  1. Add to DbContext in Repository/RepositoryContext.cs:
public DbSet<Department> Departments { get; set; }
  1. Create repository interface in Contracts/:
public interface IDepartmentRepository : IRepositoryBase<Department>
{
    Task<IEnumerable<Department>> GetAllDepartmentsAsync(bool trackChanges);
    Task<Department?> GetDepartmentAsync(Guid id, bool trackChanges);
}
  1. Implement repository in Repository/:
public class DepartmentRepository : RepositoryBase<Department>, IDepartmentRepository
{
    public DepartmentRepository(RepositoryContext repositoryContext)
        : base(repositoryContext) { }

    public async Task<IEnumerable<Department>> GetAllDepartmentsAsync(bool trackChanges) =>
        await FindAll(trackChanges).ToListAsync();

    public async Task<Department?> GetDepartmentAsync(Guid id, bool trackChanges) =>
        await FindByCondition(d => d.Id.Equals(id), trackChanges).SingleOrDefaultAsync();
}
  1. Add to RepositoryManager in Repository/RepositoryManager.cs:
private readonly Lazy<IDepartmentRepository> _departmentRepository;

public IDepartmentRepository Department => _departmentRepository.Value;

// In constructor:
_departmentRepository = new Lazy<IDepartmentRepository>(() => 
    new DepartmentRepository(repositoryContext));
  1. Update IRepositoryManager in Contracts/IRepositoryManager.cs:
public interface IRepositoryManager
{
    ICompanyRepository Company { get; }
    IDepartmentRepository Department { get; }
    Task SaveAsync();
}

πŸ§ͺ Error Handling & Validation

Exception Hierarchy

The API uses typed exceptions for different error scenarios:

// Base classes
public abstract class BadRequestException : Exception { }
public abstract class NotFoundException : Exception { }
public class ValidationAppException : Exception { }

// Specific implementations
public class CompanyNotFoundException : NotFoundException { }
public class CompanyCollectionBadRequest : BadRequestException { }
public class IdParametersBadRequestException : BadRequestException { }

Global Exception Handler

The ExceptionMiddlewareExtensions.ConfigureExceptionHandler() middleware catches exceptions and maps them to HTTP responses:

context.Response.StatusCode = contextFeature.Error switch
{
    NotFoundException => StatusCodes.Status404NotFound,
    BadRequestException => StatusCodes.Status400BadRequest,
    ValidationAppException => StatusCodes.Status422UnprocessableEntity,
    _ => StatusCodes.Status500InternalServerError
};

Validation Pipeline

The ValidationBehavior in the MediatR pipeline intercepts all requests and validates them:

  1. Checks if validators exist for the request type
  2. Runs all applicable FluentValidation validators
  3. Collects all validation errors into a dictionary
  4. Throws ValidationAppException with grouped errors if validation fails
  5. Proceeds to handler if validation passes

Custom Exception Usage

// Throw exception if resource not found
if (company is null)
    throw new CompanyNotFoundException(id);

// Throw exception for invalid input
if (ids.Count == 0)
    throw new CollectionByIdsBadRequestException();

πŸ“Š Database

The project includes two migrations:

  • DatabaseCreation - Initial schema with Company and Employee entities
  • InitialData - Seed data for development

To reapply migrations:

dotnet ef database drop -p CompanyEmployees  # Remove database
dotnet ef database update -p CompanyEmployees # Recreate with migrations

Entity Framework Core Configuration:

  • Uses SQL Server as the database provider
  • Lazy loading is disabled (explicit loading required)
  • Change tracking is controlled via repository methods
  • Migrations track all schema changes

⚑ Performance Considerations

Change Tracking Strategy

  • Repositories use trackChanges parameter to control EF Core tracking
  • Set trackChanges = false for read-only queries
  • Set trackChanges = true only when modifications are needed

Lazy Repository Initialization

  • Child repositories in RepositoryManager use Lazy<T> to defer instantiation
  • Only created when first accessed, reducing overhead

Async/Await Pattern

  • All database operations are async
  • MediatR handlers use async/await for non-blocking I/O
  • Validation pipeline respects CancellationToken for cancellation support

πŸ” Security Considerations

  • HTTPS: Enable HTTPS in production (configured via appsettings)
  • CORS: Configure appropriate CORS policy for allowed origins
  • Input Validation: All input validated through FluentValidation before processing
  • Parameterized Queries: EF Core automatically uses parameterized queries
  • Exception Information: Exception details are never exposed in responses
  • Future: Authentication/Authorization: Add [Authorize] attributes and middleware for protected endpoints

πŸ“š References

πŸ“„ License

This project is provided as-is for educational and development purposes.

🀝 Contributing

Follow the established patterns when adding new features:

CQRS Guidelines

  • Separate read operations (Queries) from write operations (Commands)
  • Keep handlers focused and single-responsibility
  • Use IRequest<T> for queries/commands, IRequest<Unit> for commands with no return value
  • Return DTOs from queries/commands, not domain entities

Validation Guidelines

  • Create a FluentValidator for each command that modifies data
  • Validate object state in Validate() method override
  • Use RuleSets for complex validation scenarios
  • Let ValidationBehavior handle validationβ€”don't do it in handlers

Repository & Persistence

  • Keep business logic out of repositories (use handlers instead)
  • Repositories should only handle data access
  • Always use the trackChanges parameter appropriately
  • Call SaveAsync() after mutations

Naming Conventions

  • Queries: Get{Resource}Query, Get{Resources}Query
  • Commands: Create{Resource}Command, Update{Resource}Command, Delete{Resource}Command
  • Handlers: {Query/Command}Handler
  • DTOs: {Resource}Dto, {Resource}ForCreationDto, {Resource}ForUpdateDto
  • Exceptions: {Resource}NotFoundException, {Reason}BadRequestException

Code Organization

  • Keep handlers internal (internal sealed class) to prevent direct instantiation
  • Use sealed classes for handlers (optimization for performance)
  • Use records for queries, commands, and notifications (immutability, value equality)
  • Dependency injection through constructor

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages