Skip to content

Commit 85ac699

Browse files
Prototype dynamic response headers
Prototype implementation to support #473.
1 parent fca25e8 commit 85ac699

File tree

3 files changed

+210
-30
lines changed

3 files changed

+210
-30
lines changed

src/HttpClientInterception/HttpRequestInterceptionBuilder.cs

Lines changed: 168 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Just Eat, 2017. All rights reserved.
22
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
33

4+
using System.Collections;
5+
using System.Diagnostics.CodeAnalysis;
46
using System.Net;
57

68
namespace JustEat.HttpClientInterception;
@@ -426,10 +428,7 @@ public HttpRequestInterceptionBuilder WithContentHeader(string name, IEnumerable
426428
throw new ArgumentNullException(nameof(values));
427429
}
428430

429-
if (_contentHeaders is null)
430-
{
431-
_contentHeaders = new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);
432-
}
431+
_contentHeaders ??= new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);
433432

434433
if (!_contentHeaders.TryGetValue(name, out var current))
435434
{
@@ -551,10 +550,7 @@ public HttpRequestInterceptionBuilder WithResponseHeader(string name, IEnumerabl
551550
throw new ArgumentNullException(nameof(values));
552551
}
553552

554-
if (_responseHeaders is null)
555-
{
556-
_responseHeaders = new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);
557-
}
553+
_responseHeaders ??= new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);
558554

559555
if (!_responseHeaders.TryGetValue(name, out ICollection<string>? current))
560556
{
@@ -617,6 +613,29 @@ public HttpRequestInterceptionBuilder WithResponseHeaders(IDictionary<string, IC
617613
return this;
618614
}
619615

616+
/// <summary>
617+
/// Sets a delegate to a method that generates any custom HTTP response headers to use.
618+
/// </summary>
619+
/// <param name="headerFactory">Any delegate that creates any custom HTTP response headers to use.</param>
620+
/// <returns>
621+
/// The current <see cref="HttpRequestInterceptionBuilder"/>.
622+
/// </returns>
623+
/// <exception cref="ArgumentNullException">
624+
/// <paramref name="headerFactory"/> is <see langword="null"/>.
625+
/// </exception>
626+
public HttpRequestInterceptionBuilder WithResponseHeaders(
627+
Func<IEnumerable<KeyValuePair<string, ICollection<string>>>> headerFactory)
628+
{
629+
if (headerFactory is null)
630+
{
631+
throw new ArgumentNullException(nameof(headerFactory));
632+
}
633+
634+
_responseHeaders = new DynamicDictionary(headerFactory);
635+
IncrementRevision();
636+
return this;
637+
}
638+
620639
/// <summary>
621640
/// Sets media type for the response body content.
622641
/// </summary>
@@ -869,10 +888,7 @@ public HttpRequestInterceptionBuilder ForRequestHeader(string name, IEnumerable<
869888
throw new ArgumentNullException(nameof(values));
870889
}
871890

872-
if (_requestHeaders is null)
873-
{
874-
_requestHeaders = new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);
875-
}
891+
_requestHeaders ??= new Dictionary<string, ICollection<string>>(StringComparer.OrdinalIgnoreCase);
876892

877893
if (!_requestHeaders.TryGetValue(name, out ICollection<string>? current))
878894
{
@@ -983,40 +999,61 @@ internal HttpInterceptionResponse Build()
983999
Version = _version,
9841000
};
9851001

986-
if (_requestHeaders?.Count > 0)
1002+
if (_requestHeaders is not null)
9871003
{
988-
var headers = new Dictionary<string, IEnumerable<string>>(_requestHeaders.Count);
989-
990-
foreach (var pair in _requestHeaders)
1004+
if (_requestHeaders is DynamicDictionary factory)
9911005
{
992-
headers[pair.Key] = pair.Value;
1006+
response.RequestHeaders = factory;
9931007
}
1008+
else if (_requestHeaders.Count > 0)
1009+
{
1010+
var headers = new Dictionary<string, IEnumerable<string>>(_requestHeaders.Count);
9941011

995-
response.RequestHeaders = headers;
1012+
foreach (var pair in _requestHeaders)
1013+
{
1014+
headers[pair.Key] = pair.Value;
1015+
}
1016+
1017+
response.RequestHeaders = headers;
1018+
}
9961019
}
9971020

998-
if (_responseHeaders?.Count > 0)
1021+
if (_responseHeaders is not null)
9991022
{
1000-
var headers = new Dictionary<string, IEnumerable<string>>(_responseHeaders.Count);
1001-
1002-
foreach (var pair in _responseHeaders)
1023+
if (_responseHeaders is DynamicDictionary factory)
10031024
{
1004-
headers[pair.Key] = pair.Value;
1025+
response.ResponseHeaders = factory;
10051026
}
1027+
else if (_responseHeaders.Count > 0)
1028+
{
1029+
var headers = new Dictionary<string, IEnumerable<string>>(_responseHeaders.Count);
10061030

1007-
response.ResponseHeaders = headers;
1031+
foreach (var pair in _responseHeaders)
1032+
{
1033+
headers[pair.Key] = pair.Value;
1034+
}
1035+
1036+
response.ResponseHeaders = headers;
1037+
}
10081038
}
10091039

1010-
if (_contentHeaders?.Count > 0)
1040+
if (_contentHeaders is not null)
10111041
{
1012-
var headers = new Dictionary<string, IEnumerable<string>>(_contentHeaders.Count);
1013-
1014-
foreach (var pair in _contentHeaders)
1042+
if (_contentHeaders is DynamicDictionary factory)
10151043
{
1016-
headers[pair.Key] = pair.Value;
1044+
response.ContentHeaders = factory;
10171045
}
1046+
else if (_contentHeaders.Count > 0)
1047+
{
1048+
var headers = new Dictionary<string, IEnumerable<string>>(_contentHeaders.Count);
10181049

1019-
response.ContentHeaders = headers;
1050+
foreach (var pair in _contentHeaders)
1051+
{
1052+
headers[pair.Key] = pair.Value;
1053+
}
1054+
1055+
response.ContentHeaders = headers;
1056+
}
10201057
}
10211058

10221059
return response;
@@ -1036,4 +1073,105 @@ private void IncrementRevision()
10361073
_revision++;
10371074
}
10381075
}
1076+
1077+
private sealed class DynamicDictionary :
1078+
IDictionary<string, ICollection<string>>,
1079+
IEnumerable<KeyValuePair<string, IEnumerable<string>>>
1080+
{
1081+
private readonly Func<IEnumerable<KeyValuePair<string, ICollection<string>>>> _generator;
1082+
1083+
internal DynamicDictionary(Func<IEnumerable<KeyValuePair<string, ICollection<string>>>> generator)
1084+
{
1085+
_generator = generator;
1086+
}
1087+
1088+
[ExcludeFromCodeCoverage]
1089+
public ICollection<string> Keys => throw new NotSupportedException();
1090+
1091+
[ExcludeFromCodeCoverage]
1092+
public ICollection<ICollection<string>> Values => throw new NotSupportedException();
1093+
1094+
[ExcludeFromCodeCoverage]
1095+
public int Count => throw new NotSupportedException();
1096+
1097+
[ExcludeFromCodeCoverage]
1098+
public bool IsReadOnly => true;
1099+
1100+
[ExcludeFromCodeCoverage]
1101+
public ICollection<string> this[string key]
1102+
{
1103+
get => throw new NotSupportedException();
1104+
set => throw new NotSupportedException();
1105+
}
1106+
1107+
public IEnumerator<KeyValuePair<string, ICollection<string>>> GetEnumerator()
1108+
=> _generator().GetEnumerator();
1109+
1110+
IEnumerator IEnumerable.GetEnumerator()
1111+
=> GetEnumerator();
1112+
1113+
IEnumerator<KeyValuePair<string, IEnumerable<string>>> IEnumerable<KeyValuePair<string, IEnumerable<string>>>.GetEnumerator()
1114+
=> new EnumeratorAdapter(GetEnumerator());
1115+
1116+
[ExcludeFromCodeCoverage]
1117+
public void Add(string key, ICollection<string> value)
1118+
=> throw new NotSupportedException();
1119+
1120+
[ExcludeFromCodeCoverage]
1121+
public void Add(KeyValuePair<string, ICollection<string>> item)
1122+
=> throw new NotSupportedException();
1123+
1124+
[ExcludeFromCodeCoverage]
1125+
public void Clear()
1126+
=> throw new NotSupportedException();
1127+
1128+
[ExcludeFromCodeCoverage]
1129+
public bool Contains(KeyValuePair<string, ICollection<string>> item)
1130+
=> throw new NotSupportedException();
1131+
1132+
[ExcludeFromCodeCoverage]
1133+
public bool ContainsKey(string key)
1134+
=> throw new NotSupportedException();
1135+
1136+
[ExcludeFromCodeCoverage]
1137+
public void CopyTo(KeyValuePair<string, ICollection<string>>[] array, int arrayIndex)
1138+
=> throw new NotSupportedException();
1139+
1140+
[ExcludeFromCodeCoverage]
1141+
public bool Remove(string key)
1142+
=> throw new NotSupportedException();
1143+
1144+
[ExcludeFromCodeCoverage]
1145+
public bool Remove(KeyValuePair<string, ICollection<string>> item)
1146+
=> throw new NotSupportedException();
1147+
1148+
[ExcludeFromCodeCoverage]
1149+
public bool TryGetValue(string key, out ICollection<string> value)
1150+
=> throw new NotSupportedException();
1151+
1152+
private sealed class EnumeratorAdapter : IEnumerator<KeyValuePair<string, IEnumerable<string>>>
1153+
{
1154+
private readonly IEnumerator<KeyValuePair<string, ICollection<string>>> _enumerator;
1155+
1156+
internal EnumeratorAdapter(IEnumerator<KeyValuePair<string, ICollection<string>>> enumerator)
1157+
{
1158+
_enumerator = enumerator;
1159+
}
1160+
1161+
public KeyValuePair<string, IEnumerable<string>> Current
1162+
=> new(_enumerator.Current.Key, _enumerator.Current.Value);
1163+
1164+
object IEnumerator.Current
1165+
=> _enumerator.Current;
1166+
1167+
public void Dispose()
1168+
=> _enumerator.Dispose();
1169+
1170+
public bool MoveNext()
1171+
=> _enumerator.MoveNext();
1172+
1173+
public void Reset()
1174+
=> _enumerator.Reset();
1175+
}
1176+
}
10391177
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithResponseHeaders(System.Func<System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, System.Collections.Generic.ICollection<string!>!>>!>! headerFactory) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder!

tests/HttpClientInterception.Tests/Examples.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,4 +755,45 @@ static bool IsHttpGetForJustEatGitHubOrg(HttpRequestMessage request)
755755
// Verify that the expected number of attempts were made
756756
count.ShouldBe(retryCount);
757757
}
758+
759+
[Fact]
760+
public static async Task Dynamic_Headers()
761+
{
762+
// Arrange
763+
int counter = 0;
764+
765+
var builder = new HttpRequestInterceptionBuilder()
766+
.ForHost("service.local")
767+
.ForPath("resource")
768+
.WithJsonContent(new object())
769+
.WithResponseHeaders(() =>
770+
{
771+
return new Dictionary<string, ICollection<string>>()
772+
{
773+
["x-count"] = new[] { (++counter).ToString(CultureInfo.InvariantCulture) },
774+
};
775+
});
776+
777+
var options = new HttpClientInterceptorOptions()
778+
.Register(builder);
779+
780+
using var client = options.CreateHttpClient();
781+
using var body = new StringContent(@"{ ""FirstName"": ""John"" }");
782+
783+
// Act
784+
using var response1 = await client.GetAsync("http://service.local/resource");
785+
786+
// Assert
787+
response1.Headers.TryGetValues("x-count", out var values).ShouldBeTrue();
788+
values.ShouldNotBeNull();
789+
values.ShouldBe(new[] { "1" });
790+
791+
// Act
792+
using var response2 = await client.GetAsync("http://service.local/resource");
793+
794+
// Assert
795+
response2.Headers.TryGetValues("x-count", out values).ShouldBeTrue();
796+
values.ShouldNotBeNull();
797+
values.ShouldBe(new[] { "2" });
798+
}
758799
}

0 commit comments

Comments
 (0)