Implementando um Framework Web Minimalista Inspirado no ASP.NET Core

A construção de um framework web minimalista é uma excelente forma de compreender a arquitetura interna de ferramentas robustas como o ASP.NET Core. Ao recriar os componentes fundamentais, é possível entender como o contexto HTTP, o servidor web, a injeção de dependências e o pipeline de middlewares interagem para processar requisições.

Contexto da Requisição

O contexto HTTP é o objeto central que encapsula todas as informações sobre a requisição recebida e a resposta a ser enviada, além de atuar como um meio de transporte de dados entre os middlewares.


public sealed class HttpContext
{
    public IServiceProvider Services { get; init; }
    public HttpRequest Request { get; }
    public HttpResponse Response { get; }
    public IFeatureContainer Features { get; }

    public HttpContext(IFeatureContainer features)
    {
        Features = features;
        Request = new HttpRequest(features);
        Response = new HttpResponse(features);
    }
}

As classes HttpRequest e HttpResponse funcionam como wrappers para as funcionalidades reais, que são expostas através de interfaces de recursos (Features).


public sealed class HttpRequest
{
    private readonly IHttpRequestFeature _feature;
    
    public HttpRequest(IFeatureContainer features) => _feature = features.Retrieve<IHttpRequestFeature>();
    
    public Uri Endpoint => _feature.Endpoint;
    public IDictionary<string, string> Headers => _feature.Headers;
    public string HttpMethod => _feature.Method;
    public string HostName => _feature.Endpoint.Host;
    public Stream Payload => _feature.Payload;
}

public sealed class HttpResponse
{
    private readonly IHttpResponseFeature _feature;
    
    public HttpResponse(IFeatureContainer features) => _feature = features.Retrieve<IHttpResponseFeature>();
    
    public bool HasStarted => _feature.Payload.Length > 0;
    
    public int Status 
    { 
        get => _feature.Status; 
        set => _feature.Status = value; 
    }

    public async Task WriteBytesAsync(byte[] data)
    {
        if (_feature.Status <= 0) _feature.Status = 200;
        if (data is { Length: > 0 }) await _feature.Payload.WriteAsync(data);
    }
}

Abstração de Recursos (Features)

O padrão de Features permite desacoplar a implementação do servidor web da lógica da aplicação. O servidor é responsável por fornecer as implementações concretas de IHttpRequestFeature e IHttpResponseFeature.


public interface IHttpRequestFeature
{
    Uri Endpoint { get; }
    string Method { get; }
    IDictionary<string, string> Headers { get; }
    Stream Payload { get; }
}

public interface IHttpResponseFeature
{
    int Status { get; set; }
    IDictionary<string, string> Headers { get; set; }
    Stream Payload { get; }
}

Para gerenciar esses recursos, utilizamos um contêiner baseado em um dicionário onde a chave é o tipo da interface e o valor é a instância do recurso.


public interface IFeatureContainer : IDictionary<Type, object> { }

public class FeatureContainer : Dictionary<Type, object>, IFeatureContainer { }

public static class FeatureContainerExtensions
{
    public static IFeatureContainer Add<T>(this IFeatureContainer container, T feature)
    {
        container[typeof(T)] = feature;
        return container;
    }

    public static T Retrieve<T>(this IFeatureContainer container)
    {
        return container.TryGetValue(typeof(T), out var val) ? (T)val : default;
    }
}

Servidor Web

O servidor web é abstruído através da interface IWebServer, que define o contrato para iniciar a escuta de requisições e repassá-las ao pipeline da aplicação.


public interface IWebServer
{
    Task StartAsync(Func<HttpContext, Task> handler, CancellationToken token = default);
}

Uma implementação prática pode utilizar o HttpListener nativo do .NET para capturar o tráfego HTTP e mapeá-lo para o nosso contexto customizado.


public class NativeHttpServer : IWebServer
{
    private readonly HttpListener _listener;
    private readonly IServiceProvider _services;

    public NativeHttpServer(IServiceProvider services, IConfiguration config)
    {
        _listener = new HttpListener();
        var endpoints = config["Server:Urls"]?.Split(';') ?? new[] { "http://localhost:8080/" };
        
        foreach (var ep in endpoints.Where(e => !string.IsNullOrWhiteSpace(e)))
        {
            _listener.Prefixes.Add(ep.EndsWith("/") ? ep : $"{ep}/");
        }
        _services = services;
    }

    public async Task StartAsync(Func<HttpContext, Task> handler, CancellationToken token = default)
    {
        _listener.Start();
        while (!token.IsCancellationRequested)
        {
            var ctx = await _listener.GetContextAsync();
            var features = new FeatureContainer();
            features.Add(ctx.ToRequestFeature());
            features.Add(ctx.ToResponseFeature());

            using var scope = _services.CreateScope();
            var httpCtx = new HttpContext(features) { Services = scope.ServiceProvider };
            
            await handler(httpCtx);
            ctx.Response.Close();
        }
        _listener.Stop();
    }
}

As extensões ToRequestFeature e ToResponseFeature adaptam os objetos nativos do HttpListener para as interfaces do nosso framework.


public class ListenerRequestFeature : IHttpRequestFeature
{
    private readonly HttpListenerRequest _req;
    public ListenerRequestFeature(HttpListenerContext ctx) => _req = ctx.Request;
    public Uri Endpoint => _req.Url;
    public string Method => _req.HttpMethod;
    
    public IDictionary<string, string> Headers 
    { 
        get 
        {
            var dict = new Dictionary<string, string>();
            foreach (var key in _req.Headers.AllKeys) dict[key] = _req.Headers[key];
            return dict;
        } 
    }
    
    public Stream Payload => _req.InputStream;
}

public class ListenerResponseFeature : IHttpResponseFeature
{
    private readonly HttpListenerResponse _res;
    public ListenerResponseFeature(HttpListenerContext ctx) => _res = ctx.Response;
    public int Status { get => _res.StatusCode; set => _res.StatusCode = value; }
    
    public IDictionary<string, string> Headers 
    { 
        get 
        {
            var dict = new Dictionary<string, string>();
            foreach (var key in _res.Headers.AllKeys) dict[key] = _res.Headers[key];
            return dict;
        }
        set 
        {
            _res.Headers = new WebHeaderCollection();
            foreach (var kvp in value) _res.Headers.Add(kvp.Key, kvp.Value);
        } 
    }
    
    public Stream Payload => _res.OutputStream;
}

public static class ListenerExtensions
{
    public static IHttpRequestFeature ToRequestFeature(this HttpListenerContext ctx) => new ListenerRequestFeature(ctx);
    public static IHttpResponseFeature ToResponseFeature(this HttpListenerContext ctx) => new ListenerResponseFeature(ctx);
}

Pipeline de Middlewares

O processamento da requisição é delegado a uma cadeia de middlewares. O delegate RequestHandler representa a assinatura de um manipulador de requisição.


public delegate Task RequestHandler(HttpContext context);

public interface IPipelineBuilder
{
    IPipelineBuilder Use(Func<RequestHandler, RequestHandler> middleware);
    RequestHandler Build();
    IPipelineBuilder CreateNew();
}

O construtor do pipeline encadeia as funções de middleware, permitindo que cada componente execute lógica antes e depois do próximo manipulador na cadeia.

Host da Aplicação

O Host é responsável por orquestrar o ciclo de vida da aplicação, unindo a configuração, os serviços e o servidor web.


public interface IApplicationHost
{
    Task RunAsync(CancellationToken token = default);
}

public class WebApplicationHost : IApplicationHost
{
    private readonly RequestHandler _handler;
    private readonly IWebServer _server;

    public WebApplicationHost(IServiceProvider services, RequestHandler handler)
    {
        _handler = handler;
        _server = services.GetRequiredService<IWebServer>();
    }

    public Task RunAsync(CancellationToken token = default) => _server.StartAsync(_handler, token);
}

Para facilitar a configuração, utilizamos um padrão Builder que centraliza a adição de configurações, serviços e middlewares.


public interface IHostBuilder
{
    IHostBuilder SetupConfig(Action<IConfigurationBuilder> action);
    IHostBuilder SetupServices(Action<IConfiguration, IServiceCollection> action);
    IHostBuilder SetupPipeline(Action<IConfiguration, IPipelineBuilder> action);
    IApplicationHost Build();
}

public class WebHostBuilder : IHostBuilder
{
    private readonly IConfigurationBuilder _configBuilder = new ConfigurationBuilder();
    private readonly IServiceCollection _services = new ServiceCollection();
    private readonly IPipelineBuilder _pipeline = PipelineBuilder.Create(ctx => 
    {
        ctx.Response.Status = 404;
        return Task.CompletedTask;
    });

    public IHostBuilder SetupConfig(Action<IConfigurationBuilder> action)
    {
        action?.Invoke(_configBuilder);
        return this;
    }

    public IHostBuilder SetupServices(Action<IConfiguration, IServiceCollection> action)
    {
        var config = _configBuilder.Build();
        action?.Invoke(config, _services);
        return this;
    }

    public IHostBuilder SetupPipeline(Action<IConfiguration, IPipelineBuilder> action)
    {
        var config = _configBuilder.Build();
        action?.Invoke(config, _pipeline);
        return this;
    }

    public IApplicationHost Build()
    {
        var config = _configBuilder.Build();
        _services.AddSingleton<IConfiguration>(config);
        var provider = _services.BuildServiceProvider();
        return new WebApplicationHost(provider, _pipeline.Build());
    }
}

Execução e Testes

Com a estrutura definida, a aplicação pode ser inicializada configurando os middlewares e iniciando o host.


public class Program
{
    private static readonly CancellationTokenSource Cts = new();

    public static async Task Main(string[] args)
    {
        Console.CancelKeyPress += (s, e) => 
        {
            e.Cancel = true;
            Cts.Cancel();
        };

        var host = new WebHostBuilder()
            .SetupConfig(cfg => cfg.AddJsonFile("appsettings.json", true, true))
            .SetupServices((cfg, svc) => 
            {
                svc.AddSingleton<IWebServer, NativeHttpServer>();
            })
            .SetupPipeline((cfg, app) =>
            {
                app.Use(async (ctx, next) =>
                {
                    Console.WriteLine($"[Log] Request: {ctx.Request.HttpMethod} {ctx.Request.Endpoint}");
                    await next();
                })
                .Use(async (ctx, next) =>
                {
                    if (ctx.Request.Endpoint.AbsolutePath == "/api/status")
                    {
                        ctx.Response.Status = 200;
                        await ctx.Response.WriteBytesAsync("OK"u8.ToArray());
                        return;
                    }
                    await next();
                });
                
                app.Use(async (ctx, next) =>
                {
                    if (ctx.Request.Endpoint.AbsolutePath == "/")
                    {
                        await ctx.Response.WriteBytesAsync("Bem-vindo ao Mini Framework"u8.ToArray());
                        return;
                    }
                    await next();
                });
            })
            .Build();

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

Ao executar o projeto e acessar os endpoints configurados, o pipeline processa a requisição sequencialmente. Middlewares de log registram a entrada, enquanto os middlewares de roteamento verificam o caminho da URL para gerar a resposta apropriada ou delegar a execução ao próximo componente na cadeia.

Tags: ASP.NET-Core middleware Pipeline httpcontext web-server

Publicado em 10-17 16:56