Preface
Artech shared 200 lines of code, 7 objects -- let you understand the essence of the ASP.NET Core framework . The core part of ASP.NET Core framework is described with a minimal simulation framework.
Here's a step-by-step mini-framework.
Second, let's start with a simple piece of code.
This code is very simple. Start the server and listen for local 5000 ports and process requests.
static async Task Main(string[] args) { HttpListener httpListener = new HttpListener(); httpListener.Prefixes.Add("http://localhost:5000/"); httpListener.Start(); while (true) { var context = await httpListener.GetContextAsync(); await context.Response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("hello world")); context.Response.Close(); } }
Now to separate the server from the handle, a simple design architecture comes out:
Pipeline =Server + HttpHandler
The abstraction of three processors
The processor retrieves data from requests and customizes response data.
It can be imagined that our processor's processing method should be as follows:
Task Handle(/*HttpRequest HttpResponse*/);
It can handle requests and responses, and since processing can be synchronous or asynchronous, it returns to Task.
It's easy to think of encapsulating http requests and responses, encapsulating them as a context for the processor to use (which benefits that other data the processor needs can also be encapsulated here for unified use), so start encapsulating HttpContext.
Encapsulating HttpContext
public class HttpRequest { public Uri Url { get; } public NameValueCollection Headers { get; } public Stream Body { get; } } public class HttpResponse { public NameValueCollection Headers { get; } public Stream Body { get; } public int StatusCode { get; set; } } public class HttpContext { public HttpRequest Request { get; set; } public HttpResponse Response { get; set; } }
To support different servers, different servers have to provide HttpContext, which presents a new problem: the adaptation between servers and HttpContext.
The current HttpContext includes HttpRequest and HttpResponse, and the request and response data are provided by the server.
Interfaces can be defined to allow different servers to provide examples of implementing interfaces:
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; } }
To facilitate the management of the adaptation between the server and HttpContext, a set of functions is defined, and instances provided by the server can be found by type.
public interface IFeatureCollection:IDictionary<Type,object> { } public static partial class Extensions { public static T Get<T>(this IFeatureCollection features) { return features.TryGetValue(typeof(T), out var value) ? (T)value : default; } public static IFeatureCollection Set<T>(this IFeatureCollection features,T feature) { features[typeof(T)] = feature; return features; } } public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection { }
Next, modify HttpContext to complete the adaptation
public class HttpContext { public HttpContext(IFeatureCollection features) { Request = new HttpRequest(features); Response = new HttpResponse(features); } public HttpRequest Request { get; set; } public HttpResponse Response { get; set; } } public class HttpRequest { private readonly IHttpRequestFeature _httpRequestFeature; public HttpRequest(IFeatureCollection features) { _httpRequestFeature = features.Get<IHttpRequestFeature>(); } public Uri Url => _httpRequestFeature.Url; public NameValueCollection Headers => _httpRequestFeature.Headers; public Stream Body => _httpRequestFeature.Body; } public class HttpResponse { private readonly IHttpResponseFeature _httpResponseFeature; public HttpResponse(IFeatureCollection features) { _httpResponseFeature = features.Get<IHttpResponseFeature>(); } public int StatusCode { get => _httpResponseFeature.StatusCode; set => _httpResponseFeature.StatusCode = value; } public NameValueCollection Headers => _httpResponseFeature.Headers; public Stream Body => _httpResponseFeature.Body; } public static partial class Extensions { public static Task WriteAsync(this HttpResponse response,string content) { var buffer = Encoding.UTF8.GetBytes(content); return response.Body.WriteAsync(buffer, 0, buffer.Length); } }
Define Processor
Encapsulated, HttpContext, finally you can look back at the processor.
Processor processing should now be as follows:
Task Handle(HttpContext context);
Next is how to define the processor.
There are at least two ways:
1. Define an interface:
public interface IHttpHandler { Task Handle(HttpContext context); }
2. Define a delegate type
public delegate Task RequestDelegate(HttpContext context);
In essence, there is no difference between the two ways. The way of delegation code is more flexible. It does not need to implement an interface, but also conforms to the duck model.
The processor chooses the delegate type.
Define the processor, and then look at the server
Abstraction of Four Servers
The server should have a start method, pass it in to the processor, and execute it.
The server is abstracted as follows:
public interface IServer { Task StartAsync(RequestDelegate handler); }
Define a server of HttpListener to implement IServer. Since the server of HttpListener needs to provide the data needed by HttpContext, define the HttpListener Feature first.
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; } }
Define HttpListener 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 string[] { "http://localhost:5000/" }; } public async Task StartAsync(RequestDelegate handler) { Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url)); _httpListener.Start(); Console.WriteLine($"The server{typeof(HttpListenerServer).Name} Open, start listening:{string.Join(";", _urls)}"); while (true) { var listtenerContext = await _httpListener.GetContextAsync(); var feature = new HttpListenerFeature(listtenerContext); var features = new FeatureCollection() .Set<IHttpRequestFeature>(feature) .Set<IHttpResponseFeature>(feature); var httpContext = new HttpContext(features); await handler(httpContext); listtenerContext.Response.Close(); } } }
Modify Main Method to Run Test
static async Task Main(string[] args) { IServer server = new HttpListenerServer(); async Task fooBar(HttpContext httpContext) { await httpContext.Response.WriteAsync("fooBar"); } await server.StartAsync(fooBar); }
The results are as follows:
So far, the abstraction of server and processor has been completed. Next, just look at the processor. All the processing logic is assembled in one method. The ideal way is to have multiple processors to process, such as processor A, then processor B.
Then you need to manage the connection between multiple processors.
Five Middleware
Definition of Middleware
Suppose there are three processors A,B,C
Framework to achieve: A processor to start processing, A processing after completion, B processor to start processing, B processing after completion, C processor to start processing.
Middleware is introduced to complete the connection of processors.
The function of middleware is very simple:
- Pass in the next processor to execute;
- In the processor in the middleware, remember the next processor to execute.
- Returns the processor in the middleware for use by other middleware.
So middleware should be like this:
/ / pseudo code Processor Middleware (passed in to the next processor to execute) { return processor { // Processor Logic The next processor to execute executes here } }
For example, there are now three middleware, FooMiddleware, BarMiddleware and Baz Middleware, which correspond to processors A, B and C respectively.
Make sure that the processing order of the processor is A - > B - > C
The last Baz Middleware is executed, and the "Completion Processor" is passed back to Processor C.
Processor C is then passed into BarMiddleware, returned to Processor B, and so on.
Pseudo code
var middlewares=new []{FooMiddleware,BarMiddleware,BazMiddleware}; middlewares.Reverse(); var next=Completed Processor; foreach(var middleware in middlewares) { next= middleware(next); } //The final next is the processor that will eventually be passed into IServer.
Pseudo-code simulating runtime:
// Input Completion Processor, Return Processor C Processor Baz Middleware { return Processor C { // Processing Code of Processor C Complete Processor }; } // Input processor C, return processor B Processor BarMiddleware (Processor C) { return Processor B { // Processing code of processor B Execution Processor C }; } // Input processor B, return processor A Processor FooMiddleware (Processor B) { return processor A { // Processing Code of Processor A Execution Processor B }; }
So when processor A executes, it executes its own code first, then processor B. When processor B executes, it executes its own code first, then processor C, and so on.
Therefore, the method of middleware should be as follows:
RequestDelegate DoMiddleware(RequestDelegate next);
Management of Middleware
To manage middleware, we need to provide the method of registering middleware and finally build the method of Request Delegate.
Define the interface for managing middleware and building processors: IApplication Builder
public interface IApplicationBuilder { IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware); RequestDelegate Build(); }
Realization:
public class ApplicationBuilder : IApplicationBuilder { private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>(); public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware) { _middlewares.Add(middleware); return this; } public RequestDelegate Build() { _middlewares.Reverse(); RequestDelegate next = context => { context.Response.StatusCode = 404; return Task.CompletedTask; }; foreach (var middleware in _middlewares) { next = middleware(next); } return next; } }
Define middleware testing
Define three Middleware in the Program class:
static RequestDelegate FooMiddleware(RequestDelegate next) { return async context => { await context.Response.WriteAsync("foo=>"); await next(context); }; } static RequestDelegate BarMiddleware(RequestDelegate next) { return async context => { await context.Response.WriteAsync("bar=>"); await next(context); }; } static RequestDelegate BazMiddleware(RequestDelegate next) { return async context => { await context.Response.WriteAsync("baz=>"); await next(context); }; }
Modify Main Method Test Run
static async Task Main(string[] args) { IServer server = new HttpListenerServer(); var handler = new ApplicationBuilder() .Use(FooMiddleware) .Use(BarMiddleware) .Use(BazMiddleware) .Build(); await server.StartAsync(handler); }
The results are as follows:
[image-711f8f-1557460509718-1)]
Six Management Servers and Processors
Abstracting web hosts for managing the relationship between servers and processors
As follows:
public interface IWebHost { Task StartAsync(); } 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() { return _server.StartAsync(_handler); } }
Main method can change the test
static async Task Main(string[] args) { IServer server = new HttpListenerServer(); var handler = new ApplicationBuilder() .Use(FooMiddleware) .Use(BarMiddleware) .Use(BazMiddleware) .Build(); IWebHost webHost = new WebHost(server, handler); await webHost.StartAsync(); }
To build WebHost, you need to know which server to use and which middleware to configure. Finally, you can build WebHost.
The code is as follows:
public interface IWebHostBuilder { IWebHostBuilder UseServer(IServer server); IWebHostBuilder Configure(Action<IApplicationBuilder> configure); IWebHost Build(); } public class WebHostBuilder : IWebHostBuilder { private readonly List<Action<IApplicationBuilder>> _configures = new List<Action<IApplicationBuilder>>(); private IServer _server; public IWebHost Build() { //All middleware is registered on builder var builder = new ApplicationBuilder(); foreach (var config in _configures) { config(builder); } return new WebHost(_server, builder.Build()); } public IWebHostBuilder Configure(Action<IApplicationBuilder> configure) { _configures.Add(configure); return this; } public IWebHostBuilder UseServer(IServer server) { _server = server; return this; } }
Add an extension method to IWebHostBuilder to use the HttpListener Server server
public static partial class Extensions { public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls) { return builder.UseServer(new HttpListenerServer(urls)); } }
Modifying the Mian method
static async Task Main(string[] args) { await new WebHostBuilder() .UseHttpListener() .Configure(app=> app.Use(FooMiddleware) .Use(BarMiddleware) .Use(BazMiddleware)) .Build() .StartAsync(); }
Done.
Add a UseMiddleware extension to play
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder application, Type type) { //Ellipsis realization } public static IApplicationBuilder UseMiddleware<T>(this IApplicationBuilder application) where T : class { return application.UseMiddleware(typeof(T)); }
Add a middleware
public class QuxMiddleware { private readonly RequestDelegate _next; public QuxMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { await context.Response.WriteAsync("qux=>"); await _next(context); } } public static partial class Extensions { public static IApplicationBuilder UseQux(this IApplicationBuilder builder) { return builder.UseMiddleware<QuxMiddleware>(); } }
Using Middleware
class Program { static async Task Main(string[] args) { await new WebHostBuilder() .UseHttpListener() .Configure(app=> app.Use(FooMiddleware) .Use(BarMiddleware) .Use(BazMiddleware) .UseQux()) .Build() .StartAsync(); }
Operation result
[Image-fb21e3-1557460509718-0]