1. Talking from Hello World

When we first came to learn from a Hello World when we first learned a technology, it seemed to be less than the theme of our article. But this is not the case. In our opinion, the following Hello World is the best embodiment of the ASP.NET Core framework.

Public  class Program
{
    Public  static  void Main()
     => new WebHostBuilder()
        .UseKestrel()
        .Configure(app => app.Run(context => context.Response.WriteAsync( " Hello World! " )))
        .Build()
        .Run();
}

Although the Hello World program is artificially divided into several lines, the entire application actually has only one statement. This statement involves two core objects WebHost and WebHostBuilder in the ASP.NET Core program . We can be understood as a boarding or carrying WebHost Web application host , start the application can be achieved by starting WebHost as host. As for WebHostBuilder, as the name suggests, is the builder of WebHost.

Before calling WebHostBuilder’s Build method to create a WebHost, we call its two methods, where UseKestrel is designed to register a servernamed Kestrel , and the Configure method is called to register a middleware to process the request . The latter writes a “Hello World” text in the body of the response.

When we call the Run method to start the WebHost as the application host, the latter will build a request processing pipeline using the server and middleware provided by WebHostBuilder . This pipeline consisting of a server and several middleware is the core of the ASP.NET Core framework. Our next core task is to let everyone figure out how this pipeline is built and what kind of request processing flow the pipeline uses.

2, ASP.NET Core Mini

In the past few years, I have been constantly asked the same question: how to go deep into a development framework. I know that everyone has a learning style that suits them, and I feel that my personal learning methods are not efficient, so I rarely respond positively to this question. However, there is a way I would like to share with you, that is, when you are learning a development framework, don’t just focus on the programming level, but focus more on learning at the architectural design level.

For a framework, it provides a complex programming model, and the underlying design principles are simple and straightforward. So how to test our design principles of the framework is thorough, I think the best way is to ” recreate ” the framework according to your understanding . When you “rebuild” the framework in your way, you will find a lot of missing things. If the framework you are rebuilding supports a working Hello World application, you can basically prove that you have a basic understanding of the most essential of the framework.

Although ASP.NET Core is currently an open source project, we can learn it entirely from source code, but I believe this is difficult for most people. To this end, we extracted the most essential and core parts of ASP.NET Core and rebuilt a mini version of the ASP.NET Core framework.

The ASP.NET Core Mini has three major features as shown above. First, it is a real simulation of the real ASP.NET Core framework , so we have made the maximum simplification in the definition of some APIs, but the essence of the two is completely consistent. If you can understand the ASP.NET Core Mini, it means that you understand the real ASP.NET Core framework. Second, this framework is executable , and we are not providing pseudo-code. Third, in order to allow everyone to understand the essence of the ASP.NET Core framework in the shortest possible time, the ASP.NET Core Mini must be simple enough , so the core code of our entire implementation will not exceed 200 lines.

3, Hello World 2

Since our ASP.NET Core Mini is executable, it means we can build our own application on it. The following is the Hello World developed on the ASP.NET Core Mini. It can be seen that it is true and true. A consistent programming model for the ASP.NET Core framework.

Public  class Program
{
    Public  static  async Task Main()
    {
        the await  new new WebHostBuilder ()
            .UseHttpListener()
            .Configure(app => app
                .Use(FooMiddleware)
                .Use(BarMiddleware)
                .Use(BazMiddleware))
            .Build()
            .StartAsync();
    }

    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 " );
}

We need to make a short answer to the above Hello World program: After creating WebHostBuilder, we call its extension method UseHttpListener to register a custom HttpListener-based server, we will introduce the server in the following content. Implementation. In the subsequent call to the Configure method, we registered three middleware. Since the middleware is ultimately represented by the Delegate object, we can define the middleware as a method with the same signature as the Delegate type.

We can now do not need to consider why the three methods of representing middleware need to be in the above form. We only need to know what the three middleware are doing in the processing flow for the request. The above code is very clear, the three middleware will write a paragraph of text in the response content, so after the program runs, if we use the browser to access the application, we will get the output as shown below.

4, the first object: HttpContext

As stated in this article, our ASP.NET Core Mini is built from seven core objects. The first one is the HttpContext object that everyone is very familiar with. It can be said to be the most frequently used object in ASP.NET Core application development. To illustrate the nature of HttpContext, you have to deal with the level of request processing pipeline. For a pipeline built by one server and multiple middleware, the transport layer-oriented server is responsible for the listening, receiving, and final response of the request. When it receives the request sent by the client, it needs to distribute it to the subsequent middleware. Process it. For a middleware, after we have completed our own request processing tasks, in most cases we need to distribute the request to subsequent middleware. The request for distribution between the server and the middleware, and between the middleware is achieved by sharing the context .

As shown in the figure above, when the server receives the request, it creates a context object represented by HttpContext. All middleware processes the request in this context. So what kind of context information does an HttpContext object carry? We know that an HTTP transaction has a very clear definition, that is, receiving a request and sending a response, so the request and response are two basic elements, and are the core context information of the HttpContext.

We can understand the request as input and response as output, so the application can use HttpContext to get all the input information of the current request, and can also use it to complete all the output work we need. To this end we have defined this minimalist version of HttpContext for ASP.NET Core Mini.

Public  class HttpContext
{           
    Public   HttpRequest Request { get ; }
     public   HttpResponse Response { get ; }
}
Public  class HttpRequest
{
    Public   Uri Url { get ; }
     public   NameValueCollection Headers { get ; }
     public   Stream Body { get ; }
}
Public  class HttpResponse
{
    public   the NameValueCollection Headers { GET ;}
     public   Stream Body { GET ;}
     public  int the StatusCode { GET ; SET ;}
}

As shown in the code snippet above, HttpContext represents requests and responses through its two properties, Request and Response. The corresponding types are HttpRequest and HttpResponse. Through the former, we can get the requested address, hand collection and body content. With the latter, we can set the response status code, and also set the header and body content.

5, the second object: RequestDelegate

RequestDelegate is the second core object we introduced. We can see from the naming that this is a delegate (Delegate) object. Just like the HttpContext introduced above, we can only fully understand the essence of this delegate object from the perspective of the pipeline.

In the software industry for more than 10 years, I have more and more understanding of the software architecture design: good design must be a ” simple ” design. So whenever I design a development framework, I will always tell myself: ” Can you be simpler? “. The design of the ASP.NET Core pipeline we introduced above has a “simple” trait: Pipeline = Server + Middlewares . But “Can you be simpler?”, in fact, it is ok: we can build multiple Middleware into a single “HttpHandler”, then the entire ASP.NET Core framework will have a simpler expression: Pipeline = Server + HttpHandler .

So how do we express HttpHandler? We can think like this: Since all input and output for the current request are represented by HttpContext, the HttpHandler can be represented as an Action<HttpContext> object. So HttpHandler is represented in ASP.NET Core by Action<HttpContext>? Actually not, the reason is very simple: Action<HttpContext> can only represent the ” synchronous ” processing operation for the request, but the HTTP request can be either synchronous or asynchronous , and more asynchronous.

So how do you represent a synchronous or asynchronous operation in the world of .NET Core? You should think about it, that is the Task object, then HttpHandler can naturally be represented as a Func<HttpContext, Task> object. Since this delegate object is really important, we define it as a separate type.

6, the third object: Middleware

After fully understanding the RequestDelegate delegate object, let’s talk about how the middleware is expressed. This is the third core object we introduced. The middleware is represented in ASP.NET Core as a Func<RequestDelegate, RequestDelegate> object, which means that its input and output are both a RequestDelegate .

It is difficult for many beginners to understand why a Func<RequestDelegate, RequestDelegate> object is used to represent middleware. We can think about this: for a middleware in the pipeline, the pipeline consisting of subsequent middleware is embodied as a RequestDelegate object. Since the current middleware completes its own request processing task, it often needs to distribute the request. middleware to the subsequent process, it requires all of its RequestDelegate consisting subsequent intermediate as an input .

After the delegate object representing the middleware is executed, we want to “incorporate” the current middleware into the pipeline, and the RequestDelegate embodied by the new pipeline naturally becomes the output . So the middleware is naturally represented as the Func<RequestDelegate, RequestDelegate> object whose input and output are both RequestDelegate.

7, the fourth object: ApplicationBuilder

ApplicationBuilder is the fourth core object we know. From the naming point of view, this is the second Builder we came into contact with. Since it is named ApplicationBuilder, it means that it is an Application . So what is the application of the application under the semantics of the ASP.NET Core framework?

For this problem, we can understand this: Since Pipeline = Server + HttpHandler, then the HttpHandler used to process the request does not carry all the responsibilities of the current application? Then HttpHandler is equal to Application . Since HttpHandler is represented by RequestDelegate, the Application built by ApplicationBuilder is a RequestDelegate object.

 

Since the RequestDelegate representing the HttpHandler is built from registered middleware, the ApplicationBuilder also has the ability to register middleware . Based on these two basic responsibilities of ApplicationBuilder, we can define the corresponding interface as follows. The Use method is used to register the provided middleware, and the Build method builds the registered middleware into a RequestDelegate object.

Public  interface   IApplicationBuilder
{
    IApplicationBuilder Use(Func <RequestDelegate, RequestDelegate> middleware);
    RequestDelegate Build();
}

The following is a specific implementation for this interface. We use a list to hold the registered middleware, so the Use method only needs to add the provided middleware to this list. After the Build method is called, we only need to execute the Func<RequestDelegate, RequestDelegate> object representing the middleware in the reverse order of registration to finally construct the RequestDelegate object representing the HttpHandler.

Public  class ApplicationBuilder : IApplicationBuilder
{
    Private  Readonly List <Func <RequestDelegate, RequestDelegate >> _middlewares = new new List <Func <RequestDelegate, RequestDelegate >> ();
     public RequestDelegate the Build ()
    {
        _middlewares.Reverse ();
         return httpContext =>
        {
            Next RequestDelegate = _ => {_.Response.StatusCode = 404 ; return Task.CompletedTask;}; 
            the foreach ( var Middleware in _middlewares)
            {
                Next = middleware(next);
            }
            Return next(httpContext);
        };
    }

    Public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
    {
        _middlewares.Add(middleware);
        Return  this ;
    }
}

When calling the first middleware (last registered), we created a RequestDelegate as input, which sets the response status code to 404 . So if the ASP.NET Core application does not register any intermediate, it will always return a 404 response. If all of the middleware chooses to distribute the request backwards after completing its own request processing task, it will also return a 404 response.

8, the fifth object: Server

The role of the server in the pipeline is very clear, when we automatically make the WebHost of the application host, the service is automatically started. The started server will bind to the specified port for request interception. Once a request arrives, the server will create an HttpContext object representing the context according to the request, and use the context as input to call the RequestDelegate constructed by all the registered middleware. Object.

For simplicity, we use the shorthand IServer interface to represent the server. We start the server by the only method defined in the IServer interface, StartAsync. The handler as a parameter is the RequestDelegate object that is built by all the registered middleware.

Public  interface IServer
{ 
    Task StartAsync(RequestDelegate handler);
}

9, the adaptation between HttpContext and Server

The application-oriented HttpContext object is a wrapper around the request and response, but the request originally originated from the server, and any response to the HttpContext must also be applied to the current server to actually work. Now the problem is, all ASP.NET Core applications use the same HttpContext type, but can register different types of servers, we must solve the adaptation problem between the two.

There is a very classic phrase in the computer field: “Any problem can be solved by adding an abstraction layer. If it can’t be solved, add another layer.” The problem of adaptation between the same HttpContext type and different server types can also be solved by adding an abstraction layer . The object we define at this layer is called Feature . As shown in the figure above, we can define a series of Feature interfaces to provide context information for the HttpContext, the most important of which is to provide the requested IRequestFeature and the IResponseFeature interface to complete the response . Then the specific server only needs to implement these Feature interfaces.

We then look at the concrete implementation from the code level. As shown in the code snippet below, we define an IFeatureCollection interface to represent a collection of Feature objects. It can be seen from the definition that this is a dictionary with Type and Object as Key and Value, Key represents the type used to register the Feature, and Value naturally represents the Feature object itself. In other words, the Feature object we provide is ultimately the corresponding Feature. The type (usually the interface type) is registered. For programming convenience, we have defined two extension methods, Set<T> and Get<T>, to set and get the Feature object.

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;
    }
}

The following definitions are used to provide the request and response IHttpRequestFeature and IHttpResponseFeature interfaces. It can be seen that they have the same member definitions as HttpRequest and HttpResponse.

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 ; }
}

Next, let’s take a look at the concrete implementation of HttpContext. ASP.NET Core Mini’s HttpContext only contains two property members, Request and Response. The corresponding types are HttpRequest and HttpResponse. The following are the concrete implementations of these two types. We can see that both HttpRequest and HttpResponse are built from an IFeatureCollection object, and their corresponding attribute members are provided by the IHttpRequestFeature and IHttpResponseFeature objects contained in this Feature collection.

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   NameValueCollection Headers => _feature.Headers;
     public   Stream Body => _feature.Body;
     public  int StatusCode { get => _feature.StatusCode; set => _feature.StatusCode = value; }

    Public HttpResponse(IFeatureCollection features) => _feature = features.Get<IHttpResponseFeature> ();

}

The implementation of HttpContext is much simpler. As shown in the following code snippet, we will also provide an IFeatureCollection object when creating an HttpContext object. We use this object to create the corresponding HttpRequest and HttpResponse objects and use them as the corresponding property values.

Public  class HttpContext
{           
    Public   HttpRequest Request { get ; }
     public   HttpResponse Response { get ; }

    Public HttpContext(IFeatureCollection features)
    {
        Request = new HttpRequest(features);
        Response = new HttpResponse(features);
    }
}

10, HttpListenerServer

After having a clear understanding of the server and its adaptation to HttpContext, let’s try to define a server ourselves. In the previous Hello World instance, we used the WebHostBuilder extension method UseHttpListener to register an HttpListenerServer . Let’s take a look at how this server type using HttpListener as a listener is implemented.

Since all servers need to automatically implement their own Feature implementation to provide corresponding context information for the HttpContext, we must first define the corresponding interface for the HttpListenerServer. A friend who knows a little about HttpListener should know that it will create its own context object after receiving the request, the corresponding type is HttpListenerContext . If HttpListenerServer is used as the application server, it means that the context information carried by HttpContext is originally derived from this HttpListenerContext, so the purpose of Feature is to solve the adaptation problem between these two contexts.

The HttpListenerFeature shown below is the feature we defined for HttpListenerServer. HttpListenerFeature implements both IHttpRequestFeature and IHttpResponseFeature. The six property members implemented are originally derived from the HttpListenerContext object provided by the Feature object.

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; }
}

The final definition of HttpListenerServer is shown below. We can provide a set of listener addresses when constructing an HttpListenerServer object. If not provided, we will use “localhost:5000” as the default listener address. In the implementation of the StartAsync method, we started the HttpListenerServer object created in the constructor, and in a loop by calling its GetContextAsync method to achieve the listen and receive for the request.

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  string [] { " http://localhost:5000/ " };
    }

    Public  async Task StartAsync(RequestDelegate handler)
    {
        Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url));    
        _httpListener.Start();
        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(features);
             await handler(httpContext);
            listenerContext.Response.Close();
        }
    }
}

When the HttpListener listens for the incoming request, we get an HttpListenerContext object. At this point we only need to create an HttpListenerFeature object and register it with the IHttpRequestFeature and IHttpResponseFeature interface types to create the FeatureCollection collection. We finally use this FeatureCollection object to create an HttpContext that represents the context, and then call it as a parameter to call the RequestDelegate object built by all the middleware.

11, the sixth object: WebHost

So far we have seen how a pipeline consisting of one server and multiple middleware can completely monitor, receive, process, and ultimately respond to requests. Let’s discuss how such a pipeline is built. The pipeline is built when the WebHost object is launched as the application host. In the ASP.NET Core Mini, we will instantiate the application host’s IWebHost interface as follows: Only one StartAsync method is used to start the application.

Public  interface IWebHost
{
    Task StartAsync();
}

Since the pipeline built by WebHost consists of Server and HttpHandler, we provide two objects directly in the WebHost type of the default implementation. In the implementation of the StartAsync method, we only need to use the latter as a parameter to call the former StartAsync method to start the server.

Public  class WebHost : IWebHost
{
    Private  readonly IServer _server;
     private  readonly RequestDelegate _handler; 
     public WebHost(IServer server, RequestDelegate handler)
    {
        _server = server;
        _handler = handler;
    } 
    Public Task StartAsync() => _server.StartAsync(_handler);
}

12, the seventh object: WebHostBuilder

As the last core object highlighted, WebHostBuilder’s mission is very clear: to create a WebHost as an application host. Since we need to provide a registered server and a RequestDelegate built by all registered middleware when creating WebHost, we define three core methods for it in the corresponding interface IWebHostBuilder.

Public  interface IWebHostBuilder
{
    IWebHostBuilder UseServer(IServer server);
    IWebHostBuilder Configure(Action <IApplicationBuilder> configure);
    IWebHost Build();
}

In addition to the Build method used to create the WebHost, we provide the UseServer method for registering the server and the Configure method for registering the middleware. The Configure method provides a parameter of type Action<IApplicationBuilder>, which means that our registration for middleware is done using the IApplicationBuilder object described above.

The WebHostBuilder shown below is the default implementation for the IWebHostBuilder interface. It has two fields for saving the registered middleware and calling the Action<IApplicationBuilder> object provided by the Configure method. When the Build method is called, we create an ApplicationBuilder object and call it the Action<IApplicationBuilder> delegate as a parameter, registering all the middleware to the ApplicationBuilder object. We finally call its Build method to get the RequestDelegate object built by all the middleware, and use it to register the WebHost object as the application host with the registered server.

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());
    }
}

13. Review Hello World 2

So far, we have introduced the seven core objects involved in ASP.NET Core Mini, and then we will review the Hello World program built on this simulation framework.

Public  class Program
{
    Public  static  async Task Main()
    {
        the await  new new WebHostBuilder ()
            .UseHttpListener()
            .Configure(app => app
                .Use(FooMiddleware)
                .Use(BarMiddleware)
                .Use(BazMiddleware))
            .Build()
            .StartAsync();
    }

    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 " );
}

First, we call WebHostBuilder’s extension method UseHttpListener to complete the registration for HttpListenerServer in the following way. Since the middleware is embodied as a Func<RequestDelegate, RequestDelegate> object, we can naturally define the corresponding middleware using the same declared methods (FooMiddleware, BarMiddleware, and BazMiddleware). The middleware calls HttpResponse’s WriteAsync to write the specified string to the output stream of the response body as follows.

Public  static  partial  class Extensions
{
   Public  static IWebHostBuilder UseHttpListener( this IWebHostBuilder builder, params  string [] urls)
     => builder.UseServer( new HttpListenerServer(urls));

    Public  static Task WriteAsync( this HttpResponse response, string contents)
    {
        Var buffer = Encoding.UTF8.GetBytes(contents);
         return response.Body.WriteAsync(buffer, 0 , buffer.Length);
     }
}