"Thoughtful API design is the backbone of scalable, secure, and maintainable applications—embrace RESTful principles and .NET 8 best practices for long-term success."
This guide documents the API design principles, patterns, and best practices implemented in CleanArchitecture.ApiTemplate. The API follows RESTful conventions, Clean Architecture principles, and modern .NET 8 standards for building secure, scalable, and maintainable web APIs.
- API Architecture
- API Versioning
- RESTful Conventions
- Request/Response Patterns
- Model Validation
- Query Parameters
- Caching Strategy
- Performance Optimization
- Error Handling
- Security
- Code Examples
- Reference Files
- Additional Resources
- Contact
Client ? Middleware ? API Controller ? MediatR ? Handler ? Service ? External API
(HTTP Pipeline) (Presentation) (Application) (Infrastructure)
?
JWT Blacklist
Validation
Request Pipeline Layers:
-
Middleware (HTTP Pipeline) -
JwtBlacklistValidationMiddleware- Validates JWT tokens before reaching controllers
- Checks token blacklist via
IsTokenBlacklistedQuery - Returns 401 Unauthorized if token is blacklisted
- Executes before authorization policies
-
API Controller (Presentation) -
SampleController,AuthController- Handles HTTP request/response
- Validates model state
- Sends commands/queries to MediatR
- Returns appropriate HTTP status codes
-
MediatR (Application) - CQRS pattern
- Routes commands/queries to handlers
- Executes pipeline behaviors (logging, caching, validation)
- Provides clean separation between presentation and business logic
-
Handler (Application) -
GetApiDataQueryHandler,LoginUserCommandHandler- Contains business logic
- Calls infrastructure services
- Returns
Result<T>pattern for consistent error handling
-
Service (Infrastructure) -
ApiIntegrationService,TokenBlacklistService- Implements external integrations
- Handles HTTP clients, databases, caching
- Uses
IHttpClientFactorywith Polly resilience policies
-
External API / Resources - Third-party APIs, databases, cache
- Azure Key Vault for secrets
- Distributed cache (Redis) for token blacklist
- Third-party RESTful APIs
Key Principles:
- ? Separation of Concerns - Middleware handles security, controllers handle HTTP, handlers handle business logic
- ? Dependency Inversion - Depend on abstractions (IMediator, IApiIntegrationService, ITokenBlacklistService)
- ? Single Responsibility - Each component has one clear purpose
- ? Testability - All layers can be tested independently
- ? Security by Design - Token validation happens before any business logic executes
Detailed Request Workflow (15 Steps)
1. Client sends GET /api/v1/sample with JWT token
?
2. JwtBlacklistValidationMiddleware extracts token
?
3. Middleware sends IsTokenBlacklistedQuery via MediatR
?
4. IsTokenBlacklistedQueryHandler checks cache (Memory ? Distributed)
?
5. If blacklisted ? return 401 Unauthorized (request stops here)
If valid ? continue to next middleware
?
6. Authorization middleware checks JWT claims & policies
?
7. Request reaches SampleController.GetAllData()
?
8. Controller sends GetApiDataQuery via MediatR
?
9. CachingBehavior checks cache (pipeline behavior)
?
10. GetApiDataQueryHandler calls ApiIntegrationService
?
11. ApiKeyHandler adds API key (DelegatingHandler)
?
12. Polly applies retry + circuit breaker policies
?
13. External API returns data
?
14. Response flows back through pipeline
?
15. Client receives 200 OK with data
Request Scenarios & Entry Points
1. Authenticated Request with Valid Token
Client ? JWT Bearer Token (valid, not blacklisted)
?
Middleware validates token ? PASS
?
Authorization checks claims ? PASS
?
Controller processes request
?
Business logic executes
2. Authenticated Request with Blacklisted Token
Client ? JWT Bearer Token (blacklisted)
?
Middleware checks blacklist ? FAIL (token revoked)
?
Request terminated at middleware (401 Unauthorized)
?
Controller NEVER reached (early termination)
3. Unauthenticated Request
Client ? No JWT token or invalid token
?
ASP.NET Core Authentication middleware ? FAIL
?
Request terminated (401 Unauthorized)
?
JwtBlacklistValidationMiddleware NOT executed
4. Authenticated Request with Insufficient Permissions
Client ? Valid JWT token (User role)
?
Middleware validates token ? PASS
?
Controller checks [Authorize(Policy = "AdminOnly")]
?
Authorization policy check ? FAIL (User != Admin)
?
Request terminated (403 Forbidden)
5. Request with Cache Hit (Fast Path)
Client ? Valid request
?
Authentication & Authorization ? PASS
?
Controller ? MediatR ? CachingBehavior
?
Cache HIT! (data found in cache)
?
Handler SKIPPED, Service SKIPPED, External API SKIPPED
?
Cached data returned immediately (~10ms response)
Request Headers Expected by API
GET /api/v1/sample HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
Accept: application/json
User-Agent: MyApp/1.0
X-Request-Id: abc123-def456-ghi789
// Optional Headers
Cache-Control: no-cache
Accept-Encoding: gzip, deflate, brPerformance Metrics by Request Type
| Request Type | Layers Executed | Average Time | Cache Status |
|---|---|---|---|
| Cached Request (Best) | Up to CachingBehavior | ~10-20ms | HIT ? |
| Uncached Request | All layers + External API | ~150-300ms | MISS ? |
| Blacklisted Token | Middleware only | ~5-10ms | N/A |
| Invalid Token | Authentication middleware | ~2-5ms | N/A |
| Insufficient Permissions | Up to Authorization | ~3-8ms | N/A |
Key Request Flow Principles
- ? Security First - Token validation happens before any business logic
- ? Early Termination - Invalid requests stop at middleware (don't waste resources)
- ? CQRS Everywhere - Even middleware uses CQRS (IsTokenBlacklistedQuery)
- ? Automatic Caching - Cache check happens transparently via pipeline behavior
- ? Dual-Cache Strategy - Memory cache (fast) + Distributed cache (shared)
- ? Resilience Patterns - Polly retry and circuit breaker for external calls
Real-World Request Example (Complete Flow)
// 1. Client sends HTTP request with JWT token
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", jwtToken);
var response = await client.GetAsync("https://api.example.com/api/v1/sample");
/ Reached controller
[HttpGet]
[Authorize] // Requires valid JWT token
public async Task<IActionResult> GetAllData()
{
_logger.LogInformation(
"User {UserId} requested all data at {Time}",
User.Identity?.Name, DateTime.UtcNow);
// 4. Controller sends query to MediatR
var result = await _mediator.Send(new GetApiDataQuery());
// 5. CachingBehavior checks cache first
// If cache miss, handler is executed
// If cache hit, handler is skipped
return result.Success
? Ok(result.Data)
: BadRequest(new { error = result.Error });
}
// 6. MediatR routing and pipeline behaviors
// LoggingBehavior ? logs request
// ValidationBehavior ? validates query (if applicable)
// CachingBehavior ? checks cache
// - Cache HIT: return cached data (~10ms total)
// - Cache MISS: execute handler (~250ms total)
// 7. Handler executes (on cache miss)
public async Task<Result<List<SampleDto>>> Handle(
GetApiDataQuery request,
CancellationToken cancellationToken)
{
_logger.LogInformation("Executing GetApiDataQuery - Cache MISS");
// 8. Handler calls service
return await _apiService.GetAllDataAsync<List<SampleDto>>("api/data");
}
// 9. Service makes external API call with resilience
// ApiKeyHandler adds X-API-Key header
// Polly retries on transient failures (3 attempts, exponential backoff)
// Circuit breaker prevents cascading failures
// 10. Response flows back through pipeline (see Response Workflow)Complete Round-Trip Journey: Request ? Processing ? Response
This workflow demonstrates how responses flow back through the architecture layers, including caching, serialization, and HTTP formatting.
Response Pipeline Flow:
External API ? Service ? Handler ? MediatR ? Controller ? Middleware ? Client
(Infrastructure) (Application) (Presentation) (HTTP Pipeline)
?
Result<T>
Wrapping
Response Pipeline Layers:
-
External API / Resources - Third-party APIs, databases, cache
- Returns JSON data or error responses
- Azure Key Vault provides secrets
- Distributed cache (Redis) stores token blacklist and cached responses
- Third-party RESTful APIs return business data
-
Service (Infrastructure) -
ApiIntegrationService,TokenBlacklistService- Receives HTTP responses from external APIs
- Deserializes JSON to strongly-typed DTOs
- Wraps data in
Result<T>pattern for consistent error handling - Handles API errors and exceptions gracefully
-
Handler (Application) -
GetApiDataQueryHandler,LoginUserCommandHandler- Receives
Result<T>from service - Validates business rules
- Returns
Result<T>to MediatR - Logs execution status and errors
- Receives
-
MediatR (Application) - CQRS pattern
- Receives response from handler
- Executes reverse pipeline behaviors (caching writes, response logging)
- CachingBehavior stores successful results in cache
- Returns
Result<T>to controller
-
API Controller (Presentation) -
SampleController,AuthController- Checks
Result<T>.Successproperty - Returns
Ok(result.Data)for success (200 OK) - Returns
BadRequest(result.Error)for failures (400 Bad Request) - Adds response metadata and correlation IDs
- Checks
-
Middleware (HTTP Pipeline) - Response middleware stack
- CORS middleware adds
Access-Control-*headers - Security Headers middleware adds
X-Content-Type-Options,X-Frame-Options - Compression middleware compresses response body (if enabled)
- Logging middleware records response time and status
- CORS middleware adds
Key Response Principles:
- ? Result Pattern - No exception-based control flow for error handling
- ? Type Safety - Strong typing maintained from service to controller (Result<T>)
- ? Automatic Caching - Successful responses automatically cached by pipeline behavior
- ? Security Headers - Added automatically by middleware on every response
- ? Structured Logging - Response status, duration, and cache status logged
- ? Content Negotiation - ASP.NET Core serializes to JSON automatically
Detailed Response Workflow (15 Steps)
1. External API returns JSON data
?
2. ApiIntegrationService receives HTTP response
?
3. Service deserializes JSON to strongly-typed DTO
?
4. Service wraps data in Result<T> pattern
Result<List<SampleDto>>.Ok(data)
?
5. Result<T> returned to GetApiDataQueryHandler
?
6. Handler validates result and returns to MediatR
?
7. MediatR Pipeline - CachingBehavior intercepts response
?
8. If query implements ICacheable ? store in cache
Memory Cache (fast access) + Distributed Cache (shared)
?
9. MediatR returns Result<T> to Controller
?
10. Controller checks Result<T>.Success
?
11. If success ? return Ok(result.Data) with 200 status
If failure ? return BadRequest(result.Error) with 400 status
?
12. ASP.NET Core serializes response to JSON
Content-Type: application/json
?
13. Response flows back through middleware pipeline
- CORS middleware adds Access-Control headers
- Security Headers middleware adds X-Content-Type-Options, X-Frame-Options
- Compression middleware (if enabled) compresses response
?
14. HTTP response sent over network
Status: 200 OK
Headers: Content-Type, Content-Length, Security Headers
Body: JSON data
?
15. Client receives complete HTTP response
{
"id": 123,
"name": "Sample Data",
"createdAt": "2024-01-15T10:30:00Z"
}
Response Scenarios & HTTP Status Codes
1. Success Response (200 OK)
Handler returns: Result<T>.Ok(data)
?
Controller returns: Ok(result.Data)
?
Client receives: 200 OK
{
"id": 123,
"name": "Sample Data",
"createdAt": "2024-01-15T10:30:00Z"
}
2. Validation Error (400 Bad Request)
Handler returns: Result<T>.Fail("Validation failed")
?
Controller returns: BadRequest(new { error = result.Error })
?
Client receives: 400 Bad Request
{
"error": "Validation failed",
"timestamp": "2024-01-15T10:30:00Z"
}
3. Unauthorized (401 Unauthorized)
Middleware detects: Token is blacklisted
?
Middleware returns: 401 Unauthorized (request stops here)
?
Client receives: 401 Unauthorized
{
"error": "Token has been revoked",
"message": "Please log in again"
}
4. External API Failure (500 Internal Server Error)
ApiIntegrationService: External API returns 500
?
Service returns: Result<T>.Fail("External API error")
?
Controller catches exception
?
Controller returns: StatusCode(500, new { error = "Unexpected error" })
?
Client receives: 500 Internal Server Error
{
"error": "An unexpected error occurred.",
"timestamp": "2024-01-15T10:30:00Z"
}
5. Cached Response (200 OK - Fast Path)
CachingBehavior: Cache HIT!
?
Handler SKIPPED (not executed)
?
Service SKIPPED (not executed)
?
External API SKIPPED (not called)
?
Cached Result<T> returned directly to Controller
?
Controller returns: Ok(cachedData)
?
Client receives: 200 OK (served from cache - ultra-fast!)
Response Time: ~10ms (vs ~200ms without cache)
Response Headers Applied by Middleware
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1234
Date: Mon, 15 Jan 2024 10:30:00 GMT
// Security Headers (Applied by SecurityHeadersMiddleware)
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
// CORS Headers (Applied by CORS Middleware)
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
// Caching Headers (Optional - for client-side caching)
Cache-Control: private, max-age=300
// Custom Headers (Optional - for tracking)
X-Request-Id: abc123-def456-ghi789
X-Response-Time: 45ms
X-Cache-Status: HITPerformance Metrics by Response Type
| Response Type | Layers Executed | Average Time | Cache Status |
|---|---|---|---|
| Cached (Best Case) | Middleware ? Controller ? MediatR ? CachingBehavior | ~10-20ms | HIT ? |
| Uncached (Normal) | All layers + External API | ~150-300ms | MISS ? |
| Auth Failure | Middleware only (early termination) | ~5-10ms | N/A |
| Validation Error | Up to Controller (no service call) | ~20-30ms | N/A |
Key Response Flow Principles
- ? Result Pattern - Consistent error handling without exceptions
- ? Early Termination - Auth failures stop at middleware (don't reach controller)
- ? Automatic Caching - Transparent caching via MediatR pipeline behavior
- ? Security Headers - Automatically added by middleware on every response
- ? Structured Logging - Response status, duration, and cache status logged
- ? Type Safety - Strong typing maintained throughout (Result<T> ? IActionResult)
Real-World Response Example (Success with Cache)
// 1. External API returns data
var httpResponse = await httpClient.GetAsync("https://api.example.com/data");
// 2. ApiIntegrationService processes response
var data = await httpResponse.Content.ReadFromJsonAsync<List<SampleDto>>();
var result = Result<List<SampleDto>>.Ok(data);
// 3. Handler returns Result<T>
return result; // Success = true, Data = [...], Error = null
// 4. CachingBehavior caches the result
await _cache.SetAsync("api-data-all", result, TimeSpan.FromMinutes(5));
// 5. Controller checks result
if (result.Success)
{
_logger.LogInformation("Successfully retrieved {Count} items", result.Data.Count);
return Ok(result.Data); // 200 OK
}
// 6. Client receives JSON response
// HTTP/1.1 200 OK
// Content-Type: application/json
// X-Cache-Status: MISS (first request)
// X-Response-Time: 245ms
//
// [
// { "id": 1, "name": "Item 1" },
// { "id": 2, "name": "Item 2" }
// ]
// 7. Subsequent request (within 5 minutes) uses cache
// HTTP/1.1 200 OK
// X-Cache-Status: HIT
// X-Response-Time: 12ms (20x faster!)[ApiController]
[Route("api/v1/[controller]")]
public class SampleController : ControllerBase
{
// v1 endpoints
}
[ApiController]
[Route("api/v2/[controller]")]
public class SampleV2Controller : ControllerBase
{
// v2 endpoints with new features
}URL Patterns:
| Version | Pattern | Example |
|---|---|---|
| v1 | /api/v1/{resource} |
/api/v1/sample |
| v2 | /api/v2/{resource} |
/api/v2/sample |
Benefits:
- Clear version identification
- Multiple versions coexist
- Clients upgrade at their own pace
- No breaking changes for existing clients
| Method | Usage | Idempotent | Safe |
|---|---|---|---|
| GET | Retrieve resource(s) | ? Yes | ? Yes |
| POST | Create new resource | ? No | ? No |
| PUT | Update/Replace entire resource | ? Yes | ? No |
| PATCH | Partial update | ? No | ? No |
| DELETE | Remove resource | ? Yes | ? No |
Rules:
- Use nouns, not verbs
- Use plural for collections
- Use lowercase with hyphens
- Use hierarchical structure for relationships
Examples:
? Good:
GET /api/v1/users # Get all users
GET /api/v1/users/123 # Get user by ID
POST /api/v1/users # Create new user
PUT /api/v1/users/123 # Update user
DELETE /api/v1/users/123 # Delete user
GET /api/v1/users/123/orders # Get user's orders
? Bad:
GET /api/v1/getUsers # Verb in URL
POST /api/v1/user # Singular instead of plural
GET /api/v1/users_list # Underscore instead of hyphen
- 200 OK - Successful GET, PUT, PATCH, DELETE
- 201 Created - Successful POST (resource created)
- 204 No Content - Successful DELETE (no response body)
- 400 Bad Request - Invalid input, validation errors
- 401 Unauthorized - Missing or invalid authentication token
- 403 Forbidden - Authenticated but not authorized
- 404 Not Found - Resource doesn't exist
- 429 Too Many Requests - Rate limit exceeded
- 500 Internal Server Error - Unexpected server error
- 503 Service Unavailable - Server temporarily unavailable
CleanArchitecture.ApiTemplate uses the Result Pattern for consistent error handling without exceptions.
Implementation:
public class Result<T>
{
public bool Success { get; private set; }
public T? Data { get; private set; }
public string? Error { get; private set; }
public static Result<T> Ok(T data) => new() { Success = true, Data = data };
public static Result<T> Fail(string error) => new() { Success = false, Error = error };
}Controller Usage:
[HttpGet]
public async Task<IActionResult> GetAllData()
{
var result = await _mediator.Send(new GetApiDataQuery());
if (!result.Success)
return BadRequest(new { error = result.Error });
return Ok(result.Data);
}Benefits:
- ? No exception-based control flow
- ? Type-safe error handling
- ? Consistent response pattern
- ? Easy to compose operations
Success Response:
{
"id": 123,
"name": "Sample Data",
"createdAt": "2024-01-15T10:30:00Z"
}Error Response:
{
"error": "Resource not found",
"timestamp": "2024-01-15T10:30:00Z"
}Validation Error Response:
{
"errors": {
"Name": ["Name is required"],
"Email": ["Invalid email format"]
}
}Effective API contract specification requires close collaboration between backend and UI developers. Early alignment on request payloads, response formats, and error structures ensures seamless integration and reduces rework. Use shared API documentation (e.g., OpenAPI/Swagger), example payloads, and regular feedback cycles to clarify expectations. Agree on field names, data types, and validation rules before implementation. This approach helps UI developers design forms and data flows that match backend requirements, while backend teams can anticipate frontend needs and edge cases. Iterative review and mock API responses further streamline development and testing.
Suppose the UI team needs a user registration endpoint. I as backend developer and UI developers meet to define the contract:
- Request Payload:
{
"name": "John Doe",
"email": "john.doe@example.com",
"password": "P@ssw0rd!"
}- Response Payload (Success):
{
"id": 101,
"name": "John Doe",
"email": "john.doe@example.com",
"createdAt": "2024-01-15T10:30:00Z"
}- Response Payload (Validation Error):
{
"errors": {
"Email": ["Invalid email format"],
"Password": ["Password must be at least 8 characters"]
}
}Both teams document this contract in the shared OpenAPI/Swagger spec and use it for development and testing.
- See
docs/APIDesign/API_CONTRACTS_EXAMPLES.mdfor more contract samples and collaboration tips. - OpenAPI Specification
public class CreateSampleDto
{
[Required(ErrorMessage = "Name is required")]
[StringLength(100, MinimumLength = 3)]
public string Name { get; set; }
[EmailAddress(ErrorMessage = "Invalid email format")]
public string? Email { get; set; }
[Range(1, 1000)]
public int Value { get; set; }
}| Attribute | Purpose | Example |
|---|---|---|
[Required] |
Field must have value | [Required] |
[StringLength] |
Min/max string length | [StringLength(100, MinimumLength = 3)] |
[Range] |
Numeric range | [Range(1, 100)] |
[EmailAddress] |
Valid email format | [EmailAddress] |
[RegularExpression] |
Custom pattern | [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] |
ASP.NET Core automatically validates models before controller actions:
[HttpPost]
public IActionResult Create([FromBody] CreateSampleDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Process valid data
return Created();
}1. Filtering
GET /api/v1/users?status=active&role=admin
2. Sorting
GET /api/v1/users?sortBy=name&order=asc
3. Pagination
GET /api/v1/users?page=1&pageSize=10
4. Searching
GET /api/v1/users?search=john
[HttpGet]
public async Task<IActionResult> GetAll(
[FromQuery] string? search,
[FromQuery] string? sortBy,
[FromQuery] string? order = "asc",
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
var query = new GetPagedDataQuery(search, sortBy, order, page, pageSize);
var result = await _mediator.Send(query);
return Ok(result);
}{
"data": [...],
"pagination": {
"currentPage": 1,
"pageSize": 10,
"totalRecords": 100,
"totalPages": 10,
"hasNext": true,
"hasPrevious": false
}
}Characteristics:
- ? Microsecond latency
- ?? Process-local (not shared across instances)
- ?? Volatile (lost on restart)
Implementation:
public async Task<T?> GetOrSetAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? expiration = null)
{
// Check cache first
var cached = await _cache.GetStringAsync(key);
if (cached != null)
return JsonSerializer.Deserialize<T>(cached);
// Cache miss - execute factory
var data = await factory();
// Store in cache
await _cache.SetStringAsync(key, JsonSerializer.Serialize(data));
return data;
}Query with Caching:
public record GetApiDataQuery : IRequest<Result<List<SampleDto>>>, ICacheable
{
public string CacheKey => "api-data-all";
public TimeSpan? Expiration => TimeSpan.FromMinutes(5);
}Pipeline Behavior:
public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (request is ICacheable cacheable)
{
var cached = await _cache.GetAsync<TResponse>(cacheable.CacheKey);
if (cached != null) return cached;
var response = await next();
await _cache.SetAsync(cacheable.CacheKey, response, cacheable.Expiration);
return response;
}
return await next();
}
}? Performance - Reduces API calls and database queries
? Scalability - Handles more requests with same resources
? Cost Savings - Fewer third-party API calls
? Resilience - Serve cached data if external API is down
?? Limitation: In-memory cache is NOT shared across instances
Production Recommendations:
- Use Redis for distributed caching
- Use SQL Server distributed cache for persistent caching
- Implement cache warming for critical data
Redis Migration:
// Development - In-memory
services.AddDistributedMemoryCache();
// Production - Redis
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = configuration["Redis:ConnectionString"];
options.InstanceName = "CleanArchitecture.ApiTemplate:";
});var stopwatch = Stopwatch.StartNew();
var result = await _mediator.Send(query);
stopwatch.Stop();
_logger.LogInformation(
"API call completed in {Duration}ms - Cache: {CacheStatus}",
stopwatch.ElapsedMilliseconds,
cacheHit ? "HIT" : "MISS");// ? Good - Non-blocking
public async Task<IActionResult> GetData()
{
var result = await _apiService.GetDataAsync();
return Ok(result);
}
// ? Bad - Blocking
public IActionResult GetData()
{
var result = _apiService.GetData().Result; // Blocks thread!
return Ok(result);
}services.AddHttpClient<IApiIntegrationService, ApiIntegrationService>(client =>
{
client.BaseAddress = new Uri(configuration["ThirdPartyApi:BaseUrl"]);
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddTransientHttpErrorPolicy(policy =>
policy.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
.AddHttpMessageHandler<ApiKeyHandler>();Benefits:
- ? Reuses connections (prevents socket exhaustion)
- ? Respects DNS TTL
- ? Built-in resilience with Polly
- ? Easy to test and mock
[HttpGet]
public async Task<IActionResult> GetData()
{
try
{
_logger.LogInformation("User {UserId} requested data", User.Identity?.Name);
var result = await _mediator.Send(new GetApiDataQuery());
if (!result.Success)
{
_logger.LogWarning("Failed: {Error}", result.Error);
return BadRequest(new { error = result.Error });
}
return Ok(result.Data);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred");
return StatusCode(500, new { error = "An unexpected error occurred." });
}
}// ? Good - Structured
_logger.LogInformation(
"Processing {RequestId} for {Endpoint} by {UserId}",
requestId, endpoint, userId);
// ? Bad - String interpolation
_logger.LogInformation($"Processing {requestId} for {endpoint}");[ApiController]
[Route("api/v1/[controller]")]
[Authorize] // All endpoints require authentication
public class SampleController : ControllerBase
{
[HttpGet]
public IActionResult GetAll()
{
// Requires authentication
}
[HttpGet("admin")]
[Authorize(Policy = "AdminOnly")] // Requires Admin role
public IActionResult GetAdminData()
{
// Requires Admin role
}
[HttpGet("status")]
[AllowAnonymous] // Public endpoint
public IActionResult GetStatus()
{
// No authentication required
}
}For complete security documentation, see:
All API endpoints return DTOs mapped from domain entities using AutoMapper, following Clean Architecture principles.
Location: Core/Application/Common/Profiles/ApiDataMappingProfile.cs
Purpose: Transform domain entities to DTOs for API responses, keeping domain logic separate from presentation concerns.
Example Mapping:
public class ApiDataMappingProfile : Profile
{
public ApiDataMappingProfile()
{
// Domain Entity ? Response DTO
CreateMap<ApiDataItem, ApiDataItemDto>()
.ForMember(dest => dest.Status,
opt => opt.MapFrom(src => src.Status.ToString()))
.ForMember(dest => dest.IsFresh,
opt => opt.MapFrom(src => src.IsFresh(TimeSpan.FromHours(1))))
.ForMember(dest => dest.Age,
opt => opt.MapFrom(src => src.GetAge()))
// Extract metadata from domain entity
.ForMember(dest => dest.Category,
opt => opt.MapFrom(src => src.GetMetadata<string>("category")))
.ForMember(dest => dest.Price,
opt => opt.MapFrom(src => src.GetMetadata<decimal?>("price")))
.ForMember(dest => dest.Rating,
opt => opt.MapFrom(src => src.GetMetadata<double?>("rating")));
}
}Handler with Domain Entities:
public class GetApiDataWithMappingQueryHandler
: IRequestHandler<GetApiDataWithMappingQuery, Result<List<ApiDataItemDto>>>
{
private readonly IApiDataItemRepository _repository;
private readonly IMapper _autoMapper;
public async Task<Result<List<ApiDataItemDto>>> Handle(...)
{
// 1. Fetch domain entities from repository
var domainEntities = await _repository.GetItemsBySourceUrlAsync(
request.ApiUrl, cancellationToken);
// 2. Use domain entity business logic
var activeItems = domainEntities
.Where(item => item.Status == DataStatus.Active &&
!item.NeedsRefresh(TimeSpan.FromHours(1)))
.ToList();
// 3. Map domain entities to DTOs using AutoMapper
var responseDtos = _autoMapper.Map<List<ApiDataItemDto>>(activeItems);
return Result<List<ApiDataItemDto>>.Ok(responseDtos);
}
}Controller Usage:
[HttpGet("data-with-mapping")]
[ProducesResponseType(typeof(List<ApiDataItemDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetDataWithMapping(
[FromQuery] string apiUrl,
[FromQuery] bool useAutoMapper = true)
{
var query = new GetApiDataWithMappingQuery(apiUrl, useAutoMapper);
var result = await _mediator.Send(query);
if (!result.Success)
return BadRequest(new ProblemDetails { Detail = result.Error });
return Ok(result.Data); // Returns List<ApiDataItemDto>
}Complete DTO Example:
/// <summary>
/// DTO for ApiDataItem responses in controllers.
/// Used for API endpoints to return clean, structured data.
/// </summary>
public class ApiDataItemDto
{
/// <summary>
/// Internal unique identifier.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// External system identifier.
/// </summary>
public string ExternalId { get; set; } = string.Empty;
/// <summary>
/// Item name/title.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Item description.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Source API URL.
/// </summary>
public string SourceUrl { get; set; } = string.Empty;
/// <summary>
/// Last synchronization timestamp.
/// </summary>
public DateTime LastSyncedAt { get; set; }
/// <summary>
/// Data status (Active, Stale, Deleted).
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// Indicates if data is fresh (< 1 hour old).
/// </summary>
public bool IsFresh { get; set; }
/// <summary>
/// Age of the data since last sync.
/// </summary>
public TimeSpan Age { get; set; }
/// <summary>
/// Item category (from metadata).
/// </summary>
public string? Category { get; set; }
/// <summary>
/// Item price (from metadata).
/// </summary>
public decimal? Price { get; set; }
/// <summary>
/// Item rating (from metadata).
/// </summary>
public double? Rating { get; set; }
/// <summary>
/// Item tags (from metadata).
/// </summary>
public string[]? Tags { get; set; }
}Sample JSON Response:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"externalId": "12345",
"name": "Sample Product",
"description": "Product description",
"sourceUrl": "https://api.example.com/products/12345",
"lastSyncedAt": "2025-01-17T10:30:00Z",
"status": "Active",
"isFresh": true,
"age": "00:15:30",
"category": "Electronics",
"price": 299.99,
"rating": 4.7,
"tags": ["new", "featured"]
}? Domain Logic Protection - Domain entities never exposed directly to API consumers
? Type Safety - Compile-time validation with AutoMapper profiles
? Metadata Extraction - Flexible key-value metadata transformed to typed properties
? Computed Properties - Domain methods (IsFresh, GetAge) exposed as DTO properties
? Clean Separation - Presentation concerns separated from business logic
? Versioning Support - Multiple DTO versions without changing domain entities
1. Client Request
GET /api/v1/data-with-mapping?apiUrl=https://api.test.com&useAutoMapper=true
?
2. Controller receives request
?
3. Controller sends GetApiDataWithMappingQuery via MediatR
?
4. Handler queries repository for domain entities (ApiDataItem)
?
5. Handler applies domain logic:
- item.NeedsRefresh(threshold)
- item.Status == DataStatus.Active
- item.IsFresh(TimeSpan.FromHours(1))
?
6. Handler maps domain entities to DTOs via AutoMapper
- Transforms Status enum to string
- Calculates IsFresh and Age
- Extracts metadata to typed properties
?
7. Controller returns 200 OK with List<ApiDataItemDto>
?
8. Client receives JSON response with clean, structured data
AutoMapper Registration:
// In ApplicationServiceExtensions.cs
services.AddAutoMapper(applicationAssembly);Profile Validation Test:
[Fact]
public void ApiDataMappingProfile_Configuration_IsValid()
{
var config = new MapperConfiguration(cfg =>
cfg.AddProfile<ApiDataMappingProfile>());
// Validates all mappings at compile time
config.AssertConfigurationIsValid();
}?? Related Documentation:
- Hybrid Mapping Strategy - Complete mapping guide
- Application Layer Guide - CQRS and mapping patterns
- Domain Layer Guide - Domain entity implementation
[ApiController]
[Route("api/v1/[controller]")]
[Authorize]
[Produces("application/json")]
public class SampleController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<SampleController> _logger;
public SampleController(IMediator mediator, ILogger<SampleController> logger)
{
_mediator = mediator;
_logger = logger;
}
/// <summary>
/// Get all data from external API
/// </summary>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetAllData()
{
try
{
_logger.LogInformation("User {UserId} requested all data",
User.Identity?.Name ?? "Anonymous");
var result = await _mediator.Send(
new GetApiDataQuery<SampleDtoModel>("api-endpoint"));
if (result.Success)
{
_logger.LogInformation("Successfully retrieved data");
return Ok(result.Data);
}
_logger.LogWarning("Failed to retrieve data: {Error}", result.Error);
return BadRequest(new { error = result.Error });
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred");
return StatusCode(500, new { error = "An unexpected error occurred." });
}
}
/// <summary>
/// Get data by ID
/// </summary>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetDataById(string id)
{
if (string.IsNullOrWhiteSpace(id))
{
_logger.LogWarning("Invalid ID provided");
return BadRequest(new { error = "ID parameter is required." });
}
_logger.LogInformation("User {UserId} requested data for ID: {Id}",
User.Identity?.Name, id);
var result = await _mediator.Send(
new GetApiDataByIdQuery<SampleDtoModel>("api-endpoint", id));
if (result.Success)
return Ok(result.Data);
_logger.LogWarning("Failed to retrieve data for ID {Id}: {Error}",
id, result.Error);
return BadRequest(new { error = result.Error });
}
/// <summary>
/// Health check endpoint
/// </summary>
[HttpGet("status")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult GetStatus() =>
Ok(new { status = "Operational", timestamp = DateTime.UtcNow });
}// Query
public record GetApiDataQuery : IRequest<Result<List<SampleDto>>>, ICacheable
{
public string CacheKey => "api-data-all";
public TimeSpan? Expiration => TimeSpan.FromMinutes(5);
}
// Handler
public class GetApiDataQueryHandler
: IRequestHandler<GetApiDataQuery, Result<List<SampleDto>>>
{
private readonly IApiIntegrationService _apiService;
private readonly ILogger<GetApiDataQueryHandler> _logger;
public GetApiDataQueryHandler(
IApiIntegrationService apiService,
ILogger<GetApiDataQueryHandler> logger)
{
_apiService = apiService;
_logger = logger;
}
public async Task<Result<List<SampleDto>>> Handle(
GetApiDataQuery request,
CancellationToken cancellationToken)
{
_logger.LogInformation("Executing GetApiDataQuery");
return await _apiService.GetAllDataAsync<List<SampleDto>>("api/data");
}
}Controllers:
- ??
Presentation/Controllers/v1/SampleController.cs- Main API controller - ??
Presentation/Controllers/v1/AuthController.cs- Authentication endpoints
CQRS:
- ??
Core/Application/Features/GetData/Queries/GetApiDataQuery.cs - ??
Core/Application/Features/GetData/Queries/GetApiDataQueryHandler.cs
Caching:
- ??
Core/Application/Common/Behaviors/CachingBehavior.cs - ??
Core/Application/Common/Behaviors/ICacheable.cs - ??
Infrastructure/Caching/SampleCache.cs
Services:
- ??
Infrastructure/Services/ApiIntegrationService.cs - ??
Infrastructure/Handlers/ApiKeyHandler.cs
Configuration:
- ??
Presentation/Extensions/DependencyInjection/PresentationServiceExtensions.cs - ??
Presentation/Extensions/HttpPipeline/WebApplicationExtensions.cs
- API Testing Guide - Complete guide for testing APIs locally, with Docker, Swagger UI, and Postman
- Clean Architecture Guide - Architecture principles and patterns
- Security Guide - Complete security implementation
- Authentication Testing Guide - Detailed authentication and authorization testing
- Deployment Guide - Azure deployment instructions
Need Help?
- ?? Documentation: Start with the deployment guides above
- ?? Issues: GitHub Issues
- ?? Email: softevolutionsl@gmail.com
- ?? GitHub: @dariemcarlosdev
Last Updated: November 2025
Maintainer: Dariemcarlos
GitHub: CleanArchitecture.ApiTemplate