Skip to content

Commit 0eabd08

Browse files
author
Chris Martinez
committed
Complete OData refactor with support for Endpoint Routing. Resolves #608, #616, #647
1 parent b376328 commit 0eabd08

File tree

229 files changed

+3597
-3369
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

229 files changed

+3597
-3369
lines changed

src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ public sealed class DefaultModelTypeBuilder : IModelTypeBuilder
2828
{
2929
static readonly Type IEnumerableOfT = typeof( IEnumerable<> );
3030
readonly ConcurrentDictionary<ApiVersion, ModuleBuilder> modules = new ConcurrentDictionary<ApiVersion, ModuleBuilder>();
31-
readonly ConcurrentDictionary<ApiVersion, IDictionary<EdmTypeKey, Type>> generatedEdmTypesPerVersion = new ConcurrentDictionary<ApiVersion, IDictionary<EdmTypeKey, Type>>();
31+
readonly ConcurrentDictionary<ApiVersion, IDictionary<EdmTypeKey, Type>> generatedEdmTypesPerVersion =
32+
new ConcurrentDictionary<ApiVersion, IDictionary<EdmTypeKey, Type>>();
33+
readonly ConcurrentDictionary<ApiVersion, ConcurrentDictionary<EdmTypeKey, Type>> generatedActionParamsPerVersion =
34+
new ConcurrentDictionary<ApiVersion, ConcurrentDictionary<EdmTypeKey, Type>>();
3235

3336
/// <inheritdoc />
3437
public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion, IEdmModel edmModel )
@@ -47,12 +50,19 @@ public Type NewActionParameters( IServiceProvider services, IEdmAction action, A
4750
throw new ArgumentNullException( nameof( action ) );
4851
}
4952

50-
var name = controllerName + "." + action.FullName() + "Parameters";
51-
var properties = action.Parameters.Where( p => p.Name != "bindingParameter" ).Select( p => new ClassProperty( services, p, this ) );
52-
var signature = new ClassSignature( name, properties, apiVersion );
53-
var moduleBuilder = modules.GetOrAdd( apiVersion, CreateModuleForApiVersion );
53+
var paramTypes = generatedActionParamsPerVersion.GetOrAdd( apiVersion, _ => new ConcurrentDictionary<EdmTypeKey, Type>() );
54+
var fullTypeName = $"{controllerName}.{action.Namespace}.{controllerName}{action.Name}Parameters";
55+
var key = new EdmTypeKey( fullTypeName, apiVersion );
56+
var type = paramTypes.GetOrAdd( key, _ =>
57+
{
58+
var properties = action.Parameters.Where( p => p.Name != "bindingParameter" ).Select( p => new ClassProperty( services, p, this ) );
59+
var signature = new ClassSignature( fullTypeName, properties, apiVersion );
60+
var moduleBuilder = modules.GetOrAdd( apiVersion, CreateModuleForApiVersion );
61+
62+
return CreateTypeInfoFromSignature( moduleBuilder, signature );
63+
} );
5464

55-
return CreateTypeInfoFromSignature( moduleBuilder, signature );
65+
return type;
5666
}
5767

5868
IDictionary<EdmTypeKey, Type> GenerateTypesForEdmModel( IEdmModel model, ApiVersion apiVersion )

src/Common.OData.ApiExplorer/AspNet.OData/EdmTypeKey.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ internal EdmTypeKey( IEdmStructuredType type, ApiVersion apiVersion ) =>
1919
internal EdmTypeKey( IEdmTypeReference type, ApiVersion apiVersion ) =>
2020
hashCode = ComputeHash( type.FullName(), apiVersion );
2121

22+
internal EdmTypeKey( string fullTypeName, ApiVersion apiVersion ) =>
23+
hashCode = ComputeHash( fullTypeName, apiVersion );
24+
2225
public static bool operator ==( EdmTypeKey obj, EdmTypeKey other ) => obj.Equals( other );
2326

2427
public static bool operator !=( EdmTypeKey obj, EdmTypeKey other ) => !obj.Equals( other );

src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ void BuildPath( StringBuilder builder )
7373

7474
void AppendRoutePrefix( IList<string> segments )
7575
{
76-
var prefix = Context.Route.RoutePrefix?.Trim( '/' );
76+
var prefix = Context.RoutePrefix;
7777

7878
if ( IsNullOrEmpty( prefix ) )
7979
{

src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717
#endif
1818
using System;
1919
using System.Collections.Generic;
20+
using System.Linq;
2021
using System.Reflection;
22+
using System.Text.RegularExpressions;
2123
#if WEBAPI
2224
using System.Web.Http.Description;
2325
using System.Web.Http.Dispatcher;
2426
using ControllerActionDescriptor = System.Web.Http.Controllers.HttpActionDescriptor;
2527
#endif
2628
using static Microsoft.OData.ODataUrlKeyDelimiter;
2729
using static ODataRouteTemplateGenerationKind;
30+
using static System.StringComparison;
2831

2932
sealed partial class ODataRouteBuilderContext
3033
{
@@ -52,7 +55,7 @@ sealed partial class ODataRouteBuilderContext
5255

5356
internal string? RouteTemplate { get; }
5457

55-
internal ODataRoute Route { get; }
58+
internal string? RoutePrefix { get; }
5659

5760
internal ControllerActionDescriptor ActionDescriptor { get; }
5861

@@ -74,7 +77,11 @@ sealed partial class ODataRouteBuilderContext
7477

7578
internal bool AllowUnqualifiedEnum => Services.GetRequiredService<ODataUriResolver>() is StringAsEnumResolver;
7679

77-
internal static ODataRouteActionType GetActionType( IEdmEntitySet entitySet, IEdmOperation operation )
80+
internal
81+
#if !WEBAPI
82+
static
83+
#endif
84+
ODataRouteActionType GetActionType( IEdmEntitySet? entitySet, IEdmOperation? operation, ControllerActionDescriptor action )
7885
{
7986
if ( entitySet == null )
8087
{
@@ -91,7 +98,14 @@ internal static ODataRouteActionType GetActionType( IEdmEntitySet entitySet, IEd
9198
{
9299
if ( operation == null )
93100
{
94-
return ODataRouteActionType.EntitySet;
101+
if ( IsActionOrFunction( entitySet, action.ActionName, GetHttpMethods( action ) ) )
102+
{
103+
return ODataRouteActionType.Unknown;
104+
}
105+
else
106+
{
107+
return ODataRouteActionType.EntitySet;
108+
}
95109
}
96110
else if ( operation.IsBound )
97111
{
@@ -105,5 +119,113 @@ internal static ODataRouteActionType GetActionType( IEdmEntitySet entitySet, IEd
105119
// Slash became the default 4/18/2018
106120
// REF: https://github.com/OData/WebApi/pull/1393
107121
static ODataUrlKeyDelimiter UrlKeyDelimiterOrDefault( ODataUrlKeyDelimiter? urlKeyDelimiter ) => urlKeyDelimiter ?? Slash;
122+
123+
// REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/ActionRoutingConvention.cs
124+
// REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/FunctionRoutingConvention.cs
125+
// REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs
126+
// REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntityRoutingConvention.cs
127+
static bool IsActionOrFunction( IEdmEntitySet? entitySet, string actionName, IEnumerable<string> methods )
128+
{
129+
using var iterator = methods.GetEnumerator();
130+
131+
if ( !iterator.MoveNext() )
132+
{
133+
return false;
134+
}
135+
136+
var method = iterator.Current;
137+
138+
if ( iterator.MoveNext() )
139+
{
140+
return false;
141+
}
142+
143+
const string ActionMethod = "Post";
144+
const string FunctionMethod = "Get";
145+
146+
if ( ActionMethod.Equals( method, OrdinalIgnoreCase ) && actionName != ActionMethod )
147+
{
148+
if ( entitySet == null )
149+
{
150+
return true;
151+
}
152+
153+
return actionName != ( ActionMethod + entitySet.Name ) &&
154+
actionName != ( ActionMethod + entitySet.EntityType().Name ) &&
155+
!actionName.StartsWith( "CreateRef", Ordinal );
156+
}
157+
else if ( FunctionMethod.Equals( method, OrdinalIgnoreCase ) && actionName != FunctionMethod )
158+
{
159+
if ( entitySet == null )
160+
{
161+
// TODO: could be a singleton here
162+
return true;
163+
}
164+
165+
if ( actionName == ( ActionMethod + entitySet.Name ) ||
166+
actionName.StartsWith( "GetRef", Ordinal ) )
167+
{
168+
return false;
169+
}
170+
171+
var entity = entitySet.EntityType();
172+
173+
if ( actionName == ( ActionMethod + entity.Name ) )
174+
{
175+
return false;
176+
}
177+
178+
foreach ( var property in entity.NavigationProperties() )
179+
{
180+
if ( actionName.StartsWith( FunctionMethod + property.Name, OrdinalIgnoreCase ) )
181+
{
182+
return false;
183+
}
184+
}
185+
186+
return true;
187+
}
188+
189+
return false;
190+
}
191+
192+
IEdmOperation? ResolveOperation( IEdmEntityContainer container, string name )
193+
{
194+
var import = container.FindOperationImports( name ).SingleOrDefault();
195+
196+
if ( import != null )
197+
{
198+
return import.Operation;
199+
}
200+
201+
var qualifiedName = container.Namespace + "." + name;
202+
203+
if ( EntitySet != null )
204+
{
205+
var operation = EdmModel.FindBoundOperations( qualifiedName, EntitySet.EntityType() ).SingleOrDefault();
206+
207+
if ( operation != null )
208+
{
209+
return operation;
210+
}
211+
}
212+
213+
return EdmModel.FindDeclaredOperations( qualifiedName ).SingleOrDefault();
214+
}
215+
216+
sealed class FixedEdmModelServiceProviderDecorator : IServiceProvider
217+
{
218+
readonly IServiceProvider decorated;
219+
readonly IEdmModel edmModel;
220+
221+
internal FixedEdmModelServiceProviderDecorator( IServiceProvider decorated, IEdmModel edmModel )
222+
{
223+
this.decorated = decorated;
224+
this.edmModel = edmModel;
225+
}
226+
227+
public object GetService( Type serviceType ) =>
228+
serviceType == typeof( IEdmModel ) ? edmModel : decorated.GetService( serviceType );
229+
}
108230
}
109231
}

src/Common.OData/AspNet.OData/Builder/DelegatingModelConfiguration.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99

1010
sealed class DelegatingModelConfiguration : IModelConfiguration
1111
{
12-
readonly Action<ODataModelBuilder, ApiVersion> action;
12+
readonly Action<ODataModelBuilder, ApiVersion, string?> action;
1313

14-
internal DelegatingModelConfiguration( Action<ODataModelBuilder, ApiVersion> action ) => this.action = action;
14+
internal DelegatingModelConfiguration( Action<ODataModelBuilder, ApiVersion, string?> action ) => this.action = action;
1515

16-
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) => action( builder, apiVersion );
16+
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix ) => action( builder, apiVersion, routePrefix );
1717
}
1818
}

src/Common.OData/AspNet.OData/Builder/IModelConfiguration.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public interface IModelConfiguration
2020
/// </summary>
2121
/// <param name="builder">The <see cref="ODataModelBuilder">builder</see> used to apply configurations.</param>
2222
/// <param name="apiVersion">The <see cref="ApiVersion">API version</see> associated with the <paramref name="builder"/>.</param>
23-
void Apply( ODataModelBuilder builder, ApiVersion apiVersion );
23+
/// <param name="routePrefix">The route prefix associated with the configuration, if any.</param>
24+
void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix );
2425
}
2526
}

src/Common.OData/AspNet.OData/Builder/VersionedODataModelBuilder.cs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#if !WEBAPI
44
using Microsoft.AspNetCore.Mvc;
55
using Microsoft.AspNetCore.Mvc.Versioning;
6+
using Microsoft.OData;
67
#endif
78
using Microsoft.OData.Edm;
89
#if WEBAPI
@@ -11,6 +12,7 @@
1112
#endif
1213
using System;
1314
using System.Collections.Generic;
15+
using System.Linq;
1416

1517
/// <summary>
1618
/// Represents a versioned variant of the <see cref="ODataModelBuilder"/>.
@@ -27,9 +29,9 @@ public partial class VersionedODataModelBuilder
2729
/// <summary>
2830
/// Gets or sets the default model configuration.
2931
/// </summary>
30-
/// <value>The <see cref="Action{T1, T2}">method</see> for the default model configuration.
32+
/// <value>The <see cref="Action{T1, T2, T3}">method</see> for the default model configuration.
3133
/// The default value is <c>null</c>.</value>
32-
public Action<ODataModelBuilder, ApiVersion>? DefaultModelConfiguration { get; set; }
34+
public Action<ODataModelBuilder, ApiVersion, string?>? DefaultModelConfiguration { get; set; }
3335

3436
/// <summary>
3537
/// Gets the list of model configurations associated with the builder.
@@ -48,13 +50,20 @@ public partial class VersionedODataModelBuilder
4850
/// Builds and returns the sequence of EDM models based on the define model configurations.
4951
/// </summary>
5052
/// <returns>A <see cref="IEnumerable{T}">sequence</see> of <see cref="IEdmModel">EDM models</see>.</returns>
51-
public virtual IEnumerable<IEdmModel> GetEdmModels()
53+
public IEnumerable<IEdmModel> GetEdmModels() => GetEdmModels( default );
54+
55+
/// <summary>
56+
/// Builds and returns the sequence of EDM models based on the define model configurations.
57+
/// </summary>
58+
/// <param name="routePrefix">The route prefix associated with the configuration, if any.</param>
59+
/// <returns>A <see cref="IEnumerable{T}">sequence</see> of <see cref="IEdmModel">EDM models</see>.</returns>
60+
public virtual IEnumerable<IEdmModel> GetEdmModels( string? routePrefix )
5261
{
5362
var apiVersions = GetApiVersions();
5463
var configurations = GetMergedConfigurations();
5564
var models = new List<IEdmModel>();
5665

57-
BuildModelPerApiVersion( apiVersions, configurations, models );
66+
BuildModelPerApiVersion( apiVersions, configurations, models, routePrefix );
5867

5968
return models;
6069
}
@@ -76,7 +85,11 @@ IList<IModelConfiguration> GetMergedConfigurations()
7685
return configurations;
7786
}
7887

79-
void BuildModelPerApiVersion( IReadOnlyList<ApiVersion> apiVersions, IList<IModelConfiguration> configurations, ICollection<IEdmModel> models )
88+
void BuildModelPerApiVersion(
89+
IReadOnlyList<ApiVersion> apiVersions,
90+
IList<IModelConfiguration> configurations,
91+
ICollection<IEdmModel> models,
92+
string? routePrefix )
8093
{
8194
for ( var i = 0; i < apiVersions.Count; i++ )
8295
{
@@ -85,10 +98,19 @@ void BuildModelPerApiVersion( IReadOnlyList<ApiVersion> apiVersions, IList<IMode
8598

8699
for ( var j = 0; j < configurations.Count; j++ )
87100
{
88-
configurations[j].Apply( builder, apiVersion );
101+
configurations[j].Apply( builder, apiVersion, routePrefix );
89102
}
90103

91104
var model = builder.GetEdmModel();
105+
var container = model.EntityContainer;
106+
var empty = !container.EntitySets().Any() &&
107+
!container.Singletons().Any() &&
108+
!container.OperationImports().Any();
109+
110+
if ( empty )
111+
{
112+
continue;
113+
}
92114

93115
model.SetAnnotationValue( model, new ApiVersionAnnotation( apiVersion ) );
94116
OnModelCreated?.Invoke( builder, model );

src/Common.OData/AspNet.OData/Routing/ODataConventionConfigurationContext.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,20 @@ public partial class ODataConventionConfigurationContext
4242
[CLSCompliant( false )]
4343
#endif
4444
public IList<IODataRoutingConvention> RoutingConventions { get; }
45+
46+
/// <summary>
47+
/// Gets the associate service provider.
48+
/// </summary>
49+
/// <value>The associated <see cref="IServiceProvider">service provider</see>.</value>
50+
public IServiceProvider ServiceProvider { get; }
51+
52+
sealed class No : IServiceProvider
53+
{
54+
No() { }
55+
56+
internal static IServiceProvider ServiceProvider { get; } = new No();
57+
58+
public object GetService( Type serviceType ) => default!;
59+
}
4560
}
4661
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace Microsoft.AspNet.OData.Routing
2+
{
3+
using Microsoft.AspNet.OData.Routing.Template;
4+
using Microsoft.OData;
5+
using System;
6+
7+
static class ODataPathTemplateHandlerExtensions
8+
{
9+
internal static ODataPathTemplate? SafeParseTemplate(
10+
this IODataPathTemplateHandler handler,
11+
string pathTemplate,
12+
IServiceProvider serviceProvider )
13+
{
14+
try
15+
{
16+
return handler.ParseTemplate( pathTemplate, serviceProvider );
17+
}
18+
catch ( ODataException )
19+
{
20+
// this 'should' mean the controller does not map to the current edm model. there's no way to know this without
21+
// forcing a developer to explicitly map it. while it could be a mistake, simply yield null. this results in the
22+
// template being skipped and will ultimately result in a 4xx if requested, which is acceptable.
23+
return default;
24+
}
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)