動手寫一個簡版 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
- https://www.cnblogs.com/weihanli/p/12700006.html
- https://www.cnblogs.com/weihanli/p/12709603.html
- https://github.com/WeihanLi/SamplesInPractice/tree/master/MiniAspNetCore