A .NET 9.0 solution demonstrating Clean Architecture principles for interacting with external APIs. This project implements a robust, generic API client for the reqres.in service with comprehensive error handling, retry logic, caching, and extensive testing.
- [This README is generated with the help of an AI tool but the project is handmade by a human developer :) ]
This solution follows Clean Architecture principles with a generic, reusable design that can support multiple entity types:
├── src/
│ ├── RaftLabs.Domain/ # Core business entities and interfaces
│ ├── RaftLabs.Application/ # Use cases and application services
│ ├── RaftLabs.Infrastructure/ # Generic HTTP client and repository patterns
│ └── RaftLabs.Presentation/ # Web API controllers and endpoints
├── tests/
│ └── RaftLabs.UnitTests/ # Comprehensive unit tests (11 tests)
├── samples/
│ └── RaftLabs.Console/ # Interactive demo console application
└── README.md
- Domain Layer (innermost): Contains core business entities, domain exceptions, and repository interfaces
- Application Layer: Implements use cases, business logic, and coordinates between layers
- Infrastructure Layer: Generic HTTP client, repository base classes, caching, and retry policies
- Presentation Layer: REST API endpoints with global exception handling and Swagger documentation
- Extensible Design: Easily add new entity types (Products, Orders, etc.) without code duplication
- Type-Safe Generic Patterns:
IRepository<T, TId>andExternalRepositoryBase<T, TId, TExternal, TExternalPage> - Reusable HTTP Client: Generic
IApiClientsupporting any endpoint and response type - Consistent Patterns: All repositories follow the same structure and behavior
- Generic HTTP Client:
IApiClientusingIHttpClientFactorywith named client registration - Generic Repository Pattern:
IRepository<T, TId>andExternalRepositoryBase<T, TId, TExternal, TExternalPage> - Options Pattern: Strongly-typed configuration with
IOptions<ApiSettings> - Async/Await Patterns: Full asynchronous implementation throughout the solution
- Pagination Support: Handles paginated data retrieval and aggregation
- Type-Safe Mapping: Clean mapping between external API models and domain entities
- Retry Logic: Implements exponential backoff using Polly library
- Timeout Handling: Configurable request timeouts per named HttpClient
- In-Memory Caching: Reduces API calls with configurable cache expiration
- Error Handling: Comprehensive exception handling with domain-specific exceptions
- Configurable Settings: External API settings via
appsettings.jsonwith Options pattern - Structured Logging: Comprehensive logging with named HttpClient identification
- Health Monitoring: Built-in error tracking and retry monitoring
- Global Exception Handling: Centralized exception middleware
- Swagger/OpenAPI: Interactive API documentation with API key authentication
- Unit Testing: 11 comprehensive unit tests with 100% pass rate
- Mocking Strategy: Clean mocking of
IApiClientand other dependencies - XML Documentation: Comprehensive code documentation
The solution exposes the following REST endpoints:
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/users/{id} |
Get a specific user by ID |
GET |
/api/users |
Get all users (aggregated from all pages) |
GET |
/api/users/page/{page} |
Get users for a specific page |
Get User by ID:
{
"id": 2,
"email": "[email protected]",
"firstName": "Janet",
"lastName": "Weaver",
"avatar": "https://reqres.in/img/faces/2-image.jpg",
"fullName": "Janet Weaver"
}Get Users Page:
{
"page": 1,
"per_page": 6,
"total": 12,
"total_pages": 2,
"data": [...],
"has_next_page": true,
"has_previous_page": false
}- .NET 9.0 SDK or later (Download here)
- IDE: Visual Studio 2022, VS Code, or JetBrains Rider
- Internet Connection: Required for external API calls to reqres.in
# Clone the repository
git clone <repository-url>
cd RaftLabs
# Restore all packages
dotnet restore
# Build the entire solution
dotnet build# Navigate to console project
cd samples/RaftLabs.Console
# Run the demo (shows all features)
dotnet runExpected Output:
=== RaftLabs User API Client Demo ===
Demo 1: Getting user with ID 2...
Found user: Janet Weaver ([email protected])
Demo 2: Getting users from page 1...
Page 1 of 2 (Total: 12 users)
Demo 3: Getting all users...
Retrieved 12 total users
Demo 4: Testing error handling...
Correctly handled non-existent user
Demo 5: Testing caching...
First call: 250ms, Second call: 0ms
Cache working: Yes
# From solution root
dotnet run --project src/RaftLabs.Presentation
# Or with specific URL
dotnet run --project src/RaftLabs.Presentation --urls "http://localhost:5000"API will be available at:
- Swagger UI:
http://localhost:5000(interactive documentation) - API Base:
http://localhost:5000/api/users
# From solution root
dotnet test
# Expected output:
# Test run for RaftLabs.UnitTests.dll (.NET 9.0)
# Total tests: 11
# Passed: 11
# Failed: 0
# Skipped: 0# Get user by ID
curl http://localhost:5000/api/users/2
# Get all users (aggregated from all pages)
curl http://localhost:5000/api/users
# Get specific page
curl http://localhost:5000/api/users/page/1
# Test error handling
curl http://localhost:5000/api/users/999# Clean and rebuild entire solution
dotnet clean && dotnet build
# Build specific project
dotnet build src/RaftLabs.Infrastructure
# Build in Release mode
dotnet build --configuration Release
# Restore packages only
dotnet restore# Run Web API (default: https://localhost:7042, http://localhost:5000)
dotnet run --project src/RaftLabs.Presentation
# Run Console Demo
dotnet run --project samples/RaftLabs.Console
# Run with specific profile
dotnet run --project src/RaftLabs.Presentation --launch-profile "https"# Run all tests
dotnet test
# Run tests with detailed output
dotnet test --verbosity normal
# Run tests with coverage (requires coverage tools)
dotnet test --collect:"XPlat Code Coverage"
# Run specific test class
dotnet test --filter "ExternalUserRepositoryTests"
# Run specific test method
dotnet test --filter "GetUserByIdAsync_WithValidUser_ReturnsUser"# 1. Make changes to code
# 2. Build to check for compilation errors
dotnet build
# 3. Run tests to ensure functionality
dotnet test
# 4. Test the console app
dotnet run --project samples/RaftLabs.Console
# 5. Test the API
dotnet run --project src/RaftLabs.Presentation{
"ApiSettings": {
"BaseUrl": "https://reqres.in/api/",
"ApiKey": "reqres-free-v1",
"TimeoutSeconds": 30,
"RetryAttempts": 3,
"RetryDelayMs": 1000,
"CacheExpirationMinutes": 5
}
}| Setting | Description | Default | Location |
|---|---|---|---|
BaseUrl |
External API base URL | https://reqres.in/api/ |
Both Console & API |
ApiKey |
API key for authentication | reqres-free-v1 |
HTTP Header x-api-key |
TimeoutSeconds |
HTTP request timeout | 30 |
HttpClient configuration |
RetryAttempts |
Number of retry attempts | 3 |
Polly retry policy |
RetryDelayMs |
Delay between retries (ms) | 1000 |
Exponential backoff base |
CacheExpirationMinutes |
Cache expiration time | 5 |
IMemoryCache settings |
The solution uses named HttpClient registration with the Options pattern:
// Named client registration
builder.Services.AddHttpClient("ReqresApiClient");
// ApiClient uses IHttpClientFactory internally
public ApiClient(IHttpClientFactory httpClientFactory, ILogger<ApiClient> logger, IOptions<ApiSettings> apiSettings)
{
_httpClient = httpClientFactory.CreateClient("ReqresApiClient");
_httpClient.BaseAddress = new Uri(_apiSettings.BaseUrl);
_httpClient.Timeout = TimeSpan.FromSeconds(_apiSettings.TimeoutSeconds);
_httpClient.DefaultRequestHeaders.Add("x-api-key", _apiSettings.ApiKey);
}The solution includes 11 comprehensive unit tests covering:
- GetUserByIdAsync_WithValidUser_ReturnsUser: Successful user retrieval
- GetUserByIdAsync_WithNonExistentUser_ReturnsNull: 404 handling
- GetUserByIdAsync_WithInvalidUserId_ThrowsValidationException: Input validation
- GetUsersPageAsync_WithValidPage_ReturnsPagedResult: Pagination support
- GetUsersPageAsync_WithInvalidPage_ThrowsValidationException: Page validation
- Additional scenarios: Caching, error handling, mapping
- Service layer business logic: Coordination between repositories
- Error propagation: Domain exception handling
- Input validation: Parameter validation at service level
- Logging verification: Structured logging validation
- Mock verification: Dependency interaction verification
// Clean mocking strategy using the generic IApiClient
public class ExternalUserRepositoryTests
{
private readonly Mock<IApiClient> _mockApiClient;
private readonly IMemoryCache _memoryCache;
private readonly Mock<ILogger<ExternalUserRepository>> _mockLogger;
private readonly ExternalUserRepository _repository;
public ExternalUserRepositoryTests()
{
_mockApiClient = new Mock<IApiClient>();
// ... setup with Options.Create(apiSettings)
_repository = new ExternalUserRepository(_mockApiClient.Object, _memoryCache, _mockLogger.Object, options);
}
}# Run all tests (11 tests)
dotnet test
# Expected: Total tests: 11, Passed: 11, Failed: 0
# Run with detailed output
dotnet test --verbosity normal
# Run with minimal output
dotnet test --verbosity quiet# Run specific test class
dotnet test --filter "ExternalUserRepositoryTests"
# Run specific test method
dotnet test --filter "GetUserByIdAsync_WithValidUser_ReturnsUser"
# Run tests with logger output
dotnet test --logger "console;verbosity=detailed"
# Run tests and generate coverage report (if coverage tools installed)
dotnet test --collect:"XPlat Code Coverage"Expected successful test run:
Test Run Successful.
Total tests: 11
Passed: 11
Failed: 0
Skipped: 0
Total time: ~2-3 seconds
If tests fail, check:
- Network connectivity (tests use mocked dependencies, but build needs packages)
- .NET 9.0 SDK installation
- NuGet package restoration (dotnet restore)
- Mock all external dependencies
- Test business logic in isolation
- Fast execution (< 3 seconds total)
- No external API calls
- Test with real HTTP calls to reqres.in
- Validate end-to-end scenarios
- Network dependency required
- Cache performance validation
- HTTP client connection pooling
- Retry policy effectiveness
- Clean Architecture: Ensures maintainability, testability, and separation of concerns
- Generic Repository Pattern:
ExternalRepositoryBase<T, TId, TExternal, TExternalPage>for reusability - HttpClientFactory with Named Clients: Prevents socket exhaustion, enables proper logging
- Options Pattern: Strongly-typed configuration with
IOptions<ApiSettings> - Polly for Resilience: Industry-standard library for retry policies and circuit breakers
- Memory Caching: Reduces external API calls and improves performance
- Structured Logging: Enables comprehensive monitoring and debugging
- Domain Exceptions: Custom exceptions (
NotFoundException,ValidationException, etc.) - HTTP Error Mapping: Proper HTTP status codes for different scenarios
- Retry Logic: Exponential backoff for transient failures
- Global Exception Middleware: Centralized error handling in the API layer
// Easy to extend for new entity types
public class ProductRepository : ExternalRepositoryBase<Product, int, ExternalProductResponse, ExternalProductsResponse>
{
protected override string EndpointName => "products";
protected override string CacheKeyPrefix => "product";
// Only need to implement mapping - everything else is inherited!
protected override Product MapFromExternalResponse(ExternalProductResponse response) => response.Data.ToProduct();
protected override PagedResult<Product> MapFromExternalPageResponse(ExternalProductsResponse response) => response.ToPagedResult();
}- Retry Logic: Exponential backoff for transient failures
- Graceful Degradation: Fallback mechanisms for service unavailability
- Async Throughout: Full asynchronous implementation
- HTTP Client Reuse: Efficient connection management
- Response Caching: Configurable in-memory caching
- Lazy Loading: Data fetched only when needed
Microsoft.Extensions.Http- HttpClientFactory and named client managementMicrosoft.Extensions.Caching.Memory- In-memory caching with IMemoryCacheMicrosoft.Extensions.Options- Options pattern for strongly-typed configurationPolly.Extensions.Http- Retry policies and resilience patternsSwashbuckle.AspNetCore- Swagger/OpenAPI documentation
System.Text.Json- JSON serialization/deserializationMicrosoft.Extensions.Logging- Structured logging throughout the applicationMicrosoft.Extensions.DependencyInjection- Dependency injection container
xUnit- Modern testing framework for .NETMoq- Mocking framework for unit testingMicrosoft.Extensions.Logging.Abstractions- Logging abstractions for testing
The included console application demonstrates all key features:
dotnet run --project samples/RaftLabs.ConsoleFeatures demonstrated:
- Individual user retrieval with caching
- Paginated data retrieval
- Error handling for non-existent resources
- Performance comparison (first call vs cached call)
- Comprehensive logging output
# Start the API
dotnet run --project src/RaftLabs.Presentation
# Test endpoints
curl http://localhost:5000/api/users # Get all users
curl http://localhost:5000/api/users/2 # Get specific user
curl http://localhost:5000/api/users/page/1 # Get users page
curl http://localhost:5000/api/users/999 # Test error handling (404)// Dependency injection setup (Program.cs)
builder.Services.Configure<ApiSettings>(builder.Configuration.GetSection("ApiSettings"));
builder.Services.AddHttpClient("ReqresApiClient");
builder.Services.AddScoped<IApiClient, ApiClient>();
builder.Services.AddScoped<IUserRepository, ExternalUserRepository>();
builder.Services.AddScoped<IUserService, UserService>();
// Usage in your application
public class MyService
{
private readonly IUserService _userService;
public MyService(IUserService userService)
{
_userService = userService;
}
public async Task<UserDto> GetUserAsync(int id)
{
return await _userService.GetUserByIdAsync(id);
}
public async Task<PagedResult<UserDto>> GetUsersPageAsync(int page)
{
return await _userService.GetUsersPageAsync(page);
}
}// 1. Define domain entity
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
// 2. Define external models
public class ExternalProductResponse
{
public ExternalProductModel Data { get; set; }
}
public class ExternalProductsResponse
{
public List<ExternalProductModel> Data { get; set; }
// ... pagination properties
}
// 3. Create repository (inherits all functionality!)
public class ProductRepository : ExternalRepositoryBase<Product, int, ExternalProductResponse, ExternalProductsResponse>, IProductRepository
{
protected override string EndpointName => "products";
protected override string CacheKeyPrefix => "product";
public ProductRepository(IApiClient apiClient, IMemoryCache cache, ILogger<ProductRepository> logger, IOptions<ApiSettings> apiSettings)
: base(apiClient, cache, logger, apiSettings) { }
protected override Product MapFromExternalResponse(ExternalProductResponse response) => response.Data.ToProduct();
protected override PagedResult<Product> MapFromExternalPageResponse(ExternalProductsResponse response) => response.ToPagedResult();
}
// 4. Register in DI container
builder.Services.AddScoped<IProductRepository, ProductRepository>();The application provides structured logging at multiple levels:
- Information: Successful operations, HTTP requests, cache hits/misses
- Warning: Retry attempts, validation warnings, cache evictions
- Error: Exceptions, service failures, network timeouts
Named HttpClient Logging:
info: System.Net.Http.HttpClient.ReqresApiClient.LogicalHandler[100]
Start processing HTTP request GET https://reqres.in/api/users/2
info: System.Net.Http.HttpClient.ReqresApiClient.ClientHandler[101]
Received HTTP response headers after 245ms - 200
The console demo shows real performance data:
- API Response Times: Actual HTTP call duration
- Cache Effectiveness: "First call: 250ms, Second call: 0ms"
- Retry Monitoring: Failed attempts and retry logic execution
- Error Rates: Success/failure ratios by operation type
- HttpClient Connection Pooling: Automatic via HttpClientFactory
- Request/Response Logging: Detailed HTTP operation tracking
- Cache Hit Ratios: Memory cache performance metrics
- Exception Tracking: Comprehensive error logging with context
# Clean and rebuild if you encounter build issues
dotnet clean
dotnet restore
dotnet build# Ensure all packages are restored
dotnet restore
dotnet test --verbosity normal# Verify network connectivity
curl https://reqres.in/api/users/1
# Check configuration in appsettings.json
cat samples/RaftLabs.Console/appsettings.json# Use different port if 5000 is occupied
dotnet run --project src/RaftLabs.Presentation --urls "http://localhost:8080"- Solution: Uses .NET 9.0 target framework
- Dependencies: All packages compatible with .NET 9.0
- Architecture: Clean Architecture with generic patterns
- Test Framework: xUnit with Moq for mocking
- HTTP Client: Named client with HttpClientFactory
- Cold Start: ~250ms (first API call)
- Cached Response: ~0ms (subsequent identical calls)
- Page Retrieval: ~20-50ms (typical API response)
- Error Handling: ~500ms (network timeout scenarios)
- Startup: Minimal footprint with DI container
- Caching: Configurable memory cache (5-minute default expiration)
- HTTP Connections: Pooled and reused via HttpClientFactory
- Connection Pooling: Automatic via HttpClientFactory
- Async Operations: Full async/await throughout
- Generic Design: Easy horizontal scaling for new entity types
- Stateless Design: API can be deployed across multiple instances
- Fork the repository
- Create a feature branch
- Make your changes
- Add/update tests
- Ensure all tests pass
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
For questions or support, please contact:
- Developer: @ayushwritescode
- Email: [email protected]
Built with ❤️ using .NET 9.0 and Clean Architecture principles