Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / containers / docker

Create HTTP Request Pipeline using ASP.NET Core Custom Middleware: Build/run on Mac, Windows, Linux or Docker Container

4.99/5 (24 votes)
27 Dec 2016MIT13 min read 50K   68  
How to build HTTP request pipeline on ASP.NET Core
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.

Image 1

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:

  1. Install .NET Core SDK.
  2. 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

Image 2

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.

JavaScript
{
  "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:

C#
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.

C#
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace DemoApp
{
    public class Startup
    {
        // Use this method to add framework services (MVC, EF, Identity, Logging) 
        // and application services 
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // Use this method to configure the HTTP request pipeline.
        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.

Image 3

We will now run the project using below command. This will compile and execute our project.

dotnet run

Image 4

If your project compiled successfully, open your browser and go to http://localhost:5000/.

Image 5

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.

C#
public void Configure(IApplicationBuilder app)
{
    // add inline demo middleware
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Hello from inline demo middleware...");
        // invoke the next 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.

C#
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.

C#
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.

C#
public void Configure(IApplicationBuilder app)
{
    // inline demo middleware
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Hello from inline demo middleware...");
        // invoke the next middleware
        await next.Invoke();
    });

    // standalone DemoMiddleWare;
    app.UseDemoMiddleware();

    // terminal middleware
    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.

Image 6

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.

Image 7

Here is the code for our interface IMessage.cs:

C#
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:

C#
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:

C#
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.

C#
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.

Image 8

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:

Image 9

Below image is when I ran application in a Docker container.

Image 10

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:

Image 11

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:

  1. Add package dependencies to project.json.
  2. If required add services for middleware in ConfigureServices method of Startup.cs.
  3. Add middleware to HTTP request pipeline in Configure method of Startup.cs.
  4. Let us add the following dependencies in project.json.
JavaScript
"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:

Image 12

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.

C#
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            // WelcomePage middleware added to pipeline for Production environment.
            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.

Image 13

Press F5 to start debugging which will launch the Command Window on successful build.

Image 14

Once your application is started, browse to http://localhost:5000 to see Welcome page as seen in the below image:

Image 15

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.

C#
        public void ConfigureServices(IServiceCollection services)
        {
            // add GZipCompression service
            services.AddResponseCompression();

To use GZip compression using the fastest compression level, add ResponseCompression middleware to HTTP pipeline in Configure method of Startup class.

C#
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            // Response Compression Middleware
            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.

Image 16

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.

JavaScript
            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.

JavaScript
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.

Image 17

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

License

This article, along with any associated source code and files, is licensed under The MIT License