2026-05-14 11:00:12 -05:00
|
|
|
|
# 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`)
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
[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`)
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
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`)
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
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`:**
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
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.cs`
|
|
|
|
|
|
- `SeaHaven.Services/DependencyInjection/ServicesModule.cs`
|
|
|
|
|
|
|
|
|
|
|
|
**Process:**
|
|
|
|
|
|
1. Scans assembly for all classes
|
|
|
|
|
|
2. Finds interface matching pattern `I{ClassName}`
|
|
|
|
|
|
3. Registers as Scoped service
|
|
|
|
|
|
4. 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:
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
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`)
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
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`)
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
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`)
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
[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)**
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
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)**
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
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 `ValidationException` BEFORE generic `Exception`
|
|
|
|
|
|
- [ ] Return appropriate HTTP status codes
|
|
|
|
|
|
- [ ] NO business logic in controllers
|
|
|
|
|
|
- [ ] NO direct database access (`_db`)
|
|
|
|
|
|
|
|
|
|
|
|
### Services
|
|
|
|
|
|
- [ ] Validate using FluentValidation
|
|
|
|
|
|
- [ ] Throw `ValidationException` for validation errors
|
|
|
|
|
|
- [ ] Throw `KeyNotFoundException` for 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!
|
2026-06-05 11:56:54 -04:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 🔐 Configuration & Secrets
|
|
|
|
|
|
|
|
|
|
|
|
No secrets are stored in source. The committed `appsettings*.json` files hold only
|
|
|
|
|
|
`${PLACEHOLDER}` tokens; real values must be supplied at runtime through one of the
|
|
|
|
|
|
standard .NET configuration sources (later sources win):
|
|
|
|
|
|
|
|
|
|
|
|
1. `appsettings.json` / `appsettings.{Environment}.json` — committed, placeholders only.
|
|
|
|
|
|
2. **User Secrets** (local dev): `dotnet user-secrets set "Section:Key" "value"`.
|
|
|
|
|
|
3. **Environment variables** — use `__` (double underscore) as the section separator.
|
|
|
|
|
|
In AWS Elastic Beanstalk these are the environment properties.
|
|
|
|
|
|
|
|
|
|
|
|
See `.env.example` in the repo root for the full list. Required values:
|
|
|
|
|
|
|
|
|
|
|
|
| Setting | Env-var form | Used by | Notes |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| `ConnectionStrings:DefaultConnection` | `ConnectionStrings__DefaultConnection` | Both projects | SQL Server connection string |
|
|
|
|
|
|
| `SendGrid:ApiKey` | `SendGrid__ApiKey` | Both projects | Transactional email |
|
|
|
|
|
|
| `JWT:Secret` | `JWT__Secret` | `Api.SeaHavenIndustries` | JWT signing key |
|
|
|
|
|
|
| `GoogleMaps:ApiKey` | `GoogleMaps__ApiKey` | `SeaHavenIndustries` (Blazor) | Maps/Places; read in `App.razor` + `Home.razor` |
|
|
|
|
|
|
|
|
|
|
|
|
### AWS credentials
|
|
|
|
|
|
|
|
|
|
|
|
`UploadFileHp` (S3 uploads) uses the **AWS SDK default credential chain** — it no longer
|
|
|
|
|
|
hardcodes access keys. Credentials resolve from `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`
|
|
|
|
|
|
env vars, the shared profile/SSO, or (preferred in production) the EC2/ECS instance role.
|
|
|
|
|
|
The region comes from `AddDefaultAWSOptions` in `Program.cs`.
|
|
|
|
|
|
|
|
|
|
|
|
> ⚠️ The secrets previously committed to this repo (DB password, SendGrid key, JWT secret,
|
|
|
|
|
|
> Google Maps keys, AWS access key) should be considered compromised and **rotated**.
|
|
|
|
|
|
|