Foreword

The ASP.NET Core Mini framework with less than 200 lines of code.

Microsoft’s official website on the concept of ASP.NET Core “ASP.NET Core is an open source and cross-platform framework for building modern Web-based Internet applications, such as Web applications, IoT applications and mobile backends. ASP. NET Core applications can run on .NET Core or the full .NET Framework. Its architecture is designed to provide an optimized development framework for applications deployed to the cloud or run locally. It consists of modular components with minimal overhead. So you can stay flexible while building your solution. You can develop and run ASP.NET core applications across platforms on Windows, Mac and Linux.” It can be seen from the definition that the ASP.NET Core framework has the characteristics of cross-platform, flexible deployment, and modularity.

Secret of the ASP.NET Core framework

ASP.NET Core Mini is a mini-version ASP.NET Core framework with 200 lines of code. It has three features: “simple”, “real simulation” and “executable” to make it easier for us to understand ASP.NET Core.

Code structure:

200_lines_code_to_complete_Mini_ASP.NET_Core_1.png

The following figure is the result of the project run page output:

This article is described in the following five perspectives:

  • Program : Project Entrance
  • Middleware : Middleware
  • HttpContext : Http related
  • WebHost : WebHost
  • Server : Server related

Program

using System.Threading.Tasks;
using App.Server;
using App.WebHost;
 
namespace App
{
    public static class Program
    {
        public static async Task Main(string[] args)
        {
            await CreateWebHostBuilder()
                .Build()
                .Run();
        }
 
        private static IWebHostBuilder CreateWebHostBuilder()
        {
            return new WebHostBuilder()
                .UseHttpListener()
                .Configure(app => app
                    .Use(Middleware.Middleware.FooMiddleware)
                    .Use(Middleware.Middleware.BarMiddleware)
                    .Use(Middleware.Middleware.BazMiddleware));
        }
    }
}

 

You can see that the entry to the project is the Main method. It only does three things, constructs the WebHostBuilder, then the Build method constructs the WebHost, and finally the Run method starts the WebHost. We can simply understand the role of WebHostBuilder in order to construct WebHost, which is the constructor of WebHost, and WebHost is the host of our web application.

Look at the specific method of CreateWebHostBuilder. First create WebHostBuilder, then UseHttpListener to configure Server (such as Kestrel or IIS in ASP.NET Core, etc.), generally including address and port, etc., and finally register a series of middleware.

From the Program, you can see the flow of the entire App running, as shown below:

200_lines_code_to_complete_Mini_ASP.NET_Core_3.png

Middleware

200_lines_code_to_complete_Mini_ASP.NET_Core_4.png

Before looking at the HttpContext, let’s first look at the ASP.NET Core Http processing pipeline. The above figure is the official pipeline processing diagram. When our server receives an Http request, the pipeline processes it, and then processes it. Return, you can see that our Http request is processed through multiple middleware and finally returned.

using System.Threading.Tasks;
 
namespace App.Middleware
{
    public delegate Task RequestDelegate(HttpContext.HttpContext context);
}

 

First look at RequestDelegate.cs, which defines a parameter type that is HttpContext and returns the result of the task of the Task.

Why define this delegate, you can think of an Http request will be processed through multiple middleware, then multi-level middleware processing can be imagined as an HttpHandler, his parameter is HttpContext, the return result is the task of the Task.

200_lines_code_to_complete_Mini_ASP.NET_Core_5.png

using App.HttpContext;
 
namespace App.Middleware
{
    public static class Middleware
    {
        public static RequestDelegate FooMiddleware(RequestDelegate next)
            => async context =>
            {
                await context.Response.WriteAsync("Foo=>");
                await next(context);
            };
 
        public static RequestDelegate BarMiddleware(RequestDelegate next)
            => async context =>
            {
                await context.Response.WriteAsync("Bar=>");
                await next(context);
            };
 
        public static RequestDelegate BazMiddleware(RequestDelegate next)
            => context => context.Response.WriteAsync("Baz");
    }
}

 

Middleware defines three simple middleware. As you can see, the middleware is actually a delegate, and the HttpContext is processed layer by layer.

The Http request enters the pipeline, the first middleware is processed, and the result is transmitted to the next middleware for processing. The parameter is RequestDelegate, and the return value is RequestDelegate’s delegate is the middleware, so the middleware is actually Func<RequestDelegate, RequestDelegate>, in simple terms, the middleware is the processing factory of RequestDelegate.

200_lines_code_to_complete_Mini_ASP.NET_Core_6.png

HttpContext

According to Middleware, HttpContext is the parameter of RequestDelegate, which is the source of every Middleware processing data.

We can understand that HttpContext is the shared resource in our entire Http request, so the middleware shares it, and each middleware is processing it.

using System;
using System.Collections.Specialized;
using System.IO;
using System.Text;
using System.Threading.Tasks;
 
namespace App.HttpContext
{
    public class HttpContext
    {
        public HttpRequest Request { get; }
        public HttpResponse Response { get; }
 
        public HttpContext(IFeatureCollection features)
        {
            Request = new HttpRequest(features);
            Response = new HttpResponse(features);
        }
    }
 
    public class HttpRequest
    {
        private readonly IHttpRequestFeature _feature;
         
        public Uri Url => _feature.Url;
         
        public NameValueCollection Headers => _feature.Headers;
         
        public Stream Body => _feature.Body;
         
        public HttpRequest(IFeatureCollection features) => _feature = features.Get<IHttpRequestFeature>();
    }
 
    public class HttpResponse
    {
        private readonly IHttpResponseFeature _feature;
        public HttpResponse(IFeatureCollection features) => _feature = features.Get<IHttpResponseFeature>();
         
        public NameValueCollection Headers => _feature.Headers;
        public Stream Body => _feature.Body;
 
        public int StatusCode
        {
            get => _feature.StatusCode;
            set => _feature.StatusCode = value;
        }
    }
 
    public static partial class Extensions
    {
        public static Task WriteAsync(this HttpResponse response, string contents)
        {
            var buffer = Encoding.UTF8.GetBytes(contents);
            return response.Body.WriteAsync(buffer, 0, buffer.Length);
        }
    }
}

 

The code structure can be seen that the request and response constitute httpcontext, and also reflects the responsibilities of httpcontext: the context of the Http request.

200_lines_code_to_complete_Mini_ASP.NET_Core_7.png

However, how do you need to adapt between different Servers and a single HttpContext? Because we can register a variety of Server, it can be IIS or Kestrel or HttpListenerServer here.

So we need to define a uniform request and response interface to adapt to different Servers. IHttpRequestFeature and IHttpResponseFeature as shown below.

using System;
using System.Collections.Specialized;
using System.IO;
 
namespace App.HttpContext
{
    public interface IHttpRequestFeature
    {
        Uri Url { get; }
         
        NameValueCollection Headers { get; }
         
        Stream Body { get; }
    }
 
    public interface IHttpResponseFeature
    {
        int StatusCode { get; set; }
         
        NameValueCollection Headers { get; }
         
        Stream Body { get; }
    }
}

 

Implement the request and response interfaces in HttpListenerFeature.cs, and implement different adapters.

using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
 
namespace App.HttpContext
{
    public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature
    {
        private readonly HttpListenerContext _context;
 
        public HttpListenerFeature(HttpListenerContext context) => _context = context;
 
        Uri IHttpRequestFeature.Url => _context.Request.Url;
 
        NameValueCollection IHttpRequestFeature.Headers => _context.Request.Headers;
 
        NameValueCollection IHttpResponseFeature.Headers => _context.Response.Headers;
 
        Stream IHttpRequestFeature.Body => _context.Request.InputStream;
 
        Stream IHttpResponseFeature.Body => _context.Response.OutputStream;
 
        int IHttpResponseFeature.StatusCode
        {
            get => _context.Response.StatusCode;
            set => _context.Response.StatusCode = value;
        }
    }
}

 

As for FeatureCollection.cs, its role is to store the Http information obtained from the httpListenerContext in the Dictionary of the FeatureCollection, which is more convenient for HttpRequestFeature and HttpResponseFeature.

The extension methods Get and Set are useful for manipulating the FeatureCollection.

using System;
using System.Collections.Generic;
 
namespace App.HttpContext
{
    public interface IFeatureCollection : IDictionary<Type, object>
    {
    }
 
    public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection
    {
    }
 
    public static partial class Extensions
    {
        public static T Get<T>(this IFeatureCollection features) =>
            features.TryGetValue(typeof(T), out var value) ? (T) value : default(T);
 
        public static IFeatureCollection Set<T>(this IFeatureCollection features, T feature)
        {
            features[typeof(T)] = feature;
            return features;
        }
    }
}

 

WebHost

using System;
 using System.Collections.Generic;
 using App.Server;
  
 namespace App.WebHost
 {
     public interface IWebHostBuilder
     {
         IWebHostBuilder UseServer(IServer server);
          
         IWebHostBuilder Configure(Action<IApplicationBuilder> configure);
          
         IWebHost Build();
     }
  
     public class WebHostBuilder : IWebHostBuilder
     {
         private IServer _server;
         private readonly List<Action<IApplicationBuilder>> _configures = new List<Action<IApplicationBuilder>>();
  
         public IWebHostBuilder Configure(Action<IApplicationBuilder> configure)
         {
             _configures.Add(configure);
             return this;
         }
  
         public IWebHostBuilder UseServer(IServer server)
         {
             _server = server;
             return this;
         }
  
         public IWebHost Build()
         {
             var builder = new ApplicationBuilder();
             foreach (var configure in _configures)
             {
                 configure(builder);
             }
  
             return new WebHost(_server, builder.Build());
         }
     }
 }

 

WebHost is the host of our app. Through the WebHostBuild construct, three methods are defined in the code.

  • UseServer: Configure server
  • Configure: Register middleware
  • Build: Constructing WebHost
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using App.Middleware;
 
namespace App.WebHost
{
    public interface IApplicationBuilder
    {
        IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
 
        RequestDelegate Build();
    }
 
    public class ApplicationBuilder : IApplicationBuilder
    {
        private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares =
            new List<Func<RequestDelegate, RequestDelegate>>();
 
        public RequestDelegate Build()
        {
            _middlewares.Reverse();
            return httpContext =>
            {
                RequestDelegate next = _ =>
                {
                    _.Response.StatusCode = 404;
                    return Task.CompletedTask;
                };
 
                foreach (var middleware in _middlewares)
                {
                    next = middleware(next);
                }
 
                return next(httpContext);
            };
        }
 
        public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
        {
            _middlewares.Add(middleware);
            return this;
        }
    }
}

 

What does ApplicationBuilder do, the Use method we put the custom middleware into the collection, and the build method is to build the webhost. First, the intermediate key set is inverted in order, then a middleware with a StatusCode of 404 is constructed, then the middleware set is traversed, and finally the constructed pipeline is returned.

If the middleware collection is empty, we return an Http 404 error.

As for why Reverse() is used, the order in which we register middleware is the reverse of the order we need to execute.

using System.Threading.Tasks;
 using App.Middleware;
 using App.Server;
  
 namespace App.WebHost
 {
     public interface IWebHost
     {
         Task Run();
     }
  
     public class WebHost : IWebHost
     {
         private readonly IServer _server;
         private readonly RequestDelegate _handler;
  
         public WebHost(IServer server, RequestDelegate handler)
         {
             _server = server;
             _handler = handler;
         }
  
         public Task Run() => _server.RunAsync(_handler);
     }
 }

 

WebHost only did one thing, running the middleware pipeline processor we constructed on the specified server.

Server

We customize a server, IServer defines a unified interface, HttpListenerServer implements our custom Server

using System;
 using System.Linq;
 using System.Net;
 using System.Threading.Tasks;
 using App.HttpContext;
 using App.Middleware;
 using App.WebHost;
  
 namespace App.Server
 {
     public class HttpListenerServer : IServer
     {
         private readonly HttpListener _httpListener;
         private readonly string[] _urls;
  
         public HttpListenerServer(params string[] urls)
         {
             _httpListener = new HttpListener();
             _urls = urls.Any() ? urls : new[] {"http://localhost:5000/"};
         }
  
         public async Task RunAsync(RequestDelegate handler)
         {
             Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url));
              
             if (!_httpListener.IsListening)
             {
                 _httpListener.Start();
             }
  
             Console.WriteLine("Server started and is listening on: {0}", string.Join(';', _urls));
  
             while (true)
             {
                  var listenerContext = await _httpListener.GetContextAsync();
                 var feature = new HttpListenerFeature(listenerContext);
                 var features = new FeatureCollection()
                     .Set<IHttpRequestFeature>(feature)
                     .Set<IHttpResponseFeature>(feature);
                 var httpContext = new HttpContext.HttpContext(features);
                  
                 await handler(httpContext);
                  
                 listenerContext.Response.Close();
             }
         }
     }
  
     public static class Extensions
     {
         public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls)
             => builder.UseServer(new HttpListenerServer(urls));
     }
 }

 

Use the UseHttpListener extension method to specify the listening address. The default is “http://localhost:5000/”.

The RunAsync method is the Run method of our WebHost. The loop listens to receive and receive requests by calling its GetContextAsync method.

Sumary

After reading this article, you should have a certain understanding of ASP.NET Core. The core is the middleware pipeline. However, the ASP.NET Core source is much more than that. The implementation of each module is more complicated, and there are other essential modules (dependency injection, log system, exception handling, etc.) that require more in-depth learning. I will also record my learning record, and finally come to a complete Http request pipeline diagram.

200_lines_code_to_complete_Mini_ASP.NET_Core_8.png

 

References:
200 lines of code, 7 objects – let you know the essence of the ASP.NET Core framework

Code address: 
GitHub

Orignal link:https://www.cnblogs.com/xiandnc/p/11480735.html