Middleware
Middleware Interview Preparation for .NET Core Developers (3+ Years Experience)
This guide covers key Middleware concepts, best practices, and common interview questions for experienced .NET Core developers. Focus on practical understanding and real-world application scenarios in ASP.NET Core.
Understanding Middleware in ASP.NET Core
Middleware in ASP.NET Core is software that's assembled into an application pipeline to handle requests and responses. Each component can:
- Choose whether to pass the request to the next component in the pipeline
- Perform work before and after the next component in the pipeline
- Short-circuit the request pipeline
Middleware Pipeline Architecture
The middleware pipeline is a series of components that handle HTTP requests and responses. Each middleware component:
- Receives an
HttpContextrepresenting the current request - Can modify the request/response
- Can decide to pass control to the next middleware or short-circuit
- Can perform work after the next middleware completes
Basic Pipeline Structure:
// Program.cs in .NET 6+
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Middleware registration order matters
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Interview Questions:
- Explain the middleware pipeline and how requests flow through it.
- What happens if middleware doesn't call
next()? - How does middleware ordering affect application behavior?
Creating Custom Middleware Components
Convention-based Middleware
The most common way to create middleware is using the convention-based approach with a class and extension method.
Example: Request Logging Middleware
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation($"Incoming request: {context.Request.Method} {context.Request.Path}");
await _next(context);
_logger.LogInformation($"Outgoing response: {context.Response.StatusCode}");
}
}
// Extension method for clean registration
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
// Usage in Program.cs
app.UseRequestLogging();
Factory-based Middleware
For middleware that needs services from the DI container, use the factory-based approach.
Example: Scoped Service Middleware
public class ScopedServiceMiddleware
{
private readonly RequestDelegate _next;
public ScopedServiceMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IScopedService scopedService)
{
// scopedService is resolved per request
await scopedService.DoWorkAsync();
await _next(context);
}
}
Inline Middleware (Lambda)
For simple middleware, you can use lambda expressions.
Example: Simple CORS Middleware
app.Use(async (context, next) =>
{
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
context.Response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
context.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (context.Request.Method == "OPTIONS")
{
context.Response.StatusCode = 200;
return;
}
await next();
});
Interview Questions:
- What are the different ways to create custom middleware?
- When would you use factory-based vs convention-based middleware?
- How do you inject services into middleware?
Middleware Pipeline Ordering and Execution
Critical Ordering Rules
Exception Handling: Must be first to catch all exceptions
app.UseExceptionHandler("/error"); // FirstHTTPS Redirection: Before other middleware that might redirect
app.UseHttpsRedirection(); // EarlyStatic Files: Before routing to avoid unnecessary processing
app.UseStaticFiles(); // Before routingRouting: Must come before endpoint execution
app.UseRouting(); // Before UseEndpoints/MapControllersAuthentication & Authorization: After routing, before endpoints
app.UseAuthentication(); app.UseAuthorization(); // After authenticationCORS: Before endpoints that need CORS
app.UseCors(); // Before MapControllersEndpoints: Must be last
app.MapControllers(); // Last
Execution Flow
Request Flow:
Request → Middleware1 → Middleware2 → ... → Endpoint → ... → Middleware2 → Middleware1 → Response
Short-circuiting Example:
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/api/health"))
{
await context.Response.WriteAsync("OK");
return; // Short-circuits the pipeline
}
await next();
});
Interview Questions:
- Why does middleware order matter?
- What middleware must come first in the pipeline?
- How do you handle middleware that should run for all requests vs specific routes?
Exception Handling Middleware
Global Exception Handling
Built-in Exception Handler Middleware:
// In Program.cs
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/error");
}
app.MapGet("/error", (HttpContext context) =>
{
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature?.Error;
// Log the exception
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError(exception, "Unhandled exception occurred");
return Results.Problem(
detail: app.Environment.IsDevelopment() ? exception?.ToString() : null,
title: "An error occurred"
);
});
Custom Exception Handling Middleware
Advanced Exception Middleware:
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
private readonly IHostEnvironment _environment;
public GlobalExceptionMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionMiddleware> logger,
IHostEnvironment environment)
{
_next = next;
_logger = logger;
_environment = environment;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (ValidationException ex)
{
await HandleValidationExceptionAsync(context, ex);
}
catch (UnauthorizedAccessException ex)
{
await HandleUnauthorizedExceptionAsync(context, ex);
}
catch (Exception ex)
{
await HandleGenericExceptionAsync(context, ex);
}
}
private async Task HandleValidationExceptionAsync(HttpContext context, ValidationException ex)
{
_logger.LogWarning(ex, "Validation error occurred");
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/json";
var errorResponse = new
{
type = "ValidationError",
errors = ex.Errors.Select(e => new { field = e.PropertyName, message = e.ErrorMessage })
};
await context.Response.WriteAsJsonAsync(errorResponse);
}
private async Task HandleUnauthorizedExceptionAsync(HttpContext context, UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized access attempt");
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new { message = "Unauthorized" });
}
private async Task HandleGenericExceptionAsync(HttpContext context, Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
var errorResponse = new
{
message = _environment.IsDevelopment() ? ex.Message : "An error occurred",
requestId = context.TraceIdentifier
};
await context.Response.WriteAsJsonAsync(errorResponse);
}
}
Exception Handling Best Practices
- Don't catch everything: Let critical exceptions bubble up in development
- Log appropriately: Use different log levels for different exception types
- Return consistent responses: Use ProblemDetails for API consistency
- Include correlation IDs: For request tracing
- Handle async exceptions: Ensure async operations are properly awaited
Interview Questions:
- How do you implement global exception handling in ASP.NET Core?
- What are the differences between UseExceptionHandler and custom exception middleware?
- How do you handle different types of exceptions differently?
Performance Monitoring Middleware
Response Time Monitoring
Performance Monitoring Middleware:
public class PerformanceMonitoringMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<PerformanceMonitoringMiddleware> _logger;
private readonly IConfiguration _configuration;
public PerformanceMonitoringMiddleware(
RequestDelegate next,
ILogger<PerformanceMonitoringMiddleware> logger,
IConfiguration configuration)
{
_next = next;
_logger = logger;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
var originalBodyStream = context.Response.Body;
using (var responseBody = new MemoryStream())
{
context.Response.Body = responseBody;
try
{
await _next(context);
stopwatch.Stop();
// Log performance metrics
await LogPerformanceMetricsAsync(context, stopwatch.ElapsedMilliseconds);
// Copy response back to original stream
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBodyStream);
}
catch (Exception)
{
stopwatch.Stop();
throw;
}
finally
{
context.Response.Body = originalBodyStream;
}
}
}
private async Task LogPerformanceMetricsAsync(HttpContext context, long elapsedMs)
{
var threshold = _configuration.GetValue<int>("Performance:ResponseTimeThresholdMs", 1000);
if (elapsedMs > threshold)
{
_logger.LogWarning(
"Slow request detected: {Method} {Path} took {ElapsedMs}ms",
context.Request.Method,
context.Request.Path,
elapsedMs);
}
// Could send metrics to monitoring service
// await _metricsService.RecordResponseTimeAsync(context.Request.Path, elapsedMs);
}
}
Memory Usage Monitoring
Memory Monitoring Middleware:
public class MemoryMonitoringMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<MemoryMonitoringMiddleware> _logger;
public async Task InvokeAsync(HttpContext context)
{
var beforeMemory = GC.GetTotalMemory(false);
await _next(context);
var afterMemory = GC.GetTotalMemory(false);
var memoryUsed = afterMemory - beforeMemory;
if (memoryUsed > 10 * 1024 * 1024) // 10MB threshold
{
_logger.LogWarning(
"High memory usage detected for {Path}: {MemoryUsed} bytes",
context.Request.Path,
memoryUsed);
}
}
}
Request Metrics Collection
Metrics Middleware:
public class MetricsMiddleware
{
private readonly RequestDelegate _next;
private readonly IMetricsService _metrics;
public async Task InvokeAsync(HttpContext context)
{
var startTime = DateTime.UtcNow;
try
{
await _next(context);
// Record successful request
await _metrics.RecordRequestAsync(
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
DateTime.UtcNow - startTime);
}
catch (Exception)
{
// Record failed request
await _metrics.RecordRequestAsync(
context.Request.Method,
context.Request.Path,
500,
DateTime.UtcNow - startTime);
throw;
}
}
}
Health Check Middleware
Custom Health Check Endpoint:
app.MapGet("/health", async (HttpContext context, IServiceProvider services) =>
{
var healthChecks = services.GetServices<IHealthCheck>();
var results = new Dictionary<string, HealthCheckResult>();
foreach (var check in healthChecks)
{
var result = await check.CheckHealthAsync(context.RequestAborted);
results[check.GetType().Name] = result;
}
var overallStatus = results.Values.All(r => r.Status == HealthStatus.Healthy)
? HealthStatus.Healthy
: HealthStatus.Unhealthy;
context.Response.StatusCode = overallStatus == HealthStatus.Healthy ? 200 : 503;
await context.Response.WriteAsJsonAsync(new
{
status = overallStatus.ToString(),
checks = results.ToDictionary(
kvp => kvp.Key,
kvp => new { status = kvp.Value.Status.ToString(), description = kvp.Value.Description })
});
});
Interview Questions:
- How do you monitor application performance using middleware?
- What metrics should you track in production applications?
- How do you handle middleware performance overhead?
Best Practices for Middleware Development
Middleware Design Principles
- Single Responsibility: Each middleware should do one thing well
- Async/Await: Always use async methods for I/O operations
- Dependency Injection: Inject services through constructor or InvokeAsync parameters
- Configuration: Make middleware configurable
- Logging: Implement proper logging for debugging and monitoring
- Error Handling: Don't let middleware exceptions crash the application
Common Patterns
Conditional Middleware Execution:
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
appBuilder => appBuilder.UseMiddleware<ApiLoggingMiddleware>());
Branching the Pipeline:
app.MapWhen(
context => context.Request.Path.StartsWithSegments("/api/v1"),
appBuilder => appBuilder.UseMiddleware<V1ApiMiddleware>());
Middleware with Options:
public class ConfigurableMiddleware
{
private readonly RequestDelegate _next;
private readonly MiddlewareOptions _options;
public ConfigurableMiddleware(RequestDelegate next, IOptions<MiddlewareOptions> options)
{
_next = next;
_options = options.Value;
}
public async Task InvokeAsync(HttpContext context)
{
if (_options.Enabled)
{
// Middleware logic
}
await _next(context);
}
}
public class MiddlewareOptions
{
public bool Enabled { get; set; } = true;
public int Threshold { get; set; } = 1000;
}
Testing Middleware
Unit Testing Middleware:
public class RequestLoggingMiddlewareTests
{
[Fact]
public async Task LogsRequestAndResponse()
{
// Arrange
var logger = new Mock<ILogger<RequestLoggingMiddleware>>();
var middleware = new RequestLoggingMiddleware(
next: (context) => Task.CompletedTask,
logger: logger.Object);
var context = new DefaultHttpContext();
context.Request.Method = "GET";
context.Request.Path = "/test";
context.Response.StatusCode = 200;
// Act
await middleware.InvokeAsync(context);
// Assert
logger.Verify(l => l.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => o.ToString().Contains("Incoming request")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once);
}
}
Integration Testing:
public class MiddlewareIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public MiddlewareIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task MiddlewareExecutesInCorrectOrder()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/test");
// Assert middleware behavior
}
}
Interview Questions:
- How do you make middleware configurable?
- What are some common middleware anti-patterns?
- How do you test custom middleware?
Common Interview Scenarios and Code Challenges
Scenario 1: Implementing API Rate Limiting
Question: How would you implement rate limiting middleware?
Answer:
public class RateLimitingMiddleware
{
private readonly RequestDelegate _next;
private readonly IRateLimitService _rateLimiter;
public async Task InvokeAsync(HttpContext context)
{
var clientId = GetClientId(context);
if (!await _rateLimiter.IsAllowedAsync(clientId))
{
context.Response.StatusCode = 429;
await context.Response.WriteAsync("Rate limit exceeded");
return;
}
await _next(context);
}
}
Scenario 2: Request/Response Transformation
Question: How do you create middleware that transforms request/response bodies?
Answer: Use streams to intercept and modify the request/response bodies.
Scenario 3: Distributed Tracing
Question: How do you implement request tracing across services?
Answer: Add correlation IDs and use distributed tracing libraries like OpenTelemetry.
Interview Questions
Basic Level (1-2 years exp)
Q1. What is middleware in ASP.NET Core?
Answer: Middleware is software that is assembled into the application pipeline to handle requests and responses. Each piece of middleware can examine, route, or modify the request and response as it passes through the pipeline.
Q2. How do you add middleware to the pipeline?
Answer:
// In Program.cs
var app = builder.Build();
app.UseRouting();
app.UseAuthorization();
// Add your middleware
app.UseMiddleware<MyCustomMiddleware>();
app.MapControllers();
Q3. What is the difference between Use and Run?
Answer:
Use: Adds middleware that can call the next middleware in the pipelineRun: Adds middleware that terminates the pipeline (doesn't call next)
Q4. How do you create simple middleware using a lambda?
Answer:
app.Use(async (context, next) =>
{
// Before next middleware
await next();
// After next middleware
});
Q5. What is RequestDelegate?
Answer: RequestDelegate is a delegate that represents the next middleware component in the pipeline. It's passed to middleware constructors and called in InvokeAsync methods.
Intermediate Level (2-3 Years Experience)
Q6. Explain middleware pipeline execution order.
Answer: Requests flow through middleware in the order they are registered. Each middleware can:
- Execute code before calling the next middleware
- Call the next middleware
- Execute code after the next middleware returns
- Short-circuit by not calling next
Q7. How do you handle exceptions in middleware?
Answer:
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Middleware error");
context.Response.StatusCode = 500;
await context.Response.WriteAsync("An error occurred");
}
}
Q8. What is the UseExceptionHandler middleware?
Answer: UseExceptionHandler catches unhandled exceptions and redirects to an error handling endpoint. It's typically used in production to provide user-friendly error pages.
Q9. How do you inject services into middleware?
Answer:
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly IService _service;
public MyMiddleware(RequestDelegate next, IService service)
{
_next = next;
_service = service;
}
public async Task InvokeAsync(HttpContext context)
{
// Use _service
await _next(context);
}
}
Q10. What are some built-in ASP.NET Core middleware?
Answer:
- UseHttpsRedirection
- UseStaticFiles
- UseRouting
- UseAuthentication
- UseAuthorization
- UseCors
- UseExceptionHandler
Advanced Level (3+ Years Experience)
Q11. How do you implement custom exception handling middleware?
Answer: See the GlobalExceptionMiddleware example above. Key points:
- Catch different exception types
- Return appropriate HTTP status codes
- Log errors appropriately
- Provide different responses for development vs production
Q12. Explain middleware ordering and why it matters.
Answer: Middleware order is critical because:
- Exception handling must come first
- Authentication must come before authorization
- Routing must come before endpoint execution
- Static files should be served before routing
Wrong order can lead to security vulnerabilities or broken functionality.
Q13. How do you implement performance monitoring in middleware?
Answer: See the PerformanceMonitoringMiddleware example. Key aspects:
- Measure response times
- Log slow requests
- Monitor memory usage
- Collect metrics for monitoring systems
Q14. What are the different ways to create middleware?
Answer:
- Convention-based: Class with InvokeAsync method and extension method
- Factory-based: For middleware needing scoped services
- Inline/lambda: For simple middleware
- IMiddleware interface: Less common, for testing
Q15. How do you handle middleware that should only run for certain routes?
Answer:
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
appBuilder => appBuilder.UseMiddleware<ApiMiddleware>());
Q16. What is middleware branching?
Answer:
app.MapWhen(
context => context.Request.Path.StartsWithSegments("/api/v1"),
appBuilder =>
{
appBuilder.UseMiddleware<V1Middleware>();
appBuilder.MapControllers();
});
Q17. How do you test custom middleware?
Answer:
- Unit test the InvokeAsync method with mocked dependencies
- Integration test with WebApplicationFactory
- Test middleware ordering and behavior
Q18. What are some performance considerations for middleware?
Answer:
- Avoid expensive operations in hot paths
- Use async/await properly
- Don't buffer entire request/response bodies unnecessarily
- Consider middleware order to minimize processing
Q19. How do you implement request/response transformation middleware?
Answer:
public async Task InvokeAsync(HttpContext context)
{
// Transform request
var originalRequestBody = context.Request.Body;
using (var newRequestBody = new MemoryStream())
{
// Modify request body
context.Request.Body = newRequestBody;
// Transform response
var originalResponseBody = context.Response.Body;
using (var newResponseBody = new MemoryStream())
{
context.Response.Body = newResponseBody;
await _next(context);
// Modify response body
newResponseBody.Seek(0, SeekOrigin.Begin);
await newResponseBody.CopyToAsync(originalResponseBody);
}
}
}
Q20. How do you implement health check middleware?
Answer:
app.MapGet("/health", async (IServiceProvider services) =>
{
var checks = services.GetServices<IHealthCheck>();
var results = await Task.WhenAll(checks.Select(c => c.CheckHealthAsync()));
return results.All(r => r.Status == HealthStatus.Healthy)
? Results.Ok("Healthy")
: Results.StatusCode(503);
});
Additional Interview Tips
Q21. What are some common middleware anti-patterns?
Answer:
- Doing too much: Middleware should have single responsibility
- Blocking calls: Don't use synchronous I/O in async middleware
- Not handling exceptions: Middleware exceptions can crash the app
- Wrong ordering: Can cause security or functionality issues
- Memory leaks: Not disposing streams or resources properly
Q22. How do you debug middleware issues?
Answer:
- Use logging extensively
- Check middleware order
- Use breakpoints in InvokeAsync
- Test middleware in isolation
- Check HttpContext state changes
Q23. What is the difference between middleware and filters?
Answer:
- Middleware: Part of the ASP.NET Core pipeline, handles all requests
- Filters: Part of MVC, only apply to controller actions
- Middleware can short-circuit, filters cannot
- Middleware runs for all requests, filters only for matched routes
Q24. How do you implement conditional middleware execution?
Answer:
app.UseWhen(
condition: context => context.Request.IsHttps,
configuration: appBuilder => appBuilder.UseMiddleware<HttpsOnlyMiddleware>());
Q25. What are some security considerations for custom middleware?
Answer:
- Validate input properly
- Don't log sensitive information
- Implement proper CORS handling
- Use secure headers
- Handle authentication/authorization correctly
Best Practices
- Keep it simple: Each middleware should do one thing
- Order matters: Register middleware in the correct order
- Handle errors: Implement proper exception handling
- Use async: Always use async/await for I/O operations
- Test thoroughly: Unit and integration tests are crucial
- Log appropriately: Include relevant context in logs
- Configure properly: Make middleware configurable
- Monitor performance: Track metrics in production
- Document behavior: Comment complex middleware logic
- Follow conventions: Use standard patterns for consistency
Common Pitfalls
- Wrong registration order: Can break authentication, routing, etc.
- Synchronous I/O in async methods: Causes thread starvation
- Not disposing resources: Memory leaks from streams
- Catching all exceptions: Hides important errors
- Blocking the pipeline: Forgetting to call next()
- Large request/response buffering: Memory issues
- Not handling edge cases: Like OPTIONS requests for CORS
- Hardcoded values: Instead of configuration
- Missing logging: Makes debugging impossible
- Performance overhead: Inefficient middleware implementation
Performance Tips
- Minimize allocations: Reuse objects where possible
- Use streams efficiently: Don't buffer unnecessarily
- Cache expensive operations: Like regex compilation
- Use async properly: Avoid async void
- Profile regularly: Use tools to identify bottlenecks
- Consider middleware order: Put cheap checks first
- Implement short-circuiting: For known fast paths
- Monitor memory usage: Watch for leaks
- Use pooled objects: For frequently allocated objects
- Load test middleware: Ensure it scales
Key Takeaways for Interviews
Demonstrate Pipeline Understanding: Explain how middleware fits into the request/response flow.
Show Code Examples: Be ready to write custom middleware on the spot.
Discuss Ordering: Know why order matters and common ordering rules.
Cover Error Handling: Explain different approaches to exception handling.
Address Performance: Discuss monitoring and optimization techniques.
Know Built-in Middleware: Understand what UseRouting, UseAuthentication, etc. do.
Remember to discuss real-world scenarios and trade-offs when implementing middleware in production applications.
Comments
Post a Comment