mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-01 05:13:14 +00:00
14 KiB
14 KiB
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
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
public DbSet<Product> Products { get; set; }
Step 2: Create Migration
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
public class CreateProductDataDTO
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
}
ProductDataDTO.cs
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
public class CreateProductDTO
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
}
ProductDTO.cs
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
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
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)
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)
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)
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)
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
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:
builder.Services.AddDataServices(); // Scans SeaHaven.DataServices
builder.Services.AddBusinessServices(); // Scans SeaHaven.Services + Validators
Step 9: Test
# 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
[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
[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
[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,ContactDataServiceEmployeeController,EmployeeService,EmployeeDataServiceAssetController,AssetService,AssetDataService