Skip to content

Commit b41c043

Browse files
authored
Merge pull request #55 from microsoft/feature/multi-valued-headers
- adds support for multi-valued headers
2 parents a71c6db + 0cfa42f commit b41c043

File tree

8 files changed

+262
-17
lines changed

8 files changed

+262
-17
lines changed

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"cSpell.words": [
3+
"Kiota"
4+
]
5+
}

.vscode/tasks.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@
1717
],
1818
"group": "build",
1919
"problemMatcher": "$msCompile"
20+
},
21+
{
22+
"label": "test",
23+
"command": "dotnet",
24+
"type": "process",
25+
"args": [
26+
"test",
27+
"${workspaceFolder}/Microsoft.Kiota.Abstractions.sln",
28+
// Ask dotnet build to generate full paths for file names.
29+
"/property:GenerateFullPaths=true",
30+
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
31+
"/consoleloggerparameters:NoSummary"
32+
],
33+
"group": "test",
34+
"problemMatcher": "$msCompile"
2035
}
2136
]
2237
}

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Changed
1313

14+
## [1.0.0-preview.19] - 2022-12-13
15+
16+
### Changed
17+
18+
- Added support for multi-valued request headers
19+
1420
## [1.0.0-preview.18] - 2022-11-22
1521

1622
### Changed

Microsoft.Kiota.Abstractions.Tests/Authentication/AuthenticationTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ public async Task AnonymousAuthenticationProviderReturnsSameRequestAsync()
2020
HttpMethod = Method.GET,
2121
URI = new Uri("http://localhost")
2222
};
23-
Assert.Empty(testRequest.Headers); // header collection is empty
23+
Assert.Empty(testRequest.Headers.Keys); // header collection is empty
2424

2525
// Act
2626
await anonymousAuthenticationProvider.AuthenticateRequestAsync(testRequest);
2727

2828
// Assert
29-
Assert.Empty(testRequest.Headers); // header collection is still empty
29+
Assert.Empty(testRequest.Headers.Keys); // header collection is still empty
3030

3131
}
3232

@@ -43,15 +43,15 @@ public async Task BaseBearerTokenAuthenticationProviderSetsBearerHeader()
4343
HttpMethod = Method.GET,
4444
URI = new Uri("http://localhost")
4545
};
46-
Assert.Empty(testRequest.Headers); // header collection is empty
46+
Assert.Empty(testRequest.Headers.Keys); // header collection is empty
4747

4848
// Act
4949
await testAuthProvider.AuthenticateRequestAsync(testRequest);
5050

5151
// Assert
52-
Assert.NotEmpty(testRequest.Headers); // header collection is longer empty
53-
Assert.Equal("Authorization", testRequest.Headers.First().Key); // First element is Auth header
54-
Assert.Equal($"Bearer {expectedToken}", testRequest.Headers.First().Value); // First element is Auth header
52+
Assert.NotEmpty(testRequest.Headers.Keys); // header collection is longer empty
53+
Assert.True(testRequest.Headers.ContainsKey("Authorization")); // First element is Auth header
54+
Assert.Equal($"Bearer {expectedToken}", testRequest.Headers["Authorization"].First()); // First element is Auth header
5555
}
5656

5757
[Theory]
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Xunit;
4+
5+
namespace Microsoft.Kiota.Abstractions.Tests;
6+
7+
public class RequestHeadersTests {
8+
[Fact]
9+
public void Defensive() {
10+
var instance = new RequestHeaders();
11+
Assert.Throws<ArgumentNullException>(() => instance.Add(null, "value"));
12+
Assert.Throws<ArgumentNullException>(() => instance.Add("name", (string[])null));
13+
instance.Add("name", Array.Empty<string>());
14+
instance.Add("name", new List<string>());
15+
instance.Add(new KeyValuePair<string, IEnumerable<string>>("name", Array.Empty<string>()));
16+
Assert.Throws<ArgumentNullException>(() => instance[null]);
17+
Assert.Throws<ArgumentNullException>(() => instance.Remove(null));
18+
Assert.Throws<ArgumentNullException>(() => instance.Remove(null, "value"));
19+
Assert.Throws<ArgumentNullException>(() => instance.Remove("name", null));
20+
Assert.Throws<ArgumentNullException>(() => instance.AddAll(null));
21+
instance.ContainsKey(null);
22+
}
23+
[Fact]
24+
public void AddsToNonExistent() {
25+
var instance = new RequestHeaders();
26+
instance.Add("name", "value");
27+
Assert.Equal(new [] { "value" }, instance["name"]);
28+
}
29+
[Fact]
30+
public void AddsToExistent() {
31+
var instance = new RequestHeaders();
32+
instance.Add("name", "value");
33+
instance.Add("name", "value2");
34+
Assert.Equal(new [] { "value", "value2" }, instance["name"]);
35+
}
36+
[Fact]
37+
public void RemovesValue() {
38+
var instance = new RequestHeaders();
39+
instance.Remove("name", "value");
40+
instance.Add("name", "value");
41+
instance.Add("name", "value2");
42+
instance.Remove("name", "value");
43+
Assert.Equal(new [] { "value2" }, instance["name"]);
44+
instance.Remove("name", "value2");
45+
Assert.Null(instance["name"]);
46+
}
47+
[Fact]
48+
public void Removes() {
49+
var instance = new RequestHeaders();
50+
instance.Add("name", "value");
51+
instance.Add("name", "value2");
52+
Assert.True(instance.Remove("name"));
53+
Assert.Null(instance["name"]);
54+
Assert.False(instance.Remove("name"));
55+
}
56+
[Fact]
57+
public void RemovesKVP() {
58+
var instance = new RequestHeaders();
59+
instance.Add("name", "value");
60+
instance.Add("name", "value2");
61+
Assert.True(instance.Remove(new KeyValuePair<string, IEnumerable<string>>("name", new [] { "value", "value2" })));
62+
Assert.Null(instance["name"]);
63+
Assert.False(instance.Remove("name"));
64+
}
65+
[Fact]
66+
public void Clears() {
67+
var instance = new RequestHeaders();
68+
instance.Add("name", "value");
69+
instance.Add("name", "value2");
70+
instance.Clear();
71+
Assert.Null(instance["name"]);
72+
}
73+
[Fact]
74+
public void GetsEnumerator() {
75+
var instance = new RequestHeaders();
76+
instance.Add("name", "value");
77+
instance.Add("name", "value2");
78+
using var enumerator = instance.GetEnumerator();
79+
Assert.True(enumerator.MoveNext());
80+
Assert.Equal("name", enumerator.Current.Key);
81+
Assert.Equal(new [] { "value", "value2" }, enumerator.Current.Value);
82+
Assert.False(enumerator.MoveNext());
83+
}
84+
[Fact]
85+
public void Updates() {
86+
var instance = new RequestHeaders();
87+
instance.Add("name", "value");
88+
instance.Add("name", "value2");
89+
var instance2 = new RequestHeaders();
90+
instance2.AddAll(instance);
91+
Assert.NotEmpty(instance["name"]);
92+
}
93+
}

src/Microsoft.Kiota.Abstractions.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<EmbedUntrackedSources>true</EmbedUntrackedSources>
1515
<Deterministic>true</Deterministic>
1616
<VersionPrefix>1.0.0</VersionPrefix>
17-
<VersionSuffix>preview.18</VersionSuffix>
17+
<VersionSuffix>preview.19</VersionSuffix>
1818
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1919
<SignAssembly>false</SignAssembly>
2020
<DelaySign>false</DelaySign>
@@ -23,7 +23,7 @@
2323
<!-- Enable this line once we go live to prevent breaking changes -->
2424
<!-- <PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion> -->
2525
<PackageReleaseNotes>
26-
- Bumps Tavis.UriTemplates to strongly name binary version
26+
- Adds support for multi-valued headers
2727
</PackageReleaseNotes>
2828
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
2929
<PackageLicenseFile>LICENSE</PackageLicenseFile>

src/RequestHeaders.cs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
namespace Microsoft.Kiota.Abstractions;
7+
8+
/// <summary>Represents a collection of request headers.</summary>
9+
public class RequestHeaders : IDictionary<string,IEnumerable<string>> {
10+
private readonly Dictionary<string, HashSet<string>> _headers = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
11+
/// <summary>
12+
/// Adds values to the header with the specified name.
13+
/// </summary>
14+
/// <param name="headerName">The name of the header to add values to.</param>
15+
/// <param name="headerValues">The values to add to the header.</param>
16+
public void Add(string headerName, params string[] headerValues) {
17+
if(string.IsNullOrEmpty(headerName))
18+
throw new ArgumentNullException(nameof(headerName));
19+
if(headerValues == null)
20+
throw new ArgumentNullException(nameof(headerValues));
21+
if(!headerValues.Any())
22+
return;
23+
if(_headers.TryGetValue(headerName, out var values))
24+
foreach(var headerValue in headerValues)
25+
values.Add(headerValue);
26+
else
27+
_headers.Add(headerName, new HashSet<string>(headerValues));
28+
}
29+
/// <inheritdoc/>
30+
public ICollection<string> Keys => _headers.Keys;
31+
/// <inheritdoc/>
32+
public ICollection<IEnumerable<string>> Values => _headers.Values.Cast<IEnumerable<string>>().ToList();
33+
/// <inheritdoc/>
34+
public int Count => _headers.Count;
35+
/// <inheritdoc/>
36+
public bool IsReadOnly => false;
37+
/// <inheritdoc/>
38+
public IEnumerable<string> this[string key] { get => TryGetValue(key, out var result) ? result : null; set => Add(key, value); }
39+
40+
/// <summary>
41+
/// Removes the specified value from the header with the specified name.
42+
/// </summary>
43+
/// <param name="headerName">The name of the header to remove the value from.</param>
44+
/// <param name="headerValue">The value to remove from the header.</param>
45+
public bool Remove(string headerName, string headerValue) {
46+
if(string.IsNullOrEmpty(headerName))
47+
throw new ArgumentNullException(nameof(headerName));
48+
if(headerValue == null)
49+
throw new ArgumentNullException(nameof(headerValue));
50+
if(_headers.TryGetValue(headerName, out var values)) {
51+
var result = values.Remove(headerValue);
52+
if (!values.Any())
53+
_headers.Remove(headerName);
54+
return result;
55+
}
56+
return false;
57+
}
58+
/// <summary>
59+
/// Adds all the headers values from the specified headers collection.
60+
/// </summary>
61+
/// <param name="headers">The headers to update the current headers with.</param>
62+
public void AddAll(RequestHeaders headers) {
63+
if(headers == null)
64+
throw new ArgumentNullException(nameof(headers));
65+
foreach(var header in headers)
66+
foreach(var value in header.Value)
67+
Add(header.Key, value);
68+
}
69+
/// <summary>
70+
/// Removes all headers.
71+
/// </summary>
72+
public void Clear() {
73+
_headers.Clear();
74+
}
75+
/// <inheritdoc/>
76+
public bool ContainsKey(string key) => !string.IsNullOrEmpty(key) && _headers.ContainsKey(key);
77+
/// <inheritdoc/>
78+
public void Add(string key, IEnumerable<string> value) => Add(key, value?.ToArray());
79+
/// <inheritdoc/>
80+
public bool Remove(string key) {
81+
if(string.IsNullOrEmpty(key))
82+
throw new ArgumentNullException(nameof(key));
83+
return _headers.Remove(key);
84+
}
85+
/// <inheritdoc/>
86+
public bool TryGetValue(string key, out IEnumerable<string> value) {
87+
if(string.IsNullOrEmpty(key))
88+
throw new ArgumentNullException(nameof(key));
89+
if(_headers.TryGetValue(key, out var values)) {
90+
value = values;
91+
return true;
92+
}
93+
value = Enumerable.Empty<string>();
94+
return false;
95+
}
96+
/// <inheritdoc/>
97+
public void Add(KeyValuePair<string, IEnumerable<string>> item) => Add(item.Key, item.Value);
98+
/// <inheritdoc/>
99+
public bool Contains(KeyValuePair<string, IEnumerable<string>> item) => TryGetValue(item.Key, out var values) && item.Value.All(x => values.Contains(x)) && values.Count() == item.Value.Count();
100+
/// <inheritdoc/>
101+
public void CopyTo(KeyValuePair<string, IEnumerable<string>>[] array, int arrayIndex) => throw new NotImplementedException();
102+
/// <inheritdoc/>
103+
public bool Remove(KeyValuePair<string, IEnumerable<string>> item) {
104+
var result = false;
105+
foreach (var value in item.Value)
106+
result |= Remove(item.Key, value);
107+
return result;
108+
}
109+
/// <inheritdoc/>
110+
public IEnumerator<KeyValuePair<string, IEnumerable<string>>> GetEnumerator() => new RequestHeadersEnumerator(_headers.GetEnumerator());
111+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
112+
private sealed class RequestHeadersEnumerator : IEnumerator<KeyValuePair<string, IEnumerable<string>>> {
113+
private readonly IEnumerator _enumerator;
114+
public RequestHeadersEnumerator(IEnumerator enumerator)
115+
{
116+
_enumerator = enumerator;
117+
}
118+
public KeyValuePair<string, IEnumerable<string>> Current => _enumerator.Current is KeyValuePair<string, HashSet<string>> current ? new(current.Key, current.Value) : throw new InvalidOperationException();
119+
120+
object IEnumerator.Current => Current;
121+
122+
public void Dispose() {
123+
(_enumerator as IDisposable)?.Dispose();
124+
GC.SuppressFinalize(this);
125+
}
126+
public bool MoveNext() => _enumerator.MoveNext();
127+
public void Reset() => _enumerator.Reset();
128+
}
129+
}

src/RequestInformation.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,22 +113,19 @@ public void AddQueryParameters(object source)
113113
/// <summary>
114114
/// The Request Headers.
115115
/// </summary>
116-
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
116+
public RequestHeaders Headers { get; private set; } = new ();
117117
/// <summary>
118-
/// Adds request headers to the request.
118+
/// Vanity method to add the headers to the request headers dictionary.
119119
/// </summary>
120-
/// <param name="source">The request headers to add.</param>
121-
public void AddHeaders(IDictionary<string, string> source)
122-
{
123-
if(source == null) return;
124-
foreach(var header in source)
125-
Headers.AddOrReplace(header.Key, header.Value);
120+
public void AddHeaders(RequestHeaders headers) {
121+
if(headers == null) return;
122+
Headers.AddAll(headers);
126123
}
127124
/// <summary>
128125
/// The Request Body.
129126
/// </summary>
130127
public Stream Content { get; set; }
131-
private Dictionary<string, IRequestOption> _requestOptions = new Dictionary<string, IRequestOption>(StringComparer.OrdinalIgnoreCase);
128+
private readonly Dictionary<string, IRequestOption> _requestOptions = new Dictionary<string, IRequestOption>(StringComparer.OrdinalIgnoreCase);
132129
/// <summary>
133130
/// Gets the options for this request. Options are unique by type. If an option of the same type is added twice, the last one wins.
134131
/// </summary>

0 commit comments

Comments
 (0)