Skip to content

6.0 Preview 3 Fixes #843

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 8 commits into from
Aug 23, 2022
3 changes: 3 additions & 0 deletions asp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebApi", "WebApi", "{E0E64F6F-FB0C-4534-B815-2217700B50BA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OData", "OData", "{49EA6476-901C-4D4F-8E45-98BC8A2780EB}"
ProjectSection(SolutionItems) = preProject
examples\AspNetCore\OData\Directory.Build.props = examples\AspNetCore\OData\Directory.Build.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicWebApiExample", "examples\AspNet\WebApi\BasicWebApiExample\BasicWebApiExample.csproj", "{B0457E07-161A-4ED0-949A-8CF7EFA765F5}"
EndProject
Expand Down
11 changes: 11 additions & 0 deletions examples/AspNetCore/OData/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<Import Project="$([MSBuild]::GetPathOfFileAbove('$(MSBuildThisFile)','$(MSBuildThisFileDirectory)../'))"
Condition="Exists($([MSBuild]::GetPathOfFileAbove('$(MSBuildThisFile)','$(MSBuildThisFileDirectory)../')))"/>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OData" Version="8.0.10" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ protected virtual ApiDescriptionGroupCollection InitializeApiDescriptions()

for ( var j = 0; j < apiDescriptions.Count; j++ )
{
apiDescriptions[i].TryUpdateRelativePathAndRemoveApiVersionParameter( Options );
apiDescriptions[j].TryUpdateRelativePathAndRemoveApiVersionParameter( Options );
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )
for ( var i = results.Count - 1; i >= 0; i-- )
{
var result = results[i];
var metadata = result.ActionDescriptor.EndpointMetadata.OfType<IODataRoutingMetadata>().ToArray();

if ( result.ActionDescriptor.EndpointMetadata is not IList<object> endpointMetadata )
{
continue;
}

var metadata = endpointMetadata.OfType<IODataRoutingMetadata>().ToArray();
var notOData = metadata.Length == 0;

if ( notOData )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OData" Version="[8.0.10,9.0.0)" />
<PackageReference Include="Microsoft.AspNetCore.OData" Version="[8.0.2,9.0.0)" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ internal static ApiResponseType Clone( this ApiResponseType responseType )
{
var clone = new ApiResponseType()
{
IsDefaultResponse = responseType.IsDefaultResponse,
ModelMetadata = responseType.ModelMetadata,
StatusCode = responseType.StatusCode,
Type = responseType.Type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
using static Asp.Versioning.ApiVersionParameterLocation;
using static System.Linq.Enumerable;
using static System.StringComparison;
Expand Down Expand Up @@ -43,6 +44,11 @@ public ApiVersionParameterDescriptionContext(
optional = options.AssumeDefaultVersionWhenUnspecified && apiVersion == options.DefaultApiVersion;
}

// intentionally an internal property so the public contract doesn't change. this will be removed
// once the ASP.NET Core team fixes the bug
// BUG: https://github.com/dotnet/aspnetcore/issues/41773
internal IInlineConstraintResolver? ConstraintResolver { get; set; }

/// <summary>
/// Gets the associated API description.
/// </summary>
Expand Down Expand Up @@ -160,7 +166,8 @@ protected virtual void AddHeader( string name )
protected virtual void UpdateUrlSegment()
{
var parameter = FindByRouteConstraintType( ApiDescription ) ??
FindByRouteConstraintName( ApiDescription, Options.RouteConstraintName );
FindByRouteConstraintName( ApiDescription, Options.RouteConstraintName ) ??
TryCreateFromRouteTemplate( ApiDescription, ConstraintResolver );

if ( parameter == null )
{
Expand Down Expand Up @@ -309,6 +316,73 @@ routeInfo.Constraints is IEnumerable<IRouteConstraint> constraints &&
return default;
}

private static ApiParameterDescription? TryCreateFromRouteTemplate( ApiDescription description, IInlineConstraintResolver? constraintResolver )
{
if ( constraintResolver == null )
{
return default;
}

var relativePath = description.RelativePath;

if ( string.IsNullOrEmpty( relativePath ) )
{
return default;
}

var constraints = new List<IRouteConstraint>();
var template = TemplateParser.Parse( relativePath );
var constraintName = default( string );

for ( var i = 0; i < template.Parameters.Count; i++ )
{
var match = false;
var parameter = template.Parameters[i];

foreach ( var inlineConstraint in parameter.InlineConstraints )
{
var constraint = constraintResolver.ResolveConstraint( inlineConstraint.Constraint )!;

constraints.Add( constraint );

if ( constraint is ApiVersionRouteConstraint )
{
match = true;
constraintName = inlineConstraint.Constraint;
}
}

if ( !match )
{
continue;
}

constraints.TrimExcess();

// ASP.NET Core does not discover route parameters without using Reflection in 6.0. unclear if it will be fixed before 7.0
// BUG: https://github.com/dotnet/aspnetcore/issues/41773
// REF: https://github.com/dotnet/aspnetcore/blob/release/6.0/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs#L323
var result = new ApiParameterDescription()
{
Name = parameter.Name!,
RouteInfo = new()
{
Constraints = constraints,
DefaultValue = parameter.DefaultValue,
IsOptional = parameter.IsOptional || parameter.DefaultValue != null,
},
Source = BindingSource.Path,
};
var token = $"{parameter.Name}:{constraintName}";

description.RelativePath = relativePath.Replace( token, parameter.Name, Ordinal );
description.ParameterDescriptions.Insert( 0, result );
return result;
}

return default;
}

private ApiParameterDescription NewApiVersionParameter( string name, BindingSource source )
{
var parameter = new ApiParameterDescription()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace Microsoft.Extensions.DependencyInjection;
using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using static ServiceDescriptor;
Expand Down Expand Up @@ -60,6 +62,15 @@ private static void AddApiExplorerServices( IServiceCollection services )
services.AddMvcCore().AddApiExplorer();
services.TryAddSingleton<IOptionsFactory<ApiExplorerOptions>, ApiExplorerOptionsFactory<ApiExplorerOptions>>();
services.TryAddSingleton<IApiVersionDescriptionProvider, DefaultApiVersionDescriptionProvider>();
services.TryAddEnumerable( Transient<IApiDescriptionProvider, VersionedApiDescriptionProvider>() );

// use internal constructor until ASP.NET Core fixes their bug
// BUG: https://github.com/dotnet/aspnetcore/issues/41773
services.TryAddEnumerable(
Transient<IApiDescriptionProvider, VersionedApiDescriptionProvider>(
sp => new(
sp.GetRequiredService<ISunsetPolicyManager>(),
sp.GetRequiredService<IModelMetadataProvider>(),
sp.GetRequiredService<IInlineConstraintResolver>(),
sp.GetRequiredService<IOptions<ApiExplorerOptions>>() ) ) );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace Asp.Versioning.ApiExplorer;

using Asp.Versioning.Routing;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using static Asp.Versioning.ApiVersionMapping;
using static System.Globalization.CultureInfo;
Expand All @@ -18,6 +20,7 @@ namespace Asp.Versioning.ApiExplorer;
public class VersionedApiDescriptionProvider : IApiDescriptionProvider
{
private readonly IOptions<ApiExplorerOptions> options;
private readonly IInlineConstraintResolver constraintResolver;
private ApiVersionModelMetadata? modelMetadata;

/// <summary>
Expand All @@ -31,9 +34,19 @@ public VersionedApiDescriptionProvider(
ISunsetPolicyManager sunsetPolicyManager,
IModelMetadataProvider modelMetadataProvider,
IOptions<ApiExplorerOptions> options )
: this( sunsetPolicyManager, modelMetadataProvider, new SimpleConstraintResolver( options ), options ) { }

// intentionally hiding IInlineConstraintResolver from public signature until ASP.NET Core fixes their bug
// BUG: https://github.com/dotnet/aspnetcore/issues/41773
internal VersionedApiDescriptionProvider(
ISunsetPolicyManager sunsetPolicyManager,
IModelMetadataProvider modelMetadataProvider,
IInlineConstraintResolver constraintResolver,
IOptions<ApiExplorerOptions> options )
{
SunsetPolicyManager = sunsetPolicyManager;
ModelMetadataProvider = modelMetadataProvider;
this.constraintResolver = constraintResolver;
this.options = options;
}

Expand Down Expand Up @@ -83,6 +96,7 @@ protected virtual void PopulateApiVersionParameters( ApiDescription apiDescripti
var parameterSource = Options.ApiVersionParameterSource;
var context = new ApiVersionParameterDescriptionContext( apiDescription, apiVersion, ModelMetadata, Options );

context.ConstraintResolver = constraintResolver;
parameterSource.AddParameters( context );
}

Expand Down Expand Up @@ -179,6 +193,11 @@ private static bool IsUnversioned( ActionDescriptor action )
{
var endpointMetadata = action.EndpointMetadata;

if ( endpointMetadata == null )
{
return true;
}

for ( var i = 0; i < endpointMetadata.Count; i++ )
{
if ( endpointMetadata[i] is ApiVersionMetadata )
Expand Down Expand Up @@ -242,4 +261,21 @@ private IEnumerable<ApiVersion> FlattenApiVersions( IList<ApiDescription> descri

return versions;
}

private sealed class SimpleConstraintResolver : IInlineConstraintResolver
{
private readonly IOptions<ApiExplorerOptions> options;

internal SimpleConstraintResolver( IOptions<ApiExplorerOptions> options ) => this.options = options;

public IRouteConstraint? ResolveConstraint( string inlineConstraint )
{
if ( options.Value.RouteConstraintName == inlineConstraint )
{
return new ApiVersionRouteConstraint();
}

return default;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public static ApiVersionMetadata GetApiVersionMetadata( this ActionDescriptor ac

var endpointMetadata = action.EndpointMetadata;

if ( endpointMetadata == null )
{
return ApiVersionMetadata.Empty;
}

for ( var i = 0; i < endpointMetadata.Count; i++ )
{
if ( endpointMetadata[i] is ApiVersionMetadata metadata )
Expand All @@ -39,6 +44,12 @@ internal static void AddOrReplaceApiVersionMetadata( this ActionDescriptor actio
{
var endpointMetadata = action.EndpointMetadata;

if ( endpointMetadata == null )
{
action.EndpointMetadata = new List<object>() { value };
return;
}

for ( var i = 0; i < endpointMetadata.Count; i++ )
{
if ( endpointMetadata[i] is not ApiVersionMetadata )
Expand Down
11 changes: 11 additions & 0 deletions src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionCollator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace Asp.Versioning;

using Asp.Versioning.ApplicationModels;
using Asp.Versioning.Conventions;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Runtime.CompilerServices;
using static Asp.Versioning.ApiVersionMapping;
Expand Down Expand Up @@ -102,13 +104,22 @@ protected virtual string GetControllerName( ActionDescriptor action )
return NamingConvention.GroupName( name );
}

[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static bool IsUnversioned( ActionDescriptor action ) => action.GetApiVersionMetadata() == ApiVersionMetadata.Empty;

private IEnumerable<IReadOnlyList<ActionDescriptor>> GroupActionsByController( IList<ActionDescriptor> actions )
{
var groups = new Dictionary<string, List<ActionDescriptor>>( StringComparer.OrdinalIgnoreCase );

for ( var i = 0; i < actions.Count; i++ )
{
var action = actions[i];

if ( IsUnversioned( action ) )
{
continue;
}

var key = GetControllerName( action );

if ( string.IsNullOrEmpty( key ) )
Expand Down
19 changes: 14 additions & 5 deletions src/Common/src/Common/DefaultApiVersionReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel )
AddApiVersionHeader( headers, ApiDeprecatedVersions, apiVersionModel.DeprecatedApiVersions );

#if NETFRAMEWORK
var request = response.RequestMessage;
var statusCode = (int) response.StatusCode;
#else
var context = response.HttpContext;
Expand All @@ -75,13 +74,23 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel )
}

#if NETFRAMEWORK
var dependencyResolver = request.GetConfiguration().DependencyResolver;
var policyManager = dependencyResolver.GetSunsetPolicyManager();
var name = request.GetActionDescriptor().GetApiVersionMetadata().Name;
if ( response.RequestMessage is not HttpRequestMessage request ||
request.GetActionDescriptor()?.GetApiVersionMetadata() is not ApiVersionMetadata metadata )
{
return;
}

var name = metadata.Name;
var policyManager = request.GetConfiguration().DependencyResolver.GetSunsetPolicyManager();
var version = request.GetRequestedApiVersion();
#else
if ( context.GetEndpoint()?.Metadata.GetMetadata<ApiVersionMetadata>() is not ApiVersionMetadata metadata )
{
return;
}

var name = metadata.Name;
var policyManager = context.RequestServices.GetRequiredService<ISunsetPolicyManager>();
var name = context.GetEndpoint()!.Metadata.GetMetadata<ApiVersionMetadata>()?.Name;
var version = context.GetRequestedApiVersion();
#endif

Expand Down