Skip to content

Commit ba9ddcd

Browse files
Chris Martinezcommonsensesoftware
Chris Martinez
authored andcommitted
Improved support fro building OData route path in API descriptions
1 parent e5a771f commit ba9ddcd

11 files changed

+773
-33
lines changed

src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Description/ODataApiExplorer.cs

+20-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
namespace Microsoft.Web.Http.Description
22
{
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.OData.Edm;
35
using Microsoft.Web.Http.Routing;
46
using System;
57
using System.Collections.Generic;
@@ -230,19 +232,19 @@ ApiParameterDescription CreateParameterDescriptionFromBinding( HttpParameterBind
230232

231233
if ( parameterBinding.WillReadBody )
232234
{
233-
description.Source = ApiParameterSource.FromBody;
235+
description.Source = FromBody;
234236
return description;
235237
}
236238

237239
if ( WillReadUri( parameterBinding ) )
238240
{
239-
description.Source = ApiParameterSource.FromUri;
241+
description.Source = FromUri;
240242
}
241243

242244
return description;
243245
}
244246

245-
IList<ApiParameterDescription> CreateParameterDescriptions( HttpActionDescriptor actionDescriptor, IHttpRoute route )
247+
IReadOnlyList<ApiParameterDescription> CreateParameterDescriptions( HttpActionDescriptor actionDescriptor, IHttpRoute route )
246248
{
247249
Contract.Requires( actionDescriptor != null );
248250
Contract.Requires( route != null );
@@ -292,7 +294,7 @@ IList<ApiParameterDescription> CreateParameterDescriptions( HttpActionDescriptor
292294

293295
void PopulateMediaTypeFormatters(
294296
HttpActionDescriptor actionDescriptor,
295-
IList<ApiParameterDescription> parameterDescriptions,
297+
IReadOnlyList<ApiParameterDescription> parameterDescriptions,
296298
IHttpRoute route,
297299
Type responseType,
298300
IList<MediaTypeFormatter> requestFormatters,
@@ -331,11 +333,18 @@ void PopulateMediaTypeFormatters(
331333

332334
void PopulateActionDescriptions( HttpActionDescriptor actionDescriptor, IHttpRoute route, string localPath, Collection<VersionedApiDescription> apiDescriptions, ApiVersion apiVersion )
333335
{
334-
var documentation = DocumentationProvider?.GetDocumentation( actionDescriptor );
335336
var parameterDescriptions = CreateParameterDescriptions( actionDescriptor, route );
337+
var context = new ODataRouteBuilderContext( Configuration, localPath, (ODataRoute) route, actionDescriptor, parameterDescriptions );
338+
339+
if ( context.EdmModel.EntityContainer == null )
340+
{
341+
return;
342+
}
343+
344+
var relativePath = new ODataRouteBuilder( context ).Build();
345+
var documentation = DocumentationProvider?.GetDocumentation( actionDescriptor );
336346
var responseDescription = CreateResponseDescription( actionDescriptor );
337347
var responseType = responseDescription.ResponseType ?? responseDescription.DeclaredType;
338-
var relativePath = new ODataRouteBuilder( localPath, route, actionDescriptor ).Build();
339348
var requestFormatters = new List<MediaTypeFormatter>();
340349
var responseFormatters = new List<MediaTypeFormatter>();
341350
var supportedMethods = GetHttpMethodsSupportedByAction( route, actionDescriptor );
@@ -354,7 +363,11 @@ void PopulateActionDescriptions( HttpActionDescriptor actionDescriptor, IHttpRou
354363
Route = route,
355364
ResponseDescription = responseDescription,
356365
ApiVersion = apiVersion,
357-
IsDeprecated = deprecated
366+
IsDeprecated = deprecated,
367+
Properties =
368+
{
369+
[typeof( IEdmModel )] = context.EdmModel
370+
}
358371
};
359372

360373
apiDescription.ParameterDescriptions.AddRange( parameterDescriptions );
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,238 @@
11
namespace Microsoft.Web.Http.Description
22
{
3+
using Microsoft.OData.Edm;
34
using System;
45
using System.Collections.Generic;
56
using System.Diagnostics.Contracts;
67
using System.Linq;
7-
using System.Web.Http.Controllers;
8-
using System.Web.Http.Routing;
8+
using System.Text;
9+
using System.Web.Http.Description;
10+
using System.Web.OData;
11+
using System.Web.OData.Query;
912
using System.Web.OData.Routing;
1013
using static System.Linq.Enumerable;
1114
using static System.String;
15+
using static System.StringComparison;
16+
using static System.Web.Http.Description.ApiParameterSource;
1217

1318
sealed class ODataRouteBuilder
1419
{
15-
readonly string routeTemplate;
16-
readonly IHttpRoute route;
17-
readonly HttpActionDescriptor actionDescriptor;
18-
19-
internal ODataRouteBuilder( string routeTemplate, IHttpRoute route, HttpActionDescriptor actionDescriptor )
20+
static readonly Type GeographyType = typeof( Spatial.Geography );
21+
static readonly Type GeometryType = typeof( Spatial.Geometry );
22+
static readonly Dictionary<Type, string> quotedTypes = new Dictionary<Type, string>()
2023
{
21-
Contract.Requires( !IsNullOrEmpty( routeTemplate ) );
22-
Contract.Requires( route != null );
23-
Contract.Requires( actionDescriptor != null );
24+
[typeof( string )] = "",
25+
[typeof( TimeSpan )] = "duration",
26+
[typeof( byte[] )] = "binary"
27+
};
2428

25-
this.routeTemplate = routeTemplate;
26-
this.route = route;
27-
this.actionDescriptor = actionDescriptor;
29+
internal ODataRouteBuilder( ODataRouteBuilderContext context )
30+
{
31+
Contract.Requires( context != null );
32+
Context = context;
2833
}
2934

3035
internal string Build()
3136
{
32-
if ( !( route is ODataRoute odataRoute ) )
33-
{
34-
return routeTemplate;
35-
}
37+
var builder = new StringBuilder();
38+
39+
BuildPath( builder );
40+
BuildQuery( builder );
41+
42+
return builder.ToString();
43+
}
44+
45+
ODataRouteBuilderContext Context { get; }
46+
47+
void BuildPath( StringBuilder builder )
48+
{
49+
Contract.Requires( builder != null );
3650

3751
var segments = new List<string>();
38-
var prefix = odataRoute.RoutePrefix?.Trim( '/' );
52+
var prefix = Context.Route.RoutePrefix?.Trim( '/' );
3953

4054
if ( !IsNullOrEmpty( prefix ) )
4155
{
4256
segments.Add( prefix );
4357
}
4458

45-
var controllerDescriptor = actionDescriptor.ControllerDescriptor;
46-
var path = controllerDescriptor.GetCustomAttributes<ODataRoutePrefixAttribute>().FirstOrDefault()?.Prefix?.Trim( '/' );
59+
var path = GetEntitySetSegment() + GetEntityKeySegment();
60+
61+
segments.Add( path );
62+
builder.Append( Join( "/", segments ) );
63+
}
64+
65+
void BuildQuery( StringBuilder builder )
66+
{
67+
Contract.Requires( builder != null );
68+
69+
var queryParameters = FilterQueryParameters( Context.ParameterDescriptions );
70+
71+
if ( queryParameters.Count == 0 )
72+
{
73+
return;
74+
}
75+
76+
var queryString = new StringBuilder();
4777

48-
if ( IsNullOrEmpty( path ) )
78+
using ( var iterator = queryParameters.GetEnumerator() )
4979
{
50-
path = controllerDescriptor.ControllerName;
80+
iterator.MoveNext();
81+
var name = iterator.Current.Name;
82+
83+
queryString.Append( name );
84+
queryString.Append( "={" );
85+
queryString.Append( name );
86+
queryString.Append( '}' );
87+
88+
while ( iterator.MoveNext() )
89+
{
90+
name = iterator.Current.Name;
91+
queryString.Append( '&' );
92+
queryString.Append( name );
93+
queryString.Append( "={" );
94+
queryString.Append( name );
95+
queryString.Append( '}' );
96+
}
5197
}
5298

53-
var template = actionDescriptor.GetCustomAttributes<ODataRouteAttribute>().FirstOrDefault()?.PathTemplate;
99+
if ( queryString.Length > 0 )
100+
{
101+
builder.Append( '?' );
102+
builder.Append( queryString );
103+
}
104+
}
105+
106+
string GetEntitySetSegment()
107+
{
108+
var controllerDescriptor = Context.ActionDescriptor.ControllerDescriptor;
109+
var prefix = controllerDescriptor.GetCustomAttributes<ODataRoutePrefixAttribute>().FirstOrDefault()?.Prefix?.Trim( '/' );
110+
return IsNullOrEmpty( prefix ) ? controllerDescriptor.ControllerName : prefix;
111+
}
112+
113+
string GetEntityKeySegment()
114+
{
115+
var template = Context.ActionDescriptor.GetCustomAttributes<ODataRouteAttribute>().FirstOrDefault()?.PathTemplate;
54116

55117
if ( !IsNullOrEmpty( template ) )
56118
{
57-
path += template;
119+
return template;
58120
}
59121

60-
segments.Add( path );
122+
var keys = Context.EntityKeys.Where( key => Context.ParameterDescriptions.Any( p => key.Name.Equals( p.Name, OrdinalIgnoreCase ) ) );
123+
var convention = new StringBuilder();
124+
125+
using ( var iterator = keys.GetEnumerator() )
126+
{
127+
if ( iterator.MoveNext() )
128+
{
129+
convention.Append( '(' );
130+
131+
var key = iterator.Current;
132+
133+
if ( iterator.MoveNext() )
134+
{
135+
convention.Append( key.Name );
136+
convention.Append( '=' );
137+
ExpandParameterTemplate( convention, key );
138+
139+
while ( iterator.MoveNext() )
140+
{
141+
convention.Append( ',' );
142+
convention.Append( key.Name );
143+
convention.Append( '=' );
144+
ExpandParameterTemplate( convention, key );
145+
}
146+
}
147+
else
148+
{
149+
ExpandParameterTemplate( convention, key );
150+
}
151+
152+
convention.Append( ')' );
153+
}
154+
}
155+
156+
return convention.ToString();
157+
}
158+
159+
void ExpandParameterTemplate( StringBuilder template, IEdmStructuralProperty key )
160+
{
161+
Contract.Requires( template != null );
162+
Contract.Requires( key != null );
163+
164+
var name = key.Name;
165+
var typeDef = key.Type.Definition;
166+
167+
template.Append( "{" );
168+
template.Append( name );
169+
template.Append( "}" );
170+
171+
if ( typeDef.TypeKind == EdmTypeKind.Enum )
172+
{
173+
template.Insert( 0, '\'' );
174+
175+
if ( !Context.AllowUnqualifiedEnum )
176+
{
177+
template.Insert( 0, key.Type.FullName() );
178+
}
179+
180+
template.Append( '\'' );
181+
return;
182+
}
183+
184+
var type = typeDef.GetClrType( Context.AssembliesResolver );
185+
186+
if ( quotedTypes.TryGetValue( type, out var prefix ) )
187+
{
188+
template.Insert( 0, '\'' );
189+
template.Insert( 0, prefix );
190+
template.Append( '\'' );
191+
}
192+
else if ( GeographyType.IsAssignableFrom( type ) )
193+
{
194+
template.Insert( 0, "geography'" );
195+
template.Append( '\'' );
196+
}
197+
else if ( GeometryType.IsAssignableFrom( type ) )
198+
{
199+
template.Insert( 0, "geometry'" );
200+
template.Append( '\'' );
201+
}
202+
}
203+
204+
IReadOnlyList<ApiParameterDescription> FilterQueryParameters( IReadOnlyList<ApiParameterDescription> parameterDescriptions )
205+
{
206+
Contract.Requires( parameterDescriptions != null );
207+
Contract.Ensures( Contract.Result<IReadOnlyList<ApiParameterDescription>>() != null );
208+
209+
var queryParameters = new List<ApiParameterDescription>();
210+
var queryOptions = typeof( ODataQueryOptions );
211+
var actionParameters = typeof( ODataActionParameters );
212+
213+
foreach ( var parameter in parameterDescriptions )
214+
{
215+
if ( parameter.Source != FromUri )
216+
{
217+
continue;
218+
}
219+
220+
var parameterType = parameter.ParameterDescriptor?.ParameterType;
221+
222+
if ( parameterType == null ||
223+
queryOptions.IsAssignableFrom( parameterType ) ||
224+
actionParameters.IsAssignableFrom( parameterType ) )
225+
{
226+
continue;
227+
}
228+
229+
if ( !Context.EntityKeys.Any( key => key.Name.Equals( parameter.Name, OrdinalIgnoreCase ) ) )
230+
{
231+
queryParameters.Add( parameter );
232+
}
233+
}
61234

62-
return Join( "/", segments );
235+
return queryParameters;
63236
}
64237
}
65238
}

0 commit comments

Comments
 (0)