mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-02 03:53:24 +00:00
11 KiB
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
- Never skip layers - Always go Controller → Service → DataService
- No direct
_dbin Controllers - Database access only in DataService - Validate in Service - Use FluentValidation, not manual checks
- Always check userId - Return 401 if null
- Catch ValidationException first - Before generic Exception
- Use interfaces everywhere - For dependency injection
- Set audit fields - CreatedBy, CreatedDate, ModifiedBy, ModifiedDate
- 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