mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 03:32:11 +00:00
fixing architecture
This commit is contained in:
parent
59385cf5b1
commit
57d378125f
5 changed files with 0 additions and 1758 deletions
|
|
@ -1,344 +0,0 @@
|
||||||
# Architecture At A Glance
|
|
||||||
|
|
||||||
## 📐 The 4-Layer Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────────────────────────────────────┐
|
|
||||||
│ 🌐 API LAYER (Controllers) │
|
|
||||||
│ - HTTP Endpoints │
|
|
||||||
│ - Authentication │
|
|
||||||
│ - Exception → HTTP Status Code mapping │
|
|
||||||
│ ❌ NO Business Logic │
|
|
||||||
│ ❌ NO Database Access │
|
|
||||||
└─────────────────┬────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
│ Calls
|
|
||||||
▼
|
|
||||||
┌──────────────────────────────────────────────────────┐
|
|
||||||
│ 💼 SERVICE LAYER (Business Logic) │
|
|
||||||
│ - FluentValidation │
|
|
||||||
│ - Business Rules │
|
|
||||||
│ - Orchestration │
|
|
||||||
│ - DTO Transformations │
|
|
||||||
│ ❌ NO HTTP Concerns │
|
|
||||||
│ ❌ NO Database Access │
|
|
||||||
└─────────────────┬────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
│ Calls
|
|
||||||
▼
|
|
||||||
┌──────────────────────────────────────────────────────┐
|
|
||||||
│ 💾 DATA SERVICE LAYER (Persistence) │
|
|
||||||
│ - CRUD Operations │
|
|
||||||
│ - Database Queries │
|
|
||||||
│ - Audit Field Management │
|
|
||||||
│ ❌ NO Validation │
|
|
||||||
│ ❌ NO Business Logic │
|
|
||||||
└─────────────────┬────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
│ Uses
|
|
||||||
▼
|
|
||||||
┌──────────────────────────────────────────────────────┐
|
|
||||||
│ 🗄️ DATA LAYER (EF Core + Entities) │
|
|
||||||
│ - DbContext │
|
|
||||||
│ - Entity Models (POCOs) │
|
|
||||||
│ - Migrations │
|
|
||||||
└──────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Responsibilities Matrix
|
|
||||||
|
|
||||||
| Layer | What It Does | What It Doesn't Do |
|
|
||||||
|-------|--------------|-------------------|
|
|
||||||
| **Controller** | • Handle HTTP requests<br>• Authenticate users<br>• Map API DTOs<br>• Catch exceptions<br>• Return HTTP status codes | • Business logic<br>• Validation rules<br>• Database queries |
|
|
||||||
| **Service** | • Validate data (FluentValidation)<br>• Apply business rules<br>• Coordinate operations<br>• Transform data | • HTTP concerns<br>• Database operations |
|
|
||||||
| **DataService** | • Execute database queries<br>• CRUD operations<br>• Set audit fields | • Validation<br>• Business rules |
|
|
||||||
| **Data** | • Define entities<br>• Database schema | • Application logic |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Request Flow
|
|
||||||
|
|
||||||
### Creating a Contact (Example)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. HTTP POST /api/Contact/Create
|
|
||||||
↓
|
|
||||||
2. ContactController.Create()
|
|
||||||
- Checks authentication
|
|
||||||
- Gets userId from claims
|
|
||||||
↓
|
|
||||||
3. ContactService.CreateContactAsync()
|
|
||||||
- Validates using FluentValidation
|
|
||||||
- Applies business rules
|
|
||||||
↓
|
|
||||||
4. ContactDataService.CreateAsync()
|
|
||||||
- Creates entity
|
|
||||||
- Sets audit fields
|
|
||||||
- Saves to database
|
|
||||||
↓
|
|
||||||
5. Returns through layers:
|
|
||||||
Entity → DataDTO → ServiceDTO → ApiDTO → HTTP Response
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 DTO Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
Frontend (JSON)
|
|
||||||
↓
|
|
||||||
API DTO (Contact_DTO) ← Controller receives this
|
|
||||||
↓
|
|
||||||
Service DTO (CreateContactDTO) ← Service validates this
|
|
||||||
↓
|
|
||||||
Data DTO (CreateContactDataDTO) ← DataService uses this
|
|
||||||
↓
|
|
||||||
Entity (Contact) ← Database stores this
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔌 Dependency Injection
|
|
||||||
|
|
||||||
### Automatic Assembly Scanning
|
|
||||||
|
|
||||||
Services auto-register via assembly scanning (no manual registration needed).
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Program.cs
|
|
||||||
builder.Services.AddDataServices(); // Auto-register data services
|
|
||||||
builder.Services.AddBusinessServices(); // Auto-register business services + validators
|
|
||||||
```
|
|
||||||
|
|
||||||
### Naming Convention (REQUIRED)
|
|
||||||
|
|
||||||
| Interface | Implementation | Registered? |
|
|
||||||
|-----------|---------------|-------------|
|
|
||||||
| `IContactService` | `ContactService` | ✅ YES |
|
|
||||||
| `ISomething` | `SomethingElse` | ❌ NO |
|
|
||||||
|
|
||||||
**Rule**: Interface = `I` + Class name exactly
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Validation
|
|
||||||
|
|
||||||
### Where and How
|
|
||||||
|
|
||||||
```
|
|
||||||
❌ Controller → Catches ValidationException
|
|
||||||
✅ Service → Runs FluentValidation.Validate()
|
|
||||||
❌ DataService → No validation
|
|
||||||
```
|
|
||||||
|
|
||||||
### FluentValidation Example
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class CreateContactDTOValidator : AbstractValidator<CreateContactDTO>
|
|
||||||
{
|
|
||||||
public CreateContactDTOValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.FirstName)
|
|
||||||
.NotEmpty().WithMessage("First name is required")
|
|
||||||
.MaximumLength(50);
|
|
||||||
|
|
||||||
RuleFor(x => x.Email)
|
|
||||||
.EmailAddress().WithMessage("Invalid email");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### In Service
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var validationResult = await _validator.ValidateAsync(dto);
|
|
||||||
if (!validationResult.IsValid)
|
|
||||||
{
|
|
||||||
throw new ValidationException(validationResult.Errors);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 Exception Handling
|
|
||||||
|
|
||||||
### Standard Pattern (All Controllers)
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
||||||
if (userId == null)
|
|
||||||
return Unauthorized(...);
|
|
||||||
|
|
||||||
await _service.SomeMethod(dto, userId);
|
|
||||||
return Ok(...);
|
|
||||||
}
|
|
||||||
catch (ValidationException vex) // 400 Bad Request
|
|
||||||
{
|
|
||||||
return BadRequest(...);
|
|
||||||
}
|
|
||||||
catch (KeyNotFoundException) // 404 Not Found
|
|
||||||
{
|
|
||||||
return NotFound(...);
|
|
||||||
}
|
|
||||||
catch (Exception ex) // 500 Internal Server Error
|
|
||||||
{
|
|
||||||
return StatusCode(500, ...);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Authentication
|
|
||||||
|
|
||||||
### Getting User ID
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Method 1: Claims (Preferred)
|
|
||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
||||||
|
|
||||||
// Method 2: UserManager (for Identity)
|
|
||||||
var userId = _userManager.GetUserId(User);
|
|
||||||
|
|
||||||
// Always check for null
|
|
||||||
if (userId == null)
|
|
||||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
|
||||||
```
|
|
||||||
|
|
||||||
### Audit Fields
|
|
||||||
|
|
||||||
Every entity has:
|
|
||||||
```csharp
|
|
||||||
public string CreatedBy { get; set; } // User ID (GUID)
|
|
||||||
public DateTime CreatedDate { get; set; } // UTC timestamp
|
|
||||||
public string ModifiedBy { get; set; } // User ID (GUID)
|
|
||||||
public DateTime ModifiedDate { get; set; } // UTC timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
Set in DataService:
|
|
||||||
```csharp
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatedDate = DateTime.UtcNow,
|
|
||||||
ModifiedBy = userId,
|
|
||||||
ModifiedDate = DateTime.UtcNow
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Naming Conventions
|
|
||||||
|
|
||||||
| Component | Pattern | Example |
|
|
||||||
|-----------|---------|---------|
|
|
||||||
| Entity | `[Entity]` | `Contact` |
|
|
||||||
| Controller | `[Entity]Controller` | `ContactController` |
|
|
||||||
| Service Interface | `I[Entity]Service` | `IContactService` |
|
|
||||||
| Service Class | `[Entity]Service` | `ContactService` |
|
|
||||||
| DataService Interface | `I[Entity]DataService` | `IContactDataService` |
|
|
||||||
| DataService Class | `[Entity]DataService` | `ContactDataService` |
|
|
||||||
| API DTO | `[Entity]_DTO` | `Contact_DTO` |
|
|
||||||
| Service DTO | `[Action][Entity]DTO` | `CreateContactDTO` |
|
|
||||||
| Data DTO | `[Action][Entity]DataDTO` | `CreateContactDataDTO` |
|
|
||||||
| Validator | `[DTO]Validator` | `CreateContactDTOValidator` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Code Templates
|
|
||||||
|
|
||||||
### Controller Method Template
|
|
||||||
```csharp
|
|
||||||
[HttpPost("ActionName")]
|
|
||||||
public async Task<IActionResult> ActionName([FromBody] SomeDTO dto)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
||||||
if (userId == null)
|
|
||||||
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
|
||||||
|
|
||||||
await _service.Method(dto, userId);
|
|
||||||
|
|
||||||
return Ok(new Response { Status = "Success", Message = "Success message" });
|
|
||||||
}
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Service Method Template
|
|
||||||
```csharp
|
|
||||||
public async Task<ResultDTO> MethodAsync(InputDTO dto, string userId)
|
|
||||||
{
|
|
||||||
// 1. Validate
|
|
||||||
var validationResult = await _validator.ValidateAsync(dto);
|
|
||||||
if (!validationResult.IsValid)
|
|
||||||
{
|
|
||||||
throw new ValidationException(validationResult.Errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Business logic (if any)
|
|
||||||
|
|
||||||
// 3. Call DataService
|
|
||||||
var result = await _dataService.MethodAsync(dataDto, userId);
|
|
||||||
|
|
||||||
// 4. Map and return
|
|
||||||
return MapToServiceDTO(result);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### DataService Method Template
|
|
||||||
```csharp
|
|
||||||
public async Task<DataDTO> MethodAsync(InputDataDTO dto, string userId)
|
|
||||||
{
|
|
||||||
// 1. Create/modify entity
|
|
||||||
var entity = new Entity
|
|
||||||
{
|
|
||||||
Property = dto.Property,
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatedDate = DateTime.UtcNow,
|
|
||||||
ModifiedBy = userId,
|
|
||||||
ModifiedDate = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
// 2. Database operation
|
|
||||||
_db.Entities.Add(entity);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
|
|
||||||
// 3. Map and return
|
|
||||||
return MapToDataDTO(entity);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Key Rules
|
|
||||||
|
|
||||||
1. **Never skip layers** - Always go Controller → Service → DataService
|
|
||||||
2. **No direct `_db` in Controllers** - Database access only in DataService
|
|
||||||
3. **Validate in Service** - Use FluentValidation, not manual checks
|
|
||||||
4. **Always check userId** - Return 401 if null
|
|
||||||
5. **Catch ValidationException first** - Before generic Exception
|
|
||||||
6. **Use interfaces everywhere** - For dependency injection
|
|
||||||
7. **Set audit fields** - CreatedBy, CreatedDate, ModifiedBy, ModifiedDate
|
|
||||||
8. **Return proper HTTP codes** - 200, 400, 401, 404, 500
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎓 Learn More
|
|
||||||
|
|
||||||
- **Full Details**: See `BACKEND_ARCHITECTURE.md`
|
|
||||||
- **Step-by-Step Guide**: See `QUICK_START_GUIDE.md`
|
|
||||||
- **Working Examples**: Look at `ContactController`, `ContactService`, `ContactDataService`
|
|
||||||
|
|
@ -1,553 +0,0 @@
|
||||||
/*
|
|
||||||
===================================================================================
|
|
||||||
CALENDAR/EVENTS MODULE - COMPLETE IMPLEMENTATION
|
|
||||||
===================================================================================
|
|
||||||
This file contains everything needed for the Calendar/Events module:
|
|
||||||
1. Events Model (Database Entity)
|
|
||||||
2. Event_DTO (for GET responses)
|
|
||||||
3. EditEvent_DTO (for POST/PUT requests)
|
|
||||||
4. CalendarController (API endpoints)
|
|
||||||
5. SQL Script for database table creation and sample data
|
|
||||||
|
|
||||||
Author: AI Agent
|
|
||||||
Date: 2026-03-30
|
|
||||||
===================================================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Api.SeaHavenIndustries.DTOs;
|
|
||||||
using Data.SeaHavenIndustries;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
// ===================================================================================
|
|
||||||
// SECTION 1: DATABASE MODEL (Data.SeaHavenIndustries/Models/Events.cs)
|
|
||||||
// ===================================================================================
|
|
||||||
|
|
||||||
namespace Data.SeaHavenIndustries
|
|
||||||
{
|
|
||||||
public class Events : FullAuditEntity
|
|
||||||
{
|
|
||||||
public string Title { get; set; } = null!;
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public string? Location { get; set; }
|
|
||||||
|
|
||||||
// Related Record (can link to Accounts, Contacts, Work Orders, etc.)
|
|
||||||
public int? RecordID { get; set; }
|
|
||||||
public string? RecordName { get; set; }
|
|
||||||
public string? RecordTargetType { get; set; }
|
|
||||||
|
|
||||||
// Reminder Settings
|
|
||||||
public int ReminderMinutes { get; set; } = 15; // Default: 15 minutes before
|
|
||||||
|
|
||||||
// Color for calendar display
|
|
||||||
public string EventColor { get; set; } = "primary"; // primary, success, info, warning, danger
|
|
||||||
|
|
||||||
// Schedule
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
public string? StartTime { get; set; } // e.g., "08:00 AM"
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
public string? EndTime { get; set; } // e.g., "08:30 AM"
|
|
||||||
public bool AllDay { get; set; } = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================================================
|
|
||||||
// SECTION 2: DTOs (Api.SeaHavenIndustries/DTOs/Event_DTO.cs)
|
|
||||||
// ===================================================================================
|
|
||||||
|
|
||||||
namespace Api.SeaHavenIndustries.DTOs
|
|
||||||
{
|
|
||||||
// DTO for GET responses
|
|
||||||
public class Event_DTO
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Title { get; set; } = null!;
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public string? Location { get; set; }
|
|
||||||
|
|
||||||
public int? RecordID { get; set; }
|
|
||||||
public string? RecordName { get; set; }
|
|
||||||
public string? RecordTargetType { get; set; }
|
|
||||||
|
|
||||||
public int ReminderMinutes { get; set; }
|
|
||||||
public string EventColor { get; set; } = null!;
|
|
||||||
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
public string? StartTime { get; set; }
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
public string? EndTime { get; set; }
|
|
||||||
public bool AllDay { get; set; }
|
|
||||||
|
|
||||||
public DateTime? CreatedDate { get; set; }
|
|
||||||
public string? createdby { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// DTO for POST/PUT requests (Create/Edit)
|
|
||||||
public class EditEvent_DTO
|
|
||||||
{
|
|
||||||
public int? Id { get; set; } // Null for create, populated for edit
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "Please enter a title")]
|
|
||||||
[StringLength(100, ErrorMessage = "Title cannot be longer than 100 characters")]
|
|
||||||
public string Title { get; set; } = null!;
|
|
||||||
|
|
||||||
public string? Description { get; set; }
|
|
||||||
|
|
||||||
[StringLength(150, ErrorMessage = "Location cannot be longer than 150 characters")]
|
|
||||||
public string? Location { get; set; }
|
|
||||||
|
|
||||||
public int? RecordID { get; set; }
|
|
||||||
public string? RecordName { get; set; }
|
|
||||||
public string? RecordTargetType { get; set; }
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "The Remind Attendees field is required")]
|
|
||||||
public int ReminderMinutes { get; set; } = 15;
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "The Select a Calendar Color field is required")]
|
|
||||||
public string EventColor { get; set; } = "primary";
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "Please select a start date")]
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "Please select a start time")]
|
|
||||||
public string StartTime { get; set; } = null!;
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "Please select an end date")]
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
|
|
||||||
[Required(ErrorMessage = "Please select an end time")]
|
|
||||||
public string EndTime { get; set; } = null!;
|
|
||||||
|
|
||||||
public bool AllDay { get; set; } = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================================================
|
|
||||||
// SECTION 3: CONTROLLER (Api.SeaHavenIndustries/Controllers/CalendarController.cs)
|
|
||||||
// ===================================================================================
|
|
||||||
|
|
||||||
namespace Api.SeaHavenIndustries.Controllers
|
|
||||||
{
|
|
||||||
//[Authorize] // Uncomment when authentication is needed
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/Calendar")]
|
|
||||||
public class CalendarController : Controller
|
|
||||||
{
|
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
|
||||||
private readonly ApplicationDbContext _db;
|
|
||||||
|
|
||||||
public CalendarController(UserManager<ApplicationUser> userManager, ApplicationDbContext db)
|
|
||||||
{
|
|
||||||
_userManager = userManager;
|
|
||||||
_db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================================================
|
|
||||||
// CREATE EVENT
|
|
||||||
// ===================================================================================
|
|
||||||
[HttpPost]
|
|
||||||
[Route("AddEvent")]
|
|
||||||
public async Task<IActionResult> AddEvent(EditEvent_DTO eventDto)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var model = new Events
|
|
||||||
{
|
|
||||||
Title = eventDto.Title,
|
|
||||||
Description = eventDto.Description,
|
|
||||||
Location = eventDto.Location,
|
|
||||||
RecordID = eventDto.RecordID,
|
|
||||||
RecordName = eventDto.RecordName,
|
|
||||||
RecordTargetType = eventDto.RecordTargetType,
|
|
||||||
ReminderMinutes = eventDto.ReminderMinutes,
|
|
||||||
EventColor = eventDto.EventColor,
|
|
||||||
StartDate = eventDto.StartDate,
|
|
||||||
StartTime = eventDto.StartTime,
|
|
||||||
EndDate = eventDto.EndDate,
|
|
||||||
EndTime = eventDto.EndTime,
|
|
||||||
AllDay = eventDto.AllDay,
|
|
||||||
CreatedDate = DateTime.Now,
|
|
||||||
IsDeleted = false
|
|
||||||
};
|
|
||||||
|
|
||||||
_db.Events.Add(model);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Ok(new DataResponse { Message = "Event Created Successfully", Status = "200" });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================================================
|
|
||||||
// UPDATE EVENT
|
|
||||||
// ===================================================================================
|
|
||||||
[HttpPost]
|
|
||||||
[Route("EditEvent")]
|
|
||||||
public async Task<IActionResult> EditEvent(EditEvent_DTO eventDto)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!eventDto.Id.HasValue)
|
|
||||||
{
|
|
||||||
return BadRequest(new Response { Status = "Error", Message = "Event ID is required for editing" });
|
|
||||||
}
|
|
||||||
|
|
||||||
var model = await _db.Events.FindAsync(eventDto.Id.Value);
|
|
||||||
if (model == null || model.IsDeleted == true)
|
|
||||||
{
|
|
||||||
return NotFound(new Response { Status = "Error", Message = "Event not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
model.Title = eventDto.Title;
|
|
||||||
model.Description = eventDto.Description;
|
|
||||||
model.Location = eventDto.Location;
|
|
||||||
model.RecordID = eventDto.RecordID;
|
|
||||||
model.RecordName = eventDto.RecordName;
|
|
||||||
model.RecordTargetType = eventDto.RecordTargetType;
|
|
||||||
model.ReminderMinutes = eventDto.ReminderMinutes;
|
|
||||||
model.EventColor = eventDto.EventColor;
|
|
||||||
model.StartDate = eventDto.StartDate;
|
|
||||||
model.StartTime = eventDto.StartTime;
|
|
||||||
model.EndDate = eventDto.EndDate;
|
|
||||||
model.EndTime = eventDto.EndTime;
|
|
||||||
model.AllDay = eventDto.AllDay;
|
|
||||||
model.LastModificationTime = DateTime.Now;
|
|
||||||
|
|
||||||
_db.Events.Update(model);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Ok(new DataResponse { Message = "Event Updated Successfully", Status = "200" });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================================================
|
|
||||||
// GET EVENT BY ID
|
|
||||||
// ===================================================================================
|
|
||||||
[HttpGet]
|
|
||||||
[Route("GetEventById/{id}")]
|
|
||||||
public async Task<IActionResult> GetEventById(int id)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var model = await _db.Events
|
|
||||||
.Where(e => e.Id == id && e.IsDeleted != true)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (model == null)
|
|
||||||
{
|
|
||||||
return NotFound(new Response { Status = "Error", Message = "Event not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
var dto = new Event_DTO
|
|
||||||
{
|
|
||||||
Id = model.Id,
|
|
||||||
Title = model.Title,
|
|
||||||
Description = model.Description,
|
|
||||||
Location = model.Location,
|
|
||||||
RecordID = model.RecordID,
|
|
||||||
RecordName = model.RecordName,
|
|
||||||
RecordTargetType = model.RecordTargetType,
|
|
||||||
ReminderMinutes = model.ReminderMinutes,
|
|
||||||
EventColor = model.EventColor,
|
|
||||||
StartDate = model.StartDate,
|
|
||||||
StartTime = model.StartTime,
|
|
||||||
EndDate = model.EndDate,
|
|
||||||
EndTime = model.EndTime,
|
|
||||||
AllDay = model.AllDay,
|
|
||||||
CreatedDate = model.CreatedDate,
|
|
||||||
createdby = model.createdby
|
|
||||||
};
|
|
||||||
|
|
||||||
return Ok(new DataResponse { Message = "Success", Status = "200", Data = dto });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ===================================================================================
|
|
||||||
// DELETE EVENT
|
|
||||||
// ===================================================================================
|
|
||||||
[HttpDelete]
|
|
||||||
[Route("DeleteEvent/{id}")]
|
|
||||||
public async Task<IActionResult> DeleteEvent(int id)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var model = await _db.Events.FindAsync(id);
|
|
||||||
if (model == null)
|
|
||||||
{
|
|
||||||
return NotFound(new Response { Status = "Error", Message = "Event not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Soft delete
|
|
||||||
model.IsDeleted = true;
|
|
||||||
model.DeletionTime = DateTime.Now;
|
|
||||||
|
|
||||||
_db.Events.Update(model);
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Ok(new DataResponse { Message = "Event Deleted Successfully", Status = "200" });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================================================
|
|
||||||
// GET EVENTS LIST (WITH PAGINATION AND FILTERS)
|
|
||||||
// ===================================================================================
|
|
||||||
[HttpPost]
|
|
||||||
[Route("GetEventList")]
|
|
||||||
public IActionResult GetEventList(Pagination_DTO dto)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var query = _db.Events.Where(e => e.IsDeleted != true);
|
|
||||||
|
|
||||||
// Apply search filter if provided
|
|
||||||
if (!string.IsNullOrWhiteSpace(dto.Search))
|
|
||||||
{
|
|
||||||
query = query.Where(e =>
|
|
||||||
e.Title.Contains(dto.Search) ||
|
|
||||||
(e.Description != null && e.Description.Contains(dto.Search)) ||
|
|
||||||
(e.Location != null && e.Location.Contains(dto.Search))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: Calculate totalCount AFTER applying filters
|
|
||||||
var totalCount = query.Count();
|
|
||||||
|
|
||||||
// Apply pagination
|
|
||||||
var events = query
|
|
||||||
.OrderByDescending(e => e.StartDate)
|
|
||||||
.ThenByDescending(e => e.CreatedDate)
|
|
||||||
.Skip((dto.PageNumber - 1) * dto.PageSize)
|
|
||||||
.Take(dto.PageSize)
|
|
||||||
.Select(e => new Event_DTO
|
|
||||||
{
|
|
||||||
Id = e.Id,
|
|
||||||
Title = e.Title,
|
|
||||||
Description = e.Description,
|
|
||||||
Location = e.Location,
|
|
||||||
RecordID = e.RecordID,
|
|
||||||
RecordName = e.RecordName,
|
|
||||||
RecordTargetType = e.RecordTargetType,
|
|
||||||
ReminderMinutes = e.ReminderMinutes,
|
|
||||||
EventColor = e.EventColor,
|
|
||||||
StartDate = e.StartDate,
|
|
||||||
StartTime = e.StartTime,
|
|
||||||
EndDate = e.EndDate,
|
|
||||||
EndTime = e.EndTime,
|
|
||||||
AllDay = e.AllDay,
|
|
||||||
CreatedDate = e.CreatedDate,
|
|
||||||
createdby = e.createdby
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return Ok(new DataResponse
|
|
||||||
{
|
|
||||||
Message = "Success",
|
|
||||||
Status = "200",
|
|
||||||
Data = new { rows = events, totalCount }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================================================================================
|
|
||||||
// GET EVENTS FOR CALENDAR (Date Range Filter)
|
|
||||||
// ===================================================================================
|
|
||||||
[HttpGet]
|
|
||||||
[Route("GetEventsForCalendar")]
|
|
||||||
public IActionResult GetEventsForCalendar([FromQuery] DateTime? start, [FromQuery] DateTime? end)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var query = _db.Events.Where(e => e.IsDeleted != true);
|
|
||||||
|
|
||||||
// Filter by date range if provided
|
|
||||||
if (start.HasValue && end.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Where(e =>
|
|
||||||
(e.StartDate >= start.Value && e.StartDate <= end.Value) ||
|
|
||||||
(e.EndDate >= start.Value && e.EndDate <= end.Value) ||
|
|
||||||
(e.StartDate <= start.Value && e.EndDate >= end.Value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var events = query
|
|
||||||
.Select(e => new Event_DTO
|
|
||||||
{
|
|
||||||
Id = e.Id,
|
|
||||||
Title = e.Title,
|
|
||||||
Description = e.Description,
|
|
||||||
Location = e.Location,
|
|
||||||
RecordID = e.RecordID,
|
|
||||||
RecordName = e.RecordName,
|
|
||||||
RecordTargetType = e.RecordTargetType,
|
|
||||||
ReminderMinutes = e.ReminderMinutes,
|
|
||||||
EventColor = e.EventColor,
|
|
||||||
StartDate = e.StartDate,
|
|
||||||
StartTime = e.StartTime,
|
|
||||||
EndDate = e.EndDate,
|
|
||||||
EndTime = e.EndTime,
|
|
||||||
AllDay = e.AllDay,
|
|
||||||
CreatedDate = e.CreatedDate,
|
|
||||||
createdby = e.createdby
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return Ok(new DataResponse { Message = "Success", Status = "200", Data = events });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
===================================================================================
|
|
||||||
SECTION 4: DATABASE SETUP
|
|
||||||
===================================================================================
|
|
||||||
|
|
||||||
STEP 1: Add Events DbSet to ApplicationDbContext.cs
|
|
||||||
-------------------------------------------------------
|
|
||||||
Add this line in the ApplicationDbContext class (around line 48):
|
|
||||||
|
|
||||||
public DbSet<Events> Events { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
STEP 2: Create Database Migration
|
|
||||||
-------------------------------------------------------
|
|
||||||
Run these commands in the terminal from the Api.SeaHavenIndustries project directory:
|
|
||||||
|
|
||||||
dotnet ef migrations add AddEventsTable --project ../Data.SeaHavenIndustries
|
|
||||||
dotnet ef database update
|
|
||||||
|
|
||||||
|
|
||||||
STEP 3: SQL Script for Manual Table Creation (Alternative to migrations)
|
|
||||||
-------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
-- SQL SCRIPT TO CREATE EVENTS TABLE
|
|
||||||
/*
|
|
||||||
CREATE TABLE Events (
|
|
||||||
Id INT PRIMARY KEY IDENTITY(1,1),
|
|
||||||
Title NVARCHAR(100) NOT NULL,
|
|
||||||
Description NVARCHAR(MAX) NULL,
|
|
||||||
Location NVARCHAR(150) NULL,
|
|
||||||
|
|
||||||
RecordID INT NULL,
|
|
||||||
RecordName NVARCHAR(255) NULL,
|
|
||||||
RecordTargetType NVARCHAR(50) NULL,
|
|
||||||
|
|
||||||
ReminderMinutes INT NOT NULL DEFAULT 15,
|
|
||||||
EventColor NVARCHAR(20) NOT NULL DEFAULT 'primary',
|
|
||||||
|
|
||||||
StartDate DATETIME NOT NULL,
|
|
||||||
StartTime NVARCHAR(10) NULL,
|
|
||||||
EndDate DATETIME NOT NULL,
|
|
||||||
EndTime NVARCHAR(10) NULL,
|
|
||||||
AllDay BIT NOT NULL DEFAULT 0,
|
|
||||||
|
|
||||||
-- Audit fields
|
|
||||||
IsDeleted BIT NULL,
|
|
||||||
createdby NVARCHAR(450) NULL,
|
|
||||||
DeleterUserId NVARCHAR(450) NULL,
|
|
||||||
DeletionTime DATETIME NULL,
|
|
||||||
CreatedDate DATETIME NULL,
|
|
||||||
LastModificationTime DATETIME NULL,
|
|
||||||
LastModifierUserId INT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Insert sample data
|
|
||||||
INSERT INTO Events (Title, Description, Location, StartDate, StartTime, EndDate, EndTime, AllDay, EventColor, ReminderMinutes, CreatedDate, IsDeleted)
|
|
||||||
VALUES
|
|
||||||
('Team Meeting', 'Weekly team sync', 'Conference Room A', '2026-04-01', '10:00 AM', '2026-04-01', '11:00 AM', 0, 'primary', 15, GETDATE(), 0),
|
|
||||||
('Client Presentation', 'Q1 Results Review with ABC Corp', 'Virtual - Zoom', '2026-04-03', '02:00 PM', '2026-04-03', '03:30 PM', 0, 'success', 30, GETDATE(), 0),
|
|
||||||
('Project Deadline', 'Final delivery for Project Phoenix', NULL, '2026-04-05', NULL, '2026-04-05', NULL, 1, 'danger', 1440, GETDATE(), 0),
|
|
||||||
('Training Session', 'New system training for all employees', 'Main Office', '2026-04-08', '09:00 AM', '2026-04-08', '12:00 PM', 0, 'info', 60, GETDATE(), 0),
|
|
||||||
('Equipment Maintenance', 'Scheduled maintenance for all assets', 'Warehouse', '2026-04-10', '08:00 AM', '2026-04-10', '05:00 PM', 0, 'warning', 120, GETDATE(), 0);
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
===================================================================================
|
|
||||||
SECTION 5: IMPLEMENTATION CHECKLIST
|
|
||||||
===================================================================================
|
|
||||||
|
|
||||||
BACKEND SETUP:
|
|
||||||
--------------
|
|
||||||
1. ✓ Create Events.cs in Data.SeaHavenIndustries/Models/
|
|
||||||
2. ✓ Create Event_DTO.cs in Api.SeaHavenIndustries/DTOs/
|
|
||||||
3. ✓ Create EditEvent_DTO.cs in Api.SeaHavenIndustries/DTOs/
|
|
||||||
4. ✓ Create CalendarController.cs in Api.SeaHavenIndustries/Controllers/
|
|
||||||
5. ☐ Add DbSet<Events> to ApplicationDbContext.cs
|
|
||||||
6. ☐ Run migrations (or execute SQL script)
|
|
||||||
7. ☐ Test all endpoints using Postman/Swagger
|
|
||||||
|
|
||||||
FRONTEND SETUP (Next Step):
|
|
||||||
---------------------------
|
|
||||||
1. ☐ Create src/pages/calendar/CalendarPage.js (List view with FullCalendar)
|
|
||||||
2. ☐ Create src/pages/calendar/EventFormPage.js (Create/Edit form)
|
|
||||||
3. ☐ Create src/pages/calendar/api.js (API calls)
|
|
||||||
4. ☐ Add routes to App.js
|
|
||||||
5. ☐ Install FullCalendar: npm install @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction
|
|
||||||
|
|
||||||
API ENDPOINTS SUMMARY:
|
|
||||||
---------------------
|
|
||||||
POST /api/Calendar/AddEvent - Create new event
|
|
||||||
POST /api/Calendar/EditEvent - Update existing event
|
|
||||||
GET /api/Calendar/GetEventById/{id} - Get single event by ID
|
|
||||||
DELETE /api/Calendar/DeleteEvent/{id} - Delete event (soft delete)
|
|
||||||
POST /api/Calendar/GetEventList - Get paginated list with search
|
|
||||||
GET /api/Calendar/GetEventsForCalendar - Get events for calendar view (with date range)
|
|
||||||
|
|
||||||
KEY FEATURES IMPLEMENTED:
|
|
||||||
-------------------------
|
|
||||||
✓ CRUD operations (Create, Read, Update, Delete)
|
|
||||||
✓ Pagination with search filter
|
|
||||||
✓ Date range filtering for calendar view
|
|
||||||
✓ Soft delete (IsDeleted flag)
|
|
||||||
✓ Related record linking (RecordID, RecordName, RecordTargetType)
|
|
||||||
✓ Color coding for calendar display
|
|
||||||
✓ All-day event support
|
|
||||||
✓ Reminder settings
|
|
||||||
✓ Proper error handling
|
|
||||||
✓ Consistent with existing controller patterns (AssetController)
|
|
||||||
✓ Fixed pagination bug (totalCount calculated AFTER filters)
|
|
||||||
|
|
||||||
NOTES:
|
|
||||||
------
|
|
||||||
- EventColor values: 'primary', 'success', 'info', 'warning', 'danger'
|
|
||||||
- ReminderMinutes: 0=Never, 5, 10, 15, 30, 60, 120, 240, 360, 720, 1440 (1 day)
|
|
||||||
- StartTime/EndTime format: "08:00 AM", "02:30 PM", etc.
|
|
||||||
- AllDay: When true, time fields can be null/ignored
|
|
||||||
- RecordTargetType: Can be "Account", "Contact", "WorkOrder", "FollowUp", etc.
|
|
||||||
|
|
||||||
===================================================================================
|
|
||||||
END OF FILE
|
|
||||||
===================================================================================
|
|
||||||
*/
|
|
||||||
|
|
@ -1,237 +0,0 @@
|
||||||
# Dependency Injection - Assembly Scanning Guide
|
|
||||||
|
|
||||||
## 🎯 Overview
|
|
||||||
|
|
||||||
This project uses **assembly scanning** to automatically register services instead of manual registration. This means you don't need to add each service to `Program.cs` individually.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 How It Works
|
|
||||||
|
|
||||||
### In `Program.cs`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// These two lines register ALL services automatically
|
|
||||||
builder.Services.AddDataServices(); // Scans SeaHaven.DataServices assembly
|
|
||||||
builder.Services.AddBusinessServices(); // Scans SeaHaven.Services assembly + Validators
|
|
||||||
```
|
|
||||||
|
|
||||||
That's it! No need for this:
|
|
||||||
```csharp
|
|
||||||
// ❌ DON'T DO THIS - It's automatic now!
|
|
||||||
builder.Services.AddScoped<IContactDataService, ContactDataService>();
|
|
||||||
builder.Services.AddScoped<IEmployeeDataService, EmployeeDataService>();
|
|
||||||
builder.Services.AddScoped<IAssetDataService, AssetDataService>();
|
|
||||||
// ... hundreds of lines
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 DataServicesModule
|
|
||||||
|
|
||||||
**Location**: `SeaHaven.DataServices/DependencyInjection/DataServicesModule.cs`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public static IServiceCollection AddDataServices(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
|
|
||||||
// Find all non-abstract classes in the assembly
|
|
||||||
var allClasses = assembly.GetTypes()
|
|
||||||
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var implementationType in allClasses)
|
|
||||||
{
|
|
||||||
// Find interface matching pattern: I{ClassName}
|
|
||||||
var defaultInterface = implementationType.GetInterfaces()
|
|
||||||
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
|
|
||||||
|
|
||||||
if (defaultInterface != null)
|
|
||||||
{
|
|
||||||
// Register as Scoped
|
|
||||||
services.AddScoped(defaultInterface, implementationType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### What It Does
|
|
||||||
|
|
||||||
1. Scans entire `SeaHaven.DataServices` assembly
|
|
||||||
2. Finds all concrete classes (e.g., `ContactDataService`, `EmployeeDataService`)
|
|
||||||
3. For each class, looks for an interface named `I{ClassName}`
|
|
||||||
4. If found, registers the pair as **Scoped** service
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
If you create:
|
|
||||||
- `IProductDataService` (interface)
|
|
||||||
- `ProductDataService` (class implementing `IProductDataService`)
|
|
||||||
|
|
||||||
Then `IProductDataService → ProductDataService` is **automatically registered** as Scoped.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💼 ServicesModule
|
|
||||||
|
|
||||||
**Location**: `SeaHaven.Services/DependencyInjection/ServicesModule.cs`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
|
|
||||||
var allClasses = assembly.GetTypes()
|
|
||||||
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var implementationType in allClasses)
|
|
||||||
{
|
|
||||||
var defaultInterface = implementationType.GetInterfaces()
|
|
||||||
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
|
|
||||||
|
|
||||||
if (defaultInterface != null)
|
|
||||||
{
|
|
||||||
services.AddScoped(defaultInterface, implementationType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ALSO auto-register all FluentValidation validators
|
|
||||||
services.AddValidatorsFromAssembly(assembly);
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### What It Does
|
|
||||||
|
|
||||||
1. Scans entire `SeaHaven.Services` assembly
|
|
||||||
2. Registers all services using naming convention (same as DataServices)
|
|
||||||
3. **BONUS**: Automatically registers ALL FluentValidation validators in the assembly
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
If you create:
|
|
||||||
- `IProductService` (interface)
|
|
||||||
- `ProductService` (class)
|
|
||||||
- `CreateProductDTOValidator` (FluentValidation validator)
|
|
||||||
|
|
||||||
Then **all three** are automatically registered.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ CRITICAL: Naming Convention
|
|
||||||
|
|
||||||
For auto-registration to work, you MUST follow this pattern:
|
|
||||||
|
|
||||||
### ✅ CORRECT
|
|
||||||
|
|
||||||
| Interface | Implementation | Result |
|
|
||||||
|-----------|---------------|--------|
|
|
||||||
| `IContactService` | `ContactService` | ✅ Registered as Scoped |
|
|
||||||
| `IContactDataService` | `ContactDataService` | ✅ Registered as Scoped |
|
|
||||||
| `IProductService` | `ProductService` | ✅ Registered as Scoped |
|
|
||||||
| `IEmployeeService` | `EmployeeService` | ✅ Registered as Scoped |
|
|
||||||
|
|
||||||
### ❌ WRONG
|
|
||||||
|
|
||||||
| Interface | Implementation | Result |
|
|
||||||
|-----------|---------------|--------|
|
|
||||||
| `IProductService` | `ProductServiceImpl` | ❌ NOT registered - names don't match |
|
|
||||||
| `IContactRepository` | `ContactService` | ❌ NOT registered - names don't match |
|
|
||||||
| `ISomething` | `SomethingElse` | ❌ NOT registered - names don't match |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Rules
|
|
||||||
|
|
||||||
1. **Interface naming**: `I{ClassName}`
|
|
||||||
2. **Class naming**: `{ClassName}`
|
|
||||||
3. **Example**:
|
|
||||||
- Interface: `IContactService`
|
|
||||||
- Class: `ContactService` (NOT `ContactServiceImpl`, `ContactServiceImplementation`, etc.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Benefits
|
|
||||||
|
|
||||||
✅ **Less boilerplate** - No need to add each service to `Program.cs`
|
|
||||||
✅ **Auto-discovery** - New services are automatically registered
|
|
||||||
✅ **Consistent** - All services registered the same way
|
|
||||||
✅ **FluentValidation included** - Validators work automatically
|
|
||||||
✅ **Cleaner Program.cs** - Only 2 lines instead of 50+
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 How to Add a New Service
|
|
||||||
|
|
||||||
### Old Way (Manual)
|
|
||||||
```csharp
|
|
||||||
// 1. Create interface and class
|
|
||||||
public interface IProductService { }
|
|
||||||
public class ProductService : IProductService { }
|
|
||||||
|
|
||||||
// 2. Manually add to Program.cs
|
|
||||||
builder.Services.AddScoped<IProductService, ProductService>(); // ❌ DON'T DO THIS
|
|
||||||
```
|
|
||||||
|
|
||||||
### New Way (Automatic)
|
|
||||||
```csharp
|
|
||||||
// 1. Create interface and class following naming convention
|
|
||||||
public interface IProductService { }
|
|
||||||
public class ProductService : IProductService { }
|
|
||||||
|
|
||||||
// 2. Done! It's automatically registered ✅
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Lifecycle
|
|
||||||
|
|
||||||
All services are registered as **Scoped**:
|
|
||||||
- New instance per HTTP request
|
|
||||||
- Shared within the same request
|
|
||||||
- Disposed at end of request
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Example Flow
|
|
||||||
|
|
||||||
1. You create `IProductDataService` and `ProductDataService` in `SeaHaven.DataServices`
|
|
||||||
2. You create `IProductService` and `ProductService` in `SeaHaven.Services`
|
|
||||||
3. You create `CreateProductDTOValidator` in `SeaHaven.Services/Validation`
|
|
||||||
4. Application starts → `AddDataServices()` and `AddBusinessServices()` run
|
|
||||||
5. Assembly scanner finds your services and validator
|
|
||||||
6. All registered automatically as Scoped
|
|
||||||
7. You inject `IProductService` in controller → works immediately ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Service not being injected?
|
|
||||||
|
|
||||||
Check:
|
|
||||||
1. ✅ Is the interface named `I{ClassName}`?
|
|
||||||
2. ✅ Is the class named `{ClassName}`?
|
|
||||||
3. ✅ Is the class in the correct assembly?
|
|
||||||
4. ✅ Is the class public and concrete (not abstract)?
|
|
||||||
5. ✅ Does the class implement the interface?
|
|
||||||
|
|
||||||
### Validator not working?
|
|
||||||
|
|
||||||
Check:
|
|
||||||
1. ✅ Is the validator in `SeaHaven.Services` assembly?
|
|
||||||
2. ✅ Does it inherit from `AbstractValidator<T>`?
|
|
||||||
3. ✅ Is it public and concrete?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📖 See Also
|
|
||||||
|
|
||||||
- **Full Architecture Guide**: `BACKEND_ARCHITECTURE.md`
|
|
||||||
- **Quick Start Guide**: `QUICK_START_GUIDE.md`
|
|
||||||
- **At a Glance**: `ARCHITECTURE_AT_A_GLANCE.md`
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
-- SQL Script to add missing columns to Locations table
|
|
||||||
-- Run this in SQL Server Management Studio
|
|
||||||
|
|
||||||
USE [db_a7141e_seahavenindustry]
|
|
||||||
GO
|
|
||||||
|
|
||||||
-- Check and add AccountId column
|
|
||||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'AccountId')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE [dbo].[Locations] ADD [AccountId] int NULL;
|
|
||||||
PRINT 'Added AccountId column';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'AccountId column already exists';
|
|
||||||
|
|
||||||
-- Check and add Address column
|
|
||||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Address')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE [dbo].[Locations] ADD [Address] nvarchar(max) NULL;
|
|
||||||
PRINT 'Added Address column';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'Address column already exists';
|
|
||||||
|
|
||||||
-- Check and add City column
|
|
||||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'City')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE [dbo].[Locations] ADD [City] nvarchar(max) NULL;
|
|
||||||
PRINT 'Added City column';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'City column already exists';
|
|
||||||
|
|
||||||
-- Check and add Contact column
|
|
||||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Contact')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE [dbo].[Locations] ADD [Contact] nvarchar(max) NULL;
|
|
||||||
PRINT 'Added Contact column';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'Contact column already exists';
|
|
||||||
|
|
||||||
-- Check and add ContactEmail column
|
|
||||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'ContactEmail')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE [dbo].[Locations] ADD [ContactEmail] nvarchar(max) NULL;
|
|
||||||
PRINT 'Added ContactEmail column';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'ContactEmail column already exists';
|
|
||||||
|
|
||||||
-- Check and add Phone column
|
|
||||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Phone')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE [dbo].[Locations] ADD [Phone] nvarchar(max) NULL;
|
|
||||||
PRINT 'Added Phone column';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'Phone column already exists';
|
|
||||||
|
|
||||||
-- Check and add State column
|
|
||||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'State')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE [dbo].[Locations] ADD [State] nvarchar(max) NULL;
|
|
||||||
PRINT 'Added State column';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'State column already exists';
|
|
||||||
|
|
||||||
-- Check and add Status column
|
|
||||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'Status')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE [dbo].[Locations] ADD [Status] nvarchar(max) NULL;
|
|
||||||
PRINT 'Added Status column';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'Status column already exists';
|
|
||||||
|
|
||||||
-- Check and add ZipCode column
|
|
||||||
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Locations' AND COLUMN_NAME = 'ZipCode')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE [dbo].[Locations] ADD [ZipCode] nvarchar(max) NULL;
|
|
||||||
PRINT 'Added ZipCode column';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'ZipCode column already exists';
|
|
||||||
|
|
||||||
-- Check and add foreign key index
|
|
||||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Locations_AccountId' AND object_id = OBJECT_ID('dbo.Locations'))
|
|
||||||
BEGIN
|
|
||||||
CREATE NONCLUSTERED INDEX [IX_Locations_AccountId] ON [dbo].[Locations] ([AccountId] ASC);
|
|
||||||
PRINT 'Added IX_Locations_AccountId index';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'IX_Locations_AccountId index already exists';
|
|
||||||
|
|
||||||
-- Check and add foreign key constraint if Accounts table exists
|
|
||||||
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Accounts')
|
|
||||||
AND NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_Locations_Accounts_AccountId')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE [dbo].[Locations] WITH CHECK ADD CONSTRAINT [FK_Locations_Accounts_AccountId]
|
|
||||||
FOREIGN KEY([AccountId]) REFERENCES [dbo].[Accounts] ([Id]);
|
|
||||||
PRINT 'Added FK_Locations_Accounts_AccountId foreign key';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
PRINT 'FK_Locations_Accounts_AccountId foreign key already exists or Accounts table does not exist';
|
|
||||||
|
|
||||||
PRINT 'Schema fix completed!';
|
|
||||||
GO
|
|
||||||
|
|
@ -1,515 +0,0 @@
|
||||||
# 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`
|
|
||||||
Loading…
Add table
Reference in a new issue