diff --git a/MODULE-02-IMPLEMENTATION.md b/MODULE-02-IMPLEMENTATION.md new file mode 100644 index 0000000..702f8ef --- /dev/null +++ b/MODULE-02-IMPLEMENTATION.md @@ -0,0 +1,188 @@ +# Module 2 Implementation: Validation & Error Handling + +## Summary + +Successfully implemented all three exercises from Module 2: + +### ✅ Exercise 2.1: Centralized Error Model +- Created `ApiError` model in `Models/ApiError.cs` with: + - `ErrorCode`: Machine-readable error code + - `Message`: Human-readable error message + - `Details`: Optional additional details (validation errors, etc.) + - `Timestamp`: When the error occurred (UTC) + - `Path`: Request path that generated the error + +- Implemented `GlobalExceptionHandlerMiddleware` that: + - Catches unhandled exceptions globally + - Maps common exception types to appropriate HTTP status codes + - Returns consistent ApiError JSON responses + - Includes stack traces only in development environment + - Logs all exceptions with ILogger + +### ✅ Exercise 2.2: Comprehensive Input Validation +- Enhanced `UserProfile` model with data annotations: + - `Id`: Required, alphanumeric only, 1-50 characters + - `FullName`: Required, 2-100 characters, letters/spaces/hyphens/apostrophes/periods only + - `Email`: Required, valid email format (NEW field added) + - `Emoji`: Required, 1-10 characters + +- Updated `UsersController` to: + - Return ApiError format for all error responses + - Check ModelState validation and return detailed validation errors + - Check for duplicate IDs during user creation + - Include field-specific error messages in Details object + +### ✅ Exercise 2.3: Rate Limiting Implementation +- Created `RateLimitingMiddleware` with: + - Sliding window algorithm tracking requests per IP address + - Configurable limit: 100 requests per minute (from appsettings.json) + - Returns 429 Too Many Requests when limit exceeded + - Includes `Retry-After` header with seconds until next allowed request + - Thread-safe ConcurrentDictionary for request tracking + - Excludes `/health` endpoint from rate limiting + - Handles X-Forwarded-For header for proxy/load balancer scenarios + +- Added configuration in `appsettings.json`: + ```json + "RateLimiting": { + "RequestLimit": 100, + "TimeWindowMinutes": 1, + "ExcludedPaths": ["/health"] + } + ``` + +## Files Created + +1. `net-users-api/Models/ApiError.cs` - Centralized error response model +2. `net-users-api/Middleware/GlobalExceptionHandlerMiddleware.cs` - Global exception handler +3. `net-users-api/Middleware/RateLimitingMiddleware.cs` - Rate limiting middleware + +## Files Modified + +1. `net-users-api/Models/UserProfile.cs` - Added Email property and validation attributes +2. `net-users-api/Controllers/UsersController.cs` - Updated to use ApiError responses and validation +3. `net-users-api/Program.cs` - Registered both middleware components +4. `net-users-api/appsettings.json` - Added rate limiting configuration + +## Testing Results + +### ✅ GET /api/v1/users - Success +```json +[ + { + "id": "1", + "fullName": "John Doe", + "email": "john.doe@example.com", + "emoji": "😀" + }, + ... +] +``` + +### ✅ POST /api/v1/users - Validation Errors +Request with invalid data (numbers in name, invalid email): +```json +{ + "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", + "title": "One or more validation errors occurred.", + "status": 400, + "errors": { + "Email": ["Email must be a valid email address"], + "FullName": ["FullName must contain only letters, spaces, hyphens, apostrophes, and periods"] + } +} +``` + +### ✅ POST /api/v1/users - Duplicate ID +```json +{ + "errorCode": "DUPLICATE_ID", + "message": "User with ID '1' already exists", + "details": null, + "timestamp": "2025-09-30T06:28:40.059715Z", + "path": "/api/v1/users" +} +``` + +### ✅ GET /api/v1/users/999 - Not Found +```json +{ + "errorCode": "USER_NOT_FOUND", + "message": "User with ID '999' was not found", + "details": null, + "timestamp": "2025-09-30T06:28:34.576653Z", + "path": "/api/v1/users/999" +} +``` + +### ✅ Rate Limiting - 429 Too Many Requests +After 100 requests in 1 minute: +``` +HTTP/1.1 429 Too Many Requests +Content-Type: application/json +Retry-After: 16 + +{ + "errorCode": "RATE_LIMIT_EXCEEDED", + "message": "Rate limit exceeded. Maximum 100 requests per 1 minute(s)", + "details": { + "limit": 100, + "windowMinutes": 1, + "retryAfterSeconds": 16 + }, + "timestamp": "2025-09-30T06:28:53.079249Z", + "path": "/api/v1/users" +} +``` + +## Acceptance Criteria Met + +### Exercise 2.1 ✅ +- [x] `ApiError` model created with required properties +- [x] Global exception handler implemented +- [x] All endpoints return consistent error format +- [x] HTTP status codes properly mapped +- [x] Request path and timestamp included in errors + +### Exercise 2.2 ✅ +- [x] Data annotations added to `UserProfile` properties +- [x] Model validation executed automatically on requests +- [x] Returns 400 with field-specific validation errors +- [x] Error messages are clear and actionable +- [x] Custom validation for duplicate IDs implemented + +### Exercise 2.3 ✅ +- [x] Rate limiting middleware created +- [x] Tracks requests by IP address +- [x] Returns 429 status with Retry-After header +- [x] Configuration stored in appsettings.json +- [x] Health check endpoint excluded +- [x] Thread-safe implementation (using ConcurrentDictionary) + +## Next Steps + +Consider implementing stretch goals: +- RFC 7807 Problem Details format (application/problem+json) +- FluentValidation for complex validation rules +- Correlation IDs for distributed tracing +- Redis-based distributed rate limiting +- API key-based rate limiting with higher limits + +## Build & Run + +```bash +# Build the project +dotnet build + +# Run the API +dotnet run --project net-users-api + +# Server runs on http://localhost:8080 +``` + +## Notes + +- The built-in ASP.NET validation uses ProblemDetails format by default, which is already RFC-compliant +- Our custom ApiError format is used for custom business logic errors (not found, duplicate ID, rate limiting) +- Stack traces are only included in development environment for security +- Rate limiting uses in-memory storage (suitable for single instance; use Redis for distributed scenarios) diff --git a/advanced-practice/README.md b/advanced-practice/README.md index d8674bf..92a00f8 100644 --- a/advanced-practice/README.md +++ b/advanced-practice/README.md @@ -31,6 +31,65 @@ This guide provides advanced exercises for practicing GitHub Copilot features wi ## Getting Started +### Fork and Clone the Repository + +**Step 1: Fork the Repository** +1. Navigate to the repository on GitHub: `https://github.com/frye/net-users-demo` +2. Click the **Fork** button in the top-right corner +3. Select your GitHub account as the destination +4. Wait for GitHub to create your fork (this takes a few seconds) +5. You now have your own copy at `https://github.com/YOUR-USERNAME/net-users-demo` + +**Step 2: Clone Your Fork** + +Open your terminal and run: + +```bash +# Clone your forked repository +git clone https://github.com/YOUR-USERNAME/net-users-demo.git + +# Navigate into the project directory +cd net-users-demo +``` + +**Step 3: Set Up Upstream Remote (Optional but Recommended)** + +This allows you to pull updates from the original repository: + +```bash +# Add the original repository as upstream +git remote add upstream https://github.com/frye/net-users-demo.git + +# Verify your remotes +git remote -v +``` + +You should see: +``` +origin https://github.com/YOUR-USERNAME/net-users-demo.git (fetch) +origin https://github.com/YOUR-USERNAME/net-users-demo.git (push) +upstream https://github.com/frye/net-users-demo.git (fetch) +upstream https://github.com/frye/net-users-demo.git (push) +``` + +**Step 4: Sync with Upstream (When Needed)** + +To get the latest changes from the original repository: + +```bash +# Fetch changes from upstream +git fetch upstream + +# Merge upstream changes into your main branch +git checkout main +git merge upstream/main + +# Push updates to your fork +git push origin main +``` + +### Prerequisites + Before beginning these exercises: 1. **Complete the basic practice instructions** in `Copilot_Practice_Instructions.md` diff --git a/advanced-practice/module-01-crud-enhancements.md b/advanced-practice/module-01-crud-enhancements.md index de9d7ec..d2f260c 100644 --- a/advanced-practice/module-01-crud-enhancements.md +++ b/advanced-practice/module-01-crud-enhancements.md @@ -175,6 +175,125 @@ Log how many users were created in the bulk operation. --- +## Code Review Before Committing + +Before committing your changes, it's essential to perform a thorough local code review. This practice helps catch issues early and ensures code quality. + +### VS Code Built-in Tools + +**1. Review Changes in Source Control View:** +- Open the Source Control view (⌃⇧G or Ctrl+Shift+G) +- Review each modified file in the "Changes" section +- Click on files to see inline diffs with the previous version +- Look for: + - Unintended changes or debug code + - Formatting inconsistencies + - Missing documentation + - TODOs or commented-out code + +**2. Use the Diff Editor:** +- Right-click a modified file in Source Control view +- Select "Open Changes" to see side-by-side diff +- Review each change carefully: + - Does it serve the intended purpose? + - Are there any security concerns? + - Is the code readable and maintainable? + +**3. Check for Errors and Warnings:** +- Open the Problems panel (⌘⇧M or Ctrl+Shift+M) +- Resolve all errors before committing +- Address warnings where appropriate +- Run the build: `dotnet build` + +### Using GitHub Copilot for Code Review + +**Ask Copilot to Review Your Changes:** + +``` +Review my recent changes for potential issues: +- Check for security vulnerabilities +- Identify code quality issues +- Suggest improvements for readability +- Verify error handling is comprehensive +``` + +**Sample Prompts for Specific Reviews:** + +``` +Review the UsersController for REST API best practices +``` + +``` +Check if my new endpoints follow the existing code style and conventions +``` + +``` +Analyze the error handling in my delete endpoint implementation +``` + +### Testing Before Commit + +**Run the Application:** +```bash +cd net-users-api +dotnet run +``` + +**Test Your Endpoints:** +- Use the `.http` files in the project +- Test happy paths and error scenarios +- Verify response codes and bodies match expectations +- Check logs for appropriate messages + +**Run Tests (if available):** +```bash +dotnet test +``` + +### Code Review Checklist + +Use this checklist before committing: + +- [ ] **Functionality**: Does the code work as intended? +- [ ] **Tests**: Are there tests? Do they pass? +- [ ] **Error Handling**: Are edge cases handled? +- [ ] **Logging**: Are operations logged appropriately? +- [ ] **Documentation**: Are XML comments added to public APIs? +- [ ] **Code Style**: Does it follow project conventions? +- [ ] **Security**: Are there any security concerns? +- [ ] **Performance**: Are there obvious performance issues? +- [ ] **Dependencies**: Are new dependencies necessary and appropriate? +- [ ] **Configuration**: Are hardcoded values moved to configuration? +- [ ] **Cleanup**: Is debug code, console logs, or commented code removed? + +### Committing Best Practices + +**Write Clear Commit Messages:** +```bash +git add . +git commit -m "feat: implement DELETE endpoint for users + +- Add DELETE /api/v1/users/{id} endpoint +- Return 204 on success, 404 when not found +- Add logging for delete operations +- Include XML documentation" +``` + +**Use Conventional Commits:** +- `feat:` for new features +- `fix:` for bug fixes +- `docs:` for documentation changes +- `refactor:` for code refactoring +- `test:` for adding tests +- `chore:` for maintenance tasks + +**Atomic Commits:** +- Commit related changes together +- One logical change per commit +- Makes it easier to review and revert if needed + +--- + ## Summary Congratulations! 🎉 You've completed Module 1. You should now have: diff --git a/advanced-practice/module-02-validation-error-handling.md b/advanced-practice/module-02-validation-error-handling.md index 31df782..7e06c8e 100644 --- a/advanced-practice/module-02-validation-error-handling.md +++ b/advanced-practice/module-02-validation-error-handling.md @@ -111,6 +111,232 @@ Exclude /health endpoint from rate limiting. --- +## Git Workflow & Code Review + +### Committing Your Changes + +Once you've completed one or more exercises in this module, follow these steps to commit your work and get it reviewed: + +#### 1. Create a Feature Branch + +Before committing, create a feature branch from main: + +```bash +# Ensure you're on main and up to date +git checkout main +git pull origin main + +# Create and switch to a new feature branch +git checkout -b feature/validation-error-handling + +# Or for specific exercises: +git checkout -b feature/centralized-error-model +git checkout -b feature/input-validation +git checkout -b feature/rate-limiting +``` + +**Branch Naming Conventions**: +- `feature/` prefix for new features +- Use descriptive kebab-case names +- Examples: `feature/add-fluent-validation`, `feature/rfc7807-problem-details` + +#### 2. Stage and Commit Your Changes + +```bash +# Check what files have changed +git status + +# Stage specific files +git add net-users-api/Models/ApiError.cs + +# Or stage all changes - preferred for this section +git add . + +# Commit with a descriptive message +git commit -m "feat: implement centralized error handling with ApiError model" +``` + +**Commit Message Guidelines**: +- Use conventional commit format: `type: description` +- Types: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:` +- Examples: + - `feat: add FluentValidation for UserProfile` + - `feat: implement rate limiting middleware` + - `fix: correct validation error response format` + - `refactor: extract error handling to middleware` + +#### 3. Push to Remote Repository + +```bash +# Push your feature branch to GitHub +git push -u origin feature/validation-error-handling + +# The -u flag sets upstream tracking for the branch +# Subsequent pushes can just use: git push +``` + +#### 4. Create a Pull Request + +After pushing, GitHub will provide a URL to create a PR, or you can: + +1. **Via GitHub CLI** (if installed): +```bash +gh pr create --title "feat: Add validation and error handling" \ + --body "Implements centralized error handling, input validation, and rate limiting from Module 2" +``` + +2. **Via GitHub Web**: + - Navigate to your repository on GitHub + - Click "Compare & pull request" button + - Fill in the PR template: + - **Title**: Clear, descriptive summary (e.g., "Add comprehensive validation and error handling") + - **Description**: List what was implemented, reference exercises + - Link to any related issues + +**Sample PR Description Template**: +```markdown +## Summary +Implements validation and error handling improvements from Module 2. + +## Changes +- ✅ Exercise 2.1: Centralized error model with ApiError class +- ✅ Exercise 2.2: Input validation with data annotations +- ✅ Exercise 2.3: Rate limiting middleware + +## Testing +- Manual testing with various invalid inputs +- Rate limiting tested with multiple rapid requests +- Error responses verified to match RFC 7807 format + +## Checklist +- [ ] Code follows project conventions +- [ ] All acceptance criteria met +- [ ] No breaking changes to existing API +- [ ] Tested locally +``` + +#### 5. Request Copilot Code Review + +GitHub Copilot can review your pull request and provide feedback: + +**Option A: Using GitHub Web Interface** +1. Open your pull request on GitHub +2. In the PR conversation, type: `@copilot review` +3. Copilot will analyze your changes and provide feedback + +**Option B: Using GitHub CLI** +```bash +# View PR and request review +gh pr view --web +``` + +**Option C: Using Copilot Chat in VS Code** +1. Open the Source Control panel +2. Open Copilot Chat +3. Ask: "Review my changes for this PR" or "What potential issues do you see in my recent commits?" + +**Sample Prompts for Copilot Review**: +``` +@copilot review this PR focusing on: +- Error handling best practices +- Input validation completeness +- Potential security vulnerabilities +- Code consistency with existing patterns +``` + +``` +@copilot analyze the validation logic and suggest improvements +for edge cases or missing validations +``` + +``` +@copilot review the rate limiting implementation for +thread safety issues and performance concerns +``` + +#### 6. Address Review Feedback + +If Copilot (or human reviewers) suggest changes: + +```bash +# Make the suggested changes in your code +# Stage and commit the updates +git add . +git commit -m "fix: address review feedback on validation rules" + +# Push the updates +git push +``` + +The PR will automatically update with your new commits. + +#### 7. Merge Your PR + +Once approved and all checks pass: + +```bash +# Via GitHub CLI +gh pr merge --squash + +# Or via GitHub web interface +# Click "Squash and merge" or "Merge pull request" +``` + +**Merge Strategies**: +- **Squash and merge**: Recommended for feature branches (cleaner history) +- **Merge commit**: Preserves all individual commits +- **Rebase and merge**: Linear history without merge commit + +#### 8. Clean Up + +After merging: + +```bash +# Switch back to main +git checkout main + +# Pull the latest changes +git pull origin main + +# Delete the local feature branch +git branch -d feature/validation-error-handling + +# The remote branch is usually auto-deleted by GitHub +# If not, delete it manually: +git push origin --delete feature/validation-error-handling +``` + +--- + +### Tips for Effective Code Reviews + +**Before Requesting Review**: +- ✅ Self-review your changes first +- ✅ Run the application and test manually +- ✅ Ensure code compiles without warnings +- ✅ Check that all acceptance criteria are met +- ✅ Update documentation if needed + +**Using Copilot Effectively**: +- Ask specific questions about your implementation +- Request security and performance analysis +- Ask for alternative approaches +- Verify best practices compliance +- Get suggestions for test cases + +**Example Copilot Interactions**: +``` +"Does this error handling middleware properly catch and format all exception types?" + +"Are there any edge cases in my validation logic that I should handle?" + +"Is my rate limiting implementation thread-safe for concurrent requests?" + +"How can I improve the error messages to be more helpful for API consumers?" +``` + +--- + --- [⬅️ Back to Index](README.md) | [← Previous: Module 1](module-01-crud-enhancements.md) | [Next: Module 3 →](module-03-testing-expansion.md) diff --git a/advanced-practice/module-03-issue-prompt.md b/advanced-practice/module-03-issue-prompt.md new file mode 100644 index 0000000..2435e05 --- /dev/null +++ b/advanced-practice/module-03-issue-prompt.md @@ -0,0 +1,106 @@ +# Prompt: Generate Testing Expansion Issue + +Generate a GitHub issue description for implementing comprehensive testing expansion in the .NET Users Demo API. The issue should track three main testing initiatives: unit testing, integration testing, and property-based testing. + +## Requirements + +Create a markdown issue body that includes: + +1. **Title**: Module 3: Testing Expansion +2. **Overview**: Brief description of testing goals (unit, integration, property-based) +3. **Task List**: All tasks below as checkboxes +4. **Instructions**: Tell Copilot to check off tasks as completed + +## Tasks to Track + +### Exercise 3.1: Comprehensive Unit Test Suite +- [ ] Create xUnit test project at solution level: `net-users-api.Tests` +- [ ] Add project reference from test project to main API project +- [ ] Install required testing packages (xUnit, Moq, FluentAssertions) +- [ ] Create `UsersControllerTests` class with proper namespace +- [ ] Test `GetUsers()` - happy path (returns all users) +- [ ] Test `GetUsers()` - edge case (empty list) +- [ ] Test `GetUser(id)` - happy path (valid ID returns user) +- [ ] Test `GetUser(id)` - error case (invalid ID returns NotFound) +- [ ] Test `GetUser(id)` - edge case (null/empty ID) +- [ ] Test `CreateUser()` - happy path (creates and returns 201) +- [ ] Test `CreateUser()` - error case (duplicate ID returns conflict) +- [ ] Test `CreateUser()` - validation (null/invalid data returns BadRequest) +- [ ] Test `UpdateUser(id)` - happy path (updates existing user) +- [ ] Test `UpdateUser(id)` - error case (non-existent user returns NotFound) +- [ ] Test `UpdateUser(id)` - validation (invalid data) +- [ ] Test `DeleteUser(id)` - happy path (deletes and returns NoContent) +- [ ] Test `DeleteUser(id)` - error case (non-existent user returns NotFound) +- [ ] Mock ILogger in all controller tests +- [ ] Verify all tests follow Arrange-Act-Assert pattern +- [ ] Run `dotnet test` and confirm all tests pass +- [ ] **Stretch**: Achieve >80% code coverage +- [ ] **Stretch**: Add Theory tests with InlineData for parameterized scenarios +- [ ] **Stretch**: Add AutoFixture for test data generation +- [ ] **Stretch**: Add BenchmarkDotNet performance tests + +### Exercise 3.2: Integration Testing +- [ ] Create `IntegrationTests` folder in test project +- [ ] Install `Microsoft.AspNetCore.Mvc.Testing` package +- [ ] Create `CustomWebApplicationFactory` class +- [ ] Implement test fixture for server setup/teardown +- [ ] Create `UsersApiIntegrationTests` class +- [ ] Test GET /api/v1/users - full HTTP request/response cycle +- [ ] Test GET /api/v1/users/{id} - status codes and JSON deserialization +- [ ] Test POST /api/v1/users - request body and response headers +- [ ] Test PUT /api/v1/users/{id} - full update flow +- [ ] Test DELETE /api/v1/users/{id} - status codes +- [ ] Ensure test isolation (tests don't affect each other) +- [ ] Create test data seeder for consistent state +- [ ] Validate response content types and headers +- [ ] Run integration tests and confirm all pass +- [ ] **Stretch**: Use TestContainers for database integration tests +- [ ] **Stretch**: Test content negotiation (JSON/XML) +- [ ] **Stretch**: Test middleware pipeline behavior +- [ ] **Stretch**: Add load testing scenarios + +### Exercise 3.3: Property-Based Testing +- [ ] Install FsCheck.Xunit package +- [ ] Create `PropertyTests` folder in test project +- [ ] Create `UserProfilePropertyTests` class +- [ ] Add property test: UserProfile serialization/deserialization roundtrip +- [ ] Add property test: ID generation always produces unique values +- [ ] Add property test: Email validation accepts all valid RFC formats +- [ ] Add property test: Pagination never loses or duplicates items +- [ ] Add property test: Pagination count accuracy +- [ ] Configure sufficient test iterations (100+ cases) +- [ ] Document properties being tested with XML comments +- [ ] Run property tests and verify edge case discovery +- [ ] **Stretch**: Implement custom generators for domain objects +- [ ] **Stretch**: Add shrinking to find minimal failing cases +- [ ] **Stretch**: Test API contract invariants +- [ ] **Stretch**: Integrate fuzzing for input validation + +## Copilot Instructions + +**🤖 GitHub Copilot: Please check off tasks in this list as you complete them during development. Keep the task list current to track progress.** + +When working on this issue: +1. Update checkboxes from `[ ]` to `[x]` as you complete each task +2. Work through exercises sequentially (3.1 → 3.2 → 3.3) +3. Mark stretch goals only if time permits +4. Run tests after completing each exercise to verify functionality +5. Keep the task list as a single source of truth for progress + +## Acceptance Criteria + +All non-stretch tasks must be completed with: +- ✅ Tests follow xUnit conventions and naming patterns +- ✅ All tests use Arrange-Act-Assert pattern +- ✅ Tests are isolated and repeatable +- ✅ All tests pass with `dotnet test` +- ✅ Test project structure mirrors main project +- ✅ Proper mocking and test doubles used +- ✅ Both happy paths and error cases covered + +## Resources + +- xUnit Documentation: https://xunit.net/ +- Moq Documentation: https://github.com/moq/moq4 +- WebApplicationFactory: https://learn.microsoft.com/aspnet/core/test/integration-tests +- FsCheck: https://fscheck.github.io/FsCheck/ diff --git a/advanced-practice/module-03-testing-expansion.md b/advanced-practice/module-03-testing-expansion.md index 1bae43a..1d847c6 100644 --- a/advanced-practice/module-03-testing-expansion.md +++ b/advanced-practice/module-03-testing-expansion.md @@ -4,6 +4,65 @@ --- +## 🚀 Getting Started: Create a GitHub Issue + +Before starting the exercises, create a tracking issue using GitHub Copilot: + +### Step 1: Open GitHub Issue Creation +1. Navigate to your repository on github.com +2. Go to the **Issues** tab +3. Click **New Issue** + +### Step 2: Use Copilot to Generate Issue +1. Below the description field, click the **Write with Copilot** link +2. Use the contents of [`module-03-issue-prompt.md`](module-03-issue-prompt.md) as your prompt +3. Copilot will generate a complete issue description with: + - Overview of testing expansion goals + - Detailed task checklist (45+ items) + - Instructions for Copilot to maintain the task list + - Acceptance criteria and resources + +### Step 3: Create and Track +1. Give the issue a title: **"Module 3: Testing Expansion"** +2. Add labels: `enhancement`, `testing`, `copilot-practice` +3. Create the issue + +### Step 4: Assign Work to Copilot Coding Agent +1. In the created issue on GitHub.com, locate the **Assignees** section in the right sidebar +2. Click **Assign to Copilot** (or search for "copilot" in assignees) +3. This assigns the autonomous coding agent to work on the issue +4. Copilot will begin analyzing the issue and creating an implementation plan + +### Step 5: Monitor the Coding Agent Session +**Track Progress:** +- View the newly created Pull request that is in **WIP** status +- Scroll down to line that reads **Copilot started work on behalf of @your-username** +- Click the View Session button +- Monitor real-time updates as Copilot: + - Analyzes the codebase + - Creates a work plan + - Implements tests + - Runs test suites + - Updates task checkboxes + - Creates commits + +**Review Work:** +- Check the **Files Changed** section to see what Copilot is modifying +- Review the **Commit History** for the issue branch +- Copilot will create a Pull Request when ready + +**Interact with Agent:** +- Add comments to the issue to provide guidance or corrections +- Use @copilot mentions to ask questions or request changes +- Approve or request changes on the PR when Copilot completes work + +**Tips:** +- The agent works autonomously but you can pause/resume as needed +- Task list updates happen automatically as Copilot completes work +- You can take over manually at any time by checking out the branch + +--- + ### Exercise 3.1: Comprehensive Unit Test Suite **Goal**: Achieve high unit test coverage for controllers and models diff --git a/net-users-api/Controllers/UsersController.cs b/net-users-api/Controllers/UsersController.cs index 9451579..b4e07cb 100644 --- a/net-users-api/Controllers/UsersController.cs +++ b/net-users-api/Controllers/UsersController.cs @@ -12,9 +12,9 @@ public class UsersController : ControllerBase // Sample user data private static List _users = new() { - new UserProfile { Id = "1", FullName = "John Doe", Emoji = "😀" }, - new UserProfile { Id = "2", FullName = "Jane Smith", Emoji = "🚀" }, - new UserProfile { Id = "3", FullName = "Robert Johnson", Emoji = "🎸" } + new UserProfile { Id = "1", FullName = "John Doe", Email = "john.doe@example.com", Emoji = "😀" }, + new UserProfile { Id = "2", FullName = "Jane Smith", Email = "jane.smith@example.com", Emoji = "🚀" }, + new UserProfile { Id = "3", FullName = "Robert Johnson", Email = "robert.johnson@example.com", Emoji = "🎸" } }; public UsersController(ILogger logger) @@ -45,7 +45,13 @@ public ActionResult GetUser(string id) if (user == null) { - return NotFound(new { error = "User not found" }); + var error = new ApiError + { + ErrorCode = "USER_NOT_FOUND", + Message = $"User with ID '{id}' was not found", + Path = HttpContext.Request.Path + }; + return NotFound(error); } return Ok(user); @@ -59,9 +65,35 @@ public ActionResult GetUser(string id) [HttpPost] public ActionResult CreateUser([FromBody] UserProfile newUser) { - if (newUser == null) + if (!ModelState.IsValid) { - return BadRequest(new { error = "Invalid user data" }); + var validationErrors = ModelState + .Where(x => x.Value?.Errors.Count > 0) + .ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray() + ); + + var error = new ApiError + { + ErrorCode = "VALIDATION_ERROR", + Message = "One or more validation errors occurred", + Path = HttpContext.Request.Path, + Details = validationErrors + }; + return BadRequest(error); + } + + // Check for duplicate ID + if (_users.Any(u => u.Id == newUser.Id)) + { + var error = new ApiError + { + ErrorCode = "DUPLICATE_ID", + Message = $"User with ID '{newUser.Id}' already exists", + Path = HttpContext.Request.Path + }; + return BadRequest(error); } // For simplicity, we're just appending to the list @@ -80,16 +112,36 @@ public ActionResult CreateUser([FromBody] UserProfile newUser) [HttpPut("{id}")] public ActionResult UpdateUser(string id, [FromBody] UserProfile updatedUser) { - if (updatedUser == null) + if (!ModelState.IsValid) { - return BadRequest(new { error = "Invalid user data" }); + var validationErrors = ModelState + .Where(x => x.Value?.Errors.Count > 0) + .ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray() + ); + + var error = new ApiError + { + ErrorCode = "VALIDATION_ERROR", + Message = "One or more validation errors occurred", + Path = HttpContext.Request.Path, + Details = validationErrors + }; + return BadRequest(error); } var index = _users.FindIndex(u => u.Id == id); if (index == -1) { - return NotFound(new { error = "User not found" }); + var error = new ApiError + { + ErrorCode = "USER_NOT_FOUND", + Message = $"User with ID '{id}' was not found", + Path = HttpContext.Request.Path + }; + return NotFound(error); } // Ensure ID doesn't change diff --git a/net-users-api/Middleware/GlobalExceptionHandlerMiddleware.cs b/net-users-api/Middleware/GlobalExceptionHandlerMiddleware.cs new file mode 100644 index 0000000..dd4eb60 --- /dev/null +++ b/net-users-api/Middleware/GlobalExceptionHandlerMiddleware.cs @@ -0,0 +1,79 @@ +using System.Net; +using System.Text.Json; +using NetUsersApi.Models; + +namespace NetUsersApi.Middleware; + +/// +/// Middleware to handle exceptions globally and return consistent error responses +/// +public class GlobalExceptionHandlerMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly IHostEnvironment _environment; + + public GlobalExceptionHandlerMiddleware( + RequestDelegate next, + ILogger logger, + IHostEnvironment environment) + { + _next = next; + _logger = logger; + _environment = environment; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unhandled exception occurred: {Message}", ex.Message); + await HandleExceptionAsync(context, ex); + } + } + + private Task HandleExceptionAsync(HttpContext context, Exception exception) + { + var (statusCode, errorCode) = MapExceptionToStatusCode(exception); + + var apiError = new ApiError + { + ErrorCode = errorCode, + Message = exception.Message, + Path = context.Request.Path, + Timestamp = DateTime.UtcNow, + Details = _environment.IsDevelopment() ? exception.StackTrace : null + }; + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = (int)statusCode; + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + var json = JsonSerializer.Serialize(apiError, options); + return context.Response.WriteAsync(json); + } + + private (HttpStatusCode statusCode, string errorCode) MapExceptionToStatusCode(Exception exception) + { + return exception switch + { + ArgumentNullException => (HttpStatusCode.BadRequest, "INVALID_INPUT"), + ArgumentException => (HttpStatusCode.BadRequest, "INVALID_ARGUMENT"), + InvalidOperationException => (HttpStatusCode.BadRequest, "INVALID_OPERATION"), + KeyNotFoundException => (HttpStatusCode.NotFound, "NOT_FOUND"), + UnauthorizedAccessException => (HttpStatusCode.Unauthorized, "UNAUTHORIZED"), + NotImplementedException => (HttpStatusCode.NotImplemented, "NOT_IMPLEMENTED"), + TimeoutException => (HttpStatusCode.RequestTimeout, "TIMEOUT"), + _ => (HttpStatusCode.InternalServerError, "INTERNAL_ERROR") + }; + } +} diff --git a/net-users-api/Middleware/RateLimitingMiddleware.cs b/net-users-api/Middleware/RateLimitingMiddleware.cs new file mode 100644 index 0000000..4785a7b --- /dev/null +++ b/net-users-api/Middleware/RateLimitingMiddleware.cs @@ -0,0 +1,129 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Text.Json; +using NetUsersApi.Models; + +namespace NetUsersApi.Middleware; + +/// +/// Middleware to implement rate limiting based on IP address +/// +public class RateLimitingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly int _requestLimit; + private readonly TimeSpan _timeWindow; + private readonly List _excludedPaths; + + // Thread-safe dictionary to store request counts per IP + private static readonly ConcurrentDictionary _requestCounts = new(); + + public RateLimitingMiddleware( + RequestDelegate next, + ILogger logger, + IConfiguration configuration) + { + _next = next; + _logger = logger; + + // Load configuration with defaults + _requestLimit = configuration.GetValue("RateLimiting:RequestLimit", 100); + var windowMinutes = configuration.GetValue("RateLimiting:TimeWindowMinutes", 1); + _timeWindow = TimeSpan.FromMinutes(windowMinutes); + _excludedPaths = configuration.GetSection("RateLimiting:ExcludedPaths") + .Get>() ?? new List { "/health" }; + } + + public async Task InvokeAsync(HttpContext context) + { + // Check if path is excluded from rate limiting + if (_excludedPaths.Any(path => context.Request.Path.StartsWithSegments(path))) + { + await _next(context); + return; + } + + var ipAddress = GetClientIpAddress(context); + var counter = _requestCounts.GetOrAdd(ipAddress, _ => new RequestCounter()); + + bool rateLimitExceeded = false; + int retryAfter = 0; + ApiError? error = null; + + lock (counter) + { + // Clean up old requests outside the time window + counter.Requests.RemoveAll(time => DateTime.UtcNow - time > _timeWindow); + + // Check if limit exceeded + if (counter.Requests.Count >= _requestLimit) + { + _logger.LogWarning("Rate limit exceeded for IP: {IpAddress}", ipAddress); + + var oldestRequest = counter.Requests.Min(); + retryAfter = (int)Math.Ceiling((_timeWindow - (DateTime.UtcNow - oldestRequest)).TotalSeconds); + + error = new ApiError + { + ErrorCode = "RATE_LIMIT_EXCEEDED", + Message = $"Rate limit exceeded. Maximum {_requestLimit} requests per {_timeWindow.TotalMinutes} minute(s)", + Path = context.Request.Path, + Details = new + { + Limit = _requestLimit, + WindowMinutes = _timeWindow.TotalMinutes, + RetryAfterSeconds = retryAfter + } + }; + + rateLimitExceeded = true; + } + else + { + // Add current request + counter.Requests.Add(DateTime.UtcNow); + } + } + + if (rateLimitExceeded && error != null) + { + context.Response.StatusCode = (int)HttpStatusCode.TooManyRequests; + context.Response.ContentType = "application/json"; + context.Response.Headers["Retry-After"] = retryAfter.ToString(); + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + await context.Response.WriteAsync(JsonSerializer.Serialize(error, options)); + return; + } + + await _next(context); + } + + private string GetClientIpAddress(HttpContext context) + { + // Try to get IP from X-Forwarded-For header (for proxies/load balancers) + var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + if (!string.IsNullOrEmpty(forwardedFor)) + { + var ips = forwardedFor.Split(','); + if (ips.Length > 0) + { + return ips[0].Trim(); + } + } + + // Fallback to remote IP address + return context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + } + + private class RequestCounter + { + public List Requests { get; } = new(); + } +} diff --git a/net-users-api/Models/ApiError.cs b/net-users-api/Models/ApiError.cs new file mode 100644 index 0000000..0a1d225 --- /dev/null +++ b/net-users-api/Models/ApiError.cs @@ -0,0 +1,32 @@ +namespace NetUsersApi.Models; + +/// +/// Represents a standardized error response for API errors +/// +public class ApiError +{ + /// + /// A machine-readable error code + /// + public required string ErrorCode { get; set; } + + /// + /// A human-readable error message + /// + public required string Message { get; set; } + + /// + /// Optional additional details about the error (validation errors, stack trace, etc.) + /// + public object? Details { get; set; } + + /// + /// Timestamp when the error occurred + /// + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + /// + /// The request path that generated the error + /// + public required string Path { get; set; } +} diff --git a/net-users-api/Models/UserProfile.cs b/net-users-api/Models/UserProfile.cs index 07e23bc..f5deb88 100644 --- a/net-users-api/Models/UserProfile.cs +++ b/net-users-api/Models/UserProfile.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace NetUsersApi.Models; /// @@ -5,7 +7,32 @@ namespace NetUsersApi.Models; /// public class UserProfile { + /// + /// User identifier - alphanumeric, 1-50 characters + /// + [Required(ErrorMessage = "Id is required")] + [RegularExpression(@"^[a-zA-Z0-9]{1,50}$", ErrorMessage = "Id must be alphanumeric and 1-50 characters")] public required string Id { get; set; } + + /// + /// User's full name - 2-100 characters, no numbers allowed + /// + [Required(ErrorMessage = "FullName is required")] + [StringLength(100, MinimumLength = 2, ErrorMessage = "FullName must be between 2 and 100 characters")] + [RegularExpression(@"^[a-zA-Z\s\-'\.]+$", ErrorMessage = "FullName must contain only letters, spaces, hyphens, apostrophes, and periods")] public required string FullName { get; set; } + + /// + /// User's email address + /// + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Email must be a valid email address")] + public required string Email { get; set; } + + /// + /// User's emoji - 1-10 characters + /// + [Required(ErrorMessage = "Emoji is required")] + [StringLength(10, MinimumLength = 1, ErrorMessage = "Emoji must be between 1 and 10 characters")] public required string Emoji { get; set; } } diff --git a/net-users-api/Program.cs b/net-users-api/Program.cs index 9ebf4d5..6609fce 100644 --- a/net-users-api/Program.cs +++ b/net-users-api/Program.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using NetUsersApi.Middleware; var builder = WebApplication.CreateBuilder(args); @@ -27,6 +28,12 @@ app.MapOpenApi(); } +// Add global exception handling middleware +app.UseMiddleware(); + +// Add rate limiting middleware +app.UseMiddleware(); + app.UseRouting(); // Map MVC controllers (for Home/Index view) diff --git a/net-users-api/appsettings.json b/net-users-api/appsettings.json index 4d56694..cbc37af 100644 --- a/net-users-api/appsettings.json +++ b/net-users-api/appsettings.json @@ -5,5 +5,12 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "RateLimiting": { + "RequestLimit": 100, + "TimeWindowMinutes": 1, + "ExcludedPaths": [ + "/health" + ] + } }