Skip to content

Commit b4a0cba

Browse files
Chris Martinezcommonsensesoftware
Chris Martinez
authored andcommitted
Support API version in LinkGenerator
1 parent cb95c4f commit b4a0cba

File tree

11 files changed

+259
-15
lines changed

11 files changed

+259
-15
lines changed

src/Common/Common.projitems

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@
6464
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionNeutral.cs" />
6565
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionParameterDescriptionContext.cs" />
6666
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionParameterSource.cs" />
67+
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionParameterSourceExtensions.cs" />
6768
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionProvider.cs" />
6869
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionReader.cs" />
69-
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionReaderExtensions.cs" />
7070
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IApiVersionSelector.cs" />
7171
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IErrorResponseProvider.cs" />
7272
<Compile Include="$(MSBuildThisFileDirectory)Versioning\IReportApiVersions.cs" />

src/Common/Versioning/IApiVersionReaderExtensions.cs renamed to src/Common/Versioning/IApiVersionParameterSourceExtensions.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,26 @@ namespace Microsoft.AspNetCore.Mvc.Versioning
99
using System.Linq;
1010
using static ApiVersionParameterLocation;
1111

12-
internal static class IApiVersionReaderExtensions
12+
internal static class IApiVersionParameterSourceExtensions
1313
{
14-
internal static bool VersionsByUrlSegment( this IApiVersionReader reader )
14+
internal static bool VersionsByUrlSegment( this IApiVersionParameterSource source )
1515
{
1616
var context = new UrlSegmentDescriptionContext();
17-
reader.AddParameters( context );
17+
source.AddParameters( context );
1818
return context.HasPathApiVersion;
1919
}
2020

21-
internal static bool VersionsByMediaType( this IApiVersionReader reader )
21+
internal static bool VersionsByMediaType( this IApiVersionParameterSource source )
2222
{
2323
var context = new MediaTypeDescriptionContext();
24-
reader.AddParameters( context );
24+
source.AddParameters( context );
2525
return context.HasMediaTypeApiVersion;
2626
}
2727

28-
internal static string GetMediaTypeVersionParameter( this IApiVersionReader reader )
28+
internal static string GetMediaTypeVersionParameter( this IApiVersionParameterSource source )
2929
{
3030
var context = new MediaTypeDescriptionContext();
31-
reader.AddParameters( context );
31+
source.AddParameters( context );
3232
return context.ParameterName;
3333
}
3434

src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ static void AddApiVersioningServices( IServiceCollection services )
5252
}
5353

5454
services.Add( Singleton( sp => sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionReader ) );
55+
services.Add( Singleton( sp => (IApiVersionParameterSource) sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionReader ) );
5556
services.Add( Singleton( sp => sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ApiVersionSelector ) );
5657
services.Add( Singleton( sp => sp.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.ErrorResponses ) );
5758
services.Replace( Singleton<IActionSelector, ApiVersionActionSelector>() );
@@ -68,6 +69,7 @@ static void AddApiVersioningServices( IServiceCollection services )
6869
services.TryAddEnumerable( Singleton<MatcherPolicy, ApiVersionMatcherPolicy>() );
6970
services.AddTransient<IStartupFilter, AutoRegisterMiddleware>();
7071
services.Replace( WithUrlHelperFactoryDecorator( services ) );
72+
services.Replace( WithLinkGeneratorDecorator( services ) );
7173
}
7274

7375
static IReportApiVersions OnRequestIReportApiVersions( IServiceProvider serviceProvider )
@@ -97,6 +99,26 @@ static object CreateInstance( this IServiceProvider services, ServiceDescriptor
9799
return ActivatorUtilities.GetServiceOrCreateInstance( services, descriptor.ImplementationType );
98100
}
99101

102+
// REF: https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceDescriptor.cs#L125
103+
static Type GetImplementationType( this ServiceDescriptor descriptor )
104+
{
105+
if ( descriptor.ImplementationType != null )
106+
{
107+
return descriptor.ImplementationType;
108+
}
109+
else if ( descriptor.ImplementationInstance != null )
110+
{
111+
return descriptor.ImplementationInstance.GetType();
112+
}
113+
else if ( descriptor.ImplementationFactory != null )
114+
{
115+
var typeArguments = descriptor.ImplementationFactory.GetType().GenericTypeArguments;
116+
return typeArguments[1];
117+
}
118+
119+
throw new InvalidOperationException();
120+
}
121+
100122
static ServiceDescriptor WithUrlHelperFactoryDecorator( IServiceCollection services )
101123
{
102124
var descriptor = services.FirstOrDefault( sd => sd.ServiceType == typeof( IUrlHelperFactory ) );
@@ -112,10 +134,10 @@ static ServiceDescriptor WithUrlHelperFactoryDecorator( IServiceCollection servi
112134
IUrlHelperFactory NewFactory( IServiceProvider serviceProvider )
113135
{
114136
var decorated = instantiate( serviceProvider );
115-
var options = serviceProvider.GetRequiredService<IOptions<ApiVersioningOptions>>().Value;
137+
var source = serviceProvider.GetRequiredService<IApiVersionParameterSource>();
116138
var instance = decorated;
117139

118-
if ( options.ApiVersionReader.VersionsByUrlSegment() )
140+
if ( source.VersionsByUrlSegment() )
119141
{
120142
var factory = ActivatorUtilities.CreateFactory( typeof( ApiVersionUrlHelperFactory ), new[] { typeof( IUrlHelperFactory ) } );
121143
instance = factory( serviceProvider, new[] { decorated } );
@@ -126,5 +148,24 @@ IUrlHelperFactory NewFactory( IServiceProvider serviceProvider )
126148

127149
return Describe( typeof( IUrlHelperFactory ), NewFactory, lifetime );
128150
}
151+
152+
static ServiceDescriptor WithLinkGeneratorDecorator( IServiceCollection services )
153+
{
154+
var descriptor = services.FirstOrDefault( sd => sd.ServiceType == typeof( LinkGenerator ) );
155+
156+
if ( descriptor == null )
157+
{
158+
services.AddRouting();
159+
descriptor = services.First( sd => sd.ServiceType == typeof( LinkGenerator ) );
160+
}
161+
162+
var lifetime = descriptor.Lifetime;
163+
var decoratedType = descriptor.GetImplementationType();
164+
var decoratorType = typeof( ApiVersionLinkGenerator<> ).MakeGenericType( decoratedType );
165+
166+
services.Replace( Describe( decoratedType, decoratedType, lifetime ) );
167+
168+
return Describe( typeof( LinkGenerator ), decoratorType, lifetime );
169+
}
129170
}
130171
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
namespace Microsoft.AspNetCore.Mvc.Routing
2+
{
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc.Versioning;
5+
using Microsoft.AspNetCore.Routing;
6+
using System;
7+
8+
/// <summary>
9+
/// Represents an API version aware <see cref="AspNetCore.Routing.LinkGenerator">link generator</see>.
10+
/// </summary>
11+
[CLSCompliant( false )]
12+
public class ApiVersionLinkGenerator : LinkGenerator
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="ApiVersionLinkGenerator"/> class.
16+
/// </summary>
17+
/// <param name="linkGenerator">The inner <see cref="AspNetCore.Routing.LinkGenerator">link generator</see>.</param>
18+
public ApiVersionLinkGenerator( LinkGenerator linkGenerator ) => LinkGenerator = linkGenerator;
19+
20+
/// <summary>
21+
/// Gets the inner link generator.
22+
/// </summary>
23+
/// <value>The inner <see cref="AspNetCore.Routing.LinkGenerator">link generator</see>.</value>
24+
protected LinkGenerator LinkGenerator { get; }
25+
26+
/// <inheritdoc />
27+
public override string GetPathByAddress<TAddress>(
28+
HttpContext httpContext,
29+
TAddress address,
30+
RouteValueDictionary values,
31+
RouteValueDictionary? ambientValues = null,
32+
PathString? pathBase = null,
33+
FragmentString fragment = default,
34+
LinkOptions? options = null )
35+
{
36+
AddApiVersionRouteValueIfNecessary( httpContext, values );
37+
return LinkGenerator.GetPathByAddress( httpContext, address, values, ambientValues, pathBase, fragment, options );
38+
}
39+
40+
/// <inheritdoc />
41+
public override string GetPathByAddress<TAddress>(
42+
TAddress address,
43+
RouteValueDictionary values,
44+
PathString pathBase = default,
45+
FragmentString fragment = default,
46+
LinkOptions? options = null ) => LinkGenerator.GetPathByAddress( address, values, pathBase, fragment, options );
47+
48+
/// <inheritdoc />
49+
public override string GetUriByAddress<TAddress>(
50+
HttpContext httpContext,
51+
TAddress address,
52+
RouteValueDictionary values,
53+
RouteValueDictionary? ambientValues = null,
54+
string? scheme = null,
55+
HostString? host = null,
56+
PathString? pathBase = null,
57+
FragmentString fragment = default,
58+
LinkOptions? options = null )
59+
{
60+
AddApiVersionRouteValueIfNecessary( httpContext, values );
61+
return LinkGenerator.GetUriByAddress( httpContext, address, values, ambientValues, scheme, host, pathBase, fragment, options );
62+
}
63+
64+
/// <inheritdoc />
65+
public override string GetUriByAddress<TAddress>(
66+
TAddress address,
67+
RouteValueDictionary values,
68+
string scheme,
69+
HostString host,
70+
PathString pathBase = default,
71+
FragmentString fragment = default,
72+
LinkOptions? options = null ) => LinkGenerator.GetUriByAddress( address, values, scheme, host, pathBase, fragment, options );
73+
74+
static void AddApiVersionRouteValueIfNecessary( HttpContext httpContext, RouteValueDictionary values )
75+
{
76+
if ( httpContext == null )
77+
{
78+
throw new ArgumentNullException( nameof( httpContext ) );
79+
}
80+
81+
if ( values == null )
82+
{
83+
throw new ArgumentNullException( nameof( values ) );
84+
}
85+
86+
var feature = httpContext.Features.Get<IApiVersioningFeature>();
87+
var key = feature.RouteParameter;
88+
89+
if ( string.IsNullOrEmpty( key ) )
90+
{
91+
return;
92+
}
93+
94+
var value = feature.RawRequestedApiVersion;
95+
96+
if ( !string.IsNullOrEmpty( value ) && !values.ContainsKey( key ) )
97+
{
98+
values.Add( key, value );
99+
}
100+
}
101+
}
102+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Microsoft.AspNetCore.Mvc.Routing
2+
{
3+
using Microsoft.AspNetCore.Routing;
4+
using System;
5+
6+
/// <summary>
7+
/// Represents an API version aware <see cref="LinkGenerator">link generator</see> that can
8+
/// be used as a decorator.
9+
/// </summary>
10+
/// <typeparam name="T">The decorated type of <see cref="LinkGenerator">link generator</see>.</typeparam>
11+
/// <remarks>This type is meant to be used as a Decorator when combined with dependency injection.</remarks>
12+
[CLSCompliant( false )]
13+
public sealed class ApiVersionLinkGenerator<T> : ApiVersionLinkGenerator where T : LinkGenerator
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="ApiVersionLinkGenerator{T}"/> class.
17+
/// </summary>
18+
/// <param name="linkGenerator">The inner <see cref="AspNetCore.Routing.LinkGenerator">link generator</see>.</param>
19+
public ApiVersionLinkGenerator( T linkGenerator ) : base( linkGenerator ) { }
20+
}
21+
}

src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionUrlHelper.cs renamed to src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionUrlHelper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
namespace Microsoft.AspNetCore.Mvc.Versioning
1+
namespace Microsoft.AspNetCore.Mvc.Routing
22
{
3-
using Microsoft.AspNetCore.Mvc.Routing;
3+
using Microsoft.AspNetCore.Mvc.Versioning;
44
using Microsoft.AspNetCore.Routing;
55
using System;
66

src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionUrlHelperFactory.cs renamed to src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionUrlHelperFactory.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
namespace Microsoft.AspNetCore.Mvc.Versioning
1+
namespace Microsoft.AspNetCore.Mvc.Routing
22
{
3-
using Microsoft.AspNetCore.Mvc.Routing;
43
using System;
54

65
/// <summary>

src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/IODataBuilderExtensions.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
using Microsoft.AspNetCore.Mvc.ApplicationModels;
1010
using Microsoft.AspNetCore.Mvc.ApplicationParts;
1111
using Microsoft.AspNetCore.Mvc.Infrastructure;
12+
using Microsoft.AspNetCore.Mvc.Routing;
1213
using Microsoft.AspNetCore.Mvc.Versioning;
14+
using Microsoft.AspNetCore.Routing;
1315
using Microsoft.Extensions.DependencyInjection.Extensions;
16+
using Microsoft.Extensions.Options;
1417
using System;
1518
using System.Linq;
19+
using static Microsoft.AspNetCore.Mvc.Versioning.ApiVersionParameterLocation;
1620
using static ServiceDescriptor;
1721

1822
/// <summary>
@@ -80,6 +84,7 @@ static void AddODataServices( IServiceCollection services )
8084
services.TryAddEnumerable( Transient<IApiControllerSpecification, ODataControllerSpecification>() );
8185
services.AddTransient<IStartupFilter, RaiseVersionedODataRoutesMapped>();
8286
services.AddModelConfigurationsAsServices( partManager );
87+
services.TryReplace( WithLinkGeneratorDecorator( services ) );
8388
}
8489

8590
static T GetService<T>( this IServiceCollection services ) => (T) services.LastOrDefault( d => d.ServiceType == typeof( T ) )?.ImplementationInstance!;
@@ -97,12 +102,70 @@ static void AddModelConfigurationsAsServices( this IServiceCollection services,
97102
}
98103
}
99104

105+
static IServiceCollection TryReplace( this IServiceCollection services, ServiceDescriptor? descriptor )
106+
{
107+
if ( descriptor != null )
108+
{
109+
services.Replace( descriptor );
110+
}
111+
112+
return services;
113+
}
114+
100115
static void ConfigureDefaultFeatureProviders( ApplicationPartManager partManager )
101116
{
102117
if ( !partManager.FeatureProviders.OfType<ModelConfigurationFeatureProvider>().Any() )
103118
{
104119
partManager.FeatureProviders.Add( new ModelConfigurationFeatureProvider() );
105120
}
106121
}
122+
123+
static ServiceDescriptor? WithLinkGeneratorDecorator( IServiceCollection services )
124+
{
125+
// HACK: even though the core api versioning services decorate the default LinkGenerator, we need to get in front of the odata
126+
// implementation in order to add the necessary route values when versioning by url segment.
127+
//
128+
// REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNetCore.OData/Extensions/ODataServiceCollectionExtensions.cs#L99
129+
// REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNetCore.OData/Extensions/Endpoint/ODataEndpointLinkGenerator.cs
130+
var descriptor = services.FirstOrDefault( sd => sd.ServiceType == typeof( LinkGenerator ) );
131+
132+
if ( descriptor == null )
133+
{
134+
return default;
135+
}
136+
137+
var lifetime = descriptor.Lifetime;
138+
var factory = descriptor.ImplementationFactory;
139+
140+
if ( factory == null )
141+
{
142+
throw new InvalidOperationException( LocalSR.MissingLinkGenerator );
143+
}
144+
145+
LinkGenerator NewFactory( IServiceProvider serviceProvider )
146+
{
147+
var instance = (LinkGenerator) factory( serviceProvider );
148+
var source = serviceProvider.GetRequiredService<IApiVersionParameterSource>();
149+
var context = new UrlSegmentDescriptionContext();
150+
151+
source.AddParameters( context );
152+
153+
if ( context.HasPathApiVersion )
154+
{
155+
instance = new ApiVersionLinkGenerator( instance );
156+
}
157+
158+
return instance;
159+
}
160+
161+
return Describe( typeof( LinkGenerator ), NewFactory, lifetime );
162+
}
163+
164+
sealed class UrlSegmentDescriptionContext : IApiVersionParameterDescriptionContext
165+
{
166+
internal bool HasPathApiVersion { get; private set; }
167+
168+
public void AddParameter( string name, ApiVersionParameterLocation location ) => HasPathApiVersion |= location == Path;
169+
}
107170
}
108171
}

src/Microsoft.AspNetCore.OData.Versioning/LocalSR.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.AspNetCore.OData.Versioning/LocalSR.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,7 @@
120120
<data name="MissingAnnotation" xml:space="preserve">
121121
<value>The entity model (EDM) does not have the required {0} annotation.</value>
122122
</data>
123+
<data name="MissingLinkGenerator" xml:space="preserve">
124+
<value>Expected OData to register a custom LinkGenerator using a factory method.</value>
125+
</data>
123126
</root>

0 commit comments

Comments
 (0)