From 4093dc0c98b66b3bb92d5747ef3b145edf416a6e Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 22 Mar 2024 10:09:45 -0700 Subject: [PATCH 1/2] Parse versions single header CSVs. Fixes #1070 --- .../ApiVersionEnumerator.cs | 73 ++++++++--- .../ApiVersionEnumeratorTest.cs | 116 ++++++++++++++++++ 2 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionEnumeratorTest.cs diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs index 49139052..6f940aee 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs @@ -5,14 +5,20 @@ namespace Asp.Versioning.Http; +#if NET +using System.Buffers; +#endif using System.Collections; +#if NET +using static System.StringSplitOptions; +#endif /// /// Represents an enumerator of API versions from a HTTP header. /// public readonly struct ApiVersionEnumerator : IEnumerable { - private readonly IEnumerable values; + private readonly string[] values; private readonly IApiVersionParser parser; /// @@ -29,37 +35,72 @@ public ApiVersionEnumerator( ArgumentNullException.ThrowIfNull( response ); ArgumentException.ThrowIfNullOrEmpty( headerName ); - this.values = - response.Headers.TryGetValues( headerName, out var values ) - ? values - : Enumerable.Empty(); - + this.values = response.Headers.TryGetValues( headerName, out var values ) ? values.ToArray() : []; this.parser = parser ?? ApiVersionParser.Default; } /// public IEnumerator GetEnumerator() { - using var iterator = values.GetEnumerator(); +#if NETSTANDARD + for ( var i = 0; i < values.Length; i++ ) + { + var items = values[i].Split( ',' ); - if ( !iterator.MoveNext() ) + for ( var j = 0; j < items.Length; j++ ) + { + var item = items[j].Trim(); + + if ( item.Length > 0 && parser.TryParse( item, out var result ) ) + { + yield return result!; + } + } + } +#else + for ( var i = 0; i < values.Length; i++ ) { - yield break; + var (count, versions) = ParseVersions( values[i] ); + + for ( var j = 0; j < count; j++ ) + { + yield return versions[j]; + } } +#endif + } - if ( parser.TryParse( iterator.Current, out var value ) ) + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +#if NET + private (int Count, ApiVersion[] Results) ParseVersions( ReadOnlySpan value ) + { + var pool = ArrayPool.Shared; + var ranges = pool.Rent( 5 ); + var length = value.Split( ranges, ',', RemoveEmptyEntries | TrimEntries ); + + while ( length >= ranges.Length ) { - yield return value!; + pool.Return( ranges ); + length <<= 1; + ranges = pool.Rent( length ); + length = value.Split( ranges, ',', RemoveEmptyEntries | TrimEntries ); } - while ( iterator.MoveNext() ) + var results = new ApiVersion[length]; + var count = 0; + + for ( var i = 0; i < length; i++ ) { - if ( parser.TryParse( iterator.Current, out value ) ) + var text = value[ranges[i]]; + + if ( text.Length > 0 && parser.TryParse( text, out var result ) ) { - yield return value!; + results[count++] = result; } } - } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + pool.Return( ranges ); + return (count, results); + } +#endif } \ No newline at end of file diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionEnumeratorTest.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionEnumeratorTest.cs new file mode 100644 index 00000000..864a8d92 --- /dev/null +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionEnumeratorTest.cs @@ -0,0 +1,116 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Http; + +public class ApiVersionEnumeratorTest +{ + [Fact] + public void enumerator_should_process_single_header_value() + { + // arrange + var response = new HttpResponseMessage(); + + response.Headers.Add( "api-supported-versions", "1.0" ); + + var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" ); + + // act + var results = enumerator.ToArray(); + + // assert + results.Should().BeEquivalentTo( [new ApiVersion( 1.0 )] ); + } + + [Fact] + public void enumerator_should_process_multiple_header_values() + { + // arrange + var response = new HttpResponseMessage(); + + response.Headers.Add( "api-supported-versions", ["1.0", "2.0"] ); + + var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" ); + + // act + var results = enumerator.ToArray(); + + // assert + results.Should().BeEquivalentTo( new ApiVersion[] { new( 1.0 ), new( 2.0 ) } ); + } + + [Theory] + [InlineData( "1.0,2.0" )] + [InlineData( "1.0, 2.0" )] + [InlineData( "1.0,,2.0" )] + [InlineData( "1.0, abc, 2.0" )] + public void enumerator_should_process_single_header_comma_separated_values( string value ) + { + // arrange + var response = new HttpResponseMessage(); + + response.Headers.Add( "api-supported-versions", [value] ); + + var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" ); + + // act + var results = enumerator.ToArray(); + + // assert + results.Should().BeEquivalentTo( new ApiVersion[] { new( 1.0 ), new( 2.0 ) } ); + } + + [Fact] + public void enumerator_should_process_many_header_comma_separated_values() + { + // arrange + const string Value = "1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0"; + var response = new HttpResponseMessage(); + + response.Headers.Add( "api-supported-versions", [Value] ); + + var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" ); + + // act + var results = enumerator.ToArray(); + + // assert + results.Should().BeEquivalentTo( + new ApiVersion[] + { + new( 1.0 ), + new( 2.0 ), + new( 3.0 ), + new( 4.0 ), + new( 5.0 ), + new( 6.0 ), + new( 7.0 ), + new( 8.0 ), + new( 9.0 ), + new( 10.0 ), + } ); + } + + [Fact] + public void enumerator_should_process_multiple_header_comma_separated_values() + { + // arrange + var response = new HttpResponseMessage(); + + response.Headers.Add( "api-supported-versions", ["1.0, 2.0", "3.0, 4.0"] ); + + var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" ); + + // act + var results = enumerator.ToArray(); + + // assert + results.Should().BeEquivalentTo( + new ApiVersion[] + { + new( 1.0 ), + new( 2.0 ), + new( 3.0 ), + new( 4.0 ), + } ); + } +} \ No newline at end of file From 6642cd86ba1f396520b956953aa8a3d8df44888d Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 22 Mar 2024 10:10:05 -0700 Subject: [PATCH 2/2] Bump version and add release notes --- .../Asp.Versioning.Http.Client.csproj | 2 +- src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj index 75416005..e508c993 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj +++ b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj @@ -1,7 +1,7 @@  - 8.0.0 + 8.0.1 8.0.0.0 $(DefaultTargetFramework);netstandard1.1;netstandard2.0 Asp.Versioning.Http diff --git a/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt b/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt index 5f282702..0cf60623 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt +++ b/src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +Parse single header CSVs ([#1070](https://github.com/dotnet/aspnet-api-versioning/issues/1070)) \ No newline at end of file