写一个简版 asp.net core
動手寫一個簡版 asp.net core
Intro
之前看到過蔣金楠老師的一篇 200 行代碼帶你了解 asp.net core 框架,最近參考蔣老師和 Edison 的文章和代碼,結合自己對 asp.net core 的理解 ,最近自己寫了一個 MiniAspNetCore ,寫篇文章總結一下。
HttpContext
HttpContext?可能是最為常用的一個類了,?HttpContext?是請求上下文,包含了所有的請求信息以及響應信息,以及一些自定義的用于在不同中間件中傳輸數據的信息
來看一下?HttpContext?的定義:
public class HttpContext {public IServiceProvider RequestServices { get; set; }public HttpRequest Request { get; set; }public HttpResponse Response { get; set; }public IFeatureCollection Features { get; set; }public HttpContext(IFeatureCollection featureCollection){Features = featureCollection;Request = new HttpRequest(featureCollection);Response = new HttpResponse(featureCollection);} }HttpRequest?即為請求信息對象,包含了所有請求相關的信息,
HttpResponse?為響應信息對象,包含了請求對應的響應信息
RequestServices?為 asp.net core 里的?RequestServices,代表當前請求的服務提供者,可以使用它來獲取具體的服務實例
Features?為 asp.net core 里引入的對象,可以用來在不同中間件中傳遞信息和用來解耦合
,下面我們就來看下?HttpRequest?和?HttpResponse?是怎么實現的
HttpRequest:
public class HttpRequest {private readonly IRequestFeature _requestFeature;public HttpRequest(IFeatureCollection featureCollection){_requestFeature = featureCollection.Get<IRequestFeature>();}public Uri Url => _requestFeature.Url;public NameValueCollection Headers => _requestFeature.Headers;public string Method => _requestFeature.Method;public string Host => _requestFeature.Url.Host;public Stream Body => _requestFeature.Body; }HttpResponse:
public class HttpResponse {private readonly IResponseFeature _responseFeature;public HttpResponse(IFeatureCollection featureCollection){_responseFeature = featureCollection.Get<IResponseFeature>();}public bool ResponseStarted => _responseFeature.Body.Length > 0;public int StatusCode{get => _responseFeature.StatusCode;set => _responseFeature.StatusCode = value;}public async Task WriteAsync(byte[] responseBytes){if (_responseFeature.StatusCode <= 0){_responseFeature.StatusCode = 200;}if (responseBytes != null && responseBytes.Length > 0){await _responseFeature.Body.WriteAsync(responseBytes);}} }Features
上面我們提供我們可以使用?Features?在不同中間件中傳遞信息和解耦合
由上面?HttpRequest/?HttpResponse?的代碼我們可以看出來,?HttpRequest?和?HttpResponse?其實就是在?IRequestFeature?和?IResponseFeature?的基礎上封裝了一層,真正的核心其實是?IRequestFeature/?IResponseFeature?,而這里使用接口就很好的實現了解耦,可以根據不同的 WebServer 使用不同的?RequestFeature/?ResponseFeature,來看下?IRequestFeature/?IResponseFeature?的實現
public interface IRequestFeature {Uri Url { get; }string Method { get; }NameValueCollection Headers { get; }Stream Body { get; } } public interface IResponseFeature {public int StatusCode { get; set; }NameValueCollection Headers { get; set; }public Stream Body { get; } }這里的實現和 asp.net core 的實際的實現方式應該不同,asp.net core 里 Headers 同一個 Header 允許有多個值,asp.net core 里是 StringValues 來實現的,這里簡單處理了,使用了一個?NameValueCollection?對象
上面提到的?Features?是一個?IFeatureCollection?對象,相當于是一系列的?Feature?對象組成的,來看下?FeatureCollection?的定義:
public interface IFeatureCollection : IDictionary<Type, object> { } public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection { }這里?IFeatureCollection?直接實現?IDictionary<Type,object>?,通過一個字典 Feature 類型為 Key,Feature 對象為 Value 的字典來保存
為了方便使用,可以定義兩個擴展方法來方便的Get/Set
public static class FeatureExtensions {public static IFeatureCollection Set<TFeature>(this IFeatureCollection featureCollection, TFeature feature){featureCollection[typeof(TFeature)] = feature;return featureCollection;}public static TFeature Get<TFeature>(this IFeatureCollection featureCollection){var featureType = typeof(TFeature);return featureCollection.ContainsKey(featureType) ? (TFeature)featureCollection[featureType] : default(TFeature);} }Web服務器
上面我們已經提到了 Web 服務器通過?IRequestFeature/?IResponseFeature?來實現不同 web 服務器和應用程序的解耦,web 服務器只需要提供自己的?RequestFeature/?ResponseFeature?即可
為了抽象不同的 Web 服務器,我們需要定義一個?IServer?的抽象接口,定義如下:
public interface IServer {Task StartAsync(Func<HttpContext, Task> requestHandler, CancellationToken cancellationToken = default); }IServer?定義了一個?StartAsync?方法,用來啟動 Web服務器,
StartAsync?方法有兩個參數,一個是 requestHandler,是一個用來處理請求的委托,另一個是取消令牌用來停止 web 服務器
示例使用了?HttpListener?來實現了一個簡單 Web 服務器,?HttpListenerServer?定義如下:
public class HttpListenerServer : IServer {private readonly HttpListener _listener;private readonly IServiceProvider _serviceProvider;public HttpListenerServer(IServiceProvider serviceProvider, IConfiguration configuration){_listener = new HttpListener();var urls = configuration.GetAppSetting("ASPNETCORE_URLS")?.Split(';');if (urls != null && urls.Length > 0){foreach (var url in urls.Where(u => u.IsNotNullOrEmpty()).Select(u => u.Trim()).Distinct()){// Prefixes must end in a forward slash ("/")// https://stackoverflow.com/questions/26157475/use-of-httplistener_listener.Prefixes.Add(url.EndsWith("/") ? url : $"{url}/");}}else{_listener.Prefixes.Add("http://localhost:5100/");}_serviceProvider = serviceProvider;}public async Task StartAsync(Func<HttpContext, Task> requestHandler, CancellationToken cancellationToken = default){_listener.Start();if (_listener.IsListening){Console.WriteLine("the server is listening on ");Console.WriteLine(_listener.Prefixes.StringJoin(","));}while (!cancellationToken.IsCancellationRequested){var listenerContext = await _listener.GetContextAsync();var featureCollection = new FeatureCollection();featureCollection.Set(listenerContext.GetRequestFeature());featureCollection.Set(listenerContext.GetResponseFeature());using (var scope = _serviceProvider.CreateScope()){var httpContext = new HttpContext(featureCollection){RequestServices = scope.ServiceProvider,};await requestHandler(httpContext);}listenerContext.Response.Close();}_listener.Stop();} }HttpListenerServer?實現的?RequestFeature/?ResponseFeatue
public class HttpListenerRequestFeature : IRequestFeature {private readonly HttpListenerRequest _request;public HttpListenerRequestFeature(HttpListenerContext listenerContext){_request = listenerContext.Request;}public Uri Url => _request.Url;public string Method => _request.HttpMethod;public NameValueCollection Headers => _request.Headers;public Stream Body => _request.InputStream; } public class HttpListenerResponseFeature : IResponseFeature {private readonly HttpListenerResponse _response;public HttpListenerResponseFeature(HttpListenerContext httpListenerContext){_response = httpListenerContext.Response;}public int StatusCode { get => _response.StatusCode; set => _response.StatusCode = value; }public NameValueCollection Headers{get => _response.Headers;set{_response.Headers = new WebHeaderCollection();foreach (var key in value.AllKeys)_response.Headers.Add(key, value[key]);}}public Stream Body => _response.OutputStream; }為了方便使用,為?HttpListenerContext?定義了兩個擴展方法,就是上面?HttpListenerServer?中的?GetRequestFeature/?GetResponseFeature:
public static class HttpListenerContextExtensions {public static IRequestFeature GetRequestFeature(this HttpListenerContext context){return new HttpListenerRequestFeature(context);}public static IResponseFeature GetResponseFeature(this HttpListenerContext context){return new HttpListenerResponseFeature(context);} }RequestDelegate
在上面的?IServer?定義里有一個 requestHandler 的 對象,在 asp.net core 里是一個名稱為?RequestDelegate?的對象,而用來構建這個委托的在 asp.net core 里是?IApplicationBuilder,這些在蔣老師和 Edison 的文章和代碼里都可以看到,這里我們只是簡單介紹下,我在 MiniAspNetCore 的示例中沒有使用這些對象,而是使用了自己抽象的?PipelineBuilder?和原始委托實現的
asp.net core 里?RequestDelegate?定義:
public delegate Task RequestDelegate(HttpContext context);其實和我們上面定義用的?Func<HttpContext,Task>?是等價的
IApplicationBuilder?定義:
/// <summary> /// Defines a class that provides the mechanisms to configure an application's request pipeline. /// </summary> public interface IApplicationBuilder {/// <summary>/// Gets or sets the <see cref="T:System.IServiceProvider" /> that provides access to the application's service container./// </summary>IServiceProvider ApplicationServices { get; set; }/// <summary>/// Gets the set of HTTP features the application's server provides./// </summary>IFeatureCollection ServerFeatures { get; }/// <summary>/// Gets a key/value collection that can be used to share data between middleware./// </summary>IDictionary<string, object> Properties { get; }/// <summary>/// Adds a middleware delegate to the application's request pipeline./// </summary>/// <param name="middleware">The middleware delegate.</param>/// <returns>The <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" />.</returns>IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);/// <summary>/// Creates a new <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" /> that shares the <see cref="P:Microsoft.AspNetCore.Builder.IApplicationBuilder.Properties" /> of this/// <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" />./// </summary>/// <returns>The new <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" />.</returns>IApplicationBuilder New();/// <summary>/// Builds the delegate used by this application to process HTTP requests./// </summary>/// <returns>The request handling delegate.</returns>RequestDelegate Build(); }我們這里沒有定義?IApplicationBuilder,使用了簡化抽象的?IAsyncPipelineBuilder,定義如下:
public interface IAsyncPipelineBuilder<TContext> {IAsyncPipelineBuilder<TContext> Use(Func<Func<TContext, Task>, Func<TContext, Task>> middleware);Func<TContext, Task> Build();IAsyncPipelineBuilder<TContext> New(); }對于 asp.net core 的中間件來說 ,上面的?TContext?就是?HttpContext,替換之后也就是下面這樣的:
public interface IAsyncPipelineBuilder<HttpContext> {IAsyncPipelineBuilder<HttpContext> Use(Func<Func<HttpContext, Task>, Func<HttpContext, Task>> middleware);Func<HttpContext, Task> Build();IAsyncPipelineBuilder<HttpContext> New(); }是不是和?IApplicationBuilder?很像,如果不像可以進一步把?Func<HttpContext,Task>?使用?RequestDelegate?替換
public interface IAsyncPipelineBuilder<HttpContext> {IAsyncPipelineBuilder<HttpContext> Use(Func<RequestDelegate, RequestDelegate> middleware);RequestDelegate Build();IAsyncPipelineBuilder<HttpContext> New(); }最后再將接口名稱替換一下:
public interface IApplicationBuilder1 {IApplicationBuilder1 Use(Func<RequestDelegate, RequestDelegate> middleware);RequestDelegate Build();IApplicationBuilder1 New(); }至此,就完全可以看出來了,這?IAsyncPipelineBuilder<HttpContext>?就是一個簡版的?IApplicationBuilder
IAsyncPipelineBuilder?和?IApplicationBuilder?的作用是將注冊的多個中間件構建成一個請求處理的委托
中間件處理流程:
更多關于 PipelineBuilder 構建中間件的信息可以查看?讓 .NET 輕松構建中間件模式代碼?了解更多
WebHost
通過除了 Web 服務器之外,還有一個 Web Host 的概念,可以簡單的這樣理解,一個 Web 服務器上可以有多個 Web Host,就像 IIS/nginx (Web Server) 可以 host 多個站點
可以說 WebHost 離我們的應用更近,所以我們還需要?IHost?來托管應用
public interface IHost {Task RunAsync(CancellationToken cancellationToken = default); }WebHost?定義:
public class WebHost : IHost {private readonly Func<HttpContext, Task> _requestDelegate;private readonly IServer _server;public WebHost(IServiceProvider serviceProvider, Func<HttpContext, Task> requestDelegate){_requestDelegate = requestDelegate;_server = serviceProvider.GetRequiredService<IServer>();}public async Task RunAsync(CancellationToken cancellationToken = default){await _server.StartAsync(_requestDelegate, cancellationToken).ConfigureAwait(false);} }為了方便的構建?Host對象,引入了?HostBuilder?來方便的構建一個?Host,定義如下:
public interface IHostBuilder {IHostBuilder ConfigureConfiguration(Action<IConfigurationBuilder> configAction);IHostBuilder ConfigureServices(Action<IConfiguration, IServiceCollection> configureAction);IHostBuilder Initialize(Action<IConfiguration, IServiceProvider> initAction);IHostBuilder ConfigureApplication(Action<IConfiguration, IAsyncPipelineBuilder<HttpContext>> configureAction);IHost Build(); }WebHostBuilder:
public class WebHostBuilder : IHostBuilder {private readonly IConfigurationBuilder _configurationBuilder = new ConfigurationBuilder();private readonly IServiceCollection _serviceCollection = new ServiceCollection();private Action<IConfiguration, IServiceProvider> _initAction = null;private readonly IAsyncPipelineBuilder<HttpContext> _requestPipeline = PipelineBuilder.CreateAsync<HttpContext>(context =>{context.Response.StatusCode = 404;return Task.CompletedTask;});public IHostBuilder ConfigureConfiguration(Action<IConfigurationBuilder> configAction){configAction?.Invoke(_configurationBuilder);return this;}public IHostBuilder ConfigureServices(Action<IConfiguration, IServiceCollection> configureAction){if (null != configureAction){var configuration = _configurationBuilder.Build();configureAction.Invoke(configuration, _serviceCollection);}return this;}public IHostBuilder ConfigureApplication(Action<IConfiguration, IAsyncPipelineBuilder<HttpContext>> configureAction){if (null != configureAction){var configuration = _configurationBuilder.Build();configureAction.Invoke(configuration, _requestPipeline);}return this;}public IHostBuilder Initialize(Action<IConfiguration, IServiceProvider> initAction){if (null != initAction){_initAction = initAction;}return this;}public IHost Build(){var configuration = _configurationBuilder.Build();_serviceCollection.AddSingleton<IConfiguration>(configuration);var serviceProvider = _serviceCollection.BuildServiceProvider();_initAction?.Invoke(configuration, serviceProvider);return new WebHost(serviceProvider, _requestPipeline.Build());}public static WebHostBuilder CreateDefault(string[] args){var webHostBuilder = new WebHostBuilder();webHostBuilder.ConfigureConfiguration(builder => builder.AddJsonFile("appsettings.json", true, true)).UseHttpListenerServer();return webHostBuilder;} }這里的示例我在?IHostBuilder?里增加了一個?Initialize?的方法來做一些初始化的操作,我覺得有些數據初始化配置初始化等操作應該在這里操作,而不應該在?Startup?的?Configure?方法里處理,這樣?Configure?方法可以更純粹一些,只配置 asp.net core 的請求管道,這純屬個人意見,沒有對錯之分
這里 Host 的實現和 asp.net core 的實現不同,有需要的可以深究源碼,在 asp.net core 2.x 的版本里是有一個?IWebHost?的,在 asp.net core 3.x 以及 .net 5 里是沒有?IWebHost?的取而代之的是通用主機?IHost, 通過實現了一個?IHostedService?來實現?WebHost?的
Run
運行示例代碼:
public class Program {private static readonly CancellationTokenSource Cts = new CancellationTokenSource();public static async Task Main(string[] args){Console.CancelKeyPress += OnExit;var host = WebHostBuilder.CreateDefault(args).ConfigureServices((configuration, services) =>{}).ConfigureApplication((configuration, app) =>{app.When(context => context.Request.Url.PathAndQuery.StartsWith("/favicon.ico"), pipeline => { });app.When(context => context.Request.Url.PathAndQuery.Contains("test"),p => { p.Run(context => context.Response.WriteAsync("test")); });app.Use(async (context, next) =>{await context.Response.WriteLineAsync($"middleware1, requestPath:{context.Request.Url.AbsolutePath}");await next();}).Use(async (context, next) =>{await context.Response.WriteLineAsync($"middleware2, requestPath:{context.Request.Url.AbsolutePath}");await next();}).Use(async (context, next) =>{await context.Response.WriteLineAsync($"middleware3, requestPath:{context.Request.Url.AbsolutePath}");await next();});app.Run(context => context.Response.WriteAsync("Hello Mini Asp.Net Core"));}).Initialize((configuration, services) =>{}).Build();await host.RunAsync(Cts.Token);}private static void OnExit(object sender, EventArgs e){Console.WriteLine("exiting ...");Cts.Cancel();} }在示例項目目錄下執行?dotnet run,并訪問?http://localhost:5100/:
仔細觀察瀏覽器?console?或?network?的話,會發現還有一個請求,瀏覽器會默認請求?/favicon.ico?獲取網站的圖標
因為我們針對這個請求沒有任何中間件的處理,所以直接返回了 404
在訪問?/test,可以看到和剛才的輸出完全不同,因為這個請求走了另外一個分支,相當于 asp.net core 里?Map/?MapWhen?的效果,另外?Run?代表里中間件的中斷,不會執行后續的中間件
More
上面的實現只是我在嘗試寫一個簡版的 asp.net core 框架時的實現,和 asp.net core 的實現并不完全一樣,如果需要請參考源碼,上面的實現僅供參考,上面實現的源碼可以在 Github 上獲取?https://github.com/WeihanLi/SamplesInPractice/tree/master/MiniAspNetCore
asp.net core 源碼:https://github.com/dotnet/aspnetcore
Reference
https://www.cnblogs.com/artech/p/inside-asp-net-core-framework.html
https://www.cnblogs.com/artech/p/mini-asp-net-core-3x.html
https://www.cnblogs.com/edisonchou/p/aspnet_core_mini_implemention_introduction.html
讓 .NET 輕松構建中間件模式代碼
讓 .NET 輕松構建中間件模式代碼(二)
https://github.com/WeihanLi/SamplesInPractice/tree/master/MiniAspNetCore
總結
以上是生活随笔為你收集整理的写一个简版 asp.net core的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 人在职场,表达似水
- 下一篇: 如何在Windows上使用Git创建一个