shoc-backend/ARCHITECTURE_AT_A_GLANCE.md
2026-05-14 11:00:12 -05:00

11 KiB

Architecture At A Glance

📐 The 4-Layer Architecture

┌──────────────────────────────────────────────────────┐
│  🌐 API LAYER (Controllers)                          │
│  - HTTP Endpoints                                    │
│  - Authentication                                    │
│  - Exception → HTTP Status Code mapping             │
│  ❌ NO Business Logic                               │
│  ❌ NO Database Access                              │
└─────────────────┬────────────────────────────────────┘
                  │
                  │ Calls
                  ▼
┌──────────────────────────────────────────────────────┐
│  💼 SERVICE LAYER (Business Logic)                   │
│  - FluentValidation                                  │
│  - Business Rules                                    │
│  - Orchestration                                     │
│  - DTO Transformations                               │
│  ❌ NO HTTP Concerns                                │
│  ❌ NO Database Access                              │
└─────────────────┬────────────────────────────────────┘
                  │
                  │ Calls
                  ▼
┌──────────────────────────────────────────────────────┐
│  💾 DATA SERVICE LAYER (Persistence)                 │
│  - CRUD Operations                                   │
│  - Database Queries                                  │
│  - Audit Field Management                            │
│  ❌ NO Validation                                   │
│  ❌ NO Business Logic                               │
└─────────────────┬────────────────────────────────────┘
                  │
                  │ Uses
                  ▼
┌──────────────────────────────────────────────────────┐
│  🗄️ DATA LAYER (EF Core + Entities)                 │
│  - DbContext                                         │
│  - Entity Models (POCOs)                             │
│  - Migrations                                        │
└──────────────────────────────────────────────────────┘

🎯 Responsibilities Matrix

Layer What It Does What It Doesn't Do
Controller • Handle HTTP requests
• Authenticate users
• Map API DTOs
• Catch exceptions
• Return HTTP status codes
• Business logic
• Validation rules
• Database queries
Service • Validate data (FluentValidation)
• Apply business rules
• Coordinate operations
• Transform data
• HTTP concerns
• Database operations
DataService • Execute database queries
• CRUD operations
• Set audit fields
• Validation
• Business rules
Data • Define entities
• Database schema
• Application logic

🔄 Request Flow

Creating a Contact (Example)

1. HTTP POST /api/Contact/Create
   ↓
2. ContactController.Create()
   - Checks authentication
   - Gets userId from claims
   ↓
3. ContactService.CreateContactAsync()
   - Validates using FluentValidation
   - Applies business rules
   ↓
4. ContactDataService.CreateAsync()
   - Creates entity
   - Sets audit fields
   - Saves to database
   ↓
5. Returns through layers:
   Entity → DataDTO → ServiceDTO → ApiDTO → HTTP Response

📦 DTO Flow

Frontend (JSON)
    ↓
API DTO (Contact_DTO)          ← Controller receives this
    ↓
Service DTO (CreateContactDTO) ← Service validates this
    ↓
Data DTO (CreateContactDataDTO) ← DataService uses this
    ↓
Entity (Contact)                ← Database stores this

🔌 Dependency Injection

Automatic Assembly Scanning

Services auto-register via assembly scanning (no manual registration needed).

// Program.cs
builder.Services.AddDataServices();       // Auto-register data services
builder.Services.AddBusinessServices();   // Auto-register business services + validators

Naming Convention (REQUIRED)

Interface Implementation Registered?
IContactService ContactService ✅ YES
ISomething SomethingElse ❌ NO

Rule: Interface = I + Class name exactly


✅ Validation

Where and How

❌ Controller   → Catches ValidationException
✅ Service      → Runs FluentValidation.Validate()
❌ DataService  → No validation

FluentValidation Example

public class CreateContactDTOValidator : AbstractValidator<CreateContactDTO>
{
    public CreateContactDTOValidator()
    {
        RuleFor(x => x.FirstName)
            .NotEmpty().WithMessage("First name is required")
            .MaximumLength(50);
        
        RuleFor(x => x.Email)
            .EmailAddress().WithMessage("Invalid email");
    }
}

In Service

var validationResult = await _validator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
    throw new ValidationException(validationResult.Errors);
}

🚨 Exception Handling

Standard Pattern (All Controllers)

try
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    if (userId == null)
        return Unauthorized(...);
    
    await _service.SomeMethod(dto, userId);
    return Ok(...);
}
catch (ValidationException vex)        // 400 Bad Request
{
    return BadRequest(...);
}
catch (KeyNotFoundException)           // 404 Not Found
{
    return NotFound(...);
}
catch (Exception ex)                   // 500 Internal Server Error
{
    return StatusCode(500, ...);
}

🔐 Authentication

Getting User ID

// Method 1: Claims (Preferred)
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);

// Method 2: UserManager (for Identity)
var userId = _userManager.GetUserId(User);

// Always check for null
if (userId == null)
    return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });

Audit Fields

Every entity has:

public string CreatedBy { get; set; }      // User ID (GUID)
public DateTime CreatedDate { get; set; }  // UTC timestamp
public string ModifiedBy { get; set; }     // User ID (GUID)
public DateTime ModifiedDate { get; set; } // UTC timestamp

Set in DataService:

CreatedBy = userId,
CreatedDate = DateTime.UtcNow,
ModifiedBy = userId,
ModifiedDate = DateTime.UtcNow

📋 Naming Conventions

Component Pattern Example
Entity [Entity] Contact
Controller [Entity]Controller ContactController
Service Interface I[Entity]Service IContactService
Service Class [Entity]Service ContactService
DataService Interface I[Entity]DataService IContactDataService
DataService Class [Entity]DataService ContactDataService
API DTO [Entity]_DTO Contact_DTO
Service DTO [Action][Entity]DTO CreateContactDTO
Data DTO [Action][Entity]DataDTO CreateContactDataDTO
Validator [DTO]Validator CreateContactDTOValidator

🎨 Code Templates

Controller Method Template

[HttpPost("ActionName")]
public async Task<IActionResult> ActionName([FromBody] SomeDTO dto)
{
    try
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (userId == null)
            return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
        
        await _service.Method(dto, userId);
        
        return Ok(new Response { Status = "Success", Message = "Success message" });
    }
    catch (ValidationException vex)
    {
        var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
        return BadRequest(new Response { Status = "Validation Error", Message = errors });
    }
    catch (KeyNotFoundException)
    {
        return NotFound(new Response { Status = "Error", Message = "Not found" });
    }
    catch (Exception ex)
    {
        return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
    }
}

Service Method Template

public async Task<ResultDTO> MethodAsync(InputDTO dto, string userId)
{
    // 1. Validate
    var validationResult = await _validator.ValidateAsync(dto);
    if (!validationResult.IsValid)
    {
        throw new ValidationException(validationResult.Errors);
    }
    
    // 2. Business logic (if any)
    
    // 3. Call DataService
    var result = await _dataService.MethodAsync(dataDto, userId);
    
    // 4. Map and return
    return MapToServiceDTO(result);
}

DataService Method Template

public async Task<DataDTO> MethodAsync(InputDataDTO dto, string userId)
{
    // 1. Create/modify entity
    var entity = new Entity
    {
        Property = dto.Property,
        CreatedBy = userId,
        CreatedDate = DateTime.UtcNow,
        ModifiedBy = userId,
        ModifiedDate = DateTime.UtcNow
    };
    
    // 2. Database operation
    _db.Entities.Add(entity);
    await _db.SaveChangesAsync();
    
    // 3. Map and return
    return MapToDataDTO(entity);
}

📚 Key Rules

  1. Never skip layers - Always go Controller → Service → DataService
  2. No direct _db in Controllers - Database access only in DataService
  3. Validate in Service - Use FluentValidation, not manual checks
  4. Always check userId - Return 401 if null
  5. Catch ValidationException first - Before generic Exception
  6. Use interfaces everywhere - For dependency injection
  7. Set audit fields - CreatedBy, CreatedDate, ModifiedBy, ModifiedDate
  8. Return proper HTTP codes - 200, 400, 401, 404, 500

🎓 Learn More

  • Full Details: See BACKEND_ARCHITECTURE.md
  • Step-by-Step Guide: See QUICK_START_GUIDE.md
  • Working Examples: Look at ContactController, ContactService, ContactDataService