Click here to Skip to main content
15,867,686 members
Articles / Web Development / ASP.NET / ASP.NET Core

An Absolute Beginner's Tutorial on Middleware in ASP.NET Core/MVC (and Writing Custom Middleware)

Rate me:
Please Sign up or sign in to vote.
4.81/5 (9 votes)
12 Sep 2018CPOL8 min read 13.5K   139   19   1
In this article, we will try to understand the concept of middleware in ASP.NET core.

Introduction

In this article, we will try to understand the concept of middleware in ASP.NET core. We will see how middleware plays an important part in request response pipeline and how we can write and plug-in our custom middleware.

Background

Before we could get into what middleware is and the value it brings, we need to understand how the request response worked in classic ASP.NET model. In earlier days, the request and response objects in ASP.NET were very big and had very tight coupling with IIS. This was a problem because some of the values in these objects are filled by the IIS request-response pipeline and unit testing such bloated objects was a very big challenge.

So the first problem that needed to be solved was to decouple the applications from web servers. This was very nicely defined by community owned standards called Open Web Interface for .NET (OWIN). Since the older ASP.NET applications were dependent on System.Web DLL which internally had very tight coupling with IIS, it was very difficult to decouple the applications from web servers. To circumvent this problem, OWIN defines is to remove the dependency of web applications on System.web assembly so that the coupling with web server (IIS) gets removed.

OWIN primarily defines the following actors in its specifications:

  • Server — The HTTP server that directly communicates with the client and then uses OWIN semantics to process requests. Servers may require an adapter layer that converts to OWIN semantics.
  • Web Framework — A self-contained component on top of OWIN exposing its own object model or API that applications may use to facilitate request processing. Web Frameworks may require an adapter layer that converts from OWIN semantics.
  • Web Application — A specific application, possibly built on top of a Web Framework, which is run using OWIN compatible Servers.
  • Middleware — Pass through components that form a pipeline between a server and application to inspect, route, or modify request and response messages for a specific purpose.
  • Host — The process an application and server execute inside of, primarily responsible for application startup. Some Servers are also Hosts.

Since OWIN is just a standard, there are multiple implementations for this in the last few years starting from Katana to the present day implementation in ASP.NET core. We will now focus on how the middleware implementation looks like in ASP.NET core.

Before that, let's try to understand what a middleware is. For the developer coming from the ASP.NET world, the concept of HTTPModule and HTTPHander is fairly familiar. These are used to intercept the request-response pipeline and implement our custom logic by writing custom modules or handlers. In the OWIN world, the same thing is achieved by the middleware.

OWIN specifies that the request coming from web server to the web application has to pass through multiple components in a pipeline sort of fashion where each component can inspect, redirect, modify or provide a response for this incoming request. The response then will get passed back to the web server in the opposite order back to the web server which then can be served back to the user. The following image visualizes this concept:

Image 1

If we look at the above diagram, we can see that the request passes through a chain of middleware and then some middleware decides to provide a response for the request and then response travels back to web server passing through all the same middleware it passed through while request. So a middleware typically can:

  • Process the request and generate the response
  • Monitor the request and let it pass thorough to next middleware in line
  • Monitor the request, modify it and then let it pass through to next middleware in line

If we try to find the middleware with actual use cases defined above:

  • Process the request and generate the response: MVC itself is a middleware that typically gets configured in the very end of middleware pipeline
  • Monitor the request and let it pass through to the next middleware in line: Logging middleware which simply logs the request and response details
  • Monitor the request, modify it and then let it pass through to the next middleware in line: Routing and Authentication module where we monitor the request decide which controller to call (routing) and perhaps update the identity and Principle for authorization (Auth-Auth).

Using the Code

In this article, we will create 2 owin middleware. First one will demonstrate the scenario where we are not altering the request. For this, we will simply log the request and response time in the log - TimingMiddleware. Second one will check the incoming response, find a specific header value to determine which tenant is calling the code and then returning back if the tenant is not valid - MyTenantValidator.

Note: Before we get started with the sample implementation, it's good to highlight the point that middleware is an implementation of pipes and filter pattern. Pipes and filter pattern says that if we need to performs a complex processing that involves a series of separate activity, it's better to separate out each activity as a separate task that can be reused. This gives us benefits in terms of reusability, performance and scalability.

Let's start by looking at how the middleware class definition should look like. There are two ways to define our custom middleware:

  1. Custom middleware class
  2. Inline custom middleware

Custom Middleware Class

First way is to have a custom class containing our middleware logic.

C#
public class MyCustomMiddleware
{
	private readonly RequestDelegate _next;

	public MyCustomMiddleware(RequestDelegate next)
	{
		_next = next;
	}

	public async Task InvokeAsync(HttpContext context)
	{
		// Todo: Our logic that we need to put in when the request is coming in

		// Call the next delegate/middleware in the pipeline
		await _next(context);

		// Todo: Our logic that we need to put in when the response is going back
	}
}

What this class does is that it gets called once the request reached this middleware. InvokeAsync function will get called and the current HttpContext will be passed to it. We can then execute our custom logic using this context and then call the next middleware in the pipeline. Once the request is processed by all middleware after this middleware, the response is generated and the response will follow the reverse chain and the function will reach after our _next call where we can put the logic that we want to execute before the response goes back to the previous middleware.

For our middleware to get into the pipeline, we need to use the Configure method in our Startup class to hook our middleware.

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	// OUR CUSTOM MIDDLEWARE
	app.UseMiddleware<MyCustomMiddleware>();

	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseExceptionHandler("/Home/Error");
		app.UseHsts();
	}

	app.UseHttpsRedirection();
	app.UseStaticFiles();
	app.UseCookiePolicy();

	app.UseMvc(routes =>
	{
		routes.MapRoute(
			name: "default",
			template: "{controller=Home}/{action=Index}/{id?}");
	});
}

The above code shows how we have hooked in our custom middleware as the first middleware in the pipeline. The middleware will be called in the same order as they are hooked in this method. So in the above code, our middleware will be called first and the MVC middleware will be the last one to get called.

Inline Custom Middleware

The inline custom middleware is directly defined in the Configure method. The following code shows how to achieve this:

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	// OUR CUSTOM MIDDLEWARE
	app.Use(async (context, next) =>
	{
		// Todo: Our logic that we need to put in when the request is coming in

		// Call the next delegate/middleware in the pipeline
		await next();

		// Todo: Our logic that we need to put in when the response is going back
	});

	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseExceptionHandler("/Home/Error");
		app.UseHsts();
	}

	app.UseHttpsRedirection();
	app.UseStaticFiles();
	app.UseCookiePolicy();

	app.UseMvc(routes =>
	{
		routes.MapRoute(
			name: "default",
			template: "{controller=Home}/{action=Index}/{id?}");
	});
}

The end result will be the same for both approaches. So if our middleware is doing some trivial things that do not impact the readability of code if we put as inline, we could create the inline custom middleware. If the code that we want has significant code and logic in our middleware, we should use the custom middleware class to define our middleware.

Coming back to the middleware that we are going to implement, we will use the inline approach to define the TimingMiddleware and the custom class approach to define the MyTenantValidator.

Implementing the TimingMiddleware

The sole purpose of this middleware is to inspect the request and response and log the time that this current request took to process. Let's define it as inline middleware. The following code shows how this can be done.

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseExceptionHandler("/Home/Error");
		app.UseHsts();
	}

	app.UseHttpsRedirection();
	app.UseStaticFiles();
	app.UseCookiePolicy();

	app.Use(async (context, next) =>
	{
		DateTime startTime = DateTime.Now;

		// Call the next delegate/middleware in the pipeline
		await next();

		DateTime endTime = DateTime.Now;

		TimeSpan responseTime = endTime - startTime;
		// Log the response time here using your favorite logging or telemetry module
	});

	app.UseMvc(routes =>
	{
		routes.MapRoute(
			name: "default",
			template: "{controller=Home}/{action=Index}/{id?}");
	});
}

We have hooked this middleware just before MVC middleware so that we can measure the time our request processing is taking. It is defined after UseStaticFiles middleware so that this middleware will not get invoked for all static files that are being served from our application.

Implementing the MyTenantValidator

Now let's implement a middleware that will take care of tenant verification. It will check for the incoming header and if the tenant is not valid, it will stop the request processing.

Note: For the sake of simplicity, I will be looking for a hard coded tenant id value. But in real world applications, this approach should never be used. This is being done only for demonstration purposes. Note that we will be using the tenant id value as 12345678.

This middleware will be written in its separate class. The logic is simple, check for the headers in incoming request. If the header matches the hard coded tenant id, let the request proceed to the next middleware else terminate the request by sending response from this middleware itself. Let's look at the code of this middleware.

C#
public class MyTenantValidator
{
	private readonly RequestDelegate _next;

	public MyTenantValidator(RequestDelegate next)
	{
		_next = next;
	}

	public async Task InvokeAsync(HttpContext context)
	{
		StringValues authorizationToken;
		context.Request.Headers.TryGetValue("x-tenant-id", out authorizationToken);

		if(authorizationToken.Count > 0 && authorizationToken[0] == "12345678") 
		{
			// Call the next delegate/middleware in the pipeline
			await _next(context);
		}
		else
		{
			context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;    
			await context.Response.WriteAsync("Invalid calling tenant");
			return;
		}	   
	}
}    

Now let's register this middleware in our Startup class.

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	app.UseMiddleware<MyTenantValidator>();

	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseExceptionHandler("/Home/Error");
		app.UseHsts();
	}

	app.UseHttpsRedirection();
	app.UseStaticFiles();
	app.UseCookiePolicy();

	app.Use(async (context, next) =>
	{
		DateTime startTime = DateTime.Now;

		// Call the next delegate/middleware in the pipeline
		await next();

		DateTime endTime = DateTime.Now;

		TimeSpan responseTime = endTime - startTime;
		// Log the response time here using your favorite logging or telemetry module
	});

	app.UseMvc(routes =>
	{
		routes.MapRoute(
			name: "default",
			template: "{controller=Home}/{action=Index}/{id?}");
	});
}

With this code in place, if we try to run the application, we can see the response as error.

Image 2

To circumvent this issue, we need to pass the tenant id in the header.

Image 3

With this change, when we access the application again, we should be able to browse our application.

Image 4

Note: Even though we were talking in context of ASP.NET core, the concept of middleware is the same in all MVC implementations that are adhering to OWIN standards.

Points of Interest

In this article, we talked about ASP.NET core middleware. We looked at what middleware is and how we can write our own custom middleware. This article has been written from a beginner's perspective. I hope this has been somewhat informative.

References

History

  • 12th September, 2018: First version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
India India

I Started my Programming career with C++. Later got a chance to develop Windows Form applications using C#. Currently using C#, ASP.NET & ASP.NET MVC to create Information Systems, e-commerce/e-governance Portals and Data driven websites.

My interests involves Programming, Website development and Learning/Teaching subjects related to Computer Science/Information Systems. IMO, C# is the best programming language and I love working with C# and other Microsoft Technologies.

  • Microsoft Certified Technology Specialist (MCTS): Web Applications Development with Microsoft .NET Framework 4
  • Microsoft Certified Technology Specialist (MCTS): Accessing Data with Microsoft .NET Framework 4
  • Microsoft Certified Technology Specialist (MCTS): Windows Communication Foundation Development with Microsoft .NET Framework 4

If you like my articles, please visit my website for more: www.rahulrajatsingh.com[^]

  • Microsoft MVP 2015

Comments and Discussions

 
QuestionVery useful Pin
Prabu ram20-Sep-18 1:02
Prabu ram20-Sep-18 1:02 
Good article, neat and simple.Thank you for writing this
Praburam

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.