This tutorial is about building HTTP request pipeline on ASP.NET Core. We will start from scratch using Command Line, write custom middleware in C# and finish with adding built-in middleware to pipeline. We will build and run the demo application on Mac, Windows and Docker container in Linux.
Introduction
Just as I/O is the means a computer uses to exchange information, the World Wide Web uses Request-Response pattern to communicate. HTTP is the protocol for Request-Response communication between client-server on web. In this article, we will build a pipeline to process HTTP request using middleware components on ASP.NET Core. One of the big advantages of building HTTP request pipeline on ASP.NET Core is that you can make a lean and modular pipeline which can run on Windows, Linux and Mac. ASP.NET Core is open-source and there are many NuGet packages that can help in making custom pipeline to serve your needs. You can also write your own custom middleware. ASP.NET Core has built-in support for Dependency Injection (DI), a technique used to achieve loose coupling in your application.
The HTTP request pipeline we build can be used to develop custom microservices running in a Docker container. It is ideal for small independent tasks but being modular can be extended with more features.
Background
As the old saying goes, you can't know where you are going until you know where you have been. So how does HTTP request flow in classic ASP.NET page running on Microsoft IIS web server? In an integrated approach, IIS and ASP.NET request pipelines will be combined to process the request through native and managed modules. The below diagram from IIS.NET illustrates the flow of HTTP request in IIS 7.
Reference: https://www.iis.net/learn/get-started/introduction-to-iis/introduction-to-iis-architecture
In classic ASP.NET, the HTTP request passes through several events of an HTTP application pipeline. Developers can write their code to run when events are raised. They can also create custom modules using IHttpModule interface and add to the configuration section of the application's Web.config file.
In comparison, ASP.NET Core is designed for performance with minimal footprint. You start with a clean slate and add features you need to request pipeline using middleware. This article is about how to add middleware to build your custom HTTP request pipeline.
Prerequisites
If you intend to follow the tutorial, you will need to install the following items:
- Install .NET Core SDK.
- We will use Command Line and start from scratch for a better understanding. Initially, I suggest using a text editor of your choice. Later on, you can open the project in Visual Studio Code or Visual Studio 2015 to take advantage of IDE.
Let's Get Started
In this section, we will create a working app directory DemoApp and create a new application using Command Line. We will also update project file and install required packages using Command Line.
Create App Directory and Build New Project
First, check that .NET Core is installed. Create a directory to hold your application, and make it your working directory. On my machine, the working directory is located at C:\Projects\DemoApp. Open Command Prompt and change the directory to your working directory. Use the dotnet new
command to create a new application.
dotnet new
The above screenshot shows the dotnet version, the working directory and the two files in the new C# project.
We are starting with bare minimum in our project. Since we are building our app on AspNet Core, we will need to add required dependencies in project.json file. Open project.json file, add "Microsoft.AspNetCore.Server.Kestrel
": "1.1.0
" as one of the dependencies.
{
"version": "1.0.0-*",
"buildOptions": {
"debugType": "portable",
"emitEntryPoint": true
},
"dependencies": {
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0"
},
"frameworks": {
"netcoreapp1.1": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.1.0"
}
},
"imports": "dnxcore50"
}
}
Our current app is a simple Console Application with one Program.cs file with Main
method which outputs to Console
. We will change the Program.cs file to create a host that will use Kestrel web server and Startup
class to run our app. Replace the code in Program.cs with the below code:
using System;
using Microsoft.AspNetCore.Hosting;
namespace DemoApp
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseUrls("http://*:5000")
.UseKestrel()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}
In classic ASP.NET, the Application_Start
and Application_End
events in Global.asax (which is derived from the HttpApplication
) were called during the application life cycle. In ASP.NET Core, the application is initialized in the Main
method of the Program.cs. The Main
method in our code creates an instance of WebHostBuilder
and uses various extension methods to specify the URL, the Server and the Startup
class to build Host and run the application. The Startup
class (which we will create next) is the entry point of our HTTP request pipeline.
In ASP.NET Core, we program using conventions. Create a new Startup.cs file and add the following code. The Startup Constructor can optionally accept dependencies like IHostingEnvironment
which will be provided through dependency injection.
The optional ConfigureServices
method is used to define and configure the services available to the application. The ConfigureServices
can only take the parameter of IServiceCollection
type.
The Configure
method is to build the pipeline using Middleware and how to handle request response. The Configure
method must take IApplicationBuilder
type parameter. Services of type IApplicationBuilder
, IHostingEnvironment
and ILoggerFactory
can be passed as parameter and they will be injected if available.
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace DemoApp
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
}
public void Configure(IApplicationBuilder app)
{
app.Run(context =>
{
return context.Response.WriteAsync("Hello from ASP.NET Core!");
});
}
}
}
Let us restore the dependencies specified in Project.json by using the below command:
dotnet restore
This will create a new file project.lock.json which contains list of all the NuGet packages used by the app.
We will now run the project using below command. This will compile and execute our project.
dotnet run
If your project compiled successfully, open your browser and go to http://localhost:5000/.
Congratulations, you have successfully built a simple http application on ASP.NET core.
Custom Middleware
Middleware are components that handle request and response in HTTP request pipeline. These components chained together compose a pipeline. RequestDelegate
is used to chain the middlewares. What is RequestDelegate
? A RequestDelegate
is a function that accepts HttpContext
type and returns Task
type (a promise).
Each middleware in the pipeline invokes the next middleware in sequence or terminates the request. You can perform actions both before and after the next middleware is invoked. Let us add an in-line demo middleware to HTTP request pipeline in Configure
method in Startup.cs file.
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Hello from inline demo middleware...");
await next.Invoke();
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Welcome to ASP.NET Core!");
});
}
In the above code, the in-line demo middleware code is registered with app.Use
. Note that if you don’t call next.Invoke()
, it will short-circuit the request pipeline. Also note that in app.Run
method, what you have is a terminal middleware which is called at the end of HTTP request pipeline. I have changed the text to differentiate messages.
In our in-line demo middleware, we are passing HttpContext
and RequestDelegate
to a lambda expression but for a complex middleware processing, you should create its own class. Below is the code for DemoMiddleware
class. It has a constructor that accepts a RequestDelegate
and an Invoke
method that accepts HttpContext
.
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace DemoApp
{
public class DemoMiddleware
{
private readonly RequestDelegate _next;
public DemoMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
await context.Response.WriteAsync("Message from DemoMiddleware...");
await _next(context);
}
}
}
In order to register the DemoMiddleware
, we will create a class that will extend IApplicationBuilder
to provide an easy way to add middleware to the request pipeline. Below is the code for DemoMiddlewareExtensions.cs.
using Microsoft.AspNetCore.Builder;
namespace DemoApp
{
public static class DemoMiddlewareExtensions
{
public static void UseDemoMiddleware(this IApplicationBuilder builder)
{
builder.UseMiddleware<DemoMiddleware>();
}
}
}
Now by calling app.UseDemoMiddleware()
in Configure
method of Startup
class, we can add DemoMiddleware
to HTTP request pipeline.
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Hello from inline demo middleware...");
await next.Invoke();
});
app.UseDemoMiddleware();
app.Run(async (context) =>
{
await context.Response.WriteAsync("Welcome to ASP.NET Core!...");
});
}
On successfully compiling and executing our DemoApp
project using dotnet run
, and browsing to http://localhost:5000/, you should see messages from inline middleware, standalone middleware and terminal middleware added to Http request pipeline in Configure
method of Startup
class.
Note that the http request is processed in the same order as the sequence of middleware components and the response is processed in reverse. It is important to understand the business logic followed by chaining middleware in http pipeline. The sequence of middleware can make a big difference in security, performance and behavior of your application.
Dependency Injection and Strategy Design Pattern
ASP.NET Core framework is designed to support dependency injection. It has built-in Inversion of Control (IoC) container, also called DI container. The built-in DI container provides services which are responsible for providing instances of types configured in ConfigureServices
method of the Startup
class.
If we want our DemoMiddleware
class to provide custom message, we can use Strategy design pattern. We will create an Interface
and provide implementation of this interface as parameters. You can keep using your favorite Text Editor or IDE. To open the DemoApp
project in Visual Studio 2015, go to File menu, select Open > Project/Solution. In Open Project dialog box, select project.json in DemoApp folder. To open the Demo Project in Visual Studio Code, go to File menu, select Open and then open DemoApp folder. Here is the screenshot of DemoApp
project when opened for the first time in Visual Studio Code on Mac.
Here is the code for our interface IMessage.cs:
namespace DemoApp
{
public interface IMesssage
{
string Info();
}
}
We will implement the above interface in one of RuntimeMessage
class to provide information about OS and Framework where DemoApp
is running. Below is the code for RuntimeMessage.cs:
using System.Runtime.InteropServices;
namespace DemoApp
{
public class RuntimeMessage : IMesssage
{
public string Info()
{
return $@"
OS Description: {RuntimeInformation.OSDescription}
OS Architecture: {RuntimeInformation.OSArchitecture}
Framework: {RuntimeInformation.FrameworkDescription}
Process Architecture: {RuntimeInformation.ProcessArchitecture}";
}
}
}
In DemoMiddleware
class, we will update the constructor to accept IMessage
as parameter and update the Invoke
method to write the response returned from above Info
method. The updated DemoMiddleware.cs is as follows:
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace DemoApp
{
public class DemoMiddleware
{
private readonly RequestDelegate _next;
private readonly IMesssage _message;
public DemoMiddleware(RequestDelegate next, IMesssage message)
{
_next = next;
_message = message;
}
public async Task Invoke(HttpContext context)
{
await context.Response.WriteAsync("\r\nMessage from DemoMiddleware:");
await context.Response.WriteAsync(_message.Info() + "\r\n");
await _next(context);
}
}
}
In order to resolve IMessage
to RuntimeMessage
, we will register our dependencies in ConfigureServices
method of Startup
class. The AddTransient
method is used to map abstract types to concrete types (ASP.NET's container refers to the types it manages as services) whenever it is requested.
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IMesssage, RuntimeMessage>();
}
Let us compile and run the code. You can use dotnet run
in Command line or press F5 to debug in Visual Studio tools. Below screenshot is when I debug code in Visual Studio Code on Mac.
Build and Run Application on Mac, Windows, Linux or Docker Container
The runtime info message from DemoMiddleware
will be based on the platform on which DemoApp
is running. The above runtime info message I got is when I ran DemoApp
on my Mac OS X El Capitan.
ASP.NET Core can run on Windows, Linux and Mac, you have a choice where you want to run application. On Windows 10 computer, the runtime info message in Chrome browser looks as follows:
Below image is when I ran application in a Docker container.
Docker is an excellent platform for building and running Microservices. Installing Docker, building Docker image, publishing and running the app in a Docker container are topics by itself. There are some excellent documents on these topics at https://docs.docker.com/ and https://www.microsoft.com/net/core#dockercmd. If you do have Docker running in your machine, you can copy below code in a dockerfile in your DemoApp folder and use it to build your Docker image.
FROM microsoft/dotnet:1.1.0-sdk-projectjson
COPY . /demoapp
WORKDIR /demoapp
EXPOSE 5000
ENV ASPNETCORE_URLS http://+:5000
RUN dotnet restore
RUN dotnet build
ENTRYPOINT ["dotnet", "run"]
You can build Docker image in Command Line from DemoApp working directory.
docker build . -t np:demoapp
When the Docker image is built successfully, spin the Docker container using the below command.
docker run -d -p 80:5000 -t np:demoapp
You can see list of all containers with command:
docker ps -a
For more Docker commands, refer to https://docs.docker.com/engine/reference/commandline/. Here is the screenshot of building docker image of demoapp commandline:
Built-in Middleware
The ASP.NET Core 1.1 comes with built-in middleware such as Authentication, CORS, Routing, Session, Static Files, Diagnostics, URL Rewriting, Response Caching and Response Compression. There are also many middleware available in nugget packages.
In our DemoApp
, we will add Diagnostics middleware for exception handling, Static Files middleware to serve content from www folder and Response Compression middleware for GZip content for faster network transfers. In adding middleware, we follow these steps:
- Add package dependencies to project.json.
- If required add services for middleware in
ConfigureServices
method of Startup.cs. - Add middleware to HTTP request pipeline in
Configure
method of Startup.cs. - Let us add the following dependencies in project.json.
"Microsoft.AspNetCore.Diagnostics": "1.1.0",
"Microsoft.AspNetCore.ResponseCompression": "1.0.0",
"Microsoft.AspNetCore.StaticFiles": "1.1.0"
It should be obvious from the package names, the features we can provide to our DemoApp
. You can run dotnet restore
using CLI or use Visual Studio Code or Visual Studio to restore packages. With ASP.NET, Core is cross-platform and you can develop applications on Mac, Linux or Windows. You can use your favorite tool and I am going to open the DemoApp
project in Visual Studio 2015. As soon as I add the above dependencies in Project.json and save the file, Visual Studio will restore packages as seen below:
Because ASP.NET Core has a built-in dependency injection, we will be able to inject services (types managed by DI container) by passing appropriate Interface as a parameter in methods of Startup
class. You can also replace the DI container (represented by the IServiceProvider
interface) provided by framework with Autofac, NInject or any other Inversion of Control containers. In our DemoApp
, we will use the default dependency injection provided by ASP.NET Core framework.
The constructor and Configure
method of Startup
class accept IHostingEnvironment
and ILoggerFactory
as parameters to request appropriate services required. In the below code, I added WelcomePage
built-in middleware in Diagnostics
package. I want to add this middleware only for production hosting environment. I have included IHostingEnvironment
as parameter in Configure
method to get information about the environment.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsProduction())
{
app.UseWelcomePage();
}
To add environment
variable, right click DemoApp
, select Properties > Debug and click Add to enter values as shown in the below screen. This will create or update launchSetting.json in Properties folder under DemoApp
project.
Press F5 to start debugging which will launch the Command Window on successful build.
Once your application is started, browse to http://localhost:5000 to see Welcome page as seen in the below image:
If you want to see the above page on a different path, WelcomePageMiddleware
has few overloaded versions. For example, UseWelcomePage(“/home”)
will show welcome page only on http://localhost:5000/home path.
To use GZip Compression service available to our DI container, we need to add middleware in ConfigureServices
method of Startup
class.
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression();
To use GZip compression using the fastest compression level, add ResponseCompression
middleware to HTTP pipeline in Configure
method of Startup
class.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseResponseCompression();
Now if you build and browse the site and view the response in developer tools of your browser, you should see response content encoded in GZip.
We will end this tutorial by adding file serving features to our HTTP request pipeline. The files will be served from the web root of the folder which has public content. I want www folder to be the root folder serving static pages and single page application (SPA). We can specify the root folder using UseWebRoot
extension method of WebHostBulder
class in program.cs.
var host = new WebHostBuilder()
.UseUrls("http://*:5000")
.UseKestrel()
.UseWebRoot(Directory.GetCurrentDirectory() + "/www")
.UseStartup<Startup>()
.Build();
By adding the following static file middlewares in Configure
method of Startup
class, we can enable default files, serve static files and directory browsing for www folder and subfolders inside.
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseDirectoryBrowser();
Let us change the environment to Development in launchSettings.json, build and run DemoApp
to verify the file serving feature added to HTTP request pipeline. I have added html pages, single page application and an Angularjs client API in subfolders of web root folder. You can add any static content such as html, images, videos, css, text, json and JavaScript.
Conclusion
In this tutorial, I have demonstrated how to build a custom and lightweight HTTP request pipeline on a new ASP.NET Core framework. I started from scratch and then added custom middleware to a request pipeline. I used Dependency Injection in Startup
class to configure middleware to provide custom messages. I also later on added built-in middlewares in ASP.NET core to the pipeline. Generally, I use my favorite IDE installed on my computer for development but here, I have used Command Line Interface, Visual Studio Code and Visual Studio 2015 for development and demonstrated compiling and running of ASP.NET Core application on Mac, Linux and Windows.
References
History
I understand this is a long (tl;dr) cross-platform tutorial on a new technology. I appreciate your suggestions or corrections. Any changes or improvements I made here will be posted here.
- 27th December, 2016: Added uncompiled source code for
DemoApp