Class OpenApiCachingMiddleware
Namespace: Momentum.ServiceDefaults.Api.OpenApi
Assembly: Momentum.ServiceDefaults.Api.dll
Middleware that caches OpenAPI documentation to improve performance.
public class OpenApiCachingMiddleware
Inheritance
object ← OpenApiCachingMiddleware
Inherited Members
object.GetType(), object.MemberwiseClone(), object.ToString(), object.Equals(object?), object.Equals(object?, object?), object.ReferenceEquals(object?, object?), object.GetHashCode()
Remarks
OpenAPI Caching Middleware
Middleware Overview
This middleware:
- Intercepts OpenAPI Requests: Identifies and handles OpenAPI document requests
- Loads XML Documentation: Processes XML docs on first request for enrichment
- Caches Generated Documents: Stores OpenAPI documents to disk for performance
- Serves Cached Responses: Returns cached documents with proper ETag headers
- Supports Multiple Formats: Handles both JSON and YAML OpenAPI formats
- Handles Conditional Requests: Responds with 304 Not Modified when appropriate
The cache is stored in the system temp directory and persists across application restarts.
Request Processing Flow
OpenAPI Request Detection
private static bool IsOpenApiRequest(HttpRequest request)
{
if (!request.Path.HasValue || request.Path.Value.Length > MaxOpenApiRequestPathLenght)
return false;
return request.Path.Value.Contains("/openapi", StringComparison.OrdinalIgnoreCase);
}
Cache Key Generation
private static string GetCacheKey(HttpRequest request) =>
Convert.ToBase64String(Encoding.UTF8.GetBytes(request.Path));
Content Type Detection
private static string GetContentType(HttpContext httpContext)
{
var path = httpContext.Request.Path.ToString();
if (path.Contains(".yaml", StringComparison.OrdinalIgnoreCase) ||
path.Contains(".yml", StringComparison.OrdinalIgnoreCase))
{
return "application/yaml";
}
return MediaTypeNames.Application.Json;
}
Caching Strategy
Cache Directory Structure
private static string GetCacheDirectory()
{
var assemblyDir = Assembly.GetEntryAssembly()?.GetName().Name?.Replace('.', '_') ??
Guid.NewGuid().ToString("N");
return Path.Combine(Path.GetTempPath(), assemblyDir, "openapi-cache");
}
Cache File Management
private static string GetCacheFilePath(string cacheKey) =>
Path.Combine(CacheDirectory, $"{cacheKey}.txt");
Initialization Tracking
private readonly Dictionary<string, bool> _cacheInitialized = [];
XML Documentation Integration
Documentation Loading
await xmlDocService.LoadDocumentationAsync(GetXmlDocLocation());
Documentation Path Resolution
private static string GetXmlDocLocation()
{
var assembly = Assembly.GetEntryAssembly();
var xmlFileName = Path.GetFileNameWithoutExtension(assembly?.Location) + ".xml";
return Path.Combine(Path.GetDirectoryName(assembly?.Location) ?? "", xmlFileName);
}
Cache Cleanup
finally
{
xmlDocService.ClearCache();
_fileLock.Release();
}
HTTP Caching Implementation
ETag Generation
private static string GenerateETag(FileInfo fileInfo)
{
var combined = $"{fileInfo.Length}_{fileInfo.LastWriteTimeUtc.Ticks}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(combined));
return $"\"{Convert.ToBase64String(hash)[..16]}\"";
}
Cache Headers
private static void SetCacheHeaders(HttpContext httpContext, string filePath)
{
var fileInfo = new FileInfo(filePath);
var response = httpContext.Response;
response.ContentType ??= GetContentType(httpContext);
response.Headers.ETag = GenerateETag(fileInfo);
response.Headers.LastModified = fileInfo.LastWriteTimeUtc.ToString("R");
}
Conditional Request Handling
if (context.Request.Headers.IfNoneMatch.Contains(eTag.ToString()))
{
context.Response.StatusCode = 304;
return true;
}
Thread Safety and Concurrency
File Locking
private readonly SemaphoreSlim _fileLock = new(1, 1);
await _fileLock.WaitAsync();
try
{
// Cache generation logic
}
finally
{
_fileLock.Release();
}
Concurrent Request Handling
The middleware handles concurrent requests safely:
- Single Generation: Only one thread generates cache per key
- Safe Reading: Multiple threads can read cached files
- Initialization Tracking: Prevents duplicate initialization
Error Handling and Fallback
Exception Handling
try
{
await HandleOpenApiRequestAsync(context);
}
catch (Exception ex)
{
logger.LogError(ex, "Error handling OpenAPI request");
await next(context);
}
Graceful Degradation
- Cache Miss: Falls back to regular OpenAPI generation
- File Errors: Continues with non-cached response
- XML Doc Failures: Proceeds without documentation enrichment
Performance Optimizations
Stream Handling
await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read,
FileShare.Read, BufferSize, useAsync: true);
await fileStream.CopyToAsync(context.Response.Body);
Memory Management
using var memoryStream = new MemoryStream();
context.Response.Body = memoryStream;
Async Operations
All I/O operations use async patterns:
- Async File Operations: Non-blocking file reading/writing
- Async XML Processing: Non-blocking documentation loading
- Async Stream Copying: Efficient response streaming
Configuration Options
Buffer Size
private const int BufferSize = 8192;
Path Length Limits
private const int MaxOpenApiRequestPathLenght = 500;
Cache Persistence
Cache files persist across application restarts, improving:
- Cold Start Performance: Faster initial OpenAPI responses
- Development Experience: Consistent response times
- Resource Utilization: Reduced XML processing overhead
Constructors
OpenApiCachingMiddleware(ILogger<OpenApiCachingMiddleware>, IXmlDocumentationService, RequestDelegate)
Middleware that caches OpenAPI documentation to improve performance.
public OpenApiCachingMiddleware(ILogger<OpenApiCachingMiddleware> logger, IXmlDocumentationService xmlDocService, RequestDelegate next)
Parameters
logger
ILogger<OpenApiCachingMiddleware>
xmlDocService
IXmlDocumentationService
next
RequestDelegate
Remarks
OpenAPI Caching Middleware
Middleware Overview
This middleware:
- Intercepts OpenAPI Requests: Identifies and handles OpenAPI document requests
- Loads XML Documentation: Processes XML docs on first request for enrichment
- Caches Generated Documents: Stores OpenAPI documents to disk for performance
- Serves Cached Responses: Returns cached documents with proper ETag headers
- Supports Multiple Formats: Handles both JSON and YAML OpenAPI formats
- Handles Conditional Requests: Responds with 304 Not Modified when appropriate
The cache is stored in the system temp directory and persists across application restarts.
Request Processing Flow
OpenAPI Request Detection
private static bool IsOpenApiRequest(HttpRequest request)
{
if (!request.Path.HasValue || request.Path.Value.Length > MaxOpenApiRequestPathLenght)
return false;
return request.Path.Value.Contains("/openapi", StringComparison.OrdinalIgnoreCase);
}
Cache Key Generation
private static string GetCacheKey(HttpRequest request) =>
Convert.ToBase64String(Encoding.UTF8.GetBytes(request.Path));
Content Type Detection
private static string GetContentType(HttpContext httpContext)
{
var path = httpContext.Request.Path.ToString();
if (path.Contains(".yaml", StringComparison.OrdinalIgnoreCase) ||
path.Contains(".yml", StringComparison.OrdinalIgnoreCase))
{
return "application/yaml";
}
return MediaTypeNames.Application.Json;
}
Caching Strategy
Cache Directory Structure
private static string GetCacheDirectory()
{
var assemblyDir = Assembly.GetEntryAssembly()?.GetName().Name?.Replace('.', '_') ??
Guid.NewGuid().ToString("N");
return Path.Combine(Path.GetTempPath(), assemblyDir, "openapi-cache");
}
Cache File Management
private static string GetCacheFilePath(string cacheKey) =>
Path.Combine(CacheDirectory, $"{cacheKey}.txt");
Initialization Tracking
private readonly Dictionary<string, bool> _cacheInitialized = [];
XML Documentation Integration
Documentation Loading
await xmlDocService.LoadDocumentationAsync(GetXmlDocLocation());
Documentation Path Resolution
private static string GetXmlDocLocation()
{
var assembly = Assembly.GetEntryAssembly();
var xmlFileName = Path.GetFileNameWithoutExtension(assembly?.Location) + ".xml";
return Path.Combine(Path.GetDirectoryName(assembly?.Location) ?? "", xmlFileName);
}
Cache Cleanup
finally
{
xmlDocService.ClearCache();
_fileLock.Release();
}
HTTP Caching Implementation
ETag Generation
private static string GenerateETag(FileInfo fileInfo)
{
var combined = $"{fileInfo.Length}_{fileInfo.LastWriteTimeUtc.Ticks}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(combined));
return $"\"{Convert.ToBase64String(hash)[..16]}\"";
}
Cache Headers
private static void SetCacheHeaders(HttpContext httpContext, string filePath)
{
var fileInfo = new FileInfo(filePath);
var response = httpContext.Response;
response.ContentType ??= GetContentType(httpContext);
response.Headers.ETag = GenerateETag(fileInfo);
response.Headers.LastModified = fileInfo.LastWriteTimeUtc.ToString("R");
}
Conditional Request Handling
if (context.Request.Headers.IfNoneMatch.Contains(eTag.ToString()))
{
context.Response.StatusCode = 304;
return true;
}
Thread Safety and Concurrency
File Locking
private readonly SemaphoreSlim _fileLock = new(1, 1);
await _fileLock.WaitAsync();
try
{
// Cache generation logic
}
finally
{
_fileLock.Release();
}
Concurrent Request Handling
The middleware handles concurrent requests safely:
- Single Generation: Only one thread generates cache per key
- Safe Reading: Multiple threads can read cached files
- Initialization Tracking: Prevents duplicate initialization
Error Handling and Fallback
Exception Handling
try
{
await HandleOpenApiRequestAsync(context);
}
catch (Exception ex)
{
logger.LogError(ex, "Error handling OpenAPI request");
await next(context);
}
Graceful Degradation
- Cache Miss: Falls back to regular OpenAPI generation
- File Errors: Continues with non-cached response
- XML Doc Failures: Proceeds without documentation enrichment
Performance Optimizations
Stream Handling
await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read,
FileShare.Read, BufferSize, useAsync: true);
await fileStream.CopyToAsync(context.Response.Body);
Memory Management
using var memoryStream = new MemoryStream();
context.Response.Body = memoryStream;
Async Operations
All I/O operations use async patterns:
- Async File Operations: Non-blocking file reading/writing
- Async XML Processing: Non-blocking documentation loading
- Async Stream Copying: Efficient response streaming
Configuration Options
Buffer Size
private const int BufferSize = 8192;
Path Length Limits
private const int MaxOpenApiRequestPathLenght = 500;
Cache Persistence
Cache files persist across application restarts, improving:
- Cold Start Performance: Faster initial OpenAPI responses
- Development Experience: Consistent response times
- Resource Utilization: Reduced XML processing overhead
Methods
InvokeAsync(HttpContext)
public Task InvokeAsync(HttpContext context)
Parameters
context
HttpContext