Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions .agent/rules/rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
trigger: always_on
---

# AI Coding Standards & Project Specification

This document serves as the **Single Source of Truth** for AI Agents and Developers working on this solution. It combines Architectural Standards, Technology Choices, and strict Test Driven Development (TDD) protocols.

## 1. High-Level Architecture

All solutions must follow a **Clean Architecture** (Onion Architecture) style. The dependency flow is strictly unidirectional, pointing "inward" toward the Domain.

### Dependency Graph
```mermaid
graph TD
IoC["[ProjectName].IoC"] --> Api["[ProjectName].Api"]
IoC --> DataAccess["[ProjectName].DataAccess"]
IoC --> Domain["[ProjectName].Domain"]
Api --> Domain
DataAccess --> Domain
```

### Component Responsibilities
1. **[ProjectName].Domain** (The Core)
* **Role**: Entities, Value Objects, Logic, Commands, Queries, Repository Interfaces.
* **Dependencies**: NONE.
2. **[ProjectName].DataAccess** (The Infrastructure)
* **Role**: Implements Repository Interfaces (Entity Framework/Dapper).
* **Dependencies**: Domain.
3. **[ProjectName].Api** (The Presentation)
* **Role**: REST/HTTP Endpoints, Controllers, DTOs.
* **Dependencies**: Domain. (NO DataAccess reference).
4. **[ProjectName].IoC** (The Composition Root)
* **Role**: `Program.cs`, Dependency Injection wiring.
* **Dependencies**: Api, DataAccess, Domain.

## 2. Technology Stack

* **Framework**: .NET Standard / Latest LTS (.NET 8/10+)
* **Language**: C# (Latest)
* **Testing**: MSTest
* **Assertions**: FluentAssertions
* **Rule**: Assert on Return Values or State Changes whenever possible.
* **Fallback**: Use `Received()` (interaction testing) ONLY for void methods where no state change or return value can be observed (e.g., logging, or the final boundary call).

### Dependencies Checklist
Ensure the following are installed/available before starting work:
1. **MSTest.Sdk**: Core testing framework.
2. **FluentAssertions**: For expressive assertions.
3. **NSubstitute**: For mocking.
4. **Microsoft.AspNetCore.App**: Framework Reference (Required for API Integration Tests).

## 3. Development Workflow: Test Driven Development (TDD)

**CRITICAL INSTRUCTION**: AI Agents must strictly adhere to the **Red/Green/Refactor** cycle. You are FORBIDDEN from writing implementation code without a verified failing test.

### Test Coverage Requirements
Every feature MUST cover the following scenarios (where applicable):
1. **Happy Path**:
* **200 OK**: Request succeeded with data.
* **201 Created**: Resource successfully created.
* **204 No Content**: Action succeeded (Update/Delete).
2. **Unhappy Paths (Error Handling)**:
* **400 Bad Request**: Validation failure (Invalid model, missing fields).
* **404 Not Found**: Resource does not exist (Get/Update/Delete).
* **500 Internal Server Error**: Unexpected downstream failures (Mocked Exception from Wrapper).

### Phase 1: RED (Write Test)
1. **Goal**: Write a failing test for a specific behavior.
2. **Action**: Create a test file following the [File Organization](#5-file-organization--naming-conventions) rules.
3. **Best Practice**: Use **Randomized Data** via the `RandomTestValues` package instead of static constants or manual random calls.
* *Why?* Prevents "hardcoding" solutions (e.g., `return 0`) that accidentally pass static tests. It forces the implementation to handle dynamic input.
4. **Run**: `dotnet test --filter <TestName>`.
5. **Validation**:
* MUST fail.
* If logic is missing, write *stubs* (throw `NotImplementedException`) to make it compile, then confirm failure.

### Phase 2: GREEN (Simplest Implementation)
1. **Goal**: Pass the test with the *minimum code necessary*.
2. **Rule**: Do not implement full algorithms yet. If `Add(0,0)` expects `0`, return `0`.
* *Why?* To force the creation of the next test case.
3. **Validation**: Run test. It must pass.

### Phase 3: REFACTOR
1. **Goal**: Clean up code/tests without changing behavior.
2. **Action**:
* **Clean Code Principles**: Use expressive variable names.
* **Comments**:
* **REQUIRED**: Use `// Arrange`, `// Act`, `// Assert` comments to clearly delineate test phases.
* **FORBIDDEN**: Avoid inline comments explaining *WHAT* or *WHY* (e.g., `// Using real instance...`). The code structure should be self-explanatory. Extract logic to private methods with descriptive names instead.
* **Structure**: Remove magic strings, extract methods. Use `TestBase` classes to reduce setup boilerplate.

### Phase 4: ITERATE
1. **Goal**: Evolve logic by adding new test cases.
2. **Action**: Add the next test case (e.g., `Add(1,1)` returns `2`).
* **Technique**: Use `[DataRow]`, `[TestCase]`, or similar attributes to modify existing tests or add new permutations efficiently.

### Mocking Rules & Isolation
* **Architectural Rule**: The DataAccess layer must implement the **Repository Pattern** by using a **Wrapper Pattern** for the underlying store (e.g., `IMongoDbWrapper`, `ISqlWrapper`).
* *Reason*: Allows testing business logic with real Repository implementations but mocked low-level drivers.
* **ALLOWED**: Mocking **Infrastructure Wrappers** (e.g., `IMongoDbWrapper`, `IHttpClientWrapper`, `IDateTimeWrapper`).
* **FORBIDDEN**: Mocking **Domain Repository Interfaces** (e.g., `IStudentRepository`) in logical tests.
* *Correction*: When testing Commands/Queries, use the **REAL** Repository implementation injected with a **MOCKED** Wrapper.
* **FORBIDDEN**: Mocking internal domain entities or value objects. Use real instances.

## 4. Design Patterns & Conventions

### CQRS (Command Query Responsibility Segregation)
* **Commands (Write)**: `[ProjectName].Domain/Commands`. Return `Task`.
* **Queries (Read)**: `[ProjectName].Domain/Queries`. Return `Task<T>`.
* **Handlers**: Implement specific interfaces, inject Repository Interfaces.

### Centralized Dependency Injection
* **No Magic in API**: `Program.cs` resides in **IoC** project.
* **Registration**: `IoC` project coordinates `services.Add[LayerName]()` calls.

## 5. File Organization & Naming Conventions

### Directory Structure
```text
[SolutionName]/
├── [ProjectName].Api/ # Controllers, ApiConfig
├── [ProjectName].DataAccess/ # Repositories
├── [ProjectName].Domain/ # Commands, Queries, Entities, Interfaces
├── [ProjectName].IoC/ # Program.cs, RegisterDependencies
└── [ProjectName].Tests/ # MSTest Project
```

### Test File Granularity / Naming
To maintain context and clarity, use granular test files:
* **Pattern**: `[ProjectName].Tests/[Namespace]/[ClassName]Tests/[MethodName]_[Scenario].cs`
* **Example**:
* `Api/FooControllerTests/GetShould_ReturnOk.cs`
* `Api/FooControllerTests/GetShould_Throw_WhenInvalid.cs`
* **Tip**: Use `partial class` for shared setup if splitting tests for the same method.

## 6. Instructions for AI Code Generation

**CRITICAL: ITERATIVE WORKFLOW REQUIRED**
* **Small Steps**: Do NOT generate large amounts of code at once.
* **Stop & Check**: After *every* file change or test run, pause and evaluate next steps.
* **No "Overshooting"**: Do not anticipate future requirements. Solve ONLY the current failing test.
* **Maintainability**: Enterprise-grade code requires careful adherence to patterns. Rushing breaks design.

When asked to implement a feature:
1. **Check Context**: Do I understand the requirement?
2. **Plan TDD**: What is the FIRST failing test?
3. **Execute Cycle**:
* Create Test -> Run (Fail) -> Implement (Pass) -> Refactor.
* Repeat until feature is complete.
4. **Conformity**: Ensure all new files land in the correct folders (Clean Architecture) and use specified namespaces.
84 changes: 84 additions & 0 deletions DotNetExample.Api/Controllers/StudentController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using DotNetExample.Domain.Commands;
using DotNetExample.Domain.Entities;
using DotNetExample.Domain.Queries;

using Microsoft.AspNetCore.Mvc;

namespace DotNetExample.Api.Controllers;

[Route("api/[controller]")]
[ApiController]
public class StudentController : ControllerBase
{
private readonly ICreateStudent _createStudent;
private readonly IGetStudent _getStudent;
private readonly IGetAllStudents _getAllStudents;
private readonly IUpdateStudent _updateStudent;
private readonly IDeleteStudent _deleteStudent;

public StudentController(
ICreateStudent createStudent,
IGetStudent getStudent,
IGetAllStudents getAllStudents,
IUpdateStudent updateStudent,
IDeleteStudent deleteStudent)
{
_createStudent = createStudent;
_getStudent = getStudent;
_getAllStudents = getAllStudents;
_updateStudent = updateStudent;
_deleteStudent = deleteStudent;
}

[HttpPost]
public async Task<IActionResult> Create([FromBody] Student student)
{
await _createStudent.ExecuteAsync(student);
return CreatedAtAction(nameof(Get), new { id = student.Id }, student);
}

[HttpGet("{id}")]
public async Task<IActionResult> Get(string id)
{
var student = await _getStudent.ExecuteAsync(id);
if (student == null)
{
return NotFound();
}
return Ok(student);
}

[HttpGet]
public async Task<IActionResult> GetAll()
{
var students = await _getAllStudents.ExecuteAsync();
return Ok(students);
}

[HttpPut("{id}")]
public async Task<IActionResult> Update(string id, [FromBody] Student student)
{
var existingStudent = await _getStudent.ExecuteAsync(id);
if (existingStudent == null)
{
return NotFound();
}

student.Id = id;
await _updateStudent.ExecuteAsync(student);
return NoContent();
}

[HttpDelete("{id}")]
public async Task<IActionResult> Delete(string id)
{
var existingStudent = await _getStudent.ExecuteAsync(id);
if (existingStudent == null)
{
return NotFound();
}

await _deleteStudent.ExecuteAsync(id);
return NoContent();
}
}
40 changes: 40 additions & 0 deletions DotNetExample.DataAccess/StudentRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using DotNetExample.DataAccess.Wrappers;
using DotNetExample.Domain.DataAccess;
using DotNetExample.Domain.Entities;

namespace DotNetExample.DataAccess;

public class StudentRepository : IStudentRepository
{
private readonly IMongoDbWrapper _mongoDbWrapper;

public StudentRepository(IMongoDbWrapper mongoDbWrapper)
{
_mongoDbWrapper = mongoDbWrapper;
}

public Task<Student?> GetByIdAsync(string id)
{
return _mongoDbWrapper.GetStudentByIdAsync(id);
}

public Task<IEnumerable<Student>> GetAllAsync()
{
return _mongoDbWrapper.GetAllStudentsAsync();
}

public Task CreateAsync(Student student)
{
return _mongoDbWrapper.InsertStudentAsync(student);
}

public Task UpdateAsync(Student student)
{
return _mongoDbWrapper.UpdateStudentAsync(student);
}

public Task DeleteAsync(string id)
{
return _mongoDbWrapper.DeleteStudentAsync(id);
}
}
12 changes: 12 additions & 0 deletions DotNetExample.DataAccess/Wrappers/IMongoDbWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using DotNetExample.Domain.Entities;

namespace DotNetExample.DataAccess.Wrappers;

public interface IMongoDbWrapper
{
Task<Student?> GetStudentByIdAsync(string id);
Task<IEnumerable<Student>> GetAllStudentsAsync();
Task InsertStudentAsync(Student student);
Task UpdateStudentAsync(Student student);
Task DeleteStudentAsync(string id);
}
19 changes: 19 additions & 0 deletions DotNetExample.Domain/Commands/CreateStudent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using DotNetExample.Domain.DataAccess;
using DotNetExample.Domain.Entities;

namespace DotNetExample.Domain.Commands;

public class CreateStudent : ICreateStudent
{
private readonly IStudentRepository _studentRepository;

public CreateStudent(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}

public Task ExecuteAsync(Student student)
{
return _studentRepository.CreateAsync(student);
}
}
18 changes: 18 additions & 0 deletions DotNetExample.Domain/Commands/DeleteStudent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using DotNetExample.Domain.DataAccess;

namespace DotNetExample.Domain.Commands;

public class DeleteStudent : IDeleteStudent
{
private readonly IStudentRepository _studentRepository;

public DeleteStudent(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}

public Task ExecuteAsync(string id)
{
return _studentRepository.DeleteAsync(id);
}
}
8 changes: 8 additions & 0 deletions DotNetExample.Domain/Commands/ICreateStudent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using DotNetExample.Domain.Entities;

namespace DotNetExample.Domain.Commands;

public interface ICreateStudent
{
Task ExecuteAsync(Student student);
}
6 changes: 6 additions & 0 deletions DotNetExample.Domain/Commands/IDeleteStudent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace DotNetExample.Domain.Commands;

public interface IDeleteStudent
{
Task ExecuteAsync(string id);
}
8 changes: 8 additions & 0 deletions DotNetExample.Domain/Commands/IUpdateStudent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using DotNetExample.Domain.Entities;

namespace DotNetExample.Domain.Commands;

public interface IUpdateStudent
{
Task ExecuteAsync(Student student);
}
19 changes: 19 additions & 0 deletions DotNetExample.Domain/Commands/UpdateStudent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using DotNetExample.Domain.DataAccess;
using DotNetExample.Domain.Entities;

namespace DotNetExample.Domain.Commands;

public class UpdateStudent : IUpdateStudent
{
private readonly IStudentRepository _studentRepository;

public UpdateStudent(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}

public Task ExecuteAsync(Student student)
{
return _studentRepository.UpdateAsync(student);
}
}
12 changes: 12 additions & 0 deletions DotNetExample.Domain/DataAccess/IStudentRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using DotNetExample.Domain.Entities;

namespace DotNetExample.Domain.DataAccess;

public interface IStudentRepository
{
Task<Student?> GetByIdAsync(string id);
Task<IEnumerable<Student>> GetAllAsync();
Task CreateAsync(Student student);
Task UpdateAsync(Student student);
Task DeleteAsync(string id);
}
9 changes: 9 additions & 0 deletions DotNetExample.Domain/Entities/Student.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace DotNetExample.Domain.Entities;

public class Student
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public double Gpa { get; set; }
public string Grade { get; set; } = string.Empty;
}
Loading