暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

写一个简版 asp.net core

amazingdotnet 2020-05-22
237

动手写一个简版 asp.net core

Intro

之前看到过蒋金楠老师的一篇 200 行代码带你了解 asp.net core 框架,最近参考蒋老师和 Edison 的文章和代码,结合自己对 asp.net core 的理解 ,最近自己写了一个 MiniAspNetCore ,写篇文章总结一下。

HttpContext

HttpContext
 可能是最为常用的一个类了, HttpContext
 是请求上下文,包含了所有的请求信息以及响应信息,以及一些自定义的用于在不同中间件中传输数据的信息

来看一下 HttpContext
 的定义:

  1. public class HttpContext

  2. {

  3. public IServiceProvider RequestServices { get; set; }


  4. public HttpRequest Request { get; set; }


  5. public HttpResponse Response { get; set; }


  6. public IFeatureCollection Features { get; set; }


  7. public HttpContext(IFeatureCollection featureCollection)

  8. {

  9. Features = featureCollection;

  10. Request = new HttpRequest(featureCollection);

  11. Response = new HttpResponse(featureCollection);

  12. }

  13. }

HttpRequest
 即为请求信息对象,包含了所有请求相关的信息,

HttpResponse
 为响应信息对象,包含了请求对应的响应信息

RequestServices
 为 asp.net core 里的 RequestServices
,代表当前请求的服务提供者,可以使用它来获取具体的服务实例

Features
 为 asp.net core 里引入的对象,可以用来在不同中间件中传递信息和用来解耦合

,下面我们就来看下 HttpRequest
 和 HttpResponse
 是怎么实现的

HttpRequest:

  1. public class HttpRequest

  2. {

  3. private readonly IRequestFeature _requestFeature;


  4. public HttpRequest(IFeatureCollection featureCollection)

  5. {

  6. _requestFeature = featureCollection.Get<IRequestFeature>();

  7. }


  8. public Uri Url => _requestFeature.Url;


  9. public NameValueCollection Headers => _requestFeature.Headers;


  10. public string Method => _requestFeature.Method;


  11. public string Host => _requestFeature.Url.Host;


  12. public Stream Body => _requestFeature.Body;

  13. }

HttpResponse:

  1. public class HttpResponse

  2. {

  3. private readonly IResponseFeature _responseFeature;


  4. public HttpResponse(IFeatureCollection featureCollection)

  5. {

  6. _responseFeature = featureCollection.Get<IResponseFeature>();

  7. }


  8. public bool ResponseStarted => _responseFeature.Body.Length > 0;


  9. public int StatusCode

  10. {

  11. get => _responseFeature.StatusCode;

  12. set => _responseFeature.StatusCode = value;

  13. }


  14. public async Task WriteAsync(byte[] responseBytes)

  15. {

  16. if (_responseFeature.StatusCode <= 0)

  17. {

  18. _responseFeature.StatusCode = 200;

  19. }

  20. if (responseBytes != null && responseBytes.Length > 0)

  21. {

  22. await _responseFeature.Body.WriteAsync(responseBytes);

  23. }

  24. }

  25. }

Features

上面我们提到我们可以使用 Features
 在不同中间件中传递信息和解耦合

由上面 HttpRequest
HttpResponse
 的代码我们可以看出来, HttpRequest
 和 HttpResponse
 其实就是在 IRequestFeature
 和 IResponseFeature
 的基础上封装了一层,真正的核心其实是 IRequestFeature
IResponseFeature
 ,而这里使用接口就很好的实现了解耦,可以根据不同的 WebServer 使用不同的 RequestFeature
ResponseFeature
,来看下 IRequestFeature
IResponseFeature
 的实现

  1. public interface IRequestFeature

  2. {

  3. Uri Url { get; }


  4. string Method { get; }


  5. NameValueCollection Headers { get; }


  6. Stream Body { get; }

  7. }


  8. public interface IResponseFeature

  9. {

  10. public int StatusCode { get; set; }


  11. NameValueCollection Headers { get; set; }


  12. public Stream Body { get; }

  13. }

这里的实现和 asp.net core 的实际的实现方式应该不同,asp.net core 里 Headers 同一个 Header 允许有多个值,asp.net core 里是 StringValues 来实现的,这里简单处理了,使用了一个 NameValueCollection
 对象

上面提到的 Features
 是一个 IFeatureCollection
 对象,相当于是一系列的 Feature
 对象组成的,来看下 FeatureCollection
 的定义:

  1. public interface IFeatureCollection : IDictionary<Type, object> { }


  2. public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection

  3. {

  4. }

这里 IFeatureCollection
 直接实现 IDictionary<Type,object>
 ,通过一个字典 Feature 类型为 Key,Feature 对象为 Value 的字典来保存

为了方便使用,可以定义两个扩展方法来方便的Get/Set

  1. public static class FeatureExtensions

  2. {

  3. public static IFeatureCollection Set<TFeature>(this IFeatureCollection featureCollection, TFeature feature)

  4. {

  5. featureCollection[typeof(TFeature)] = feature;

  6. return featureCollection;

  7. }


  8. public static TFeature Get<TFeature>(this IFeatureCollection featureCollection)

  9. {

  10. var featureType = typeof(TFeature);

  11. return featureCollection.ContainsKey(featureType) ? (TFeature)featureCollection[featureType] : default(TFeature);

  12. }

  13. }

Web服务器

上面我们已经提到了 Web 服务器通过 IRequestFeature
IResponseFeature
 来实现不同 web 服务器和应用程序的解耦,web 服务器只需要提供自己的 RequestFeature
ResponseFeature
 即可

为了抽象不同的 Web 服务器,我们需要定义一个 IServer
 的抽象接口,定义如下:

  1. public interface IServer

  2. {

  3. Task StartAsync(Func<HttpContext, Task> requestHandler, CancellationToken cancellationToken = default);

  4. }

IServer
 定义了一个 StartAsync
 方法,用来启动 Web服务器,

StartAsync
 方法有两个参数,一个是 requestHandler,是一个用来处理请求的委托,另一个是取消令牌用来停止 web 服务器

示例使用了 HttpListener
 来实现了一个简单 Web 服务器, HttpListenerServer
 定义如下:

  1. public class HttpListenerServer : IServer

  2. {

  3. private readonly HttpListener _listener;

  4. private readonly IServiceProvider _serviceProvider;


  5. public HttpListenerServer(IServiceProvider serviceProvider, IConfiguration configuration)

  6. {

  7. _listener = new HttpListener();

  8. var urls = configuration.GetAppSetting("ASPNETCORE_URLS")?.Split(';');

  9. if (urls != null && urls.Length > 0)

  10. {

  11. foreach (var url in urls

  12. .Where(u => u.IsNotNullOrEmpty())

  13. .Select(u => u.Trim())

  14. .Distinct()

  15. )

  16. {

  17. // Prefixes must end in a forward slash ("/")

  18. // https://stackoverflow.com/questions/26157475/use-of-httplistener

  19. _listener.Prefixes.Add(url.EndsWith("/") ? url : $"{url}/");

  20. }

  21. }

  22. else

  23. {

  24. _listener.Prefixes.Add("http://localhost:5100/");

  25. }


  26. _serviceProvider = serviceProvider;

  27. }


  28. public async Task StartAsync(Func<HttpContext, Task> requestHandler, CancellationToken cancellationToken = default)

  29. {

  30. _listener.Start();

  31. if (_listener.IsListening)

  32. {

  33. Console.WriteLine("the server is listening on ");

  34. Console.WriteLine(_listener.Prefixes.StringJoin(","));

  35. }

  36. while (!cancellationToken.IsCancellationRequested)

  37. {

  38. var listenerContext = await _listener.GetContextAsync();


  39. var featureCollection = new FeatureCollection();

  40. featureCollection.Set(listenerContext.GetRequestFeature());

  41. featureCollection.Set(listenerContext.GetResponseFeature());


  42. using (var scope = _serviceProvider.CreateScope())

  43. {

  44. var httpContext = new HttpContext(featureCollection)

  45. {

  46. RequestServices = scope.ServiceProvider,

  47. };


  48. await requestHandler(httpContext);

  49. }

  50. listenerContext.Response.Close();

  51. }

  52. _listener.Stop();

  53. }

  54. }

HttpListenerServer
 实现的 RequestFeature
ResponseFeatue

  1. public class HttpListenerRequestFeature : IRequestFeature

  2. {

  3. private readonly HttpListenerRequest _request;


  4. public HttpListenerRequestFeature(HttpListenerContext listenerContext)

  5. {

  6. _request = listenerContext.Request;

  7. }


  8. public Uri Url => _request.Url;

  9. public string Method => _request.HttpMethod;

  10. public NameValueCollection Headers => _request.Headers;

  11. public Stream Body => _request.InputStream;

  12. }


  13. public class HttpListenerResponseFeature : IResponseFeature

  14. {

  15. private readonly HttpListenerResponse _response;


  16. public HttpListenerResponseFeature(HttpListenerContext httpListenerContext)

  17. {

  18. _response = httpListenerContext.Response;

  19. }


  20. public int StatusCode { get => _response.StatusCode; set => _response.StatusCode = value; }


  21. public NameValueCollection Headers

  22. {

  23. get => _response.Headers;

  24. set

  25. {

  26. _response.Headers = new WebHeaderCollection();

  27. foreach (var key in value.AllKeys)

  28. _response.Headers.Add(key, value[key]);

  29. }

  30. }


  31. public Stream Body => _response.OutputStream;

  32. }

为了方便使用,为 HttpListenerContext
 定义了两个扩展方法,就是上面 HttpListenerServer
 中的 GetRequestFeature
GetResponseFeature

  1. public static class HttpListenerContextExtensions

  2. {

  3. public static IRequestFeature GetRequestFeature(this HttpListenerContext context)

  4. {

  5. return new HttpListenerRequestFeature(context);

  6. }


  7. public static IResponseFeature GetResponseFeature(this HttpListenerContext context)

  8. {

  9. return new HttpListenerResponseFeature(context);

  10. }

  11. }

RequestDelegate

在上面的 IServer
 定义里有一个 requestHandler 的 对象,在 asp.net core 里是一个名称为 RequestDelegate
 的对象,而用来构建这个委托的在 asp.net core 里是 IApplicationBuilder
,这些在蒋老师和 Edison 的文章和代码里都可以看到,这里我们只是简单介绍下,我在 MiniAspNetCore 的示例中没有使用这些对象,而是使用了自己抽象的 PipelineBuilder
 和原始委托实现的

asp.net core 里 RequestDelegate
 定义:

  1. public delegate Task RequestDelegate(HttpContext context);

其实和我们上面定义用的 Func<HttpContext,Task>
 是等价的

IApplicationBuilder
 定义:

  1. /// <summary>

  2. /// Defines a class that provides the mechanisms to configure an application's request pipeline.

  3. /// </summary>

  4. public interface IApplicationBuilder

  5. {

  6. /// <summary>

  7. /// Gets or sets the <see cref="T:System.IServiceProvider" /> that provides access to the application's service container.

  8. /// </summary>

  9. IServiceProvider ApplicationServices { get; set; }


  10. /// <summary>

  11. /// Gets the set of HTTP features the application's server provides.

  12. /// </summary>

  13. IFeatureCollection ServerFeatures { get; }


  14. /// <summary>

  15. /// Gets a key/value collection that can be used to share data between middleware.

  16. /// </summary>

  17. IDictionary<string, object> Properties { get; }


  18. /// <summary>

  19. /// Adds a middleware delegate to the application's request pipeline.

  20. /// </summary>

  21. /// <param name="middleware">The middleware delegate.</param>

  22. /// <returns>The <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" />.</returns>

  23. IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);


  24. /// <summary>

  25. /// Creates a new <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" /> that shares the <see cref="P:Microsoft.AspNetCore.Builder.IApplicationBuilder.Properties" /> of this

  26. /// <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" />.

  27. /// </summary>

  28. /// <returns>The new <see cref="T:Microsoft.AspNetCore.Builder.IApplicationBuilder" />.</returns>

  29. IApplicationBuilder New();


  30. /// <summary>

  31. /// Builds the delegate used by this application to process HTTP requests.

  32. /// </summary>

  33. /// <returns>The request handling delegate.</returns>

  34. RequestDelegate Build();

  35. }

我们这里没有定义 IApplicationBuilder
,使用了简化抽象的 IAsyncPipelineBuilder
,定义如下:

  1. public interface IAsyncPipelineBuilder<TContext>

  2. {

  3. IAsyncPipelineBuilder<TContext> Use(Func<Func<TContext, Task>, Func<TContext, Task>> middleware);


  4. Func<TContext, Task> Build();


  5. IAsyncPipelineBuilder<TContext> New();

  6. }

对于 asp.net core 的中间件来说 ,上面的 TContext
 就是 HttpContext
,替换之后也就是下面这样的:

  1. public interface IAsyncPipelineBuilder<HttpContext>

  2. {

  3. IAsyncPipelineBuilder<HttpContext> Use(Func<Func<HttpContext, Task>, Func<HttpContext, Task>> middleware);


  4. Func<HttpContext, Task> Build();


  5. IAsyncPipelineBuilder<HttpContext> New();

  6. }

是不是和 IApplicationBuilder
 很像,如果不像可以进一步把 Func<HttpContext,Task>
 使用 RequestDelegate
 替换

  1. public interface IAsyncPipelineBuilder<HttpContext>

  2. {

  3. IAsyncPipelineBuilder<HttpContext> Use(Func<RequestDelegate, RequestDelegate> middleware);


  4. RequestDelegate Build();


  5. IAsyncPipelineBuilder<HttpContext> New();

  6. }

最后再将接口名称替换一下:

  1. public interface IApplicationBuilder1

  2. {

  3. IApplicationBuilder1 Use(Func<RequestDelegate, RequestDelegate> middleware);


  4. RequestDelegate Build();


  5. IApplicationBuilder1 New();

  6. }

至此,就完全可以看出来了,这 IAsyncPipelineBuilder<HttpContext>
 就是一个简版的 IApplicationBuilder

IAsyncPipelineBuilder
 和 IApplicationBuilder
 的作用是将注册的多个中间件构建成一个请求处理的委托

中间件处理流程:

更多关于 PipelineBuilder 构建中间件的信息可以查看 让 .NET 轻松构建中间件模式代码 了解更多

WebHost

通过除了 Web 服务器之外,还有一个 Web Host 的概念,可以简单的这样理解,一个 Web 服务器上可以有多个 Web Host,就像 IIS/nginx (Web Server) 可以 host 多个站点

可以说 WebHost 离我们的应用更近,所以我们还需要 IHost
 来托管应用

  1. public interface IHost

  2. {

  3. Task RunAsync(CancellationToken cancellationToken = default);

  4. }

WebHost
 定义:

  1. public class WebHost : IHost

  2. {

  3. private readonly Func<HttpContext, Task> _requestDelegate;

  4. private readonly IServer _server;


  5. public WebHost(IServiceProvider serviceProvider, Func<HttpContext, Task> requestDelegate)

  6. {

  7. _requestDelegate = requestDelegate;

  8. _server = serviceProvider.GetRequiredService<IServer>();

  9. }


  10. public async Task RunAsync(CancellationToken cancellationToken = default)

  11. {

  12. await _server.StartAsync(_requestDelegate, cancellationToken).ConfigureAwait(false);

  13. }

  14. }

为了方便的构建 Host
对象,引入了 HostBuilder
 来方便的构建一个 Host
,定义如下:

  1. public interface IHostBuilder

  2. {

  3. IHostBuilder ConfigureConfiguration(Action<IConfigurationBuilder> configAction);


  4. IHostBuilder ConfigureServices(Action<IConfiguration, IServiceCollection> configureAction);


  5. IHostBuilder Initialize(Action<IConfiguration, IServiceProvider> initAction);


  6. IHostBuilder ConfigureApplication(Action<IConfiguration, IAsyncPipelineBuilder<HttpContext>> configureAction);


  7. IHost Build();

  8. }

WebHostBuilder

  1. public class WebHostBuilder : IHostBuilder

  2. {

  3. private readonly IConfigurationBuilder _configurationBuilder = new ConfigurationBuilder();

  4. private readonly IServiceCollection _serviceCollection = new ServiceCollection();


  5. private Action<IConfiguration, IServiceProvider> _initAction = null;


  6. private readonly IAsyncPipelineBuilder<HttpContext> _requestPipeline = PipelineBuilder.CreateAsync<HttpContext>(context =>

  7. {

  8. context.Response.StatusCode = 404;

  9. return Task.CompletedTask;

  10. });


  11. public IHostBuilder ConfigureConfiguration(Action<IConfigurationBuilder> configAction)

  12. {

  13. configAction?.Invoke(_configurationBuilder);

  14. return this;

  15. }


  16. public IHostBuilder ConfigureServices(Action<IConfiguration, IServiceCollection> configureAction)

  17. {

  18. if (null != configureAction)

  19. {

  20. var configuration = _configurationBuilder.Build();

  21. configureAction.Invoke(configuration, _serviceCollection);

  22. }


  23. return this;

  24. }


  25. public IHostBuilder ConfigureApplication(Action<IConfiguration, IAsyncPipelineBuilder<HttpContext>> configureAction)

  26. {

  27. if (null != configureAction)

  28. {

  29. var configuration = _configurationBuilder.Build();

  30. configureAction.Invoke(configuration, _requestPipeline);

  31. }

  32. return this;

  33. }


  34. public IHostBuilder Initialize(Action<IConfiguration, IServiceProvider> initAction)

  35. {

  36. if (null != initAction)

  37. {

  38. _initAction = initAction;

  39. }


  40. return this;

  41. }


  42. public IHost Build()

  43. {

  44. var configuration = _configurationBuilder.Build();

  45. _serviceCollection.AddSingleton<IConfiguration>(configuration);

  46. var serviceProvider = _serviceCollection.BuildServiceProvider();


  47. _initAction?.Invoke(configuration, serviceProvider);


  48. return new WebHost(serviceProvider, _requestPipeline.Build());

  49. }


  50. public static WebHostBuilder CreateDefault(string[] args)

  51. {

  52. var webHostBuilder = new WebHostBuilder();

  53. webHostBuilder

  54. .ConfigureConfiguration(builder => builder.AddJsonFile("appsettings.json", true, true))

  55. .UseHttpListenerServer()

  56. ;


  57. return webHostBuilder;

  58. }

  59. }

这里的示例我在 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

运行示例代码:

  1. public class Program

  2. {

  3. private static readonly CancellationTokenSource Cts = new CancellationTokenSource();


  4. public static async Task Main(string[] args)

  5. {

  6. Console.CancelKeyPress += OnExit;


  7. var host = WebHostBuilder.CreateDefault(args)

  8. .ConfigureServices((configuration, services) =>

  9. {

  10. })

  11. .ConfigureApplication((configuration, app) =>

  12. {

  13. app.When(context => context.Request.Url.PathAndQuery.StartsWith("/favicon.ico"), pipeline => { });


  14. app.When(context => context.Request.Url.PathAndQuery.Contains("test"),

  15. p => { p.Run(context => context.Response.WriteAsync("test")); });

  16. app

  17. .Use(async (context, next) =>

  18. {

  19. await context.Response.WriteLineAsync($"middleware1, requestPath:{context.Request.Url.AbsolutePath}");

  20. await next();

  21. })

  22. .Use(async (context, next) =>

  23. {

  24. await context.Response.WriteLineAsync($"middleware2, requestPath:{context.Request.Url.AbsolutePath}");

  25. await next();

  26. })

  27. .Use(async (context, next) =>

  28. {

  29. await context.Response.WriteLineAsync($"middleware3, requestPath:{context.Request.Url.AbsolutePath}");

  30. await next();

  31. })

  32. ;

  33. app.Run(context => context.Response.WriteAsync("Hello Mini Asp.Net Core"));

  34. })

  35. .Initialize((configuration, services) =>

  36. {

  37. })

  38. .Build();

  39. await host.RunAsync(Cts.Token);

  40. }


  41. private static void OnExit(object sender, EventArgs e)

  42. {

  43. Console.WriteLine("exiting ...");

  44. Cts.Cancel();

  45. }

  46. }

在示例项目目录下执行 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


文章转载自amazingdotnet,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论