Middleware in .Net Core

Middleware in .Net Core

What is Middleware in .Net Core?

A middleware in a .net core application pipeline is a component that handles requests and responses by handling their logic, or putting custom logic and passing it to next component of the application that can be either another middleware or controller, service etc.

In other words, it acts as a middle person between requester and responder in a .net core application pipeline, that can handle upcoming request and apply custom logic on it.

It does 2 of the jobs

  • Decides whether the request can be passed to next component? In some contexts, it can act as a short circuit breaker to prevent requests to be sent further, like security concerns or fault in the pipeline etc.
  • Applies custom logic and then passes to next component. Like checking the request data and adding custom parameters or modifying request params according to the logic that can be used to next components.

By this way this middle component acts as a bridge between the requester and the responder.

Types of Middleware in .Net Core

There are mainly 2 types of middleware based on the work they do – first type is the non-terminal and other is terminal. But we can further classify on different parameters to make the classification easier to understand.

Classification of middleware by function in the pipeline

Based on what is does in the pipeline, middleware can be in terminal category or non- terminal category.

Non-Terminal Middleware

  • These components process the request/response and call the next middleware in the pipeline.
  • They typically perform operations both on the way in (request) and the way out (response).
  • Examples: Logging, Authentication, Authorization, Response Compression.

Terminal Middleware

  • These components process the request and do not call the next middleware, effectively ending or short-circuiting the pipeline.
  • They are responsible for generating and sending the final response.
  • Examples: Static Files (if a static file is found), MVC/Endpoint Routing (if an endpoint is matched), or a simple app.Run() call.

Classification of middleware by Implementation/Configuration in the pipeline

Middleware can also be classified by how it’s added or implemented in your application:

  • Built-in Middleware:
    • Components provided by the ASP.NET Core framework itself, added via extension methods on IApplicationBuilder.
    • Examples: UseRouting(), UseAuthentication(), UseStaticFiles(), UseExceptionHandler().
  • Custom Middleware:
    • Components you write yourself to implement application-specific logic.
    • These can be defined inline using app.Use() with a lambda or as a separate class.

Classification of middleware by Configuration Method in the pipeline

Another way to look at it is the specific methods used to configure them in the Program.cs file (or Startup.cs in older versions):

MethodType/BehaviourDescription
app.use()Non-Terminal (Chaining)Executes the delegate and calls the next middleware. Allows logic to run before and after the next component.
app.run()Terminal (Short-circuiting)Executes the delegate and terminates the pipeline; it does not have a next parameter. It must be the last component in a chain.
app.map()BranchingCreates a separate pipeline branch based on a matching request path (prefix match). The branch does not return to the main pipeline.
app.MapWhen()BranchingCreates a separate pipeline branch based on a predicate (Func<HttpContext, bool>). The branch does not return to the main pipeline.
app.useWhen()ConditionalExecutes a delegate only if a predicate is true, and the execution rejoins the main pipeline if the branch does not short-circuit.

Ordering of inbuilt Middleware in .Net Core

// 1: Exception Handling (Essential - Must be first to wrap all)
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// 2: Forwarded Headers (Situational - Must be early to fix context)
app.UseForwardedHeaders();

// 3: Https Redirection (Essential - Security enforcement)
app.UseHttpsRedirection();

// 4: WebSockets (Situational - Before routing/static files)
app.UseWebSockets();

// 5: Localization (Situational - Early for culture setting)
app.UseRequestLocalization();

// 6: Response Compression (Essential - Saves bandwidth)
app.UseResponseCompression();

// 7: Static Content (Essential - Early exit for files)
app.UseDefaultFiles();
app.UseStaticFiles();

// 8: Routing Setup (Essential - Determines endpoint)
app.UseRouting();

// 9: CORS (Essential - After routing to know the endpoint, before auth)
app.UseCors("MyCorsPolicy");

// 10: Rate Limiter (Situational - Traffic control before security check)
app.UseRateLimiter();

// 11: Authentication & Authorization (Essential - Security core)
app.UseAuthentication();
app.UseAuthorization();

// 12: Additional Session/Cookie Policies (Essential/Situational - After Identity)
app.UseCookiePolicy();
app.UseSession();

// 13: Response Caching (Essential - Wraps execution logic for caching)
app.UseResponseCaching();

// 14: Endpoint Execution (Essential - Terminal logic)
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
    endpoints.MapRazorPages();
    endpoints.MapHealthChecks("/health");
});

// 15: Swagger (Situational - Development/Documentation, outside core pipeline)
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

Which is the best place to keep custom middleware in this order?

This is a crucial thing as we may write our own middleware and then placing it at correct place is a must have requirement, otherwise this purpose will not complete.

Below is the expected order for middleware that is custom and written by developer as per the need.

1. Request Manipulation or Early Veto Middleware
This type of middleware should be placed between HTTPS Redirection (step 3) and Response Compression (step 6). It’s responsible for performing early checks such as API key validation, maintenance mode activation, or request header sanitization. Placing it here ensures your middleware executes before any static file serving, routing, or endpoint selection, allowing it to reject or modify requests as early as possible to save resources.

2. Correlation ID or Logging Context Middleware
Middleware that injects a Correlation ID or enriches the logging context should execute right after Request Localization (step 5) and before static file handling. This ensures every subsequent middleware, including UseStaticFiles(), routing, and controller execution, benefits from consistent request tracking and diagnostic information. It’s also helpful for distributed tracing systems and request logging across multiple services.

3. Global Header or Payload Modification Middleware
If your middleware modifies the response headers or body content (for example, appending a custom footer, adding security headers, or transforming JSON responses), place it after Session and Cookie Policy (step 12) but before Response Caching (step 13). This placement ensures that the response is fully formed (authorization and session are complete) but still interceptable before being cached or compressed.

4. Security or Role Enforcement Middleware
Middleware that performs custom security logic—like source IP blocking, tenant resolution, or business rule enforcement—should sit between the Rate Limiter (step 10) and Authentication (step 11). This allows your middleware to evaluate requests before authentication runs but still after routing and rate limiting have occurred. If your middleware depends on authenticated user information (e.g., user roles or claims), move it to after UseAuthentication().

Example of a Custom Middleware in .Net Core

I am creating a middleware that will handle unhandled exception in a .net core API, and will return user friendly error message by terminating the pipeline.

Let’s create a .Net core web API

Go to visual studio and create a Asp.Net Core Web API (in C#)

I am taking a dummy controller that will have a get method and will return some data.

using Microsoft.AspNetCore.Mvc;

namespace ExceptionHandlerMiddlewareExample
{
    [ApiController]
    [Route("[controller]")]
    public class TestController : ControllerBase
    {  
        public TestController()
        {
        }

        [HttpGet(Name = "GetTestData")]
        public List<string> Get()
        {
            List<string> names = null;
            // This will throw a NullReferenceException
            var length = names.Count;
            return names;
        }
    }
}

The Get() methid has string list names that will throw a NullReferenceException as the list is null

Without middleware, this will be unhandled and will give below response

500 : Internal Server Error
System.NullReferenceException: Object reference not set to an instance of an object.
   at ExceptionHandlerMiddlewareExample.TestController.Get() in D:\Avinash Joshi\My Projects\middleware-demo-exception-handler-dot-net-core\ExceptionHandlerMiddlewareExample\ExceptionHandlerMiddlewareExample\Controllers\TestController.cs:line 18
   at lambda_method2(Closure, Object, Object[])
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: text/plain
Host: localhost:7229
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9,en-IN;q=0.8
Referer: https://localhost:7229/swagger/index.html
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Microsoft Edge";v="141", "Not?A_Brand";v="8", "Chromium";v="141"
sec-ch-ua-mobile: ?0
sec-fetch-site: same-origin
sec-fetch-mode: cors
sec-fetch-dest: empty
priority: u=1, i

This is default behaviour of API.

Now let’s add a Middleware that can catch this and return a user friendly error that can be printed on screen.

using System.Net;

namespace ExceptionHandlerMiddlewareExamples
{
    public class ExceptionHandlingMiddleware
    {
        private readonly RequestDelegate _next;
        private const string GENERIC_ERROR_MESSAGE = "An unexpected error occurred. Please try again later.";
        public ExceptionHandlingMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            try
            {
                await _next(context); // Proceed to next middleware or endpoint
            }
            catch (Exception ex)
            {
                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                context.Response.ContentType = "application/json";

                var errorResponse = new
                {
                    IsSuccess = false,
                    Message = GENERIC_ERROR_MESSAGE,
                    ErrorDetails = ex
                };

                var json = Newtonsoft.Json.JsonConvert.SerializeObject(errorResponse);
                await context.Response.WriteAsync(json);
            }
        }
    }
}

It can be clearly seen that if the exception is there, this will handle this and return a user friendly error with details of exception that can be easily understood by API user as well as this will block further component call.

So to do that we need to place this in program.cs

using ExceptionHandlerMiddlewareExamples;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Custom Middleware for Exception Handling
app.UseMiddleware<ExceptionHandlingMiddleware>();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

You can see now this will handle the exception before request is going to next component and return user friendly error. So now the output will be

500 : Internal Server Error
{
  "IsSuccess": false,
  "Message": "An unexpected error occurred. Please try again later.",
  "ErrorDetails": {
    "ClassName": "System.NullReferenceException",
    "Message": "Object reference not set to an instance of an object.",
    "Data": null,
    "InnerException": null,
    "HelpURL": null,
    "StackTraceString": "   at ExceptionHandlerMiddlewareExample.TestController.Get() in D:\\Avinash Joshi\\My Projects\\middleware-demo-exception-handler-dot-net-core\\ExceptionHandlerMiddlewareExample\\ExceptionHandlerMiddlewareExample\\Controllers\\TestController.cs:line 18\r\n   at lambda_method2(Closure, Object, Object[])\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()\r\n--- End of stack trace from previous location ---\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()\r\n--- End of stack trace from previous location ---\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)\r\n   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)\r\n   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)\r\n   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)\r\n   at ExceptionHandlerMiddlewareExamples.ExceptionHandlingMiddleware.InvokeAsync(HttpContext context) in D:\\Avinash Joshi\\My Projects\\middleware-demo-exception-handler-dot-net-core\\ExceptionHandlerMiddlewareExample\\ExceptionHandlerMiddlewareExample\\Middlewares\\ExceptionHandlingMiddleware.cs:line 18",
    "RemoteStackTraceString": null,
    "RemoteStackIndex": 0,
    "ExceptionMethod": null,
    "HResult": -2147467261,
    "Source": "ExceptionHandlerMiddlewareExample",
    "WatsonBuckets": null
  }
}

Now it gives user friendly error object that can be read by requester with proper explanation.

So, this way we use the middleware and make our application robust to understand and operate on requests.

To view or download the source code click here

Thanks for reading

Read more here

Recommended Topics

Popular Tags

.net .Net Core .NETCore agentic ai agentic ai in .net AI coding ai in dot net ASPNETCore bot driven development C# chatgpt circuit breaker pattern coding in 2025 Coding in AI csharp CustomMiddleware DependencyInjection distributed systems dotnet dot net dotnet core resilience fault tolerance future of coding future of dot net high availability Intelligent coding tools microservices resilience Middleware Middleware in .Net Core MiddlewareOrder polly library polly v8 RequestPipeline resilience architecture resilience architecture in net core resilience architecture in net core the avinash joshi retry pattern The Avinash Joshi transient fault handling trending coding methods vibe coding