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

515 lines
14 KiB
Markdown

# Backend Quick Start Guide
## 🚀 Adding a New Feature (Step-by-Step)
### Example: Adding a "Product" Module
---
## Step 1: Create Entity (Data Layer)
**File**: `Data.SeaHavenIndustries/Models/Product.cs`
```csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public bool IsActive { get; set; }
// Audit fields (REQUIRED)
public string CreatedBy { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
public string ModifiedBy { get; set; } = string.Empty;
public DateTime ModifiedDate { get; set; }
}
```
**Add to DbContext**: `Data.SeaHavenIndustries/ApplicationDbContext.cs`
```csharp
public DbSet<Product> Products { get; set; }
```
---
## Step 2: Create Migration
```bash
cd backend/shoc-backend
dotnet ef migrations add AddProductTable --project Data.SeaHavenIndustries --startup-project Api.SeaHavenIndustries
dotnet ef database update --project Data.SeaHavenIndustries --startup-project Api.SeaHavenIndustries
```
---
## Step 3: Create DTOs
### 3A. Data Layer DTOs (`SeaHaven.DataServices/DTOs/`)
**CreateProductDataDTO.cs**
```csharp
public class CreateProductDataDTO
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
}
```
**ProductDataDTO.cs**
```csharp
public class ProductDataDTO
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public bool IsActive { get; set; }
}
```
### 3B. Service Layer DTOs (`SeaHaven.Services/DTOs/`)
**CreateProductDTO.cs**
```csharp
public class CreateProductDTO
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
}
```
**ProductDTO.cs**
```csharp
public class ProductDTO
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public bool IsActive { get; set; }
}
```
### 3C. API Layer DTOs (`Api.SeaHavenIndustries/DTOs/`)
**Product_DTO.cs**
```csharp
public class Product_DTO
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
}
```
---
## Step 4: Create FluentValidation Validator
**File**: `SeaHaven.Services/Validators/CreateProductDTOValidator.cs`
```csharp
using FluentValidation;
using SeaHaven.Services.DTOs;
public class CreateProductDTOValidator : AbstractValidator<CreateProductDTO>
{
public CreateProductDTOValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Product name is required")
.MaximumLength(100).WithMessage("Name cannot exceed 100 characters");
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Price must be greater than 0");
}
}
```
---
## Step 5: Create DataService
### 5A. Interface (`SeaHaven.DataServices/Interfaces/IProductDataService.cs`)
```csharp
public interface IProductDataService
{
Task<ProductDataDTO> CreateAsync(CreateProductDataDTO dto, string userId);
Task<ProductDataDTO> UpdateAsync(int id, UpdateProductDataDTO dto, string userId);
Task DeleteAsync(int id, string userId);
Task<ProductDataDTO?> GetByIdAsync(int id);
Task<PagedResult<ProductDataDTO>> GetPagedAsync(int page, int pageSize, string? search);
}
```
### 5B. Implementation (`SeaHaven.DataServices/Implementation/ProductDataService.cs`)
```csharp
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Models;
using Microsoft.EntityFrameworkCore;
public class ProductDataService : IProductDataService
{
private readonly ApplicationDbContext _db;
public ProductDataService(ApplicationDbContext db)
{
_db = db;
}
public async Task<ProductDataDTO> CreateAsync(CreateProductDataDTO dto, string userId)
{
var product = new Product
{
Name = dto.Name,
Description = dto.Description,
Price = dto.Price,
IsActive = true,
CreatedBy = userId,
CreatedDate = DateTime.UtcNow,
ModifiedBy = userId,
ModifiedDate = DateTime.UtcNow
};
_db.Products.Add(product);
await _db.SaveChangesAsync();
return new ProductDataDTO
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
IsActive = product.IsActive
};
}
public async Task<ProductDataDTO?> GetByIdAsync(int id)
{
var product = await _db.Products.FindAsync(id);
if (product == null) return null;
return new ProductDataDTO
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
IsActive = product.IsActive
};
}
// Implement other methods...
}
```
---
## Step 6: Create Service
### 6A. Interface (`SeaHaven.Services/Interfaces/IProductService.cs`)
```csharp
public interface IProductService
{
Task<ProductDTO> CreateProductAsync(CreateProductDTO dto, string userId);
Task<ProductDTO> UpdateProductAsync(int id, UpdateProductDTO dto, string userId);
Task DeleteProductAsync(int id, string userId);
Task<ProductDTO> GetProductByIdAsync(int id);
Task<PagedResult<ProductDTO>> GetProductsPagedAsync(int page, int pageSize, string? search);
}
```
### 6B. Implementation (`SeaHaven.Services/Implementation/ProductService.cs`)
```csharp
using FluentValidation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Interfaces;
public class ProductService : IProductService
{
private readonly IProductDataService _dataService;
private readonly IValidator<CreateProductDTO> _createValidator;
public ProductService(
IProductDataService dataService,
IValidator<CreateProductDTO> createValidator)
{
_dataService = dataService;
_createValidator = createValidator;
}
public async Task<ProductDTO> CreateProductAsync(CreateProductDTO dto, string userId)
{
// Validate
var validationResult = await _createValidator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
// Map to Data DTO
var dataDto = new CreateProductDataDTO
{
Name = dto.Name,
Description = dto.Description,
Price = dto.Price
};
// Call DataService
var created = await _dataService.CreateAsync(dataDto, userId);
// Map to Service DTO
return new ProductDTO
{
Id = created.Id,
Name = created.Name,
Description = created.Description,
Price = created.Price,
IsActive = created.IsActive
};
}
public async Task<ProductDTO> GetProductByIdAsync(int id)
{
var product = await _dataService.GetByIdAsync(id);
if (product == null)
throw new KeyNotFoundException($"Product with ID {id} not found");
return new ProductDTO
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
IsActive = product.IsActive
};
}
// Implement other methods...
}
```
---
## Step 7: Create Controller
**File**: `Api.SeaHavenIndustries/Controllers/ProductController.cs`
```csharp
using Api.SeaHavenIndustries.DTOs;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using SeaHaven.Services.Interfaces;
using System.Security.Claims;
[ApiController]
[Route("api/[controller]")]
public class ProductController : Controller
{
private readonly IProductService _productService;
public ProductController(IProductService productService)
{
_productService = productService;
}
[HttpPost("Create")]
public async Task<IActionResult> Create([FromBody] Product_DTO dto)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
var serviceDto = new CreateProductDTO
{
Name = dto.Name,
Description = dto.Description,
Price = dto.Price
};
await _productService.CreateProductAsync(serviceDto, userId);
return Ok(new Response { Status = "Success", Message = "Product created successfully" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet("GetById")]
public async Task<IActionResult> GetById(int id)
{
try
{
var product = await _productService.GetProductByIdAsync(id);
return Ok(product);
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Product not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
// Add other endpoints...
}
```
---
## Step 8: Services Auto-Registered ✅
**No action needed!** Services are automatically registered by assembly scanning.
If you followed naming conventions (`IProductService` → `ProductService`), your services are already registered.
**How?** These lines in `Program.cs` scan assemblies:
```csharp
builder.Services.AddDataServices(); // Scans SeaHaven.DataServices
builder.Services.AddBusinessServices(); // Scans SeaHaven.Services + Validators
```
---
## Step 9: Test
```bash
# Build
dotnet build
# Run
dotnet run --project Api.SeaHavenIndustries
# Test endpoint
curl -X POST "http://localhost:5141/api/Product/Create" \
-H "Content-Type: application/json" \
-d '{"name":"Test Product","description":"Test","price":99.99}'
```
---
## ✅ Checklist
- [ ] Entity created with audit fields
- [ ] DbSet added to DbContext
- [ ] Migration created and applied
- [ ] Data DTOs created
- [ ] Service DTOs created
- [ ] API DTOs created
- [ ] FluentValidation validator created
- [ ] DataService interface created
- [ ] DataService implementation created
- [ ] Service interface created
- [ ] Service implementation created
- [ ] Controller created
- [ ] Services registered in DI container
- [ ] Exception handling implemented
- [ ] User ID authentication added
- [ ] Build succeeds
- [ ] Endpoints tested
---
## 🎯 Common Patterns
### Get List with Pagination
```csharp
[HttpGet("GetList")]
public async Task<IActionResult> GetList(string? search = "", int page = 1, int pageSize = 10)
{
var result = await _productService.GetProductsPagedAsync(page, pageSize, search);
return Ok(new Pagination_DTO
{
Data = result.Items,
PageNumber = page,
PageSize = pageSize,
TotalCount = result.TotalCount,
TotalPages = (int)Math.Ceiling(result.TotalCount / (double)pageSize)
});
}
```
### Update
```csharp
[HttpPost("Update")]
public async Task<IActionResult> Update([FromBody] EditProduct_DTO dto)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _productService.UpdateProductAsync(dto.Id, dto.ToServiceDTO(), userId);
return Ok(new Response { Status = "Success", Message = "Updated" });
}
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 });
}
}
```
### Delete
```csharp
[HttpPost("Delete")]
public async Task<IActionResult> Delete(int id)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
await _productService.DeleteProductAsync(id, userId);
return Ok(new Response { Status = "Success", Message = "Deleted" });
}
catch (KeyNotFoundException)
{
return NotFound(new Response { Status = "Error", Message = "Not found" });
}
catch (Exception ex)
{
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
}
}
```
---
## 📚 Reference Implementations
For complete working examples, see:
- `ContactController`, `ContactService`, `ContactDataService`
- `EmployeeController`, `EmployeeService`, `EmployeeDataService`
- `AssetController`, `AssetService`, `AssetDataService`