I had a request come through regarding one of my NuGet packages yesterday. It’s primarily a wrapper around an API, and there was a problem with it deserializing one of the requests – the user was seeing the API perform the call (it altered the server resource) but it was throwing an exception trying to give back a response. How do you find out what the problem is?
This is a scenario I was very aware could happen when I was writing the packages (especially as they’re not my APIs so things could change). Because of that, I implemented a simple design that I’d seen in a couple of other API wrappers. It’s something you don’t really question when it all goes swimmingly – but once you enter a situation that requires debugging it’s
You have your regular constructor, passing in the required parameters, this is what is used the vast majority of the time. And then you add a second constructor, exactly the same as the first but with one addition – an HttpClient object.
public class ApiWrapper { protected HttpClient Client{ get; } public ApiWrapper(string publication, string token):this(publication,token,null) { } public ApiWrapper(string publication, string token, HttpClient client) { //Use your constructor parameters; Client = client ?? new HttpClient(); } public async Task<ResponseObject> MakeApiCall() { var httpResponse = await Client.GetAsync(uri); //your work here... } }
Okay…so how is that useful?
This second constructor highlights to the user that you’re going to make all your calls using a central client. So if only they could intercept the calls being made through that client, then they’d be able to see what the problem was! Step in HttpMessageHandler
This class has a simple structure, and allows you to control and monitor the requests and responses being sent from and to the Client that your code is using.
public class MyMessageHandler: HttpMessageHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { //logic here } }
Now in most scenarios you still want the underlying call to be able to be made, so you need to pass off your handler to run some real code eventually. For that there’s HttpClientHandler, the eventual endpoint for any handler chain we create. To help with this chaining there’s a class that deals with an “inner” handler, DelegatingHandler. By putting these together we end up with a seamless interception, ready for us to add our logic.
public class MyMessageHandler: DelegatingHandler { public MyMessageHandler():base(new HttpClientHandler()) protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return base.SendAsync(request,cancellationToken); } }
Okay – so what kind of logic can we add?
Here are the most common scenarios
Logging
This is the situation I was able to use to debug the serialization problem I had. You can log request information, header information etc. as well as the response status and content before allowing it to carry on.
And no – this is not how I recommend you implement logging.
public class LoggingHandler: DelegatingHandler { public List<string> Logs{ get; } public LoggingHandler():base(new HttpClientHandler()) { Logs = new List<string>(); } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Logs.Add($"Request: {request.RequestUri}"); var response = await base.SendAsync(request,cancellationToken); Logs.Add($"Response status: {response.StatusCode}"); return response; } }
Mocking
This technique is great for test projects. Here you never actually allow the call to make it out. You create a mock response in code and send it back. The advantage here is that because, as far as your code is concerned, it’s coming from the HttpClient object – it’s testing code all the way down rather than worrying about having to abstract away the code that creates the requests
By keeping the DelegatingHandler it means each instance of the class can handle a specific mock, and by not passing in HttpClientHandler at the end it means any calls not mocked will cause an exception. Obviously you could have large amounts of logic determining whether or not the mock “matched”, uri is just a simple example
public class MockingHandler: DelegatingHandler { public string RequestUri { get; } public HttpResponseMessage Response {get; } public MockingHandler(string requestUri, HttpResponseMessage response, HttpMessageHandler inner):base(inner) { RequestUri = requestUri; Response = response; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if(request.RequestUri.PathAndQuery == "/SomeRandomPath") { return Task.FromResult(response); } return base.SendAsync(request,cancellationToken); } }
“Generic Other”
I use this technique a lot, a single object that allows a lambda to be run against any call made by the client. This should be used sparingly, but can be useful. For example, it can be used alongside the mocking example above – the lambda containing assertions about the format of the request your library is generating.
public class ActionMessageHandler:DelegatingHandler { private Func<HttpRequestMessage,Task<HttpResponseMessage>> Action { get; } public ActionMessageHandler(Func<HttpRequestMessage,Task<HttpResponseMessage>> action, HttpMessageHandler inner):base(inner) { Action = action; } public ActionMessageHandler(Func<HttpRequestMessage, Task<HttpResponseMessage>> action) { Action = action; } protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return Action(request) ?? base.SendAsync(request,cancellationToken); } }
And that’s it
Libraries are for helping your clients perform actions, but with a small tweak like this one you’re really enabling them to help themselves, as well as gain a better understanding of how your library works.
If you’re the guy on your team who has to maintain the package, and there’s a specific situation causing you trouble – how much easier is it to drop in some monitoring, than having to try and recreate the issue using a project reference instead of a package reference. How often has your recreation not been able to replicate the bug? I hope this technique helps.