Processing Requests and Responses with HttpClient

In a recent project, I wanted to log outgoing requests and incoming responses using my own (JSON) format. This can be accomplished using a custom handler derived from DelegatingHandler. Here is an implementation that can be used in e.g. a console program:

        internal class LoggingHandler : DelegatingHandler
        {
            private static int _requestNumber = 0;
            private static readonly string LogFolder;

            static LoggingHandler()
            {
                LogFolder = Path.Combine(Path.GetTempPath(), "Log");
                if (Directory.Exists(LogFolder))
                {
                    foreach (var file in Directory.GetFiles(LogFolder))
                    {
                        File.Delete(file);
                    }
                }
                else
                    Directory.CreateDirectory(LogFolder);
            }

            protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
            {
                try
                {
                    var response = await base.SendAsync(request, cancellationToken);

                    if (request.Method == HttpMethod.Post)
                    {
                        var logEntry = new LogEntry
                        {
                            Request = new Request
                            {
                                Method = request.Method,
                                Uri = request.RequestUri,
                                Headers = request.Headers,
                                Content = await GetContent(request.Content)
                            },
                            Response = new Response
                            {
                                Headers = response.Headers,
                                StatusCode = response.StatusCode,
                                Content = await GetContent(response.Content)
                            }
                        };

                        var logEntryString = JsonConvert.SerializeObject(logEntry, Formatting.Indented);
                        var parts = request.RequestUri.AbsolutePath.Split('/');
                        string controller = parts.Length >= 6 ? parts[5] : "";
                        string path = Path.Combine(LogFolder, string.Format("{0:0000}0_{1}_{2}_{3}.json", ++_requestNumber, logEntry.Request.Method, controller, logEntry.Response.StatusCode));
                        File.WriteAllText(path, logEntryString, Encoding.UTF8);
                        System.Diagnostics.Debug.WriteLine("Wrote {0:N0} characters to {1}", logEntryString.Length, path);
                    }

                    return response;
                }
                catch (Exception ex)
                {
                    logger.Fatal(ex);
                    throw;
                }
            }

            private static async Task<object> GetContent(HttpContent content)
            {
                try
                {
                    if (content == null)
                        return null;
                    string s = await content.ReadAsStringAsync();
                    s = s.Trim();
                    if (s.StartsWith("["))
                        return JArray.Parse(s);
                    else if (s.StartsWith("{"))
                        return JObject.Parse(s);
                    else
                        return s;
                }
                catch (Exception ex)
                {
                    return ex.Message;
                }
            }
        }

Usage is simple:

var httpClient = new HttpClient(new LoggingHandler());

However, the above implementation didn’t work in a web application. The call to base.SendAsync never returns for some reason. I suppose it has to do with a different threading model. Luckily, I found the solution, to use Task.ContinueWith, in the following blog post: http://byterot.blogspot.se/2012/05/aspnet-web-api-series-messagehandler.html. Here is the alternative implementation.

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            try
            {
                _logger.Debug($"{GetType().Name} sending request  '{request.RequestUri}'...");
                return base.SendAsync(request, cancellationToken).ContinueWith(task =>
                {
                    _logger.Debug($"Task status is {task.Status}");
                    var response = task.Result;
                    _logger.Debug($"{GetType().Name} got response from'{request.RequestUri}'.");

                    if (request.Method == HttpMethod.Post)
                    {
                        var logEntry = new LogEntry
                        {
                            Request = new Request
                            {
                                Method = request.Method,
                                Uri = request.RequestUri,
                                Headers = request.Headers,
                                Content = GetContent(request.Content)
                            },
                            Response = new Response
                            {
                                Headers = response.Headers,
                                StatusCode = response.StatusCode,
                                Content = GetContent(response.Content)
                            }
                        };

                        var logEntryString = JsonConvert.SerializeObject(logEntry, Formatting.Indented);
                        var controller = GetController(request);
                        string path = Path.Combine(LogFolder, $"{++_requestNumber:0000}0_{logEntry.Request.Method}_{controller}_{logEntry.Response.StatusCode}.json");
                        _logger.Debug($"Writing {logEntryString.Length:N0} characters to {path}...");
                        File.WriteAllText(path, logEntryString, Encoding.UTF8);
                        _logger.Debug($"Wrote {logEntryString.Length:N0} characters to {path}.");
                    }

                    return response;
                }, cancellationToken);
            }
            catch (Exception ex)
            {
                _logger.Fatal(ex.Message, ex);
                throw;
            }
        }

        private static object GetContent(HttpContent content)
        {
            try
            {
                if (content == null)
                    return null;
                string s = content.ReadAsStringAsync().Result;
                s = s.Trim();
                if (s.StartsWith("["))
                    return JArray.Parse(s);
                else if (s.StartsWith("{"))
                    return JObject.Parse(s);
                else
                    return s;
            }
            catch (Exception ex)
            {
                return ex.Message;
            }
        }
Advertisements

Patching Assembly Version in TeamCity

We use TeamCity for building our .NET solutions and Octopus Deploy for deployment. We use semantic versioning using the <major>.<minor>.<patch>.<build> pattern, and I wanted to automatically set AssemblyInformationalVersion (a.k.a. product version) in all built assemblies. This was set to 2.0.2 in AssemblyInfo.cs, so I had to add the build number.

This was fairly easy using a build feature in TeamCity. Select the desired build configuration (I called it Build and Publish), and instead of going to build steps, click on build features in the left menu, and select the File Content Replacer type and begin by loading the AssemblyInformationalVersion in AssemblyInfo (C#) template. I then modified the search pattern to:

(^\s*\[\s*assembly\s*:\s*((System\s*\.)?\s*Reflection\s*\.)?\s*AssemblyInformationalVersion(Attribute)?\s*\(\s*@?\")(([0-9\*]+\.?)+)(\"\s*\)\s*\])

This will capture the following groups:

1: [assembly: AssemblyInformationalVersion(“
5: 2.0.2
6: 2
7: “)]

so the replacement is:

$1$5.\%build.number%$7

As AssemblyVersion, I wanted to stick with Microsoft’s standard <major>.<minor>.<build>.<revision>, and in the projects, this was:

[assembly: AssemblyVersion("2.0.*")]

I wanted to change that to

[assembly: AssemblyVersion("2.0.nnn.*")]

The search pattern in case is

(^\s*\[\s*assembly\s*:\s*((System\s*\.)?\s*Reflection\s*\.)?\s*AssemblyVersion(Attribute)?\s*\(\s*@?\")(([0-9\*]+\.)+)[0-9\*]+(\"\s*\)\s*\])

and the replacement

$1$5\%build.number%.*$7