Skip to content

Commit 1987182

Browse files
Add API Explorer support for OData query options for non-OData endpoints
1 parent e45f7b7 commit 1987182

File tree

20 files changed

+568
-80
lines changed

20 files changed

+568
-80
lines changed

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

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Asp.Versioning.ApiExplorer;
77
using Asp.Versioning.OData;
88
using Asp.Versioning.Routing;
99
using Microsoft.AspNet.OData;
10+
using Microsoft.AspNet.OData.Extensions;
1011
using Microsoft.AspNet.OData.Formatter;
1112
using Microsoft.AspNet.OData.Routing;
1213
using Microsoft.AspNet.OData.Routing.Template;
@@ -63,15 +64,12 @@ public ODataApiExplorer( HttpConfiguration configuration, ODataApiExplorerOption
6364
protected virtual IModelTypeBuilder ModelTypeBuilder =>
6465
modelTypeBuilder ??= Configuration.DependencyResolver.GetModelTypeBuilder();
6566

66-
/// <summary>
67-
/// Determines whether the action should be considered.
68-
/// </summary>
69-
/// <param name="actionRouteParameterValue">The action route parameter value.</param>
70-
/// <param name="actionDescriptor">The associated <see cref="HttpActionDescriptor">action descriptor</see>.</param>
71-
/// <param name="route">The associated <see cref="IHttpRoute">route</see>.</param>
72-
/// <param name="apiVersion">The <see cref="ApiVersion">API version</see> to consider the controller for.</param>
73-
/// <returns>True if the action should be explored; otherwise, false.</returns>
74-
protected override bool ShouldExploreAction( string actionRouteParameterValue, HttpActionDescriptor actionDescriptor, IHttpRoute route, ApiVersion apiVersion )
67+
/// <inheritdoc />
68+
protected override bool ShouldExploreAction(
69+
string actionRouteParameterValue,
70+
HttpActionDescriptor actionDescriptor,
71+
IHttpRoute route,
72+
ApiVersion apiVersion )
7573
{
7674
if ( actionDescriptor == null )
7775
{
@@ -96,15 +94,12 @@ protected override bool ShouldExploreAction( string actionRouteParameterValue, H
9694
return actionDescriptor.GetApiVersionMetadata().IsMappedTo( apiVersion );
9795
}
9896

99-
/// <summary>
100-
/// Determines whether the controller should be considered.
101-
/// </summary>
102-
/// <param name="controllerRouteParameterValue">The controller route parameter value.</param>
103-
/// <param name="controllerDescriptor">The associated <see cref="HttpControllerDescriptor">controller descriptor</see>.</param>
104-
/// <param name="route">The associated <see cref="IHttpRoute">route</see>.</param>
105-
/// <param name="apiVersion">The <see cref="ApiVersion">API version</see> to consider the controller for.</param>
106-
/// <returns>True if the controller should be explored; otherwise, false.</returns>
107-
protected override bool ShouldExploreController( string controllerRouteParameterValue, HttpControllerDescriptor controllerDescriptor, IHttpRoute route, ApiVersion apiVersion )
97+
/// <inheritdoc />
98+
protected override bool ShouldExploreController(
99+
string controllerRouteParameterValue,
100+
HttpControllerDescriptor controllerDescriptor,
101+
IHttpRoute route,
102+
ApiVersion apiVersion )
108103
{
109104
if ( controllerDescriptor == null )
110105
{
@@ -141,26 +136,26 @@ protected override bool ShouldExploreController( string controllerRouteParameter
141136
return true;
142137
}
143138

144-
/// <summary>
145-
/// Explores controllers that do not use direct routes (aka "attribute" routing).
146-
/// </summary>
147-
/// <param name="controllerMappings">The <see cref="IDictionary{TKey, TValue}">collection</see> of controller mappings.</param>
148-
/// <param name="route">The <see cref="IHttpRoute">route</see> to explore.</param>
149-
/// <param name="apiVersion">The <see cref="ApiVersion">API version</see> to explore.</param>
150-
/// <returns>The <see cref="Collection{T}">collection</see> of discovered <see cref="VersionedApiDescription">API descriptions</see>.</returns>
151-
protected override Collection<VersionedApiDescription> ExploreRouteControllers( IDictionary<string, HttpControllerDescriptor> controllerMappings, IHttpRoute route, ApiVersion apiVersion )
139+
/// <inheritdoc />
140+
protected override Collection<VersionedApiDescription> ExploreRouteControllers(
141+
IDictionary<string, HttpControllerDescriptor> controllerMappings,
142+
IHttpRoute route,
143+
ApiVersion apiVersion )
152144
{
153145
if ( controllerMappings == null )
154146
{
155147
throw new ArgumentNullException( nameof( controllerMappings ) );
156148
}
157149

150+
Collection<VersionedApiDescription> apiDescriptions;
151+
158152
if ( route is not ODataRoute )
159153
{
160-
return base.ExploreRouteControllers( controllerMappings, route, apiVersion );
154+
apiDescriptions = base.ExploreRouteControllers( controllerMappings, route, apiVersion );
155+
return ExploreQueryOptions( route, apiDescriptions );
161156
}
162157

163-
var apiDescriptions = new Collection<VersionedApiDescription>();
158+
apiDescriptions = new();
164159
var modelSelector = Configuration.GetODataRootContainer( route ).GetRequiredService<IEdmModelSelector>();
165160
var edmModel = modelSelector.SelectModel( apiVersion );
166161

@@ -184,17 +179,28 @@ protected override Collection<VersionedApiDescription> ExploreRouteControllers(
184179
}
185180
}
186181

187-
ExploreQueryOptions( apiDescriptions, Configuration.GetODataRootContainer( route ).GetRequiredService<ODataUriResolver>() );
182+
return ExploreQueryOptions( route, apiDescriptions );
183+
}
188184

189-
return apiDescriptions;
185+
/// <inheritdoc />
186+
protected override Collection<VersionedApiDescription> ExploreDirectRouteControllers(
187+
HttpControllerDescriptor controllerDescriptor,
188+
IReadOnlyList<HttpActionDescriptor> candidateActionDescriptors,
189+
IHttpRoute route,
190+
ApiVersion apiVersion )
191+
{
192+
var apiDescriptions = base.ExploreDirectRouteControllers( controllerDescriptor, candidateActionDescriptors, route, apiVersion );
193+
return ExploreQueryOptions( route, apiDescriptions );
190194
}
191195

192196
/// <summary>
193197
/// Explores the OData query options for the specified API descriptions.
194198
/// </summary>
195199
/// <param name="apiDescriptions">The <see cref="IEnumerable{T}">sequence</see> of <see cref="VersionedApiDescription">API descriptions</see> to explore.</param>
196200
/// <param name="uriResolver">The associated <see cref="ODataUriResolver">OData URI resolver</see>.</param>
197-
protected virtual void ExploreQueryOptions( IEnumerable<VersionedApiDescription> apiDescriptions, ODataUriResolver uriResolver )
201+
protected virtual void ExploreQueryOptions(
202+
IEnumerable<VersionedApiDescription> apiDescriptions,
203+
ODataUriResolver uriResolver )
198204
{
199205
if ( uriResolver == null )
200206
{
@@ -206,12 +212,32 @@ protected virtual void ExploreQueryOptions( IEnumerable<VersionedApiDescription>
206212
{
207213
NoDollarPrefix = uriResolver.EnableNoDollarQueryOptions,
208214
DescriptionProvider = queryOptions.DescriptionProvider,
215+
DefaultQuerySettings = Configuration.GetDefaultQuerySettings(),
209216
};
210217

211218
queryOptions.ApplyTo( apiDescriptions, settings );
212219
}
213220

214-
private ResponseDescription CreateResponseDescriptionWithRoute( HttpActionDescriptor actionDescriptor, IHttpRoute route, ApiVersion apiVersion )
221+
private Collection<VersionedApiDescription> ExploreQueryOptions(
222+
IHttpRoute route,
223+
Collection<VersionedApiDescription> apiDescriptions )
224+
{
225+
if ( apiDescriptions.Count == 0 )
226+
{
227+
return apiDescriptions;
228+
}
229+
230+
var uriResolver = Configuration.GetODataRootContainer( route ).GetRequiredService<ODataUriResolver>();
231+
232+
ExploreQueryOptions( apiDescriptions, uriResolver );
233+
234+
return apiDescriptions;
235+
}
236+
237+
private ResponseDescription CreateResponseDescriptionWithRoute(
238+
HttpActionDescriptor actionDescriptor,
239+
IHttpRoute route,
240+
ApiVersion apiVersion )
215241
{
216242
var description = CreateResponseDescription( actionDescriptor );
217243
var serviceProvider = actionDescriptor.Configuration.GetODataRootContainer( route );
@@ -347,7 +373,10 @@ private static bool WillReadUri( HttpParameterBinding parameterBinding )
347373
return willReadUri;
348374
}
349375

350-
private ApiParameterDescription CreateParameterDescriptionFromBinding( HttpParameterBinding parameterBinding, IServiceProvider serviceProvider, ApiVersion apiVersion )
376+
private ApiParameterDescription CreateParameterDescriptionFromBinding(
377+
HttpParameterBinding parameterBinding,
378+
IServiceProvider serviceProvider,
379+
ApiVersion apiVersion )
351380
{
352381
var descriptor = parameterBinding.Descriptor;
353382
var description = CreateParameterDescription( descriptor );
@@ -378,7 +407,10 @@ private ApiParameterDescription CreateParameterDescriptionFromBinding( HttpParam
378407
return description;
379408
}
380409

381-
private IList<ApiParameterDescription> CreateParameterDescriptions( HttpActionDescriptor actionDescriptor, IHttpRoute route, ApiVersion apiVersion )
410+
private IList<ApiParameterDescription> CreateParameterDescriptions(
411+
HttpActionDescriptor actionDescriptor,
412+
IHttpRoute route,
413+
ApiVersion apiVersion )
382414
{
383415
var list = new List<ApiParameterDescription>();
384416
var actionBinding = GetActionBinding( actionDescriptor );
@@ -422,7 +454,8 @@ private IList<ApiParameterDescription> CreateParameterDescriptions( HttpActionDe
422454
return list;
423455
}
424456

425-
private static IEnumerable<MediaTypeFormatter> GetInnerFormatters( IEnumerable<MediaTypeFormatter> mediaTypeFormatters ) => mediaTypeFormatters.Select( Decorator.GetInner );
457+
private static IEnumerable<MediaTypeFormatter> GetInnerFormatters( IEnumerable<MediaTypeFormatter> mediaTypeFormatters ) =>
458+
mediaTypeFormatters.Select( Decorator.GetInner );
426459

427460
private static void PopulateMediaTypeFormatters(
428461
HttpActionDescriptor actionDescriptor,

src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ private static Type GetController( ApiDescription apiDescription ) =>
1616
apiDescription.ActionDescriptor.ControllerDescriptor.ControllerType;
1717

1818
[MethodImpl( MethodImplOptions.AggressiveInlining )]
19-
private static bool IsODataLike( ApiDescription description ) =>
20-
description.ActionDescriptor.GetCustomAttributes<EnableQueryAttribute>( inherit: true ).Count > 0;
19+
private static bool IsODataLike( ApiDescription description )
20+
{
21+
var parameters = description.ParameterDescriptions;
22+
23+
for ( var i = 0; i < parameters.Count; i++ )
24+
{
25+
if ( parameters[i].ParameterDescriptor.ParameterType.IsODataQueryOptions() )
26+
{
27+
return true;
28+
}
29+
}
30+
31+
return false;
32+
}
2133
}

src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace System.Web.Http;
44

55
using Asp.Versioning;
66
using Asp.Versioning.ApiExplorer;
7+
using Microsoft.AspNet.OData.Routing;
78
using Microsoft.OData;
89
using System.Collections.Concurrent;
910
using System.Web.Http.Description;
@@ -15,6 +16,7 @@ namespace System.Web.Http;
1516
public static class HttpConfigurationExtensions
1617
{
1718
private const string RootContainerMappingsKey = "Microsoft.AspNet.OData.RootContainerMappingsKey";
19+
private const string NonODataRootContainerKey = "Microsoft.AspNet.OData.NonODataRootContainerKey";
1820
private const string UrlKeyDelimiterKey = "Microsoft.AspNet.OData.UrlKeyDelimiterKey";
1921

2022
/// <summary>
@@ -69,14 +71,22 @@ private static ODataApiExplorer AddODataApiExplorer( this HttpConfiguration conf
6971

7072
internal static IServiceProvider GetODataRootContainer( this HttpConfiguration configuration, IHttpRoute route )
7173
{
72-
var containers = (ConcurrentDictionary<string, IServiceProvider>) configuration.Properties.GetOrAdd( RootContainerMappingsKey, key => new ConcurrentDictionary<string, IServiceProvider>() );
74+
var properties = configuration.Properties;
75+
var containers = (ConcurrentDictionary<string, IServiceProvider>) properties.GetOrAdd( RootContainerMappingsKey, key => new ConcurrentDictionary<string, IServiceProvider>() );
7376
var routeName = configuration.Routes.GetRouteName( route );
7477

7578
if ( !string.IsNullOrEmpty( routeName ) && containers.TryGetValue( routeName!, out var serviceProvider ) )
7679
{
7780
return serviceProvider;
7881
}
7982

83+
if ( route is not ODataRoute &&
84+
properties.TryGetValue( NonODataRootContainerKey, out var value ) &&
85+
( serviceProvider = value as IServiceProvider ) is not null )
86+
{
87+
return serviceProvider;
88+
}
89+
8090
throw new InvalidOperationException( ODataExpSR.NullContainer );
8191
}
8292

src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Conventions/ODataValidationSettingsConventionTest.cs

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
namespace Asp.Versioning.Conventions;
44

55
using Asp.Versioning.Description;
6+
using Asp.Versioning.Simulators.Models;
7+
using Asp.Versioning.Simulators.V1;
68
using Microsoft.AspNet.OData;
79
using Microsoft.AspNet.OData.Builder;
810
using Microsoft.AspNet.OData.Extensions;
911
using Microsoft.AspNet.OData.Query;
1012
using Microsoft.OData.Edm;
1113
using System.Collections.ObjectModel;
1214
using System.Net.Http;
15+
using System.Reflection;
1316
using System.Web.Http;
1417
using System.Web.Http.Controllers;
1518
using System.Web.Http.Description;
@@ -457,6 +460,97 @@ public void apply_to_should_use_model_bound_query_attributes()
457460
options => options.ExcludingMissingMembers() );
458461
}
459462

463+
[Fact]
464+
public void apply_to_should_process_odataX2Dlike_api_description()
465+
{
466+
// arrange
467+
var controllerType = typeof( BooksController );
468+
var controllerName = controllerType.Name.Substring( 0, controllerType.Name.Length - 10 );
469+
var action = controllerType.GetRuntimeMethods()
470+
.First( m => m.Name == "Get" && m.GetParameters().Length == 1 );
471+
var configuration = new HttpConfiguration();
472+
var controllerDescriptor = new HttpControllerDescriptor( configuration, controllerName, controllerType );
473+
var actionDescriptor = new ReflectedHttpActionDescriptor( controllerDescriptor, action ) { Configuration = configuration };
474+
var parameter = actionDescriptor.GetParameters()[0];
475+
var description = new VersionedApiDescription()
476+
{
477+
ActionDescriptor = actionDescriptor,
478+
HttpMethod = HttpMethod.Get,
479+
ParameterDescriptions =
480+
{
481+
new()
482+
{
483+
Name = parameter.ParameterName,
484+
ParameterDescriptor = parameter,
485+
Source = Unknown,
486+
},
487+
},
488+
ResponseDescription = new() { ResponseType = typeof( IEnumerable<Book> ) },
489+
};
490+
var builder = new ODataQueryOptionsConventionBuilder();
491+
var settings = new ODataQueryOptionSettings()
492+
{
493+
DescriptionProvider = builder.DescriptionProvider,
494+
DefaultQuerySettings = new(),
495+
};
496+
497+
configuration.EnableDependencyInjection();
498+
builder.Controller<BooksController>()
499+
.Action( c => c.Get( default ) )
500+
.Allow( Select | Count )
501+
.AllowOrderBy( "title", "published" );
502+
503+
// act
504+
builder.ApplyTo( new[] { description }, settings );
505+
506+
// assert
507+
description.ParameterDescriptions.RemoveAt( 0 );
508+
description.ParameterDescriptions.Should().BeEquivalentTo(
509+
new[]
510+
{
511+
new
512+
{
513+
Name = "$select",
514+
Source = FromUri,
515+
ParameterDescriptor = new
516+
{
517+
ParameterName = "$select",
518+
ParameterType = typeof( string ),
519+
Prefix = "$",
520+
IsOptional = true,
521+
DefaultValue = default( object ),
522+
},
523+
},
524+
new
525+
{
526+
Name = "$orderby",
527+
Source = FromUri,
528+
ParameterDescriptor = new
529+
{
530+
ParameterName = "$orderby",
531+
ParameterType = typeof( string ),
532+
Prefix = "$",
533+
IsOptional = true,
534+
DefaultValue = default( object ),
535+
},
536+
},
537+
new
538+
{
539+
Name = "$count",
540+
Source = FromUri,
541+
ParameterDescriptor = new
542+
{
543+
ParameterName = "$count",
544+
ParameterType = typeof( bool ),
545+
Prefix = "$",
546+
IsOptional = true,
547+
DefaultValue = (object) false,
548+
},
549+
},
550+
},
551+
options => options.ExcludingMissingMembers() );
552+
}
553+
460554
public static IEnumerable<object[]> EnableQueryAttributeData
461555
{
462556
get
@@ -495,7 +589,8 @@ private static ApiDescription NewApiDescription( Type controllerType ) =>
495589
private static ApiDescription NewApiDescription( Type controllerType, Type responseType, IEdmModel model )
496590
{
497591
var configuration = new HttpConfiguration();
498-
var controllerDescriptor = new HttpControllerDescriptor( configuration, "Orders", controllerType );
592+
var controllerName = controllerType.Name.Substring( 0, controllerType.Name.Length - 10 );
593+
var controllerDescriptor = new HttpControllerDescriptor( configuration, controllerName, controllerType );
499594
var method = controllerType.GetMethod( "Get" );
500595
var actionDescriptor = new ReflectedHttpActionDescriptor( controllerDescriptor, method ) { Configuration = configuration };
501596

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.Simulators.Models;
4+
5+
public class Book
6+
{
7+
public string Id { get; set; }
8+
9+
public string Author { get; set; }
10+
11+
public string Title { get; set; }
12+
13+
public int Published { get; set; }
14+
}

0 commit comments

Comments
 (0)