動手寫一個簡版 asp.net core Intro 之前看到過蔣金楠老師的一篇 200 行代碼帶你瞭解 asp.net core 框架,最近參考蔣老師和 Edison 的文章和代碼,結合自己對 asp.net core 的理解 ,最近自己寫了一個 MiniAspNetCore ,寫篇文章總結一下。 ...
動手寫一個簡版 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