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.