Skip to content

Instantly share code, notes, and snippets.

@davidfowl
Last active June 5, 2022 04:38
Show Gist options
  • Save davidfowl/2ae62e7c34c27a58faacf8b0463b1586 to your computer and use it in GitHub Desktop.
Save davidfowl/2ae62e7c34c27a58faacf8b0463b1586 to your computer and use it in GitHub Desktop.
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCompositeServer(this IServiceCollection services)
{
// HACK: We're removing existing IServer implementations from the DI container and
// resolving them by implementation type. This allows us to build a composite server that starts all registered
// IServer implementations.
var descriptors = services.Where(x => x.Lifetime == ServiceLifetime.Singleton && x.ServiceType == typeof(IServer)).ToArray();
// Remove the IServer descriptors but add it back as the implementation type.
var serverTypes = new HashSet<Type>();
foreach (var d in descriptors)
{
services.Remove(d);
// Remove dupes
serverTypes.Add(d.ImplementationType!);
}
foreach (var serverType in serverTypes)
{
services.AddSingleton(serverType);
}
// Add the new composite server and resolves the existing servers by implementation type (that's what allows them to co-exist in the DI Container)
services.AddSingleton<IServer>(s => new CompositeServer(serverTypes.Select(t => (IServer)s.GetRequiredService(t)).ToArray()));
return services;
}
private class CompositeServer : IServer
{
private readonly IServer[] _servers;
private readonly ServerAddressesFeature _serverAddressesFeature;
public CompositeServer(IServer[] servers)
{
_servers = servers ?? throw new ArgumentNullException(nameof(servers));
var features = new FeatureCollection();
// This will overwrite features based on registration order
foreach (var s in servers)
{
foreach (var f in s.Features)
{
features[f.Key] = f.Value;
}
}
// We need to combine all server addresses into a single feature
_serverAddressesFeature = new ServerAddressesFeature();
// Overwrite the server addresses feature
features.Set<IServerAddressesFeature>(_serverAddressesFeature);
Features = features;
}
public IFeatureCollection Features { get; }
public void Dispose()
{
foreach (var server in _servers)
{
server.Dispose();
}
}
public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull
{
foreach (var server in _servers)
{
await server.StartAsync(application, cancellationToken);
// Server addresses are populated after starting, now update
// the list of addresses
if (server.Features.Get<IServerAddressesFeature>() is { } feature)
{
foreach (var addresses in feature.Addresses)
{
_serverAddressesFeature.Add(addresses);
}
}
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
foreach (var server in _servers)
{
await server.StopAsync(cancellationToken);
}
}
private class ServerAddressesFeature : IServerAddressesFeature
{
private readonly List<string> _addresses = new List<string>();
public ICollection<string> Addresses { get; }
public bool PreferHostingUrls { get; set; }
public ServerAddressesFeature()
{
Addresses = _addresses.AsReadOnly();
}
public void Add(string address)
{
_addresses.Add(address);
}
}
}
}
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseHttpSys(o =>
{
o.UrlPrefixes.Add("http://localhost:8080");
});
// Kestrel is already configured by default, so tweak the url to avoid port conflict
builder.WebHost.ConfigureKestrel(o =>
{
o.ListenLocalhost(8081);
});
// The order of this is important. It must be done after the IServer implementations have been configured
builder.Services.AddCompositeServer();
var app = builder.Build();
app.MapGet("/", (HttpContext context) => context.Features.ToString());
app.Run();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment