Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Create Middleware Extensions Correctly?

Learn how to correctly create middleware extensions and configure options like base address using Refit in external assemblies.
Developer inserting Refit into a glowing modular middleware pipeline representing correct middleware extensions setup in .NET with HttpClient and service configuration Developer inserting Refit into a glowing modular middleware pipeline representing correct middleware extensions setup in .NET with HttpClient and service configuration
  • ✅ 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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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);
    }
}
  • RequestDelegate represents the remaining pipeline.
  • InvokeAsync is 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

  1. Make a new Class Library Project—a good way to set it up is:
/YourSolution
  /SharedMiddleware
    /Middleware
        - LoggingMiddleware.cs
    /Extensions
        - LoggingMiddlewareExtensions.cs
  /WebApp1
  /WebApp2
  1. 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" />
  1. 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.

  1. 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:

  1. 🧩 Missing Service Registrations
    Middleware with AutoDI must have services registered (e.g., ILogger, IMyRepo). Otherwise, you'll face InvalidOperationException.

  2. 🔀 Improper Order
    Calling app.UseAuthorization() before app.UseAuthentication() will silently fail—auth will always fail even if tokens are valid.

  3. 🔄 Circular Dependencies
    Middleware and services must avoid circular calls that deadlock or stack overflow. Always trace service relationships.

  4. ⚠️ Scoped-to-Singleton Issues
    Never inject scoped services like DbContext into singleton middlewares. It leads to runtime scope errors.

  5. 🛠 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 HttpContext or 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 HttpClient base 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:


Citations

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading