From c204a866edb103fd2e56fc78cf3c6f4882f4cb35 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 25 Mar 2024 09:32:11 -0700 Subject: [PATCH 1/3] Improve string formatting --- .../ApiDescriptionExtensions.cs | 9 +++++++-- .../ApiVersionParameterDescriptionContext.cs | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs index a0cf3835..30aaf0cd 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs @@ -106,9 +106,14 @@ public static bool TryUpdateRelativePathAndRemoveApiVersionParameter( this ApiDe return false; } - var token = '{' + parameter.Name + '}'; + Span token = stackalloc char[parameter.Name.Length + 2]; + + token[0] = '{'; + token[^1] = '}'; + parameter.Name.AsSpan().CopyTo( token.Slice( 1, parameter.Name.Length ) ); + var value = apiVersion.ToString( options.SubstitutionFormat, CultureInfo.InvariantCulture ); - var newRelativePath = relativePath.Replace( token, value, StringComparison.Ordinal ); + var newRelativePath = relativePath.Replace( token.ToString(), value, StringComparison.Ordinal ); if ( relativePath == newRelativePath ) { diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs index 136ad268..da70b954 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs @@ -9,6 +9,7 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; +using System.Runtime.CompilerServices; using static Asp.Versioning.ApiVersionParameterLocation; using static System.Linq.Enumerable; using static System.StringComparison; @@ -304,7 +305,7 @@ routeInfo.Constraints is IEnumerable constraints && continue; } - var token = $"{parameter.Name}:{constraintName}"; + var token = FormatToken( parameter.Name, constraintName ); parameterDescription.Name = parameter.Name; description.RelativePath = relativePath.Replace( token, parameter.Name, Ordinal ); @@ -375,7 +376,7 @@ routeInfo.Constraints is IEnumerable constraints && }, Source = BindingSource.Path, }; - var token = $"{parameter.Name}:{constraintName}"; + var token = FormatToken( parameter.Name!, constraintName! ); description.RelativePath = relativePath.Replace( token, parameter.Name, Ordinal ); description.ParameterDescriptions.Insert( 0, result ); @@ -457,4 +458,18 @@ private static bool FirstParameterIsOptional( return apiVersion == defaultApiVersion; } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static string FormatToken( ReadOnlySpan parameterName, ReadOnlySpan constraintName ) + { + var left = parameterName.Length; + var right = constraintName.Length; + Span token = stackalloc char[left + right + 1]; + + parameterName.CopyTo( token[..left] ); + token[left] = ':'; + constraintName.CopyTo( token.Slice( left + 1, right ) ); + + return token.ToString(); + } } \ No newline at end of file From 6c1ba78da7de5388f6b91f3e0473592afb20a887 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 25 Mar 2024 09:32:46 -0700 Subject: [PATCH 2/3] Use collection initializers --- .../ApiExplorer/PartialODataDescriptionProvider.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs index fc1ff1b6..47eac901 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs @@ -145,14 +145,10 @@ private static int ODataOrder() => new ODataApiDescriptionProvider( new StubModelMetadataProvider(), new StubModelTypeBuilder(), - new OptionsFactory( - Enumerable.Empty>(), - Enumerable.Empty>() ), + new OptionsFactory( [], [] ), Opts.Create( new ODataApiExplorerOptions( - new( - new StubODataApiVersionCollectionProvider(), - Enumerable.Empty() ) ) ) ).Order; + new( new StubODataApiVersionCollectionProvider(), [] ) ) ) ).Order; [MethodImpl( MethodImplOptions.AggressiveInlining )] private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) => From 2c9061c60ca666d40c172cf7789ab4b478f70677 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 25 Mar 2024 09:34:37 -0700 Subject: [PATCH 3/3] Support collection parameters in functions. Fixes #999 --- .../ODataApiDescriptionProvider.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs index 5736e8f4..fdb4ff8d 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs @@ -142,6 +142,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) else { UpdateModelTypes( result, matched ); + UpdateFunctionCollectionParameters( result, matched ); } } @@ -456,6 +457,75 @@ private void UpdateModelTypes( ApiDescription description, IODataRoutingMetadata } } + private static void UpdateFunctionCollectionParameters( ApiDescription description, IODataRoutingMetadata metadata ) + { + var parameters = description.ParameterDescriptions; + + if ( parameters.Count == 0 ) + { + return; + } + + var function = default( IEdmFunction ); + var mapping = default( IDictionary ); + + for ( var i = 0; i < metadata.Template.Count; i++ ) + { + var segment = metadata.Template[i]; + + if ( segment is FunctionSegmentTemplate func ) + { + function = func.Function; + mapping = func.ParameterMappings; + break; + } + else if ( segment is FunctionImportSegmentTemplate import ) + { + function = import.FunctionImport.Function; + mapping = import.ParameterMappings; + break; + } + } + + if ( function is null || mapping is null ) + { + return; + } + + var name = default( string ); + + foreach ( var parameter in function.Parameters ) + { + if ( parameter.Type.IsCollection() && + mapping.TryGetValue( parameter.Name, out name ) && + parameters.SingleOrDefault( p => p.Name == name ) is { } param ) + { + param.Source = BindingSource.Path; + break; + } + } + + var path = description.RelativePath; + + if ( string.IsNullOrEmpty( name ) || string.IsNullOrEmpty( path ) ) + { + return; + } + + var span = name.AsSpan(); + Span oldValue = stackalloc char[name.Length + 2]; + Span newValue = stackalloc char[name.Length + 4]; + + newValue[1] = oldValue[0] = '{'; + newValue[^2] = oldValue[^1] = '}'; + newValue[0] = '['; + newValue[^1] = ']'; + span.CopyTo( oldValue.Slice( 1, name.Length ) ); + span.CopyTo( newValue.Slice( 2, name.Length ) ); + + description.RelativePath = path.Replace( oldValue.ToString(), newValue.ToString(), Ordinal ); + } + private sealed class ApiDescriptionComparer : IEqualityComparer { private readonly IEqualityComparer comparer = StringComparer.OrdinalIgnoreCase;