- ✅ Middleware extension order impacts security and performance.
- 🔧 Refit configuration avoids hardcoded URLs using dependency injection.
- 🧱 Middleware and services must be correctly registered to prevent runtime failures.
- 💡 HttpClient base address setup must consider dynamic environments and DI patterns.
- 🧪 Middleware logic should be decoupled from orchestration to be unit testable.
Middleware extensions are key to building clean, modular, and scalable web apps in .NET. They put logic like authentication, logging, response shaping, or header injection into components you can plug in. But if you don't structure and register them well, especially across projects or with tools like Refit and HttpClient, you can easily get confused by bugs or messy code. This is how to build middleware extensions correctly.
Middleware Extensions and Modern .NET Architectures
In ASP.NET Core apps today, middleware is a strong idea. It sets up how HTTP requests come in and how responses go out. Middleware runs one after another through a request pipeline that goes into the app when it starts. Each middleware part handles the request. It can either stop the request or send it to the next part. This makes it easy to add things like logging, performance tracking, authentication, response caching, and URL rewriting in a way that keeps parts separate.
Middleware extensions are an easier way to use middleware. Instead of filling your Program.cs or Startup.cs with setup code, you put the logic into extension methods for IApplicationBuilder. This makes your code easier to read, reuse, and keep separate. And this is important for today's microservices or modular monoliths.
And also, you can put middleware extensions in shared code libraries. This makes them even more useful for teams working on many services.
Real-World Example: Why Middleware Abstraction Matters
Think about when many services in your company use the same external API. All requests going out should meet security rules. This means they need headers like X-Auth-Token, trace info, or custom IDs. Copying and pasting this logic into every app wastes time and can lead to mistakes. But with a shared middleware extension, you handle this once. Then you apply it everywhere the same way with a simple call like:
app.UseOutgoingRequestHeaderMiddleware();
This abstraction:
- Ensures behavior consistency across projects.
- Simplifies onboarding for new services or developers.
- Reduces bugs due to missed or inconsistent implementation.
- Makes upgrades and bug fixes faster (change logic in one place).
From watching data to limiting requests to logging that follows GDPR, smart middleware extensions help keep your system strong with little added complexity.
Anatomy of a Middleware Extension
Making middleware extensions usually involves two parts. First, you set up the logic in a custom middleware class. Then, you make it available through an extension method for others to use.
Step 1: Create the Middleware Class
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;
public MyCustomMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Custom logic here, such as logging, telemetry, header injection
await _next(context);
}
}
RequestDelegaterepresents the remaining pipeline.InvokeAsyncis the required signature for middleware.- Additional services can be injected into the constructor via Dependency Injection.
Step 2: Define the Extension Method
public static class MyCustomMiddlewareExtensions
{
public static IApplicationBuilder UseMyCustomMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyCustomMiddleware>();
}
}
This way of doing things makes the code clear:
app.UseRouting();
app.UseMyCustomMiddleware();
app.UseEndpoints(...);
📌 Naming rules are important. Use prefixes like UseSecurityHeaders() or UseExceptionHandling(). This makes it clear what they do when you read the app setup.
Creating Extensions in an External Assembly
To use middleware extensions again in different projects and apps, you need to put them into a shared or outside assembly. This makes your code more modular. It also helps you avoid repeating yourself across your code.
Steps to Externalize Middleware
- Make a new Class Library Project—a good way to set it up is:
/YourSolution
/SharedMiddleware
/Middleware
- LoggingMiddleware.cs
/Extensions
- LoggingMiddlewareExtensions.cs
/WebApp1
/WebApp2
- Add references to main parts it needs:
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Builder" Version="2.2.0" />
- Organize Namespaces: Use clear namespaces like:
namespace Devsolus.Middleware.Security
This helps you find them easily when adding them to your code or looking through it.
- Publish as NuGet: For a simpler way to add them, you can publish the shared middleware as a NuGet package. Then your services can use it.
🔁 Now, update any app to install this shared part. Then set it up using:
app.UseLoggingMiddleware();
Registering Services vs. Using Middleware
It is important to know the difference between middleware and services. This helps you design .NET Core apps well.
Middleware = Request/Response Pipeline
Middleware is added to the app using IApplicationBuilder. The app processes it for each HTTP request. For example:
app.UseMyCustomMiddleware();
Services = Dependency Management
You add services using IServiceCollection. These are parts you can reuse across the app, like for logging, caching, or getting to data:
services.AddSingleton<ILoggerService, LoggerService>();
To correctly set up middleware that needs services, you must register the types it needs. Do this before using UseMiddleware<>.
✅ Example with DI-injected dependencies:
services.AddScoped<IMetricsTracker, MetricsTrackerImpl>();
app.UseMiddleware<MyMetricsMiddleware>();
⛔ If you do not register them, you will get errors when the app runs. For example, Cannot resolve IMyDependency from service provider.
🔄 Keeping these things separate stops startup code from being weak and hard to fix.
Configuring HttpClient Base Address with Refit in Extensions
When you use Refit, which is a type-safe REST API client for .NET, you need to set up HttpClient.BaseAddress the right way. This helps make sure it works safely and adapts to different environments. If you hardcode base URLs, it can cause bugs, make things less flexible, and break the difference between local and production setups.
Correct Approach Using IConfiguration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddExternalApiClient(this IServiceCollection services, IConfiguration configuration)
{
var baseUrl = configuration["ExternalApi:BaseUrl"];
services.AddRefitClient<IExternalApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri(baseUrl));
return services;
}
}
🧠 Benefits of doing this:
- Supports environment variable overrides.
- Encourages 12-factor-compliant designs.
- Easy updates and centralized values.
- Scales across environments like dev, staging, prod.
📌 Refit uses IHttpClientFactory. This means you can combine handlers, retry rules, and loggers in a neat way. This makes the code easier to keep up.
Common Pitfalls in Middleware Extension Setup
Here’s what often breaks even experienced .NET developers:
-
🧩 Missing Service Registrations
Middleware with AutoDI must have services registered (e.g.,ILogger,IMyRepo). Otherwise, you'll faceInvalidOperationException. -
🔀 Improper Order
Callingapp.UseAuthorization()beforeapp.UseAuthentication()will silently fail—auth will always fail even if tokens are valid. -
🔄 Circular Dependencies
Middleware and services must avoid circular calls that deadlock or stack overflow. Always trace service relationships. -
⚠️ Scoped-to-Singleton Issues
Never inject scoped services likeDbContextinto singleton middlewares. It leads to runtime scope errors. -
🛠 Internal Visibility
Make extension methods public so they can be used across consuming projects.
If you avoid these issues, you will save many hours of fixing problems and looking through error logs.
Practical Example: Custom Header Injection Middleware with Refit
Sometimes, services need to add audit or trace headers to HTTP calls going out to other services. Middleware alone cannot change requests made with HttpClient. But we can use DelegatingHandler.
1. Create a DelegatingHandler
public class CustomHeaderHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("X-Custom-Header", "Devsolus123");
return await base.SendAsync(request, cancellationToken);
}
}
2. Wire Handler into Refit Client via Extension
public static class ExternalApiExtensions
{
public static IServiceCollection AddExternalApiWithHeaders(this IServiceCollection services, IConfiguration config)
{
var baseAddress = config["ExternalApi:BaseUrl"];
services.AddTransient<CustomHeaderHandler>(); // lifecycle matters
services.AddRefitClient<IExternalApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri(baseAddress))
.AddHttpMessageHandler<CustomHeaderHandler>();
return services;
}
}
🧪 Benefit: This setup works well with many APIs. It adds headers without needing global handlers or repeating code.
Testing Middleware Extensions
You can and should test middleware that is designed well. Use isolated integration tests or mocked services with TestServer.
Sample Middleware Test Setup
var server = new TestServer(new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddSingleton<IMyDependency, MyDependency>();
})
.Configure(app =>
{
app.UseRouting();
app.UseMyCustomMiddleware();
}));
var client = server.CreateClient();
🎯 Best testing practices:
- Move processing logic into a service.
- Keep middleware focused on pipeline control.
- Mock
HttpContextor verify key changes like headers, response code, etc.
Best Practices for Reusable Middleware
✅ Lean and Focused: One responsibility per middleware (e.g. logging vs. compression).
✅ Dependency Injection First: Avoid static classes and tightly coupled dependencies.
✅ Stateless Implementations: Middleware is reused per request—mutable state causes race conditions.
✅ Naming Conventions: Clearly articulate purpose (UseRequestTiming()).
✅ Testability: All complex logic should exist in a service, not directly in middleware.
✅ Metrics/Tracing: Perfect spot for injecting telemetry, tracing spans, or logging HTTP metrics.
These practices help you build systems that last, grow, and are easy to read.
When to Avoid Middleware Extensions
Despite their usefulness, middleware should not be the default pattern for every concern.
Avoid creating middleware when:
- 🏷 You're trying to modify controller-specific behavior—use filters.
- 🔁 You need access to action parameters—middleware happens too early.
- ☑️ The logic only needs to run once or at startup—use hosted services instead.
- 📦 Cleaner implementation can be done via endpoint behaviors or service configuration.
Knowing where middleware sits in the request pipeline helps you make better design choices.
Modern Middleware Design for Future-proof Projects
Good middleware extension design is not just a coding trick. It is a main part of building .NET systems that can grow and are easy to maintain.
A modern approach includes:
- Extension methods that read from
IConfiguration. - Strongly typed configuration sections.
- Middleware that’s safe, stateless, and testable.
- Proper service registration for extensibility.
- Smart
HttpClientbase address configuration with Refit.
Here is a checklist to see if your middleware is ready for production:
✔ Extension code is organized and published to shared libraries
✔ Refit configuration gets base addresses as needed
✔ HttpClient and handlers work through IHttpClientFactory
✔ Services used in middleware are registered and scoped correctly
✔ Middleware runs in the correct order without hidden side effects
Your codebase becomes more composable, more testable, and ultimately more reliable.
For more deep dives, check out our guides on:
- Refit client setup
- Clean HTTP client design with IHttpClientFactory
- Managing Dependency Injection lifetimes in .NET
Citations
- Microsoft Docs. (n.d.). Middleware in ASP.NET Core. Retrieved from https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware
- Refit GitHub Repo. (n.d.). Refit: The automatic type-safe REST library for .NET Standard. Retrieved from https://github.com/reactiveui/refit
- Microsoft Docs. (n.d.). IHttpClientFactory typed clients. Retrieved from https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests