Skip to content

Commit ea50eb2

Browse files
Add support for exploring OData metadata routes. Resolves #893
1 parent 665afc0 commit ea50eb2

File tree

11 files changed

+206
-15
lines changed

11 files changed

+206
-15
lines changed

src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,24 @@ protected override bool ShouldExploreAction(
8282
return base.ShouldExploreAction( actionRouteParameterValue, actionDescriptor, route, apiVersion );
8383
}
8484

85+
if ( actionDescriptor.ControllerDescriptor.ControllerType.IsMetadataController() )
86+
{
87+
if ( actionDescriptor.ActionName == nameof( MetadataController.GetServiceDocument ) )
88+
{
89+
if ( !Options.MetadataOptions.HasFlag( ODataMetadataOptions.ServiceDocument ) )
90+
{
91+
return false;
92+
}
93+
}
94+
else
95+
{
96+
if ( !Options.MetadataOptions.HasFlag( ODataMetadataOptions.Metadata ) )
97+
{
98+
return false;
99+
}
100+
}
101+
}
102+
85103
if ( Options.UseApiExplorerSettings )
86104
{
87105
var setting = actionDescriptor.GetCustomAttributes<ApiExplorerSettingsAttribute>().FirstOrDefault();
@@ -112,9 +130,10 @@ protected override bool ShouldExploreController(
112130
throw new ArgumentNullException( nameof( route ) );
113131
}
114132

115-
if ( typeof( MetadataController ).IsAssignableFrom( controllerDescriptor.ControllerType ) )
133+
if ( controllerDescriptor.ControllerType.IsMetadataController() )
116134
{
117-
return false;
135+
controllerDescriptor.ControllerName = "OData";
136+
return Options.MetadataOptions > ODataMetadataOptions.None;
118137
}
119138

120139
var routeTemplate = route.RouteTemplate;

src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Routing/ODataRouteBuilder.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,25 @@ private void AppendPathFromConventions( IList<string> segments, string controlle
285285
case UnboundOperation:
286286
builder.Append( Context.Operation!.Name );
287287
AppendParametersFromConvention( builder, Context.Operation );
288+
break;
289+
default:
290+
var action = Context.ActionDescriptor;
291+
292+
if ( action.ControllerDescriptor.ControllerType.IsMetadataController() )
293+
{
294+
if ( action.ActionName == nameof( MetadataController.GetServiceDocument ) )
295+
{
296+
if ( segments.Count == 0 )
297+
{
298+
segments.Add( "/" );
299+
}
300+
}
301+
else
302+
{
303+
segments.Add( "$metadata" );
304+
}
305+
}
306+
288307
break;
289308
}
290309

src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Routing/ODataRouteBuilderContext.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ internal ODataRouteBuilderContext(
4343
ActionDescriptor = actionDescriptor;
4444
ParameterDescriptions = parameterDescriptions;
4545
Options = options;
46-
UrlKeyDelimiter = UrlKeyDelimiterOrDefault( configuration.GetUrlKeyDelimiter() ?? Services.GetService<IODataPathHandler>()?.UrlKeyDelimiter );
46+
UrlKeyDelimiter = UrlKeyDelimiterOrDefault(
47+
configuration.GetUrlKeyDelimiter() ??
48+
Services.GetService<IODataPathHandler>()?.UrlKeyDelimiter );
4749

4850
var selector = Services.GetRequiredService<IEdmModelSelector>();
4951
var model = selector.SelectModel( apiVersion );
@@ -64,7 +66,8 @@ internal ODataRouteBuilderContext(
6466
Singleton = container.FindSingleton( controllerName );
6567
Operation = ResolveOperation( container, actionDescriptor );
6668
ActionType = GetActionType( actionDescriptor );
67-
IsRouteExcluded = ActionType == ODataRouteActionType.Unknown;
69+
IsRouteExcluded = ActionType == ODataRouteActionType.Unknown &&
70+
!actionDescriptor.ControllerDescriptor.ControllerType.IsMetadataController();
6871

6972
if ( Operation?.IsAction() == true )
7073
{

src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ public void api_descriptions_should_group_versioned_controllers( HttpConfigurati
3434
{
3535
// arrange
3636
var assembliesResolver = configuration.Services.GetAssembliesResolver();
37-
var controllerTypes = configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes( assembliesResolver );
37+
var controllerTypes = configuration.Services
38+
.GetHttpControllerTypeResolver()
39+
.GetControllerTypes( assembliesResolver )
40+
.Where( t => !typeof( MetadataController ).IsAssignableFrom( t ) );
3841
var apiExplorer = new ODataApiExplorer( configuration );
3942

4043
// act
@@ -54,7 +57,10 @@ public void api_descriptions_should_flatten_versioned_controllers( HttpConfigura
5457
{
5558
// arrange
5659
var assembliesResolver = configuration.Services.GetAssembliesResolver();
57-
var controllerTypes = configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes( assembliesResolver );
60+
var controllerTypes = configuration.Services
61+
.GetHttpControllerTypeResolver()
62+
.GetControllerTypes( assembliesResolver )
63+
.Where( t => !typeof( MetadataController ).IsAssignableFrom( t ) );
5864
var apiExplorer = new ODataApiExplorer( configuration );
5965

6066
// act
@@ -86,6 +92,37 @@ public void api_descriptions_should_not_contain_metadata_controllers( HttpConfig
8692
.NotContain( type => typeof( MetadataController ).IsAssignableFrom( type ) );
8793
}
8894

95+
[Theory]
96+
[InlineData( ODataMetadataOptions.ServiceDocument )]
97+
[InlineData( ODataMetadataOptions.Metadata )]
98+
[InlineData( ODataMetadataOptions.All )]
99+
public void api_descriptions_should_contain_metadata_controllers( ODataMetadataOptions metadataOptions )
100+
{
101+
// arrange
102+
var configuration = TestConfigurations.NewOrdersConfiguration();
103+
var options = new ODataApiExplorerOptions( configuration ) { MetadataOptions = metadataOptions };
104+
var apiExplorer = new ODataApiExplorer( configuration, options );
105+
106+
// act
107+
var groups = apiExplorer.ApiDescriptions;
108+
109+
// assert
110+
for ( var i = 0; i < groups.Count; i++ )
111+
{
112+
var group = groups[i];
113+
114+
if ( metadataOptions.HasFlag( ODataMetadataOptions.ServiceDocument ) )
115+
{
116+
group.ApiDescriptions.Should().Contain( item => item.RelativePath == "api" );
117+
}
118+
119+
if ( metadataOptions.HasFlag( ODataMetadataOptions.Metadata ) )
120+
{
121+
group.ApiDescriptions.Should().Contain( item => item.RelativePath == "api/$metadata" );
122+
}
123+
}
124+
}
125+
89126
[Theory]
90127
[ClassData( typeof( TestConfigurations ) )]
91128
public void api_description_group_should_explore_v3_actions( HttpConfiguration configuration )

src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Description/TestConfigurations.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Asp.Versioning.Description;
44

5+
using Asp.Versioning.Controllers;
56
using Asp.Versioning.Conventions;
67
using Asp.Versioning.OData;
78
using Asp.Versioning.Simulators.Configuration;
@@ -24,6 +25,7 @@ public static HttpConfiguration NewOrdersConfiguration()
2425
{
2526
var configuration = new HttpConfiguration();
2627
var controllerTypeResolver = new ControllerTypeCollection(
28+
typeof( VersionedMetadataController ),
2729
typeof( Simulators.V1.OrdersController ),
2830
typeof( Simulators.V2.OrdersController ),
2931
typeof( Simulators.V3.OrdersController ) );
@@ -57,9 +59,10 @@ public static HttpConfiguration NewPeopleConfiguration()
5759
{
5860
var configuration = new HttpConfiguration();
5961
var controllerTypeResolver = new ControllerTypeCollection(
60-
typeof( Simulators.V1.PeopleController ),
61-
typeof( Simulators.V2.PeopleController ),
62-
typeof( Simulators.V3.PeopleController ) );
62+
typeof( VersionedMetadataController ),
63+
typeof( Simulators.V1.PeopleController ),
64+
typeof( Simulators.V2.PeopleController ),
65+
typeof( Simulators.V3.PeopleController ) );
6366

6467
configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver );
6568
configuration.AddApiVersioning();
@@ -79,8 +82,9 @@ public static HttpConfiguration NewProductAndSupplierConfiguration()
7982
{
8083
var configuration = new HttpConfiguration();
8184
var controllerTypeResolver = new ControllerTypeCollection(
82-
typeof( Simulators.V3.ProductsController ),
83-
typeof( Simulators.V3.SuppliersController ) );
85+
typeof( VersionedMetadataController ),
86+
typeof( Simulators.V3.ProductsController ),
87+
typeof( Simulators.V3.SuppliersController ) );
8488

8589
configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver );
8690
configuration.AddApiVersioning();

src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace Asp.Versioning.ApiExplorer;
1616
using System.Diagnostics.CodeAnalysis;
1717
using System.Runtime.CompilerServices;
1818
using static System.StringComparison;
19+
using static ODataMetadataOptions;
1920
using Opts = Microsoft.Extensions.Options.Options;
2021

2122
/// <summary>
@@ -118,11 +119,24 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )
118119
}
119120

120121
if ( !TryMatchModelVersion( result, metadata, out var matched ) ||
121-
IsServiceDocumentOrMetadata( matched.Template ) ||
122122
!visited.Add( result ) )
123123
{
124124
results.RemoveAt( i );
125125
}
126+
else if ( IsServiceDocument( matched.Template ) )
127+
{
128+
if ( !Options.MetadataOptions.HasFlag( ServiceDocument ) )
129+
{
130+
results.RemoveAt( i );
131+
}
132+
}
133+
else if ( IsMetadata( matched.Template ) )
134+
{
135+
if ( !Options.MetadataOptions.HasFlag( Metadata ) )
136+
{
137+
results.RemoveAt( i );
138+
}
139+
}
126140
else if ( IsNavigationPropertyLink( matched.Template ) )
127141
{
128142
results.RemoveAt( i );
@@ -176,8 +190,10 @@ private static int ApiVersioningOrder()
176190
}
177191

178192
[MethodImpl( MethodImplOptions.AggressiveInlining )]
179-
private static bool IsServiceDocumentOrMetadata( ODataPathTemplate template ) =>
180-
template.Count == 0 || ( template.Count == 1 && template[0] is MetadataSegmentTemplate );
193+
private static bool IsServiceDocument( ODataPathTemplate template ) => template.Count == 0;
194+
195+
[MethodImpl( MethodImplOptions.AggressiveInlining )]
196+
private static bool IsMetadata( ODataPathTemplate template ) => template.Count == 1 && template[0] is MetadataSegmentTemplate;
181197

182198
[MethodImpl( MethodImplOptions.AggressiveInlining )]
183199
private static bool IsNavigationPropertyLink( ODataPathTemplate template ) =>

src/AspNetCore/OData/src/Asp.Versioning.OData/Controllers/VersionedMetadataController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace Asp.Versioning.Controllers;
1414
/// </summary>
1515
[CLSCompliant( false )]
1616
[ReportApiVersions]
17+
[ControllerName( "OData" )]
1718
public class VersionedMetadataController : MetadataController
1819
{
1920
/// <summary>

src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,62 @@ public void odata_api_explorer_should_group_and_order_descriptions_on_providers_
5656
AssertVersion3( groups[3] );
5757
}
5858

59+
[Theory]
60+
[InlineData( ODataMetadataOptions.ServiceDocument )]
61+
[InlineData( ODataMetadataOptions.Metadata )]
62+
[InlineData( ODataMetadataOptions.All )]
63+
public void odata_api_explorer_should_explore_metadata_routes( ODataMetadataOptions metadataOptions )
64+
{
65+
// arrange
66+
var builder = new WebHostBuilder()
67+
.ConfigureServices(
68+
services =>
69+
{
70+
services.AddControllers()
71+
.AddOData(
72+
options =>
73+
{
74+
options.Count().Select().OrderBy();
75+
options.RouteOptions.EnableKeyInParenthesis = false;
76+
options.RouteOptions.EnableNonParenthesisForEmptyParameterFunction = true;
77+
options.RouteOptions.EnableQualifiedOperationCall = false;
78+
options.RouteOptions.EnableUnqualifiedOperationCall = true;
79+
} );
80+
81+
services.AddApiVersioning()
82+
.AddOData( options => options.AddRouteComponents( "api" ) )
83+
.AddODataApiExplorer( options => options.MetadataOptions = metadataOptions );
84+
85+
services.TryAddEnumerable( ServiceDescriptor.Transient<IApplicationModelProvider, TestApiExplorerApplicationModelProvider>() );
86+
} )
87+
.Configure( app => app.UseRouting().UseEndpoints( endpoints => endpoints.MapControllers() ) );
88+
var host = builder.Build();
89+
var serviceProvider = host.Services;
90+
91+
// act
92+
var groups = serviceProvider.GetRequiredService<IApiDescriptionGroupCollectionProvider>()
93+
.ApiDescriptionGroups
94+
.Items
95+
.OrderBy( i => i.GroupName )
96+
.ToArray();
97+
98+
// assert
99+
for ( var i = 0; i < groups.Length; i++ )
100+
{
101+
var group = groups[i];
102+
103+
if ( metadataOptions.HasFlag( ODataMetadataOptions.ServiceDocument ) )
104+
{
105+
group.Items.Should().Contain( item => item.RelativePath == "api" );
106+
}
107+
108+
if ( metadataOptions.HasFlag( ODataMetadataOptions.Metadata ) )
109+
{
110+
group.Items.Should().Contain( item => item.RelativePath == "api/$metadata" );
111+
}
112+
}
113+
}
114+
59115
private readonly ITestOutputHelper console;
60116

61117
public ODataApiDescriptionProviderTest( ITestOutputHelper console ) => this.console = console;

src/Common/src/Common.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,10 @@ public ODataQueryOptionsConventionBuilder QueryOptions
3838
get => queryOptions ??= new();
3939
set => queryOptions = value;
4040
}
41+
42+
/// <summary>
43+
/// Gets or sets the OData metadata options used during API exploration.
44+
/// </summary>
45+
/// <value>One or more <see cref="ODataMetadataOptions"/> values.</value>
46+
public ODataMetadataOptions MetadataOptions { get; set; } = ODataMetadataOptions.None;
4147
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.ApiExplorer;
4+
5+
/// <summary>
6+
/// Represents the possible OData metadata options used during API exploration.
7+
/// </summary>
8+
[Flags]
9+
public enum ODataMetadataOptions
10+
{
11+
/// <summary>
12+
/// Indicates no OData metadata options.
13+
/// </summary>
14+
None = 0,
15+
16+
/// <summary>
17+
/// Indicates the OData service document will be included.
18+
/// </summary>
19+
ServiceDocument = 1,
20+
21+
/// <summary>
22+
/// Indicates the OData metadata document will be included.
23+
/// </summary>
24+
Metadata = 2,
25+
26+
/// <summary>
27+
/// Indicates all OData metadata options.
28+
/// </summary>
29+
All = ServiceDocument | Metadata,
30+
}

src/Common/src/Common.OData/TypeExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ internal static partial class TypeExtensions
2828

2929
internal static bool IsODataController( this Type controllerType ) => controllerType.UsingOData();
3030

31-
internal static bool IsMetadataController( this TypeInfo controllerType )
31+
internal static bool IsMetadataController( this Type controllerType )
3232
{
3333
metadataController ??= typeof( MetadataController );
3434
return metadataController.IsAssignableFrom( controllerType );

0 commit comments

Comments
 (0)