The bulk of our client UI is HTML5 (via CEF) which uses Apache Thrift to talk to a Windows service (via HTTP and WebSockets). As part of our migration to .NET Core we set out to:
- Use the new
netcore
generator in thrift 0.11 - Handle HTTP requests with
Thrift.Transports.Server.THttpServerTransport
atop ASP.NET Core instead ofSystem.Net.HttpListener
handing requests toThrift.Transport.THttpHandler
Before- HttpListener
The original implementation based on System.Net.HttpListener
was similar to:
if (!System.Net.HttpListener.IsSupported)
{
return;
}
// MultiProcessors.HttpProcessor is a Thrift.TMultiplexedProcessor
var thrift = new Thrift.Transport.THttpHandler(MultiProcessors.HttpProcessor, new Thrift.Protocol.TJSONProtocol.Factory());
var listener = new System.Net.HttpListener();
listener.Prefixes.Add("http://localhost:8282/");
listener.Start();
while (!cts.IsCancellationRequested)
{
// Receive HTTP request
var ctx = await listener.GetContextAsync();
await Task.Run(() =>
{
try
{
// FIXME: implement CORS correctly
ctx.Response.AppendHeader("Access-Control-Allow-Origin", "*");
if (ctx.Request.HttpMethod == "OPTIONS")
{
ctx.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
ctx.Response.AddHeader("Access-Control-Allow-Methods", "Get, POST");
ctx.Response.AddHeader("Access-Control-Max-Age", "1728000");
ctx.Response.Close();
}
else
{
// Pass request to thrift services registered with multiplexed processor
thrift.ProcessRequest(ctx);
}
}
catch(Exception e)
{
Log(LogLevel.Warn, $"HttpListener Exception: {e.Message}");
}
});
}
- Create
THttpHandler
usingTMultiplexedProcessor
instance andTJSONProtocol
- Wait for HttpListenerContext with
GetContextAsync()
- Pass it to
ProcessRequest()
The CORS hack was needed for CEF to load content directly from disk.
After- ASP.NET Core
As we started looking into ASP.NET Core the level of configurability and sophistication was pretty daunting. The MS documentation is extensive and the following will help you get started:
It wasn’t immediately clear how to handle HTTP requests with thrift. Thrift 0.11.0 features a new generator targeting netcore. The netcore client library contains
THttpServerTransport with Task Invoke(HttpContext)
which seems to be the telltale signature of ASP.NET Core.
The following was cobbled together from numerous sources:
try
{
webHostBuilder = WebHost.CreateDefaultBuilder()
.UseKestrel(options =>
{
options.Listen(IPAddress.Loopback, 8282);
})
//.UseUrls("http://localhost:8282/")
//.UseStartup<Startup>()
.ConfigureServices(services =>
{
services.AddCors();
})
.Configure(app =>
{
app.UseCors(corsPolicyBuilder =>
{
corsPolicyBuilder.AllowAnyHeader();
corsPolicyBuilder.AllowAnyOrigin();
corsPolicyBuilder.AllowAnyMethod();
});
// Here's our thrift middleware
app.UseMiddleware<Thrift.Transports.Server.THttpServerTransport>(MultiProcessors.HttpProcessor, new TJsonProtocol.Factory());
})
.Build();
webHostBuilder.Start();
}
catch (Exception ex)
{
log(LogLevel.Error, "HTTP server failed: " + ex.Message);
}
This uses the ConfigureServices() and Configure() helpers instead of a Startup class.
Rather than waiting for a routine to return an HTTP request and then passing it to a handler, ASP.NET Core can be configured with “middleware” to handle requests and process responses.
We were initially trying to add thrift as a service via ASP.NET Core dependency injection:
.ConfigureServices(services =>
{
// Couldn't make variants of these work
services.AddSingleton<ITAsyncProcessor>(MultiProcessors.HttpProcessor);
services.AddSingleton<ITProtocolFactory>(new TJsonProtocol.Factory());
})
But got errors like:
Unable to resolve service for type 'Thrift.ITAsyncProcessor' while attempting to activate 'Thrift.Transports.Server.THttpServerTransport'.
Services probably aren’t the correct mechanism, while middleware seems intended for request/response handling.
Update 2018/8/31
The correct way to do it is:
.ConfigureServices(services => {
//...
var processor = new TMultiplexedProcessor();
processor.RegisterProcessor("test", new TestProcessor());
services.AddSingleton<ITAsyncProcessor>(processor);
services.AddSingleton<ITProtocolFactory, TJsonProtocol.Factory>();
})
.Configure(app => {
//...
// Services registered above are passed to THttpServerTransport ctor
app.UseMiddleware<Thrift.Transports.Server.THttpServerTransport>();
});
Misc. notes:
- The difference between
AddSingleton()
,AddTransient()
, etc. pertains to the lifetime of the service. - Make sure to call
webHostBuilder.StopAsync()
on shutdown. Otherwise you’ll get a native exception in the GC finalizer.
Conclusions
Unfortunately, we don’t have any perfomance tests for this area of code. Because I would have loved to see the results after learning what a beast ASP.NET Core is.
We’ve got a first version working, but I don’t fully understand it yet. The same architecture can be used to build Net Core console applications, so it would be well-worth investing more time.