Out with the Old, Bring in the New
Alas! Azure Dev Spaces has fallen! But we welcome its successor: Bridge to Kubernetes. The goal is still to make debugging and integreation testing easy in a complex kubernetes setup. The main difference is that your code now runs locally on your machine.
One thing that hasn't changed is the fact that you have to use header forwarding. The header itself is now called kubernetes-route-as
instead of azds-route-as
. And once again Microsoft documentation fails to mention the best way to do it.
This is a re-take of my previous blog, but updated for Bridge to Kubernetes.
Routing in Bridge to Kubernetes
To make routing work properly, you need to forward the "kubernetes-route-as" header when making outgoing HTTP calls. That means you have to write code like this for EVERY SINGLE CALL:
var request = new HttpRequestMessage();
request.RequestUri = new Uri("http://mywebapi/api/values/1");
if (this.Request.Headers.ContainsKey("kubernetes-route-as"))
{
// Propagate the routing header
request.Headers.Add("kubernetes-route-as", this.Request.Headers["kubernetes-route-as"] as IEnumerable<string>);
}
var response = await client.SendAsync(request);
Unacceptable! To hide this monstrosity, I'll make use of a DelegatingHandler
to intercept outgoing HTTP messages and add the "kubernetes-route-as" header on-the-fly.
Talking to the Backend
I made a separate class for all communication with my backend. As a good boy, I used the HttpClientFactory with dependency injection.
BackHttpClient.cs
public class BackHttpClient : IBackHttpClient
{
private readonly HttpClient client;
public BackHttpClient(HttpClient client)
{
this.client = client;
}
public async Task<string> GetValueAsync(int id)
{
var response = await client.GetAsync($"api/values/{id}");
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadAsStringAsync();
return result;
}
else
{
throw new Exception($"Value with id {id} could not be retrieved: {response.ReasonPhrase}");
}
}
}
Startup.cs
services.AddHttpClient<IBackHttpClient, BackHttpClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:63448");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
(I used a local service, so you don't have to set up an AKS to run the demo)
Intercepting the Message
Since I don't want to worry about the "kubernetes-route-as" header with every HTTP call, I'm going to intercept every call before it is made.
ASP.NET Core middleware is not going to be of any help here. This middleware is used for incoming HTTP calls, not outgoing. Instead you can add a collection of DelegatingHandler
objects to your HttpClient
. This builds an outgoing pipeline for the HttpClient
similar to the ASP.NET Core middleware.
The following DelegatingHandler
will add the "kubernetes-route-as" header to the outgoing request if it was set on the incoming request. Notice that getting the incoming request is not straightforward and will be done in the next part.
RoutingHttpHandler
public class RoutingHttpHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage outgoingRequest, CancellationToken cancellationToken)
{
//var incomingRequest = ???; //TODO
// Propagate the routing header
if (incomingRequest.Headers.ContainsKey("kubernetes-route-as"))
{
outgoingRequest.Headers.Add("kubernetes-route-as", incomingRequest.Headers["kubernetes-route-as"] as IEnumerable<string>);
}
return base.SendAsync(outgoingRequest, cancellationToken);
}
}
By using AddHttpMessageHandler
, I can add the DelegatingHandler
to the HttpClient
injected into my IBackHttpClient
.
Startup.cs
services.AddTransient<RoutingHttpHandler>();
services.AddHttpClient<IBackHttpClient, BackHttpClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:63448");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<RoutingHttpHandler>();
Getting the HttpContext
The final piece of the puzzle is getting the incoming HTTP Request. Since HttpContext.Current
is not available in .NET Core, I will need another trick: the IHttpContextAccessor
. This is basically the injectable version of HttpContext.Current
, and is the result of Microsoft's continuing effort to replace static singletons with injectable ones.
Startup.cs
services.AddHttpContextAccessor();
services.AddTransient<RoutingHttpHandler>();
services.AddHttpClient<IBackHttpClient, BackHttpClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:63448");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<RoutingHttpHandler>();
RoutingHttpHandler.cs
public class RoutingHttpHandler : DelegatingHandler
{
private readonly IHttpContextAccessor httpContextAccessor;
public RoutingHttpHandler(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage outgoingRequest, CancellationToken cancellationToken)
{
var incomingRequest = httpContextAccessor.HttpContext.Request;
// Propagate the routing header
if (incomingRequest.Headers.ContainsKey("kubernetes-route-as"))
{
outgoingRequest.Headers.Add("kubernetes-route-as", incomingRequest.Headers["kubernetes-route-as"] as IEnumerable<string>);
}
return base.SendAsync(outgoingRequest, cancellationToken);
}
}
Conclusion
With a little bit of effort, you can shield your code from the requirements to use Bridge to Kubernetes, resulting in a more flexible, futureproof application.
UPDATE!
Someone informed me that there is already a NuGet package that does this.
Using this package, the resulting code would be:
services.AddHttpClient<IBackHttpClient, BackHttpClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:63448");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHeaderPropagation(options => options.Headers.Add("kubernetes-route-as"));