Skip to content

Commit 7d09a45

Browse files
Support custom reporting HTTP headers. Resolves #875
1 parent caacc44 commit 7d09a45

File tree

9 files changed

+167
-51
lines changed

9 files changed

+167
-51
lines changed

src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,48 @@
22

33
namespace Asp.Versioning.Http;
44

5+
#pragma warning disable CA1815 // Override equals and operator equals on value types
6+
57
using System.Collections;
68

7-
internal readonly struct ApiVersionEnumerator : IEnumerable<ApiVersion>
9+
/// <summary>
10+
/// Represents an enumerator of API versions from a HTTP header.
11+
/// </summary>
12+
public readonly struct ApiVersionEnumerator : IEnumerable<ApiVersion>
813
{
9-
private const string ApiSupportedVersions = "api-supported-versions";
10-
private const string ApiDeprecatedVersions = "api-deprecated-versions";
1114
private readonly IEnumerable<string> values;
12-
private readonly IApiVersionParser? parser;
15+
private readonly IApiVersionParser parser;
1316

14-
private ApiVersionEnumerator( IEnumerable<string> values, IApiVersionParser? parser = default )
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="ApiVersionEnumerator"/> struct.
19+
/// </summary>
20+
/// <param name="response">The HTTP response to create the enumerator from.</param>
21+
/// <param name="headerName">The HTTP header name to enumerate.</param>
22+
/// <param name="parser">The optional <see cref="IApiVersionParser">API version parser</see>.</param>
23+
public ApiVersionEnumerator(
24+
HttpResponseMessage response,
25+
string headerName,
26+
IApiVersionParser? parser = default )
1527
{
16-
this.values = values;
17-
this.parser = parser;
28+
if ( response == null )
29+
{
30+
throw new ArgumentNullException( nameof( response ) );
31+
}
32+
33+
if ( string.IsNullOrEmpty( headerName ) )
34+
{
35+
throw new ArgumentNullException( nameof( headerName ) );
36+
}
37+
38+
this.values =
39+
response.Headers.TryGetValues( headerName, out var values )
40+
? values
41+
: Enumerable.Empty<string>();
42+
43+
this.parser = parser ?? ApiVersionParser.Default;
1844
}
1945

46+
/// <inheritdoc />
2047
public IEnumerator<ApiVersion> GetEnumerator()
2148
{
2249
using var iterator = values.GetEnumerator();
@@ -26,8 +53,6 @@ public IEnumerator<ApiVersion> GetEnumerator()
2653
yield break;
2754
}
2855

29-
var parser = this.parser ?? ApiVersionParser.Default;
30-
3156
if ( parser.TryParse( iterator.Current, out var value ) )
3257
{
3358
yield return value!;
@@ -43,28 +68,4 @@ public IEnumerator<ApiVersion> GetEnumerator()
4368
}
4469

4570
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
46-
47-
internal static ApiVersionEnumerator Supported(
48-
HttpResponseMessage response,
49-
IApiVersionParser? parser = default )
50-
{
51-
if ( response.Headers.TryGetValues( ApiSupportedVersions, out var values ) )
52-
{
53-
return new( values, parser );
54-
}
55-
56-
return new( Enumerable.Empty<string>() );
57-
}
58-
59-
internal static ApiVersionEnumerator Deprecated(
60-
HttpResponseMessage response,
61-
IApiVersionParser? parser = default )
62-
{
63-
if ( response.Headers.TryGetValues( ApiDeprecatedVersions, out var values ) )
64-
{
65-
return new( values, parser );
66-
}
67-
68-
return new( Enumerable.Empty<string>() );
69-
}
7071
}

src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class ApiVersionHandler : DelegatingHandler
99
{
1010
private readonly IApiVersionWriter apiVersionWriter;
1111
private readonly ApiVersion apiVersion;
12+
private readonly ApiVersionHeaderEnumerable enumerable;
1213
private readonly IApiNotification notification;
1314
private readonly IApiVersionParser parser;
1415

@@ -22,16 +23,20 @@ public class ApiVersionHandler : DelegatingHandler
2223
/// that is signaled when changes to an API are detected.</param>
2324
/// <param name="parser">The optional <see cref="IApiVersionParser">parser</see> used to process
2425
/// API versions from HTTP responses.</param>
26+
/// <param name="enumerable">The optional <see cref="ApiVersionHeaderEnumerable">enumerable</see>
27+
/// used to enumerate retrieved API versions from HTTP responses.</param>
2528
public ApiVersionHandler(
2629
IApiVersionWriter apiVersionWriter,
2730
ApiVersion apiVersion,
2831
IApiNotification? notification = default,
29-
IApiVersionParser? parser = default )
32+
IApiVersionParser? parser = default,
33+
ApiVersionHeaderEnumerable? enumerable = default)
3034
{
3135
this.apiVersionWriter = apiVersionWriter ?? throw new ArgumentNullException( nameof( apiVersionWriter ) );
3236
this.apiVersion = apiVersion ?? throw new ArgumentNullException( nameof( apiVersion ) );
3337
this.notification = notification ?? ApiNotification.None;
3438
this.parser = parser ?? ApiVersionParser.Default;
39+
this.enumerable = enumerable ?? new();
3540
}
3641

3742
/// <inheritdoc />
@@ -67,7 +72,7 @@ protected virtual bool IsDeprecatedApi( HttpResponseMessage response )
6772
throw new ArgumentNullException( nameof( response ) );
6873
}
6974

70-
foreach ( var reportedApiVersion in ApiVersionEnumerator.Deprecated( response, parser ) )
75+
foreach ( var reportedApiVersion in enumerable.Deprecated( response, parser ) )
7176
{
7277
// don't use '==' operator because a derived type may not overload it
7378
if ( apiVersion.CompareTo( reportedApiVersion ) == 0 )
@@ -91,7 +96,7 @@ protected virtual bool IsNewApiAvailable( HttpResponseMessage response )
9196
throw new ArgumentNullException( nameof( response ) );
9297
}
9398

94-
foreach ( var reportedApiVersion in ApiVersionEnumerator.Supported( response, parser ) )
99+
foreach ( var reportedApiVersion in enumerable.Supported( response, parser ) )
95100
{
96101
// don't use '<' operator because a derived type may not overload it
97102
if ( apiVersion.CompareTo( reportedApiVersion ) < 0 )
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.Http;
4+
#pragma warning disable CA1815 // Override equals and operator equals on value types
5+
6+
/// <summary>
7+
/// Represents the enumerable object used to create API version enumerators.
8+
/// </summary>
9+
public sealed class ApiVersionHeaderEnumerable
10+
{
11+
private const string ApiSupportedVersions = "api-supported-versions";
12+
private const string ApiDeprecatedVersions = "api-deprecated-versions";
13+
private readonly string apiSupportedVersionsName;
14+
private readonly string apiDeprecatedVersionsName;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="ApiVersionHeaderEnumerable"/> class.
18+
/// </summary>
19+
/// <param name="supportedHeaderName">The HTTP header name used for supported API versions.
20+
/// The default value is "api-supported-versions".</param>
21+
/// <param name="deprecatedHeaderName">THe HTTP header name used for deprecated API versions.
22+
/// The default value is "api-deprecated-versions".</param>
23+
public ApiVersionHeaderEnumerable(
24+
string supportedHeaderName = ApiSupportedVersions,
25+
string deprecatedHeaderName = ApiDeprecatedVersions )
26+
{
27+
if ( string.IsNullOrEmpty( apiSupportedVersionsName = supportedHeaderName ) )
28+
{
29+
throw new ArgumentNullException( nameof( supportedHeaderName ) );
30+
}
31+
32+
if ( string.IsNullOrEmpty( apiDeprecatedVersionsName = deprecatedHeaderName ) )
33+
{
34+
throw new ArgumentNullException( nameof( deprecatedHeaderName ) );
35+
}
36+
}
37+
38+
/// <summary>
39+
/// Creates and returns an enumerator for supported API versions.
40+
/// </summary>
41+
/// <param name="response">The <see cref="HttpResponseMessage">HTTP response</see> to evaluate.</param>
42+
/// <param name="parser">The optional <see cref="IApiVersionParser">API version parser</see>.</param>
43+
/// <returns>A new <see cref="ApiVersionEnumerator"/>.</returns>
44+
public ApiVersionEnumerator Supported(
45+
HttpResponseMessage response,
46+
IApiVersionParser? parser = default ) =>
47+
new( response, apiSupportedVersionsName, parser );
48+
49+
/// <summary>
50+
/// Creates and returns an enumerator for deprecated API versions.
51+
/// </summary>
52+
/// <param name="response">The <see cref="HttpResponseMessage">HTTP response</see> to evaluate.</param>
53+
/// <param name="parser">The optional <see cref="IApiVersionParser">API version parser</see>.</param>
54+
/// <returns>A new <see cref="ApiVersionEnumerator"/>.</returns>
55+
public ApiVersionEnumerator Deprecated(
56+
HttpResponseMessage response,
57+
IApiVersionParser? parser = default ) =>
58+
new( response, apiDeprecatedVersionsName, parser );
59+
}

src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpClientExtensions.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public static class HttpClientExtensions
1818
/// <param name="requestUrl">The URL to get the API information from.</param>
1919
/// <param name="parser">The optional <see cref="IApiVersionParser">parser</see> used
2020
/// to process retrieved API versions.</param>
21+
/// <param name="enumerable">The optional <see cref="ApiVersionHeaderEnumerable">enumerable</see>
22+
/// used to enumerate retrieved API versions.</param>
2123
/// <param name="cancellationToken">The token that can be used to cancel the operation.</param>
2224
/// <returns>A task containing the retrieved <see cref="ApiInformation">API information</see>.</returns>
2325
/// <remarks>API information is retrieved by sending an OPTIONS request to the specified URL.
@@ -27,10 +29,12 @@ public static Task<ApiInformation> GetApiInformationAsync(
2729
this HttpClient client,
2830
string requestUrl,
2931
IApiVersionParser? parser = default,
32+
ApiVersionHeaderEnumerable? enumerable = default,
3033
CancellationToken cancellationToken = default ) =>
3134
client.GetApiInformationAsync(
3235
new Uri( requestUrl, UriKind.RelativeOrAbsolute ),
3336
parser,
37+
enumerable,
3438
cancellationToken );
3539

3640
/// <summary>
@@ -40,6 +44,8 @@ public static Task<ApiInformation> GetApiInformationAsync(
4044
/// <param name="requestUrl">The URL to get the API information from.</param>
4145
/// <param name="parser">The optional <see cref="IApiVersionParser">parser</see> used
4246
/// to process retrieved API versions.</param>
47+
/// <param name="enumerable">The optional <see cref="ApiVersionHeaderEnumerable">enumerable</see>
48+
/// used to enumerate retrieved API versions.</param>
4349
/// <param name="cancellationToken">The token that can be used to cancel the operation.</param>
4450
/// <returns>A task containing the retrieved <see cref="ApiInformation">API information</see>.</returns>
4551
/// <remarks>API information is retrieved by sending an OPTIONS request to the specified URL.
@@ -49,6 +55,7 @@ public static async Task<ApiInformation> GetApiInformationAsync(
4955
this HttpClient client,
5056
Uri requestUrl,
5157
IApiVersionParser? parser = default,
58+
ApiVersionHeaderEnumerable? enumerable = default,
5259
CancellationToken cancellationToken = default )
5360
{
5461
if ( client == null )
@@ -69,12 +76,13 @@ public static async Task<ApiInformation> GetApiInformationAsync(
6976
}
7077

7178
parser ??= ApiVersionParser.Default;
72-
var versions = new SortedSet<ApiVersion>( ApiVersionEnumerator.Supported( response, parser ) );
79+
enumerable ??= new();
80+
var versions = new SortedSet<ApiVersion>( enumerable.Supported( response, parser ) );
7381
var supported = versions.ToArray();
7482

7583
versions.Clear();
7684

77-
foreach ( var version in ApiVersionEnumerator.Deprecated( response, parser ) )
85+
foreach ( var version in enumerable.Deprecated( response, parser ) )
7886
{
7987
versions.Add( version );
8088
}

src/Client/src/Asp.Versioning.Http.Client/net6.0/ApiVersionHandlerLogger{T}.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@ public class ApiVersionHandlerLogger<T> : ApiNotification
1313
{
1414
private readonly ILogger logger;
1515
private readonly IApiVersionParser parser;
16+
private readonly ApiVersionHeaderEnumerable enumerable;
1617

1718
/// <summary>
1819
/// Initializes a new instance of the <see cref="ApiVersionHandlerLogger{T}"/> class.
1920
/// </summary>
2021
/// <param name="logger">The <see cref="ILogger{TCategoryName}">logger</see> used to log API notifications.</param>
2122
/// <param name="parser">The <see cref="IApiVersionParser">parser</see> used to process API versions.</param>
22-
public ApiVersionHandlerLogger( ILogger<T> logger, IApiVersionParser parser )
23+
/// <param name="enumerable">The <see cref="ApiVersionHeaderEnumerable">enumerable</see> used to enumerate API versions.</param>
24+
public ApiVersionHandlerLogger( ILogger<T> logger, IApiVersionParser parser, ApiVersionHeaderEnumerable enumerable )
2325
{
2426
this.logger = logger ?? throw new ArgumentNullException( nameof( logger ) );
2527
this.parser = parser ?? throw new ArgumentNullException( nameof( parser ) );
28+
this.enumerable = enumerable ?? throw new ArgumentNullException( nameof( enumerable ) );
2629
}
2730

2831
/// <inheritdoc />
@@ -51,7 +54,7 @@ protected override void OnNewApiAvailable( ApiNotificationContext context )
5154
var requestUrl = context.Response.RequestMessage!.RequestUri!;
5255
var currentApiVersion = context.ApiVersion;
5356
var sunsetPolicy = context.SunsetPolicy;
54-
var newApiVersion = ApiVersionEnumerator.Supported( context.Response, parser ).Max() ?? currentApiVersion;
57+
var newApiVersion = enumerable.Supported( context.Response, parser ).Max() ?? currentApiVersion;
5558

5659
logger.NewApiVersionAvailable( requestUrl, currentApiVersion, newApiVersion, sunsetPolicy );
5760
}

src/Client/src/Asp.Versioning.Http.Client/net6.0/DependencyInjection/IHttpClientBuilderExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public static IHttpClientBuilder AddApiVersion(
112112

113113
services.TryAddSingleton<IApiVersionWriter, QueryStringApiVersionWriter>();
114114
services.TryAddSingleton<IApiVersionParser, ApiVersionParser>();
115+
services.TryAddTransient<ApiVersionHeaderEnumerable>();
115116
builder.AddHttpMessageHandler( sp => NewApiVersionHandler( sp, apiVersion, apiVersionWriter ) );
116117

117118
return builder;
@@ -142,8 +143,11 @@ private static ApiVersionHandler NewApiVersionHandler(
142143
return default;
143144
}
144145

146+
var enumerable = serviceProvider.GetService<ApiVersionHeaderEnumerable>();
147+
145148
return new ApiVersionHandlerLogger<ApiVersionHandler>(
146149
logger,
147-
parser ?? ApiVersionParser.Default );
150+
parser ?? ApiVersionParser.Default,
151+
enumerable ?? new() );
148152
}
149153
}

src/Client/test/Asp.Versioning.Http.Client.Tests/net6.0/ApiVersionHandlerLoggerTTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public async Task on_api_deprecated_should_log_message()
1414
using var factory = TestLoggerFactory.Create();
1515
var logger = factory.CreateLogger<ApiVersionHandler>();
1616
var parser = ApiVersionParser.Default;
17-
var notification = new ApiVersionHandlerLogger<ApiVersionHandler>( logger, parser );
17+
var notification = new ApiVersionHandlerLogger<ApiVersionHandler>( logger, parser, new() );
1818
var response = new HttpResponseMessage()
1919
{
2020
RequestMessage = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ),
@@ -50,7 +50,7 @@ public async Task on_new_api_available_should_log_message()
5050
using var factory = TestLoggerFactory.Create();
5151
var logger = factory.CreateLogger<ApiVersionHandler>();
5252
var parser = ApiVersionParser.Default;
53-
var notification = new ApiVersionHandlerLogger<ApiVersionHandler>( logger, parser );
53+
var notification = new ApiVersionHandlerLogger<ApiVersionHandler>( logger, parser, new() );
5454
var response = new HttpResponseMessage()
5555
{
5656
RequestMessage = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" ),

src/Client/test/Asp.Versioning.Http.Client.Tests/net6.0/DependencyInjection/IHttpClientBuilderExtensionsTest.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ public async Task add_api_version_should_ignore_registered_writer()
7272
response.RequestMessage.RequestUri.Should().Be( new Uri( "http://tempuri.org?ver=2022-02-01" ) );
7373
}
7474

75+
[Fact]
76+
public void add_api_version_should_register_transient_header_enumerable()
77+
{
78+
// arrange
79+
var services = new ServiceCollection();
80+
81+
services.AddHttpClient( "Test" ).AddApiVersion( 1.0 );
82+
83+
var provider = services.BuildServiceProvider();
84+
85+
// act
86+
var result1 = provider.GetRequiredService<ApiVersionHeaderEnumerable>();
87+
var result2 = provider.GetRequiredService<ApiVersionHeaderEnumerable>();
88+
89+
// assert
90+
result1.Should().NotBeSameAs( result2 );
91+
}
92+
7593
#pragma warning disable CA1812
7694

7795
private sealed class LastHandler : DelegatingHandler

0 commit comments

Comments
 (0)