mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 08:23:12 +00:00
18 KiB
18 KiB
Backend Architecture Guide
🏗️ Clean Architecture Overview
The backend follows Clean Architecture with strict layer separation:
┌─────────────────────────────────────────────────────────────┐
│ API LAYER (Controllers) │
│ - HTTP Request/Response │
│ - Authentication & Authorization │
│ - Exception Handling │
│ - Input Validation (basic) │
└─────────────────┬───────────────────────────────────────────┘
│ Calls
▼
┌─────────────────────────────────────────────────────────────┐
│ SERVICE LAYER (Business Logic) │
│ - Business Rules │
│ - FluentValidation (detailed validation) │
│ - Data Transformation │
│ - Orchestration │
└─────────────────┬───────────────────────────────────────────┘
│ Calls
▼
┌─────────────────────────────────────────────────────────────┐
│ DATA SERVICE LAYER (Persistence) │
│ - Database Access │
│ - Entity Operations (CRUD) │
│ - Query Building │
│ - Transaction Management │
└─────────────────┬───────────────────────────────────────────┘
│ Uses
▼
┌─────────────────────────────────────────────────────────────┐
│ DATA LAYER (EF Core) │
│ - DbContext │
│ - Entity Models │
│ - Database Schema │
└─────────────────────────────────────────────────────────────┘
📁 Project Structure
backend/shoc-backend/
│
├── Api.SeaHavenIndustries/ # 🌐 API Layer
│ ├── Controllers/ # HTTP endpoints
│ ├── DTOs/ # Data Transfer Objects (API contracts)
│ ├── Program.cs # App startup & DI registration
│ └── appsettings.json # Configuration
│
├── SeaHaven.Services/ # 💼 Service Layer (Business Logic)
│ ├── Interfaces/ # Service contracts (IContactService, etc.)
│ ├── Implementation/ # Service implementations
│ ├── DTOs/ # Internal DTOs for service layer
│ └── Validators/ # FluentValidation validators
│
├── SeaHaven.DataServices/ # 💾 Data Access Layer
│ ├── Interfaces/ # DataService contracts (IContactDataService, etc.)
│ ├── Implementation/ # DataService implementations
│ └── DTOs/ # Data layer DTOs
│
└── Data.SeaHavenIndustries/ # 🗄️ Data Layer (EF Core)
├── Models/ # Entity models (Contact, Employee, etc.)
├── ApplicationDbContext.cs # EF Core DbContext
└── Migrations/ # Database migrations
🔄 Data Flow Example (Creating a Contact)
1️⃣ API Layer (ContactController.cs)
[HttpPost("Create")]
public async Task<IActionResult> AddContacts(Contact_DTO dto)
{
try
{
// Get authenticated user ID
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
// Convert API DTO → Service DTO
var serviceDto = dto.ToServiceCreateDTO();
// Call Service Layer
await _contactService.CreateContactAsync(serviceDto, userId);
return Ok(new Response { Status = "Success", Message = "Contact created" });
}
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 = "Resource not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
Responsibilities:
- ✅ Authenticate user
- ✅ Extract user ID from claims
- ✅ Map API DTO to Service DTO
- ✅ Catch exceptions and return proper HTTP status codes
- ❌ NO business logic
- ❌ NO database access
2️⃣ Service Layer (ContactService.cs)
public class ContactService : IContactService
{
private readonly IContactDataService _dataService;
private readonly IValidator<CreateContactDTO> _createValidator;
public ContactService(
IContactDataService dataService,
IValidator<CreateContactDTO> createValidator)
{
_dataService = dataService;
_createValidator = createValidator;
}
public async Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId)
{
// STEP 1: Validate using FluentValidation
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
// STEP 2: Business logic (if any)
// Example: Check for duplicates, apply business rules, etc.
// STEP 3: Map Service DTO → Data DTO
var dataDto = new CreateContactDataDTO
{
FirstName = dto.FirstName,
LastName = dto.LastName,
Email = dto.Email,
Phone = dto.Phone
};
// STEP 4: Call Data Service Layer
var createdContact = await _dataService.CreateAsync(dataDto, userId);
// STEP 5: Map Data DTO → Service DTO and return
return new ContactDTO
{
Id = createdContact.Id,
FirstName = createdContact.FirstName,
LastName = createdContact.LastName,
Email = createdContact.Email,
Phone = createdContact.Phone
};
}
}
Responsibilities:
- ✅ Validate input using FluentValidation
- ✅ Apply business rules
- ✅ Orchestrate operations
- ✅ Transform data between layers
- ❌ NO HTTP concerns
- ❌ NO direct database access
3️⃣ Data Service Layer (ContactDataService.cs)
public class ContactDataService : IContactDataService
{
private readonly ApplicationDbContext _db;
public ContactDataService(ApplicationDbContext db)
{
_db = db;
}
public async Task<ContactDataDTO> CreateAsync(CreateContactDataDTO dto, string userId)
{
// STEP 1: Create entity from DTO
var contact = new Contact
{
FirstName = dto.FirstName,
LastName = dto.LastName,
Email = dto.Email,
Phone = dto.Phone,
CreatedBy = userId,
CreatedDate = DateTime.UtcNow,
ModifiedBy = userId,
ModifiedDate = DateTime.UtcNow
};
// STEP 2: Add to database
_db.Contacts.Add(contact);
await _db.SaveChangesAsync();
// STEP 3: Map entity → Data DTO and return
return new ContactDataDTO
{
Id = contact.Id,
FirstName = contact.FirstName,
LastName = contact.LastName,
Email = contact.Email,
Phone = contact.Phone
};
}
}
Responsibilities:
- ✅ Direct database operations (CRUD)
- ✅ Entity mapping
- ✅ Set audit fields (CreatedBy, CreatedDate, etc.)
- ✅ Transaction management
- ❌ NO validation
- ❌ NO business logic
🔌 Dependency Injection (DI)
Automatic Assembly Scanning
Services are automatically registered by scanning assemblies using naming convention.
In Program.cs:
builder.Services.AddDataServices(); // Auto-register all Data Services
builder.Services.AddBusinessServices(); // Auto-register all Business Services + Validators
How It Works
Module Files:
SeaHaven.DataServices/DependencyInjection/DataServicesModule.csSeaHaven.Services/DependencyInjection/ServicesModule.cs
Process:
- Scans assembly for all classes
- Finds interface matching pattern
I{ClassName} - Registers as Scoped service
- Also auto-registers FluentValidation validators
Naming Convention Required
| Interface | Implementation | Registered? |
|---|---|---|
IContactService |
ContactService |
✅ YES |
IContactDataService |
ContactDataService |
✅ YES |
ISomething |
SomethingDifferent |
❌ NO |
Rule: Interface must be I + Class name exactly
Usage in Code
Once registered, inject via constructor:
public class ContactController : Controller
{
private readonly IContactService _contactService;
public ContactController(IContactService contactService)
{
_contactService = contactService;
}
}
Benefits
✅ No manual registration - just follow naming convention ✅ Auto-discovery - new services work automatically ✅ FluentValidation auto-registered
✅ FluentValidation
Validator Example (CreateContactDTOValidator.cs)
public class CreateContactDTOValidator : AbstractValidator<CreateContactDTO>
{
public CreateContactDTOValidator()
{
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("First name is required")
.MaximumLength(50).WithMessage("First name cannot exceed 50 characters");
RuleFor(x => x.LastName)
.NotEmpty().WithMessage("Last name is required")
.MaximumLength(50).WithMessage("Last name cannot exceed 50 characters");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format")
.MaximumLength(100).WithMessage("Email cannot exceed 100 characters");
RuleFor(x => x.Phone)
.Matches(@"^\+?[1-9]\d{1,14}$").When(x => !string.IsNullOrEmpty(x.Phone))
.WithMessage("Invalid phone number format");
}
}
Where Validation Happens
❌ Controller → NO detailed validation (only catches ValidationException)
✅ Service → YES - FluentValidation runs here
❌ DataService → NO validation
🎯 Interface Examples
Service Interface (IContactService.cs)
public interface IContactService
{
Task<ContactDTO> CreateContactAsync(CreateContactDTO dto, string userId);
Task<ContactDTO> UpdateContactAsync(int id, UpdateContactDTO dto, string userId);
Task DeleteContactAsync(int id, string userId);
Task<ContactDTO> GetContactByIdAsync(int id);
Task<PagedResult<ContactDTO>> GetContactsPagedAsync(int page, int pageSize, string? search);
}
Data Service Interface (IContactDataService.cs)
public interface IContactDataService
{
Task<ContactDataDTO> CreateAsync(CreateContactDataDTO dto, string userId);
Task<ContactDataDTO> UpdateAsync(int id, UpdateContactDataDTO dto, string userId);
Task DeleteAsync(int id, string userId);
Task<ContactDataDTO?> GetByIdAsync(int id);
Task<PagedResult<ContactDataDTO>> GetPagedAsync(int page, int pageSize, string? search);
}
📦 DTO Types
1. API DTOs (Api.SeaHavenIndustries/DTOs/)
- Used for HTTP requests/responses
- Map to frontend JSON structure
- Example:
Contact_DTO,EditContact_DTO
2. Service DTOs (SeaHaven.Services/DTOs/)
- Used between Controller ↔ Service
- Business-focused structure
- Example:
CreateContactDTO,UpdateContactDTO,ContactDTO
3. Data DTOs (SeaHaven.DataServices/DTOs/)
- Used between Service ↔ DataService
- Database-focused structure
- Example:
CreateContactDataDTO,ContactDataDTO
DTO Mapping Flow
API DTO → Service DTO → Data DTO → Entity
(HTTP) (Business) (Data) (Database)
🚨 Exception Handling Pattern
Controller Exception Handling (STANDARD PATTERN)
[HttpPost("Create")]
public async Task<IActionResult> Create([FromBody] SomeDTO dto)
{
try
{
// Get user ID
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
// Call service
await _service.CreateAsync(dto, userId);
return Ok(new Response { Status = "Success", Message = "Created successfully" });
}
catch (ValidationException vex) // ⚠️ CATCH THIS FIRST
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (KeyNotFoundException) // ⚠️ THEN THIS
{
return NotFound(new Response { Status = "Error", Message = "Not found" });
}
catch (Exception ex) // ⚠️ GENERIC LAST
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
HTTP Status Code Mapping
- 200 OK → Success
- 400 Bad Request → Validation errors
- 401 Unauthorized → Not authenticated
- 404 Not Found → Resource not found
- 500 Internal Server Error → Unexpected errors
🔐 Authentication & User ID
Getting User ID (Two Methods)
Method 1: Using Claims (Preferred)
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
Method 2: Using UserManager (for Identity)
var userId = _userManager.GetUserId(User);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
Audit Fields
Every entity should track:
CreatedBy(string - User ID GUID)CreatedDate(DateTime)ModifiedBy(string - User ID GUID)ModifiedDate(DateTime)
📋 Naming Conventions
Controllers
- Named after entity + "Controller"
- Example:
ContactController,EmployeeController
Services
- Interface:
I[Entity]Service - Implementation:
[Entity]Service - Example:
IContactService,ContactService
Data Services
- Interface:
I[Entity]DataService - Implementation:
[Entity]DataService - Example:
IContactDataService,ContactDataService
DTOs
- API Layer:
[Entity]_DTO,Edit[Entity]_DTO - Service Layer:
Create[Entity]DTO,Update[Entity]DTO,[Entity]DTO - Data Layer:
Create[Entity]DataDTO,[Entity]DataDTO
✅ Best Practices Checklist
Controllers
- Use dependency injection for services
- Get user ID from
User.FindFirstValue(ClaimTypes.NameIdentifier) - Return 401 if userId is null
- Catch
ValidationExceptionBEFORE genericException - Return appropriate HTTP status codes
- NO business logic in controllers
- NO direct database access (
_db)
Services
- Validate using FluentValidation
- Throw
ValidationExceptionfor validation errors - Throw
KeyNotFoundExceptionfor not found - Pass userId to DataService
- Transform DTOs between layers
- Implement business rules here
Data Services
- Direct database access only
- Set audit fields (CreatedBy, CreatedDate, ModifiedBy, ModifiedDate)
- Use async/await for all database operations
- NO validation logic
- NO business logic
🎓 Quick Reference
| Layer | Purpose | Can Access | Cannot Access |
|---|---|---|---|
| Controller | HTTP handling, auth | Service layer | Database directly |
| Service | Business logic, validation | DataService layer | Database directly |
| DataService | Database operations | DbContext, Entities | HTTP context |
| Data | Entity definitions | Nothing (POCOs) | Application logic |
📚 Example: Full CRUD Implementation
See these files for complete examples:
- Contact:
ContactController.cs,ContactService.cs,ContactDataService.cs - Employee:
EmployeeController.cs,EmployeeService.cs,EmployeeDataService.cs - Asset:
AssetController.cs,AssetService.cs,AssetDataService.cs
All follow the same Clean Architecture pattern!