Skip to content

Commit

Permalink
dh: Teach to handle multiple correlationIds
Browse files Browse the repository at this point in the history
  • Loading branch information
rmandvikar committed Sep 24, 2023
1 parent 75dfbe9 commit dc639ed
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 11 deletions.
36 changes: 27 additions & 9 deletions src/rm.DelegatingHandlers/CorrelationIdHandler.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using rm.Extensions;

namespace rm.DelegatingHandlers;
Expand Down Expand Up @@ -31,23 +33,39 @@ protected override async Task<HttpResponseMessage> SendAsync(
throw new InvalidOperationException($"{RequestHeaders.CorrelationId} header already present with value '{value}'");
}

var correlationId = correlationIdContext.GetValue();
if (correlationId.IsNullOrWhiteSpace())
var correlationIds = correlationIdContext.GetValue().Where(c => !c.IsNullOrWhiteSpace());
if (correlationIds.IsNullOrEmpty())
{
throw new InvalidOperationException($"{RequestHeaders.CorrelationId} header value cannot be null/empty/whitespace");
}

request.Headers.Add(RequestHeaders.CorrelationId, correlationId);
foreach (var correlationId in correlationIds)
{
request.Headers.Add(RequestHeaders.CorrelationId, correlationId);
}

var response = await base.SendAsync(request, cancellationToken)
.ConfigureAwait(false);

// add correlationId to response header
// if correlationId already present, noop
// assume valid correlationId value if already present
if (!response.Headers.Contains(ResponseHeaders.CorrelationId))
// add correlationId(s) to response header
// if correlationId(s) already present, dedupe but can't guarantee order
response.Headers.TryGetValues(ResponseHeaders.CorrelationId, out var responseCorrelationIds);
responseCorrelationIds = responseCorrelationIds.EmptyIfDefault();
foreach (var correlationId in correlationIds)
{
response.Headers.Add(ResponseHeaders.CorrelationId, correlationId);
var found = false;
foreach (var responseCorrelationId in responseCorrelationIds)
{
if (correlationId == responseCorrelationId)
{
found = true;
break;
}
}
if (!found)
{
response.Headers.Add(ResponseHeaders.CorrelationId, correlationId);
}
}

return response;
Expand All @@ -62,5 +80,5 @@ public interface ICorrelationIdContext
/// <summary>
/// Returns correlationId value.
/// </summary>
string GetValue();
StringValues GetValue();
}
1 change: 1 addition & 0 deletions src/rm.DelegatingHandlers/rm.DelegatingHandlers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<ItemGroup>
<PackageReference Include="IsExternalInit" Version="1.0.3" />
<PackageReference Include="K4os.Hash.xxHash" Version="1.0.7" />
<PackageReference Include="Microsoft.Extensions.Primitives" Version="2.1.6" />
<PackageReference Include="Polly" Version="7.2.2" />
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageReference Include="rm.Extensions" Version="2.8.2" />
Expand Down
126 changes: 124 additions & 2 deletions tests/rm.DelegatingHandlersTest/CorrelationIdHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Net;
using AutoFixture;
using AutoFixture.AutoMoq;
using Microsoft.Extensions.Primitives;
using Moq;
using NUnit.Framework;
using rm.DelegatingHandlers;
Expand Down Expand Up @@ -32,11 +33,88 @@ public async Task Adds_CorrelationId()
}

[Test]
public async Task Does_Not_Add_CorrelationId_To_Response_If_Already_Present()
public async Task Adds_Multiple_CorrelationIds()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var shortCircuitingCannedResponseHandler = new ShortCircuitingCannedResponseHandler(new HttpResponseMessage(HttpStatusCode.OK));
var correlationIdAlreadyPresent = "oops!";
var correlationId1 = "boom1!";
var correlationId2 = "boom2!";
var correlationIdContextMock = fixture.Freeze<Mock<ICorrelationIdContext>>();
correlationIdContextMock.Setup(x => x.GetValue()).Returns(new StringValues(new[] { correlationId1, correlationId2, }));
var correlationIdHandler = fixture.Create<CorrelationIdHandler>();
using var invoker = HttpMessageInvokerFactory.Create(
correlationIdHandler, shortCircuitingCannedResponseHandler);
using var request = new HttpRequestMessage();

using var response = await invoker.SendAsync(request, CancellationToken.None);

Assert.IsTrue(request.Headers.Contains(RequestHeaders.CorrelationId));
Assert.AreEqual(new[] { correlationId1, correlationId2 }, request.Headers.GetValues(RequestHeaders.CorrelationId));

Assert.IsTrue(response.Headers.Contains(RequestHeaders.CorrelationId));
Assert.AreEqual(new[] { correlationId1, correlationId2 }, response.Headers.GetValues(RequestHeaders.CorrelationId));
}

[Test]
public void Throws_If_CorrelationId_Values_Is_Null_Value()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var shortCircuitingCannedResponseHandler = new ShortCircuitingCannedResponseHandler(new HttpResponseMessage(HttpStatusCode.OK));
var correlationIdContextMock = fixture.Freeze<Mock<ICorrelationIdContext>>();
correlationIdContextMock.Setup(x => x.GetValue()).Returns(new StringValues(new string[] { null!, }));
var correlationIdHandler = fixture.Create<CorrelationIdHandler>();
using var invoker = HttpMessageInvokerFactory.Create(
correlationIdHandler, shortCircuitingCannedResponseHandler);
using var request = new HttpRequestMessage();

Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
using var response = await invoker.SendAsync(request, CancellationToken.None);
});
}

[Test]
public void Throws_If_CorrelationId_Values_Is_Empty_Value()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var shortCircuitingCannedResponseHandler = new ShortCircuitingCannedResponseHandler(new HttpResponseMessage(HttpStatusCode.OK));
var correlationIdContextMock = fixture.Freeze<Mock<ICorrelationIdContext>>();
correlationIdContextMock.Setup(x => x.GetValue()).Returns(new StringValues(new string[] { }));
var correlationIdHandler = fixture.Create<CorrelationIdHandler>();
using var invoker = HttpMessageInvokerFactory.Create(
correlationIdHandler, shortCircuitingCannedResponseHandler);
using var request = new HttpRequestMessage();

Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
using var response = await invoker.SendAsync(request, CancellationToken.None);
});
}

[Test]
public void Throws_If_CorrelationId_Values_Is_WhiteSpace_Value()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var shortCircuitingCannedResponseHandler = new ShortCircuitingCannedResponseHandler(new HttpResponseMessage(HttpStatusCode.OK));
var correlationIdContextMock = fixture.Freeze<Mock<ICorrelationIdContext>>();
correlationIdContextMock.Setup(x => x.GetValue()).Returns(new StringValues(new string[] { "" }));
var correlationIdHandler = fixture.Create<CorrelationIdHandler>();
using var invoker = HttpMessageInvokerFactory.Create(
correlationIdHandler, shortCircuitingCannedResponseHandler);
using var request = new HttpRequestMessage();

Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
using var response = await invoker.SendAsync(request, CancellationToken.None);
});
}

[Test]
public async Task Does_Not_Add_CorrelationId_To_Response_If_Same_Value_Already_Present()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var shortCircuitingCannedResponseHandler = new ShortCircuitingCannedResponseHandler(new HttpResponseMessage(HttpStatusCode.OK));
var correlationIdAlreadyPresent = "boom!";
var addsCorrelationIdToResponseHandler = new AddsCorrelationIdToResponseHandler(correlationIdAlreadyPresent);
var correlationId = "boom!";
var correlationIdContextMock = fixture.Freeze<Mock<ICorrelationIdContext>>();
Expand All @@ -51,4 +129,48 @@ public async Task Does_Not_Add_CorrelationId_To_Response_If_Already_Present()
Assert.IsTrue(response.Headers.Contains(RequestHeaders.CorrelationId));
Assert.AreEqual(correlationIdAlreadyPresent, response.Headers.GetValues(RequestHeaders.CorrelationId).Single());
}

[Test]
public async Task Adds_CorrelationId_To_Response_If_Different_Value_Present()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var shortCircuitingCannedResponseHandler = new ShortCircuitingCannedResponseHandler(new HttpResponseMessage(HttpStatusCode.OK));
var correlationIdAlreadyPresent = "oops!";
var addsCorrelationIdToResponseHandler = new AddsCorrelationIdToResponseHandler(correlationIdAlreadyPresent);
var correlationId = "boom!";
var correlationIdContextMock = fixture.Freeze<Mock<ICorrelationIdContext>>();
correlationIdContextMock.Setup(x => x.GetValue()).Returns(correlationId);
var correlationIdHandler = fixture.Create<CorrelationIdHandler>();
using var invoker = HttpMessageInvokerFactory.Create(
correlationIdHandler, addsCorrelationIdToResponseHandler, shortCircuitingCannedResponseHandler);
using var request = new HttpRequestMessage();

using var response = await invoker.SendAsync(request, CancellationToken.None);

Assert.IsTrue(response.Headers.Contains(RequestHeaders.CorrelationId));
Assert.AreEqual(new[] { correlationIdAlreadyPresent, correlationId }, response.Headers.GetValues(RequestHeaders.CorrelationId));
}

[Test]
public async Task Adds_CorrelationId_Deduped_To_Response_If_Different_Value_Present()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var shortCircuitingCannedResponseHandler = new ShortCircuitingCannedResponseHandler(new HttpResponseMessage(HttpStatusCode.OK));
var correlationIdAlreadyPresent = "boom2!";
var addsCorrelationIdToResponseHandler = new AddsCorrelationIdToResponseHandler(correlationIdAlreadyPresent);
var correlationId1 = "boom1!";
var correlationId2 = "boom2!";
var correlationIdContextMock = fixture.Freeze<Mock<ICorrelationIdContext>>();
correlationIdContextMock.Setup(x => x.GetValue()).Returns(new StringValues(new[] { correlationId1, correlationId2, }));
var correlationIdHandler = fixture.Create<CorrelationIdHandler>();

using var invoker = HttpMessageInvokerFactory.Create(
correlationIdHandler, addsCorrelationIdToResponseHandler, shortCircuitingCannedResponseHandler);
using var request = new HttpRequestMessage();

using var response = await invoker.SendAsync(request, CancellationToken.None);

Assert.IsTrue(response.Headers.Contains(RequestHeaders.CorrelationId));
Assert.AreEqual(new[] { correlationId2, correlationId1 }, response.Headers.GetValues(RequestHeaders.CorrelationId));
}
}

0 comments on commit dc639ed

Please sign in to comment.