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

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, ContactDataService
  • EmployeeController, EmployeeService, EmployeeDataService
  • AssetController, AssetService, AssetDataService