Skip to content

Issue 1025 Fix #1027

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,14 @@ public partial class ApiExplorerOptions
/// </summary>
/// <value>The name associated with the <see cref="ApiVersionRouteConstraint">API version route constraint</see>.</value>
public string RouteConstraintName => options.Value.RouteConstraintName;

/// <summary>
/// Gets or sets the API version selector.
/// </summary>
/// <value>An <see cref="IApiVersionSelector">API version selector</see> object.</value>
public IApiVersionSelector ApiVersionSelector
{
get => apiVersionSelector ?? options.Value.ApiVersionSelector;
set => apiVersionSelector = value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ public class ApiVersionParameterDescriptionContext : IApiVersionParameterDescrip
public ApiVersionParameterDescriptionContext( ApiDescription apiDescription, ApiVersion apiVersion, ApiExplorerOptions options )
{
Options = options ?? throw new ArgumentNullException( nameof( options ) );
ApiDescription = apiDescription;
ApiVersion = apiVersion;
optional = options.AssumeDefaultVersionWhenUnspecified && apiVersion == options.DefaultApiVersion;
ApiDescription = apiDescription ?? throw new ArgumentNullException( nameof( apiDescription ) );
ApiVersion = apiVersion ?? throw new ArgumentNullException( nameof( apiVersion ) );
optional = FirstParameterIsOptional( apiDescription, apiVersion, options );
}

/// <summary>
Expand Down Expand Up @@ -242,4 +242,26 @@ private static void CloneFormattersAndAddMediaTypeParameter( NameValueHeaderValu
formatters.Add( formatter );
}
}

private static bool FirstParameterIsOptional(
ApiDescription apiDescription,
ApiVersion apiVersion,
ApiExplorerOptions options )
{
if ( !options.AssumeDefaultVersionWhenUnspecified )
{
return false;
}

var mapping = ApiVersionMapping.Explicit | ApiVersionMapping.Implicit;
var model = apiDescription.ActionDescriptor.GetApiVersionMetadata().Map( mapping );
ApiVersion defaultApiVersion;

using ( var request = new HttpRequestMessage() )
{
defaultApiVersion = options.ApiVersionSelector.SelectVersion( request, model );
}

return apiVersion == defaultApiVersion;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,19 @@ public void add_parameter_should_add_optional_parameter_when_allowed()
var configuration = new HttpConfiguration();
var action = NewActionDescriptor();
var description = new ApiDescription() { ActionDescriptor = action };
var version = new ApiVersion( 1, 0 );
var options = new ApiExplorerOptions( configuration );
var version = new ApiVersion( 2.0 );
var options = new ApiExplorerOptions( configuration )
{
ApiVersionSelector = new ConstantApiVersionSelector( version ),
};

action.Configuration = configuration;
configuration.AddApiVersioning( o => o.AssumeDefaultVersionWhenUnspecified = true );
configuration.AddApiVersioning(
options =>
{
options.DefaultApiVersion = ApiVersion.Default;
options.AssumeDefaultVersionWhenUnspecified = true;
} );

var context = new ApiVersionParameterDescriptionContext( description, version, options );

Expand All @@ -255,7 +263,7 @@ public void add_parameter_should_add_optional_parameter_when_allowed()
ParameterDescriptor = new
{
ParameterName = "api-version",
DefaultValue = "1.0",
DefaultValue = "2.0",
IsOptional = true,
Configuration = configuration,
ActionDescriptor = action,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,12 @@ public ODataApiExplorerOptionsFactory(
}

/// <inheritdoc />
protected override ODataApiExplorerOptions CreateInstance( string name ) =>
new( new( CollateApiVersions( providers, Options ), modelConfigurations ) );
protected override ODataApiExplorerOptions CreateInstance( string name )
{
var options = new ODataApiExplorerOptions( new( CollateApiVersions( providers, Options ), modelConfigurations ) );
CopyOptions( Options, options );
return options;
}

private static ODataApiVersionCollectionProvider CollateApiVersions(
IApiVersionMetadataCollationProvider[] providers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Asp.Versioning.ApiExplorer;

using Asp.Versioning.Routing;
using Microsoft.AspNetCore.Http;

/// <content>
/// Provides additional implementation specific to ASP.NET Core.
Expand Down Expand Up @@ -37,7 +38,7 @@ public ApiVersion DefaultApiVersion
/// <value>The <see cref="IApiVersionParameterSource">API version parameter source</see> used to describe API version parameters.</value>
public IApiVersionParameterSource ApiVersionParameterSource
{
get => parameterSource ??= ApiVersionReader.Combine( new QueryStringApiVersionReader(), new UrlSegmentApiVersionReader() );
get => parameterSource ??= ApiVersionReader.Default;
set => parameterSource = value;
}

Expand All @@ -47,6 +48,17 @@ public IApiVersionParameterSource ApiVersionParameterSource
/// <value>The name associated with the <see cref="ApiVersionRouteConstraint">API version route constraint</see>.</value>
public string RouteConstraintName { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the API version selector.
/// </summary>
/// <value>An <see cref="IApiVersionSelector">API version selector</see> object.</value>
[CLSCompliant( false )]
public IApiVersionSelector ApiVersionSelector
{
get => apiVersionSelector ??= new DefaultApiVersionSelector( this );
set => apiVersionSelector = value;
}

/// <summary>
/// Gets or sets the function used to format the combination of a group name and API version.
/// </summary>
Expand All @@ -55,4 +67,13 @@ public IApiVersionParameterSource ApiVersionParameterSource
/// <remarks>The specified callback will only be invoked if a group name has been configured. The API
/// version will be provided formatted according to the <see cref="GroupNameFormat">group name format</see>.</remarks>
public FormatGroupNameCallback? FormatGroupName { get; set; }

private sealed class DefaultApiVersionSelector : IApiVersionSelector
{
private readonly ApiExplorerOptions options;

public DefaultApiVersionSelector( ApiExplorerOptions options ) => this.options = options;

public ApiVersion SelectVersion( HttpRequest request, ApiVersionModel model ) => options.DefaultApiVersion;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,25 @@ public ApiExplorerOptionsFactory(
/// <inheritdoc />
protected override T CreateInstance( string name )
{
var apiVersioningOptions = Options;
var options = base.CreateInstance( name );
CopyOptions( Options, options );
return options;
}

options.AssumeDefaultVersionWhenUnspecified = apiVersioningOptions.AssumeDefaultVersionWhenUnspecified;
options.ApiVersionParameterSource = apiVersioningOptions.ApiVersionReader;
options.DefaultApiVersion = apiVersioningOptions.DefaultApiVersion;
options.RouteConstraintName = apiVersioningOptions.RouteConstraintName;
/// <summary>
/// Copies the following source options to the target options.
/// </summary>
/// <param name="sourceOptions">The source options.</param>
/// <param name="targetOptions">The target options.</param>
protected static void CopyOptions( ApiVersioningOptions sourceOptions, T targetOptions )
{
ArgumentNullException.ThrowIfNull( targetOptions, nameof( targetOptions ) );
ArgumentNullException.ThrowIfNull( sourceOptions, nameof( sourceOptions ) );

return options;
targetOptions.AssumeDefaultVersionWhenUnspecified = sourceOptions.AssumeDefaultVersionWhenUnspecified;
targetOptions.ApiVersionParameterSource = sourceOptions.ApiVersionReader;
targetOptions.DefaultApiVersion = sourceOptions.DefaultApiVersion;
targetOptions.RouteConstraintName = sourceOptions.RouteConstraintName;
targetOptions.ApiVersionSelector = sourceOptions.ApiVersionSelector;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public ApiVersionParameterDescriptionContext(
ApiDescription = apiDescription ?? throw new ArgumentNullException( nameof( apiDescription ) );
ApiVersion = apiVersion ?? throw new ArgumentNullException( nameof( apiVersion ) );
ModelMetadata = modelMetadata ?? throw new ArgumentNullException( nameof( modelMetadata ) );
optional = options.AssumeDefaultVersionWhenUnspecified && apiVersion == options.DefaultApiVersion;
optional = FirstParameterIsOptional( apiDescription, apiVersion, options );
}

// intentionally an internal property so the public contract doesn't change. this will be removed
Expand Down Expand Up @@ -440,4 +440,22 @@ private void RemoveAllParametersExcept( ApiParameterDescription parameter )
}
}
}

private static bool FirstParameterIsOptional(
ApiDescription apiDescription,
ApiVersion apiVersion,
ApiExplorerOptions options )
{
if ( !options.AssumeDefaultVersionWhenUnspecified )
{
return false;
}

var mapping = ApiVersionMapping.Explicit | ApiVersionMapping.Implicit;
var model = apiDescription.ActionDescriptor.GetApiVersionMetadata().Map( mapping );
var context = new Microsoft.AspNetCore.Http.DefaultHttpContext();
var defaultApiVersion = options.ApiVersionSelector.SelectVersion( context.Request, model );

return apiVersion == defaultApiVersion;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )

groupResult.SetApiVersion( version );
PopulateApiVersionParameters( groupResult, version );
groupResults.Add( groupResult );
AddOrUpdateResult( groupResults, groupResult, metadata, version );
}
}

Expand Down Expand Up @@ -245,6 +245,47 @@ private static void TryUpdateControllerRouteValueForMinimalApi( ApiDescription d
}
}

private static void AddOrUpdateResult(
List<ApiDescription> results,
ApiDescription result,
ApiVersionMetadata metadata,
ApiVersion version )
{
var comparer = StringComparer.OrdinalIgnoreCase;

for ( var i = results.Count - 1; i >= 0; i-- )
{
var other = results[i];

if ( comparer.Equals( result.GroupName, other.GroupName ) &&
comparer.Equals( result.RelativePath, other.RelativePath ) &&
comparer.Equals( result.HttpMethod, other.HttpMethod ) )
{
var mapping = other.ActionDescriptor.GetApiVersionMetadata().MappingTo( version );

switch ( metadata.MappingTo( version ) )
{
case Explicit:
if ( mapping == Implicit )
{
results.RemoveAt( i );
}

break;
case Implicit:
if ( mapping == Explicit )
{
return;
}

break;
}
}
}

results.Add( result );
}

private IEnumerable<ApiVersion> FlattenApiVersions( IList<ApiDescription> descriptions )
{
var versions = default( SortedSet<ApiVersion> );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,14 @@ public void add_parameter_should_add_descriptor_for_media_type_parameter()
public void add_parameter_should_add_optional_parameter_when_allowed()
{
// arrange
var version = new ApiVersion( 1, 0 );
var version = new ApiVersion( 2.0 );
var description = NewApiDescription( version );
var modelMetadata = new Mock<ModelMetadata>( ModelMetadataIdentity.ForType( typeof( string ) ) ).Object;
var options = new ApiExplorerOptions()
{
DefaultApiVersion = version,
DefaultApiVersion = ApiVersion.Default,
ApiVersionParameterSource = new QueryStringApiVersionReader(),
ApiVersionSelector = new ConstantApiVersionSelector( version ),
AssumeDefaultVersionWhenUnspecified = true,
};
var context = new ApiVersionParameterDescriptionContext( description, version, modelMetadata, options );
Expand All @@ -270,7 +271,7 @@ public void add_parameter_should_add_optional_parameter_when_allowed()
Name = "api-version",
ModelMetadata = modelMetadata,
Source = BindingSource.Query,
DefaultValue = (object) "1.0",
DefaultValue = (object) "2.0",
IsRequired = false,
Type = typeof( string ),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ namespace Asp.Versioning.ApiExplorer;

internal sealed class TestActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
{
private readonly Lazy<ActionDescriptorCollection> collection = new( CreateActionDescriptors );
private readonly Lazy<ActionDescriptorCollection> collection;

public TestActionDescriptorCollectionProvider() { }
public TestActionDescriptorCollectionProvider() => collection = new( CreateActionDescriptors );

public TestActionDescriptorCollectionProvider( ActionDescriptor action, params ActionDescriptor[] otherActions )
{
Expand All @@ -21,7 +21,7 @@ public TestActionDescriptorCollectionProvider( ActionDescriptor action, params A
}
else
{
actions = new ActionDescriptor[otherActions.Length];
actions = new ActionDescriptor[otherActions.Length + 1];
actions[0] = action;
Array.Copy( otherActions, 0, actions, 1, otherActions.Length );
}
Expand Down
Loading