Mastering Asp.Net Core: Unraveling the Concepts of Middleware
Understand Middleware and Custom Middleware
What is ASP.Net core Middleware? It is a component or piece of code executes in the request/response pipeline which decides whether to pass the request to the next component in the pipeline or not. Middleware does some kind of work before deciding to pass the request to next component in the pipeline.
What is HTTP Request Pipeline? Before understanding ASP.NET Core Middleware components, let us first understand what is HTTP Request Pipeline and how does it work. Please have a look at the following image for a better understanding of the HTTP Request Pipeline. As you can see in the below image, on the left-hand side, we have the client i.e. a browser and on the right-hand side, we have the server where our ASP.NET Core Web API application is hosted. Further, the Web API Application has three controllers. So, when the client sends a request to the server, we generally believe that it is the controller action method that is going to serve the request and then we get the response.
But in reality, before hitting the controller action method, the request has to pass through a pipeline. Once the pipeline is completed, then only it navigates the request to the corresponding controller action method as shown in the below image.
What is HTTP Request Pipeline in ASP.NET Core Web API Application? The Request Pipeline in ASP.NET Core Web API Application can have multiple middlewares as shown in the below image. If you are confusing what is Middleware, let us assume a middleware is a piece of code with some logic. Whenever a request comes from a client to the server, then the request comes to the first middleware which is registered in the request pipeline. In our case it is Middleware1. The code or logic which is there in Middleware1 will be executed and then if it will call the next method, then the request goes to the next middleware which is registered in the request processing pipeline i.e. Middlware2. The code or logic which is there in Middleware2 will be executed and if it calls the next method, then it navigates to the request to the next middleware i.e. Middleware3. Let us assume, in Middleware3, we don’t have the next method. So, the code or logic which is there in Middleware3 will be executed and as there is no next method call, so the request will come back to the previous middleware i.e. Middleware2. And if there is some code after the next method in Middleware2, then those codes will be executed and once the code executed the request again comes back to the previous middleware i.e. Middlwware1. Similarly, if there is some code after the next method in Middleware1, then those codes will be executed and once the code gets executed the final response sends to the client who initially made the request.
This is how the Request Pipeline works in ASP.Net Core Web API Application. These Middleware are nothing but a piece of code or we can say these are some functionalities that we want to insert in our ASP.Net Core Web API Application.
Middleware in ASP.Net Core Web API: Middleware is a piece of code that is used in the HTTP Request Pipeline. An ASP.Net Core Web API Application can have n numbers of middleware. So, depending upon the requirement, we can configure n numbers of middleware in the application request processing pipeline.
The order of middleware matters a lot in the execution. That means in the order they are configured into the request processing pipeline; in the same order, they are going to be executed when a request comes. Each middleware in the ASP.Net Core Web API Application performs the following tasks.
Chooses whether to pass the HTTP Request to the next component in the pipeline. This can be achieved by calling the next() method within the middleware.
Can perform work before and after the next component in the pipeline.
The most important point that you need to keep in mind is, in ASP.Net Core a given Middleware component should only have a specific purpose i.e. single responsibility.
The order that middleware components are added in the Program.cs
file defines the order in which the middleware components are invoked on requests and the reverse order for the response. The order is critical for security, performance, and functionality.
The following highlighted code in Program.cs
adds security-related middleware components in the typical recommended order:
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMiddleware.Data;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();
app.UseRouting();
// app.UseRateLimiter();
// app.UseRequestLocalization();
// app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();
app.MapRazorPages();
app.MapDefaultControllerRoute();
app.Run();
In the preceding code:
Middleware that is not added when creating a new web app with individual users accounts is commented out.
Not every middleware appears in this exact order, but many do. For example:
UseCors
,UseAuthentication
, andUseAuthorization
must appear in the order shown.UseCors
currently must appear beforeUseResponseCaching
.UseRequestLocalization
must appear before any middleware that might check the request culture, for example,app.UseStaticFiles()
.UseRateLimiter must be called after
UseRouting
when rate limiting endpoint specific APIs are used. For example, if the[EnableRateLimiting]
attribute is used,UseRateLimiter
must be called afterUseRouting
. When calling only global limiters,UseRateLimiter
can be called beforeUseRouting
.
In some scenarios, middleware has different ordering. For example, caching and compression ordering is scenario specific, and there are multiple valid orderings. For example:
app.UseResponseCaching();
app.UseResponseCompression();
With the preceding code, CPU usage could be reduced by caching the compressed response, but you might end up caching multiple representations of a resource using different compression algorithms such as Gzip or Brotli.
The following ordering combines static files to allow caching compressed static files:
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();
The following Program.cs
code adds middleware components for common app scenarios:
Exception/error handling
When the app runs in the Development environment:
Developer Exception Page Middleware (UseDeveloperExceptionPage) reports app runtime errors.
Database Error Page Middleware (UseDatabaseErrorPage) reports database runtime errors.
When the app runs in the Production environment:
Exception Handler Middleware (UseExceptionHandler) catches exceptions thrown in the following middlewares.
HTTP Strict Transport Security Protocol (HSTS) Middleware (UseHsts) adds the
Strict-Transport-Security
header.
HTTPS Redirection Middleware (UseHttpsRedirection) redirects HTTP requests to HTTPS.
Static File Middleware (UseStaticFiles) returns static files and short-circuits further request processing.
Cookie Policy Middleware (UseCookiePolicy) conforms the app to the EU General Data Protection Regulation (GDPR) regulations.
Routing Middleware (UseRouting) to route requests.
Authentication Middleware (UseAuthentication) attempts to authenticate the user before they're allowed access to secure resources.
Authorization Middleware (UseAuthorization) authorizes a user to access secure resources.
Session Middleware (UseSession) establishes and maintains session state. If the app uses session state, call Session Middleware after Cookie Policy Middleware and before MVC Middleware.
Endpoint Routing Middleware (UseEndpoints with MapRazorPages) to add Razor Pages endpoints to the request pipeline.
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();
In the preceding example code, each middleware extension method is exposed on WebApplicationBuilder through the Microsoft.AspNetCore.Builder namespace.
UseExceptionHandler is the first middleware component added to the pipeline. Therefore, the Exception Handler Middleware catches any exceptions that occur in later calls.
Static File Middleware is called early in the pipeline so that it can handle requests and short-circuit without going through the remaining components. The Static File Middleware provides no authorization checks. Any files served by Static File Middleware, including those under wwwroot, are publicly available. For an approach to secure static files, see Static files in ASP.NET Core.
If the request isn't handled by the Static File Middleware, it's passed on to the Authentication Middleware (UseAuthentication), which performs authentication. Authentication doesn't short-circuit unauthenticated requests. Although Authentication Middleware authenticates requests, authorization (and rejection) occurs only after MVC selects a specific Razor Page or MVC controller and action.
The following example demonstrates a middleware order where requests for static files are handled by Static File Middleware before Response Compression Middleware. Static files aren't compressed with this middleware order. The Razor Pages responses can be compressed.
// Static files aren't compressed by Static File Middleware.
app.UseStaticFiles();
app.UseRouting();
app.UseResponseCompression();
app.MapRazorPages();
Run, Use, Next, and Map Methods in Middleware:
In order to work with ASP.NET Core Middleware Components, we need to learn about few methods are as follows:
Run() Method: The Run() Extension Method is used to complete the Middleware Execution.
Use() Method: The Use() Extension Method is used to insert a new Middleware component to the Request Processing Pipeline.
Next() Method: The Next() Extension Method is used to call the next middleware component in the request processing pipeline.
Map() Method: The Map() Extension Method is used to map the Middleware to a specific URL.
Built-in Middleware:
ASP.NET Core ships with the following middleware components. The Order column provides notes on middleware placement in the request processing pipeline and under what conditions the middleware may terminate request processing. When a middleware short-circuits the request processing pipeline and prevents further downstream middleware from processing a request, it's called a terminal middleware. For more information on short-circuiting, see the Create a middleware pipeline with WebApplication section.
Middleware | Description | Order |
Authentication | Provides authentication support. | Before HttpContext.User is needed. Terminal for OAuth callbacks. |
Authorization | Provides authorization support. | Immediately after the Authentication Middleware. |
Cookie Policy | Tracks consent from users for storing personal information and enforces minimum standards for cookie fields, such as secure and SameSite . | Before middleware that issues cookies. Examples: Authentication, Session, MVC (TempData). |
CORS | Configures Cross-Origin Resource Sharing. | Before components that use CORS. UseCors currently must go before UseResponseCaching due to this bug. |
DeveloperExceptionPage | Generates a page with error information that is intended for use only in the Development environment. | Before components that generate errors. The project templates automatically register this middleware as the first middleware in the pipeline when the environment is Development. |
Diagnostics | Several separate middlewares that provide a developer exception page, exception handling, status code pages, and the default web page for new apps. | Before components that generate errors. Terminal for exceptions or serving the default web page for new apps. |
Forwarded Headers | Forwards proxied headers onto the current request. | Before components that consume the updated fields. Examples: scheme, host, client IP, method. |
Health Check | Checks the health of an ASP.NET Core app and its dependencies, such as checking database availability. | Terminal if a request matches a health check endpoint. |
Header Propagation | Propagates HTTP headers from the incoming request to the outgoing HTTP Client requests. | |
HTTP Logging | Logs HTTP Requests and Responses. | At the beginning of the middleware pipeline. |
HTTP Method Override | Allows an incoming POST request to override the method. | Before components that consume the updated method. |
HTTPS Redirection | Redirect all HTTP requests to HTTPS. | Before components that consume the URL. |
HTTP Strict Transport Security (HSTS) | Security enhancement middleware that adds a special response header. | Before responses are sent and after components that modify requests. Examples: Forwarded Headers, URL Rewriting. |
MVC | Processes requests with MVC/Razor Pages. | Terminal if a request matches a route. |
OWIN | Interop with OWIN-based apps, servers, and middleware. | Terminal if the OWIN Middleware fully processes the request. |
Output Caching | Provides support for caching responses based on configuration. | Before components that require caching. UseRouting must come before UseOutputCaching . UseCORS must come before UseOutputCaching . |
Response Caching | Provides support for caching responses. This requires client participation to work. Use output caching for complete server control. | Before components that require caching. UseCORS must come before UseResponseCaching . Is typically not beneficial for UI apps such as Razor Pages because browsers generally set request headers that prevent caching. Output caching benefits UI apps. |
Request Decompression | Provides support for decompressing requests. | Before components that read the request body. |
Response Compression | Provides support for compressing responses. | Before components that require compression. |
Request Localization | Provides localization support. | Before localization sensitive components. Must appear after Routing Middleware when using RouteDataRequestCultureProvider. |
Endpoint Routing | Defines and constrains request routes. | Terminal for matching routes. |
SPA | Handles all requests from this point in the middleware chain by returning the default page for the Single Page Application (SPA) | Late in the chain, so that other middleware for serving static files, MVC actions, etc., takes precedence. |
Session | Provides support for managing user sessions. | Before components that require Session. |
Static Files | Provides support for serving static files and directory browsing. | Terminal if a request matches a file. |
URL Rewrite | Provides support for rewriting URLs and redirecting requests. | Before components that consume the URL. |
W3CLogging | Generates server access logs in the W3C Extended Log File Format. | At the beginning of the middleware pipeline. |
WebSockets | Enables the WebSockets protocol. | Before components that are required to accept WebSocket requests. |
How to create custom Middleware?
ASP.Net core provided many in-build Middleware but in some scenarios, we need our own middleware to fulfill our requirement, in such case will go with custom middleware.
In the first article, we have written inline middleware but in the real world, we will create a separate class for middleware that is called custom middleware.
There are 2 ways to create Custom Middleware in Asp.net Core.
Using IMiddleware interface
Using the extension method
Let’s try to learn how to create custom middleware using IMiddelware Interface the below example:
Step 1: Create a class and name it “CustomeMiddlewareDemo” and inherit from “IMiddleware” interface from “Microsoft.AspNetCore.Http”
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace WebApplication1 {
public class CustomMiddlewareDemo: IMiddleware {
public Task InvokeAsync(HttpContext context, RequestDelegate next) {
throw new System.NotImplementedException();
}
}
}
Step 2: Add code for Custom middleware in the “InvokeAsync” method
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace WebApplication1 {
public class CustomMiddlewareDemo: IMiddleware {
public async Task InvokeAsync(HttpContext context, RequestDelegate next) {
await context.Response.WriteAsync("Hellow from Custom Middleware");
await next(context);
}
}
}
How to use Middleware in the HTTP Pipeline? We will consume newly created custom middleware in the HHTP pipeline. As we know if want to use any kind of new services in the ASP.Net Core application then before we use we need to register startup.cs -> “ConfigureServices” method. Before we use Custom middleware in the code, we will register in the “Configure Service” method.
Step 3: Configure CustomMiddleware in the “ConfigureService” Method.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WebApplication1 {
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration {
get;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
services.AddControllersWithViews();
services.AddTransient < CustomMiddlewareDemo > ();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
} else {
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Step 4: Now we will use CustomMiddleware in the HTTP Pipeline. For that, we will inject custom Middleware in the “Configure” method like below.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WebApplication1 {
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration {
get;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
services.AddControllersWithViews();
services.AddTransient < CustomMiddlewareDemo > ();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
app.UseMiddleware < CustomMiddlewareDemo > ();
app.Run(async context => await context.Response.WriteAsync("Hello"));
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
} else {
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Output:
Thanks for your time. I hope this post was helpful and gave you a mental model for ASP.Net Core Middleware