diff --git a/build/file-version.targets b/build/file-version.targets
index 4f6f27a3..3c8bc9b1 100644
--- a/build/file-version.targets
+++ b/build/file-version.targets
@@ -7,7 +7,7 @@
$(AssemblyVersion.Split(`.`)[0]).$(AssemblyVersion.Split(`.`)[1])
$([System.DateTime]::Now.IsDaylightSavingTime())
$([System.DateTime]::Today.Subtract($([System.DateTime]::Parse("1/1/2000"))).ToString("%d"))
- $([System.Convert]::ToInt32($([MSBuild]::Divide($([System.DateTime]::Now.TimeOfDay.Subtract($([System.TimeSpan]::FromHours(1.0))).TotalSeconds),2))))
+ $([System.Convert]::ToInt32($([MSBuild]::Divide($([System.DateTime]::Now.Subtract($([System.TimeSpan]::FromHours(1.0))).TimeOfDay.TotalSeconds),2))))
$([System.Convert]::ToInt32($([MSBuild]::Divide($([System.DateTime]::Now.TimeOfDay.TotalSeconds),2))))
$(MajorAndMinorVersion).$(FileBuildNumber).$(FileBuildRevision)
diff --git a/samples/webapi/AdvancedODataWebApiSample/Startup.cs b/samples/webapi/AdvancedODataWebApiSample/Startup.cs
index 12b0f1b2..5237f5c8 100644
--- a/samples/webapi/AdvancedODataWebApiSample/Startup.cs
+++ b/samples/webapi/AdvancedODataWebApiSample/Startup.cs
@@ -2,12 +2,14 @@
namespace Microsoft.Examples
{
- using Configuration;
using global::Owin;
+ using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Batch;
using Microsoft.AspNet.OData.Builder;
using Microsoft.AspNet.OData.Routing;
+ using Microsoft.Examples.Configuration;
using Microsoft.OData;
+ using Microsoft.OData.UriParser;
using Microsoft.Web.Http.Versioning;
using System.Web.Http;
using static Microsoft.OData.ODataUrlKeyDelimiter;
@@ -33,7 +35,6 @@ public void Configuration( IAppBuilder appBuilder )
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
- ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase(),
ModelConfigurations =
{
new PersonModelConfiguration(),
@@ -43,14 +44,15 @@ public void Configuration( IAppBuilder appBuilder )
var models = modelBuilder.GetEdmModels();
var batchHandler = new DefaultODataBatchHandler( httpServer );
- configuration.MapVersionedODataRoutes( "odata", "api", models, OnConfigureContainer, batchHandler );
+ configuration.MapVersionedODataRoutes( "odata", "api", models, ConfigureContainer, batchHandler );
configuration.Routes.MapHttpRoute( "orders", "api/{controller}/{id}", new { id = Optional } );
appBuilder.UseWebApi( httpServer );
}
- static void OnConfigureContainer( IContainerBuilder builder )
+ static void ConfigureContainer( IContainerBuilder builder )
{
- builder.AddService( Singleton, typeof( IODataPathHandler ), sp => new DefaultODataPathHandler() { UrlKeyDelimiter = Parentheses } );
+ builder.AddService( Singleton, sp => new DefaultODataPathHandler() { UrlKeyDelimiter = Parentheses } );
+ builder.AddService( Singleton, sp => new UnqualifiedCallAndEnumPrefixFreeResolver() { EnableCaseInsensitive = true } );
}
}
}
\ No newline at end of file
diff --git a/samples/webapi/BasicODataWebApiSample/Startup.cs b/samples/webapi/BasicODataWebApiSample/Startup.cs
index bbc058c8..a6fdcde5 100644
--- a/samples/webapi/BasicODataWebApiSample/Startup.cs
+++ b/samples/webapi/BasicODataWebApiSample/Startup.cs
@@ -2,11 +2,17 @@
namespace Microsoft.Examples
{
- using Configuration;
using global::Owin;
+ using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Batch;
using Microsoft.AspNet.OData.Builder;
+ using Microsoft.AspNet.OData.Routing;
+ using Microsoft.Examples.Configuration;
+ using Microsoft.OData;
+ using Microsoft.OData.UriParser;
using System.Web.Http;
+ using static Microsoft.OData.ODataUrlKeyDelimiter;
+ using static Microsoft.OData.ServiceLifetime;
public class Startup
{
@@ -20,7 +26,6 @@ public void Configuration( IAppBuilder appBuilder )
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
- ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase(),
ModelConfigurations =
{
new PersonModelConfiguration(),
@@ -30,9 +35,15 @@ public void Configuration( IAppBuilder appBuilder )
var models = modelBuilder.GetEdmModels();
var batchHandler = new DefaultODataBatchHandler( httpServer );
- configuration.MapVersionedODataRoutes( "odata", "api", models, batchHandler );
- configuration.MapVersionedODataRoutes( "odata-bypath", "v{apiVersion}", models );
+ configuration.MapVersionedODataRoutes( "odata", "api", models, ConfigureContainer, batchHandler );
+ configuration.MapVersionedODataRoutes( "odata-bypath", "v{apiVersion}", models, ConfigureContainer );
appBuilder.UseWebApi( httpServer );
}
+
+ static void ConfigureContainer( IContainerBuilder builder )
+ {
+ builder.AddService( Singleton, sp => new DefaultODataPathHandler() { UrlKeyDelimiter = Parentheses } );
+ builder.AddService( Singleton, sp => new UnqualifiedCallAndEnumPrefixFreeResolver() { EnableCaseInsensitive = true } );
+ }
}
}
\ No newline at end of file
diff --git a/samples/webapi/ConventionsODataWebApiSample/Startup.cs b/samples/webapi/ConventionsODataWebApiSample/Startup.cs
index cfa6f343..c228297f 100644
--- a/samples/webapi/ConventionsODataWebApiSample/Startup.cs
+++ b/samples/webapi/ConventionsODataWebApiSample/Startup.cs
@@ -2,13 +2,19 @@
namespace Microsoft.Examples
{
- using Configuration;
- using Controllers;
using global::Owin;
+ using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Batch;
using Microsoft.AspNet.OData.Builder;
+ using Microsoft.AspNet.OData.Routing;
+ using Microsoft.Examples.Configuration;
+ using Microsoft.Examples.Controllers;
+ using Microsoft.OData;
+ using Microsoft.OData.UriParser;
using Microsoft.Web.Http.Versioning.Conventions;
using System.Web.Http;
+ using static Microsoft.OData.ODataUrlKeyDelimiter;
+ using static Microsoft.OData.ServiceLifetime;
public class Startup
{
@@ -38,7 +44,6 @@ public void Configuration( IAppBuilder appBuilder )
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
- ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase(),
ModelConfigurations =
{
new PersonModelConfiguration(),
@@ -48,9 +53,15 @@ public void Configuration( IAppBuilder appBuilder )
var models = modelBuilder.GetEdmModels();
var batchHandler = new DefaultODataBatchHandler( httpServer );
- configuration.MapVersionedODataRoutes( "odata", "api", models, batchHandler );
- configuration.MapVersionedODataRoutes( "odata-bypath", "v{apiVersion}", models );
+ configuration.MapVersionedODataRoutes( "odata", "api", models, ConfigureContainer, batchHandler );
+ configuration.MapVersionedODataRoutes( "odata-bypath", "v{apiVersion}", models, ConfigureContainer );
appBuilder.UseWebApi( httpServer );
}
+
+ static void ConfigureContainer( IContainerBuilder builder )
+ {
+ builder.AddService( Singleton, sp => new DefaultODataPathHandler() { UrlKeyDelimiter = Parentheses } );
+ builder.AddService( Singleton, sp => new UnqualifiedCallAndEnumPrefixFreeResolver() { EnableCaseInsensitive = true } );
+ }
}
}
\ No newline at end of file
diff --git a/samples/webapi/SwaggerODataWebApiSample/Configuration/OrderModelConfiguration.cs b/samples/webapi/SwaggerODataWebApiSample/Configuration/OrderModelConfiguration.cs
index 6002cb5e..c6e35b88 100644
--- a/samples/webapi/SwaggerODataWebApiSample/Configuration/OrderModelConfiguration.cs
+++ b/samples/webapi/SwaggerODataWebApiSample/Configuration/OrderModelConfiguration.cs
@@ -30,12 +30,12 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion )
if ( apiVersion >= ApiVersions.V1 )
{
- // order.Collection.Function( "MostExpensive" ).ReturnsFromEntitySet( "Orders" );
+ order.Collection.Function( "MostExpensive" ).ReturnsFromEntitySet( "Orders" );
}
if ( apiVersion >= ApiVersions.V2 )
{
- //order.Action( "Rate" ).Parameter( "rating" );
+ order.Action( "Rate" ).Parameter( "rating" );
}
}
}
diff --git a/samples/webapi/SwaggerODataWebApiSample/Startup.cs b/samples/webapi/SwaggerODataWebApiSample/Startup.cs
index 5eaa3ef0..f64a7a18 100644
--- a/samples/webapi/SwaggerODataWebApiSample/Startup.cs
+++ b/samples/webapi/SwaggerODataWebApiSample/Startup.cs
@@ -3,15 +3,21 @@
namespace Microsoft.Examples
{
using global::Owin;
+ using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Builder;
using Microsoft.AspNet.OData.Extensions;
+ using Microsoft.AspNet.OData.Routing;
using Microsoft.Examples.Configuration;
+ using Microsoft.OData;
+ using Microsoft.OData.UriParser;
using Newtonsoft.Json.Serialization;
using Swashbuckle.Application;
using System.IO;
using System.Reflection;
using System.Web.Http;
using System.Web.Http.Description;
+ using static Microsoft.OData.ODataUrlKeyDelimiter;
+ using static Microsoft.OData.ServiceLifetime;
///
/// Represents the startup process for the application.
@@ -28,8 +34,6 @@ public void Configuration( IAppBuilder builder )
var configuration = new HttpConfiguration();
var httpServer = new HttpServer( configuration );
- configuration.SetUrlKeyDelimiter( OData.ODataUrlKeyDelimiter.Parentheses );
-
// reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
configuration.AddApiVersioning( o => o.ReportApiVersions = true );
@@ -38,7 +42,6 @@ public void Configuration( IAppBuilder builder )
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
- ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase(),
ModelConfigurations =
{
new AllConfigurations(),
@@ -51,10 +54,10 @@ public void Configuration( IAppBuilder builder )
// TODO: while you can use both, you should choose only ONE of the following; comment, uncomment, or remove as necessary
// WHEN VERSIONING BY: query string, header, or media type
- configuration.MapVersionedODataRoutes( "odata", routePrefix, models );
+ configuration.MapVersionedODataRoutes( "odata", routePrefix, models, ConfigureContainer );
// WHEN VERSIONING BY: url segment
- //configuration.MapVersionedODataRoutes( "odata-bypath", "api/v{apiVersion}", models, ConfigureODataServices );
+ //configuration.MapVersionedODataRoutes( "odata-bypath", "api/v{apiVersion}", models, ConfigureContainer );
// add the versioned IApiExplorer and capture the strongly-typed implementation (e.g. ODataApiExplorer vs IApiExplorer)
// note: the specified format code will format the version as "'v'major[.minor][-status]"
@@ -114,5 +117,11 @@ static string XmlCommentsFilePath
return Path.Combine( basePath, fileName );
}
}
+
+ static void ConfigureContainer( IContainerBuilder builder )
+ {
+ builder.AddService( Singleton, sp => new DefaultODataPathHandler() { UrlKeyDelimiter = Parentheses } );
+ builder.AddService( Singleton, sp => new UnqualifiedCallAndEnumPrefixFreeResolver() { EnableCaseInsensitive = true } );
+ }
}
}
\ No newline at end of file
diff --git a/samples/webapi/SwaggerODataWebApiSample/SwaggerODataWebApiSample.csproj b/samples/webapi/SwaggerODataWebApiSample/SwaggerODataWebApiSample.csproj
index fa53b426..868707fa 100644
--- a/samples/webapi/SwaggerODataWebApiSample/SwaggerODataWebApiSample.csproj
+++ b/samples/webapi/SwaggerODataWebApiSample/SwaggerODataWebApiSample.csproj
@@ -84,9 +84,6 @@
..\..\..\packages\Swashbuckle.Core.5.5.3\lib\net40\Swashbuckle.Core.dll
-
..\..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll
@@ -178,7 +175,7 @@
True
1874
/
- http://localhost:1874/
+ http://localhost:1874/swagger
False
False
diff --git a/src/Common.ApiExplorer/ApiExplorerOptions.cs b/src/Common.ApiExplorer/ApiExplorerOptions.cs
index 0790d11a..dc0ea6d8 100644
--- a/src/Common.ApiExplorer/ApiExplorerOptions.cs
+++ b/src/Common.ApiExplorer/ApiExplorerOptions.cs
@@ -53,5 +53,22 @@ public partial class ApiExplorerOptions
/// The default description for API version parameters. The default value
/// is "The requested API version".
public string DefaultApiVersionParameterDescription { get; set; } = LocalSR.DefaultApiVersionParamDesc;
+
+ ///
+ /// Gets or sets a value indicating whether API version parameters are added when an API is version-neutral.
+ ///
+ /// True if API version parameters should be included when exploring a version-neutral API; otherwise, false.
+ /// The default value is false.
+ ///
+ ///
+ /// A version-neutral API can accept any API version, including none at all. Setting this property to true
+ /// will enable exploring parameter descriptors for an API version that can be used to generate user input, which
+ /// may be useful for a version-neutral API that its own per-API version logic.
+ ///
+ ///
+ /// An API version defined using the URLsegment method is unaffected by this setting because path-based route
+ /// parameters are always required.
+ ///
+ public bool AddApiVersionParametersWhenVersionNeutral { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs b/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs
index 152b6eb7..e53dc634 100644
--- a/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs
+++ b/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs
@@ -11,8 +11,8 @@
struct ClassProperty
{
+ readonly Type type;
internal readonly string Name;
- internal readonly Type Type;
internal ClassProperty( PropertyInfo clrProperty, Type propertyType )
{
@@ -20,7 +20,7 @@ internal ClassProperty( PropertyInfo clrProperty, Type propertyType )
Contract.Requires( propertyType != null );
Name = clrProperty.Name;
- Type = propertyType;
+ type = propertyType;
Attributes = AttributesFromProperty( clrProperty );
}
@@ -30,12 +30,36 @@ internal ClassProperty( IEnumerable assemblies, IEdmOperationParameter
Contract.Requires( parameter != null );
Name = parameter.Name;
- Type = parameter.Type.Definition.GetClrType( assemblies );
+ type = parameter.Type.Definition.GetClrType( assemblies );
Attributes = AttributesFromOperationParameter( parameter );
}
internal IEnumerable Attributes { get; }
+ public override int GetHashCode() => ( Name.GetHashCode() * 397 ) ^ type.GetHashCode();
+
+ public Type GetType( Type declaringType )
+ {
+ Contract.Requires( declaringType != null );
+ Contract.Ensures( Contract.Result() != null );
+
+ if ( type == DeclaringType.Value )
+ {
+ return declaringType;
+ }
+ else if ( type.IsGenericType )
+ {
+ var typeArgs = type.GetGenericArguments();
+
+ if ( typeArgs.Length == 1 && typeArgs[0] == DeclaringType.Value )
+ {
+ return type.GetGenericTypeDefinition().MakeGenericType( declaringType );
+ }
+ }
+
+ return type;
+ }
+
static IEnumerable AttributesFromProperty( PropertyInfo clrProperty )
{
Contract.Requires( clrProperty != null );
@@ -93,7 +117,5 @@ static IEnumerable AttributesFromOperationParameter( IEd
yield return new CustomAttributeBuilder( ctor, args );
}
-
- public override int GetHashCode() => ( Name.GetHashCode() * 397 ) ^ Type.GetHashCode();
}
}
\ No newline at end of file
diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/DeclaringType.cs b/src/Common.OData.ApiExplorer/AspNet.OData/DeclaringType.cs
new file mode 100644
index 00000000..db66520a
--- /dev/null
+++ b/src/Common.OData.ApiExplorer/AspNet.OData/DeclaringType.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.AspNet.OData
+{
+ using System;
+
+ struct DeclaringType
+ {
+ internal static Type Value = typeof( DeclaringType );
+ }
+}
\ No newline at end of file
diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs
index a19e813f..53efac86 100644
--- a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs
+++ b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs
@@ -30,6 +30,7 @@ public sealed class DefaultModelTypeBuilder : IModelTypeBuilder
readonly ICollection assemblies;
readonly ConcurrentDictionary modules = new ConcurrentDictionary();
readonly ConcurrentDictionary generatedTypes = new ConcurrentDictionary();
+ readonly Dictionary visitedEdmTypes = new Dictionary();
///
/// Initializes a new instance of the class.
@@ -49,6 +50,13 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType,
Arg.NotNull( apiVersion, nameof( apiVersion ) );
Contract.Ensures( Contract.Result() != null );
+ var typeKey = new EdmTypeKey( structuredType, apiVersion );
+
+ if ( visitedEdmTypes.TryGetValue( typeKey, out var generatedType ) )
+ {
+ return generatedType;
+ }
+
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance;
var properties = new List();
@@ -63,33 +71,19 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType,
continue;
}
- var propertyType = property.PropertyType;
var structuredTypeRef = structuralProperty.Type;
+ var propertyType = property.PropertyType;
if ( structuredTypeRef.IsCollection() )
{
- var collectionType = structuredTypeRef.AsCollection();
- var elementType = collectionType.ElementType();
-
- if ( elementType.IsStructured() )
- {
- assemblies.Add( clrType.Assembly );
-
- var itemType = elementType.Definition.GetClrType( assemblies );
- var newItemType = NewStructuredType( elementType.ToStructuredType(), itemType, apiVersion );
-
- if ( !itemType.Equals( newItemType ) )
- {
- propertyType = IEnumerableOfT.MakeGenericType( newItemType );
- }
- }
+ propertyType = NewStructuredTypeOrSelf( typeKey, structuredTypeRef.AsCollection(), propertyType, apiVersion );
}
else if ( structuredTypeRef.IsStructured() )
{
- propertyType = NewStructuredType( structuredTypeRef.ToStructuredType(), property.PropertyType, apiVersion );
+ propertyType = NewStructuredTypeOrSelf( typeKey, structuredTypeRef.ToStructuredType(), propertyType, apiVersion );
}
- clrTypeMatchesEdmType &= property.PropertyType.Equals( propertyType );
+ clrTypeMatchesEdmType &= propertyType.IsDeclaringType() || property.PropertyType.Equals( propertyType );
properties.Add( new ClassProperty( property, propertyType ) );
}
@@ -100,7 +94,10 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType,
var signature = new ClassSignature( clrType.FullName, properties, apiVersion );
- return generatedTypes.GetOrAdd( signature, CreateFromSignature );
+ generatedType = generatedTypes.GetOrAdd( signature, CreateFromSignature );
+ visitedEdmTypes.Add( typeKey, generatedType );
+
+ return generatedType;
}
///
@@ -128,10 +125,12 @@ TypeInfo CreateFromSignature( ClassSignature @class )
foreach ( var property in @class.Properties )
{
- var field = typeBuilder.DefineField( "_" + property.Name, property.Type, FieldAttributes.Private );
- var propertyBuilder = typeBuilder.DefineProperty( property.Name, PropertyAttributes.HasDefault, property.Type, null );
- var getter = typeBuilder.DefineMethod( "get_" + property.Name, PropertyMethodAttributes, property.Type, Type.EmptyTypes );
- var setter = typeBuilder.DefineMethod( "set_" + property.Name, PropertyMethodAttributes, null, new[] { property.Type } );
+ var type = property.GetType( typeBuilder );
+ var name = property.Name;
+ var field = typeBuilder.DefineField( "_" + name, type, FieldAttributes.Private );
+ var propertyBuilder = typeBuilder.DefineProperty( name, PropertyAttributes.HasDefault, type, null );
+ var getter = typeBuilder.DefineMethod( "get_" + name, PropertyMethodAttributes, type, Type.EmptyTypes );
+ var setter = typeBuilder.DefineMethod( "set_" + name, PropertyMethodAttributes, null, new[] { type } );
var il = getter.GetILGenerator();
il.Emit( OpCodes.Ldarg_0 );
@@ -156,6 +155,55 @@ TypeInfo CreateFromSignature( ClassSignature @class )
return typeBuilder.CreateTypeInfo();
}
+ Type NewStructuredTypeOrSelf( EdmTypeKey declaringTypeKey, IEdmCollectionTypeReference collectionType, Type clrType, ApiVersion apiVersion )
+ {
+ Contract.Requires( collectionType != null );
+ Contract.Requires( clrType != null );
+ Contract.Requires( apiVersion != null );
+ Contract.Ensures( Contract.Result() != null );
+
+ var elementType = collectionType.ElementType();
+
+ if ( !elementType.IsStructured() )
+ {
+ return clrType;
+ }
+
+ var structuredType = elementType.ToStructuredType();
+
+ if ( declaringTypeKey == new EdmTypeKey( structuredType, apiVersion ) )
+ {
+ return IEnumerableOfT.MakeGenericType( DeclaringType.Value );
+ }
+
+ assemblies.Add( clrType.Assembly );
+
+ var itemType = elementType.Definition.GetClrType( assemblies );
+ var newItemType = NewStructuredType( structuredType, itemType, apiVersion );
+
+ if ( !itemType.Equals( newItemType ) )
+ {
+ clrType = IEnumerableOfT.MakeGenericType( newItemType );
+ }
+
+ return clrType;
+ }
+
+ Type NewStructuredTypeOrSelf( EdmTypeKey declaringTypeKey, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion )
+ {
+ Contract.Requires( structuredType != null );
+ Contract.Requires( clrType != null );
+ Contract.Requires( apiVersion != null );
+ Contract.Ensures( Contract.Result() != null );
+
+ if ( declaringTypeKey == new EdmTypeKey( structuredType, apiVersion ) )
+ {
+ return DeclaringType.Value;
+ }
+
+ return NewStructuredType( structuredType, clrType, apiVersion );
+ }
+
static ModuleBuilder CreateModuleForApiVersion( ApiVersion apiVersion )
{
var name = new AssemblyName( $"T{NewGuid().ToString( "n", InvariantCulture )}.DynamicModels" );
diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/EdmTypeKey.cs b/src/Common.OData.ApiExplorer/AspNet.OData/EdmTypeKey.cs
new file mode 100644
index 00000000..53afce4a
--- /dev/null
+++ b/src/Common.OData.ApiExplorer/AspNet.OData/EdmTypeKey.cs
@@ -0,0 +1,51 @@
+namespace Microsoft.AspNet.OData
+{
+#if !WEBAPI
+ using Microsoft.AspNetCore.Mvc;
+#endif
+ using Microsoft.OData.Edm;
+#if WEBAPI
+ using Microsoft.Web.Http;
+#endif
+ using System;
+ using System.Diagnostics.Contracts;
+
+ struct EdmTypeKey : IEquatable
+ {
+ readonly int hashCode;
+
+ internal EdmTypeKey( IEdmStructuredType type, ApiVersion apiVersion )
+ {
+ Contract.Requires( type != null );
+ Contract.Requires( apiVersion != null );
+
+ hashCode = ComputeHash( type.FullTypeName(), apiVersion );
+ }
+
+ internal EdmTypeKey( IEdmTypeReference type, ApiVersion apiVersion )
+ {
+ Contract.Requires( type != null );
+ Contract.Requires( apiVersion != null );
+
+ hashCode = ComputeHash( type.FullName(), apiVersion );
+ }
+
+ public static bool operator ==( EdmTypeKey obj, EdmTypeKey other ) => obj.Equals( other );
+
+ public static bool operator !=( EdmTypeKey obj, EdmTypeKey other ) => !obj.Equals( other );
+
+ public override int GetHashCode() => hashCode;
+
+ public override bool Equals( object obj ) => obj is EdmTypeKey other && Equals( other );
+
+ public bool Equals( EdmTypeKey other ) => hashCode == other.hashCode;
+
+ static int ComputeHash( string fullName, ApiVersion apiVersion )
+ {
+ Contract.Requires( !string.IsNullOrEmpty( fullName ) );
+ Contract.Requires( apiVersion != null );
+
+ return ( fullName.GetHashCode() * 397 ) ^ apiVersion.GetHashCode();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
index 0e911e62..a502eff7 100644
--- a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
+++ b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
@@ -80,7 +80,7 @@ void AppendRoutePrefix( IList segments )
return;
}
- prefix = UpdateRoutePrefixAndRemoveApiVersionParameterIfNecessary( prefix );
+ prefix = RemoveRouteConstraints( prefix );
segments.Add( prefix );
}
diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs b/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs
index 9c4c6bc8..94747563 100644
--- a/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs
+++ b/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs
@@ -25,7 +25,6 @@ public static partial class TypeExtensions
static readonly Type ActionResultType = typeof( IActionResult );
static readonly Type HttpResponseType = typeof( HttpResponseMessage );
static readonly Type IEnumerableOfT = typeof( IEnumerable<> );
- static readonly Type DeltaOfT = typeof( Delta<> );
static readonly Type ODataValueOfT = typeof( ODataValue<> );
///
@@ -56,7 +55,13 @@ public static Type SubstituteIfNecessary( this Type type, TypeSubstitutionContex
}
var newType = context.ModelTypeBuilder.NewStructuredType( structuredType, innerType, apiVersion );
- return innerType.Equals( newType ) ? type : CloseGeneric( openTypes, newType );
+
+ if ( innerType.Equals( newType ) )
+ {
+ return type.ExtractInnerType() ? innerType : type;
+ }
+
+ return CloseGeneric( openTypes, newType );
}
if ( CanBeSubstituted( type ) )
@@ -77,6 +82,25 @@ internal static void Deconstruct( this Tuple tuple, out T1 item1
item2 = tuple.Item2;
}
+ internal static bool IsDeclaringType( this Type type )
+ {
+ Contract.Requires( type != null );
+
+ if ( type == DeclaringType.Value )
+ {
+ return true;
+ }
+
+ if ( !type.IsGenericType )
+ {
+ return false;
+ }
+
+ var typeArgs = type.GetGenericArguments();
+
+ return typeArgs.Length == 1 && typeArgs[0] == DeclaringType.Value;
+ }
+
static bool IsSubstitutableGeneric( Type type, Stack openTypes, out Type innerType )
{
Contract.Requires( type != null );
@@ -101,10 +125,7 @@ static bool IsSubstitutableGeneric( Type type, Stack openTypes, out Type i
var typeArg = typeArgs[0];
- if ( typeDef.Equals( IEnumerableOfT ) ||
- typeDef.Equals( DeltaOfT ) ||
- typeDef.Equals( ODataValueOfT ) ||
- typeDef.FullName.Equals( "Microsoft.AspNetCore.Mvc.ActionResult`1", Ordinal ) )
+ if ( typeDef.Equals( IEnumerableOfT ) || typeDef.IsDelta() || typeDef.Equals( ODataValueOfT ) || typeDef.IsActionResult() )
{
innerType = typeArg;
}
@@ -140,7 +161,14 @@ static Type CloseGeneric( Stack openTypes, Type innerType )
Contract.Requires( openTypes.Count > 0 );
Contract.Requires( innerType != null );
- var type = openTypes.Pop().MakeGenericType( innerType );
+ var type = openTypes.Pop();
+
+ if ( type.ExtractInnerType() )
+ {
+ return innerType;
+ }
+
+ type = type.MakeGenericType( innerType );
while ( openTypes.Count > 0 )
{
@@ -158,7 +186,8 @@ static bool CanBeSubstituted( Type type )
!type.IsValueType &&
!type.Equals( VoidType ) &&
!type.Equals( ActionResultType ) &&
- !type.Equals( HttpResponseType );
+ !type.Equals( HttpResponseType ) &&
+ !type.IsODataActionParameters();
}
static bool IsEnumerable( this Type type, out Type itemType )
@@ -190,5 +219,11 @@ static bool IsEnumerable( this Type type, out Type itemType )
return false;
}
+
+ static bool IsActionResult( this Type type ) =>
+ type.IsGenericType &&
+ type.GetGenericTypeDefinition().FullName.Equals( "Microsoft.AspNetCore.Mvc.ActionResult`1", Ordinal );
+
+ static bool ExtractInnerType( this Type type ) => type.IsDelta() || type.IsActionResult();
}
}
\ No newline at end of file
diff --git a/src/Common.OData.ApiExplorer/Common.OData.ApiExplorer.projitems b/src/Common.OData.ApiExplorer/Common.OData.ApiExplorer.projitems
index 105106ec..b2e7ea36 100644
--- a/src/Common.OData.ApiExplorer/Common.OData.ApiExplorer.projitems
+++ b/src/Common.OData.ApiExplorer/Common.OData.ApiExplorer.projitems
@@ -11,6 +11,8 @@
+
+
diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
index 56234168..b2266a45 100644
--- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
+++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
@@ -1,37 +1,9 @@
namespace Microsoft.AspNet.OData.Routing
{
- using Microsoft.Web.Http.Description;
- using System.Diagnostics.Contracts;
- using System.Linq;
- using static System.Globalization.CultureInfo;
+ using System;
partial class ODataRouteBuilder
{
- string UpdateRoutePrefixAndRemoveApiVersionParameterIfNecessary( string routePrefix )
- {
- Contract.Requires( !string.IsNullOrEmpty( routePrefix ) );
- Contract.Ensures( !string.IsNullOrEmpty( Contract.Result() ) );
-
- var parameters = Context.ParameterDescriptions;
- var parameter = parameters.FirstOrDefault( p => p.ParameterDescriptor is ApiVersionParameterDescriptor pd && pd.FromPath );
-
- if ( parameter == null )
- {
- return routePrefix;
- }
-
- var apiVersionFormat = Context.Options.SubstitutionFormat;
- var token = string.Concat( '{', parameter.Name, '}' );
- var value = Context.ApiVersion.ToString( apiVersionFormat, InvariantCulture );
- var newRoutePrefix = routePrefix.Replace( token, value );
-
- if ( routePrefix == newRoutePrefix )
- {
- return routePrefix;
- }
-
- parameters.Remove( parameter );
- return newRoutePrefix;
- }
+ static string RemoveRouteConstraints( string routePrefix ) => routePrefix;
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs
index e2814870..e801ade9 100644
--- a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs
+++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs
@@ -1,6 +1,7 @@
namespace Microsoft.Web.Http.Description
{
using Microsoft.Web.Http.Versioning;
+ using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
@@ -18,6 +19,7 @@
public class ApiVersionParameterDescriptionContext : IApiVersionParameterDescriptionContext
{
readonly List parameters = new List( 1 );
+ readonly Lazy versionNeutral;
bool optional;
///
@@ -39,6 +41,7 @@ public ApiVersionParameterDescriptionContext(
ApiVersion = apiVersion;
Options = options;
optional = options.AssumeDefaultVersionWhenUnspecified && apiVersion == options.DefaultApiVersion;
+ versionNeutral = new Lazy( TestIfApiVersionNeutral );
}
///
@@ -53,6 +56,12 @@ public ApiVersionParameterDescriptionContext(
/// The associated API version.
protected ApiVersion ApiVersion { get; }
+ ///
+ /// Gets a value indicating whether the current API is version-neutral.
+ ///
+ /// True if the current API is version-neutral; otherwise, false.
+ protected bool IsApiVersionNeutral => versionNeutral.Value;
+
///
/// Gets the options associated with the API explorer.
///
@@ -78,20 +87,29 @@ bool HasPathParameter
/// One of the values.
public virtual void AddParameter( string name, ApiVersionParameterLocation location )
{
+ var add = default( Action );
+
switch ( location )
{
case Query:
- AddQueryString( name );
+ add = AddQueryString;
break;
case Header:
- AddHeader( name );
+ add = AddHeader;
break;
case Path:
UpdateUrlSegment();
- break;
+ return;
case MediaTypeParameter:
- AddMediaTypeParameter( name );
+ add = AddMediaTypeParameter;
break;
+ default:
+ return;
+ }
+
+ if ( Options.AddApiVersionParametersWhenVersionNeutral || !IsApiVersionNeutral )
+ {
+ add( name );
}
}
@@ -188,6 +206,14 @@ ApiParameterDescription NewApiVersionParameter( string name, ApiParameterSource
return parameter;
}
+ bool TestIfApiVersionNeutral()
+ {
+ var action = ApiDescription.ActionDescriptor;
+ var model = action.GetApiVersionModel();
+
+ return model.IsApiVersionNeutral || ( model.DeclaredApiVersions.Count == 0 && action.ControllerDescriptor.IsApiVersionNeutral() );
+ }
+
void RemoveAllParametersExcept( ApiParameterDescription parameter )
{
// note: in a scenario where multiple api version parameters are allowed, we can remove all other parameters because
diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs
index 6e2dc63b..f6962d75 100644
--- a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs
+++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs
@@ -271,7 +271,10 @@ protected virtual ApiDescriptionGroupCollection InitializeApiDescriptions()
if ( Options.SubstituteApiVersionInUrl )
{
- UpdateRelativePathAndRemoveApiVersionParameterIfNecessary( apiDescriptionGroup, Options.SubstitutionFormat );
+ foreach ( var apiDescription in apiDescriptionGroup.ApiDescriptions )
+ {
+ apiDescription.TryUpdateRelativePathAndRemoveApiVersionParameter( Options );
+ }
}
}
@@ -394,32 +397,6 @@ protected virtual bool TryExpandUriParameters( IHttpRoute route, IParsedRoute pa
return true;
}
- static void UpdateRelativePathAndRemoveApiVersionParameterIfNecessary( ApiDescriptionGroup apiDescriptionGroup, string apiVersionFormat )
- {
- Contract.Requires( apiDescriptionGroup != null );
-
- foreach ( var apiDescription in apiDescriptionGroup.ApiDescriptions )
- {
- var parameter = apiDescription.ParameterDescriptions.FirstOrDefault( p => p.ParameterDescriptor is ApiVersionParameterDescriptor pd && pd.FromPath );
-
- if ( parameter == null )
- {
- continue;
- }
-
- var relativePath = apiDescription.RelativePath;
- var token = '{' + parameter.ParameterDescriptor.ParameterName + '}';
- var value = apiDescription.ApiVersion.ToString( apiVersionFormat, InvariantCulture );
- var newRelativePath = relativePath.Replace( token, value );
-
- if ( relativePath != newRelativePath )
- {
- apiDescription.RelativePath = newRelativePath;
- apiDescription.ParameterDescriptions.Remove( parameter );
- }
- }
- }
-
static IEnumerable FlattenRoutes( IEnumerable routes )
{
Contract.Requires( routes != null );
@@ -691,14 +668,6 @@ protected virtual void PopulateApiVersionParameters( ApiDescription apiDescripti
Arg.NotNull( apiDescription, nameof( apiDescription ) );
Arg.NotNull( apiVersion, nameof( apiVersion ) );
- var action = apiDescription.ActionDescriptor;
- var model = action.GetApiVersionModel();
-
- if ( model.IsApiVersionNeutral || ( model.DeclaredApiVersions.Count == 0 && action.ControllerDescriptor.IsApiVersionNeutral() ) )
- {
- return;
- }
-
var parameterSource = Options.ApiVersionParameterSource;
var context = new ApiVersionParameterDescriptionContext( apiDescription, apiVersion, Options );
diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs
index f1b012d9..6c2adcb5 100644
--- a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs
+++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs
@@ -3,6 +3,8 @@
using Microsoft;
using Microsoft.Web.Http.Description;
using System.Diagnostics.Contracts;
+ using System.Linq;
+ using static System.Globalization.CultureInfo;
///
/// Provides extension methods for the class.
@@ -49,6 +51,44 @@ public static string GetUniqueID( this ApiDescription apiDescription )
return apiDescription.ID;
}
+ ///
+ /// Attempts to update the relate path of the specified API description and remove the corresponding parameter according to the specified options.
+ ///
+ /// The API description to attempt to update.
+ /// The current API Explorer options.
+ /// True if the API description was updated; otherwise, false.
+ public static bool TryUpdateRelativePathAndRemoveApiVersionParameter( this ApiDescription apiDescription, ApiExplorerOptions options )
+ {
+ Arg.NotNull( apiDescription, nameof( apiDescription ) );
+ Arg.NotNull( options, nameof( options ) );
+
+ if ( !options.SubstituteApiVersionInUrl || !( apiDescription is VersionedApiDescription versionedApiDescription ) )
+ {
+ return false;
+ }
+
+ var parameter = versionedApiDescription.ParameterDescriptions.FirstOrDefault( p => p.ParameterDescriptor is ApiVersionParameterDescriptor pd && pd.FromPath );
+
+ if ( parameter == null )
+ {
+ return false;
+ }
+
+ var relativePath = apiDescription.RelativePath;
+ var token = '{' + parameter.ParameterDescriptor.ParameterName + '}';
+ var value = versionedApiDescription.ApiVersion.ToString( options.SubstitutionFormat, InvariantCulture );
+ var newRelativePath = relativePath.Replace( token, value );
+
+ if ( relativePath == newRelativePath )
+ {
+ return false;
+ }
+
+ apiDescription.RelativePath = newRelativePath;
+ apiDescription.ParameterDescriptions.Remove( parameter );
+ return true;
+ }
+
///
/// Gets a property of the specified type from the API description.
///
@@ -59,12 +99,12 @@ public static T GetProperty( this VersionedApiDescription apiDescription )
{
Arg.NotNull( apiDescription, nameof( apiDescription ) );
- if ( apiDescription.Properties.TryGetValue( typeof( T ), out object value ) )
+ if ( apiDescription.Properties.TryGetValue( typeof( T ), out var value ) )
{
return (T) value;
}
- return default( T );
+ return default;
}
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiDescriptionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiDescriptionExtensions.cs
index 3cd5b468..76c13ac7 100644
--- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiDescriptionExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiDescriptionExtensions.cs
@@ -3,7 +3,11 @@
using System;
using System.ComponentModel;
using System.Diagnostics.Contracts;
+ using System.Linq;
+ using static Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource;
using static System.ComponentModel.EditorBrowsableState;
+ using static System.Globalization.CultureInfo;
+ using static System.Linq.Enumerable;
///
/// Provides extension methods for the class.
@@ -26,6 +30,44 @@ public static class ApiDescriptionExtensions
[EditorBrowsable( Never )]
public static void SetApiVersion( this ApiDescription apiDescription, ApiVersion apiVersion ) => apiDescription.SetProperty( apiVersion );
+ ///
+ /// Attempts to update the relate path of the specified API description and remove the corresponding parameter according to the specified options.
+ ///
+ /// The API description to attempt to update.
+ /// The current API Explorer options.
+ /// True if the API description was updated; otherwise, false.
+ public static bool TryUpdateRelativePathAndRemoveApiVersionParameter( this ApiDescription apiDescription, ApiExplorerOptions options )
+ {
+ Arg.NotNull( apiDescription, nameof( apiDescription ) );
+ Arg.NotNull( options, nameof( options ) );
+
+ if ( !options.SubstituteApiVersionInUrl )
+ {
+ return false;
+ }
+
+ var parameter = apiDescription.ParameterDescriptions.FirstOrDefault( pd => pd.Source == Path && pd.ModelMetadata?.DataTypeName == nameof( ApiVersion ) );
+
+ if ( parameter == null )
+ {
+ return false;
+ }
+
+ var relativePath = apiDescription.RelativePath;
+ var token = '{' + parameter.Name + '}';
+ var value = apiDescription.GetApiVersion().ToString( options.SubstitutionFormat, InvariantCulture );
+ var newRelativePath = relativePath.Replace( token, value );
+
+ if ( relativePath == newRelativePath )
+ {
+ return false;
+ }
+
+ apiDescription.RelativePath = newRelativePath;
+ apiDescription.ParameterDescriptions.Remove( parameter );
+ return true;
+ }
+
///
/// Creates a shallow copy of the current API description.
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiVersionParameterDescriptionContext.cs b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiVersionParameterDescriptionContext.cs
index d5b3dfaf..128b544b 100644
--- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiVersionParameterDescriptionContext.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ApiVersionParameterDescriptionContext.cs
@@ -1,5 +1,7 @@
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
+ using Microsoft.AspNetCore.Mvc.Abstractions;
+ using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.Versioning;
@@ -17,6 +19,7 @@
public class ApiVersionParameterDescriptionContext : IApiVersionParameterDescriptionContext
{
readonly List parameters = new List( 1 );
+ readonly Lazy versionNeutral;
bool optional;
///
@@ -43,6 +46,7 @@ public ApiVersionParameterDescriptionContext(
ModelMetadata = modelMetadata;
Options = options;
optional = options.AssumeDefaultVersionWhenUnspecified && apiVersion == options.DefaultApiVersion;
+ versionNeutral = new Lazy( TestIfApiVersionNeutral );
}
///
@@ -58,6 +62,12 @@ public ApiVersionParameterDescriptionContext(
/// The associated API version.
protected ApiVersion ApiVersion { get; }
+ ///
+ /// Gets a value indicating whether the current API is version-neutral.
+ ///
+ /// True if the current API is version-neutral; otherwise, false.
+ protected bool IsApiVersionNeutral => versionNeutral.Value;
+
///
/// Gets the model metadata for API version parameters.
///
@@ -93,20 +103,29 @@ where constraints.OfType().Any()
/// One of the values.
public virtual void AddParameter( string name, ApiVersionParameterLocation location )
{
+ var add = default( Action );
+
switch ( location )
{
case Query:
- AddQueryString( name );
+ add = AddQueryString;
break;
case Header:
- AddHeader( name );
+ add = AddHeader;
break;
case Path:
UpdateUrlSegment();
- break;
+ return;
case MediaTypeParameter:
- AddMediaTypeParameter( name );
+ add = AddMediaTypeParameter;
break;
+ default:
+ return;
+ }
+
+ if ( Options.AddApiVersionParametersWhenVersionNeutral || !IsApiVersionNeutral )
+ {
+ add( name );
}
}
@@ -237,6 +256,26 @@ ApiParameterDescription NewApiVersionParameter( string name, BindingSource sourc
return parameter;
}
+ bool TestIfApiVersionNeutral()
+ {
+ var action = ApiDescription.ActionDescriptor;
+ var model = action.GetProperty();
+
+ if ( model.IsApiVersionNeutral )
+ {
+ return true;
+ }
+
+ if ( model.DeclaredApiVersions.Count > 0 )
+ {
+ return false;
+ }
+
+ model = action.GetProperty()?.GetProperty();
+
+ return model?.IsApiVersionNeutral == true;
+ }
+
void RemoveAllParametersExcept( ApiParameterDescription parameter )
{
// note: in a scenario where multiple api version parameters are allowed, we can remove all other parameters because
diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs
index ece1d95d..833b69b8 100644
--- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs
@@ -97,23 +97,6 @@ protected virtual void PopulateApiVersionParameters( ApiDescription apiDescripti
Arg.NotNull( apiDescription, nameof( apiDescription ) );
Arg.NotNull( apiVersion, nameof( apiVersion ) );
- var action = apiDescription.ActionDescriptor;
- var model = action.GetProperty();
-
- if ( model.IsApiVersionNeutral )
- {
- return;
- }
- else if ( model.DeclaredApiVersions.Count == 0 )
- {
- model = action.GetProperty()?.GetProperty();
-
- if ( model?.IsApiVersionNeutral == true )
- {
- return;
- }
- }
-
var parameterSource = Options.ApiVersionParameterSource;
var context = new ApiVersionParameterDescriptionContext( apiDescription, apiVersion, modelMetadata.Value, Options );
@@ -155,12 +138,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )
groupResult.GroupName = groupName;
groupResult.SetApiVersion( version );
PopulateApiVersionParameters( groupResult, version );
-
- if ( Options.SubstituteApiVersionInUrl )
- {
- UpdateRelativePathAndRemoveApiVersionParameterIfNecessary( groupResult, Options.SubstitutionFormat );
- }
-
+ groupResult.TryUpdateRelativePathAndRemoveApiVersionParameter( Options );
groupResults.Add( groupResult );
}
}
@@ -180,29 +158,6 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )
/// The default implementation performs no operation.
public virtual void OnProvidersExecuting( ApiDescriptionProviderContext context ) { }
- static void UpdateRelativePathAndRemoveApiVersionParameterIfNecessary( ApiDescription apiDescription, string apiVersionFormat )
- {
- Contract.Requires( apiDescription != null );
-
- var parameter = apiDescription.ParameterDescriptions.FirstOrDefault( pd => pd.Source == BindingSource.Path && pd.ModelMetadata?.DataTypeName == nameof( ApiVersion ) );
-
- if ( parameter == null )
- {
- return;
- }
-
- var relativePath = apiDescription.RelativePath;
- var token = '{' + parameter.Name + '}';
- var value = apiDescription.GetApiVersion().ToString( apiVersionFormat, InvariantCulture );
- var newRelativePath = relativePath.Replace( token, value );
-
- if ( relativePath != newRelativePath )
- {
- apiDescription.RelativePath = newRelativePath;
- apiDescription.ParameterDescriptions.Remove( parameter );
- }
- }
-
IEnumerable FlattenApiVersions( IEnumerable descriptions )
{
Contract.Requires( descriptions != null );
diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
index 6b517ccd..337b13aa 100644
--- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
+++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
@@ -1,11 +1,8 @@
namespace Microsoft.AspNet.OData.Routing
{
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.AspNetCore.Mvc.ModelBinding;
+ using Microsoft.AspNetCore.Routing.Template;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
- using System.Linq;
- using static System.Globalization.CultureInfo;
using static System.String;
partial class ODataRouteBuilder
@@ -17,31 +14,34 @@ internal string BuildPath()
return Join( "/", segments );
}
- string UpdateRoutePrefixAndRemoveApiVersionParameterIfNecessary( string routePrefix )
+ static string RemoveRouteConstraints( string routePrefix )
{
Contract.Requires( !IsNullOrEmpty( routePrefix ) );
Contract.Ensures( !IsNullOrEmpty( Contract.Result() ) );
- var parameters = Context.ParameterDescriptions;
- var parameter = parameters.FirstOrDefault( pd => pd.Source == BindingSource.Path && pd.ModelMetadata?.DataTypeName == nameof( ApiVersion ) );
+ var parsedTemplate = TemplateParser.Parse( routePrefix );
+ var segments = new List( parsedTemplate.Segments.Count );
- if ( parameter == null )
+ foreach ( var segment in parsedTemplate.Segments )
{
- return routePrefix;
- }
+ var currentSegment = Empty;
- var apiVersionFormat = Context.Options.SubstitutionFormat;
- var token = Concat( '{', parameter.Name, '}' );
- var value = Context.ApiVersion.ToString( apiVersionFormat, InvariantCulture );
- var newRoutePrefix = routePrefix.Replace( token, value );
+ foreach ( var part in segment.Parts )
+ {
+ if ( part.IsLiteral )
+ {
+ currentSegment += part.Text;
+ }
+ else if ( part.IsParameter )
+ {
+ currentSegment += Concat( "{", part.Name, "}" );
+ }
+ }
- if ( routePrefix == newRoutePrefix )
- {
- return routePrefix;
+ segments.Add( currentSegment );
}
- parameters.Remove( parameter );
- return newRoutePrefix;
+ return Join( "/", segments );
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProvider.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProvider.cs
index bf6d8966..cb2b386a 100644
--- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProvider.cs
+++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProvider.cs
@@ -11,7 +11,10 @@
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
+ using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.Versioning;
+ using Microsoft.AspNetCore.Routing;
+ using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Options;
using Microsoft.OData.Edm;
using System;
@@ -54,12 +57,14 @@ public class ODataApiDescriptionProvider : IApiDescriptionProvider
///
/// The OData route collection provider associated with the description provider.
/// The application part manager containing the configured application parts.
+ /// The inline constraint resolver used to parse route template constraints.
/// The provider used to retrieve model metadata.
/// The container of configured API explorer options.
/// A holder containing the current MVC options.
public ODataApiDescriptionProvider(
IODataRouteCollectionProvider routeCollectionProvider,
ApplicationPartManager partManager,
+ IInlineConstraintResolver inlineConstraintResolver,
IModelMetadataProvider metadataProvider,
IOptions options,
IOptions mvcOptions )
@@ -73,6 +78,7 @@ public ODataApiDescriptionProvider(
RouteCollectionProvider = routeCollectionProvider;
Assemblies = partManager.ApplicationParts.OfType().Select( p => p.Assembly ).ToArray();
ModelTypeBuilder = new DefaultModelTypeBuilder( Assemblies );
+ ConstraintResolver = inlineConstraintResolver;
MetadataProvider = metadataProvider;
this.options = options;
MvcOptions = mvcOptions.Value;
@@ -99,6 +105,12 @@ public ODataApiDescriptionProvider(
/// The provider used to retrieve model metadata.
protected IModelMetadataProvider MetadataProvider { get; }
+ ///
+ /// Gets the object used to resolve inline constraints.
+ ///
+ /// The associated inline constraint resolver.
+ protected IInlineConstraintResolver ConstraintResolver { get; }
+
///
/// Gets the options associated with the API explorer.
///
@@ -200,23 +212,6 @@ protected virtual void PopulateApiVersionParameters( ApiDescription apiDescripti
Arg.NotNull( apiDescription, nameof( apiDescription ) );
Arg.NotNull( apiVersion, nameof( apiVersion ) );
- var action = apiDescription.ActionDescriptor;
- var model = action.GetProperty();
-
- if ( model.IsApiVersionNeutral )
- {
- return;
- }
- else if ( model.DeclaredApiVersions.Count == 0 )
- {
- model = action.GetProperty()?.GetProperty();
-
- if ( model?.IsApiVersionNeutral == true )
- {
- return;
- }
- }
-
var parameterSource = Options.ApiVersionParameterSource;
var context = new ApiVersionParameterDescriptionContext( apiDescription, apiVersion, modelMetadata.Value, Options );
@@ -315,6 +310,24 @@ static IReadOnlyList GetResponseMetadataAttributes
return action.FilterDescriptors.Select( fd => fd.Filter ).OfType().ToArray();
}
+ static string BuildRelativePath( ControllerActionDescriptor action, ODataRouteBuilderContext routeContext )
+ {
+ Contract.Requires( action != null );
+ Contract.Requires( routeContext != null );
+ Contract.Ensures( !string.IsNullOrEmpty( Contract.Result() ) );
+
+ var relativePath = action.AttributeRouteInfo?.Template;
+
+ // note: if path happens to be built adhead of time, it's expected to be qualified; rebuild it as necessary
+ if ( string.IsNullOrEmpty( relativePath ) || !routeContext.Options.UseQualifiedOperationNames )
+ {
+ var builder = new ODataRouteBuilder( routeContext );
+ relativePath = builder.Build();
+ }
+
+ return relativePath;
+ }
+
IEnumerable NewODataApiDescriptions( ControllerActionDescriptor action, string groupName, ODataRouteMapping mapping )
{
Contract.Requires( action != null );
@@ -364,6 +377,7 @@ IEnumerable NewODataApiDescriptions( ControllerActionDescriptor
PopulateApiVersionParameters( apiDescription, mapping.ApiVersion );
apiDescription.SetApiVersion( mapping.ApiVersion );
+ apiDescription.TryUpdateRelativePathAndRemoveApiVersionParameter( Options );
yield return apiDescription;
}
}
@@ -411,6 +425,8 @@ IList GetParameters( ApiParameterContext context )
}
}
+ ProcessRouteParameters( context );
+
return context.Results;
}
@@ -602,22 +618,81 @@ IReadOnlyList GetApiResponseTypes( IReadOnlyList new ApiVersionModelMetadata( MetadataProvider, Options.DefaultApiVersionParameterDescription );
- static string BuildRelativePath( ControllerActionDescriptor action, ODataRouteBuilderContext routeContext )
+ void ProcessRouteParameters( ApiParameterContext context )
{
- Contract.Requires( action != null );
- Contract.Requires( routeContext != null );
- Contract.Ensures( !string.IsNullOrEmpty( Contract.Result() ) );
+ var prefix = context.RouteContext.Route.RoutePrefix;
- var relativePath = action.AttributeRouteInfo?.Template;
+ if ( string.IsNullOrEmpty( prefix ) )
+ {
+ return;
+ }
- // note: if path happens to be built adhead of time, it's expected to be qualified; rebuild it as necessary
- if ( string.IsNullOrEmpty( relativePath ) || !routeContext.Options.UseQualifiedOperationNames )
+ var routeTemplate = TemplateParser.Parse( prefix );
+ var routeParameters = new Dictionary( StringComparer.OrdinalIgnoreCase );
+
+ foreach ( var routeParameter in routeTemplate.Parameters )
{
- var builder = new ODataRouteBuilder( routeContext );
- relativePath = builder.Build();
+ routeParameters.Add( routeParameter.Name, CreateRouteInfo( routeParameter ) );
}
- return relativePath;
+ foreach ( var parameter in context.Results )
+ {
+ if ( parameter.Source == Path || parameter.Source == ModelBinding || parameter.Source == Custom )
+ {
+ if ( routeParameters.TryGetValue( parameter.Name, out var routeInfo ) )
+ {
+ parameter.RouteInfo = routeInfo;
+ routeParameters.Remove( parameter.Name );
+
+ if ( parameter.Source == ModelBinding && !parameter.RouteInfo.IsOptional )
+ {
+ parameter.Source = Path;
+ }
+ }
+ }
+ }
+
+ foreach ( var routeParameter in routeParameters )
+ {
+ var result = new ApiParameterDescription()
+ {
+ Name = routeParameter.Key,
+ RouteInfo = routeParameter.Value,
+ Source = Path,
+ };
+
+ context.Results.Add( result );
+
+ if ( !routeParameter.Value.Constraints.OfType().Any() )
+ {
+ continue;
+ }
+
+ var metadata = NewModelMetadata();
+
+ result.ModelMetadata = metadata;
+ result.Type = metadata.ModelType;
+ }
+ }
+
+ ApiParameterRouteInfo CreateRouteInfo( TemplatePart routeParameter )
+ {
+ var constraints = new List();
+
+ if ( routeParameter.InlineConstraints != null )
+ {
+ foreach ( var constraint in routeParameter.InlineConstraints )
+ {
+ constraints.Add( ConstraintResolver.ResolveConstraint( constraint.Constraint ) );
+ }
+ }
+
+ return new ApiParameterRouteInfo()
+ {
+ Constraints = constraints,
+ DefaultValue = routeParameter.DefaultValue,
+ IsOptional = routeParameter.IsOptional || routeParameter.DefaultValue != null,
+ };
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs
index 0daed33d..6b0ef28f 100644
--- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs
+++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs
@@ -24,11 +24,11 @@ internal PseudoModelBindingVisitor( ApiParameterContext context, ParameterDescri
internal ParameterDescriptor Parameter { get; }
- private HashSet Visited { get; } = new HashSet( new PropertyKeyEqualityComparer() );
+ HashSet Visited { get; } = new HashSet( new PropertyKeyEqualityComparer() );
internal void WalkParameter( ApiParameterDescriptionContext context ) => Visit( context, BindingSource.ModelBinding, containerName: string.Empty );
- private static string GetName( string containerName, ApiParameterDescriptionContext metadata )
+ static string GetName( string containerName, ApiParameterDescriptionContext metadata )
{
if ( string.IsNullOrEmpty( metadata.BinderModelName ) )
{
@@ -38,7 +38,7 @@ private static string GetName( string containerName, ApiParameterDescriptionCont
return metadata.BinderModelName;
}
- private void Visit( ApiParameterDescriptionContext bindingContext, BindingSource ambientSource, string containerName )
+ void Visit( ApiParameterDescriptionContext bindingContext, BindingSource ambientSource, string containerName )
{
var source = bindingContext.BindingSource;
diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilder.Core.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilder.Core.cs
index a52df3c4..83331289 100644
--- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilder.Core.cs
+++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilder.Core.cs
@@ -1,6 +1,8 @@
namespace Microsoft.AspNet.OData.Routing
{
+ using Microsoft.AspNetCore.Routing.Template;
using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
using static System.String;
partial class ODataRouteBuilder
@@ -19,6 +21,34 @@ internal string BuildPath( bool includePrefix )
return Join( "/", segments );
}
- static string UpdateRoutePrefixAndRemoveApiVersionParameterIfNecessary( string routePrefix ) => routePrefix;
+ static string RemoveRouteConstraints( string routePrefix )
+ {
+ Contract.Requires( !IsNullOrEmpty( routePrefix ) );
+ Contract.Ensures( !IsNullOrEmpty( Contract.Result() ) );
+
+ var parsedTemplate = TemplateParser.Parse( routePrefix );
+ var segments = new List( parsedTemplate.Segments.Count );
+
+ foreach ( var segment in parsedTemplate.Segments )
+ {
+ var currentSegment = Empty;
+
+ foreach ( var part in segment.Parts )
+ {
+ if ( part.IsLiteral )
+ {
+ currentSegment += part.Text;
+ }
+ else if ( part.IsParameter )
+ {
+ currentSegment += Concat( "{", part.Name, "}" );
+ }
+ }
+
+ segments.Add( currentSegment );
+ }
+
+ return Join( "/", segments );
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Company.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Company.cs
new file mode 100644
index 00000000..2b696bd5
--- /dev/null
+++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Company.cs
@@ -0,0 +1,18 @@
+namespace Microsoft.AspNet.OData
+{
+ using System;
+ using System.Collections.Generic;
+
+ public class Company
+ {
+ public int CompanyId { get; set; }
+
+ public Company ParentCompany { get; set; }
+
+ public List Subsidiaries { get; set; }
+
+ public string Name { get; set; }
+
+ public DateTime DateFounded { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs
index 837bfd96..02989db4 100644
--- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs
+++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs
@@ -31,6 +31,51 @@ public void substituted_type_should_be_same_as_original_type( Type originalType
substitutedType.Should().Be( originalType );
}
+ [Fact]
+ public void substituted_type_should_be_extracted_from_parent_generic()
+ {
+ // arrange
+ var modelBuilder = new ODataConventionModelBuilder();
+
+ modelBuilder.EntitySet( "Contacts" );
+ modelBuilder.EntityType();
+
+ var context = NewContext( modelBuilder.GetEdmModel() );
+ var originalType = typeof( Delta );
+
+ // act
+ var substitutedType = originalType.SubstituteIfNecessary( context );
+
+ // assert
+ substitutedType.Should().Be( typeof( Contact ) );
+ }
+
+ [Fact]
+ public void type_should_be_match_edm_when_extracted_and_substituted_from_parent_generic()
+ {
+ // arrange
+ var modelBuilder = new ODataConventionModelBuilder();
+ var contact = modelBuilder.EntitySet( "Contacts" ).EntityType;
+
+ contact.Ignore( p => p.Email );
+ contact.Ignore( p => p.Phone );
+ contact.Ignore( p => p.Addresses );
+
+ var context = NewContext( modelBuilder.GetEdmModel() );
+ var originalType = typeof( Delta );
+
+ // act
+ var substitutedType = originalType.SubstituteIfNecessary( context );
+
+ // assert
+ substitutedType.Should().NotBe( originalType );
+ substitutedType.Should().NotBe( typeof( Contact ) );
+ substitutedType.GetRuntimeProperties().Should().HaveCount( 3 );
+ substitutedType.Should().HaveProperty( nameof( Contact.ContactId ) );
+ substitutedType.Should().HaveProperty( nameof( Contact.FirstName ) );
+ substitutedType.Should().HaveProperty( nameof( Contact.LastName ) );
+ }
+
[Theory]
[MemberData( nameof( SubstitutionData ) )]
public void type_should_match_edm_with_top_entity_substitution( Type originalType )
@@ -87,6 +132,47 @@ public void type_should_match_edm_with_nested_entity_substitution()
innerType.Should().HaveProperty( nameof( Contact.LastName ) );
}
+ [Fact]
+ public void type_should_match_with_self_referencing_property_substitution()
+ {
+ // arrange
+ var modelBuilder = new ODataConventionModelBuilder();
+
+ modelBuilder.EntitySet( "Companies" );
+
+ var context = NewContext( modelBuilder.GetEdmModel() );
+ var originalType = typeof( Company );
+
+ //act
+ var subsitutedType = originalType.SubstituteIfNecessary( context );
+
+ // assert
+ subsitutedType.Should().Be( typeof( Company ) );
+ }
+
+ [Fact]
+ public void type_should_use_self_referencing_property_substitution()
+ {
+ // arrange
+ var modelBuilder = new ODataConventionModelBuilder();
+ var company = modelBuilder.EntitySet( "Companies" ).EntityType;
+
+ company.Ignore( c => c.DateFounded );
+
+ var context = NewContext( modelBuilder.GetEdmModel() );
+ var originalType = typeof( Company );
+
+ //act
+ var subsitutedType = originalType.SubstituteIfNecessary( context );
+
+ // assert
+ subsitutedType.GetRuntimeProperties().Should().HaveCount( 4 );
+ subsitutedType.Should().HaveProperty( nameof( Company.CompanyId ) );
+ subsitutedType.Should().HaveProperty( nameof( Company.Name ) );
+ subsitutedType.Should().Be( subsitutedType.GetRuntimeProperty( nameof( Company.ParentCompany ) ).PropertyType );
+ subsitutedType.Should().Be( subsitutedType.GetRuntimeProperty( nameof( Company.Subsidiaries ) ).PropertyType.GetGenericArguments()[0] );
+ }
+
[Theory]
[MemberData( nameof( SubstitutionData ) )]
public void type_should_match_edm_with_child_entity_substitution( Type originalType )
@@ -113,7 +199,7 @@ public void type_should_match_edm_with_child_entity_substitution( Type originalT
nextType.Should().HaveProperty( nameof( Contact.LastName ) );
nextType.Should().HaveProperty( nameof( Contact.Email ) );
nextType.Should().HaveProperty( nameof( Contact.Phone ) );
- nextType = nextType.GetRuntimeProperties().Single( p => p.Name == nameof( Contact.Addresses ) ).PropertyType.GetGenericArguments()[0];
+ nextType = nextType.GetRuntimeProperty( nameof( Contact.Addresses ) ).PropertyType.GetGenericArguments()[0];
nextType.GetRuntimeProperties().Should().HaveCount( 5 );
nextType.Should().HaveProperty( nameof( Address.AddressId ) );
nextType.Should().HaveProperty( nameof( Address.Street ) );
@@ -156,7 +242,6 @@ public static IEnumerable