Skip to content

Commit 8842855

Browse files
author
Chris Martinez
committed
Add support for exploring OData query options using an ad hoc EDM. Related #928
1 parent 244450d commit 8842855

File tree

20 files changed

+857
-102
lines changed

20 files changed

+857
-102
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.ApiExplorer;
4+
5+
using Asp.Versioning.Conventions;
6+
using Asp.Versioning.Description;
7+
using Asp.Versioning.OData;
8+
using Microsoft.OData.Edm;
9+
using System.Collections.Generic;
10+
using System.Web.Http;
11+
using System.Web.Http.Description;
12+
13+
internal sealed class AdHocEdmScope : IDisposable
14+
{
15+
private readonly IReadOnlyList<VersionedApiDescription> results;
16+
private bool disposed;
17+
18+
internal AdHocEdmScope(
19+
IReadOnlyList<VersionedApiDescription> apiDescriptions,
20+
VersionedODataModelBuilder builder )
21+
{
22+
var conventions = builder.ModelConfigurations.OfType<IODataQueryOptionsConvention>().ToArray();
23+
24+
results = FilterResults( apiDescriptions, conventions );
25+
26+
if ( results.Count > 0 )
27+
{
28+
ApplyAdHocEdm( builder.GetEdmModels(), results );
29+
}
30+
}
31+
32+
public void Dispose()
33+
{
34+
if ( disposed )
35+
{
36+
return;
37+
}
38+
39+
disposed = true;
40+
41+
for ( var i = 0; i < results.Count; i++ )
42+
{
43+
results[i].SetProperty( default( IEdmModel ) );
44+
}
45+
}
46+
47+
private static IReadOnlyList<VersionedApiDescription> FilterResults(
48+
IReadOnlyList<VersionedApiDescription> apiDescriptions,
49+
IReadOnlyList<IODataQueryOptionsConvention> conventions )
50+
{
51+
if ( conventions.Count == 0 )
52+
{
53+
return Array.Empty<VersionedApiDescription>();
54+
}
55+
56+
var results = default( List<VersionedApiDescription> );
57+
58+
for ( var i = 0; i < apiDescriptions.Count; i++ )
59+
{
60+
var apiDescription = apiDescriptions[i];
61+
62+
if ( apiDescription.EdmModel() != null || !apiDescription.IsODataLike() )
63+
{
64+
continue;
65+
}
66+
67+
results ??= new();
68+
results.Add( apiDescription );
69+
70+
for ( var j = 0; j < conventions.Count; j++ )
71+
{
72+
conventions[j].ApplyTo( apiDescription );
73+
}
74+
}
75+
76+
return results?.ToArray() ?? Array.Empty<VersionedApiDescription>();
77+
}
78+
79+
private static void ApplyAdHocEdm(
80+
IReadOnlyList<IEdmModel> models,
81+
IReadOnlyList<VersionedApiDescription> results )
82+
{
83+
for ( var i = 0; i < models.Count; i++ )
84+
{
85+
var model = models[i];
86+
var version = model.GetApiVersion();
87+
88+
for ( var j = 0; j < results.Count; j++ )
89+
{
90+
var result = results[j];
91+
var metadata = result.ActionDescriptor.GetApiVersionMetadata();
92+
93+
if ( metadata.IsMappedTo( version ) )
94+
{
95+
result.SetProperty( model );
96+
}
97+
}
98+
}
99+
}
100+
}

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

Lines changed: 48 additions & 10 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.Builder;
1011
using Microsoft.AspNet.OData.Extensions;
1112
using Microsoft.AspNet.OData.Formatter;
1213
using Microsoft.AspNet.OData.Routing;
@@ -16,6 +17,7 @@ namespace Asp.Versioning.ApiExplorer;
1617
using Microsoft.OData.UriParser;
1718
using System.Collections.ObjectModel;
1819
using System.Net.Http.Formatting;
20+
using System.Runtime.CompilerServices;
1921
using System.Text.RegularExpressions;
2022
using System.Web.Http;
2123
using System.Web.Http.Controllers;
@@ -50,7 +52,11 @@ public ODataApiExplorer( HttpConfiguration configuration )
5052
/// <param name="configuration">The current <see cref="HttpConfiguration">HTTP configuration</see>.</param>
5153
/// <param name="options">The associated <see cref="ODataApiExplorerOptions">API explorer options</see>.</param>
5254
public ODataApiExplorer( HttpConfiguration configuration, ODataApiExplorerOptions options )
53-
: base( configuration, options ) => this.options = options;
55+
: base( configuration, options )
56+
{
57+
this.options = options;
58+
options.AdHocModelBuilder.OnModelCreated += MarkAsAdHoc;
59+
}
5460

5561
/// <summary>
5662
/// Gets the options associated with the API explorer.
@@ -172,7 +178,20 @@ protected override Collection<VersionedApiDescription> ExploreRouteControllers(
172178
if ( route is not ODataRoute )
173179
{
174180
apiDescriptions = base.ExploreRouteControllers( controllerMappings, route, apiVersion );
175-
return ExploreQueryOptions( route, apiDescriptions );
181+
182+
if ( Options.AdHocModelBuilder.ModelConfigurations.Count == 0 )
183+
{
184+
ExploreQueryOptions( route, apiDescriptions );
185+
}
186+
else if ( apiDescriptions.Count > 0 )
187+
{
188+
using ( new AdHocEdmScope( apiDescriptions, Options.AdHocModelBuilder ) )
189+
{
190+
ExploreQueryOptions( route, apiDescriptions );
191+
}
192+
}
193+
194+
return apiDescriptions;
176195
}
177196

178197
apiDescriptions = new();
@@ -199,7 +218,8 @@ protected override Collection<VersionedApiDescription> ExploreRouteControllers(
199218
}
200219
}
201220

202-
return ExploreQueryOptions( route, apiDescriptions );
221+
ExploreQueryOptions( route, apiDescriptions );
222+
return apiDescriptions;
203223
}
204224

205225
/// <inheritdoc />
@@ -210,7 +230,25 @@ protected override Collection<VersionedApiDescription> ExploreDirectRouteControl
210230
ApiVersion apiVersion )
211231
{
212232
var apiDescriptions = base.ExploreDirectRouteControllers( controllerDescriptor, candidateActionDescriptors, route, apiVersion );
213-
return ExploreQueryOptions( route, apiDescriptions );
233+
234+
if ( apiDescriptions.Count == 0 )
235+
{
236+
return apiDescriptions;
237+
}
238+
239+
if ( Options.AdHocModelBuilder.ModelConfigurations.Count == 0 )
240+
{
241+
ExploreQueryOptions( route, apiDescriptions );
242+
}
243+
else if ( apiDescriptions.Count > 0 )
244+
{
245+
using ( new AdHocEdmScope( apiDescriptions, Options.AdHocModelBuilder ) )
246+
{
247+
ExploreQueryOptions( route, apiDescriptions );
248+
}
249+
}
250+
251+
return apiDescriptions;
214252
}
215253

216254
/// <summary>
@@ -238,20 +276,20 @@ protected virtual void ExploreQueryOptions(
238276
queryOptions.ApplyTo( apiDescriptions, settings );
239277
}
240278

241-
private Collection<VersionedApiDescription> ExploreQueryOptions(
242-
IHttpRoute route,
243-
Collection<VersionedApiDescription> apiDescriptions )
279+
[MethodImpl( MethodImplOptions.AggressiveInlining )]
280+
private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) =>
281+
model.SetAnnotationValue( model, AdHocAnnotation.Instance );
282+
283+
private void ExploreQueryOptions( IHttpRoute route, Collection<VersionedApiDescription> apiDescriptions )
244284
{
245285
if ( apiDescriptions.Count == 0 )
246286
{
247-
return apiDescriptions;
287+
return;
248288
}
249289

250290
var uriResolver = Configuration.GetODataRootContainer( route ).GetRequiredService<ODataUriResolver>();
251291

252292
ExploreQueryOptions( apiDescriptions, uriResolver );
253-
254-
return apiDescriptions;
255293
}
256294

257295
private ResponseDescription CreateResponseDescriptionWithRoute(

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public partial class ODataApiExplorerOptions : ApiExplorerOptions
1515
/// Initializes a new instance of the <see cref="ODataApiExplorerOptions"/> class.
1616
/// </summary>
1717
/// <param name="configuration">The current <see cref="HttpConfiguration">configuration</see> associated with the options.</param>
18-
public ODataApiExplorerOptions( HttpConfiguration configuration ) : base( configuration ) { }
18+
public ODataApiExplorerOptions( HttpConfiguration configuration )
19+
: base( configuration ) => AdHocModelBuilder = new( configuration );
1920

2021
/// <summary>
2122
/// Gets or sets a value indicating whether the API explorer settings are honored.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.Conventions;
4+
5+
using Asp.Versioning.OData;
6+
using System.Web.Http.Description;
7+
8+
/// <content>
9+
/// Provides additional implementation specific to ASP.NET Web API.
10+
/// </content>
11+
public partial class ImplicitModelBoundSettingsConvention : IModelConfiguration, IODataQueryOptionsConvention
12+
{
13+
/// <inheritdoc />
14+
public void ApplyTo( ApiDescription apiDescription )
15+
{
16+
var response = apiDescription.ResponseDescription;
17+
var type = response.ResponseType ?? response.DeclaredType;
18+
19+
if ( type == null )
20+
{
21+
return;
22+
}
23+
24+
if ( type.IsEnumerable( out var itemType ) )
25+
{
26+
type = itemType;
27+
}
28+
29+
types.Add( type! );
30+
}
31+
}

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

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

33
namespace Asp.Versioning.Conventions;
44

5-
using Microsoft.AspNet.OData;
65
using System.Runtime.CompilerServices;
76
using System.Web.Http.Description;
87

@@ -14,20 +13,4 @@ public partial class ODataQueryOptionsConventionBuilder
1413
[MethodImpl( MethodImplOptions.AggressiveInlining )]
1514
private static Type GetController( ApiDescription apiDescription ) =>
1615
apiDescription.ActionDescriptor.ControllerDescriptor.ControllerType;
17-
18-
[MethodImpl( MethodImplOptions.AggressiveInlining )]
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-
}
3316
}

0 commit comments

Comments
 (0)