Routing in Azure Dev Spaces
Azure Dev Spaces is pure dopamine, it makes debugging and integreation testing a breeze in a complex kubernetes setup. And the best thing is: you don't need to modify anything in your code to make it work.
Except for one thing...
To make routing work properly, you need to forward the "azds-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("azds-route-as"))
{
// Propagate the dev space routing header
request.Headers.Add("azds-route-as", this.Request.Headers["azds-route-as"] as IEnumerable<string>);
}
var response = await client.SendAsync(request);
(this is a snippet of the official tutorial)
Unacceptable! To hide this monstrosity, I'll make use of a DelegatingHandler
to intercept outgoing HTTP messages and add the "azds-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 "azds-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 "azds-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 dev space routing header
if (incomingRequest.Headers.ContainsKey("azds-route-as"))
{
outgoingRequest.Headers.Add("azds-route-as", incomingRequest.Headers["azds-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 dev space routing header
if (incomingRequest.Headers.ContainsKey("azds-route-as"))
{
outgoingRequest.Headers.Add("azds-route-as", incomingRequest.Headers["azds-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 Azure Dev Spaces, resulting in a more flexible, futureproof application.
You can find all the code here.