From 54b3b89cf1ebccbeb78d29a3bb9a26ba814c0cac Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sat, 28 Mar 2020 12:31:21 -0700 Subject: [PATCH 01/13] Ensure VersionedMetadataController assembly is registered with PartManager. Resolves #551. --- .../IODataBuilderExtensions.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/IODataBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/IODataBuilderExtensions.cs index 55cd49c5..031836ca 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/IODataBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/IODataBuilderExtensions.cs @@ -59,10 +59,15 @@ public static IODataBuilder EnableApiVersioning( this IODataBuilder builder, Act static void AddODataServices( IServiceCollection services ) { - // note: if we end up creating a new ApplicationPartManager here we won't fail, but the setup - // will not register any model configurations automatically. this is almost certainly because - // services.AddMvcCore() hasn't be called yet, which is unexpected - var partManager = services.GetService() ?? new ApplicationPartManager(); + var partManager = services.GetService(); + + if ( partManager == null ) + { + partManager = new ApplicationPartManager(); + services.TryAddSingleton( partManager ); + } + + partManager.ApplicationParts.Add( new AssemblyPart( typeof( IODataBuilderExtensions ).Assembly ) ); ConfigureDefaultFeatureProviders( partManager ); services.Replace( Singleton() ); From 6787afd7cfeacc87dc03abf4a669c0222da96a4c Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Wed, 16 Sep 2020 17:59:37 -0700 Subject: [PATCH 02/13] Checkpoint to fixes started in 2020-03 --- .../Builder/ODataValidationSettingsConvention.cs | 11 +++++++++++ .../Builder/ODataValidationSettingsConvention.cs | 12 +----------- .../HttpActionDescriptorExtensions.cs | 1 + .../System.Web.Http/HttpRequestMessageExtensions.cs | 9 ++++++++- .../Builder/ODataValidationSettingsConvention.cs | 12 +----------- .../Builder/ODataValidationSettingsConventionTest.cs | 9 +++------ .../Builder/ODataValidationSettingsConventionTest.cs | 9 +++------ 7 files changed, 28 insertions(+), 35 deletions(-) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs b/src/Common.OData.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs index 98fede11..81f9fd09 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs @@ -115,6 +115,17 @@ protected virtual ApiParameterDescription NewCountParameter( ODataQueryOptionDes return NewParameterDescription( GetName( Count ), description, typeof( bool ), defaultValue: false ); } + // REF: http://docs.oasis-open.org/odata/odata/v4.01/cs01/part2-url-conventions/odata-v4.01-cs01-part2-url-conventions.html#sec_SystemQueryOptions + static bool IsSupported( string httpMethod ) => + httpMethod.ToUpperInvariant() switch + { + "GET" => true, + "PUT" => true, + "PATCH" => true, + "POST" => true, + _ => false, + }; + string GetName( AllowedQueryOptions option ) { #pragma warning disable CA1308 // Normalize strings to uppercase diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs index 8c648291..2d39b086 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs @@ -24,7 +24,7 @@ public virtual void ApplyTo( ApiDescription apiDescription ) throw new ArgumentNullException( nameof( apiDescription ) ); } - if ( !IsSupported( apiDescription ) ) + if ( !IsSupported( apiDescription.HttpMethod.Method ) ) { return; } @@ -142,15 +142,5 @@ static ApiParameterDescription SetAction( ApiParameterDescription parameter, Api return parameter; } - - static bool IsSupported( ApiDescription apiDescription ) - { - return apiDescription.HttpMethod.Method.ToUpperInvariant() switch - { - "GET" => true, - "POST" => apiDescription.Operation()?.IsAction() == true, - _ => false, - }; - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs index 5082be89..6ad849c5 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs @@ -110,6 +110,7 @@ internal static bool IsAttributeRouted( this HttpActionDescriptor action ) => internal static T? GetProperty( this HttpActionDescriptor action ) where T : class => action.Properties.TryGetValue( typeof( T ), out T value ) ? value : default; +#pragma warning restore CS8603 // Possible null reference return. internal static void SetProperty( this HttpActionDescriptor action, T value ) => action.Properties.AddOrUpdate( typeof( T ), value, ( key, oldValue ) => value ); diff --git a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs index 77c48099..616c0d2c 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs @@ -3,7 +3,6 @@ using Microsoft.Web.Http; using Microsoft.Web.Http.Versioning; using System; - using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http; @@ -12,6 +11,7 @@ /// public static class HttpRequestMessageExtensions { + const string RoutingContextKey = "MS_RoutingContext"; const string ApiVersionPropertiesKey = "MS_" + nameof( ApiVersionRequestProperties ); static HttpResponseMessage CreateErrorResponse( this HttpRequestMessage request, HttpStatusCode statusCode, Func errorCreator ) @@ -83,7 +83,14 @@ public static ApiVersionRequestProperties ApiVersionProperties( this HttpRequest if ( !request.Properties.TryGetValue( ApiVersionPropertiesKey, out ApiVersionRequestProperties properties ) ) { + var forceRouteConstraintEvaluation = !request.Properties.ContainsKey( RoutingContextKey ); + request.Properties[ApiVersionPropertiesKey] = properties = new ApiVersionRequestProperties( request ); + + if ( forceRouteConstraintEvaluation ) + { + request.GetConfiguration()?.Routes.GetRouteData( request ); + } } return properties; diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs index e0e5f951..f0bd5e7c 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Builder/ODataValidationSettingsConvention.cs @@ -23,7 +23,7 @@ public virtual void ApplyTo( ApiDescription apiDescription ) throw new ArgumentNullException( nameof( apiDescription ) ); } - if ( !IsSupported( apiDescription ) ) + if ( !IsSupported( apiDescription.HttpMethod ) ) { return; } @@ -152,15 +152,5 @@ static bool IsSingleResult( ApiDescription description, out Type? resultType ) resultType = responseType; return true; } - - static bool IsSupported( ApiDescription apiDescription ) - { - return apiDescription.HttpMethod.ToUpperInvariant() switch - { - "GET" => true, - "POST" => apiDescription.Operation()?.IsAction() == true, - _ => false, - }; - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Builder/ODataValidationSettingsConventionTest.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Builder/ODataValidationSettingsConventionTest.cs index 154ec2f5..f0a5da00 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Builder/ODataValidationSettingsConventionTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Builder/ODataValidationSettingsConventionTest.cs @@ -24,14 +24,11 @@ public class ODataValidationSettingsConventionTest { - [Theory] - [InlineData( "PUT" )] - [InlineData( "PATCH" )] - [InlineData( "DELETE" )] - public void apply_to_should_ignore_nonquery_and_nonaction_descriptions( string httpMethod ) + [Fact] + public void apply_to_should_ignore_nonquery_and_nonaction_description() { // arrange - var description = NewApiDescription( httpMethod ); + var description = NewApiDescription( "DELETE" ); var validationSettings = new ODataValidationSettings(); var settings = new TestODataQueryOptionSettings(); var convention = new ODataValidationSettingsConvention( validationSettings, settings ); diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Builder/ODataValidationSettingsConventionTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Builder/ODataValidationSettingsConventionTest.cs index e0813f18..14bc37d7 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Builder/ODataValidationSettingsConventionTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Builder/ODataValidationSettingsConventionTest.cs @@ -24,14 +24,11 @@ public class ODataValidationSettingsConventionTest { - [Theory] - [InlineData( "PUT" )] - [InlineData( "PATCH" )] - [InlineData( "DELETE" )] - public void apply_to_should_ignore_nonquery_and_nonaction_descriptions( string httpMethod ) + [Fact] + public void apply_to_should_ignore_nonquery_and_nonaction_description() { // arrange - var description = NewApiDescription( httpMethod ); + var description = NewApiDescription( "DELETE" ); var validationSettings = new ODataValidationSettings(); var settings = new TestODataQueryOptionSettings( typeof( object ) ); var convention = new ODataValidationSettingsConvention( validationSettings, settings ); From 35527c677ecd1ce8dcd900623c437c3bdd239e98 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 18 Sep 2020 15:02:50 -0700 Subject: [PATCH 03/13] Configure container for unversioned route. Fixed #553 --- ApiVersioning.sln | 2 +- .../Extensions/IRouteBuilderExtensions.cs | 32 ++++++++++++++----- .../UnversionedODataPathRouteConstraint.cs | 3 ++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/ApiVersioning.sln b/ApiVersioning.sln index c370c882..eec68c5a 100644 --- a/ApiVersioning.sln +++ b/ApiVersioning.sln @@ -357,4 +357,4 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5A38B7FA-17BC-4D3C-977F-7379653DC67C} EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IRouteBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IRouteBuilderExtensions.cs index ddc6beff..4b6747e2 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IRouteBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IRouteBuilderExtensions.cs @@ -127,7 +127,13 @@ IEnumerable ConfigureRoutingConventions( IEdmModel mode routeCollection.Add( new ODataRouteMapping( route, apiVersion, rootContainer ) ); } - builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( routeName, routePrefix, unversionedConstraints, inlineConstraintResolver ); + builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( + routeName, + routePrefix, + unversionedConstraints, + inlineConstraintResolver, + builder.ConfigureDefaultServices( container => configureAction?.Invoke( container ) ) ); + NotifyRoutesMapped(); return odataRoutes; @@ -278,7 +284,13 @@ IEnumerable NewRouteConventions( IServiceProvider servi routeCollection.Add( new ODataRouteMapping( route, apiVersion, rootContainer ) ); } - builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( routeName, routePrefix, unversionedConstraints, inlineConstraintResolver ); + builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( + routeName, + routePrefix, + unversionedConstraints, + inlineConstraintResolver, + builder.ConfigureDefaultServices( _ => { } ) ); + NotifyRoutesMapped(); return odataRoutes; @@ -340,13 +352,13 @@ IEnumerable NewRoutingConventions( IServiceProvider ser var routeCollection = builder.ServiceProvider.GetRequiredService(); var perRouteContainer = builder.ServiceProvider.GetRequiredService(); var inlineConstraintResolver = builder.ServiceProvider.GetRequiredService(); - var preConfigureAction = builder.ConfigureDefaultServices( + var perConfigureAction = builder.ConfigureDefaultServices( container => { container.AddService( Singleton, typeof( IEnumerable ), NewRoutingConventions ); configureAction?.Invoke( container ); } ); - var rootContainer = perRouteContainer.CreateODataRootContainer( routeName, preConfigureAction ); + var rootContainer = perRouteContainer.CreateODataRootContainer( routeName, perConfigureAction ); var router = rootContainer.GetService() ?? builder.DefaultHandler; builder.ConfigurePathHandler( rootContainer ); @@ -357,7 +369,7 @@ IEnumerable NewRoutingConventions( IServiceProvider ser builder.ConfigureBatchHandler( rootContainer, route ); builder.Routes.Add( route ); routeCollection.Add( new ODataRouteMapping( route, apiVersion, rootContainer ) ); - builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( routeName, routePrefix, apiVersion, inlineConstraintResolver ); + builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( routeName, routePrefix, apiVersion, inlineConstraintResolver, perConfigureAction ); NotifyRoutesMapped(); return route; @@ -484,7 +496,7 @@ IEnumerable NewRoutingConventions( IServiceProvider ser builder.ConfigureBatchHandler( rootContainer, route ); builder.Routes.Add( route ); routeCollection.Add( new ODataRouteMapping( route, apiVersion, rootContainer ) ); - builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( routeName, routePrefix, apiVersion, inlineConstraintResolver ); + builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( routeName, routePrefix, apiVersion, inlineConstraintResolver, configureAction ); NotifyRoutesMapped(); return route; @@ -552,7 +564,8 @@ static void AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( string routeName, string routePrefix, IEnumerable unversionedConstraints, - IInlineConstraintResolver inlineConstraintResolver ) + IInlineConstraintResolver inlineConstraintResolver, + Action configureAction ) { routeName += UnversionedRouteSuffix; @@ -560,6 +573,7 @@ static void AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( var route = new ODataRoute( builder.DefaultHandler, routeName, routePrefix, constraint, inlineConstraintResolver ); builder.Routes.Add( route ); + builder.ServiceProvider.GetRequiredService().CreateODataRootContainer( routeName, configureAction ); } static void AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( @@ -567,7 +581,8 @@ static void AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( string routeName, string routePrefix, ApiVersion apiVersion, - IInlineConstraintResolver inlineConstraintResolver ) + IInlineConstraintResolver inlineConstraintResolver, + Action configureAction ) { routeName += UnversionedRouteSuffix; @@ -576,6 +591,7 @@ static void AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( var route = new ODataRoute( builder.DefaultHandler, routeName, routePrefix, constraint, inlineConstraintResolver ); builder.Routes.Add( route ); + builder.ServiceProvider.GetRequiredService().CreateODataRootContainer( routeName, configureAction ); } static IRouteConstraint MakeVersionedODataRouteConstraint( ApiVersion apiVersion, ref string versionedRouteName ) diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/UnversionedODataPathRouteConstraint.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/UnversionedODataPathRouteConstraint.cs index 765a276a..b8645b62 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/UnversionedODataPathRouteConstraint.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/UnversionedODataPathRouteConstraint.cs @@ -66,6 +66,9 @@ public bool Match( HttpContext httpContext, IRouter route, string routeKey, Rout return false; } + // delete the request container because ODataPathRouteConstraint will try to create it resulting in an exception + // ODataPathRouteConstraint cleans itself up afterward + // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNetCore.OData/Routing/ODataPathRouteConstraint.cs#L53 httpContext.Request.DeleteRequestContainer( true ); // by evaluating the remaining unversioned constraints, this will ultimately determine whether 400 or 404 From fefba5ce28a04f1cc6afffeff0708648931f74c1 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Fri, 18 Sep 2020 16:18:27 -0700 Subject: [PATCH 04/13] Validate candidates before matching. Fixes #600 --- .../Routing/ApiVersionMatcherPolicy.cs | 65 ++++++++++++------- ...a query string and split into two types.cs | 5 +- .../when using a url segment.cs | 7 +- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionMatcherPolicy.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionMatcherPolicy.cs index bfbf4832..36b4c5a0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionMatcherPolicy.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionMatcherPolicy.cs @@ -95,33 +95,28 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates ) httpContext.Features.Get().RequestedApiVersion = apiVersion; } - var finalMatches = EvaluateApiVersion( candidates, apiVersion ); - - if ( finalMatches.Count == 0 ) + if ( !MatchesApiVersion( candidates, apiVersion ) ) { httpContext.SetEndpoint( ClientError( httpContext, candidates ) ); } - else - { - for ( var i = 0; i < finalMatches.Count; i++ ) - { - var (index, _, valid) = finalMatches[i]; - candidates.SetValidity( index, valid ); - } - } return CompletedTask; } - static IReadOnlyList<(int Index, ActionDescriptor Action, bool Valid)> EvaluateApiVersion( CandidateSet candidates, ApiVersion? apiVersion ) + static bool MatchesApiVersion( CandidateSet candidates, ApiVersion? apiVersion ) { - var bestMatches = new List<(int Index, ActionDescriptor Action, bool)>(); - var implicitMatches = new List<(int, ActionDescriptor, bool)>(); + var bestMatches = new List(); + var implicitMatches = new List(); for ( var i = 0; i < candidates.Count; i++ ) { + if ( !candidates.IsValidCandidate( i ) ) + { + continue; + } + ref var candidate = ref candidates[i]; - var action = candidate.Endpoint.Metadata?.GetMetadata(); + var action = candidate.Endpoint.Metadata.GetMetadata(); if ( action == null ) { @@ -134,10 +129,10 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates ) switch ( action.MappingTo( apiVersion ) ) { case Explicit: - bestMatches.Add( (i, action, candidates.IsValidCandidate( i )) ); + bestMatches.Add( i ); break; case Implicit: - implicitMatches.Add( (i, action, candidates.IsValidCandidate( i )) ); + implicitMatches.Add( i ); break; } @@ -149,20 +144,39 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates ) switch ( bestMatches.Count ) { case 0: - bestMatches.AddRange( implicitMatches ); - break; + if ( implicitMatches.Count == 0 ) + { + return false; + } + + for ( var i = 0; i < implicitMatches.Count; i++ ) + { + candidates.SetValidity( implicitMatches[i], true ); + } + + return true; case 1: - var model = bestMatches[0].Action.GetApiVersionModel(); + ref var candidate = ref candidates[bestMatches[0]]; + var action = candidate.Endpoint.Metadata.GetMetadata(); + var model = action.GetApiVersionModel(); if ( model.IsApiVersionNeutral ) { - bestMatches.AddRange( implicitMatches ); + for ( var i = 0; i < implicitMatches.Count; i++ ) + { + candidates.SetValidity( implicitMatches[i], true ); + } } break; } - return bestMatches.ToArray(); + for ( var i = 0; i < bestMatches.Count; i++ ) + { + candidates.SetValidity( bestMatches[i], true ); + } + + return true; } bool IsRequestedApiVersionAmbiguous( HttpContext httpContext, out ApiVersion? apiVersion ) @@ -195,8 +209,13 @@ ApiVersion TrySelectApiVersion( HttpContext httpContext, CandidateSet candidates for ( var i = 0; i < candidates.Count; i++ ) { + if ( !candidates.IsValidCandidate( i ) ) + { + continue; + } + ref var candidate = ref candidates[i]; - var model = candidate.Endpoint.Metadata?.GetMetadata()?.GetApiVersionModel(); + var model = candidate.Endpoint.Metadata.GetMetadata()?.GetApiVersionModel(); if ( model != null ) { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a query string and split into two types.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a query string and split into two types.cs index b12bec4c..87f9163f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a query string and split into two types.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a query string and split into two types.cs @@ -61,17 +61,16 @@ public async Task then_get_with_integer_id_should_return_200() } [Fact] - public async Task then_get_returns_400_or_405_with_invalid_id() + public async Task then_get_returns_400_with_invalid_id() { // arrange var requestUrl = "api/values/abc?api-version=2.0"; - var statusCode = UsingEndpointRouting ? NotFound : BadRequest; // act var response = await GetAsync( requestUrl ); // assert - response.StatusCode.Should().Be( statusCode ); + response.StatusCode.Should().Be( BadRequest ); } [Theory] diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a url segment.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a url segment.cs index 057050e2..a6898aa8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a url segment.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a url segment.cs @@ -65,17 +65,16 @@ public async Task then_post_should_return_201( string version ) } [Fact] - public async Task then_get_returns_400_or_405_with_invalid_id() + public async Task then_get_returns_400_with_invalid_id() { // arrange var requestUrl = "api/v2/helloworld/abc"; - var statusCode = UsingEndpointRouting ? NotFound : BadRequest; // act var response = await GetAsync( requestUrl ); - // assert - response.StatusCode.Should().Be( statusCode ); + // asserts + response.StatusCode.Should().Be( BadRequest ); } [Theory] From c96147ced0109f0737ca38f524fc3c997ab42587 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 21 Sep 2020 17:20:59 -0700 Subject: [PATCH 05/13] Support ProblemDetails error response. Resolves #612 --- .../ProblemDetailsErrorResponseProvider.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ProblemDetailsErrorResponseProvider.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ProblemDetailsErrorResponseProvider.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ProblemDetailsErrorResponseProvider.cs new file mode 100644 index 00000000..b34e3468 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ProblemDetailsErrorResponseProvider.cs @@ -0,0 +1,67 @@ +namespace Microsoft.AspNetCore.Mvc.Versioning +{ + using Microsoft.AspNetCore.Mvc.Infrastructure; + using Microsoft.Extensions.DependencyInjection; + using System; + + /// + /// Represents the provider for creating HTTP error responses matching and RFC 7807. + /// + [CLSCompliant( false )] + public class ProblemDetailsErrorResponseProvider : IErrorResponseProvider + { + /// + public virtual IActionResult CreateResponse( ErrorResponseContext context ) + { + if ( context == null ) + { + throw new ArgumentNullException( nameof( context ) ); + } + + return NewResult( context ); + } + + /// + /// Creates and returns new problem details. + /// + /// The error context used to generate response. + /// New problem details. + protected virtual ProblemDetails NewProblemDetails( ErrorResponseContext context ) + { + if ( context == null ) + { + throw new ArgumentNullException( nameof( context ) ); + } + + var httpContext = context.Request.HttpContext; + var factory = httpContext.RequestServices.GetRequiredService(); + + return factory.CreateProblemDetails( httpContext, context.StatusCode, context.ErrorCode, default, context.Message, default ); + } + + /// + /// Creates and returns a new result. + /// + /// The error context used to generate response. + /// A new . + protected virtual ObjectResult NewResult( ErrorResponseContext context ) + { + if ( context == null ) + { + throw new ArgumentNullException( nameof( context ) ); + } + + // match behavior of IClientErrorFactory.GetClientError + // REF: https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs#L17 + return new ObjectResult( NewProblemDetails( context ) ) + { + StatusCode = context.StatusCode, + ContentTypes = + { + "application/problem+json", + "application/problem+xml", + }, + }; + } + } +} \ No newline at end of file From b376328d8d6d2604c857324f8966d95590e73c98 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Mon, 21 Sep 2020 17:21:55 -0700 Subject: [PATCH 06/13] Support API version in URL generation. Resolves #663 --- .../Controllers/HelloWorldController.cs | 4 +- .../V3/Controllers/PeopleController.cs | 2 +- .../Versioning/IApiVersionReaderExtensions.cs | 23 +++- .../VersionedODataPathRouteConstraint.cs | 36 +----- .../Routing/VersionedUrlHelperDecorator.cs | 44 ------- .../ApiVersionControllerSelector.cs | 20 +++ .../Routing/ApiVersionRouteConstraint.cs | 1 + .../Versioning/ApiVersionRequestProperties.cs | 9 ++ .../Versioning/ApiVersionUrlHelper.cs | 79 ++++++++++++ .../IServiceCollectionExtensions.cs | 36 ++++++ .../Routing/ApiVersionRouteConstraint.cs | 1 + .../Versioning/ApiVersionUrlHelper.cs | 115 ++++++++++++++++++ .../Versioning/ApiVersionUrlHelperFactory.cs | 40 ++++++ .../Versioning/ApiVersioningFeature.cs | 20 +-- .../Versioning/IApiVersioningFeature.cs | 9 ++ .../IServiceCollectionExtensionsTest.cs | 4 + .../Routing/ApiVersionRouteConstraintTest.cs | 88 ++++++++++---- 17 files changed, 411 insertions(+), 120 deletions(-) delete mode 100644 src/Microsoft.AspNet.OData.Versioning/Routing/VersionedUrlHelperDecorator.cs create mode 100644 src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionUrlHelper.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionUrlHelper.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionUrlHelperFactory.cs diff --git a/samples/aspnetcore/BasicSample/Controllers/HelloWorldController.cs b/samples/aspnetcore/BasicSample/Controllers/HelloWorldController.cs index 540a4eb0..3eba74e2 100644 --- a/samples/aspnetcore/BasicSample/Controllers/HelloWorldController.cs +++ b/samples/aspnetcore/BasicSample/Controllers/HelloWorldController.cs @@ -14,10 +14,10 @@ public class HelloWorldController : ControllerBase // GET api/v{version}/helloworld/{id} [HttpGet( "{id:int}" )] - public IActionResult Get( int id, ApiVersion apiVersion ) => Ok( new { Controller = GetType().Name, Id = id, Version = apiVersion.ToString() } ); + public IActionResult Get( int id, ApiVersion apiVersion ) => Ok( new { Controller = GetType().Name, Id = id } ); // POST api/v{version}/helloworld [HttpPost] - public IActionResult Post( ApiVersion apiVersion ) => CreatedAtAction( nameof( Get ), new { id = 42, version = apiVersion.ToString() }, null ); + public IActionResult Post( ApiVersion apiVersion ) => CreatedAtAction( nameof( Get ), new { id = 42 }, null ); } } \ No newline at end of file diff --git a/samples/aspnetcore/SwaggerSample/V3/Controllers/PeopleController.cs b/samples/aspnetcore/SwaggerSample/V3/Controllers/PeopleController.cs index 785afd73..6bc32bd6 100644 --- a/samples/aspnetcore/SwaggerSample/V3/Controllers/PeopleController.cs +++ b/samples/aspnetcore/SwaggerSample/V3/Controllers/PeopleController.cs @@ -91,7 +91,7 @@ public IActionResult Get( int id ) => public IActionResult Post( [FromBody] Person person, ApiVersion apiVersion ) { person.Id = 42; - return CreatedAtAction( nameof( Get ), new { id = person.Id, version = apiVersion.ToString() }, person ); + return CreatedAtAction( nameof( Get ), new { id = person.Id }, person ); } } } \ No newline at end of file diff --git a/src/Common/Versioning/IApiVersionReaderExtensions.cs b/src/Common/Versioning/IApiVersionReaderExtensions.cs index 8ec03c80..a07352ef 100644 --- a/src/Common/Versioning/IApiVersionReaderExtensions.cs +++ b/src/Common/Versioning/IApiVersionReaderExtensions.cs @@ -11,21 +11,38 @@ namespace Microsoft.AspNetCore.Mvc.Versioning internal static class IApiVersionReaderExtensions { + internal static bool VersionsByUrlSegment( this IApiVersionReader reader ) + { + var context = new UrlSegmentDescriptionContext(); + reader.AddParameters( context ); + return context.HasPathApiVersion; + } + internal static bool VersionsByMediaType( this IApiVersionReader reader ) { - var context = new DescriptionContext(); + var context = new MediaTypeDescriptionContext(); reader.AddParameters( context ); return context.HasMediaTypeApiVersion; } internal static string GetMediaTypeVersionParameter( this IApiVersionReader reader ) { - var context = new DescriptionContext(); + var context = new MediaTypeDescriptionContext(); reader.AddParameters( context ); return context.ParameterName; } - sealed class DescriptionContext : IApiVersionParameterDescriptionContext + sealed class UrlSegmentDescriptionContext : IApiVersionParameterDescriptionContext + { + internal bool HasPathApiVersion { get; private set; } + + public void AddParameter( string name, ApiVersionParameterLocation location ) + { + HasPathApiVersion |= location == Path; + } + } + + sealed class MediaTypeDescriptionContext : IApiVersionParameterDescriptionContext { readonly StringComparer comparer = StringComparer.OrdinalIgnoreCase; readonly List parameterNames = new List(); diff --git a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedODataPathRouteConstraint.cs b/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedODataPathRouteConstraint.cs index 9896200c..2dda6784 100644 --- a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedODataPathRouteConstraint.cs +++ b/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedODataPathRouteConstraint.cs @@ -82,13 +82,7 @@ public override bool Match( HttpRequestMessage request, IHttpRoute route, string return false; } - if ( ApiVersion == requestedVersion ) - { - DecorateUrlHelperWithApiVersionRouteValueIfNecessary( request, values ); - return true; - } - - return false; + return ApiVersion == requestedVersion; } static ApiVersion? GetRequestedApiVersionOrReturnBadRequest( HttpRequestMessage request ) @@ -105,33 +99,5 @@ public override bool Match( HttpRequestMessage request, IHttpRoute route, string throw new HttpResponseException( request.CreateResponse( BadRequest, error ) ); } } - - static void DecorateUrlHelperWithApiVersionRouteValueIfNecessary( HttpRequestMessage request, IDictionary values ) - { - object apiVersion; - string routeConstraintName; - var configuration = request.GetConfiguration(); - - if ( configuration == null ) - { - routeConstraintName = nameof( apiVersion ); - } - else - { - routeConstraintName = configuration.GetApiVersioningOptions().RouteConstraintName; - } - - if ( !values.TryGetValue( routeConstraintName, out apiVersion ) ) - { - return; - } - - var requestContext = request.GetRequestContext(); - - if ( !( requestContext.Url is VersionedUrlHelperDecorator ) ) - { - requestContext.Url = new VersionedUrlHelperDecorator( requestContext.Url, routeConstraintName, apiVersion ); - } - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedUrlHelperDecorator.cs b/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedUrlHelperDecorator.cs deleted file mode 100644 index f224af56..00000000 --- a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedUrlHelperDecorator.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Microsoft.AspNet.OData.Routing -{ - using System.Collections.Generic; - using System.Web.Http.Routing; - - sealed class VersionedUrlHelperDecorator : UrlHelper - { - readonly UrlHelper decorated; - readonly string routeConstraintName; - readonly object apiVersion; - - internal VersionedUrlHelperDecorator( UrlHelper decorated, string routeConstraintName, object apiVersion ) - { - this.decorated = decorated; - this.routeConstraintName = routeConstraintName; - this.apiVersion = apiVersion; - - if ( decorated.Request != null ) - { - Request = decorated.Request; - } - } - - void EnsureApiVersionRouteValue( IDictionary routeValues ) => routeValues[routeConstraintName] = apiVersion; - - public override string Content( string path ) => decorated.Content( path ); - - public override string Link( string routeName, object routeValues ) => decorated.Link( routeName, routeValues ); - - public override string Link( string routeName, IDictionary routeValues ) - { - EnsureApiVersionRouteValue( routeValues ); - return decorated.Link( routeName, routeValues ); - } - - public override string Route( string routeName, object routeValues ) => decorated.Route( routeName, routeValues ); - - public override string Route( string routeName, IDictionary routeValues ) - { - EnsureApiVersionRouteValue( routeValues ); - return decorated.Route( routeName, routeValues ); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerSelector.cs b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerSelector.cs index 05e5e4a4..98a677db 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerSelector.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ApiVersionControllerSelector.cs @@ -79,6 +79,7 @@ public virtual IDictionary GetControllerMappin if ( conventionRouteResult.Succeeded ) { + EnsureUrlHelper( request ); return request.ApiVersionProperties().SelectedController = conventionRouteResult.Controller; } @@ -90,6 +91,7 @@ public virtual IDictionary GetControllerMappin if ( directRouteResult.Succeeded ) { + EnsureUrlHelper( request ); return request.ApiVersionProperties().SelectedController = directRouteResult.Controller; } @@ -97,6 +99,7 @@ public virtual IDictionary GetControllerMappin if ( conventionRouteResult.Succeeded ) { + EnsureUrlHelper( request ); return request.ApiVersionProperties().SelectedController = conventionRouteResult.Controller; } @@ -293,5 +296,22 @@ static void EnsureRequestHasValidApiVersion( HttpRequestMessage request ) throw new HttpResponseException( response.BadRequest( request, AmbiguousApiVersion, ex.Message ) ); } } + + static void EnsureUrlHelper( HttpRequestMessage request ) + { + var context = request.GetRequestContext(); + + if ( context == null || context.Url is ApiVersionUrlHelper ) + { + return; + } + + var options = request.GetApiVersioningOptions(); + + if ( options.ApiVersionReader.VersionsByUrlSegment() ) + { + context.Url = new ApiVersionUrlHelper( context.Url ); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Routing/ApiVersionRouteConstraint.cs b/src/Microsoft.AspNet.WebApi.Versioning/Routing/ApiVersionRouteConstraint.cs index 3d0d82fd..1c0339b3 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Routing/ApiVersionRouteConstraint.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Routing/ApiVersionRouteConstraint.cs @@ -47,6 +47,7 @@ public bool Match( HttpRequestMessage request, IHttpRoute route, string paramete var properties = request.ApiVersionProperties(); + properties.RouteParameter = parameterName; properties.RawRequestedApiVersion = value; if ( TryParse( value, out var requestedVersion ) ) diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionRequestProperties.cs b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionRequestProperties.cs index 111b7f3a..f27db838 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionRequestProperties.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionRequestProperties.cs @@ -1,5 +1,6 @@ namespace Microsoft.Web.Http.Versioning { + using Microsoft.Web.Http.Routing; using System.ComponentModel; using System.Net.Http; using System.Web.Http; @@ -21,6 +22,14 @@ public class ApiVersionRequestProperties /// The current HTTP request. public ApiVersionRequestProperties( HttpRequestMessage request ) => this.request = request; + /// + /// Gets or sets the name of the route parameter containing the API Version value. + /// + /// The name of the API version route parameter or null. + /// This property will be null unless versioning by URL segment and the incoming request + /// matches the API version route constraint. + public string? RouteParameter { get; set; } + /// /// Gets or sets the raw, unparsed API version for the current request. /// diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionUrlHelper.cs b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionUrlHelper.cs new file mode 100644 index 00000000..8221481a --- /dev/null +++ b/src/Microsoft.AspNet.WebApi.Versioning/Versioning/ApiVersionUrlHelper.cs @@ -0,0 +1,79 @@ +namespace Microsoft.Web.Http.Versioning +{ + using System; + using System.Collections.Generic; + using System.Web.Http; + using System.Web.Http.Routing; + + /// + /// Represents an API version aware URL helper. + /// + public class ApiVersionUrlHelper : UrlHelper + { + /// + /// Initializes a new instance of the class. + /// + /// The inner URL helper. + public ApiVersionUrlHelper( UrlHelper url ) + { + Url = url ?? throw new ArgumentNullException( nameof( url ) ); + + if ( url.Request != null ) + { + Request = url.Request; + } + } + + /// + /// Gets the inner URL helper. + /// + /// The inner URL helper. + protected UrlHelper Url { get; } + + /// + public override string Content( string path ) => Url.Content( path ); + + /// + public override string Link( string routeName, IDictionary routeValues ) => + Url.Link( routeName, AddApiVersionRouteValueIfNecessary( routeValues ) ); + + /// + public override string Route( string routeName, IDictionary routeValues ) => + Url.Route( routeName, AddApiVersionRouteValueIfNecessary( routeValues ) ); + + IDictionary? AddApiVersionRouteValueIfNecessary( IDictionary? routeValues ) + { + if ( Request == null ) + { + return routeValues; + } + + var properties = Request.ApiVersionProperties(); + var key = properties.RouteParameter; + + if ( string.IsNullOrEmpty( key ) ) + { + return routeValues; + } + + var value = properties.RawRequestedApiVersion; + + if ( string.IsNullOrEmpty( value ) ) + { + return routeValues; + } + + if ( routeValues == null ) + { + return new HttpRouteValueDictionary() { [key!] = value! }; + } + + if ( !routeValues.ContainsKey( key! ) ) + { + routeValues.Add( key!, value! ); + } + + return routeValues; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs index b674012c..fcc66c01 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using System; + using System.Linq; using static ServiceDescriptor; /// @@ -66,6 +67,7 @@ static void AddApiVersioningServices( IServiceCollection services ) services.TryAddEnumerable( Transient() ); services.TryAddEnumerable( Singleton() ); services.AddTransient(); + services.Replace( WithUrlHelperFactoryDecorator( services ) ); } static IReportApiVersions OnRequestIReportApiVersions( IServiceProvider serviceProvider ) @@ -79,5 +81,39 @@ static IReportApiVersions OnRequestIReportApiVersions( IServiceProvider serviceP return new DoNotReportApiVersions(); } + + static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor ) + { + if ( descriptor.ImplementationInstance != null ) + { + return descriptor.ImplementationInstance; + } + + if ( descriptor.ImplementationFactory != null ) + { + return descriptor.ImplementationFactory( services ); + } + + return ActivatorUtilities.GetServiceOrCreateInstance( services, descriptor.ImplementationType ); + } + + static ServiceDescriptor WithUrlHelperFactoryDecorator( IServiceCollection services ) + { + var descriptor = services.First( sd => sd.ServiceType == typeof( IUrlHelperFactory ) ); + var factory = ActivatorUtilities.CreateFactory( typeof( ApiVersionUrlHelperFactory ), new[] { typeof( IUrlHelperFactory ) } ); + + IUrlHelperFactory NewFactory( IServiceProvider serviceProvider ) + { + var decorated = serviceProvider.CreateInstance( descriptor! ); + var options = serviceProvider.GetRequiredService>().Value; + var instance = options.ApiVersionReader.VersionsByUrlSegment() ? + factory( serviceProvider, new[] { decorated } ) : + decorated; + + return (IUrlHelperFactory) instance; + } + + return Describe( typeof( IUrlHelperFactory ), NewFactory, descriptor.Lifetime ); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionRouteConstraint.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionRouteConstraint.cs index a030b2c0..112e55be 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionRouteConstraint.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionRouteConstraint.cs @@ -52,6 +52,7 @@ public bool Match( HttpContext httpContext, IRouter route, string routeKey, Rout var feature = httpContext.Features.Get(); + feature.RouteParameter = routeKey; feature.RawRequestedApiVersion = value; if ( TryParse( value, out var requestedVersion ) ) diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionUrlHelper.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionUrlHelper.cs new file mode 100644 index 00000000..9acdd899 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionUrlHelper.cs @@ -0,0 +1,115 @@ +namespace Microsoft.AspNetCore.Mvc.Versioning +{ + using Microsoft.AspNetCore.Mvc.Routing; + using Microsoft.AspNetCore.Routing; + using System; + + /// + /// Represents an API version aware URL helper. + /// + [CLSCompliant( false )] + public class ApiVersionUrlHelper : IUrlHelper + { + readonly IApiVersioningFeature feature; + + /// + /// Initializes a new instance of the class. + /// + /// The current action context. + /// The inner URL helper. + public ApiVersionUrlHelper( ActionContext actionContext, IUrlHelper url ) + { + ActionContext = actionContext ?? throw new ArgumentNullException( nameof( actionContext ) ); + Url = url; + feature = actionContext.HttpContext.Features.Get(); + } + + /// + /// Gets the inner URL helper. + /// + /// The inner URL helper. + protected IUrlHelper Url { get; } + + /// + /// Gets the name of the API version route parameter. + /// + /// The API version route parameter name. + protected string? RouteParameter => feature.RouteParameter; + + /// + /// Gets the API version value. + /// + /// The raw API version value. + protected string? ApiVersion => feature.RawRequestedApiVersion; + + /// + public ActionContext ActionContext { get; } + + /// + public virtual string Action( UrlActionContext actionContext ) + { + if ( actionContext == null ) + { + throw new ArgumentNullException( nameof( actionContext ) ); + } + + actionContext.Values = AddApiVersionRouteValueIfNecessary( actionContext.Values ); + return Url.Action( actionContext ); + } + + /// + public virtual string Content( string contentPath ) => Url.Content( contentPath ); + + /// +#pragma warning disable CA1054 // URI-like parameters should not be strings + public virtual bool IsLocalUrl( string url ) => Url.IsLocalUrl( url ); +#pragma warning restore CA1054 + + /// + public virtual string Link( string routeName, object values ) => + Url.Link( routeName, AddApiVersionRouteValueIfNecessary( values ) ); + + /// +#pragma warning disable CA1055 // URI-like return values should not be strings + public virtual string RouteUrl( UrlRouteContext routeContext ) +#pragma warning restore CA1055 + { + if ( routeContext == null ) + { + throw new ArgumentNullException( nameof( routeContext ) ); + } + + routeContext.Values = AddApiVersionRouteValueIfNecessary( routeContext.Values ); + return Url.RouteUrl( routeContext ); + } + + object AddApiVersionRouteValueIfNecessary( object current ) + { + var key = RouteParameter; + + if ( string.IsNullOrEmpty( key ) ) + { + return current; + } + + var value = ApiVersion; + + if ( string.IsNullOrEmpty( value ) ) + { + return current; + } + + if ( !( current is RouteValueDictionary values ) ) + { + values = current == null ? new RouteValueDictionary() : new RouteValueDictionary( current ); + } + + if ( !values.ContainsKey( key ) ) + { + values.Add( key, value ); + } + + return values; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionUrlHelperFactory.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionUrlHelperFactory.cs new file mode 100644 index 00000000..f15df84f --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionUrlHelperFactory.cs @@ -0,0 +1,40 @@ +namespace Microsoft.AspNetCore.Mvc.Versioning +{ + using Microsoft.AspNetCore.Mvc.Routing; + using System; + + /// + /// Represents an API version aware URL helper factory. + /// + [CLSCompliant( false )] + public class ApiVersionUrlHelperFactory : IUrlHelperFactory + { + /// + /// Initializes a new instance of the class. + /// + /// The inner URL helper factory. + public ApiVersionUrlHelperFactory( IUrlHelperFactory factory ) => Factory = factory; + + /// + /// Gets the inner factory used to create URL helpers. + /// + /// The inner URL helper factory. + protected IUrlHelperFactory Factory { get; } + + /// + public virtual IUrlHelper GetUrlHelper( ActionContext context ) + { + if ( context == null ) + { + throw new ArgumentNullException( nameof( context ) ); + } + + var urlHelper = new ApiVersionUrlHelper( context, Factory.GetUrlHelper( context ) ); + + // REF: https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/src/Routing/UrlHelperFactory.cs#L44 + context.HttpContext.Items[typeof( IUrlHelper )] = urlHelper; + + return urlHelper; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersioningFeature.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersioningFeature.cs index e13da294..0794e040 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersioningFeature.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersioningFeature.cs @@ -21,10 +21,10 @@ public sealed class ApiVersioningFeature : IApiVersioningFeature [CLSCompliant( false )] public ApiVersioningFeature( HttpContext context ) => this.context = context; - /// - /// Gets or sets the raw, unparsed API version for the current request. - /// - /// The unparsed API version value for the current request. + /// + public string? RouteParameter { get; set; } + + /// public string? RawRequestedApiVersion { get @@ -40,12 +40,7 @@ public string? RawRequestedApiVersion set => rawApiVersion = value; } - /// - /// Gets or sets the API version for the current request. - /// - /// The current API version for the current request. - /// If an API version was not provided for the current request or the value - /// provided is invalid, this property will return null. + /// public ApiVersion? RequestedApiVersion { get @@ -62,10 +57,7 @@ public ApiVersion? RequestedApiVersion set => apiVersion = value; } - /// - /// Gets the action selection result associated with the current request. - /// - /// The action selection result associated with the current request. + /// public ActionSelectionResult SelectionResult { get; } = new ActionSelectionResult(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/IApiVersioningFeature.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/IApiVersioningFeature.cs index 88db6883..035dd50a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/IApiVersioningFeature.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/IApiVersioningFeature.cs @@ -1,5 +1,6 @@ namespace Microsoft.AspNetCore.Mvc.Versioning { + using Microsoft.AspNetCore.Mvc.Routing; using System; /// @@ -8,6 +9,14 @@ [CLSCompliant( false )] public interface IApiVersioningFeature { + /// + /// Gets or sets the name of the route parameter containing the API Version value. + /// + /// The name of the API version route parameter or null. + /// This property will be null unless versioning by URL segment and the incoming request + /// matches the API version route constraint. + string? RouteParameter { get; set; } + /// /// Gets or sets the raw, unparsed API version for the current request. /// diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensionsTest.cs index ac9a729d..830833d9 100644 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensionsTest.cs @@ -21,6 +21,8 @@ public void add_api_versioning_should_configure_mvc_with_default_options() // arrange var services = new ServiceCollection(); + services.AddMvcCore(); + // act services.AddApiVersioning(); @@ -47,6 +49,8 @@ public void add_api_versioning_should_configure_mvc_with_custom_options() // arrange var services = new ServiceCollection(); + services.AddMvcCore(); + // act services.AddApiVersioning( options => diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Routing/ApiVersionRouteConstraintTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Routing/ApiVersionRouteConstraintTest.cs index 59ced8d5..ec2fffe5 100644 --- a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Routing/ApiVersionRouteConstraintTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Routing/ApiVersionRouteConstraintTest.cs @@ -3,11 +3,11 @@ using AspNetCore.Routing; using Builder; using Extensions.DependencyInjection; - using Extensions.ObjectPool; using FluentAssertions; using Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.Extensions.ObjectPool; using Moq; using System; using System.Collections.Generic; @@ -104,24 +104,39 @@ public void match_should_return_true_when_matched() public void url_helper_should_create_route_link_with_api_version_constraint() { // arrange - var services = CreateServices().AddApiVersioning(); - var provider = services.BuildServiceProvider(); - var httpContext = new DefaultHttpContext() { RequestServices = provider }; - var routeBuilder = CreateRouteBuilder( provider ); - var actionContext = new ActionContext() { HttpContext = httpContext }; + var urlHelper = NewUrlHelper(controller: "Store", action: "Buy", version: "1" ); - httpContext.Features.Set( new ApiVersioningFeature( httpContext ) ); - routeBuilder.MapRoute( "default", "v{version:apiVersion}/{controller}/{action}" ); - actionContext.RouteData = new RouteData(); - actionContext.RouteData.Routers.Add( routeBuilder.Build() ); + // act + var url = urlHelper.Link( "default", default ); + + // assert + url.Should().Be( "/v1/Store/Buy" ); + } + + [Fact] + public void url_helper_should_create_route_url_with_api_version_constraint() + { + // arrange + var urlHelper = NewUrlHelper( controller: "Movie", action: "Rate", version: "2" ); + + // act + var url = urlHelper.RouteUrl( "default" ); + + // assert + url.Should().Be( "/v2/Movie/Rate" ); + } - var urlHelper = new UrlHelper( actionContext ); + [Fact] + public void url_helper_should_create_action_with_api_version_constraint() + { + // arrange + var urlHelper = NewUrlHelper( controller: "Order", action: "Place", version: "1.1" ); // act - var url = urlHelper.Link( "default", new { version = "1", controller = "Store", action = "Buy" } ); + var url = urlHelper.Action( action: "Place", controller: "Order" ); // assert - url.Should().Be( "/v1/Store/Buy" ); + url.Should().Be( "/v1.1/Order/Place" ); } class PassThroughRouter : IRouter @@ -147,7 +162,14 @@ static HttpContext NewHttpContext() return httpContext.Object; } - static ServiceCollection CreateServices() + static IRouteBuilder CreateRouteBuilder( IServiceProvider services ) + { + var app = new Mock(); + app.SetupGet( a => a.ApplicationServices ).Returns( services ); + return new RouteBuilder( app.Object ) { DefaultHandler = new PassThroughRouter() }; + } + + static IUrlHelper NewUrlHelper(string controller, string action, string version) { var services = new ServiceCollection(); @@ -156,15 +178,39 @@ static ServiceCollection CreateServices() services.AddRouting(); services.AddSingleton() .AddSingleton( UrlEncoder.Default ); + services.AddMvcCore(); + services.AddApiVersioning(); - return services; - } + var provider = services.BuildServiceProvider(); + var httpContext = new DefaultHttpContext() { RequestServices = provider }; + var routeBuilder = CreateRouteBuilder( provider ); + var actionContext = new ActionContext() { HttpContext = httpContext }; + var constraint = new ApiVersionRouteConstraint(); - static IRouteBuilder CreateRouteBuilder( IServiceProvider services ) - { - var app = new Mock(); - app.SetupGet( a => a.ApplicationServices ).Returns( services ); - return new RouteBuilder( app.Object ) { DefaultHandler = new PassThroughRouter() }; + httpContext.Features.Set( new ApiVersioningFeature( httpContext ) ); + routeBuilder.MapRoute( "default", "v{version:apiVersion}/{controller}/{action}" ); + + var router = routeBuilder.Build(); + + actionContext.RouteData = new RouteData() + { + Values = + { + [nameof(controller)] = controller, + [nameof(action)] = action, + [nameof(version)] = version, + }, + Routers = + { + router, + } + }; + actionContext.RouteData.Routers.Add( router ); + constraint.Match( httpContext, router, nameof( version ), actionContext.RouteData.Values, IncomingRequest ); + + var factory = provider.GetRequiredService(); + + return factory.GetUrlHelper( actionContext ); } } } \ No newline at end of file From 0eabd083ddcecbe2572c1b4915d4b630c77cecc1 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 4 Oct 2020 14:24:05 -0700 Subject: [PATCH 07/13] Complete OData refactor with support for Endpoint Routing. Resolves #608, #616, #647 --- .../AspNet.OData/DefaultModelTypeBuilder.cs | 22 +- .../AspNet.OData/EdmTypeKey.cs | 3 + .../AspNet.OData/Routing/ODataRouteBuilder.cs | 2 +- .../Routing/ODataRouteBuilderContext.cs | 128 ++- .../Builder/DelegatingModelConfiguration.cs | 6 +- .../Builder/IModelConfiguration.cs | 3 +- .../Builder/VersionedODataModelBuilder.cs | 34 +- .../ODataConventionConfigurationContext.cs | 15 + .../ODataPathTemplateHandlerExtensions.cs | 27 + .../VersionedAttributeRoutingConvention.cs | 29 +- .../VersionedODataRoutingConventions.cs | 25 +- src/Common.OData/Common.OData.projitems | 3 + .../OData.Edm/EdmModelSelector.cs | 97 +++ .../OData.Edm/IEdmModelSelector.cs | 43 + src/Common/ApiVersion.cs | 50 +- .../Routing/ODataRouteBuilderContext.cs | 34 +- ...AspNet.OData.Versioning.ApiExplorer.csproj | 7 +- .../HttpControllerDescriptorExtensions.cs | 23 - .../Description/ApiDescriptionExtensions.cs | 16 + .../HttpConfigurationExtensions.cs | 1 - .../Web.Http.Description/ODataApiExplorer.cs | 8 +- .../Builder/VersionedODataModelBuilder.cs | 19 +- .../ODataConventionConfigurationContext.cs | 11 +- .../VersionedAttributeRoutingConvention.cs | 186 ++-- .../VersionedMetadataRoutingConvention.cs | 27 +- .../VersionedODataRoutingConventions.cs | 21 + .../VersionedMetadataController.cs | 0 .../LocalSR.Designer.cs | 4 +- .../Microsoft.AspNet.OData.Versioning.csproj | 19 +- .../ODataApiVersionRequestProperties.cs | 18 - .../OData/IContainerBuilderExtensions.cs | 58 ++ .../UnversionedODataPathRouteConstraint.cs | 90 -- .../VersionedODataPathRouteConstraint.cs | 103 --- .../HttpConfigurationExtensions.cs | 810 +++--------------- .../HttpRequestMessageExtensions.cs | 31 +- .../IContainerBuilderExtensions.cs | 28 - .../Description/VersionedApiExplorer.cs | 15 +- ...spNet.WebApi.Versioning.ApiExplorer.csproj | 4 +- .../HttpActionDescriptorExtensions.cs | 24 + .../Controllers/ApiVersionActionSelector.cs | 2 +- .../Microsoft.AspNet.WebApi.Versioning.csproj | 4 +- .../ReleaseNotes.txt | 2 +- .../HttpActionDescriptorExtensions.cs | 3 +- .../HttpControllerDescriptorExtensions.cs | 50 +- .../Routing/ApiVersionMatcherPolicy.cs | 19 +- .../Routing/CatchAllRouteHandler.cs | 2 +- .../Routing/ClientErrorBuilder.cs | 81 +- .../Routing/DefaultApiVersionRoutePolicy.cs | 2 +- .../Routing/RouteContextExtensions.cs | 27 + .../Versioning/ApiVersionActionSelector.cs | 2 +- .../Routing/ODataRouteBuilderContext.cs | 29 +- .../ApiExplorer}/ApiDescriptionExtensions.cs | 21 + .../ApiExplorer}/ApiParameterContext.cs | 0 .../ApiParameterDescriptionContext.cs | 0 .../ApiExplorer}/ModelMetadataExtensions.cs | 0 .../ODataApiDescriptionProvider.cs | 128 +-- .../ODataQueryOptionModelMetadata.cs | 0 .../ApiExplorer}/PseudoModelBindingVisitor.cs | 0 .../ApiExplorer}/SubstitutedModelMetadata.cs | 0 ...etCore.OData.Versioning.ApiExplorer.csproj | 5 +- .../IEndpointRouteBuilderExtensions.cs | 219 +++++ .../Extensions/IRouteBuilderExtensions.cs | 605 +++---------- .../Extensions/ServiceProviderExtensions.cs | 29 + .../Routing/ActionParameterContext.cs | 9 +- .../Routing/IODataRouteCollection.cs | 32 - .../Routing/ImplicitHttpMethodConvention.cs | 4 +- .../Routing/ODataAttributeRouteInfo.cs | 6 + .../ODataConventionConfigurationContext.cs | 25 +- .../ODataRouteBindingInfoConvention.cs | 145 +++- .../Routing/ODataRouteBuilderContext.Core.cs | 31 +- .../Routing/ODataRouteCollectionProvider.cs | 39 +- .../AspNet.OData/Routing/ODataRouteMapping.cs | 40 +- .../UnversionedODataPathRouteConstraint.cs | 90 -- .../VersionedAttributeRoutingConvention.cs | 155 ++-- .../VersionedMetadataRoutingConvention.cs | 70 +- .../VersionedODataPathRouteConstraint.cs | 103 --- .../VersionedODataRoutingConventions.cs | 23 + .../ActionDescriptorExtensions.cs | 75 ++ .../AspNetCore.Mvc/HttpContextExtensions.cs | 36 - .../ODataActionDescriptorProvider.cs | 29 +- .../AspNetCore.Mvc/Routing/ActionCandidate.cs | 47 - .../Versioning/IODataVersioningFeature.cs | 18 - .../MetadataControllerConvention.cs | 51 +- .../ODataApiVersionActionSelector.cs | 349 ++++++-- .../Versioning/ODataVersioningFeature.cs | 10 - .../IODataBuilderExtensions.cs | 4 +- .../RaiseVersionedODataRoutesMapped.cs | 28 + ...crosoft.AspNetCore.OData.Versioning.csproj | 6 +- .../OData/IContainerBuilderExtensions.cs | 67 ++ test/Acceptance.Test.Shared/AcceptanceTest.cs | 2 +- .../Description/TestConfigurations.cs | 8 +- .../Configuration/OrderModelConfiguration.cs | 2 +- .../Configuration/PersonModelConfiguration.cs | 2 +- .../Builder/VersionedODataModelBuilderTest.cs | 57 +- .../HttpConfigurationExtensionsTest.cs | 106 +-- ...VersionedAttributeRoutingConventionTest.cs | 22 +- .../VersionedMetadataRoutingConventionTest.cs | 86 +- .../VersionedODataPathRouteConstraintTest.cs | 136 --- .../VersionedMetadataControllerTest.cs | 22 +- .../HttpServerFixture.cs | 3 +- .../OData/Advanced/AdvancedFixture.cs | 12 +- .../Advanced/Controllers/Orders2Controller.cs | 4 +- .../Advanced/Controllers/People2Controller.cs | 4 +- .../Advanced/Controllers/PeopleController.cs | 8 +- .../when orders is v2.cs | 2 +- .../when people is any version.cs | 2 +- .../when people is v1.cs | 6 +- .../when people is v2.cs | 6 +- .../when people is v3.cs | 4 +- .../OData/Basic/BasicFixture.cs | 6 +- .../Basic/Controllers/CustomersController.cs | 10 +- .../Basic/Controllers/OrdersController.cs | 4 +- .../Basic/Controllers/People2Controller.cs | 4 +- .../Basic/Controllers/PeopleController.cs | 8 +- ...a query string and split into two types.cs | 14 +- .../when using a query string.cs | 2 +- ... a url segment and split into two types.cs | 14 +- .../when using a url segment.cs | 2 +- .../when using an action.cs | 17 +- .../CustomerModelConfiguration.cs | 2 +- .../Configuration/OrderModelConfiguration.cs | 7 +- .../Configuration/PersonModelConfiguration.cs | 2 +- .../Controllers/CustomersController.cs | 15 +- .../Controllers/OrdersController.cs | 6 +- .../Controllers/People2Controller.cs | 6 +- .../Controllers/PeopleController.cs | 15 +- .../OData/Conventions/ConventionsFixture.cs | 6 +- ...a query string and split into two types.cs | 14 +- .../when using a query string.cs | 2 +- ... a url segment and split into two types.cs | 14 +- .../when using a url segment.cs | 2 +- .../when using an action.cs | 17 +- .../OData/ODataFixture.cs | 7 - .../TraceWriter.cs | 23 + .../HttpServerFixture.cs | 6 - .../Mvc/Basic/UIFixture.cs | 2 - .../when no version is specified.cs | 2 + ...ccessing a view using attribute routing.cs | 2 + ...cessing a view using convention routing.cs | 2 + .../when two route templates overlap.cs | 2 + ...a query string and split into two types.cs | 9 +- .../when using a url segment.cs | 9 +- .../when using an action.cs | 2 + .../ByNamespace/AgreementsEndpointFixture.cs | 2 - .../when using a query string.cs | 2 + .../when using a url segment.cs | 2 + .../when using an action.cs | 2 + ...a query string and split into two types.cs | 2 + .../when using a url segment.cs | 4 +- .../when using an action.cs | 2 + .../when using media type negotiation.cs | 2 + .../OData/Advanced/AdvancedEndpointFixture.cs | 46 + .../OData/Advanced/AdvancedFixture.cs | 7 +- .../AdvancedODataEndpointCollection.cs | 10 + .../{ => Classic}/Orders2Controller.cs | 12 +- .../Controllers/Classic/Orders3Controller.cs | 18 + .../Controllers/Classic/OrdersController.cs | 16 + .../Controllers/Classic}/People2Controller.cs | 6 +- .../Controllers/Classic}/PeopleController.cs | 10 +- .../Controllers/Endpoint/Orders2Controller.cs | 18 + .../Controllers/Endpoint/Orders3Controller.cs | 18 + .../Controllers/Endpoint/OrdersController.cs | 16 + .../Controllers/Endpoint/People2Controller.cs | 18 + .../Controllers/Endpoint/PeopleController.cs | 33 + .../Advanced/Controllers/Orders3Controller.cs | 17 - .../Advanced/Controllers/OrdersController.cs | 16 - .../when orders is v1.cs | 10 + .../when orders is v3.cs | 10 + .../when orders is v2.cs | 16 +- .../when people is any version.cs | 12 +- .../when people is v1.cs | 16 +- .../when people is v2.cs | 16 +- .../when people is v3.cs | 14 +- .../OData/Basic/BasicAcceptanceTest.cs | 3 +- .../OData/Basic/BasicEndpointFixture.cs | 35 + .../OData/Basic/BasicFixture.cs | 6 +- .../Basic/BasicODataEndpointCollection.cs | 10 + .../{ => Classic}/CustomersController.cs | 14 +- .../{ => Classic}/OrdersController.cs | 6 +- .../Controllers/Classic}/People2Controller.cs | 6 +- .../Controllers/Classic}/PeopleController.cs | 10 +- .../Endpoint/CustomersController.cs | 39 + .../Controllers/Endpoint/OrdersController.cs | 20 + .../Controllers/Endpoint/People2Controller.cs | 21 + .../Controllers/Endpoint/PeopleController.cs | 37 + ...a query string and split into two types.cs | 26 +- .../when using a query string.cs | 14 +- ... a url segment and split into two types.cs | 26 +- .../when using a url segment.cs | 14 +- .../when using an action.cs | 28 +- .../CustomerModelConfiguration.cs | 2 +- .../Configuration/OrderModelConfiguration.cs | 7 +- .../Configuration/PersonModelConfiguration.cs | 2 +- .../Controllers/CustomersController.cs | 13 +- .../Controllers/OrdersController.cs | 6 +- .../Controllers/People2Controller.cs | 6 +- .../Controllers/PeopleController.cs | 9 +- .../Conventions/ConventionsEndpointFixture.cs | 52 ++ .../OData/Conventions/ConventionsFixture.cs | 4 +- .../ConventionsODataEndpointCollection.cs | 10 + ...a query string and split into two types.cs | 24 +- .../when using a query string.cs | 12 +- ... a url segment and split into two types.cs | 24 +- .../when using a url segment.cs | 12 +- .../when using an action.cs | 26 +- .../Configuration/AllConfigurations.cs | 8 +- .../Configuration/OrderModelConfiguration.cs | 8 +- .../Configuration/PersonModelConfiguration.cs | 8 +- .../Configuration/ProductConfiguration.cs | 8 +- .../Configuration/SupplierConfiguration.cs | 8 +- .../Simulators/V1/OrdersController.cs | 2 +- .../Simulators/V2/OrdersController.cs | 6 +- .../Simulators/V3/OrdersController.cs | 8 +- .../ODataApiDescriptionProviderTest.cs | 20 +- .../ModelConfigurationFeatureProviderTest.cs | 10 +- .../Builder/VersionedODataModelBuilderTest.cs | 52 +- .../Extensions/IRouteBuilderExtensionsTest.cs | 84 +- ...VersionedAttributeRoutingConventionTest.cs | 31 +- .../VersionedMetadataRoutingConventionTest.cs | 31 +- .../VersionedODataPathRouteConstraintTest.cs | 212 ----- .../VersionedMetadataControllerTest.cs | 16 +- .../ODataApiVersionActionSelectorTest.cs | 10 +- .../TestODataApiVersionActionSelector.cs | 18 +- .../AspNetCore.Mvc/Versioning/WebServer.cs | 2 +- .../Simulators/TestsController.cs | 2 +- .../Simulators/TestsController2.cs | 2 +- .../Simulators/TestsController3.cs | 2 +- .../Simulators/VersionNeutralController.cs | 4 +- .../TestModelConfiguration.cs | 2 +- 229 files changed, 3597 insertions(+), 3369 deletions(-) create mode 100644 src/Common.OData/AspNet.OData/Routing/ODataPathTemplateHandlerExtensions.cs create mode 100644 src/Common.OData/OData.Edm/EdmModelSelector.cs create mode 100644 src/Common.OData/OData.Edm/IEdmModelSelector.cs delete mode 100644 src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpControllerDescriptorExtensions.cs rename src/Microsoft.AspNet.OData.Versioning/{ => AspNet.OData}/Builder/VersionedODataModelBuilder.cs (87%) rename src/Microsoft.AspNet.OData.Versioning/{ => AspNet.OData}/Routing/ODataConventionConfigurationContext.cs (76%) rename src/Microsoft.AspNet.OData.Versioning/{ => AspNet.OData}/Routing/VersionedAttributeRoutingConvention.cs (61%) rename src/Microsoft.AspNet.OData.Versioning/{ => AspNet.OData}/Routing/VersionedMetadataRoutingConvention.cs (66%) create mode 100644 src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/VersionedODataRoutingConventions.cs rename src/Microsoft.AspNet.OData.Versioning/{ => AspNet.OData}/VersionedMetadataController.cs (100%) delete mode 100644 src/Microsoft.AspNet.OData.Versioning/Microsoft.Web.Http.Versioning/ODataApiVersionRequestProperties.cs create mode 100644 src/Microsoft.AspNet.OData.Versioning/OData/IContainerBuilderExtensions.cs delete mode 100644 src/Microsoft.AspNet.OData.Versioning/Routing/UnversionedODataPathRouteConstraint.cs delete mode 100644 src/Microsoft.AspNet.OData.Versioning/Routing/VersionedODataPathRouteConstraint.cs delete mode 100644 src/Microsoft.AspNet.OData.Versioning/System.Web.Http/IContainerBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpActionDescriptorExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Versioning/Routing/RouteContextExtensions.cs rename src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/{AspNetCore.Mvc.ApiExplorer => AspNetCore.Mvc/ApiExplorer}/ApiDescriptionExtensions.cs (79%) rename src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/{AspNetCore.Mvc.ApiExplorer => AspNetCore.Mvc/ApiExplorer}/ApiParameterContext.cs (100%) rename src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/{AspNetCore.Mvc.ApiExplorer => AspNetCore.Mvc/ApiExplorer}/ApiParameterDescriptionContext.cs (100%) rename src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/{AspNetCore.Mvc.ApiExplorer => AspNetCore.Mvc/ApiExplorer}/ModelMetadataExtensions.cs (100%) rename src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/{AspNetCore.Mvc.ApiExplorer => AspNetCore.Mvc/ApiExplorer}/ODataApiDescriptionProvider.cs (89%) rename src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/{AspNetCore.Mvc.ApiExplorer => AspNetCore.Mvc/ApiExplorer}/ODataQueryOptionModelMetadata.cs (100%) rename src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/{AspNetCore.Mvc.ApiExplorer => AspNetCore.Mvc/ApiExplorer}/PseudoModelBindingVisitor.cs (100%) rename src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/{AspNetCore.Mvc.ApiExplorer => AspNetCore.Mvc/ApiExplorer}/SubstitutedModelMetadata.cs (100%) create mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IEndpointRouteBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/ServiceProviderExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/UnversionedODataPathRouteConstraint.cs delete mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedODataPathRouteConstraint.cs create mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedODataRoutingConventions.cs create mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Abstractions/ActionDescriptorExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/HttpContextExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Routing/ActionCandidate.cs delete mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/IODataVersioningFeature.cs delete mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/ODataVersioningFeature.cs create mode 100644 src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/RaiseVersionedODataRoutesMapped.cs create mode 100644 src/Microsoft.AspNetCore.OData.Versioning/OData/IContainerBuilderExtensions.cs delete mode 100644 test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedODataPathRouteConstraintTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedEndpointFixture.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedODataEndpointCollection.cs rename test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/{ => Classic}/Orders2Controller.cs (66%) create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/Orders3Controller.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/OrdersController.cs rename test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/{Basic/Controllers => Advanced/Controllers/Classic}/People2Controller.cs (79%) rename test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/{Basic/Controllers => Advanced/Controllers/Classic}/PeopleController.cs (76%) create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/Orders2Controller.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/Orders3Controller.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/OrdersController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/People2Controller.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/PeopleController.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Orders3Controller.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/OrdersController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicEndpointFixture.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicODataEndpointCollection.cs rename test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/{ => Classic}/CustomersController.cs (66%) rename test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/{ => Classic}/OrdersController.cs (74%) rename test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/{Advanced/Controllers => Basic/Controllers/Classic}/People2Controller.cs (79%) rename test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/{Advanced/Controllers => Basic/Controllers/Classic}/PeopleController.cs (76%) create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/CustomersController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/OrdersController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/People2Controller.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/PeopleController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsEndpointFixture.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsODataEndpointCollection.cs delete mode 100644 test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedODataPathRouteConstraintTest.cs diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs index e06d9219..b6531e64 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs @@ -28,7 +28,10 @@ public sealed class DefaultModelTypeBuilder : IModelTypeBuilder { static readonly Type IEnumerableOfT = typeof( IEnumerable<> ); readonly ConcurrentDictionary modules = new ConcurrentDictionary(); - readonly ConcurrentDictionary> generatedEdmTypesPerVersion = new ConcurrentDictionary>(); + readonly ConcurrentDictionary> generatedEdmTypesPerVersion = + new ConcurrentDictionary>(); + readonly ConcurrentDictionary> generatedActionParamsPerVersion = + new ConcurrentDictionary>(); /// public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion, IEdmModel edmModel ) @@ -47,12 +50,19 @@ public Type NewActionParameters( IServiceProvider services, IEdmAction action, A throw new ArgumentNullException( nameof( action ) ); } - var name = controllerName + "." + action.FullName() + "Parameters"; - var properties = action.Parameters.Where( p => p.Name != "bindingParameter" ).Select( p => new ClassProperty( services, p, this ) ); - var signature = new ClassSignature( name, properties, apiVersion ); - var moduleBuilder = modules.GetOrAdd( apiVersion, CreateModuleForApiVersion ); + var paramTypes = generatedActionParamsPerVersion.GetOrAdd( apiVersion, _ => new ConcurrentDictionary() ); + var fullTypeName = $"{controllerName}.{action.Namespace}.{controllerName}{action.Name}Parameters"; + var key = new EdmTypeKey( fullTypeName, apiVersion ); + var type = paramTypes.GetOrAdd( key, _ => + { + var properties = action.Parameters.Where( p => p.Name != "bindingParameter" ).Select( p => new ClassProperty( services, p, this ) ); + var signature = new ClassSignature( fullTypeName, properties, apiVersion ); + var moduleBuilder = modules.GetOrAdd( apiVersion, CreateModuleForApiVersion ); + + return CreateTypeInfoFromSignature( moduleBuilder, signature ); + } ); - return CreateTypeInfoFromSignature( moduleBuilder, signature ); + return type; } IDictionary GenerateTypesForEdmModel( IEdmModel model, ApiVersion apiVersion ) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/EdmTypeKey.cs b/src/Common.OData.ApiExplorer/AspNet.OData/EdmTypeKey.cs index 3e5910d0..d647f581 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/EdmTypeKey.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/EdmTypeKey.cs @@ -19,6 +19,9 @@ internal EdmTypeKey( IEdmStructuredType type, ApiVersion apiVersion ) => internal EdmTypeKey( IEdmTypeReference type, ApiVersion apiVersion ) => hashCode = ComputeHash( type.FullName(), apiVersion ); + internal EdmTypeKey( string fullTypeName, ApiVersion apiVersion ) => + hashCode = ComputeHash( fullTypeName, apiVersion ); + public static bool operator ==( EdmTypeKey obj, EdmTypeKey other ) => obj.Equals( other ); public static bool operator !=( EdmTypeKey obj, EdmTypeKey other ) => !obj.Equals( other ); diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs index 23b26971..01d27d30 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs @@ -73,7 +73,7 @@ void BuildPath( StringBuilder builder ) void AppendRoutePrefix( IList segments ) { - var prefix = Context.Route.RoutePrefix?.Trim( '/' ); + var prefix = Context.RoutePrefix; if ( IsNullOrEmpty( prefix ) ) { diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs index 5fd96a1a..c87e03fc 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs @@ -17,7 +17,9 @@ #endif using System; using System.Collections.Generic; + using System.Linq; using System.Reflection; + using System.Text.RegularExpressions; #if WEBAPI using System.Web.Http.Description; using System.Web.Http.Dispatcher; @@ -25,6 +27,7 @@ #endif using static Microsoft.OData.ODataUrlKeyDelimiter; using static ODataRouteTemplateGenerationKind; + using static System.StringComparison; sealed partial class ODataRouteBuilderContext { @@ -52,7 +55,7 @@ sealed partial class ODataRouteBuilderContext internal string? RouteTemplate { get; } - internal ODataRoute Route { get; } + internal string? RoutePrefix { get; } internal ControllerActionDescriptor ActionDescriptor { get; } @@ -74,7 +77,11 @@ sealed partial class ODataRouteBuilderContext internal bool AllowUnqualifiedEnum => Services.GetRequiredService() is StringAsEnumResolver; - internal static ODataRouteActionType GetActionType( IEdmEntitySet entitySet, IEdmOperation operation ) + internal +#if !WEBAPI + static +#endif + ODataRouteActionType GetActionType( IEdmEntitySet? entitySet, IEdmOperation? operation, ControllerActionDescriptor action ) { if ( entitySet == null ) { @@ -91,7 +98,14 @@ internal static ODataRouteActionType GetActionType( IEdmEntitySet entitySet, IEd { if ( operation == null ) { - return ODataRouteActionType.EntitySet; + if ( IsActionOrFunction( entitySet, action.ActionName, GetHttpMethods( action ) ) ) + { + return ODataRouteActionType.Unknown; + } + else + { + return ODataRouteActionType.EntitySet; + } } else if ( operation.IsBound ) { @@ -105,5 +119,113 @@ internal static ODataRouteActionType GetActionType( IEdmEntitySet entitySet, IEd // Slash became the default 4/18/2018 // REF: https://github.com/OData/WebApi/pull/1393 static ODataUrlKeyDelimiter UrlKeyDelimiterOrDefault( ODataUrlKeyDelimiter? urlKeyDelimiter ) => urlKeyDelimiter ?? Slash; + + // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/ActionRoutingConvention.cs + // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/FunctionRoutingConvention.cs + // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs + // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntityRoutingConvention.cs + static bool IsActionOrFunction( IEdmEntitySet? entitySet, string actionName, IEnumerable methods ) + { + using var iterator = methods.GetEnumerator(); + + if ( !iterator.MoveNext() ) + { + return false; + } + + var method = iterator.Current; + + if ( iterator.MoveNext() ) + { + return false; + } + + const string ActionMethod = "Post"; + const string FunctionMethod = "Get"; + + if ( ActionMethod.Equals( method, OrdinalIgnoreCase ) && actionName != ActionMethod ) + { + if ( entitySet == null ) + { + return true; + } + + return actionName != ( ActionMethod + entitySet.Name ) && + actionName != ( ActionMethod + entitySet.EntityType().Name ) && + !actionName.StartsWith( "CreateRef", Ordinal ); + } + else if ( FunctionMethod.Equals( method, OrdinalIgnoreCase ) && actionName != FunctionMethod ) + { + if ( entitySet == null ) + { + // TODO: could be a singleton here + return true; + } + + if ( actionName == ( ActionMethod + entitySet.Name ) || + actionName.StartsWith( "GetRef", Ordinal ) ) + { + return false; + } + + var entity = entitySet.EntityType(); + + if ( actionName == ( ActionMethod + entity.Name ) ) + { + return false; + } + + foreach ( var property in entity.NavigationProperties() ) + { + if ( actionName.StartsWith( FunctionMethod + property.Name, OrdinalIgnoreCase ) ) + { + return false; + } + } + + return true; + } + + return false; + } + + IEdmOperation? ResolveOperation( IEdmEntityContainer container, string name ) + { + var import = container.FindOperationImports( name ).SingleOrDefault(); + + if ( import != null ) + { + return import.Operation; + } + + var qualifiedName = container.Namespace + "." + name; + + if ( EntitySet != null ) + { + var operation = EdmModel.FindBoundOperations( qualifiedName, EntitySet.EntityType() ).SingleOrDefault(); + + if ( operation != null ) + { + return operation; + } + } + + return EdmModel.FindDeclaredOperations( qualifiedName ).SingleOrDefault(); + } + + sealed class FixedEdmModelServiceProviderDecorator : IServiceProvider + { + readonly IServiceProvider decorated; + readonly IEdmModel edmModel; + + internal FixedEdmModelServiceProviderDecorator( IServiceProvider decorated, IEdmModel edmModel ) + { + this.decorated = decorated; + this.edmModel = edmModel; + } + + public object GetService( Type serviceType ) => + serviceType == typeof( IEdmModel ) ? edmModel : decorated.GetService( serviceType ); + } } } \ No newline at end of file diff --git a/src/Common.OData/AspNet.OData/Builder/DelegatingModelConfiguration.cs b/src/Common.OData/AspNet.OData/Builder/DelegatingModelConfiguration.cs index 8fdb2188..e1fcfef5 100644 --- a/src/Common.OData/AspNet.OData/Builder/DelegatingModelConfiguration.cs +++ b/src/Common.OData/AspNet.OData/Builder/DelegatingModelConfiguration.cs @@ -9,10 +9,10 @@ sealed class DelegatingModelConfiguration : IModelConfiguration { - readonly Action action; + readonly Action action; - internal DelegatingModelConfiguration( Action action ) => this.action = action; + internal DelegatingModelConfiguration( Action action ) => this.action = action; - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) => action( builder, apiVersion ); + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix ) => action( builder, apiVersion, routePrefix ); } } \ No newline at end of file diff --git a/src/Common.OData/AspNet.OData/Builder/IModelConfiguration.cs b/src/Common.OData/AspNet.OData/Builder/IModelConfiguration.cs index 0667c280..a8047040 100644 --- a/src/Common.OData/AspNet.OData/Builder/IModelConfiguration.cs +++ b/src/Common.OData/AspNet.OData/Builder/IModelConfiguration.cs @@ -20,6 +20,7 @@ public interface IModelConfiguration /// /// The builder used to apply configurations. /// The API version associated with the . - void Apply( ODataModelBuilder builder, ApiVersion apiVersion ); + /// The route prefix associated with the configuration, if any. + void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix ); } } \ No newline at end of file diff --git a/src/Common.OData/AspNet.OData/Builder/VersionedODataModelBuilder.cs b/src/Common.OData/AspNet.OData/Builder/VersionedODataModelBuilder.cs index 4b3986ce..580b67c5 100644 --- a/src/Common.OData/AspNet.OData/Builder/VersionedODataModelBuilder.cs +++ b/src/Common.OData/AspNet.OData/Builder/VersionedODataModelBuilder.cs @@ -3,6 +3,7 @@ #if !WEBAPI using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.OData; #endif using Microsoft.OData.Edm; #if WEBAPI @@ -11,6 +12,7 @@ #endif using System; using System.Collections.Generic; + using System.Linq; /// /// Represents a versioned variant of the . @@ -27,9 +29,9 @@ public partial class VersionedODataModelBuilder /// /// Gets or sets the default model configuration. /// - /// The method for the default model configuration. + /// The method for the default model configuration. /// The default value is null. - public Action? DefaultModelConfiguration { get; set; } + public Action? DefaultModelConfiguration { get; set; } /// /// Gets the list of model configurations associated with the builder. @@ -48,13 +50,20 @@ public partial class VersionedODataModelBuilder /// Builds and returns the sequence of EDM models based on the define model configurations. /// /// A sequence of EDM models. - public virtual IEnumerable GetEdmModels() + public IEnumerable GetEdmModels() => GetEdmModels( default ); + + /// + /// Builds and returns the sequence of EDM models based on the define model configurations. + /// + /// The route prefix associated with the configuration, if any. + /// A sequence of EDM models. + public virtual IEnumerable GetEdmModels( string? routePrefix ) { var apiVersions = GetApiVersions(); var configurations = GetMergedConfigurations(); var models = new List(); - BuildModelPerApiVersion( apiVersions, configurations, models ); + BuildModelPerApiVersion( apiVersions, configurations, models, routePrefix ); return models; } @@ -76,7 +85,11 @@ IList GetMergedConfigurations() return configurations; } - void BuildModelPerApiVersion( IReadOnlyList apiVersions, IList configurations, ICollection models ) + void BuildModelPerApiVersion( + IReadOnlyList apiVersions, + IList configurations, + ICollection models, + string? routePrefix ) { for ( var i = 0; i < apiVersions.Count; i++ ) { @@ -85,10 +98,19 @@ void BuildModelPerApiVersion( IReadOnlyList apiVersions, IList RoutingConventions { get; } + + /// + /// Gets the associate service provider. + /// + /// The associated service provider. + public IServiceProvider ServiceProvider { get; } + + sealed class No : IServiceProvider + { + No() { } + + internal static IServiceProvider ServiceProvider { get; } = new No(); + + public object GetService( Type serviceType ) => default!; + } } } \ No newline at end of file diff --git a/src/Common.OData/AspNet.OData/Routing/ODataPathTemplateHandlerExtensions.cs b/src/Common.OData/AspNet.OData/Routing/ODataPathTemplateHandlerExtensions.cs new file mode 100644 index 00000000..7ef0d5d9 --- /dev/null +++ b/src/Common.OData/AspNet.OData/Routing/ODataPathTemplateHandlerExtensions.cs @@ -0,0 +1,27 @@ +namespace Microsoft.AspNet.OData.Routing +{ + using Microsoft.AspNet.OData.Routing.Template; + using Microsoft.OData; + using System; + + static class ODataPathTemplateHandlerExtensions + { + internal static ODataPathTemplate? SafeParseTemplate( + this IODataPathTemplateHandler handler, + string pathTemplate, + IServiceProvider serviceProvider ) + { + try + { + return handler.ParseTemplate( pathTemplate, serviceProvider ); + } + catch ( ODataException ) + { + // this 'should' mean the controller does not map to the current edm model. there's no way to know this without + // forcing a developer to explicitly map it. while it could be a mistake, simply yield null. this results in the + // template being skipped and will ultimately result in a 4xx if requested, which is acceptable. + return default; + } + } + } +} \ No newline at end of file diff --git a/src/Common.OData/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs b/src/Common.OData/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs index 1b8e772b..c2a0db8c 100644 --- a/src/Common.OData/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs +++ b/src/Common.OData/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs @@ -6,10 +6,12 @@ using Microsoft.AspNetCore.Mvc.Controllers; #endif using Microsoft.OData; + using Microsoft.OData.UriParser; #if WEBAPI using Microsoft.Web.Http; #endif using System; + using System.Collections.Concurrent; using System.Collections.Generic; using static System.StringComparison; #if WEBAPI @@ -21,20 +23,20 @@ /// public partial class VersionedAttributeRoutingConvention { - readonly string routeName; - IDictionary? attributeMappings; + readonly ConcurrentDictionary> attributeMappingsPerApiVersion = + new ConcurrentDictionary>(); /// - /// Gets the to be used for parsing the route templates. + /// Gets the name of the route associated the routing convention. /// - /// The to be used for parsing the route templates. - public IODataPathTemplateHandler ODataPathTemplateHandler { get; } + /// The name of the associated route. + public string RouteName { get; } /// - /// Gets the API version associated with the route convention. + /// Gets the to be used for parsing the route templates. /// - /// The associated API version. - public ApiVersion ApiVersion { get; } + /// The to be used for parsing the route templates. + public IODataPathTemplateHandler ODataPathTemplateHandler { get; } static IEnumerable GetODataRoutePrefixes( IEnumerable prefixAttributes, string controllerName ) { @@ -79,7 +81,7 @@ static bool IsODataRouteParameter( KeyValuePair routeDatum ) return routeDatum.Key.StartsWith( ParameterValuePrefix, Ordinal ) && routeDatum.Value?.GetType().Name == ODataParameterValue; } - ODataPathTemplate GetODataPathTemplate( string prefix, string pathTemplate, IServiceProvider serviceProvider, string controllerName, string actionName ) + ODataPathTemplate? GetODataPathTemplate( string prefix, string pathTemplate, IServiceProvider serviceProvider ) { if ( prefix != null && !pathTemplate.StartsWith( "/", Ordinal ) ) { @@ -102,14 +104,7 @@ ODataPathTemplate GetODataPathTemplate( string prefix, string pathTemplate, ISer pathTemplate = pathTemplate.Substring( 1 ); } - try - { - return ODataPathTemplateHandler.ParseTemplate( pathTemplate, serviceProvider ); - } - catch ( ODataException e ) - { - throw new InvalidOperationException( SR.InvalidODataRouteOnAction.FormatDefault( pathTemplate, actionName, controllerName, e.Message ) ); - } + return ODataPathTemplateHandler.SafeParseTemplate( pathTemplate, serviceProvider ); } } } \ No newline at end of file diff --git a/src/Common.OData/AspNet.OData/Routing/VersionedODataRoutingConventions.cs b/src/Common.OData/AspNet.OData/Routing/VersionedODataRoutingConventions.cs index 6298f61e..544c3a47 100644 --- a/src/Common.OData/AspNet.OData/Routing/VersionedODataRoutingConventions.cs +++ b/src/Common.OData/AspNet.OData/Routing/VersionedODataRoutingConventions.cs @@ -7,10 +7,7 @@ /// /// Provides utility functions to create OData routing conventions with support for API versioning. /// -#if !WEBAPI - [CLSCompliant( false )] -#endif - public static class VersionedODataRoutingConventions + public static partial class VersionedODataRoutingConventions { /// /// Creates a mutable list of the default OData routing conventions with support for API versioning. @@ -34,8 +31,11 @@ public static IList AddOrUpdate( IList EnsureConventions( IList conventions ) + static IList EnsureConventions( + IList conventions, + VersionedAttributeRoutingConvention? attributeRoutingConvention = default ) { + var hasVersionedAttributeConvention = false; var hasVersionedMetadataConvention = false; for ( var i = conventions.Count - 1; i >= 0; i-- ) @@ -44,7 +44,15 @@ static IList EnsureConventions( IList EnsureConventions( IList + + + $([System.IO.Path]::GetFileNameWithoutExtension('%(Filename)')).resx True diff --git a/src/Common.OData/OData.Edm/EdmModelSelector.cs b/src/Common.OData/OData.Edm/EdmModelSelector.cs new file mode 100644 index 00000000..a9adc939 --- /dev/null +++ b/src/Common.OData/OData.Edm/EdmModelSelector.cs @@ -0,0 +1,97 @@ +namespace Microsoft.OData.Edm +{ +#if !WEBAPI + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; +#endif + using Microsoft.Extensions.DependencyInjection; +#if WEBAPI + using Microsoft.Web.Http; +#endif + using System; + using System.Collections.Generic; +#if WEBAPI + using System.Net.Http; + using System.Web.Http; +#endif + + /// + /// Represents an EDM model selector. + /// +#if WEBAPI + [CLSCompliant( false )] +#endif + public class EdmModelSelector : IEdmModelSelector + { + readonly ApiVersion maxVersion; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence of models to select from. + /// The default API version. + public EdmModelSelector( IEnumerable models, ApiVersion defaultApiVersion ) + { + var versions = new List(); + var collection = new Dictionary(); + + foreach ( var model in models ?? throw new ArgumentNullException( nameof( models ) ) ) + { + var annotation = model.GetAnnotationValue( model ); + + if ( annotation == null ) + { + throw new ArgumentException( LocalSR.MissingAnnotation.FormatDefault( typeof( ApiVersionAnnotation ).Name ) ); + } + + var version = annotation.ApiVersion; + + collection.Add( version, model ); + versions.Add( version ); + } + + versions.Sort(); +#pragma warning disable IDE0056 // Use index operator (cannot be used in web api) + maxVersion = versions.Count == 0 ? defaultApiVersion : versions[versions.Count - 1]; +#pragma warning restore IDE0056 +#if !WEBAPI + collection.TrimExcess(); +#endif + ApiVersions = versions.ToArray(); + Models = collection; + } + + /// + public IReadOnlyList ApiVersions { get; } + + /// + /// Gets the collection of EDM models. + /// + /// A collection of EDM models. + protected IDictionary Models { get; } + + /// + public virtual bool Contains( ApiVersion? apiVersion ) => apiVersion != null && Models.ContainsKey( apiVersion ); + + /// + public virtual IEdmModel? SelectModel( ApiVersion? apiVersion ) => + apiVersion != null && Models.TryGetValue( apiVersion, out var model ) ? model : default; + + /// + public virtual IEdmModel? SelectModel( IServiceProvider serviceProvider ) + { + if ( Models.Count == 0 ) + { + return default; + } + +#if WEBAPI + var version = serviceProvider.GetService()?.GetRequestedApiVersion(); +#else + var version = serviceProvider.GetService()?.HttpContext.GetRequestedApiVersion(); +#endif + + return version != null && Models.TryGetValue( version, out var model ) ? model : Models[maxVersion]; + } + } +} \ No newline at end of file diff --git a/src/Common.OData/OData.Edm/IEdmModelSelector.cs b/src/Common.OData/OData.Edm/IEdmModelSelector.cs new file mode 100644 index 00000000..12d3d3e4 --- /dev/null +++ b/src/Common.OData/OData.Edm/IEdmModelSelector.cs @@ -0,0 +1,43 @@ +namespace Microsoft.OData.Edm +{ + using System; + using System.Collections.Generic; +#if WEBAPI + using Microsoft.Web.Http; +#else + using Microsoft.AspNetCore.Mvc; +#endif + + /// + /// Defines the behavior of an object that selects an EDM model. + /// + public interface IEdmModelSelector + { + /// + /// Gets a read-only list of API versions that can be selected from. + /// + /// A read-only list of API versions. + IReadOnlyList ApiVersions { get; } + + /// + /// Selects an EDM model using the given API version. + /// + /// The API version to select a model for. + /// The selected EDM model or null. + IEdmModel? SelectModel( ApiVersion? apiVersion ); + + /// + /// Selects an EDM model using the given service provider. + /// + /// The current service provider. + /// The selected EDM model or null. + IEdmModel? SelectModel( IServiceProvider serviceProvider ); + + /// + /// Returns a value indicating whether the selector contains the specified API version. + /// + /// The API version to evaluate. + /// True if the selector contains the API version; otherwise, false. + bool Contains( ApiVersion? apiVersion ); + } +} \ No newline at end of file diff --git a/src/Common/ApiVersion.cs b/src/Common/ApiVersion.cs index 6e78ef16..d2fb6779 100644 --- a/src/Common/ApiVersion.cs +++ b/src/Common/ApiVersion.cs @@ -23,9 +23,21 @@ namespace Microsoft.AspNetCore.Mvc /// public class ApiVersion : IEquatable, IComparable, IFormattable { + const int Prime = 397; const string ParsePattern = @"^(\d{4}-\d{2}-\d{2})?\.?(\d{0,9})\.?(\d{0,9})\.?-?(.*)$"; const string GroupVersionFormat = "yyyy-MM-dd"; - static readonly Lazy defaultVersion = new Lazy( () => new ApiVersion( 1, 0 ) ); + int hashCode; + + ApiVersion() + { + const int MajorVersion = int.MaxValue; + const int MinorVersion = int.MaxValue; + var groupVersion = MaxValue; + + hashCode = groupVersion.GetHashCode(); + hashCode = ( hashCode * Prime ) ^ MajorVersion; + hashCode = ( hashCode * Prime ) ^ MinorVersion; + } /// /// Initializes a new instance of the class. @@ -112,7 +124,13 @@ internal ApiVersion( DateTime? groupVersion, int? majorVersion, int? minorVersio /// Gets the default API version. /// /// The default API version, which is always "1.0". - public static ApiVersion Default => defaultVersion.Value; + public static ApiVersion Default { get; } = new ApiVersion( 1, 0 ); + + /// + /// Gets the neutral API version. + /// + /// The neutral API version. + public static ApiVersion Neutral { get; } = new ApiVersion(); /// /// Gets the group version. @@ -321,7 +339,14 @@ public static bool TryParse( string? text, out ApiVersion? version ) /// text representation of the object. public override int GetHashCode() { - var hashes = new List( 4 ); + // perf: api version is used in a lot sets and as a dictionary keys + // since it's immutable, calculate the hash code once and reuse it + if ( hashCode != default ) + { + return hashCode; + } + + var hashes = new List( capacity: 4 ); if ( GroupVersion != null ) { @@ -343,10 +368,10 @@ public override int GetHashCode() for ( var i = 1; i < hashes.Count; i++ ) { - hash = ( hash * 397 ) ^ hashes[i]; + hash = ( hash * Prime ) ^ hashes[i]; } - return hash; + return hashCode = hash; } /// @@ -408,18 +433,7 @@ public override int GetHashCode() /// /// The other to evaluate. /// True if the specified object is equal to the current instance; otherwise, false. - public virtual bool Equals( ApiVersion? other ) - { - if ( other == null ) - { - return false; - } - - return Nullable.Equals( GroupVersion, other.GroupVersion ) && - Nullable.Equals( MajorVersion, other.MajorVersion ) && - ImpliedMinorVersion.Equals( other.ImpliedMinorVersion ) && - string.Equals( Status, other.Status, StringComparison.OrdinalIgnoreCase ); - } + public virtual bool Equals( ApiVersion? other ) => other is null ? false : GetHashCode() == other.GetHashCode(); /// /// Performs a comparison of the current object to another object and returns a value @@ -499,7 +513,7 @@ public virtual string ToString( string? format, IFormatProvider? formatProvider var provider = ApiVersionFormatProvider.GetInstance( formatProvider ); #pragma warning disable CA1062 // Validate arguments of public methods (false positive) return provider.Format( format, this, formatProvider ); -#pragma warning restore CA1062 // Validate arguments of public methods +#pragma warning restore CA1062 } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs index ddf004ec..9d3ba809 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs @@ -15,6 +15,8 @@ partial class ODataRouteBuilderContext { + readonly ODataRoute route; + internal ODataRouteBuilderContext( HttpConfiguration configuration, ApiVersion apiVersion, @@ -24,40 +26,47 @@ internal ODataRouteBuilderContext( IModelTypeBuilder modelTypeBuilder, ODataApiExplorerOptions options ) { + this.route = route; ApiVersion = apiVersion; Services = configuration.GetODataRootContainer( route ); - EdmModel = Services.GetRequiredService(); routeAttribute = actionDescriptor.GetCustomAttributes().FirstOrDefault(); RouteTemplate = routeAttribute?.PathTemplate; - Route = route; + RoutePrefix = route.RoutePrefix?.Trim( '/' ); ActionDescriptor = actionDescriptor; ParameterDescriptions = parameterDescriptions; Options = options; UrlKeyDelimiter = UrlKeyDelimiterOrDefault( configuration.GetUrlKeyDelimiter() ?? Services.GetService()?.UrlKeyDelimiter ); - var container = EdmModel.EntityContainer; + var selector = Services.GetRequiredService(); + var model = selector.SelectModel( apiVersion ); + var container = model?.EntityContainer; - if ( container == null ) + if ( model == null || container == null ) { + EdmModel = Services.GetRequiredService(); IsRouteExcluded = true; return; } - EntitySet = container.FindEntitySet( actionDescriptor.ControllerDescriptor.ControllerName ); - Operation = container.FindOperationImports( actionDescriptor.ActionName ).FirstOrDefault()?.Operation ?? - EdmModel.FindDeclaredOperations( container.Namespace + "." + actionDescriptor.ActionName ).FirstOrDefault(); - ActionType = GetActionType( EntitySet, Operation ); + var controllerName = actionDescriptor.ControllerDescriptor.ControllerName; + + EdmModel = model; + Services = new FixedEdmModelServiceProviderDecorator( Services, model ); + EntitySet = container.FindEntitySet( controllerName ); + Operation = ResolveOperation( container, actionDescriptor.ActionName ); + ActionType = GetActionType( EntitySet, Operation, actionDescriptor ); + IsRouteExcluded = ActionType == ODataRouteActionType.Unknown; if ( Operation?.IsAction() == true ) { - ConvertODataActionParametersToTypedModel( modelTypeBuilder, (IEdmAction) Operation, actionDescriptor.ControllerDescriptor.ControllerName ); + ConvertODataActionParametersToTypedModel( modelTypeBuilder, (IEdmAction) Operation, controllerName ); } } + IEnumerable GetHttpMethods( HttpActionDescriptor action ) => action.GetHttpMethods( route ).Select( m => m.Method ); + void ConvertODataActionParametersToTypedModel( IModelTypeBuilder modelTypeBuilder, IEdmAction action, string controllerName ) { - var apiVersion = new Lazy( () => EdmModel.GetAnnotationValue( EdmModel ).ApiVersion ); - for ( var i = 0; i < ParameterDescriptions.Count; i++ ) { var description = ParameterDescriptions[i]; @@ -65,7 +74,8 @@ void ConvertODataActionParametersToTypedModel( IModelTypeBuilder modelTypeBuilde if ( parameter != null && parameter.ParameterType.IsODataActionParameters() ) { - description.ParameterDescriptor = new ODataModelBoundParameterDescriptor( parameter, modelTypeBuilder.NewActionParameters( Services, action, apiVersion.Value, controllerName ) ); + var parameterType = modelTypeBuilder.NewActionParameters( Services, action, ApiVersion, controllerName ); + description.ParameterDescriptor = new ODataModelBoundParameterDescriptor( parameter, parameterType ); break; } } diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Microsoft.AspNet.OData.Versioning.ApiExplorer.csproj b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Microsoft.AspNet.OData.Versioning.ApiExplorer.csproj index 0ba33c12..f3aad1ec 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Microsoft.AspNet.OData.Versioning.ApiExplorer.csproj +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Microsoft.AspNet.OData.Versioning.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 4.0.0 - 4.0.0.0 + 5.0.0 + 5.0.0.0 net45 Microsoft ASP.NET Web API Versioned API Explorer for OData v4.0 The API Explorer for Microsoft ASP.NET Web API Versioning and OData v4.0. @@ -12,7 +12,8 @@ - + + diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpControllerDescriptorExtensions.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpControllerDescriptorExtensions.cs deleted file mode 100644 index 15e75487..00000000 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpControllerDescriptorExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace System.Web.Http.Controllers -{ - using System; - using System.Collections.Generic; - - static class HttpControllerDescriptorExtensions - { - internal static IEnumerable AsEnumerable( this HttpControllerDescriptor controllerDescriptor ) - { - if ( controllerDescriptor is IEnumerable groupedControllerDescriptors ) - { - foreach ( var groupedControllerDescriptor in groupedControllerDescriptors ) - { - yield return groupedControllerDescriptor; - } - } - else - { - yield return controllerDescriptor; - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs index a4f4b559..4fa6985a 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs @@ -1,5 +1,6 @@ namespace System.Web.Http.Description { + using Microsoft.AspNet.OData.Routing; using Microsoft.OData.Edm; using Microsoft.Web.Http.Description; @@ -78,5 +79,20 @@ public static class ApiDescriptionExtensions return default; } + + /// + /// Gets the route prefix associated with the API description. + /// + /// The API description to get the route prefix for. + /// The associated route prefix or null. + public static string? RoutePrefix( this ApiDescription apiDescription ) + { + if ( apiDescription == null ) + { + throw new ArgumentNullException( nameof( apiDescription ) ); + } + + return apiDescription.Route is ODataRoute route ? route.RoutePrefix : default; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs index 9fe8d5b8..9de35766 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/System.Web.Http/HttpConfigurationExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.OData; using Microsoft.Web.Http.Description; using System.Collections.Concurrent; - using System.ComponentModel.DataAnnotations; using System.Web.Http.Description; using System.Web.Http.Routing; diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Web.Http.Description/ODataApiExplorer.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Web.Http.Description/ODataApiExplorer.cs index 1a82752a..938a148a 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Web.Http.Description/ODataApiExplorer.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Web.Http.Description/ODataApiExplorer.cs @@ -158,10 +158,10 @@ protected override Collection ExploreRouteControllers( } var apiDescriptions = new Collection(); - var edmModel = Configuration.GetODataRootContainer( route ).GetRequiredService(); - var routeApiVersion = edmModel.GetAnnotationValue( edmModel )?.ApiVersion; + var modelSelector = Configuration.GetODataRootContainer( route ).GetRequiredService(); + var edmModel = modelSelector.SelectModel( apiVersion ); - if ( routeApiVersion != apiVersion ) + if ( edmModel == null ) { return apiDescriptions; } @@ -355,7 +355,7 @@ IList CreateParameterDescriptions( HttpActionDescriptor foreach ( var entry in route.Constraints ) { - if ( entry.Value is ApiVersionRouteConstraint constraint ) + if ( entry.Value is ApiVersionRouteConstraint ) { list.Add( new ApiParameterDescription() { Name = entry.Key, Source = FromUri } ); break; diff --git a/src/Microsoft.AspNet.OData.Versioning/Builder/VersionedODataModelBuilder.cs b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Builder/VersionedODataModelBuilder.cs similarity index 87% rename from src/Microsoft.AspNet.OData.Versioning/Builder/VersionedODataModelBuilder.cs rename to src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Builder/VersionedODataModelBuilder.cs index 27ec4a00..d1931887 100644 --- a/src/Microsoft.AspNet.OData.Versioning/Builder/VersionedODataModelBuilder.cs +++ b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Builder/VersionedODataModelBuilder.cs @@ -48,7 +48,7 @@ protected virtual IReadOnlyList GetApiVersions() var typeResolver = services.GetHttpControllerTypeResolver(); var actionSelector = services.GetActionSelector(); var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver ).Where( TypeExtensions.IsODataController ); - var controllerDescriptors = services.GetHttpControllerSelector().GetControllerMapping().Values; + var controllerDescriptors = services.GetHttpControllerSelector().GetControllerMapping().Values.ToArray(); var supported = new HashSet(); var deprecated = new HashSet(); @@ -118,24 +118,17 @@ protected virtual void ConfigureMetadataController( IEnumerable supp controllerBuilder.ApplyTo( controllerDescriptor ); } - static HttpControllerDescriptor? FindControllerDescriptor( IEnumerable controllerDescriptors, Type controllerType ) + static HttpControllerDescriptor? FindControllerDescriptor( IReadOnlyList controllerDescriptors, Type controllerType ) { - foreach ( var controllerDescriptor in controllerDescriptors ) + for ( var i = 0; i < controllerDescriptors.Count; i++ ) { - if ( controllerDescriptor is IEnumerable groupedControllerDescriptors ) + foreach ( var controllerDescriptor in controllerDescriptors[i].AsEnumerable() ) { - foreach ( var groupedControllerDescriptor in groupedControllerDescriptors ) + if ( controllerType.Equals( controllerDescriptor.ControllerType ) ) { - if ( controllerType.Equals( groupedControllerDescriptor.ControllerType ) ) - { - return groupedControllerDescriptor; - } + return controllerDescriptor; } } - else if ( controllerType.Equals( controllerDescriptor.ControllerType ) ) - { - return controllerDescriptor; - } } return default; diff --git a/src/Microsoft.AspNet.OData.Versioning/Routing/ODataConventionConfigurationContext.cs b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/ODataConventionConfigurationContext.cs similarity index 76% rename from src/Microsoft.AspNet.OData.Versioning/Routing/ODataConventionConfigurationContext.cs rename to src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/ODataConventionConfigurationContext.cs index 7e385512..28b9ab5b 100644 --- a/src/Microsoft.AspNet.OData.Versioning/Routing/ODataConventionConfigurationContext.cs +++ b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/ODataConventionConfigurationContext.cs @@ -3,6 +3,7 @@ using Microsoft.AspNet.OData.Routing.Conventions; using Microsoft.OData.Edm; using Microsoft.Web.Http; + using System; using System.Collections.Generic; using System.Web.Http; @@ -19,13 +20,21 @@ public partial class ODataConventionConfigurationContext /// The current EDM model. /// The current API version. /// The initial list of routing conventions. - public ODataConventionConfigurationContext( HttpConfiguration configuration, string routeName, IEdmModel edmModel, ApiVersion apiVersion, IList routingConventions ) + /// The associated serviceProvider. + public ODataConventionConfigurationContext( + HttpConfiguration configuration, + string routeName, + IEdmModel edmModel, + ApiVersion apiVersion, + IList routingConventions, + IServiceProvider serviceProvider ) { Configuration = configuration; RouteName = routeName; EdmModel = edmModel; ApiVersion = apiVersion; RoutingConventions = routingConventions; + ServiceProvider = serviceProvider; } /// diff --git a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedAttributeRoutingConvention.cs b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs similarity index 61% rename from src/Microsoft.AspNet.OData.Versioning/Routing/VersionedAttributeRoutingConvention.cs rename to src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs index aa98a1c6..8a517b9d 100644 --- a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedAttributeRoutingConvention.cs +++ b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs @@ -4,6 +4,8 @@ using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Routing.Conventions; using Microsoft.AspNet.OData.Routing.Template; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.OData.Edm; using Microsoft.Web.Http; using Microsoft.Web.Http.Versioning; using System; @@ -19,16 +21,14 @@ public partial class VersionedAttributeRoutingConvention : IODataRoutingConvention { const string AttributeRouteData = nameof( AttributeRouteData ); - static readonly DefaultODataPathHandler defaultPathHandler = new DefaultODataPathHandler(); /// /// Initializes a new instance of the class. /// /// The name of the route. /// The current HTTP configuration. - /// The API version associated with the convention. - public VersionedAttributeRoutingConvention( string routeName, HttpConfiguration configuration, ApiVersion apiVersion ) - : this( routeName, configuration, defaultPathHandler, apiVersion ) { } + public VersionedAttributeRoutingConvention( string routeName, HttpConfiguration configuration ) + : this( routeName, configuration, new DefaultODataPathHandler() ) { } /// /// Initializes a new instance of the class. @@ -36,90 +36,45 @@ public VersionedAttributeRoutingConvention( string routeName, HttpConfiguration /// The name of the route. /// The current HTTP configuration. /// The OData path template handler associated with the routing convention. - /// The API version associated with the convention. - public VersionedAttributeRoutingConvention( string routeName, HttpConfiguration configuration, IODataPathTemplateHandler pathTemplateHandler, ApiVersion apiVersion ) + public VersionedAttributeRoutingConvention( string routeName, HttpConfiguration configuration, IODataPathTemplateHandler pathTemplateHandler ) { if ( configuration == null ) { throw new ArgumentNullException( nameof( configuration ) ); } - this.routeName = routeName; + RouteName = routeName; ODataPathTemplateHandler = pathTemplateHandler; - ApiVersion = apiVersion; if ( pathTemplateHandler is IODataPathHandler pathHandler && pathHandler.UrlKeyDelimiter == null ) { var urlKeyDelimiter = configuration.GetUrlKeyDelimiter(); pathHandler.UrlKeyDelimiter = urlKeyDelimiter; } - - var initialized = false; - var initializer = configuration.Initializer; - - configuration.Initializer = config => - { - if ( initialized ) - { - return; - } - - initialized = true; - initializer?.Invoke( config ); - - var controllerSelector = configuration.Services.GetHttpControllerSelector(); - attributeMappings = BuildAttributeMappings( controllerSelector.GetControllerMapping().Values ); - }; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the route. - /// The sequence of controller descriptors. - /// The API version associated with the convention. - public VersionedAttributeRoutingConvention( string routeName, IEnumerable controllers, ApiVersion apiVersion ) - : this( routeName, controllers, defaultPathHandler, apiVersion ) { } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the route. - /// The sequence of controller descriptors - /// associated with the routing convention. - /// The OData path template handler associated with the routing convention. - /// The API version associated with the convention. - public VersionedAttributeRoutingConvention( string routeName, IEnumerable controllers, IODataPathTemplateHandler pathTemplateHandler, ApiVersion apiVersion ) - { - if ( controllers == null ) - { - throw new ArgumentNullException( nameof( controllers ) ); - } - - this.routeName = routeName; - ODataPathTemplateHandler = pathTemplateHandler; - ApiVersion = apiVersion; - attributeMappings = BuildAttributeMappings( controllers ); } - IDictionary AttributeMappings => attributeMappings ?? throw new InvalidOperationException( SR.ObjectNotYetInitialized ); - /// /// Returns a value indicating whether the specified controller should be mapped using attribute routing conventions. /// /// The controller descriptor to evaluate. + /// The API version to evaluate. /// True if the should be mapped as an OData controller; otherwise, false. /// The default implementation always returns true. - public virtual bool ShouldMapController( HttpControllerDescriptor controller ) => true; + public virtual bool ShouldMapController( HttpControllerDescriptor controller, ApiVersion? apiVersion ) + { + var model = controller.GetApiVersionModel(); + return model.IsApiVersionNeutral || model.DeclaredApiVersions.Contains( apiVersion ); + } /// /// Returns a value indicating whether the specified action should be mapped using attribute routing conventions. /// /// The action descriptor to evaluate. + /// The API version to evaluate. /// True if the should be mapped as an OData action or function; otherwise, false. /// This method will match any OData action that explicitly or implicitly matches the API version applied /// to the associated model. - public virtual bool ShouldMapAction( HttpActionDescriptor action ) => action.IsMappedTo( ApiVersion ); + public virtual bool ShouldMapAction( HttpActionDescriptor action, ApiVersion? apiVersion ) => action.IsMappedTo( apiVersion ); /// /// Selects the controller for OData requests. @@ -129,14 +84,26 @@ public VersionedAttributeRoutingConvention( string routeName, IEnumerablenull if the request isn't handled by this convention; otherwise, the name of the selected controller. public virtual string? SelectController( ODataPath odataPath, HttpRequestMessage request ) { + if ( odataPath == null ) + { + throw new ArgumentNullException( nameof( odataPath ) ); + } + if ( request == null ) { throw new ArgumentNullException( nameof( request ) ); } + if ( odataPath.Segments.Count == 0 ) + { + return null; + } + + var version = SelectApiVersion( request ); + var attributeMappings = attributeMappingsPerApiVersion.GetOrAdd( version, key => BuildAttributeMappings( key, request ) ); var values = new Dictionary(); - foreach ( var attributeMapping in AttributeMappings ) + foreach ( var attributeMapping in attributeMappings ) { var template = attributeMapping.Key; var action = attributeMapping.Value; @@ -193,35 +160,77 @@ public VersionedAttributeRoutingConvention( string routeName, IEnumerable BuildAttributeMappings( IEnumerable controllers ) + /// + /// Selects the API version from the given HTTP request. + /// + /// The current HTTP request. + /// The selected API version. + protected virtual ApiVersion SelectApiVersion( HttpRequestMessage request ) { + var version = request.GetRequestedApiVersionOrReturnBadRequest(); + + if ( version != null ) + { + return version; + } + + var options = request.GetApiVersioningOptions(); + + if ( !options.AssumeDefaultVersionWhenUnspecified ) + { + return version ?? ApiVersion.Neutral; + } + + var modelSelector = request.GetRequestContainer().GetRequiredService(); + var versionSelector = request.GetApiVersioningOptions().ApiVersionSelector; + var model = new ApiVersionModel( modelSelector.ApiVersions, Enumerable.Empty() ); + + return versionSelector.SelectVersion( request, model ); + } + + static IEnumerable GetODataRoutePrefixes( HttpControllerDescriptor controllerDescriptor ) + { + var prefixAttributes = controllerDescriptor.GetCustomAttributes( inherit: false ); + return GetODataRoutePrefixes( prefixAttributes, controllerDescriptor.ControllerType.FullName ); + } + + IReadOnlyDictionary BuildAttributeMappings( ApiVersion version, HttpRequestMessage request ) + { + var configuration = request.GetConfiguration(); + var services = configuration.Services; + var controllerSelector = services.GetHttpControllerSelector(); + var controllers = controllerSelector.GetControllerMapping().Values.ToArray(); var attributeMappings = new Dictionary(); + var actionSelector = services.GetActionSelector(); + var serviceProvider = request.GetRequestContainer(); - foreach ( var controller in controllers ) + for ( var i = 0; i < controllers.Length; i++ ) { - if ( !controller.ControllerType.IsODataController() || !ShouldMapController( controller ) ) + foreach ( var controller in controllers[i].AsEnumerable() ) { - continue; - } + if ( !controller.ControllerType.IsODataController() || !ShouldMapController( controller, version ) ) + { + continue; + } - var actionSelector = controller.Configuration.Services.GetActionSelector(); - var actionMapping = actionSelector.GetActionMapping( controller ); - var actions = actionMapping.SelectMany( a => a ).ToArray(); + var actionMapping = actionSelector.GetActionMapping( controller ); + var actions = actionMapping.SelectMany( a => a ).ToArray(); - foreach ( var prefix in GetODataRoutePrefixes( controller ) ) - { - foreach ( var action in actions ) + foreach ( var prefix in GetODataRoutePrefixes( controller ) ) { - if ( !ShouldMapAction( action ) ) + foreach ( var action in actions ) { - continue; - } + if ( !ShouldMapAction( action, version ) ) + { + continue; + } - var pathTemplates = GetODataPathTemplates( prefix, action ); + var pathTemplates = GetODataPathTemplates( prefix, action, serviceProvider ); - foreach ( var pathTemplate in pathTemplates ) - { - attributeMappings.Add( pathTemplate, action ); + foreach ( var pathTemplate in pathTemplates ) + { + attributeMappings.Add( pathTemplate, action ); + } } } } @@ -230,20 +239,19 @@ IDictionary BuildAttributeMappings( IEn return attributeMappings; } - static IEnumerable GetODataRoutePrefixes( HttpControllerDescriptor controllerDescriptor ) - { - var prefixAttributes = controllerDescriptor.GetCustomAttributes( inherit: false ); - return GetODataRoutePrefixes( prefixAttributes, controllerDescriptor.ControllerType.FullName ); - } - - IEnumerable GetODataPathTemplates( string prefix, HttpActionDescriptor action ) + IEnumerable GetODataPathTemplates( string prefix, HttpActionDescriptor action, IServiceProvider serviceProvider ) { var routeAttributes = action.GetCustomAttributes( inherit: false ); - var serviceProvider = action.Configuration.GetODataRootContainer( routeName ); - var controllerName = action.ControllerDescriptor.ControllerName; - var actionName = action.ActionName; - return routeAttributes.Select( route => GetODataPathTemplate( prefix, route.PathTemplate, serviceProvider, controllerName, actionName ) ).Where( template => template != null ); + foreach ( var route in routeAttributes ) + { + var template = GetODataPathTemplate( prefix, route.PathTemplate, serviceProvider ); + + if ( template != null ) + { + yield return template; + } + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedMetadataRoutingConvention.cs b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/VersionedMetadataRoutingConvention.cs similarity index 66% rename from src/Microsoft.AspNet.OData.Versioning/Routing/VersionedMetadataRoutingConvention.cs rename to src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/VersionedMetadataRoutingConvention.cs index 0d5b3b52..7d21b54f 100644 --- a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedMetadataRoutingConvention.cs +++ b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/VersionedMetadataRoutingConvention.cs @@ -1,9 +1,15 @@ namespace Microsoft.AspNet.OData.Routing { + using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Routing.Conventions; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.OData.Edm; + using Microsoft.Web.Http; + using Microsoft.Web.Http.Versioning; using System; using System.Linq; using System.Net.Http; + using System.Web.Http; using System.Web.Http.Controllers; using static System.Net.Http.HttpMethod; @@ -25,7 +31,26 @@ public class VersionedMetadataRoutingConvention : IODataRoutingConvention throw new ArgumentNullException( nameof( odataPath ) ); } - return odataPath.PathTemplate == "~" || odataPath.PathTemplate == "~/$metadata" ? "VersionedMetadata" : null; + if ( odataPath.PathTemplate != "~" && odataPath.PathTemplate != "~/$metadata" ) + { + return null; + } + + var properties = request.ApiVersionProperties(); + + // the service document and metadata endpoints are special, but they are not neutral. if the client doesn't + // specify a version, they may not know to. assume a default version by policy, but it's always allowed. + // a client might also send an OPTIONS request to determine which versions are available (ex: tooling) + if ( string.IsNullOrEmpty( properties.RawRequestedApiVersion ) ) + { + var modelSelector = request.GetRequestContainer().GetRequiredService(); + var versionSelector = request.GetApiVersioningOptions().ApiVersionSelector; + var model = new ApiVersionModel( modelSelector.ApiVersions, Enumerable.Empty() ); + + properties.RequestedApiVersion = versionSelector.SelectVersion( request, model ); + } + + return "VersionedMetadata"; } /// diff --git a/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/VersionedODataRoutingConventions.cs b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/VersionedODataRoutingConventions.cs new file mode 100644 index 00000000..3c08c8ad --- /dev/null +++ b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/Routing/VersionedODataRoutingConventions.cs @@ -0,0 +1,21 @@ +namespace Microsoft.AspNet.OData.Routing +{ + using Microsoft.AspNet.OData.Routing.Conventions; + using System.Collections.Generic; + using System.Web.Http; + + /// + /// Provides additional implementation specific to ASP.NET Web API. + /// + public static partial class VersionedODataRoutingConventions + { + /// + /// Creates a mutable list of the default OData routing conventions with attribute routing enabled. + /// + /// The name of the route. + /// The current configuration. + /// A mutable list of the default OData routing conventions. + public static IList CreateDefaultWithAttributeRouting( string routeName, HttpConfiguration configuration ) => + EnsureConventions( ODataRoutingConventions.CreateDefault(), new VersionedAttributeRoutingConvention( routeName, configuration ) ); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/VersionedMetadataController.cs b/src/Microsoft.AspNet.OData.Versioning/AspNet.OData/VersionedMetadataController.cs similarity index 100% rename from src/Microsoft.AspNet.OData.Versioning/VersionedMetadataController.cs rename to src/Microsoft.AspNet.OData.Versioning/AspNet.OData/VersionedMetadataController.cs diff --git a/src/Microsoft.AspNet.OData.Versioning/LocalSR.Designer.cs b/src/Microsoft.AspNet.OData.Versioning/LocalSR.Designer.cs index 122d1dc8..c86a0862 100644 --- a/src/Microsoft.AspNet.OData.Versioning/LocalSR.Designer.cs +++ b/src/Microsoft.AspNet.OData.Versioning/LocalSR.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNet.OData { +namespace Microsoft { using System; @@ -39,7 +39,7 @@ internal LocalSR() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.OData.LocalSR", typeof(LocalSR).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.LocalSR", typeof(LocalSR).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/Microsoft.AspNet.OData.Versioning/Microsoft.AspNet.OData.Versioning.csproj b/src/Microsoft.AspNet.OData.Versioning/Microsoft.AspNet.OData.Versioning.csproj index 7f09672b..54aac89e 100644 --- a/src/Microsoft.AspNet.OData.Versioning/Microsoft.AspNet.OData.Versioning.csproj +++ b/src/Microsoft.AspNet.OData.Versioning/Microsoft.AspNet.OData.Versioning.csproj @@ -1,12 +1,12 @@  - 4.0.0 - 4.0.0.0 + 5.0.0 + 5.0.0.0 net45 Microsoft ASP.NET Web API Versioning for OData v4.0 A service API versioning library for Microsoft ASP.NET Web API and OData v4.0. - Microsoft.AspNet.OData + Microsoft $(DefineConstants);WEBAPI Microsoft;AspNet;AspNetWebAPI;OData;Versioning @@ -16,7 +16,7 @@ - + @@ -24,7 +24,18 @@ + + + True + + + + + + True + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Microsoft.Web.Http.Versioning/ODataApiVersionRequestProperties.cs b/src/Microsoft.AspNet.OData.Versioning/Microsoft.Web.Http.Versioning/ODataApiVersionRequestProperties.cs deleted file mode 100644 index 8be22eb4..00000000 --- a/src/Microsoft.AspNet.OData.Versioning/Microsoft.Web.Http.Versioning/ODataApiVersionRequestProperties.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Microsoft.Web.Http.Versioning -{ - using System; - using System.Collections.Generic; - - /// - /// Represents current OData API versioning request properties. - /// - public class ODataApiVersionRequestProperties - { - /// - /// Gets a collection of API version to route name mappings that have been matched in the current request. - /// - /// A collection of key/value pairs representing the mapping - /// of API versions to route names that have been matched in the current request. - public IDictionary MatchingRoutes { get; } = new Dictionary(); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/OData/IContainerBuilderExtensions.cs b/src/Microsoft.AspNet.OData.Versioning/OData/IContainerBuilderExtensions.cs new file mode 100644 index 00000000..6eac343e --- /dev/null +++ b/src/Microsoft.AspNet.OData.Versioning/OData/IContainerBuilderExtensions.cs @@ -0,0 +1,58 @@ +namespace Microsoft.OData +{ + using Microsoft.AspNet.OData.Routing.Conventions; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.OData.Edm; + using System.Collections.Generic; + using System.Linq; + using System.Web.Http; + using static Microsoft.AspNet.OData.Routing.VersionedODataRoutingConventions; + using static Microsoft.OData.ServiceLifetime; + + /// + /// Provides extension methods for the interface. + /// + public static class IContainerBuilderExtensions + { + /// + /// Adds service API versioning to the specified container builder. + /// + /// The extended container builder. + /// The name of the route to add API versioning to. + /// The sequence of EDM models to use for parsing OData paths. + /// The original . + public static IContainerBuilder AddApiVersioning( this IContainerBuilder builder, string routeName, IEnumerable models ) => + builder + .AddService( Transient, sp => sp.GetRequiredService().SelectModel( sp ) ) + .AddService( + Singleton, + sp => (IEdmModelSelector) new EdmModelSelector( + models, + sp.GetRequiredService().GetApiVersioningOptions().DefaultApiVersion ) ) + .AddService( + Singleton, + sp => CreateDefaultWithAttributeRouting( + routeName, + sp.GetRequiredService() ).AsEnumerable() ); + + /// + /// Adds service API versioning to the specified container builder. + /// + /// The extended container builder. + /// The sequence of EDM models to use for parsing OData paths. + /// The OData routing conventions to use for controller and action selection. + /// The original . + public static IContainerBuilder AddApiVersioning( + this IContainerBuilder builder, + IEnumerable models, + IEnumerable routingConventions ) => + builder + .AddService( Transient, sp => sp.GetRequiredService().SelectModel( sp ) ) + .AddService( + Singleton, + sp => (IEdmModelSelector) new EdmModelSelector( + models, + sp.GetRequiredService().GetApiVersioningOptions().DefaultApiVersion ) ) + .AddService( Singleton, sp => AddOrUpdate( routingConventions.ToList() ).AsEnumerable() ); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Routing/UnversionedODataPathRouteConstraint.cs b/src/Microsoft.AspNet.OData.Versioning/Routing/UnversionedODataPathRouteConstraint.cs deleted file mode 100644 index 10a1b1db..00000000 --- a/src/Microsoft.AspNet.OData.Versioning/Routing/UnversionedODataPathRouteConstraint.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace Microsoft.AspNet.OData.Routing -{ - using Microsoft.AspNet.OData.Extensions; - using Microsoft.Web.Http; - using Microsoft.Web.Http.Versioning; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; - using System.Web.Http; - using System.Web.Http.Routing; - using static System.Web.Http.Routing.HttpRouteDirection; - - sealed class UnversionedODataPathRouteConstraint : IHttpRouteConstraint - { - readonly ApiVersion? apiVersion; - readonly IEnumerable innerConstraints; - - internal UnversionedODataPathRouteConstraint( IEnumerable innerConstraints ) => this.innerConstraints = innerConstraints; - - internal UnversionedODataPathRouteConstraint( IHttpRouteConstraint innerConstraint, ApiVersion apiVersion ) - { - innerConstraints = new[] { innerConstraint }; - this.apiVersion = apiVersion; - } - - bool MatchAnyVersion => apiVersion == null; - - public bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection ) - { - if ( routeDirection == UriGeneration ) - { - return true; - } - - if ( !MatchAnyVersion && apiVersion != request.GetRequestedApiVersion() ) - { - return false; - } - - var properties = request.ApiVersionProperties(); - - // determine whether this constraint can match any api version and no api version has otherwise been matched - if ( MatchAnyVersion && properties.RequestedApiVersion == null ) - { - var options = request.GetApiVersioningOptions(); - - // is implicitly matching an api version allowed? - if ( options.AssumeDefaultVersionWhenUnspecified || IsServiceDocumentOrMetadataRoute( values ) ) - { - var odata = request.ODataApiVersionProperties(); - var model = new ApiVersionModel( odata.MatchingRoutes.Keys, Enumerable.Empty() ); - var selector = options.ApiVersionSelector; - var requestedApiVersion = properties.RequestedApiVersion = selector.SelectVersion( request, model ); - - // if an api version is selected, determine if it corresponds to a route that has been previously matched - if ( requestedApiVersion != null && odata.MatchingRoutes.TryGetValue( requestedApiVersion, out var routeName ) ) - { - // create a new versioned path constraint on the fly and evaluate it. this sets up the underlying odata - // infrastructure such as the container, edm, etc. this has no bearing the action selector which will - // already select the correct action. without this the response may be incorrect, even if the correct - // action is selected and executed. - var constraint = new VersionedODataPathRouteConstraint( routeName, requestedApiVersion ); - return constraint.Match( request, route, parameterName, values, routeDirection ); - } - } - } - else if ( !MatchAnyVersion && properties.RequestedApiVersion != apiVersion ) - { - return false; - } - - request.DeleteRequestContainer( true ); - - // by evaluating the remaining unversioned constraints, this will ultimately determine whether 400 or 404 - // is returned for an odata request - foreach ( var constraint in innerConstraints ) - { - if ( constraint.Match( request, route, parameterName, values, routeDirection ) ) - { - return true; - } - } - - return false; - } - - static bool IsServiceDocumentOrMetadataRoute( IDictionary values ) => - values.TryGetValue( "odataPath", out var value ) && ( value == null || Equals( value, "$metadata" ) ); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedODataPathRouteConstraint.cs b/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedODataPathRouteConstraint.cs deleted file mode 100644 index 2dda6784..00000000 --- a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedODataPathRouteConstraint.cs +++ /dev/null @@ -1,103 +0,0 @@ -namespace Microsoft.AspNet.OData.Routing -{ - using Microsoft.AspNet.OData.Extensions; - using Microsoft.OData; - using Microsoft.Web.Http; - using Microsoft.Web.Http.Versioning; - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Web.Http; - using System.Web.Http.Routing; - using static System.Net.HttpStatusCode; - using static System.Web.Http.Routing.HttpRouteDirection; - - /// - /// Represents an OData path route constraint which supports versioning. - /// - public class VersionedODataPathRouteConstraint : ODataPathRouteConstraint - { - /// - /// Initializes a new instance of the class. - /// - /// The name of the route this constraint is associated with. - /// The API version associated with the route constraint. - public VersionedODataPathRouteConstraint( string routeName, ApiVersion apiVersion ) - : base( routeName ) => ApiVersion = apiVersion; - - /// - /// Gets the API version matched by the current OData path route constraint. - /// - /// The API version associated with the route constraint. - public ApiVersion ApiVersion { get; } - - /// - /// Determines whether this instance equals a specified route. - /// - /// The request. - /// The route to compare. - /// The name of the parameter. - /// A list of parameter values. - /// The route direction. - /// True if this instance equals a specified route; otherwise, false. - public override bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection ) - { - if ( values == null ) - { - throw new ArgumentNullException( nameof( values ) ); - } - - if ( routeDirection == UriGeneration ) - { - return base.Match( request, route, parameterName, values, routeDirection ); - } - - var requestedVersion = GetRequestedApiVersionOrReturnBadRequest( request ); - bool matched; - - try - { - matched = base.Match( request, route, parameterName, values, routeDirection ); - } - catch ( InvalidOperationException ) - { - // note: the base implementation of Match will setup the container. if this happens more - // than once, an exception is thrown. this most often occurs when policy allows implicitly - // matching an api version and all routes must be visited to determine their candidacy. if - // this happens, delete the container and retry. - request.DeleteRequestContainer( true ); - matched = base.Match( request, route, parameterName, values, routeDirection ); - } - - if ( !matched ) - { - return false; - } - - if ( requestedVersion == null ) - { - // we definitely matched the route, but not necessarily the api version so - // track this route as a matching candidate - request.ODataApiVersionProperties().MatchingRoutes[ApiVersion] = RouteName; - return false; - } - - return ApiVersion == requestedVersion; - } - - static ApiVersion? GetRequestedApiVersionOrReturnBadRequest( HttpRequestMessage request ) - { - var properties = request.ApiVersionProperties(); - - try - { - return properties.RequestedApiVersion; - } - catch ( AmbiguousApiVersionException ex ) - { - var error = new ODataError() { ErrorCode = "AmbiguousApiVersion", Message = ex.Message }; - throw new HttpResponseException( request.CreateResponse( BadRequest, error ) ); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs index 18190b77..de8b4730 100644 --- a/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs +++ b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpConfigurationExtensions.cs @@ -1,373 +1,69 @@ namespace System.Web.Http { - using Microsoft; - using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Batch; using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Routing; using Microsoft.AspNet.OData.Routing.Conventions; - using Microsoft.Extensions.DependencyInjection; using Microsoft.OData; using Microsoft.OData.Edm; - using Microsoft.Web.Http; using Microsoft.Web.Http.Routing; - using Microsoft.Web.Http.Versioning; - using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; - using System.Web.Http.Routing; using static Microsoft.OData.ServiceLifetime; - using static System.String; - using static System.StringComparison; /// /// Provides extension methods for the class. /// public static class HttpConfigurationExtensions { - const string ContainerBuilderFactoryKey = "Microsoft.AspNet.OData.ContainerBuilderFactoryKey"; - const string RootContainerMappingsKey = "Microsoft.AspNet.OData.RootContainerMappingsKey"; - const string UrlKeyDelimiterKey = "Microsoft.AspNet.OData.UrlKeyDelimiterKey"; - const string UnversionedRouteSuffix = "-Unversioned"; - /// - /// Maps the specified versioned OData routes. - /// - /// The extended HTTP configuration. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The configuring action to add the services to the root container. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( - this HttpConfiguration configuration, - string routeName, - string routePrefix, - IEnumerable models, - Action? configureAction ) => - MapVersionedODataRoutes( configuration, routeName, routePrefix, models, configureAction, default, default ); - - /// - /// Maps the specified versioned OData routes. - /// - /// The extended HTTP configuration. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The configuring action to add the services to the root container. - /// The OData batch handler. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( - this HttpConfiguration configuration, - string routeName, - string routePrefix, - IEnumerable models, - Action? configureAction, - ODataBatchHandler? batchHandler ) => - MapVersionedODataRoutes( configuration, routeName, routePrefix, models, configureAction, default, batchHandler ); - - /// - /// Maps the specified versioned OData routes. - /// - /// The extended HTTP configuration. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The configuring action to add the services to the root container. - /// The configuring action to add or update routing conventions. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( - this HttpConfiguration configuration, - string routeName, - string routePrefix, - IEnumerable models, - Action? configureAction, - Action? configureRoutingConventions ) => - MapVersionedODataRoutes( configuration, routeName, routePrefix, models, configureAction, configureRoutingConventions, default ); - - /// - /// Maps the specified versioned OData routes. + /// Maps the specified OData route and the OData route attributes. /// - /// The extended HTTP configuration. + /// The server configuration. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The configuring action to add the services to the root container. - /// The configuring action to add or update routing conventions. - /// The OData batch handler. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( + /// The model builer used to create + /// an EDM model per API version. + /// The added . + public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configuration, string routeName, string routePrefix, - IEnumerable models, - Action? configureAction, - Action? configureRoutingConventions, - ODataBatchHandler? batchHandler ) + VersionedODataModelBuilder modelBuilder ) { - if ( configuration == null ) - { - throw new ArgumentNullException( nameof( configuration ) ); - } - - if ( models == null ) - { - throw new ArgumentNullException( nameof( models ) ); - } - - object ConfigureRoutingConventions( IEdmModel model, string versionedRouteName, ApiVersion apiVersion ) - { - var routingConventions = VersionedODataRoutingConventions.CreateDefault(); - var context = new ODataConventionConfigurationContext( configuration, versionedRouteName, model, apiVersion, routingConventions ); - - model.SetAnnotationValue( model, new ApiVersionAnnotation( apiVersion ) ); - routingConventions.Insert( 0, new VersionedAttributeRoutingConvention( versionedRouteName, configuration, apiVersion ) ); - configureRoutingConventions?.Invoke( context ); - - return context.RoutingConventions.ToArray(); - } - - if ( !IsNullOrEmpty( routePrefix ) ) - { - routePrefix = routePrefix.TrimEnd( '/' ); - } - - var routes = configuration.Routes; - var unversionedRouteName = routeName + UnversionedRouteSuffix; - - if ( batchHandler != null ) + if ( modelBuilder == null ) { - batchHandler.ODataRouteName = unversionedRouteName; - var batchTemplate = IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch; - routes.MapHttpBatchRoute( routeName + nameof( ODataRouteConstants.Batch ), batchTemplate, batchHandler ); - } - - var odataRoutes = new List(); - var unversionedConstraints = new List(); - - foreach ( var model in models ) - { - var versionedRouteName = routeName; - var annotation = model.GetAnnotationValue( model ) ?? throw new InvalidOperationException( LocalSR.MissingAnnotation.FormatDefault( typeof( ApiVersionAnnotation ).Name ) ); - var apiVersion = annotation.ApiVersion; - var routeConstraint = MakeVersionedODataRouteConstraint( apiVersion, ref versionedRouteName ); - - unversionedConstraints.Add( new ODataPathRouteConstraint( versionedRouteName ) ); - - var rootContainer = configuration.CreateODataRootContainer( - versionedRouteName, - builder => - { - builder.AddService( Singleton, typeof( IEdmModel ), sp => model ) - .AddService( Singleton, typeof( IEnumerable ), sp => ConfigureRoutingConventions( model, versionedRouteName, apiVersion ) ); - configureAction?.Invoke( builder ); - } ); - - var pathHandler = rootContainer.GetRequiredService(); - - if ( pathHandler != null && pathHandler.UrlKeyDelimiter == null ) - { - pathHandler.UrlKeyDelimiter = configuration.GetUrlKeyDelimiter(); - } - - rootContainer.InitializeAttributeRouting(); - - var route = default( ODataRoute ); - var messageHandler = rootContainer.GetService(); - var options = configuration.GetApiVersioningOptions(); - - if ( messageHandler == null ) - { - route = new ODataRoute( routePrefix, routeConstraint ); - } - else - { - route = new ODataRoute( routePrefix, routeConstraint, defaults: null, constraints: null, dataTokens: null, handler: messageHandler ); - } - - routes.Add( versionedRouteName, route ); - AddApiVersionConstraintIfNecessary( route, options ); - odataRoutes.Add( route ); + throw new ArgumentNullException( nameof( modelBuilder ) ); } - configuration.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( unversionedRouteName, routePrefix, odataRoutes, unversionedConstraints, configureAction ); - - return odataRoutes; + return configuration.MapVersionedODataRoute( routeName, routePrefix, modelBuilder.GetEdmModels( routePrefix ) ); } /// - /// Maps the specified versioned OData routes. - /// - /// The extended HTTP configuration. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( this HttpConfiguration configuration, string routeName, string routePrefix, IEnumerable models ) => - MapVersionedODataRoutes( configuration, routeName, routePrefix, models, new DefaultODataPathHandler(), VersionedODataRoutingConventions.CreateDefault(), default ); - - /// - /// Maps the specified versioned OData routes. When the is provided, it will create a - /// '$batch' endpoint to handle the batch requests. - /// - /// The extended HTTP configuration. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The OData batch handler. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( - this HttpConfiguration configuration, - string routeName, - string routePrefix, - IEnumerable models, - ODataBatchHandler? batchHandler ) => - MapVersionedODataRoutes( configuration, routeName, routePrefix, models, new DefaultODataPathHandler(), VersionedODataRoutingConventions.CreateDefault(), batchHandler ); - - /// - /// Maps the specified versioned OData routes. - /// - /// The extended HTTP configuration. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The OData path handler to use for parsing the OData path. - /// The sequence of OData routing conventions - /// to use for controller and action selection. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( - this HttpConfiguration configuration, - string routeName, - string routePrefix, - IEnumerable models, - IODataPathHandler pathHandler, - IEnumerable routingConventions ) => - MapVersionedODataRoutes( configuration, routeName, routePrefix, models, pathHandler, routingConventions, default ); - - /// - /// Maps the specified versioned OData routes. When the is provided, it will create a '$batch' endpoint to handle the batch requests. + /// Maps the specified OData route and the OData route attributes. /// - /// The extended HTTP configuration. + /// The server configuration. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The OData path handler to use for parsing the OData path. - /// The sequence of OData routing conventions - /// to use for controller and action selection. - /// The OData batch handler. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( + /// The model builer used to create + /// an EDM model per API version. + /// The configuring action to add the services to the root container. + /// The added . + public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configuration, string routeName, string routePrefix, - IEnumerable models, - IODataPathHandler pathHandler, - IEnumerable routingConventions, - ODataBatchHandler? batchHandler ) + VersionedODataModelBuilder modelBuilder, + Action configureAction ) { - if ( configuration == null ) + if ( modelBuilder == null ) { - throw new ArgumentNullException( nameof( configuration ) ); + throw new ArgumentNullException( nameof( modelBuilder ) ); } - if ( models == null ) - { - throw new ArgumentNullException( nameof( models ) ); - } - - var routeConventions = VersionedODataRoutingConventions.AddOrUpdate( routingConventions.ToList() ); - var routes = configuration.Routes; - var unversionedRouteName = routeName + UnversionedRouteSuffix; - - if ( !IsNullOrEmpty( routePrefix ) ) - { - routePrefix = routePrefix.TrimEnd( '/' ); - } - - if ( batchHandler != null ) - { - batchHandler.ODataRouteName = unversionedRouteName; - var batchTemplate = IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch; - routes.MapHttpBatchRoute( routeName + nameof( ODataRouteConstants.Batch ), batchTemplate, batchHandler ); - } - - if ( pathHandler != null && pathHandler.UrlKeyDelimiter == null ) - { - pathHandler.UrlKeyDelimiter = configuration.GetUrlKeyDelimiter(); - } - - routeConventions.Insert( 0, default! ); - - var odataRoutes = new List(); - var unversionedConstraints = new List(); - - foreach ( var model in models ) - { - var versionedRouteName = routeName; - var annotation = model.GetAnnotationValue( model ) ?? throw new InvalidOperationException( LocalSR.MissingAnnotation.FormatDefault( typeof( ApiVersionAnnotation ).Name ) ); - var apiVersion = annotation.ApiVersion; - var routeConstraint = MakeVersionedODataRouteConstraint( apiVersion, ref versionedRouteName ); - - routeConventions[0] = new VersionedAttributeRoutingConvention( versionedRouteName, configuration, apiVersion ); - unversionedConstraints.Add( new ODataPathRouteConstraint( versionedRouteName ) ); - - var edm = model; - var rootContainer = configuration.CreateODataRootContainer( - versionedRouteName, - builder => builder.AddService( Singleton, typeof( IEdmModel ), sp => edm ) - .AddService( Singleton, typeof( IODataPathHandler ), sp => pathHandler ) - .AddService( Singleton, typeof( IEnumerable ), sp => routeConventions.ToArray() ) - .AddService( Singleton, typeof( ODataBatchHandler ), sp => batchHandler ) ); - - rootContainer.InitializeAttributeRouting(); - - var route = default( ODataRoute ); - var messageHandler = rootContainer.GetService(); - var options = configuration.GetApiVersioningOptions(); - - if ( messageHandler == null ) - { - route = new ODataRoute( routePrefix, routeConstraint ); - } - else - { - route = new ODataRoute( routePrefix, routeConstraint, defaults: null, constraints: null, dataTokens: null, handler: messageHandler ); - } - - routes.Add( versionedRouteName, route ); - AddApiVersionConstraintIfNecessary( route, options ); - odataRoutes.Add( route ); - } - - configuration.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( unversionedRouteName, routePrefix, odataRoutes, unversionedConstraints, _ => { } ); - - return odataRoutes; + return configuration.MapVersionedODataRoute( routeName, routePrefix, modelBuilder.GetEdmModels( routePrefix ), configureAction ); } /// @@ -376,11 +72,25 @@ public static IReadOnlyList MapVersionedODataRoutes( /// The server configuration. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The API version associated with the model. + /// The sequence of EDM models to use for parsing OData paths. /// The configuring action to add the services to the root container. /// The added . - public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configuration, string routeName, string routePrefix, ApiVersion apiVersion, Action? configureAction ) => - MapVersionedODataRoute( configuration, routeName, routePrefix, apiVersion, configureAction, default ); + public static ODataRoute MapVersionedODataRoute( + this HttpConfiguration configuration, + string routeName, + string routePrefix, + IEnumerable models, + Action configureAction ) => + AddApiVersionConstraintIfNecessary( + configuration, + configuration.MapODataServiceRoute( + routeName, + routePrefix, + builder => + { + builder.AddApiVersioning( routeName, models ); + configureAction?.Invoke( builder ); + } ) ); /// /// Maps the specified OData route and the OData route attributes. @@ -388,133 +98,40 @@ public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configur /// The server configuration. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The API version associated with the model. - /// The configuring action to add the services to the root container. - /// The configuring action to add or update routing conventions. + /// The sequence of EDM models to use for parsing OData paths. /// The added . public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configuration, string routeName, string routePrefix, - ApiVersion apiVersion, - Action? configureAction, - Action? configureRoutingConventions ) - { - if ( configuration == null ) - { - throw new ArgumentNullException( nameof( configuration ) ); - } - - object ConfigureRoutingConventions( IServiceProvider serviceProvider ) - { - var model = serviceProvider.GetRequiredService(); - var routingConventions = VersionedODataRoutingConventions.CreateDefault(); - var context = new ODataConventionConfigurationContext( configuration, routeName, model, apiVersion, routingConventions ); - - model.SetAnnotationValue( model, new ApiVersionAnnotation( apiVersion ) ); - routingConventions.Insert( 0, new VersionedAttributeRoutingConvention( routeName, configuration, apiVersion ) ); - configureRoutingConventions?.Invoke( context ); - - return context.RoutingConventions.ToArray(); - } - - if ( !IsNullOrEmpty( routePrefix ) ) - { - routePrefix = routePrefix.TrimEnd( '/' ); - } - - var rootContainer = configuration.CreateODataRootContainer( - routeName, - builder => - { - builder.AddService( Singleton, typeof( IEnumerable ), ConfigureRoutingConventions ); - configureAction?.Invoke( builder ); - } ); - var pathHandler = rootContainer.GetRequiredService(); - - if ( pathHandler != null && pathHandler.UrlKeyDelimiter == null ) - { - pathHandler.UrlKeyDelimiter = configuration.GetUrlKeyDelimiter(); - } - - rootContainer.InitializeAttributeRouting(); - - var routeConstraint = new VersionedODataPathRouteConstraint( routeName, apiVersion ); - var route = default( ODataRoute ); - var routes = configuration.Routes; - var messageHandler = rootContainer.GetService(); - var options = configuration.GetApiVersioningOptions(); - - if ( messageHandler != null ) - { - route = new ODataRoute( - routePrefix, - routeConstraint, - defaults: null, - constraints: null, - dataTokens: null, - handler: messageHandler ); - } - else - { - var batchHandler = rootContainer.GetService(); - - if ( batchHandler != null ) - { - batchHandler.ODataRouteName = routeName; - var batchTemplate = IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch; - routes.MapHttpBatchRoute( routeName + nameof( ODataRouteConstants.Batch ), batchTemplate, batchHandler ); - } - - route = new ODataRoute( routePrefix, routeConstraint ); - } - - routes.Add( routeName, route ); - AddApiVersionConstraintIfNecessary( route, options ); - - var unversionedRouteConstraint = new ODataPathRouteConstraint( routeName ); - var unversionedRoute = new ODataRoute( routePrefix, new UnversionedODataPathRouteConstraint( unversionedRouteConstraint, apiVersion ) ); - - AddApiVersionConstraintIfNecessary( unversionedRoute, options ); - configuration.Routes.Add( routeName + UnversionedRouteSuffix, unversionedRoute ); - - return route; - } - - /// - /// Maps a versioned OData route. - /// - /// The extended HTTP configuration. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The EDM model to use for parsing OData paths. - /// The API version associated with the model. - /// The mapped OData route. - /// The API version annotation will be added or updated on the specified using - /// the provided API version. - public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configuration, string routeName, string routePrefix, IEdmModel model, ApiVersion apiVersion ) => - MapVersionedODataRoute( configuration, routeName, routePrefix, model, apiVersion, new DefaultODataPathHandler(), VersionedODataRoutingConventions.CreateDefault(), default, default ); + IEnumerable models ) => + AddApiVersionConstraintIfNecessary( + configuration, + configuration.MapODataServiceRoute( routeName, routePrefix, builder => builder.AddApiVersioning( routeName, models ) ) ); /// - /// Maps a versioned OData route. + /// Maps the specified OData route and the OData route attributes. When the is + /// non-null, it will create a '$batch' endpoint to handle the batch requests. /// - /// The extended HTTP configuration. + /// The server configuration. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The EDM model to use for parsing OData paths. - /// The API version associated with the model. - /// The OData batch handler. - /// The mapped OData route. - /// The API version annotation will be added or updated on the specified using - /// the provided API version. + /// The sequence of EDM models to use for parsing OData paths. + /// The . + /// The added . public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configuration, string routeName, string routePrefix, - IEdmModel model, - ApiVersion apiVersion, - ODataBatchHandler? batchHandler ) => - MapVersionedODataRoute( configuration, routeName, routePrefix, model, apiVersion, new DefaultODataPathHandler(), VersionedODataRoutingConventions.CreateDefault(), batchHandler, default ); + IEnumerable models, + ODataBatchHandler batchHandler ) => + AddApiVersionConstraintIfNecessary( + configuration, + configuration.MapODataServiceRoute( + routeName, + routePrefix, + builder => builder.AddApiVersioning( routeName, models ) + .AddService( Singleton, sp => batchHandler ) ) ); /// /// Maps the specified OData route and the OData route attributes. When the @@ -523,68 +140,77 @@ public static ODataRoute MapVersionedODataRoute( /// The server configuration. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The EDM model to use for parsing OData paths. - /// The API version associated with the model. + /// The sequence of EDM models to use for parsing OData paths. /// The default for this route. /// The added . public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configuration, string routeName, string routePrefix, - IEdmModel model, - ApiVersion apiVersion, - HttpMessageHandler? defaultHandler ) => - MapVersionedODataRoute( configuration, routeName, routePrefix, model, apiVersion, new DefaultODataPathHandler(), VersionedODataRoutingConventions.CreateDefault(), default, defaultHandler ); + IEnumerable models, + HttpMessageHandler defaultHandler ) => + AddApiVersionConstraintIfNecessary( + configuration, + configuration.MapODataServiceRoute( + routeName, + routePrefix, + builder => builder.AddApiVersioning( routeName, models ) + .AddService( Singleton, sp => defaultHandler ) ) ); /// - /// Maps a versioned OData route. + /// Maps the specified OData route. /// - /// The extended HTTP configuration. + /// The server configuration. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The EDM model to use for parsing OData paths. - /// The API version associated with the model. - /// The OData path handler to use for parsing the OData path. - /// The sequence of OData routing conventions - /// to use for controller and action selection. - /// The mapped OData route. - /// The API version annotation will be added or updated on the specified using - /// the provided API version. + /// The sequence of EDM models to use for parsing OData paths. + /// The to use for parsing the OData path. + /// The OData routing conventions to use for controller and action selection. + /// The added . public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configuration, string routeName, string routePrefix, - IEdmModel model, - ApiVersion apiVersion, + IEnumerable models, IODataPathHandler pathHandler, IEnumerable routingConventions ) => - MapVersionedODataRoute( configuration, routeName, routePrefix, model, apiVersion, pathHandler, routingConventions, default, default ); + AddApiVersionConstraintIfNecessary( + configuration, + configuration.MapODataServiceRoute( + routeName, + routePrefix, + builder => builder.AddApiVersioning( models, routingConventions ) + .AddService( Singleton, sp => pathHandler ) ) ); /// - /// Maps a versioned OData route. When the is provided, it will create a '$batch' endpoint to handle the batch requests. + /// Maps the specified OData route. When the is non-null, it will + /// create a '$batch' endpoint to handle the batch requests. /// - /// The extended HTTP configuration. + /// The server configuration. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The EDM model to use for parsing OData paths. - /// The API version associated with the model. - /// The OData path handler to use for parsing the OData path. - /// The sequence of OData routing conventions - /// to use for controller and action selection. - /// The OData batch handler. - /// The mapped OData route. - /// The API version annotation will be added or updated on the specified using - /// the provided API version. + /// The sequence of EDM models to use for parsing OData paths. + /// The to use for parsing the OData path. + /// The OData routing conventions to use for controller and action selection. + /// The . + /// The added . public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configuration, string routeName, string routePrefix, - IEdmModel model, - ApiVersion apiVersion, + IEnumerable models, IODataPathHandler pathHandler, IEnumerable routingConventions, - ODataBatchHandler? batchHandler ) => - MapVersionedODataRoute( configuration, routeName, routePrefix, model, apiVersion, pathHandler, routingConventions, batchHandler, default ); + ODataBatchHandler batchHandler ) => + AddApiVersionConstraintIfNecessary( + configuration, + configuration.MapODataServiceRoute( + routeName, + routePrefix, + builder => + builder.AddApiVersioning( models, routingConventions ) + .AddService( Singleton, sp => pathHandler ) + .AddService( Singleton, sp => batchHandler ) ) ); /// /// Maps the specified OData route. When the is non-null, it will map @@ -593,8 +219,7 @@ public static ODataRoute MapVersionedODataRoute( /// The server configuration. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The EDM model to use for parsing OData paths. - /// The API version associated with the model. + /// The sequence of EDM models to use for parsing OData paths. /// The to use for parsing the OData path. /// The OData routing conventions to use for controller and action selection. /// The default for this route. @@ -603,63 +228,24 @@ public static ODataRoute MapVersionedODataRoute( this HttpConfiguration configuration, string routeName, string routePrefix, - IEdmModel model, - ApiVersion apiVersion, + IEnumerable models, IODataPathHandler pathHandler, IEnumerable routingConventions, - HttpMessageHandler? defaultHandler ) => - MapVersionedODataRoute( configuration, routeName, routePrefix, model, apiVersion, pathHandler, routingConventions, default, defaultHandler ); - - /// - /// Gets the configured entity data model (EDM) for the specified API version. - /// - /// The server configuration. - /// The API version to get the model for. - /// The matching EDM model or null. - public static IEdmModel? GetEdmModel( this HttpConfiguration configuration, ApiVersion apiVersion ) - { - if ( configuration == null ) - { - throw new ArgumentNullException( nameof( configuration ) ); - } - - var routes = configuration.Routes.ToDictionary(); - var containers = configuration.GetRootContainerMappings(); - - foreach ( var route in routes ) - { - if ( !( route.Value is ODataRoute odataRoute ) ) - { - continue; - } - - if ( !containers.TryGetValue( route.Key, out var serviceProvider ) ) - { - continue; - } - - var model = serviceProvider.GetService(); - - if ( model?.EntityContainer == null ) - { - continue; - } - - var modelApiVersion = model.GetAnnotationValue( model )?.ApiVersion; - - if ( modelApiVersion == apiVersion ) - { - return model; - } - } - - return null; - } - - internal static IServiceProvider GetODataRootContainer( this HttpConfiguration configuration, string routeName ) => configuration.GetRootContainerMappings()[routeName]; + HttpMessageHandler defaultHandler ) => + AddApiVersionConstraintIfNecessary( + configuration, + configuration.MapODataServiceRoute( + routeName, + routePrefix, + builder => + builder.AddApiVersioning( models, routingConventions ) + .AddService( Singleton, sp => pathHandler ) + .AddService( Singleton, sp => defaultHandler ) ) ); internal static ODataUrlKeyDelimiter? GetUrlKeyDelimiter( this HttpConfiguration configuration ) { + const string UrlKeyDelimiterKey = "Microsoft.AspNet.OData.UrlKeyDelimiterKey"; + if ( configuration.Properties.TryGetValue( UrlKeyDelimiterKey, out var value ) ) { return value as ODataUrlKeyDelimiter; @@ -669,187 +255,49 @@ public static ODataRoute MapVersionedODataRoute( return null; } - static ODataRoute MapVersionedODataRoute( - HttpConfiguration configuration, - string routeName, - string routePrefix, - IEdmModel model, - ApiVersion apiVersion, - IODataPathHandler pathHandler, - IEnumerable routingConventions, - ODataBatchHandler? batchHandler, - HttpMessageHandler? defaultHandler ) + static ODataRoute AddApiVersionConstraintIfNecessary( HttpConfiguration configuration, ODataRoute route ) { if ( configuration == null ) { throw new ArgumentNullException( nameof( configuration ) ); } - var routeConventions = VersionedODataRoutingConventions.AddOrUpdate( routingConventions.ToList() ); - var routes = configuration.Routes; - - if ( !IsNullOrEmpty( routePrefix ) ) - { - routePrefix = routePrefix.TrimEnd( '/' ); - } + var routePrefix = route.RoutePrefix; - if ( pathHandler != null && pathHandler.UrlKeyDelimiter == null ) + if ( string.IsNullOrEmpty( routePrefix ) ) { - pathHandler.UrlKeyDelimiter = configuration.GetUrlKeyDelimiter(); + return route; } - model.SetAnnotationValue( model, new ApiVersionAnnotation( apiVersion ) ); - routeConventions.Insert( 0, new VersionedAttributeRoutingConvention( routeName, configuration, apiVersion ) ); - - var rootContainer = configuration.CreateODataRootContainer( - routeName, - builder => builder.AddService( Singleton, typeof( IEdmModel ), sp => model ) - .AddService( Singleton, typeof( IODataPathHandler ), sp => pathHandler ) - .AddService( Singleton, typeof( IEnumerable ), sp => routeConventions.ToArray() ) - .AddService( Singleton, typeof( ODataBatchHandler ), sp => batchHandler ) - .AddService( Singleton, typeof( HttpMessageHandler ), sp => defaultHandler ) ); - - rootContainer.InitializeAttributeRouting(); - - var routeConstraint = new VersionedODataPathRouteConstraint( routeName, apiVersion ); - var route = default( ODataRoute ); var options = configuration.GetApiVersioningOptions(); - if ( defaultHandler != null ) - { - route = new ODataRoute( routePrefix, routeConstraint, defaults: null, constraints: null, dataTokens: null, handler: defaultHandler ); - } - else - { - if ( batchHandler != null ) - { - batchHandler.ODataRouteName = routeName; - var batchTemplate = IsNullOrEmpty( routePrefix ) ? ODataRouteConstants.Batch : routePrefix + '/' + ODataRouteConstants.Batch; - routes.MapHttpBatchRoute( routeName + nameof( ODataRouteConstants.Batch ), batchTemplate, batchHandler ); - } - - route = new ODataRoute( routePrefix, routeConstraint ); - } - - routes.Add( routeName, route ); - AddApiVersionConstraintIfNecessary( route, options ); - - var unversionedRouteConstraint = new ODataPathRouteConstraint( routeName ); - var unversionedRoute = new ODataRoute( routePrefix, new UnversionedODataPathRouteConstraint( unversionedRouteConstraint, apiVersion ) ); - - AddApiVersionConstraintIfNecessary( unversionedRoute, options ); - routes.Add( routeName + UnversionedRouteSuffix, unversionedRoute ); - - return route; - } - - static ODataPathRouteConstraint MakeVersionedODataRouteConstraint( ApiVersion? apiVersion, ref string versionedRouteName ) - { - if ( apiVersion == null ) + if ( route.Constraints.ContainsKey( options.RouteConstraintName ) ) { - return new ODataPathRouteConstraint( versionedRouteName ); + return route; } - versionedRouteName += "-" + apiVersion.ToString(); - return new VersionedODataPathRouteConstraint( versionedRouteName, apiVersion ); - } - - static void AddApiVersionConstraintIfNecessary( ODataRoute route, ApiVersioningOptions options ) - { - var routePrefix = route.RoutePrefix; var apiVersionConstraint = "{" + options.RouteConstraintName + "}"; + var absent = routePrefix.IndexOf( apiVersionConstraint, StringComparison.Ordinal ) < 0; - if ( routePrefix == null || routePrefix.IndexOf( apiVersionConstraint, Ordinal ) < 0 || route.Constraints.ContainsKey( options.RouteConstraintName ) ) + if ( absent ) { - return; + return route; } // note: even though the constraints are a dictionary, it's important to rebuild the entire collection // to make sure the api version constraint is evaluated first; otherwise, the current api version will // not be resolved when the odata versioning constraint is evaluated - var originalConstraints = new Dictionary( route.Constraints ); + var constraints = route.Constraints.ToArray(); route.Constraints.Clear(); route.Constraints.Add( options.RouteConstraintName, new ApiVersionRouteConstraint() ); - foreach ( var constraint in originalConstraints ) - { - route.Constraints.Add( constraint.Key, constraint.Value ); - } - } - - static void AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( - this HttpConfiguration configuration, - string routeName, - string routePrefix, - List odataRoutes, - List unversionedConstraints, - Action? configureAction ) - { - var unversionedRoute = new ODataRoute( routePrefix, new UnversionedODataPathRouteConstraint( unversionedConstraints ) ); - var options = configuration.GetApiVersioningOptions(); - - AddApiVersionConstraintIfNecessary( unversionedRoute, options ); - configuration.Routes.Add( routeName, unversionedRoute ); - odataRoutes.Add( unversionedRoute ); - configuration.CreateODataRootContainer( routeName, configureAction ); - } - - static IServiceProvider CreateODataRootContainer( this HttpConfiguration configuration, string routeName, Action? configureAction ) - { - var rootContainer = configuration.CreateRootContainerImplementation( configureAction ); - configuration.SetODataRootContainer( routeName, rootContainer ); - return rootContainer; - } - - static void SetODataRootContainer( this HttpConfiguration configuration, string routeName, IServiceProvider rootContainer ) => - configuration.GetRootContainerMappings()[routeName] = rootContainer; - - static ConcurrentDictionary GetRootContainerMappings( this HttpConfiguration configuration ) => - (ConcurrentDictionary) configuration.Properties.GetOrAdd( RootContainerMappingsKey, key => new ConcurrentDictionary() ); - - static IServiceProvider CreateRootContainerImplementation( this HttpConfiguration configuration, Action? configureAction ) - { - var builder = configuration.CreateContainerBuilderWithDefaultServices(); - - configureAction?.Invoke( builder ); - - var rootContainer = builder.BuildContainer(); - - if ( rootContainer == null ) - { - throw new InvalidOperationException( SR.NullContainer ); - } - - return rootContainer; - } - - static IContainerBuilder CreateContainerBuilderWithDefaultServices( this HttpConfiguration configuration ) - { - IContainerBuilder builder; - - if ( configuration.Properties.TryGetValue( ContainerBuilderFactoryKey, out var value ) ) + for ( var i = 0; i < constraints.Length; i++ ) { - var builderFactory = (Func) value; - - builder = builderFactory(); - - if ( builder == null ) - { - throw new InvalidOperationException( SR.NullContainerBuilder ); - } - } - else - { - builder = new DefaultContainerBuilder(); + route.Constraints.Add( constraints[i].Key, constraints[i].Value ); } - builder.AddService( Singleton, sp => configuration ); - builder.AddService( Singleton, sp => configuration.GetDefaultQuerySettings() ); - builder.AddDefaultODataServices(); - builder.AddDefaultWebApiServices(); - - return builder; + return route; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs index d7b04823..8bee2d14 100644 --- a/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs +++ b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/HttpRequestMessageExtensions.cs @@ -1,33 +1,26 @@ namespace System.Web.Http { + using Microsoft.OData; + using Microsoft.Web.Http; using Microsoft.Web.Http.Versioning; using System.Net.Http; + using static System.Net.HttpStatusCode; - /// - /// Provides extension methods for the class. - /// - public static class HttpRequestMessageExtensions + static class HttpRequestMessageExtensions { - const string ODataApiVersionPropertiesKey = "MS_" + nameof( ODataApiVersionRequestProperties ); - - /// - /// Gets the current OData API versioning request properties. - /// - /// The request to get the OData API versioning properties for. - /// The current OData API versioning properties. - public static ODataApiVersionRequestProperties ODataApiVersionProperties( this HttpRequestMessage request ) + internal static ApiVersion? GetRequestedApiVersionOrReturnBadRequest( this HttpRequestMessage request ) { - if ( request == null ) + var properties = request.ApiVersionProperties(); + + try { - throw new ArgumentNullException( nameof( request ) ); + return properties.RequestedApiVersion; } - - if ( !request.Properties.TryGetValue( ODataApiVersionPropertiesKey, out var value ) || !( value is ODataApiVersionRequestProperties properties ) ) + catch ( AmbiguousApiVersionException ex ) { - request.Properties[ODataApiVersionPropertiesKey] = properties = new ODataApiVersionRequestProperties(); + var error = new ODataError() { ErrorCode = "AmbiguousApiVersion", Message = ex.Message }; + throw new HttpResponseException( request.CreateResponse( BadRequest, error ) ); } - - return properties; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/IContainerBuilderExtensions.cs b/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/IContainerBuilderExtensions.cs deleted file mode 100644 index a2755a70..00000000 --- a/src/Microsoft.AspNet.OData.Versioning/System.Web.Http/IContainerBuilderExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace System.Web.Http -{ - using Microsoft.AspNet.OData.Routing.Conventions; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.OData; - using System.Reflection; - using static System.Linq.Expressions.Expression; - - static class IContainerBuilderExtensions - { - private static readonly Lazy> addDefaultWebApiServices = new Lazy>( NewAddDefaultWebApiServicesFunc ); - - internal static void InitializeAttributeRouting( this IServiceProvider serviceProvider ) => serviceProvider.GetServices(); - - internal static void AddDefaultWebApiServices( this IContainerBuilder builder ) => addDefaultWebApiServices.Value( builder ); - - static Action NewAddDefaultWebApiServicesFunc() - { - var type = Type.GetType( "Microsoft.AspNet.OData.Extensions.ContainerBuilderExtensions, Microsoft.AspNet.OData", throwOnError: true ); - var method = type.GetRuntimeMethod( nameof( AddDefaultWebApiServices ), new[] { typeof( IContainerBuilder ) } ); - var builder = Parameter( typeof( IContainerBuilder ), "builder" ); - var body = Call( null, method, builder ); - var lambda = Lambda>( body, builder ); - - return lambda.Compile(); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs index 8ac750c9..acb73c2a 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Description/VersionedApiExplorer.cs @@ -109,20 +109,7 @@ protected virtual Collection GetHttpMethodsSupportedByAction( IHttpR throw new ArgumentNullException( nameof( actionDescriptor ) ); } - IList supportedMethods; - IList actionHttpMethods = actionDescriptor.SupportedHttpMethods; - var httpMethodConstraint = route.Constraints.Values.OfType().FirstOrDefault(); - - if ( httpMethodConstraint == null ) - { - supportedMethods = actionHttpMethods; - } - else - { - supportedMethods = httpMethodConstraint.AllowedMethods.Intersect( actionHttpMethods ).ToList(); - } - - return new Collection( supportedMethods ); + return new Collection( actionDescriptor.GetHttpMethods( route ) ); } /// diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.csproj b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.csproj index 91518fe9..221698c8 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.csproj +++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/Microsoft.AspNet.WebApi.Versioning.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 4.0.0 - 4.0.0.0 + 4.1.0 + 4.1.0.0 net45 Microsoft ASP.NET Web API Versioned API Explorer The API Explorer for Microsoft ASP.NET Web API Versioning. diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpActionDescriptorExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpActionDescriptorExtensions.cs new file mode 100644 index 00000000..1e8f4cdb --- /dev/null +++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/System.Web.Http/Controllers/HttpActionDescriptorExtensions.cs @@ -0,0 +1,24 @@ +namespace System.Web.Http.Controllers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Web.Http.Routing; + + static class HttpActionDescriptorExtensions + { + internal static IList GetHttpMethods( this HttpActionDescriptor actionDescriptor, IHttpRoute route ) + { + IList actionHttpMethods = actionDescriptor.SupportedHttpMethods; + var httpMethodConstraint = route.Constraints.Values.OfType().FirstOrDefault(); + + if ( httpMethodConstraint == null ) + { + return actionHttpMethods; + } + + return httpMethodConstraint.AllowedMethods.Intersect( actionHttpMethods ).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Controllers/ApiVersionActionSelector.cs b/src/Microsoft.AspNet.WebApi.Versioning/Controllers/ApiVersionActionSelector.cs index cfcebd8f..dce37907 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Controllers/ApiVersionActionSelector.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/Controllers/ApiVersionActionSelector.cs @@ -47,7 +47,7 @@ public virtual ILookup GetActionMapping( HttpContr throw new ArgumentNullException( nameof( controllerDescriptor ) ); } - var actionMappings = ( from descriptor in controllerDescriptor.AsEnumerable() + var actionMappings = ( from descriptor in controllerDescriptor.AsEnumerable( includeCandidates: true ) let selector = GetInternalSelector( descriptor ) select selector.GetActionMapping() ).ToArray(); diff --git a/src/Microsoft.AspNet.WebApi.Versioning/Microsoft.AspNet.WebApi.Versioning.csproj b/src/Microsoft.AspNet.WebApi.Versioning/Microsoft.AspNet.WebApi.Versioning.csproj index 03687000..993e8b45 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/Microsoft.AspNet.WebApi.Versioning.csproj +++ b/src/Microsoft.AspNet.WebApi.Versioning/Microsoft.AspNet.WebApi.Versioning.csproj @@ -1,8 +1,8 @@  - 4.0.0 - 4.0.0.0 + 4.1.0 + 4.1.0.0 net45 Microsoft ASP.NET Web API Versioning A service API versioning library for Microsoft ASP.NET Web API. diff --git a/src/Microsoft.AspNet.WebApi.Versioning/ReleaseNotes.txt b/src/Microsoft.AspNet.WebApi.Versioning/ReleaseNotes.txt index 5f282702..63cacfaf 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/ReleaseNotes.txt +++ b/src/Microsoft.AspNet.WebApi.Versioning/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +HttpControllerDescriptorExtensions.AsEnumerable is now public \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs index 6ad849c5..a898df24 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpActionDescriptorExtensions.cs @@ -103,14 +103,13 @@ public static ApiVersionMapping MappingTo( this HttpActionDescriptor action, Api /// The API version to test the mapping for. /// True if the explicitly or implicitly maps to the specified /// API version; otherwise, false. - public static bool IsMappedTo( this HttpActionDescriptor action, ApiVersion apiVersion ) => action.MappingTo( apiVersion ) > None; + public static bool IsMappedTo( this HttpActionDescriptor action, ApiVersion? apiVersion ) => action.MappingTo( apiVersion ) > None; internal static bool IsAttributeRouted( this HttpActionDescriptor action ) => action.Properties.TryGetValue( AttributeRoutedPropertyKey, out bool? value ) && ( value ?? false ); internal static T? GetProperty( this HttpActionDescriptor action ) where T : class => action.Properties.TryGetValue( typeof( T ), out T value ) ? value : default; -#pragma warning restore CS8603 // Possible null reference return. internal static void SetProperty( this HttpActionDescriptor action, T value ) => action.Properties.AddOrUpdate( typeof( T ), value, ( key, oldValue ) => value ); diff --git a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpControllerDescriptorExtensions.cs b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpControllerDescriptorExtensions.cs index f02cd824..9ae807ac 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpControllerDescriptorExtensions.cs +++ b/src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpControllerDescriptorExtensions.cs @@ -1,6 +1,7 @@ namespace System.Web.Http { using Microsoft.Web.Http; + using Microsoft.Web.Http.Controllers; using Microsoft.Web.Http.Versioning; using System; using System.Collections.Generic; @@ -30,38 +31,42 @@ public static class HttpControllerDescriptorExtensions public static ApiVersionModel GetApiVersionModel( this HttpControllerDescriptor controllerDescriptor ) => controllerDescriptor.GetProperty() ?? ApiVersionModel.Empty; - internal static void SetApiVersionModel( this HttpControllerDescriptor controller, ApiVersionModel value ) => controller.SetProperty( value ); + /// + /// Enumerates a controller descriptor as a sequence of descriptors. + /// + /// The controller descriptor to enumerate. + /// A sequence of controller descriptors. + /// This method will flatten a sequence of composite descriptors such as . + /// If the controller descriptor is not a composite, it yields itself. + public static IEnumerable AsEnumerable( this HttpControllerDescriptor controllerDescriptor ) => + AsEnumerable( controllerDescriptor, includeCandidates: false ); - internal static bool IsAttributeRouted( this HttpControllerDescriptor controller ) + internal static IEnumerable AsEnumerable( this HttpControllerDescriptor controllerDescriptor, bool includeCandidates ) { - controller.Properties.TryGetValue( AttributeRoutedPropertyKey, out bool? value ); - return value ?? false; - } - - internal static void SetPossibleCandidates( this HttpControllerDescriptor controllerDescriptor, IEnumerable value ) => - controllerDescriptor.Properties.AddOrUpdate( PossibleControllerCandidatesKey, value, ( key, oldValue ) => value ); + if ( controllerDescriptor == null ) + { + throw new ArgumentNullException( nameof( controllerDescriptor ) ); + } - internal static IEnumerable AsEnumerable( this HttpControllerDescriptor controller ) - { var visited = new HashSet(); - if ( controller is IEnumerable groupedControllers ) + if ( controllerDescriptor is IEnumerable groupedDescriptors ) { - foreach ( var groupedController in groupedControllers ) + foreach ( var groupedDescriptor in groupedDescriptors ) { - if ( visited.Add( groupedController ) ) + if ( visited.Add( groupedDescriptor ) ) { - yield return groupedController; + yield return groupedDescriptor; } } } else { - visited.Add( controller ); - yield return controller; + visited.Add( controllerDescriptor ); + yield return controllerDescriptor; } - if ( !controller.Properties.TryGetValue( PossibleControllerCandidatesKey, out IEnumerable candidates ) ) + if ( !includeCandidates || !controllerDescriptor.Properties.TryGetValue( PossibleControllerCandidatesKey, out IEnumerable candidates ) ) { yield break; } @@ -77,6 +82,17 @@ internal static IEnumerable AsEnumerable( this HttpCon visited.Clear(); } + internal static void SetApiVersionModel( this HttpControllerDescriptor controller, ApiVersionModel value ) => controller.SetProperty( value ); + + internal static bool IsAttributeRouted( this HttpControllerDescriptor controller ) + { + controller.Properties.TryGetValue( AttributeRoutedPropertyKey, out bool? value ); + return value ?? false; + } + + internal static void SetPossibleCandidates( this HttpControllerDescriptor controllerDescriptor, IEnumerable value ) => + controllerDescriptor.Properties.AddOrUpdate( PossibleControllerCandidatesKey, value, ( key, oldValue ) => value ); + static T GetProperty( this HttpControllerDescriptor controller ) { if ( controller == null ) diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionMatcherPolicy.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionMatcherPolicy.cs index 36b4c5a0..44d68403 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionMatcherPolicy.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ApiVersionMatcherPolicy.cs @@ -60,12 +60,20 @@ public bool AppliesToEndpoints( IReadOnlyList endpoints ) for ( var i = 0; i < endpoints.Count; i++ ) { - var action = endpoints[i].Metadata?.GetMetadata(); + var metadata = endpoints[i].Metadata; + var action = metadata.GetMetadata(); if ( action?.GetProperty() != null ) { return true; } + + var endpoint = metadata.GetMetadata(); + + if ( endpoint?.IsDynamic == true ) + { + return true; + } } return false; @@ -233,7 +241,14 @@ RequestHandler ClientError( HttpContext httpContext, CandidateSet candidateSet ) for ( var i = 0; i < candidateSet.Count; i++ ) { ref var candidate = ref candidateSet[i]; - var action = candidate.Endpoint.Metadata?.GetMetadata(); + var endpoint = candidate.Endpoint; + + if ( endpoint == null ) + { + continue; + } + + var action = endpoint.Metadata.GetMetadata(); if ( action != null ) { diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/CatchAllRouteHandler.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/CatchAllRouteHandler.cs index d4d064fa..2fd21507 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/CatchAllRouteHandler.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/CatchAllRouteHandler.cs @@ -3,10 +3,10 @@ using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.AspNetCore.Routing; using System.Threading.Tasks; + using static System.Threading.Tasks.Task; sealed class CatchAllRouteHandler : IRouter { - static readonly Task CompletedTask = Task.FromResult( default( object ) ); readonly IApiVersionRoutePolicy routePolicy; public CatchAllRouteHandler( IApiVersionRoutePolicy routePolicy ) => this.routePolicy = routePolicy; diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ClientErrorBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ClientErrorBuilder.cs index f9e56248..d98001a2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ClientErrorBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/ClientErrorBuilder.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; + using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.Extensions.Logging; using System; @@ -66,7 +67,7 @@ internal RequestHandler Build() requestedVersion = parsedVersion.ToString(); } - return Unmatched( handlerContext, url, method, allowedMethods.Value, actionNames.Value, parsedVersion, requestedVersion ); + return Unmatched( handlerContext, url, method, allowedMethods.Value, actionNames.Value, parsedVersion!, requestedVersion, apiVersions ); } static HashSet AllowedMethodsFromCandidates( IEnumerable candidates, ApiVersion? apiVersion ) @@ -75,18 +76,60 @@ static HashSet AllowedMethodsFromCandidates( IEnumerable GetHttpMethods( ActionDescriptor action ) + { + if ( action.ActionConstraints != null ) + { + foreach ( var constraint in action.ActionConstraints.OfType() ) + { + foreach ( var method in constraint.HttpMethods ) + { + yield return method; + } + } + } + + if ( action is ControllerActionDescriptor controllerAction ) + { + foreach ( var attribute in controllerAction.MethodInfo.GetCustomAttributes( inherit: false ).OfType() ) + { + foreach ( var method in attribute.HttpMethods ) + { + yield return method; + } + } + } + } + + bool MethodSupportedInAnyOtherVersion( string method, ApiVersion version ) + { + var comparer = StringComparer.OrdinalIgnoreCase; + + foreach ( var candidate in Candidates! ) + { + if ( candidate.IsMappedTo( version ) ) { continue; } - foreach ( var constraint in candidate.ActionConstraints.OfType() ) + var methods = GetHttpMethods( candidate ); + + if ( methods.Contains( method, comparer ) ) { - httpMethods.AddRange( constraint.HttpMethods ); + return true; } } - return httpMethods; + return false; } RequestHandler VersionNeutralUnmatched( @@ -135,22 +178,30 @@ RequestHandler Unmatched( string method, IReadOnlyCollection allowedMethods, string actionNames, - ApiVersion? parsedVersion, - string? requestedVersion ) + ApiVersion version, + string? rawVersion, + Lazy aggregateModel ) { - Logger!.ApiVersionUnmatched( parsedVersion, actionNames ); + Logger!.ApiVersionUnmatched( version, actionNames ); context.Code = UnsupportedApiVersion; - if ( allowedMethods.Count == 0 || allowedMethods.Contains( method ) ) + var methodNotAllowed = + ( allowedMethods.Count > 0 && + !allowedMethods.Contains( method ) ) || + ( allowedMethods.Count == 0 && + aggregateModel.Value.ImplementedApiVersions.Contains( version ) && + MethodSupportedInAnyOtherVersion( method, version ) ); + + if ( methodNotAllowed ) { - context.Message = SR.VersionedResourceNotSupported.FormatDefault( requestUrl, requestedVersion ); - return new BadRequestHandler( context ); - } + context.Message = SR.VersionedMethodNotSupported.FormatDefault( requestUrl, rawVersion, method ); + context.AllowedMethods = allowedMethods.Count == 0 ? new[] { method } : allowedMethods.ToArray(); - context.Message = SR.VersionedMethodNotSupported.FormatDefault( requestUrl, requestedVersion, method ); - context.AllowedMethods = allowedMethods.ToArray(); + return new MethodNotAllowedHandler( context ); + } - return new MethodNotAllowedHandler( context ); + context.Message = SR.VersionedResourceNotSupported.FormatDefault( requestUrl, rawVersion ); + return new BadRequestHandler( context ); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/DefaultApiVersionRoutePolicy.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/DefaultApiVersionRoutePolicy.cs index d2fb850c..3015e8a8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/DefaultApiVersionRoutePolicy.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/DefaultApiVersionRoutePolicy.cs @@ -142,7 +142,7 @@ protected virtual void OnUnmatched( RouteContext context, ActionSelectionResult handler = builder.Build(); } - context.Handler = handler!; + context.SetHandlerOrEndpoint( handler ); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/RouteContextExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/RouteContextExtensions.cs new file mode 100644 index 00000000..7c117d98 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/RouteContextExtensions.cs @@ -0,0 +1,27 @@ +namespace Microsoft.AspNetCore.Mvc.Routing +{ + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.AspNetCore.Routing; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + + static class RouteContextExtensions + { + // HACK: supports OData which will call back through IActionSelector in its endpoint routing implementation + internal static void SetHandlerOrEndpoint( this RouteContext context, RequestHandler? handler ) + { + var httpContext = context.HttpContext; + var options = httpContext.RequestServices.GetRequiredService>().Value; + + if ( options.EnableEndpointRouting ) + { + httpContext.SetEndpoint( handler ); + } + else + { + context.Handler = handler!; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs index e8c70d86..dcdcc21c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs @@ -293,7 +293,7 @@ protected virtual bool IsRequestedApiVersionAmbiguous( RouteContext context, out Message = ex.Message, }; - context.Handler = new BadRequestHandler( handlerContext ); + context.SetHandlerOrEndpoint( new BadRequestHandler( handlerContext ) ); return true; } diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs index 7d482c48..df58ceca 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs @@ -1,6 +1,8 @@ namespace Microsoft.AspNet.OData.Routing { using Microsoft.AspNet.OData; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -17,37 +19,42 @@ partial class ODataRouteBuilderContext internal ODataRouteBuilderContext( ODataRouteMapping routeMapping, + ApiVersion apiVersion, ControllerActionDescriptor actionDescriptor, ODataApiExplorerOptions options ) { - ApiVersion = routeMapping.ApiVersion; + ApiVersion = apiVersion; Services = routeMapping.Services; EdmModel = Services.GetRequiredService(); routeAttribute = actionDescriptor.MethodInfo.GetCustomAttributes().FirstOrDefault(); RouteTemplate = routeAttribute?.PathTemplate; - Route = routeMapping.Route; + RoutePrefix = routeMapping.RoutePrefix; ActionDescriptor = actionDescriptor; ParameterDescriptions = new List(); Options = options; UrlKeyDelimiter = UrlKeyDelimiterOrDefault( Services.GetRequiredService().UrlKeyDelimiter ); - var container = EdmModel.EntityContainer; + var selector = Services.GetRequiredService(); + var model = selector.SelectModel( apiVersion ); + var container = model?.EntityContainer; - if ( container == null ) + if ( model == null || container == null ) { + EdmModel = Services.GetRequiredService(); IsRouteExcluded = true; return; } - var controllerName = actionDescriptor.ControllerName; - var actionName = actionDescriptor.ActionName; - - EntitySet = container.FindEntitySet( controllerName ); - Operation = container.FindOperationImports( controllerName ).FirstOrDefault()?.Operation ?? - EdmModel.FindDeclaredOperations( string.Concat( container.Namespace, ".", actionName ) ).FirstOrDefault(); - ActionType = GetActionType( EntitySet, Operation ); + EdmModel = model; + Services = new FixedEdmModelServiceProviderDecorator( Services, model ); + EntitySet = container.FindEntitySet( actionDescriptor.ControllerName ); + Operation = ResolveOperation( container, actionDescriptor.ActionName ); + ActionType = GetActionType( EntitySet, Operation, actionDescriptor ); + IsRouteExcluded = ActionType == ODataRouteActionType.Unknown; } + static IEnumerable GetHttpMethods( ControllerActionDescriptor action ) => action.GetHttpMethods(); + internal IODataPathTemplateHandler PathTemplateHandler => templateHandler ??= Services.GetRequiredService(); diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ApiDescriptionExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc/ApiExplorer/ApiDescriptionExtensions.cs similarity index 79% rename from src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ApiDescriptionExtensions.cs rename to src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc/ApiExplorer/ApiDescriptionExtensions.cs index 81b208b0..2be5a650 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ApiDescriptionExtensions.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc/ApiExplorer/ApiDescriptionExtensions.cs @@ -1,5 +1,6 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer { + using Microsoft.AspNet.OData.Routing; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.OData.Edm; using System; @@ -64,5 +65,25 @@ public static class ApiDescriptionExtensions /// The API description to get the operation for. /// The associated EDM operation or null if there is no associated operation. public static IEdmOperation? Operation( this ApiDescription apiDescription ) => apiDescription.GetProperty(); + + /// + /// Gets the route prefix associated with the API description. + /// + /// The API description to get the route prefix for. + /// The associated route prefix or null. + public static string? RoutePrefix( this ApiDescription apiDescription ) + { + if ( apiDescription == null ) + { + throw new ArgumentNullException( nameof( apiDescription ) ); + } + + if ( apiDescription.ActionDescriptor.AttributeRouteInfo is ODataAttributeRouteInfo routeInfo ) + { + return routeInfo.RoutePrefix; + } + + return default; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ApiParameterContext.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc/ApiExplorer/ApiParameterContext.cs similarity index 100% rename from src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ApiParameterContext.cs rename to src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc/ApiExplorer/ApiParameterContext.cs diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ApiParameterDescriptionContext.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc/ApiExplorer/ApiParameterDescriptionContext.cs similarity index 100% rename from src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ApiParameterDescriptionContext.cs rename to src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc/ApiExplorer/ApiParameterDescriptionContext.cs diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ModelMetadataExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc/ApiExplorer/ModelMetadataExtensions.cs similarity index 100% rename from src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ModelMetadataExtensions.cs rename to src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc/ApiExplorer/ModelMetadataExtensions.cs 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 similarity index 89% rename from src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProvider.cs rename to src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc/ApiExplorer/ODataApiDescriptionProvider.cs index 80c47631..9d05201e 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 @@ -6,7 +6,6 @@ using Microsoft.AspNet.OData.Routing; using Microsoft.AspNet.OData.Routing.Template; using Microsoft.AspNetCore.Mvc.Abstractions; - using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Formatters; @@ -26,7 +25,6 @@ using static Microsoft.AspNet.OData.Routing.ODataRouteActionType; using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource; - using static Microsoft.AspNetCore.Mvc.Versioning.ApiVersionMapping; using static System.Linq.Enumerable; using static System.StringComparison; @@ -39,16 +37,6 @@ public class ODataApiDescriptionProvider : IApiDescriptionProvider { const int AfterApiVersioning = -100; - static readonly string[] SupportedHttpMethodConventions = new[] - { - "GET", - "PUT", - "POST", - "DELETE", - "PATCH", - "HEAD", - "OPTIONS", - }; readonly IOptions options; readonly Lazy modelMetadata; @@ -144,8 +132,8 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) throw new ArgumentNullException( nameof( context ) ); } - var mappings = RouteCollectionProvider.Items; var results = context.Results; + var mappings = RouteCollectionProvider.Items; var groupNameFormat = Options.GroupNameFormat; var formatProvider = CultureInfo.CurrentCulture; @@ -158,55 +146,38 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) continue; } - var model = action.GetApiVersionModel( Explicit | Implicit ); - - if ( model.IsApiVersionNeutral ) + for ( var i = 0; i < mappings.Count; i++ ) { - for ( var i = 0; i < mappings.Count; i++ ) + var mapping = mappings[i]; + + if ( !IsMappedTo( action, mapping ) ) { - var mapping = mappings[i]; - var descriptions = new List(); - var groupName = mapping.ApiVersion.ToString( groupNameFormat, formatProvider ); + continue; + } - foreach ( var apiDescription in NewODataApiDescriptions( action, groupName, mapping ) ) - { - results.Add( apiDescription ); - descriptions.Add( apiDescription ); - } + var mappedVersions = mapping.ModelSelector.ApiVersions; - if ( descriptions.Count > 0 ) - { - ExploreQueryOptions( descriptions, mapping.Services.GetRequiredService() ); - } - } - } - else - { - for ( var i = 0; i < model.DeclaredApiVersions.Count; i++ ) + for ( var j = 0; j < mappedVersions.Count; j++ ) { - var apiVersion = model.DeclaredApiVersions[i]; - var groupName = apiVersion.ToString( groupNameFormat, formatProvider ); + var apiVersion = mappedVersions[j]; - if ( !mappings.TryGetValue( apiVersion, out var mappingsPerApiVersion ) ) + if ( !action.IsMappedTo( apiVersion ) ) { continue; } - for ( var j = 0; j < mappingsPerApiVersion!.Count; j++ ) - { - var mapping = mappingsPerApiVersion[j]; - var descriptions = new List(); + var groupName = apiVersion.ToString( groupNameFormat, formatProvider ); + var descriptions = new List(); - foreach ( var apiDescription in NewODataApiDescriptions( action, groupName, mapping ) ) - { - results.Add( apiDescription ); - descriptions.Add( apiDescription ); - } + foreach ( var apiDescription in NewODataApiDescriptions( action, apiVersion, groupName, mapping ) ) + { + results.Add( apiDescription ); + descriptions.Add( apiDescription ); + } - if ( descriptions.Count > 0 ) - { - ExploreQueryOptions( descriptions, mapping.Services.GetRequiredService() ); - } + if ( descriptions.Count > 0 ) + { + ExploreQueryOptions( descriptions, mapping.Services.GetRequiredService() ); } } } @@ -276,33 +247,9 @@ protected virtual void ExploreQueryOptions( IEnumerable apiDescr queryOptions.ApplyTo( apiDescriptions, settings ); } - static IEnumerable GetHttpMethods( ControllerActionDescriptor action ) - { - var actionConstraints = ( action.ActionConstraints ?? Array.Empty() ).OfType(); - var httpMethods = new HashSet( actionConstraints.SelectMany( ac => ac.HttpMethods ), StringComparer.OrdinalIgnoreCase ); - - if ( httpMethods.Count > 0 ) - { - return httpMethods; - } - - for ( var i = 0; i < SupportedHttpMethodConventions.Length; i++ ) - { - var supportedHttpMethod = SupportedHttpMethodConventions[i]; - - if ( action.MethodInfo.Name.StartsWith( supportedHttpMethod, OrdinalIgnoreCase ) ) - { - httpMethods.Add( supportedHttpMethod ); - } - } - - if ( httpMethods.Count == 0 ) - { - httpMethods.Add( "POST" ); - } - - return httpMethods; - } + static bool IsMappedTo( ControllerActionDescriptor action, ODataRouteMapping mapping ) => + action.AttributeRouteInfo != null && + StringComparer.OrdinalIgnoreCase.Equals( action.AttributeRouteInfo.Name, mapping.RouteName ); static Type? GetDeclaredReturnType( ControllerActionDescriptor action ) { @@ -364,14 +311,18 @@ static IEnumerable GetHttpMethods( ControllerActionDescriptor action ) return relativePath; } - IEnumerable NewODataApiDescriptions( ControllerActionDescriptor action, string groupName, ODataRouteMapping mapping ) + IEnumerable NewODataApiDescriptions( + ControllerActionDescriptor action, + ApiVersion version, + string groupName, + ODataRouteMapping mapping ) { var requestMetadataAttributes = GetRequestMetadataAttributes( action ); var responseMetadataAttributes = GetResponseMetadataAttributes( action ); var declaredReturnType = GetDeclaredReturnType( action ); var runtimeReturnType = GetRuntimeReturnType( declaredReturnType! ); var apiResponseTypes = GetApiResponseTypes( responseMetadataAttributes!, runtimeReturnType!, mapping.Services ); - var routeContext = new ODataRouteBuilderContext( mapping, action, Options ) { ModelMetadataProvider = MetadataProvider }; + var routeContext = new ODataRouteBuilderContext( mapping, version, action, Options ) { ModelMetadataProvider = MetadataProvider }; if ( routeContext.IsRouteExcluded ) { @@ -387,10 +338,16 @@ IEnumerable NewODataApiDescriptions( ControllerActionDescriptor } var relativePath = BuildRelativePath( action, routeContext ); + var apiExplorer = action.GetProperty() ?? action.GetProperty()?.ApiExplorer; + + if ( apiExplorer != null && !string.IsNullOrEmpty( apiExplorer.GroupName ) ) + { + groupName = apiExplorer.GroupName; + } parameters = routeContext.ParameterDescriptions; - foreach ( var httpMethod in GetHttpMethods( action ) ) + foreach ( var httpMethod in action.GetHttpMethods() ) { var apiDescription = new ApiDescription() { @@ -448,8 +405,8 @@ IEnumerable NewODataApiDescriptions( ControllerActionDescriptor apiDescription.SupportedResponseTypes.Add( apiResponseTypes[i] ); } - PopulateApiVersionParameters( apiDescription, mapping.ApiVersion ); - apiDescription.SetApiVersion( mapping.ApiVersion ); + PopulateApiVersionParameters( apiDescription, version ); + apiDescription.SetApiVersion( version ); apiDescription.TryUpdateRelativePathAndRemoveApiVersionParameter( Options ); yield return apiDescription; } @@ -668,7 +625,10 @@ static bool IsSpecialBindingSource( BindingInfo info, Type type ) parameter.BindingInfo = bindingInfo; } - IReadOnlyList GetApiResponseTypes( IReadOnlyList responseMetadataAttributes, Type responseType, IServiceProvider serviceProvider ) + IReadOnlyList GetApiResponseTypes( + IReadOnlyList responseMetadataAttributes, + Type responseType, + IServiceProvider serviceProvider ) { var results = new List(); var objectTypes = new Dictionary(); @@ -775,7 +735,7 @@ IReadOnlyList GetApiResponseTypes( IReadOnlyList - 4.1.1 - 4.1.0.0 + 5.0.0 + 5.0.0.0 netcoreapp3.1 Microsoft ASP.NET Core Versioned API Explorer for OData v4.0 The API Explorer for Microsoft ASP.NET Core and OData v4.0. @@ -13,6 +13,7 @@ + diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IEndpointRouteBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IEndpointRouteBuilderExtensions.cs new file mode 100644 index 00000000..c38c5fa9 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IEndpointRouteBuilderExtensions.cs @@ -0,0 +1,219 @@ +namespace Microsoft.AspNet.OData.Extensions +{ + using Microsoft.AspNet.OData.Batch; + using Microsoft.AspNet.OData.Builder; + using Microsoft.AspNet.OData.Routing; + using Microsoft.AspNet.OData.Routing.Conventions; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Routing; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.OData; + using Microsoft.OData.Edm; + using System; + using System.Collections.Generic; + using static Microsoft.OData.ServiceLifetime; + + /// + /// Provides extension methods for registering versioned OData route endpoints. + /// + [CLSCompliant( false )] + public static class IEndpointRouteBuilderExtensions + { + /// + /// Maps the specified OData route and the OData route attributes. + /// + /// The to add the route to. + /// The name of the route to map. + /// The prefix to add to the OData route's path template. + /// The model builer used to create + /// an EDM model per API version. + /// The input . + public static IEndpointRouteBuilder MapVersionedODataRoute( + this IEndpointRouteBuilder builder, + string routeName, + string routePrefix, + VersionedODataModelBuilder modelBuilder ) + { + if ( modelBuilder == null ) + { + throw new ArgumentNullException( nameof( modelBuilder ) ); + } + + return builder.MapVersionedODataRoute( routeName, routePrefix, modelBuilder.GetEdmModels( routePrefix ) ); + } + + /// + /// Maps the specified OData route and the OData route attributes. + /// + /// The to add the route to. + /// The name of the route to map. + /// The prefix to add to the OData route's path template. + /// The model builer used to create + /// an EDM model per API version. + /// The configuring action to add the services to the root container. + /// The input . + public static IEndpointRouteBuilder MapVersionedODataRoute( + this IEndpointRouteBuilder builder, + string routeName, + string routePrefix, + VersionedODataModelBuilder modelBuilder, + Action configureAction ) + { + if ( modelBuilder == null ) + { + throw new ArgumentNullException( nameof( modelBuilder ) ); + } + + return builder.MapVersionedODataRoute( routeName, routePrefix, modelBuilder.GetEdmModels( routePrefix ), configureAction ); + } + + /// + /// Maps the specified OData route and the OData route attributes. + /// + /// The to add the route to. + /// The name of the route to map. + /// The prefix to add to the OData route's path template. + /// The sequence of EDM models to use for parsing OData paths. + /// The input . + public static IEndpointRouteBuilder MapVersionedODataRoute( + this IEndpointRouteBuilder builder, + string routeName, + string routePrefix, + IEnumerable models ) => + AddRoute( + builder.MapODataRoute( + routeName, + routePrefix, + container => container.AddApiVersioning( routeName, models, builder.ServiceProvider ) ), + routeName, + routePrefix ); + + /// + /// Maps the specified OData route and the OData route attributes. When the is + /// non-null, it will create a '$batch' endpoint to handle the batch requests. + /// + /// The to add the route to. + /// The name of the route to map. + /// The prefix to add to the OData route's path template. + /// The sequence of EDM models to use for parsing OData paths. + /// The . + /// The . + public static IEndpointRouteBuilder MapVersionedODataRoute( + this IEndpointRouteBuilder builder, + string routeName, + string routePrefix, + IEnumerable models, + ODataBatchHandler batchHandler ) => + AddRoute( + builder.MapODataRoute( + routeName, + routePrefix, + container => container.AddApiVersioning( routeName, models, builder.ServiceProvider ) + .AddService( Singleton, sp => batchHandler ) ), + routeName, + routePrefix ); + + /// + /// Maps the specified OData route. + /// + /// The to add the route to. + /// The name of the route to map. + /// The prefix to add to the OData route's path template. + /// The sequence of EDM models to use for parsing OData paths. + /// The to use for parsing the OData path. + /// + /// The OData routing conventions to use for controller and action selection. + /// + /// The . + public static IEndpointRouteBuilder MapVersionedODataRoute( + this IEndpointRouteBuilder builder, + string routeName, + string routePrefix, + IEnumerable models, + IODataPathHandler pathHandler, + IEnumerable routingConventions ) => + AddRoute( + builder.MapODataRoute( + routeName, + routePrefix, + container => container.AddApiVersioning( models, routingConventions, builder.ServiceProvider ) + .AddService( Singleton, sp => pathHandler ) ), + routeName, + routePrefix ); + + /// + /// Maps the specified OData route. When the is non-null, it will + /// create a '$batch' endpoint to handle the batch requests. + /// + /// The to add the route to. + /// The name of the route to map. + /// The prefix to add to the OData route's path template. + /// The sequence of EDM models to use for parsing OData paths. + /// The to use for parsing the OData path. + /// + /// The OData routing conventions to use for controller and action selection. + /// + /// The . + /// The . + public static IEndpointRouteBuilder MapVersionedODataRoute( + this IEndpointRouteBuilder builder, + string routeName, + string routePrefix, + IEnumerable models, + IODataPathHandler pathHandler, + IEnumerable routingConventions, + ODataBatchHandler batchHandler ) => + AddRoute( + builder.MapODataRoute( + routeName, + routePrefix, + container => + container.AddApiVersioning( models, routingConventions, builder.ServiceProvider ) + .AddService( Singleton, sp => pathHandler ) + .AddService( Singleton, sp => batchHandler ) ), + routeName, + routePrefix ); + + /// + /// Maps the specified OData route and the OData route attributes. + /// + /// The to add the route to. + /// The name of the route to map. + /// The prefix to add to the OData route's path template. + /// The sequence of EDM models to use for parsing OData paths. + /// The configuring action to add the services to the root container. + /// The input . + public static IEndpointRouteBuilder MapVersionedODataRoute( + this IEndpointRouteBuilder builder, + string routeName, + string routePrefix, + IEnumerable models, + Action configureAction ) => + AddRoute( + builder.MapODataRoute( + routeName, + routePrefix, + container => + { + container.AddApiVersioning( routeName, models, builder.ServiceProvider ); + configureAction?.Invoke( container ); + } ), + routeName, + routePrefix ); + + static IEndpointRouteBuilder AddRoute( IEndpointRouteBuilder builder, string routeName, string routePrefix ) + { + if ( builder == null ) + { + throw new ArgumentNullException( nameof( builder ) ); + } + + var serviceProvider = builder.ServiceProvider; + var routeCollection = serviceProvider.GetRequiredService(); + + routeCollection.Add( new ODataRouteMapping( routeName, routePrefix, serviceProvider ) ); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IRouteBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IRouteBuilderExtensions.cs index 4b6747e2..48b8369b 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IRouteBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/IRouteBuilderExtensions.cs @@ -1,614 +1,213 @@ namespace Microsoft.AspNet.OData.Extensions { - using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Batch; using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Routing; using Microsoft.AspNet.OData.Routing.Conventions; using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.OData; using Microsoft.OData.Edm; using System; using System.Collections.Generic; - using System.Linq; using static Microsoft.OData.ServiceLifetime; - using static System.Reflection.BindingFlags; - using static System.String; /// - /// Provides extension methods for to add versioned OData routes. + /// Provides extension methods for registering versioned OData routes. /// [CLSCompliant( false )] public static class IRouteBuilderExtensions { - const string UnversionedRouteSuffix = "-Unversioned"; - static readonly Func, Action> configureDefaultServicesFunc = ResolveConfigureDefaultServicesFunc(); - - /// - /// Maps the specified versioned OData routes. - /// - /// The extended route builder. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The configuring action to add the services to the root container. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( - this IRouteBuilder builder, - string routeName, - string routePrefix, - IEnumerable models, - Action configureAction ) => - MapVersionedODataRoutes( builder, routeName, routePrefix, models, configureAction, default ); - /// - /// Maps the specified versioned OData routes. + /// Maps the specified OData route and the OData route attributes. /// - /// The extended route builder. + /// The to add the route to. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The configuring action to add the services to the root container. - /// The configuring action to add or update routing conventions. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( + /// The model builer used to create + /// an EDM model per API version. + /// The added . + public static ODataRoute MapVersionedODataRoute( this IRouteBuilder builder, string routeName, string routePrefix, - IEnumerable models, - Action? configureAction, - Action? configureRoutingConventions ) + VersionedODataModelBuilder modelBuilder ) { - if ( builder == null ) + if ( modelBuilder == null ) { - throw new ArgumentNullException( nameof( builder ) ); + throw new ArgumentNullException( nameof( modelBuilder ) ); } - if ( models == null ) - { - throw new ArgumentNullException( nameof( models ) ); - } - - IEnumerable ConfigureRoutingConventions( IEdmModel model, string versionedRouteName, ApiVersion apiVersion ) - { - var routingConventions = VersionedODataRoutingConventions.CreateDefault(); - var context = new ODataConventionConfigurationContext( versionedRouteName, model, apiVersion, routingConventions ); - - model.SetAnnotationValue( model, new ApiVersionAnnotation( apiVersion ) ); - routingConventions.Insert( 0, new VersionedAttributeRoutingConvention( versionedRouteName, builder.ServiceProvider, apiVersion ) ); - configureRoutingConventions?.Invoke( context ); - - return context.RoutingConventions; - } - - builder.EnsureMetadataController(); - - var routeCollection = builder.ServiceProvider.GetRequiredService(); - var perRouteContainer = builder.ServiceProvider.GetRequiredService(); - var options = builder.ServiceProvider.GetRequiredService(); - var inlineConstraintResolver = builder.ServiceProvider.GetRequiredService(); - var routes = builder.Routes; - var odataRoutes = new List(); - var unversionedConstraints = new List(); - - foreach ( var model in models ) - { - var versionedRouteName = routeName; - var annotation = model.GetAnnotationValue( model ) ?? throw new ArgumentException( LocalSR.MissingAnnotation.FormatDefault( typeof( ApiVersionAnnotation ).Name ) ); - var apiVersion = annotation.ApiVersion; - var routeConstraint = MakeVersionedODataRouteConstraint( apiVersion, ref versionedRouteName ); - var preConfigureAction = builder.ConfigureDefaultServices( - container => - { - container.AddService( Singleton, typeof( IEdmModel ), sp => model ) - .AddService( Singleton, typeof( IEnumerable ), sp => ConfigureRoutingConventions( model, versionedRouteName, apiVersion ) ); - configureAction?.Invoke( container ); - } ); - var rootContainer = perRouteContainer.CreateODataRootContainer( versionedRouteName, preConfigureAction ); - var router = rootContainer.GetService() ?? builder.DefaultHandler; - - rootContainer.ConfigurePathHandler( options ); - - var route = new ODataRoute( router, versionedRouteName, routePrefix.RemoveTrailingSlash(), routeConstraint, inlineConstraintResolver ); - - unversionedConstraints.Add( new ODataPathRouteConstraint( versionedRouteName ) ); - builder.ConfigureBatchHandler( rootContainer, route ); - routes.Add( route ); - odataRoutes.Add( route ); - routeCollection.Add( new ODataRouteMapping( route, apiVersion, rootContainer ) ); - } - - builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( - routeName, - routePrefix, - unversionedConstraints, - inlineConstraintResolver, - builder.ConfigureDefaultServices( container => configureAction?.Invoke( container ) ) ); - - NotifyRoutesMapped(); - - return odataRoutes; + return builder.MapVersionedODataRoute( routeName, routePrefix, modelBuilder.GetEdmModels( routePrefix ) ); } /// - /// Maps the specified versioned OData routes. - /// - /// The extended route builder. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( this IRouteBuilder builder, string routeName, string routePrefix, IEnumerable models ) => - MapVersionedODataRoutes( builder, routeName, routePrefix, models, new DefaultODataPathHandler(), VersionedODataRoutingConventions.CreateDefault(), default ); - - /// - /// Maps the specified versioned OData routes. When the is provided, it will create a - /// '$batch' endpoint to handle the batch requests. - /// - /// The extended route builder. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The factory method used to create new OData batch handlers. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( - this IRouteBuilder builder, - string routeName, - string routePrefix, - IEnumerable models, - Func? newBatchHandler ) => - MapVersionedODataRoutes( builder, routeName, routePrefix, models, new DefaultODataPathHandler(), VersionedODataRoutingConventions.CreateDefault(), newBatchHandler ); - - /// - /// Maps the specified versioned OData routes. - /// - /// The extended route builder. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The OData path handler to use for parsing the OData path. - /// The sequence of OData routing conventions - /// to use for controller and action selection. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( - this IRouteBuilder builder, - string routeName, - string routePrefix, - IEnumerable models, - IODataPathHandler pathHandler, - IEnumerable routingConventions ) => - MapVersionedODataRoutes( builder, routeName, routePrefix, models, pathHandler, routingConventions, default ); - - /// - /// Maps the specified versioned OData routes. When the is provided, it will create a '$batch' endpoint to handle the batch requests. + /// Maps the specified OData route and the OData route attributes. /// - /// The extended route builder. + /// The to add the route to. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The sequence of EDM models to use for parsing OData paths. - /// The OData path handler to use for parsing the OData path. - /// The sequence of OData routing conventions - /// to use for controller and action selection. - /// The factory method used to create new OData batch handlers. - /// The read-only list of added OData routes. - /// The specified must contain the API version annotation. This annotation is - /// automatically applied when you use the and call to - /// create the . - public static IReadOnlyList MapVersionedODataRoutes( + /// The model builer used to create + /// an EDM model per API version. + /// The configuring action to add the services to the root container. + /// The added . + public static ODataRoute MapVersionedODataRoute( this IRouteBuilder builder, string routeName, string routePrefix, - IEnumerable models, - IODataPathHandler pathHandler, - IEnumerable routingConventions, - Func? newBatchHandler ) + VersionedODataModelBuilder modelBuilder, + Action configureAction ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - if ( IsNullOrEmpty( routeName ) ) - { - throw new ArgumentNullException( nameof( routeName ) ); - } - - if ( models == null ) - { - throw new ArgumentNullException( nameof( models ) ); - } - - var serviceProvider = builder.ServiceProvider; - var options = serviceProvider.GetRequiredService(); - var routeCollection = serviceProvider.GetRequiredService(); - var inlineConstraintResolver = serviceProvider.GetRequiredService(); - var routeConventions = VersionedODataRoutingConventions.AddOrUpdate( routingConventions.ToList() ); - var routes = builder.Routes; - var perRouteContainer = serviceProvider.GetRequiredService(); - var odataRoutes = new List(); - var unversionedConstraints = new List(); - - if ( pathHandler != null && pathHandler.UrlKeyDelimiter == null ) - { - pathHandler.UrlKeyDelimiter = options.UrlKeyDelimiter; - } - - foreach ( var model in models ) + if ( modelBuilder == null ) { - var versionedRouteName = routeName; - var annotation = model.GetAnnotationValue( model ) ?? throw new ArgumentException( LocalSR.MissingAnnotation.FormatDefault( typeof( ApiVersionAnnotation ).Name ) ); - var apiVersion = annotation.ApiVersion; - var routeConstraint = MakeVersionedODataRouteConstraint( apiVersion, ref versionedRouteName ); - - IEnumerable NewRouteConventions( IServiceProvider services ) - { - var conventions = new IODataRoutingConvention[routeConventions!.Count + 1]; - conventions[0] = new VersionedAttributeRoutingConvention( versionedRouteName!, serviceProvider!, apiVersion! ); - routeConventions.CopyTo( conventions, 1 ); - return conventions; - } - - var edm = model; - var batchHandler = newBatchHandler?.Invoke(); - var configureAction = builder.ConfigureDefaultServices( container => - container.AddService( Singleton, typeof( IEdmModel ), sp => edm ) - .AddService( Singleton, typeof( IODataPathHandler ), sp => pathHandler ) - .AddService( Singleton, typeof( IEnumerable ), NewRouteConventions ) - .AddService( Singleton, typeof( ODataBatchHandler ), sp => batchHandler ) ); - var rootContainer = perRouteContainer.CreateODataRootContainer( versionedRouteName, configureAction ); - var router = rootContainer.GetService() ?? builder.DefaultHandler; - var route = new ODataRoute( router, versionedRouteName, routePrefix.RemoveTrailingSlash(), routeConstraint, inlineConstraintResolver ); - - unversionedConstraints.Add( new ODataPathRouteConstraint( versionedRouteName ) ); - builder.ConfigureBatchHandler( batchHandler, route ); - routes.Add( route ); - odataRoutes.Add( route ); - routeCollection.Add( new ODataRouteMapping( route, apiVersion, rootContainer ) ); + throw new ArgumentNullException( nameof( modelBuilder ) ); } - builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( - routeName, - routePrefix, - unversionedConstraints, - inlineConstraintResolver, - builder.ConfigureDefaultServices( _ => { } ) ); - - NotifyRoutesMapped(); - - return odataRoutes; + return builder.MapVersionedODataRoute( routeName, routePrefix, modelBuilder.GetEdmModels( routePrefix ), configureAction ); } /// /// Maps the specified OData route and the OData route attributes. /// - /// The extended route builder. + /// The to add the route to. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The API version associated with the model. + /// The sequence of EDM models to use for parsing OData paths. /// The configuring action to add the services to the root container. /// The added . public static ODataRoute MapVersionedODataRoute( this IRouteBuilder builder, string routeName, string routePrefix, - ApiVersion apiVersion, - Action? configureAction ) => - MapVersionedODataRoute( builder, routeName, routePrefix, apiVersion, configureAction, default ); + IEnumerable models, + Action configureAction ) => + AddRoute( + builder, + builder.MapODataServiceRoute( + routeName, + routePrefix, + container => + { + container.AddApiVersioning( routeName, models, builder.ServiceProvider ); + configureAction?.Invoke( container ); + } ) ); /// /// Maps the specified OData route and the OData route attributes. /// - /// The extended route builder. + /// The to add the route to. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The API version associated with the model. - /// The configuring action to add the services to the root container. - /// The configuring action to add or update routing conventions. + /// The sequence of EDM models to use for parsing OData paths. /// The added . public static ODataRoute MapVersionedODataRoute( this IRouteBuilder builder, string routeName, string routePrefix, - ApiVersion apiVersion, - Action? configureAction, - Action? configureRoutingConventions ) - { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - IEnumerable NewRoutingConventions( IServiceProvider serviceProvider ) - { - var model = serviceProvider.GetRequiredService(); - var routingConventions = VersionedODataRoutingConventions.CreateDefault(); - var context = new ODataConventionConfigurationContext( routeName, model, apiVersion, routingConventions ); - - model.SetAnnotationValue( model, new ApiVersionAnnotation( apiVersion ) ); - routingConventions.Insert( 0, new VersionedAttributeRoutingConvention( routeName, builder.ServiceProvider, apiVersion ) ); - configureRoutingConventions?.Invoke( context ); - - return context.RoutingConventions.ToArray(); - } - - var routeCollection = builder.ServiceProvider.GetRequiredService(); - var perRouteContainer = builder.ServiceProvider.GetRequiredService(); - var inlineConstraintResolver = builder.ServiceProvider.GetRequiredService(); - var perConfigureAction = builder.ConfigureDefaultServices( - container => - { - container.AddService( Singleton, typeof( IEnumerable ), NewRoutingConventions ); - configureAction?.Invoke( container ); - } ); - var rootContainer = perRouteContainer.CreateODataRootContainer( routeName, perConfigureAction ); - var router = rootContainer.GetService() ?? builder.DefaultHandler; - - builder.ConfigurePathHandler( rootContainer ); - - var routeConstraint = new VersionedODataPathRouteConstraint( routeName, apiVersion ); - var route = new ODataRoute( router, routeName, routePrefix.RemoveTrailingSlash(), routeConstraint, inlineConstraintResolver ); - - builder.ConfigureBatchHandler( rootContainer, route ); - builder.Routes.Add( route ); - routeCollection.Add( new ODataRouteMapping( route, apiVersion, rootContainer ) ); - builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( routeName, routePrefix, apiVersion, inlineConstraintResolver, perConfigureAction ); - NotifyRoutesMapped(); - - return route; - } + IEnumerable models ) => + AddRoute( + builder, + builder.MapODataServiceRoute( + routeName, + routePrefix, + container => container.AddApiVersioning( routeName, models, builder.ServiceProvider ) ) ); /// - /// Maps a versioned OData route. + /// Maps the specified OData route and the OData route attributes. When the is + /// non-null, it will create a '$batch' endpoint to handle the batch requests. /// - /// The extended route builder. + /// The to add the route to. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The EDM model to use for parsing OData paths. - /// The API version associated with the model. - /// The mapped OData route. - /// The API version annotation will be added or updated on the specified using - /// the provided API version. - public static ODataRoute MapVersionedODataRoute( this IRouteBuilder builder, string routeName, string routePrefix, IEdmModel model, ApiVersion apiVersion ) => - MapVersionedODataRoute( builder, routeName, routePrefix, model, apiVersion, new DefaultODataPathHandler(), VersionedODataRoutingConventions.CreateDefault(), default ); - - /// - /// Maps a versioned OData route. - /// - /// The extended route builder. - /// The name of the route to map. - /// The prefix to add to the OData route's path template. - /// The EDM model to use for parsing OData paths. - /// The API version associated with the model. - /// The OData batch handler. - /// The mapped OData route. - /// The API version annotation will be added or updated on the specified using - /// the provided API version. + /// The sequence of EDM models to use for parsing OData paths. + /// The . + /// The added . public static ODataRoute MapVersionedODataRoute( this IRouteBuilder builder, string routeName, string routePrefix, - IEdmModel model, - ApiVersion apiVersion, - ODataBatchHandler? batchHandler ) => - MapVersionedODataRoute( builder, routeName, routePrefix, model, apiVersion, new DefaultODataPathHandler(), VersionedODataRoutingConventions.CreateDefault(), batchHandler ); + IEnumerable models, + ODataBatchHandler batchHandler ) => + AddRoute( + builder, + builder.MapODataServiceRoute( + routeName, + routePrefix, + container => container.AddApiVersioning( routeName, models, builder.ServiceProvider ) + .AddService( Singleton, sp => batchHandler ) ) ); /// - /// Maps a versioned OData route. + /// Maps the specified OData route. /// - /// The extended route builder. + /// The to add the route to. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The EDM model to use for parsing OData paths. - /// The API version associated with the model. - /// The OData path handler to use for parsing the OData path. - /// The sequence of OData routing conventions - /// to use for controller and action selection. - /// The mapped OData route. - /// The API version annotation will be added or updated on the specified using - /// the provided API version. + /// The sequence of EDM models to use for parsing OData paths. + /// The to use for parsing the OData path. + /// The OData routing conventions to use for controller and action selection. + /// The added . public static ODataRoute MapVersionedODataRoute( this IRouteBuilder builder, string routeName, string routePrefix, - IEdmModel model, - ApiVersion apiVersion, + IEnumerable models, IODataPathHandler pathHandler, IEnumerable routingConventions ) => - MapVersionedODataRoute( builder, routeName, routePrefix, model, apiVersion, pathHandler, routingConventions, default ); + AddRoute( + builder, + builder.MapODataServiceRoute( + routeName, + routePrefix, + container => + container.AddApiVersioning( models, routingConventions, builder.ServiceProvider ) + .AddService( Singleton, sp => pathHandler ) ) ); /// - /// Maps a versioned OData route. When the is provided, it will create a '$batch' endpoint to handle the batch requests. + /// Maps the specified OData route. When the is non-null, it will + /// create a '$batch' endpoint to handle the batch requests. /// - /// The extended route builder. + /// The to add the route to. /// The name of the route to map. /// The prefix to add to the OData route's path template. - /// The EDM model to use for parsing OData paths. - /// The API version associated with the model. - /// The OData path handler to use for parsing the OData path. - /// The sequence of OData routing conventions - /// to use for controller and action selection. - /// The OData batch handler. - /// The mapped OData route. - /// The API version annotation will be added or updated on the specified using - /// the provided API version. + /// The sequence of EDM models to use for parsing OData paths. + /// The to use for parsing the OData path. + /// The OData routing conventions to use for controller and action selection. + /// The . + /// The added . public static ODataRoute MapVersionedODataRoute( this IRouteBuilder builder, string routeName, string routePrefix, - IEdmModel model, - ApiVersion apiVersion, + IEnumerable models, IODataPathHandler pathHandler, IEnumerable routingConventions, - ODataBatchHandler? batchHandler ) + ODataBatchHandler batchHandler ) => + AddRoute( + builder, + builder.MapODataServiceRoute( + routeName, + routePrefix, + container => + container.AddApiVersioning( models, routingConventions, builder.ServiceProvider ) + .AddService( Singleton, sp => pathHandler ) + .AddService( Singleton, sp => batchHandler ) ) ); + + static ODataRoute AddRoute( IRouteBuilder builder, ODataRoute route ) { if ( builder == null ) { throw new ArgumentNullException( nameof( builder ) ); } - IEnumerable NewRoutingConventions( IServiceProvider serviceProvider ) - { - var conventions = VersionedODataRoutingConventions.AddOrUpdate( routingConventions.ToList() ); - conventions.Insert( 0, new VersionedAttributeRoutingConvention( routeName, builder.ServiceProvider, apiVersion ) ); - return conventions.ToArray(); - } - - var routeCollection = builder.ServiceProvider.GetRequiredService(); - var perRouteContainer = builder.ServiceProvider.GetRequiredService(); - var options = builder.ServiceProvider.GetRequiredService(); - var inlineConstraintResolver = builder.ServiceProvider.GetRequiredService(); - - if ( pathHandler != null && pathHandler.UrlKeyDelimiter == null ) - { - pathHandler.UrlKeyDelimiter = options.UrlKeyDelimiter; - } - - model.SetAnnotationValue( model, new ApiVersionAnnotation( apiVersion ) ); - - var configureAction = builder.ConfigureDefaultServices( container => - container.AddService( Singleton, typeof( IEdmModel ), sp => model ) - .AddService( Singleton, typeof( IODataPathHandler ), sp => pathHandler ) - .AddService( Singleton, typeof( IEnumerable ), NewRoutingConventions ) - .AddService( Singleton, typeof( ODataBatchHandler ), sp => batchHandler ) ); - var rootContainer = perRouteContainer.CreateODataRootContainer( routeName, configureAction ); - var router = rootContainer.GetService() ?? builder.DefaultHandler; - var routeConstraint = new VersionedODataPathRouteConstraint( routeName, apiVersion ); - var route = new ODataRoute( router, routeName, routePrefix.RemoveTrailingSlash(), routeConstraint, inlineConstraintResolver ); + var serviceProvider = builder.ServiceProvider; + var routeCollection = serviceProvider.GetRequiredService(); + var container = serviceProvider.GetRequiredService(); - builder.ConfigureBatchHandler( rootContainer, route ); - builder.Routes.Add( route ); - routeCollection.Add( new ODataRouteMapping( route, apiVersion, rootContainer ) ); - builder.AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( routeName, routePrefix, apiVersion, inlineConstraintResolver, configureAction ); - NotifyRoutesMapped(); + routeCollection.Add( new ODataRouteMapping( route.Name, route.RoutePrefix, serviceProvider ) ); + container.AddRoute( route.Name, route.RoutePrefix ); return route; } - - static Action ConfigureDefaultServices( this IRouteBuilder builder, Action configureAction ) => configureDefaultServicesFunc( builder, configureAction ); - - static Func, Action> ResolveConfigureDefaultServicesFunc() - { - var method = typeof( ODataRouteBuilderExtensions ).GetMethod( "ConfigureDefaultServices", NonPublic | Static, null, new[] { typeof( IRouteBuilder ), typeof( Action ) }, null )!; - return (Func, Action>) method.CreateDelegate( typeof( Func, Action> ) ); - } - - static void EnsureMetadataController( this IRouteBuilder builder ) - { - var applicationPartManager = builder.ServiceProvider.GetRequiredService(); - applicationPartManager.ApplicationParts.Add( new AssemblyPart( typeof( VersionedMetadataController ).Assembly ) ); - } - - static void ConfigurePathHandler( this IRouteBuilder builder, IServiceProvider rootContainer ) - { - var options = builder.ServiceProvider.GetRequiredService(); - rootContainer.ConfigurePathHandler( options ); - } - - static void ConfigurePathHandler( this IServiceProvider rootContainer, ODataOptions options ) - { - var pathHandler = rootContainer.GetRequiredService(); - - if ( pathHandler != null && pathHandler.UrlKeyDelimiter == null ) - { - pathHandler.UrlKeyDelimiter = options.UrlKeyDelimiter; - } - } - - static void ConfigureBatchHandler( this IRouteBuilder builder, IServiceProvider rootContainer, ODataRoute route ) - { - if ( rootContainer.GetService() is ODataBatchHandler batchHandler ) - { - batchHandler.Configure( builder, route ); - } - } - - static void ConfigureBatchHandler( this IRouteBuilder builder, ODataBatchHandler? batchHandler, ODataRoute route ) => batchHandler?.Configure( builder, route ); - - static void Configure( this ODataBatchHandler batchHandler, IRouteBuilder builder, ODataRoute route ) - { - batchHandler.ODataRoute = route; - batchHandler.ODataRouteName = route.Name; - - var batchPath = '/' + ODataRouteConstants.Batch; - - if ( !IsNullOrEmpty( route.RoutePrefix ) ) - { - batchPath = '/' + route.RoutePrefix + batchPath; - } - - var batchMapping = builder.ServiceProvider.GetRequiredService(); - - batchMapping.AddRoute( route.Name, batchPath ); - } - - static void AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( - this IRouteBuilder builder, - string routeName, - string routePrefix, - IEnumerable unversionedConstraints, - IInlineConstraintResolver inlineConstraintResolver, - Action configureAction ) - { - routeName += UnversionedRouteSuffix; - - var constraint = new UnversionedODataPathRouteConstraint( unversionedConstraints ); - var route = new ODataRoute( builder.DefaultHandler, routeName, routePrefix, constraint, inlineConstraintResolver ); - - builder.Routes.Add( route ); - builder.ServiceProvider.GetRequiredService().CreateODataRootContainer( routeName, configureAction ); - } - - static void AddRouteToRespondWithBadRequestWhenAtLeastOneRouteCouldMatch( - this IRouteBuilder builder, - string routeName, - string routePrefix, - ApiVersion apiVersion, - IInlineConstraintResolver inlineConstraintResolver, - Action configureAction ) - { - routeName += UnversionedRouteSuffix; - - var innerConstraint = new ODataPathRouteConstraint( routeName ); - var constraint = new UnversionedODataPathRouteConstraint( innerConstraint, apiVersion ); - var route = new ODataRoute( builder.DefaultHandler, routeName, routePrefix, constraint, inlineConstraintResolver ); - - builder.Routes.Add( route ); - builder.ServiceProvider.GetRequiredService().CreateODataRootContainer( routeName, configureAction ); - } - - static IRouteConstraint MakeVersionedODataRouteConstraint( ApiVersion apiVersion, ref string versionedRouteName ) - { - if ( apiVersion == null ) - { - return new ODataPathRouteConstraint( versionedRouteName ); - } - - versionedRouteName += "-" + apiVersion.ToString(); - return new VersionedODataPathRouteConstraint( versionedRouteName, apiVersion ); - } - - static string RemoveTrailingSlash( this string @string ) => IsNullOrEmpty( @string ) ? @string : @string.TrimEnd( '/' ); - - // note: we don't have the required information necessary to build the odata route information - // until one or more routes have been mapped. if anyone has subscribed changes, notify them now. - static void NotifyRoutesMapped() => ODataActionDescriptorChangeProvider.Instance.NotifyChanged(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/ServiceProviderExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/ServiceProviderExtensions.cs new file mode 100644 index 00000000..2805db64 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Extensions/ServiceProviderExtensions.cs @@ -0,0 +1,29 @@ +namespace Microsoft.AspNet.OData.Extensions +{ + using System; + + static class ServiceProviderExtensions + { + internal static IServiceProvider WithParent( this IServiceProvider serviceProvider, IServiceProvider parent ) => + new ServiceProviderAggregator( serviceProvider, parent ); + + internal static TService WithParent( + this IServiceProvider serviceProvider, + IServiceProvider parent, + Func implementationFactory ) => implementationFactory( serviceProvider.WithParent( parent ) ); + + sealed class ServiceProviderAggregator : IServiceProvider + { + readonly IServiceProvider parent; + readonly IServiceProvider child; + + internal ServiceProviderAggregator( IServiceProvider child, IServiceProvider parent ) + { + this.parent = parent; + this.child = child; + } + + public object GetService( Type serviceType ) => child.GetService( serviceType ) ?? parent.GetService( serviceType ); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionParameterContext.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionParameterContext.cs index 58f9ac8f..f697420b 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionParameterContext.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionParameterContext.cs @@ -5,17 +5,22 @@ sealed class ActionParameterContext { + readonly ODataPathTemplate? pathTemplate; + internal ActionParameterContext( ODataRouteBuilder routeBuilder, ODataRouteBuilderContext routeContext ) { var odataPathTemplate = routeBuilder.BuildPath( includePrefix: false ); + RouteContext = routeContext; - PathTemplate = RouteContext.PathTemplateHandler.ParseTemplate( odataPathTemplate, Services ); + pathTemplate = RouteContext.PathTemplateHandler.SafeParseTemplate( odataPathTemplate, Services ); } internal ODataRouteBuilderContext RouteContext { get; } internal IServiceProvider Services => RouteContext.Services; - internal ODataPathTemplate PathTemplate { get; } + internal ODataPathTemplate PathTemplate => pathTemplate ?? throw new NotSupportedException(); + + internal bool IsSupported => pathTemplate != null; } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/IODataRouteCollection.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/IODataRouteCollection.cs index 15295521..a03b2699 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/IODataRouteCollection.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/IODataRouteCollection.cs @@ -11,15 +11,6 @@ [CLSCompliant( false )] public interface IODataRouteCollection : IReadOnlyList { - /// - /// Gets a read-only list of OData routes for the specified API version. - /// - /// The API version to get the mapped routes for. - /// A read-only list of mapped OData routes. -#pragma warning disable CA1043 // Use Integral Or String Argument For Indexers - IReadOnlyList this[ApiVersion key] { get; } -#pragma warning restore CA1043 // Use Integral Or String Argument For Indexers - /// /// Gets a value indicating whether the collection contains the specified item. /// @@ -27,29 +18,6 @@ public interface IODataRouteCollection : IReadOnlyList /// True if the collection contains the specified item; otherwise, false. bool Contains( ODataRouteMapping item ); - /// - /// Gets a value indicating whether the collection contains the specified key. - /// - /// The API version to evaluate. - /// True if the collection contains the specified key; otherwise, false. - bool ContainsKey( ApiVersion key ); - - /// - /// Attempts to retrieve the list of mapped OData routes for the specified API version. - /// - /// The API version to evaluate. - /// A read-only list of mapped OData routes. - /// True if the value was successfully retrieved; otherwise, false. - bool TryGetValue( ApiVersion key, [NotNullWhen( true )] out IReadOnlyList? value ); - - /// - /// Searches for the specified object and returns the zero-based index of the first - /// occurrence within the entire collection. - /// - /// The item to evaluate. - /// True if the collection contains the specified item; otherwise, false. - int IndexOf( ODataRouteMapping item ); - /// /// Copies the entire collection to a compatible one-dimensional array, starting at the /// specified index of the target array. diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ImplicitHttpMethodConvention.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ImplicitHttpMethodConvention.cs index 41b60c25..fba00e47 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ImplicitHttpMethodConvention.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ImplicitHttpMethodConvention.cs @@ -42,8 +42,10 @@ static string GetImplicitHttpMethod( ControllerActionDescriptor action ) { const int Post = 2; - foreach ( var supportedHttpMethod in SupportedHttpMethodConventions ) + for ( var i = 0; i < SupportedHttpMethodConventions.Count; i++ ) { + var supportedHttpMethod = SupportedHttpMethodConventions[i]; + if ( action.MethodInfo.Name.StartsWith( supportedHttpMethod, OrdinalIgnoreCase ) ) { return supportedHttpMethod; diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataAttributeRouteInfo.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataAttributeRouteInfo.cs index 0728e3ee..cbfd4b3d 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataAttributeRouteInfo.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataAttributeRouteInfo.cs @@ -24,5 +24,11 @@ public ODataAttributeRouteInfo() /// /// The OData path template for the action. public ODataPathTemplate? ODataTemplate { get; set; } + + /// + /// Gets or sets the corresponding route prefix. + /// + /// The associated route prefix, if any. + public string? RoutePrefix { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataConventionConfigurationContext.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataConventionConfigurationContext.cs index c938443e..3e7a2eb4 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataConventionConfigurationContext.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataConventionConfigurationContext.cs @@ -19,12 +19,35 @@ public partial class ODataConventionConfigurationContext /// The current API version. /// The initial list of routing conventions. [CLSCompliant( false )] - public ODataConventionConfigurationContext( string routeName, IEdmModel edmModel, ApiVersion apiVersion, IList routingConventions ) + [Obsolete( "This constructor will be removed in the next major version. Use the constructor with IServiceProvider instead." )] + public ODataConventionConfigurationContext( + string routeName, + IEdmModel edmModel, + ApiVersion apiVersion, + IList routingConventions ) + : this( routeName, edmModel, apiVersion, routingConventions, No.ServiceProvider ) { } + + /// + /// Initializes a new instance of the class. + /// + /// The current route name. + /// The current EDM model. + /// The current API version. + /// The initial list of routing conventions. + /// The associated serviceProvider. + [CLSCompliant( false )] + public ODataConventionConfigurationContext( + string routeName, + IEdmModel edmModel, + ApiVersion apiVersion, + IList routingConventions, + IServiceProvider serviceProvider ) { RouteName = routeName; EdmModel = edmModel; ApiVersion = apiVersion; RoutingConventions = routingConventions; + ServiceProvider = serviceProvider; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBindingInfoConvention.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBindingInfoConvention.cs index 6488fba3..c6223d88 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBindingInfoConvention.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBindingInfoConvention.cs @@ -1,12 +1,15 @@ namespace Microsoft.AspNet.OData.Routing { using Microsoft.AspNet.OData.Routing.Template; + using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; + using Microsoft.OData; using Microsoft.OData.Edm; using System; using System.Collections.Generic; @@ -40,61 +43,145 @@ internal ODataRouteBindingInfoConvention( public void Apply( ActionDescriptorProviderContext context, ControllerActionDescriptor action ) { var model = action.GetApiVersionModel( Explicit | Implicit ); - var mappings = RouteCollectionProvider.Items; - var routeInfos = new HashSet( new ODataAttributeRouteInfoComparer() ); UpdateControllerName( action ); - if ( model.IsApiVersionNeutral ) + var routeInfos = model.IsApiVersionNeutral ? + ExpandVersionNeutralActions( action ) : + ExpandVersionedActions( action, model ); + + foreach ( var routeInfo in routeInfos ) { - if ( mappings.Count == 0 ) - { - return; - } + context.Results.Add( Clone( action, routeInfo ) ); + } + } - // any mapping will do for a version-neutral action; just take the first one - var mapping = mappings[0]; + IEnumerable ExpandVersionedActions( ControllerActionDescriptor action, ApiVersionModel model ) + { + var mappings = RouteCollectionProvider.Items; + var routeInfos = new HashSet( new ODataAttributeRouteInfoComparer() ); + var declaredVersions = model.DeclaredApiVersions; + var metadata = action.ControllerTypeInfo.IsMetadataController(); - UpdateBindingInfo( action, mapping, routeInfos ); - } - else + for ( var i = 0; i < declaredVersions.Count; i++ ) { - foreach ( var apiVersion in model.DeclaredApiVersions ) + for ( var j = 0; j < mappings.Count; j++ ) { - if ( !mappings.TryGetValue( apiVersion, out var mappingsPerApiVersion ) ) + var mapping = mappings[j]; + var selector = mapping.ModelSelector; + + if ( !selector.Contains( declaredVersions[i] ) ) { continue; } - foreach ( var mapping in mappingsPerApiVersion! ) + if ( metadata ) { UpdateBindingInfo( action, mapping, routeInfos ); } + else + { + var mappedVersions = selector.ApiVersions; + + for ( var k = 0; k < mappedVersions.Count; k++ ) + { + UpdateBindingInfo( action, mappedVersions[k], mapping, routeInfos ); + } + } } } - if ( routeInfos.Count == 0 ) + return routeInfos; + } + + IEnumerable ExpandVersionNeutralActions( ControllerActionDescriptor action ) + { + var mappings = RouteCollectionProvider.Items; + var routeInfos = new HashSet( new ODataAttributeRouteInfoComparer() ); + var visited = new HashSet(); + + for ( var i = 0; i < mappings.Count; i++ ) { - return; + var mapping = mappings[i]; + var mappedVersions = mapping.ModelSelector.ApiVersions; + + for ( var j = 0; j < mappedVersions.Count; j++ ) + { + var apiVersion = mappedVersions[j]; + + if ( visited.Add( apiVersion ) ) + { + UpdateBindingInfo( action, apiVersion, mapping, routeInfos ); + } + } } - using var iterator = routeInfos.GetEnumerator(); + return routeInfos; + } - iterator.MoveNext(); - action.AttributeRouteInfo = iterator.Current; + static void UpdateBindingInfo( + ControllerActionDescriptor action, + ODataRouteMapping mapping, + ICollection routeInfos ) + { + string template; + string path; - while ( iterator.MoveNext() ) + switch ( action.ActionName ) { - context.Results.Add( Clone( action, iterator.Current ) ); + case nameof( MetadataController.GetMetadata ): + case nameof( VersionedMetadataController.GetOptions ): + path = "$metadata"; + + if ( string.IsNullOrEmpty( mapping.RoutePrefix ) ) + { + template = path; + } + else + { + template = mapping.RoutePrefix + '/' + path; + } + + break; + default: + path = "/"; + template = string.IsNullOrEmpty( mapping.RoutePrefix ) ? path : mapping.RoutePrefix; + break; } + + var handler = mapping.Services.GetRequiredService(); + var routeInfo = new ODataAttributeRouteInfo() + { + Name = mapping.RouteName, + Template = template, + ODataTemplate = handler.ParseTemplate( path, mapping.Services ), + RoutePrefix = mapping.RoutePrefix, + }; + + routeInfos.Add( routeInfo ); } - void UpdateBindingInfo( ControllerActionDescriptor action, ODataRouteMapping mapping, ICollection routeInfos ) + void UpdateBindingInfo( + ControllerActionDescriptor action, + ApiVersion apiVersion, + ODataRouteMapping mapping, + ICollection routeInfos ) { - var routeContext = new ODataRouteBuilderContext( mapping, action, Options ); + var routeContext = new ODataRouteBuilderContext( apiVersion, mapping, action, Options ); + + if ( routeContext.IsRouteExcluded ) + { + return; + } + var routeBuilder = new ODataRouteBuilder( routeContext ); var parameterContext = new ActionParameterContext( routeBuilder, routeContext ); + if ( !parameterContext.IsSupported ) + { + return; + } + for ( var i = 0; i < action.Parameters.Count; i++ ) { UpdateBindingInfo( parameterContext, action.Parameters[i] ); @@ -102,8 +189,10 @@ void UpdateBindingInfo( ControllerActionDescriptor action, ODataRouteMapping map var routeInfo = new ODataAttributeRouteInfo() { + Name = mapping.RouteName, Template = routeBuilder.BuildPath( includePrefix: true ), ODataTemplate = parameterContext.PathTemplate, + RoutePrefix = mapping.RoutePrefix, }; routeInfos.Add( routeInfo ); @@ -270,10 +359,14 @@ public bool Equals( ODataAttributeRouteInfo? x, ODataAttributeRouteInfo? y ) return false; } - return StringComparer.OrdinalIgnoreCase.Equals( x.Template, y.Template ); + var comparer = StringComparer.OrdinalIgnoreCase; + + return comparer.Equals( x.Template, y.Template ) && + comparer.Equals( x.Name, y.Name ); } - public int GetHashCode( ODataAttributeRouteInfo obj ) => obj is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode( obj.Template ); + public int GetHashCode( ODataAttributeRouteInfo obj ) => + obj is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode( obj.Template ); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilderContext.Core.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilderContext.Core.cs index 476882b2..e118b8e9 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilderContext.Core.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilderContext.Core.cs @@ -1,11 +1,14 @@ namespace Microsoft.AspNet.OData.Routing { using Microsoft.AspNet.OData; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.Extensions.DependencyInjection; using Microsoft.OData; using Microsoft.OData.Edm; + using System.Collections.Generic; using System.Reflection; using static System.Linq.Enumerable; @@ -14,37 +17,41 @@ partial class ODataRouteBuilderContext private IODataPathTemplateHandler? templateHandler; internal ODataRouteBuilderContext( + ApiVersion apiVersion, ODataRouteMapping routeMapping, ControllerActionDescriptor actionDescriptor, ODataApiVersioningOptions options ) { - ApiVersion = routeMapping.ApiVersion; + ApiVersion = apiVersion; Services = routeMapping.Services; - EdmModel = Services.GetRequiredService(); routeAttribute = actionDescriptor.MethodInfo.GetCustomAttributes().FirstOrDefault(); RouteTemplate = routeAttribute?.PathTemplate; - Route = routeMapping.Route; + RoutePrefix = routeMapping.RoutePrefix; ActionDescriptor = actionDescriptor; Options = options; UrlKeyDelimiter = UrlKeyDelimiterOrDefault( Services.GetRequiredService().UrlKeyDelimiter ); - var container = EdmModel.EntityContainer; + var selector = Services.GetRequiredService(); + var model = selector.SelectModel( apiVersion ); + var container = model?.EntityContainer; - if ( container == null ) + if ( model == null || container == null ) { + EdmModel = Services.GetRequiredService(); IsRouteExcluded = true; return; } - var controllerName = actionDescriptor.ControllerName; - var actionName = actionDescriptor.ActionName; - - EntitySet = container.FindEntitySet( controllerName ); - Operation = container.FindOperationImports( controllerName ).FirstOrDefault()?.Operation ?? - EdmModel.FindDeclaredOperations( string.Concat( container.Namespace, ".", actionName ) ).FirstOrDefault(); - ActionType = GetActionType( EntitySet, Operation ); + EdmModel = model; + Services = new FixedEdmModelServiceProviderDecorator( Services, model ); + EntitySet = container.FindEntitySet( actionDescriptor.ControllerName ); + Operation = ResolveOperation( container, actionDescriptor.ActionName ); + ActionType = GetActionType( EntitySet, Operation, actionDescriptor ); + IsRouteExcluded = ActionType == ODataRouteActionType.Unknown; } + static IEnumerable GetHttpMethods( ControllerActionDescriptor action ) => action.GetHttpMethods(); + internal IODataPathTemplateHandler PathTemplateHandler => templateHandler ??= Services.GetRequiredService(); } diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteCollectionProvider.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteCollectionProvider.cs index 2d6b5238..2dedba76 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteCollectionProvider.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteCollectionProvider.cs @@ -1,10 +1,8 @@ namespace Microsoft.AspNet.OData.Routing { - using Microsoft.AspNetCore.Mvc; using System; using System.Collections; using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; /// /// Represents an object that manages the collection of registered OData routes. @@ -12,7 +10,7 @@ [CLSCompliant( false )] public sealed class ODataRouteCollectionProvider : IODataRouteCollectionProvider { - private readonly ODataRouteCollection items = new ODataRouteCollection(); + readonly ODataRouteCollection items = new ODataRouteCollection(); /// /// Initializes a new instance of the class. @@ -28,9 +26,6 @@ public ODataRouteCollectionProvider() { } sealed class ODataRouteCollection : IODataRouteCollection { private readonly List items = new List(); - private readonly Dictionary> dictionary = new Dictionary>(); - - public IReadOnlyList this[ApiVersion key] => dictionary[key]; public ODataRouteMapping this[int index] => items[index]; @@ -38,43 +33,13 @@ sealed class ODataRouteCollection : IODataRouteCollection public bool Contains( ODataRouteMapping item ) => items.Contains( item ); - public bool ContainsKey( ApiVersion key ) => dictionary.ContainsKey( key ); - public void CopyTo( ODataRouteMapping[] array, int index ) => items.CopyTo( array, index ); public IEnumerator GetEnumerator() => items.GetEnumerator(); - public int IndexOf( ODataRouteMapping item ) => items.IndexOf( item ); - - public bool TryGetValue( ApiVersion key, [NotNullWhen( true )] out IReadOnlyList? value ) - { - if ( dictionary.TryGetValue( key, out var list ) ) - { - value = list; - return true; - } - - value = default; - return false; - } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - internal void Add( ODataRouteMapping item ) - { - var key = item.ApiVersion; - - if ( dictionary.TryGetValue( key, out var list ) ) - { - list.Add( item ); - } - else - { - dictionary.Add( key, new List() { item } ); - } - - items.Add( item ); - } + internal void Add( ODataRouteMapping item ) => items.Add( item ); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteMapping.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteMapping.cs index 05b6aadd..c0886101 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteMapping.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteMapping.cs @@ -1,6 +1,7 @@ namespace Microsoft.AspNet.OData.Routing { - using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.OData.Edm; using System; /// @@ -9,35 +10,46 @@ [CLSCompliant( false )] public class ODataRouteMapping { + readonly IServiceProvider serviceProvider; + IServiceProvider? services; + IEdmModelSelector? modelSelector; + /// /// Initializes a new instance of the class. /// - /// The mapped OData route. - /// The API version associated with the route. + /// The OData route name. + /// The OData route prefix. /// The services associated with the route. - public ODataRouteMapping( ODataRoute route, ApiVersion apiVersion, IServiceProvider services ) + public ODataRouteMapping( string routeName, string? routePrefix, IServiceProvider services ) { - Route = route; - ApiVersion = apiVersion; - Services = services; + RouteName = routeName; + RoutePrefix = routePrefix?.Trim( '/' ); + serviceProvider = services; } /// - /// Gets the mapped OData route. + /// Gets the name of the mapped OData route. + /// + /// The OData route name. + public string RouteName { get; } + + /// + /// Gets the prefix of the mapped OData route. /// - /// The mapped OData route. - public ODataRoute Route { get; } + /// The OData route prefix. + public string? RoutePrefix { get; } /// - /// Gets the API version for the route. + /// Gets the EDM model selector. /// - /// The API version for the route. - public ApiVersion ApiVersion { get; } + /// The associated EDM model selector. + public IEdmModelSelector ModelSelector => modelSelector ??= Services.GetRequiredService(); /// /// Gets the services associated with the route. /// /// The services associated with the route. - public IServiceProvider Services { get; } + public IServiceProvider Services => + services ??= serviceProvider.GetRequiredService().GetODataRootContainer( RouteName ); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/UnversionedODataPathRouteConstraint.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/UnversionedODataPathRouteConstraint.cs deleted file mode 100644 index b8645b62..00000000 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/UnversionedODataPathRouteConstraint.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace Microsoft.AspNet.OData.Routing -{ - using Microsoft.AspNet.OData.Extensions; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.Versioning; - using Microsoft.AspNetCore.Routing; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Options; - using System; - using System.Collections.Generic; - using static Microsoft.AspNetCore.Routing.RouteDirection; - - sealed class UnversionedODataPathRouteConstraint : IRouteConstraint - { - readonly ApiVersion? apiVersion; - readonly IEnumerable innerConstraints; - - internal UnversionedODataPathRouteConstraint( IEnumerable innerConstraints ) => - this.innerConstraints = innerConstraints; - - internal UnversionedODataPathRouteConstraint( IRouteConstraint innerConstraint, ApiVersion apiVersion ) - { - innerConstraints = new[] { innerConstraint }; - this.apiVersion = apiVersion; - } - - bool MatchAnyVersion => apiVersion == null; - - public bool Match( HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection ) - { - if ( routeDirection == UrlGeneration ) - { - return true; - } - - var feature = httpContext.Features.Get(); - - // determine whether this constraint can match any api version and no api version has otherwise been matched - if ( MatchAnyVersion && feature.RequestedApiVersion == null ) - { - var options = httpContext.RequestServices.GetRequiredService>().Value; - - // is implicitly matching an api version allowed? - if ( options.AssumeDefaultVersionWhenUnspecified || IsServiceDocumentOrMetadataRoute( values ) ) - { - var odata = httpContext.ODataVersioningFeature(); - var model = new ApiVersionModel( odata.MatchingRoutes.Keys, Array.Empty() ); - var selector = httpContext.RequestServices.GetRequiredService(); - var requestedApiVersion = feature.RequestedApiVersion = selector.SelectVersion( httpContext.Request, model ); - - // if an api version is selected, determine if it corresponds to a route that has been previously matched - if ( requestedApiVersion != null && odata.MatchingRoutes.TryGetValue( requestedApiVersion, out var routeName ) ) - { - // create a new versioned path constraint on the fly and evaluate it. this sets up the underlying odata - // infrastructure such as the container, edm, etc. this has no bearing the action selector which will - // already select the correct action. without this the response may be incorrect, even if the correct - // action is selected and executed. - var constraint = new VersionedODataPathRouteConstraint( routeName, requestedApiVersion ); - return constraint.Match( httpContext, route, routeKey, values, routeDirection ); - } - } - } - else if ( !MatchAnyVersion && feature.RequestedApiVersion != apiVersion ) - { - return false; - } - - // delete the request container because ODataPathRouteConstraint will try to create it resulting in an exception - // ODataPathRouteConstraint cleans itself up afterward - // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNetCore.OData/Routing/ODataPathRouteConstraint.cs#L53 - httpContext.Request.DeleteRequestContainer( true ); - - // by evaluating the remaining unversioned constraints, this will ultimately determine whether 400 or 404 - // is returned for an odata request - foreach ( var constraint in innerConstraints ) - { - if ( constraint.Match( httpContext, route, routeKey, values, routeDirection ) ) - { - return true; - } - } - - return false; - } - - static bool IsServiceDocumentOrMetadataRoute( RouteValueDictionary values ) => - values.TryGetValue( "odataPath", out var value ) && ( value == null || Equals( value, "$metadata" ) ); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs index 26f469cb..8c79baea 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedAttributeRoutingConvention.cs @@ -10,6 +10,8 @@ using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Microsoft.OData.Edm; using System; using System.Collections.Generic; using System.Linq; @@ -21,15 +23,12 @@ [CLSCompliant( false )] public partial class VersionedAttributeRoutingConvention : IODataRoutingConvention { - readonly IServiceProvider serviceProvider; - /// /// Initializes a new instance of the class. /// /// The name of the route. /// The current HTTP configuration. - /// The API version associated with the convention. - public VersionedAttributeRoutingConvention( string routeName, IServiceProvider serviceProvider, ApiVersion apiVersion ) + public VersionedAttributeRoutingConvention( string routeName, IServiceProvider serviceProvider ) { if ( serviceProvider == null ) { @@ -39,50 +38,19 @@ public VersionedAttributeRoutingConvention( string routeName, IServiceProvider s var perRouteContainer = serviceProvider.GetRequiredService(); var rootContainer = perRouteContainer.GetODataRootContainer( routeName ); - this.routeName = routeName; - this.serviceProvider = serviceProvider; - ApiVersion = apiVersion; + RouteName = routeName; ODataPathTemplateHandler = rootContainer.GetRequiredService(); } - /// - /// Initializes a new instance of the class. - /// - /// The name of the route. - /// The current HTTP configuration. - /// The OData path template handler associated with the routing convention. - /// The API version associated with the convention. - public VersionedAttributeRoutingConvention( string routeName, IServiceProvider serviceProvider, IODataPathTemplateHandler pathTemplateHandler, ApiVersion apiVersion ) - { - this.routeName = routeName; - this.serviceProvider = serviceProvider; - ApiVersion = apiVersion; - ODataPathTemplateHandler = pathTemplateHandler; - } - - IDictionary AttributeMappings - { - get - { - if ( attributeMappings == null ) - { - var provider = serviceProvider.GetRequiredService(); - var actions = provider.ActionDescriptors.Items.OfType(); - attributeMappings = BuildAttributeMappings( actions ); - } - - return attributeMappings; - } - } - /// /// Returns a value indicating whether the specified action should be mapped using attribute routing conventions. /// /// The controller action descriptor to evaluate. + /// The API version to evaluate. /// True if the should be mapped as an OData action or function; otherwise, false. /// This method will match any OData action that explicitly or implicitly matches the API version applied /// to the associated model. - public virtual bool ShouldMapAction( ControllerActionDescriptor action ) => action.IsMappedTo( ApiVersion ); + public virtual bool ShouldMapAction( ControllerActionDescriptor action, ApiVersion? apiVersion ) => action.IsMappedTo( apiVersion ); /// /// Selects the controller for OData requests. @@ -96,36 +64,38 @@ protected virtual IEnumerable SelectController( RouteCon throw new ArgumentNullException( nameof( routeContext ) ); } - var items = new Dictionary(); + var version = SelectApiVersion( routeContext ); + var attributeMappings = attributeMappingsPerApiVersion.GetOrAdd( version, key => BuildAttributeMappings( key, routeContext ) ); + var values = new Dictionary(); var feature = routeContext.HttpContext.ODataFeature(); var odataPath = feature.Path; - IDictionary routeData = routeContext.RouteData.Values; + var routeData = routeContext.RouteData.Values; - foreach ( var attributeMapping in AttributeMappings ) + foreach ( var attributeMapping in attributeMappings ) { var template = attributeMapping.Key; var action = attributeMapping.Value; - if ( !template.TryMatch( odataPath, items ) ) + if ( !template.TryMatch( odataPath, values ) ) { continue; } - foreach ( var item in items ) + foreach ( var value in values ) { - if ( IsODataRouteParameter( item ) ) + if ( IsODataRouteParameter( value ) ) { - feature.RoutingConventionsStore[item.Key] = item.Value; + feature.RoutingConventionsStore[value.Key] = value.Value; } else { - routeData[item.Key] = item.Value; + routeData[value.Key] = value.Value; } } - items[ODataRouteConstants.Action] = action.ActionName; + values[ODataRouteConstants.Action] = action.ActionName; - yield return new SelectControllerResult( action.ControllerName, items ); + yield return new SelectControllerResult( action.ControllerName, values ); } } @@ -162,20 +132,81 @@ public IEnumerable SelectAction( RouteContext routeC } } - IDictionary BuildAttributeMappings( IEnumerable actions ) + /// + /// Selects the API version from the given HTTP request. + /// + /// The current context. + /// The selected API version. + protected virtual ApiVersion SelectApiVersion( RouteContext routeContext ) { + if ( routeContext == null ) + { + throw new ArgumentNullException( nameof( routeContext ) ); + } + + var httpContext = routeContext.HttpContext; + var feature = httpContext.Features.Get(); + ApiVersion? version; + + try + { + version = feature.RequestedApiVersion; + } + catch ( AmbiguousApiVersionException ) + { + version = default; + } + + if ( version != null ) + { + return version; + } + + var options = httpContext.RequestServices.GetRequiredService>().Value; + + if ( !options.AssumeDefaultVersionWhenUnspecified ) + { + return version ?? ApiVersion.Neutral; + } + + var modelSelector = httpContext.Request.GetRequestContainer().GetRequiredService(); + var versionSelector = options.ApiVersionSelector; + var model = new ApiVersionModel( modelSelector.ApiVersions, Enumerable.Empty() ); + + return versionSelector.SelectVersion( httpContext.Request, model ); + } + + static IEnumerable GetODataRoutePrefixes( ControllerActionDescriptor controllerAction ) + { + var prefixAttributes = controllerAction.ControllerTypeInfo.GetCustomAttributes( inherit: false ); + return GetODataRoutePrefixes( prefixAttributes, controllerAction.ControllerTypeInfo.FullName! ); + } + + IReadOnlyDictionary BuildAttributeMappings( ApiVersion version, RouteContext routeContext ) + { + var httpContext = routeContext.HttpContext; + var services = httpContext.RequestServices; + var provider = services.GetRequiredService(); + var actions = provider.ActionDescriptors.Items.OfType(); var attributeMappings = new Dictionary(); + var serviceProvider = httpContext.Request.GetRequestContainer(); foreach ( var action in actions ) { - if ( !action.ControllerTypeInfo.IsODataController() || !ShouldMapAction( action ) ) + if ( !action.ControllerTypeInfo.IsODataController() || !ShouldMapAction( action, version ) ) + { + continue; + } + + if ( action.AttributeRouteInfo is ODataAttributeRouteInfo routeInfo && routeInfo.ODataTemplate != null ) { + attributeMappings.Add( routeInfo.ODataTemplate, action ); continue; } foreach ( var prefix in GetODataRoutePrefixes( action ) ) { - var pathTemplates = GetODataPathTemplates( prefix, action ); + var pathTemplates = GetODataPathTemplates( prefix, action, serviceProvider ); foreach ( var pathTemplate in pathTemplates ) { @@ -187,21 +218,19 @@ IDictionary BuildAttributeMapping return attributeMappings; } - static IEnumerable GetODataRoutePrefixes( ControllerActionDescriptor controllerAction ) - { - var prefixAttributes = controllerAction.ControllerTypeInfo.GetCustomAttributes( inherit: false ); - return GetODataRoutePrefixes( prefixAttributes, controllerAction.ControllerTypeInfo?.FullName ?? string.Empty ); - } - - IEnumerable GetODataPathTemplates( string prefix, ControllerActionDescriptor controllerAction ) + IEnumerable GetODataPathTemplates( string prefix, ControllerActionDescriptor controllerAction, IServiceProvider serviceProvider ) { var routeAttributes = controllerAction.MethodInfo.GetCustomAttributes( inherit: false ); - var perRouteContainer = serviceProvider.GetRequiredService(); - var requestContainer = perRouteContainer.GetODataRootContainer( routeName ); - var controllerName = controllerAction.ControllerName; - var actionName = controllerAction.ActionName; - return routeAttributes.Select( route => GetODataPathTemplate( prefix, route.PathTemplate, requestContainer, controllerName, actionName ) ).Where( template => template != null ); + foreach ( var route in routeAttributes ) + { + var template = GetODataPathTemplate( prefix, route.PathTemplate, serviceProvider ); + + if ( template != null ) + { + yield return template; + } + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedMetadataRoutingConvention.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedMetadataRoutingConvention.cs index 1715a438..5e2d6aaa 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedMetadataRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedMetadataRoutingConvention.cs @@ -3,10 +3,15 @@ using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Routing.Conventions; using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Abstractions; + using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; + using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; + using Microsoft.OData.Edm; using System; using System.Collections.Generic; using System.Linq; @@ -31,7 +36,43 @@ public class VersionedMetadataRoutingConvention : IODataRoutingConvention throw new ArgumentNullException( nameof( odataPath ) ); } - return odataPath.PathTemplate == "~" || odataPath.PathTemplate == "~/$metadata" ? "VersionedMetadata" : null; + if ( request == null ) + { + throw new ArgumentNullException( nameof( request ) ); + } + + if ( odataPath.PathTemplate != "~" && odataPath.PathTemplate != "~/$metadata" ) + { + return null; + } + + var context = request.HttpContext; + var feature = context.Features.Get(); + string? apiVersion; + + try + { + apiVersion = feature.RawRequestedApiVersion; + } + catch ( AmbiguousApiVersionException ) + { + // the appropriate response will be handled by policy + return "VersionedMetadata"; + } + + // the service document and metadata endpoints are special, but they are not neutral. if the client doesn't + // specify a version, they may not know to. assume a default version by policy, but it's always allowed. + // a client might also send an OPTIONS request to determine which versions are available (ex: tooling) + if ( string.IsNullOrEmpty( apiVersion ) ) + { + var modelSelector = request.GetRequestContainer().GetRequiredService(); + var versionSelector = context.RequestServices.GetRequiredService(); + var model = new ApiVersionModel( modelSelector.ApiVersions, Enumerable.Empty() ); + + feature.RequestedApiVersion = versionSelector.SelectVersion( request, model ); + } + + return "VersionedMetadata"; } /// @@ -50,7 +91,6 @@ public class VersionedMetadataRoutingConvention : IODataRoutingConvention const IEnumerable? NoActions = default; var httpContext = routeContext.HttpContext; - var actionCollectionProvider = httpContext.RequestServices.GetRequiredService(); var odataPath = httpContext.ODataFeature().Path; var request = httpContext.Request; var controller = SelectController( odataPath, request ); @@ -60,11 +100,12 @@ public class VersionedMetadataRoutingConvention : IODataRoutingConvention return NoActions; } - var actionDescriptors = actionCollectionProvider.ActionDescriptors.Items.OfType().Where( c => c.ControllerName == controller ); + var actionCollectionProvider = httpContext.RequestServices.GetRequiredService(); + var actions = actionCollectionProvider.ActionDescriptors.Items; if ( odataPath.PathTemplate == "~" ) { - return actionDescriptors.Where( a => a.ActionName == nameof( VersionedMetadataController.GetServiceDocument ) ); + return SelectActions( actions, controller, nameof( VersionedMetadataController.GetServiceDocument ) ); } if ( odataPath.PathTemplate != "~/$metadata" ) @@ -76,14 +117,31 @@ public class VersionedMetadataRoutingConvention : IODataRoutingConvention if ( method.Equals( "GET", OrdinalIgnoreCase ) ) { - return actionDescriptors.Where( a => a.ActionName == nameof( VersionedMetadataController.GetMetadata ) ); + return SelectActions( actions, controller, nameof( VersionedMetadataController.GetMetadata ) ); } else if ( method.Equals( "OPTIONS", OrdinalIgnoreCase ) ) { - return actionDescriptors.Where( a => a.ActionName == nameof( VersionedMetadataController.GetOptions ) ); + return SelectActions( actions, controller, nameof( VersionedMetadataController.GetOptions ) ); } return NoActions; } + + static IEnumerable SelectActions( + IReadOnlyList actions, + string controllerName, + string actionName ) + { + for ( var i = 0; i < actions.Count; i++ ) + { + if ( actions[i] is ControllerActionDescriptor action ) + { + if ( action.ControllerName == controllerName && action.ActionName == actionName ) + { + yield return action; + } + } + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedODataPathRouteConstraint.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedODataPathRouteConstraint.cs deleted file mode 100644 index 8205fd79..00000000 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedODataPathRouteConstraint.cs +++ /dev/null @@ -1,103 +0,0 @@ -namespace Microsoft.AspNet.OData.Routing -{ - using Microsoft.AspNet.OData.Extensions; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.Versioning; - using Microsoft.AspNetCore.Routing; - using System; - using static Microsoft.AspNetCore.Routing.RouteDirection; - - /// - /// Represents an OData path route constraint which supports versioning. - /// - [CLSCompliant( false )] - public class VersionedODataPathRouteConstraint : ODataPathRouteConstraint - { - /// - /// Initializes a new instance of the class. - /// - /// The name of the route this constraint is associated with. - /// The API version associated with the route constraint. - public VersionedODataPathRouteConstraint( string routeName, ApiVersion apiVersion ) - : base( routeName ) => ApiVersion = apiVersion; - - /// - /// Gets the API version matched by the current OData path route constraint. - /// - /// The API version associated with the route constraint. - public ApiVersion ApiVersion { get; } - - /// - /// Determines whether the route constraint matches the specified criteria. - /// - /// The current HTTP context. - /// The current route. - /// The key of the route parameter to match. - /// The current collection of route values. - /// The route direction to match. - /// True if the route constraint is matched; otherwise, false. - public override bool Match( HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection ) - { - if ( httpContext == null ) - { - throw new ArgumentNullException( nameof( httpContext ) ); - } - - if ( routeDirection == UrlGeneration || !TryGetRequestedApiVersion( httpContext, out var requestedVersion ) ) - { - // note: if an error occurs reading the api version, still let the base constraint - // match the request. the IActionSelector will produce 400 during action selection. - return base.Match( httpContext, route, routeKey, values, routeDirection ); - } - - bool matched; - - try - { - matched = base.Match( httpContext, route, routeKey, values, routeDirection ); - } - catch ( InvalidOperationException ) - { - // note: the base implementation of Match will setup the container. if this happens more - // than once, an exception is thrown. this most often occurs when policy allows implicitly - // matching an api version and all routes must be visited to determine their candidacy. if - // this happens, delete the container and retry. - httpContext.Request.DeleteRequestContainer( true ); - matched = base.Match( httpContext, route, routeKey, values, routeDirection ); - } - - if ( !matched ) - { - return false; - } - - if ( requestedVersion == null ) - { - // we definitely matched the route, but not necessarily the api version so - // track this route as a matching candidate - httpContext.ODataVersioningFeature().MatchingRoutes[ApiVersion] = RouteName; - return false; - } - - return ApiVersion == requestedVersion; - } - - static bool TryGetRequestedApiVersion( HttpContext httpContext, out ApiVersion? apiVersion ) - { - var feature = httpContext.Features.Get(); - - try - { - apiVersion = feature.RequestedApiVersion; - } - catch ( AmbiguousApiVersionException ) - { - apiVersion = default; - return false; - } - - return true; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedODataRoutingConventions.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedODataRoutingConventions.cs new file mode 100644 index 00000000..4c8adaa6 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/VersionedODataRoutingConventions.cs @@ -0,0 +1,23 @@ +namespace Microsoft.AspNet.OData.Routing +{ + using Microsoft.AspNet.OData.Routing.Conventions; + using System; + using System.Collections.Generic; + + /// + /// Provides additional implementation specific to ASP.NET Core. + /// + [CLSCompliant( false )] + public static partial class VersionedODataRoutingConventions + { + /// + /// Creates a mutable list of the default OData routing conventions with attribute routing enabled. + /// + /// The name of the route. + /// The service provider. + /// A mutable list of the default OData routing conventions. + /// Use this version for Endpoint Routing. + public static IList CreateDefaultWithAttributeRouting( string routeName, IServiceProvider serviceProvider ) => + EnsureConventions( CreateDefault(), new VersionedAttributeRoutingConvention( routeName, serviceProvider ) ); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Abstractions/ActionDescriptorExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Abstractions/ActionDescriptorExtensions.cs new file mode 100644 index 00000000..fce30bd5 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Abstractions/ActionDescriptorExtensions.cs @@ -0,0 +1,75 @@ +namespace Microsoft.AspNetCore.Mvc.Abstractions +{ + using Microsoft.AspNetCore.Mvc.ActionConstraints; + using Microsoft.AspNetCore.Mvc.Controllers; + using Microsoft.AspNetCore.Mvc.Routing; + using System; + using System.Collections.Generic; + using System.Linq; + using static System.Array; + using static System.StringComparison; + + static class ActionDescriptorExtensions + { + const string DefaultHttpMethod = "POST"; + static readonly string[] SupportedHttpMethodConventions = new[] + { + "GET", + "PUT", + "POST", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS", + }; + + internal static IEnumerable GetHttpMethods( this ActionDescriptor action ) + { + if ( action is ControllerActionDescriptor controllerAction ) + { + return controllerAction.GetHttpMethods(); + } + + var constraints = ( action.ActionConstraints ?? Empty() ).OfType(); + var definedHttpMethods = constraints.SelectMany( ac => ac.HttpMethods ); + var httpMethods = new HashSet( definedHttpMethods, StringComparer.OrdinalIgnoreCase ); + + if ( httpMethods.Count == 0 ) + { + httpMethods.Add( DefaultHttpMethod ); + } + + return httpMethods; + } + + internal static IEnumerable GetHttpMethods( this ControllerActionDescriptor action ) + { + var constraints = ( action.ActionConstraints ?? Empty() ).OfType(); + var attributes = action.MethodInfo.GetCustomAttributes( inherit: false ).OfType(); + var definedHttpMethods = constraints.SelectMany( ac => ac.HttpMethods ).Concat( attributes.SelectMany( a => a.HttpMethods ) ); + var httpMethods = new HashSet( definedHttpMethods, StringComparer.OrdinalIgnoreCase ); + + if ( httpMethods.Count > 0 ) + { + return httpMethods; + } + + for ( var i = 0; i < SupportedHttpMethodConventions.Length; i++ ) + { + var supportedHttpMethod = SupportedHttpMethodConventions[i]; + + if ( action.MethodInfo.Name.StartsWith( supportedHttpMethod, OrdinalIgnoreCase ) ) + { + httpMethods.Add( supportedHttpMethod ); + } + } + + if ( httpMethods.Count == 0 ) + { + httpMethods.Add( DefaultHttpMethod ); + } + + return httpMethods; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/HttpContextExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/HttpContextExtensions.cs deleted file mode 100644 index db2a8917..00000000 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/HttpContextExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc -{ - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc.Versioning; - using System; - - /// - /// Provides extension methods for the class. - /// - [CLSCompliant( false )] - public static class HttpContextExtensions - { - /// - /// Gets the OData versioning feature. - /// - /// The current HTTP context. - /// The associated with the current HTTP context. - public static IODataVersioningFeature ODataVersioningFeature( this HttpContext httpContext ) - { - if ( httpContext == null ) - { - throw new ArgumentNullException( nameof( httpContext ) ); - } - - var features = httpContext.Features; - var feature = features.Get(); - - if ( feature == null ) - { - features.Set( feature = new ODataVersioningFeature() ); - } - - return feature; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/ODataActionDescriptorProvider.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/ODataActionDescriptorProvider.cs index b7785cd9..61f0d451 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/ODataActionDescriptorProvider.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/ODataActionDescriptorProvider.cs @@ -9,8 +9,6 @@ namespace Microsoft.AspNetCore.Mvc using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.Extensions.Options; - using System.Collections.Generic; - using System.Linq; sealed class ODataActionDescriptorProvider : IActionDescriptorProvider { @@ -37,35 +35,32 @@ public void OnProvidersExecuted( ActionDescriptorProviderContext context ) return; } - var results = context.Results.ToArray(); + var results = context.Results; var conventions = new IODataActionDescriptorConvention[] { new ImplicitHttpMethodConvention(), new ODataRouteBindingInfoConvention( routeCollectionProvider, modelMetadataProvider, options ), }; - foreach ( var action in ODataActions( results ) ) + for ( var i = results.Count - 1; i >= 0; i-- ) { - foreach ( var convention in conventions ) + var result = results[i]; + + if ( !( result is ControllerActionDescriptor action ) || + !action.ControllerTypeInfo.IsODataController() ) { - convention.Apply( context, action ); + continue; } - } - } - public void OnProvidersExecuting( ActionDescriptorProviderContext context ) { } + results.RemoveAt( i ); - static IEnumerable ODataActions( IEnumerable results ) - { - foreach ( var result in results ) - { - if ( result is ControllerActionDescriptor action && - action.ControllerTypeInfo.IsODataController() && - !action.ControllerTypeInfo.IsMetadataController() ) + for ( var j = 0; j < conventions.Length; j++ ) { - yield return action; + conventions[j].Apply( context, action ); } } } + + public void OnProvidersExecuting( ActionDescriptorProviderContext context ) { } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Routing/ActionCandidate.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Routing/ActionCandidate.cs deleted file mode 100644 index 4ea9f124..00000000 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Routing/ActionCandidate.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc.Routing -{ - using Microsoft.AspNet.OData; - using Microsoft.AspNetCore.Mvc.Abstractions; - using System.Collections.Generic; - using System.Diagnostics; - using static Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource; - - [DebuggerDisplay( "{Action.DisplayName,nq}" )] - sealed class ActionCandidate - { - internal ActionCandidate( ActionDescriptor action ) - { - TotalParameterCount = action.Parameters.Count; - - var filteredParameters = new List( TotalParameterCount ); - - for ( var i = 0; i < TotalParameterCount; i++ ) - { - var parameter = action.Parameters[i]; - - if ( parameter.ParameterType.IsModelBound() ) - { - continue; - } - - var bindingSource = parameter.BindingInfo?.BindingSource; - - if ( bindingSource != Custom && bindingSource != Path ) - { - continue; - } - - filteredParameters.Add( parameter.Name ); - } - - Action = action; - FilteredParameters = filteredParameters; - } - - internal ActionDescriptor Action { get; } - - internal int TotalParameterCount { get; } - - internal IReadOnlyList FilteredParameters { get; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/IODataVersioningFeature.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/IODataVersioningFeature.cs deleted file mode 100644 index 8cae9fc4..00000000 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/IODataVersioningFeature.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc.Versioning -{ - using System; - using System.Collections.Generic; - - /// - /// Defines the behavior of the OData versioning feature. - /// - public interface IODataVersioningFeature - { - /// - /// Gets a collection of API version to route name mappings that have been matched in the current request. - /// - /// A collection of key/value pairs representing the mapping - /// of API versions to route names that have been matched in the current request. - IDictionary MatchingRoutes { get; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/MetadataControllerConvention.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/MetadataControllerConvention.cs index ac86d88e..34ea277e 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/MetadataControllerConvention.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/MetadataControllerConvention.cs @@ -16,14 +16,13 @@ // services.AddOData().EnableApiVersioning(); sealed class MetadataControllerConvention : IApplicationModelConvention { - readonly TypeInfo metadataControllerType = typeof( MetadataController ).GetTypeInfo(); readonly ODataApiVersioningOptions options; internal MetadataControllerConvention( ODataApiVersioningOptions options ) => this.options = options; public void Apply( ApplicationModel application ) { - var metadataController = default( ControllerModel ); + var metadataControllers = new List(); var supported = new HashSet(); var deprecated = new HashSet(); @@ -31,9 +30,9 @@ public void Apply( ApplicationModel application ) { var controller = application.Controllers[i]; - if ( metadataControllerType.IsAssignableFrom( controller.ControllerType ) ) + if ( controller.ControllerType.IsMetadataController() ) { - metadataController = controller; + metadataControllers.Add( controller ); continue; } @@ -59,8 +58,11 @@ public void Apply( ApplicationModel application ) } } + var metadataController = SelectBestMetadataController( metadataControllers ); + if ( metadataController == null ) { + // graceful exit; in theory, this should never happen return; } @@ -73,5 +75,46 @@ public void Apply( ApplicationModel application ) builder.ApplyTo( metadataController ); } + + static ControllerModel? SelectBestMetadataController( IReadOnlyList controllers ) + { + // note: there should be at least 2 metadata controllers, but there could be 3+ + // if a developer defines their own custom controller. ultimately, there an be + // only one. choose and version the best controller using the following ranking: + // + // 1. original MetadataController type + // 2. VersionedMetadataController type (it's possible this has been removed upstream) + // 3. last, custom type of MetadataController from another assembly + var bestController = default( ControllerModel ); + var original = typeof( MetadataController ).GetTypeInfo(); + var versioned = typeof( VersionedMetadataController ).GetTypeInfo(); + + for ( var i = 0; i < controllers.Count; i++ ) + { + var controller = controllers[i]; + + if ( bestController == default ) + { + bestController = controller; + } + else if ( bestController.ControllerType == original && + controller.ControllerType == versioned ) + { + bestController = controller; + } + else if ( bestController.ControllerType == versioned && + controller.ControllerType != original ) + { + bestController = controller; + } + else if ( bestController.ControllerType != versioned && + controller.ControllerType != original ) + { + bestController = controller; + } + } + + return bestController; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/ODataApiVersionActionSelector.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/ODataApiVersionActionSelector.cs index af3ca2e5..3b6a5a57 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/ODataApiVersionActionSelector.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/ODataApiVersionActionSelector.cs @@ -1,17 +1,26 @@ namespace Microsoft.AspNetCore.Mvc.Versioning { + using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Routing; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; + using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; + using Microsoft.AspNetCore.Mvc.ModelBinding; + using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using Microsoft.OData.Edm; using System; using System.Collections.Generic; + using System.Diagnostics; using System.Linq; + using static System.Linq.Expressions.Expression; /// /// Represents the logic for selecting an API-versioned, action method with additional support for OData actions. @@ -19,6 +28,11 @@ [CLSCompliant( false )] public class ODataApiVersionActionSelector : ApiVersionActionSelector { + static readonly Func> getOptionalParameters = NewGetOptionalParametersFunc(); + readonly IModelBinderFactory modelBinderFactory; + readonly IModelMetadataProvider modelMetadataProvider; + readonly IOptions mvcOptions; + /// /// Initializes a new instance of the class. /// @@ -27,18 +41,32 @@ public class ODataApiVersionActionSelector : ApiVersionActionSelector /// The options associated with the action selector. /// The . /// The route policy applied to candidate matches. + /// The used to create model binders. + /// The used to resolve model metadata. + /// The MVC options associated with the action selector. public ODataApiVersionActionSelector( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, IEnumerable actionConstraintProviders, IOptions options, ILoggerFactory loggerFactory, - IApiVersionRoutePolicy routePolicy ) : base( actionDescriptorCollectionProvider, actionConstraintProviders, options, loggerFactory, routePolicy ) { } + IApiVersionRoutePolicy routePolicy, + IModelBinderFactory modelBinderFactory, + IModelMetadataProvider modelMetadataProvider, + IOptions mvcOptions ) + : base( actionDescriptorCollectionProvider, actionConstraintProviders, options, loggerFactory, routePolicy ) + { + this.modelBinderFactory = modelBinderFactory; + this.modelMetadataProvider = modelMetadataProvider; + this.mvcOptions = mvcOptions; + } /// - /// Selects a list of candidate actions from the specified route context. + /// Gets a value indicating whether endpoint routing is enabled. /// - /// The current route context to evaluate. - /// A read-only list of candidate actions. + /// True if endpoint routing is enabled; otherwise, false. + protected bool UsingEndpointRouting => mvcOptions.Value.EnableEndpointRouting; + + /// public override IReadOnlyList SelectCandidates( RouteContext context ) { if ( context == null ) @@ -46,7 +74,9 @@ public override IReadOnlyList SelectCandidates( RouteContext c throw new ArgumentNullException( nameof( context ) ); } - var odataPath = context.HttpContext.ODataFeature().Path; + var httpContext = context.HttpContext; + var feature = httpContext.ODataFeature(); + var odataPath = feature.Path; var routeValues = context.RouteData.Values; var notODataCandidate = odataPath == null || routeValues.ContainsKey( ODataRouteConstants.Action ); @@ -55,71 +85,53 @@ public override IReadOnlyList SelectCandidates( RouteContext c return base.SelectCandidates( context ); } - var routeData = context.RouteData; - var routingConventions = context.HttpContext.Request.GetRoutingConventions(); + var routingConventions = httpContext.Request.GetRoutingConventions(); if ( routingConventions == null ) { return base.SelectCandidates( context ); } - var visited = new HashSet(); - var possibleCandidates = new List(); + var bestCandidates = new List(); + var actionNames = new HashSet( StringComparer.OrdinalIgnoreCase ); + var container = httpContext.RequestServices.GetRequiredService(); + var routePrefix = container.GetRoutePrefix( feature.RouteName ); + var comparer = StringComparer.OrdinalIgnoreCase; foreach ( var convention in routingConventions ) { - var actions = convention.SelectAction( context ); - - if ( actions == null ) - { - continue; - } + var candidates = convention.SelectAction( context ); - foreach ( var action in actions ) + if ( candidates != null ) { - if ( visited.Add( action ) ) + foreach ( var candidate in candidates ) { - possibleCandidates.Add( new ActionCandidate( action ) ); + if ( !( candidate.AttributeRouteInfo is ODataAttributeRouteInfo info ) || + !comparer.Equals( routePrefix, info.RoutePrefix ) ) + { + continue; + } + + actionNames.Add( candidate.ActionName ); + bestCandidates.Add( candidate ); } } } - if ( possibleCandidates.Count == 0 ) + if ( bestCandidates.Count == 0 ) { return base.SelectCandidates( context ); } - var availableKeys = new HashSet( routeValues.Keys, StringComparer.OrdinalIgnoreCase ); - var bestCandidates = new List( possibleCandidates.Count ); - - availableKeys.Remove( ODataRouteConstants.ODataPath ); - - for ( var i = 0; i < possibleCandidates.Count; i++ ) + if ( !routeValues.ContainsKey( ODataRouteConstants.Action ) && actionNames.Count == 1 ) { - var possibleCandidate = possibleCandidates[i]; - - if ( availableKeys.Count == 0 ) - { - if ( possibleCandidate.FilteredParameters.Count == 0 ) - { - bestCandidates.Add( possibleCandidate.Action ); - } - } - else if ( possibleCandidate.FilteredParameters.All( availableKeys.Contains ) ) - { - bestCandidates.Add( possibleCandidate.Action ); - } + routeValues[ODataRouteConstants.Action] = actionNames.Single(); } return bestCandidates; } - /// - /// Selects the best action given the provided route context and list of candidate actions. - /// - /// The current route context to evaluate. - /// The read-only list of candidate actions to select from. - /// The best candidate action or null if no candidate matches. + /// public override ActionDescriptor? SelectBestCandidate( RouteContext context, IReadOnlyList candidates ) { if ( context == null ) @@ -133,7 +145,8 @@ public override IReadOnlyList SelectCandidates( RouteContext c } var httpContext = context.HttpContext; - var odataRouteCandidate = httpContext.ODataFeature().Path != null; + var odata = httpContext.ODataFeature(); + var odataRouteCandidate = odata.Path != null; if ( !odataRouteCandidate ) { @@ -148,26 +161,252 @@ public override IReadOnlyList SelectCandidates( RouteContext c var matches = EvaluateActionConstraints( context, candidates ); var selectionContext = new ActionSelectionContext( httpContext, matches, apiVersion ); var bestActions = SelectBestActions( selectionContext ); - var finalMatches = bestActions.Select( action => new ActionCandidate( action ) ) - .OrderByDescending( candidate => candidate.FilteredParameters.Count ) - .ThenByDescending( candidate => candidate.TotalParameterCount ) - .Take( 1 ) - .Select( candidate => candidate.Action ) - .ToArray(); + var finalMatches = Array.Empty(); + + if ( bestActions.Count > 0 ) + { + // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNetCore.OData/Routing/ODataActionSelector.cs + var routeValues = context.RouteData.Values; + var conventionsStore = odata.RoutingConventionsStore ?? new Dictionary( capacity: 0 ); + var availableKeys = new HashSet( routeValues.Keys.Where( IsAvailableKey ), StringComparer.OrdinalIgnoreCase ); + var availableKeysCount = conventionsStore.TryGetValue( ODataRouteConstants.KeyCount, out var v ) ? (int) v : 0; + var possibleCandidates = bestActions.Select( candidate => new ActionIdAndParameters( candidate, ParameterHasRegisteredModelBinder ) ); + var optionalParameters = routeValues.TryGetValue( ODataRouteConstants.OptionalParameters, out var wrapper ) ? + GetOptionalParameters( wrapper ) : + Array.Empty(); + var matchedCandidates = possibleCandidates + .Where( c => TryMatch( httpContext, c, availableKeys, conventionsStore, optionalParameters, availableKeysCount ) ) + .OrderByDescending( c => c.FilteredParameters.Count ) + .ThenByDescending( c => c.TotalParameterCount ) + .ToArray(); + + if ( matchedCandidates.Length == 1 ) + { + finalMatches = new[] { matchedCandidates[0].Action }; + } + else if ( matchedCandidates.Length > 1 ) + { + var results = matchedCandidates.Where( c => ActionAcceptsMethod( c.Action, httpContext.Request.Method ) ).ToArray(); + + finalMatches = results.Length switch + { + 0 => matchedCandidates.Where( c => c.FilteredParameters.Count == availableKeysCount ).Select( c => c.Action ).ToArray(), + 1 => new[] { results[0].Action }, + _ => results.Where( c => c.FilteredParameters.Count == availableKeysCount ).Select( c => c.Action ).ToArray(), + }; + } + } + var feature = httpContext.Features.Get(); var selectionResult = feature.SelectionResult; feature.RequestedApiVersion = selectionContext.RequestedVersion; selectionResult.AddCandidates( candidates ); - if ( finalMatches.Length == 0 ) + if ( finalMatches.Length > 0 ) { - return null; + selectionResult.AddMatches( finalMatches ); + } + else + { + // OData endpoint routing calls back through IActionSelector. if endpoint routing is enabled + // then the answer is final; proceed to route policy. if classic routing, it's possible the + // IActionSelector will be entered again + if ( !UsingEndpointRouting ) + { + return null; + } } - - selectionResult.AddMatches( finalMatches ); return RoutePolicy.Evaluate( context, selectionResult ); } + + static bool IsRouteParameter( string key ) + { + if ( string.IsNullOrEmpty( key ) ) + { + return false; + } + + return key[0] == '{' && key.Length > 1 && key[^1] == '}'; + } + + static bool IsAvailableKey( string key ) => !IsRouteParameter( key ) && key != ODataRouteConstants.Action && key != ODataRouteConstants.ODataPath; + + static bool RequestHasBody( HttpContext context ) + { + var method = context.Request.Method.ToUpperInvariant(); + + switch ( method ) + { + case "POST": + case "PUT": + case "PATCH": + case "MERGE": + return true; + } + + return false; + } + + static bool ActionAcceptsMethod( ActionDescriptor action, string method ) => + action.GetHttpMethods().Contains( method, StringComparer.OrdinalIgnoreCase ); + + static Func> NewGetOptionalParametersFunc() + { + var type = Type.GetType( "Microsoft.AspNet.OData.Routing.ODataOptionalParameter, Microsoft.AspNetCore.OData", throwOnError: true, ignoreCase: false ); + var p = Parameter( typeof( object ) ); + var body = Property( Convert( p, type ), "OptionalParameters" ); + var lambda = Lambda>>( body, p ); + + return lambda.Compile(); + } + + static IReadOnlyList GetOptionalParameters( object value ) + { + if ( value is null ) + { + return Array.Empty(); + } + + return getOptionalParameters( value ); + } + + bool TryMatch( + HttpContext context, + ActionIdAndParameters action, + ISet availableKeys, + IDictionary conventionsStore, + IReadOnlyList optionalParameters, + int availableKeysCount ) + { + var parameters = action.FilteredParameters; + var totalParameterCount = action.TotalParameterCount; + + if ( availableKeys.Contains( ODataRouteConstants.NavigationProperty ) ) + { + availableKeysCount -= 1; + } + + if ( totalParameterCount < availableKeysCount ) + { + return false; + } + + var matchedBody = false; + var keys = conventionsStore.Keys.ToArray(); + + for ( var i = 0; i < parameters.Count; i++ ) + { + var parameter = parameters[i]; + var parameterName = parameter.Name; + + if ( availableKeys.Contains( parameterName ) ) + { + continue; + } + + var matchesKey = false; + + for ( var j = 0; j < keys.Length; j++ ) + { + if ( keys[j].Contains( parameterName, StringComparison.Ordinal ) ) + { + matchesKey = true; + break; + } + } + + if ( matchesKey ) + { + continue; + } + + if ( context.Request.Query.ContainsKey( parameterName ) ) + { + continue; + } + + if ( parameter is ControllerParameterDescriptor param && optionalParameters.Count > 0 ) + { + if ( param.ParameterInfo.IsOptional && optionalParameters.Any( p => string.Equals( p.Name, parameterName, StringComparison.OrdinalIgnoreCase ) ) ) + { + continue; + } + } + + if ( ParameterHasRegisteredModelBinder( parameter ) ) + { + continue; + } + + if ( !matchedBody && RequestHasBody( context ) ) + { + matchedBody = true; + continue; + } + + return false; + } + + return true; + } + + bool ParameterHasRegisteredModelBinder( ParameterDescriptor parameter ) + { + var modelMetadata = modelMetadataProvider.GetMetadataForType( parameter.ParameterType ); + var binderContext = new ModelBinderFactoryContext() + { + Metadata = modelMetadata, + BindingInfo = parameter.BindingInfo, + CacheToken = modelMetadata, + }; + IModelBinder? binder; + + try + { + binder = modelBinderFactory.CreateBinder( binderContext ); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 + { + binder = default; + } + + return !( binder is SimpleTypeModelBinder ) && + !( binder is BodyModelBinder ) && + !( binder is ComplexTypeModelBinder ) && + !( binder is BinderTypeModelBinder ); + } + + [DebuggerDisplay( "{Action.DisplayName,nq}" )] + sealed class ActionIdAndParameters + { + internal ActionIdAndParameters( ActionDescriptor action, Func modelBound ) + { + Action = action; + + var filteredParameters = new List(); + + foreach ( var parameter in action.Parameters ) + { + TotalParameterCount += 1; + + if ( !parameter.ParameterType.IsModelBound() && !modelBound( parameter ) ) + { + filteredParameters.Add( parameter ); + } + } + + FilteredParameters = filteredParameters.ToArray(); + } + + public int TotalParameterCount { get; } + + public IReadOnlyList FilteredParameters { get; } + + public ActionDescriptor Action { get; } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/ODataVersioningFeature.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/ODataVersioningFeature.cs deleted file mode 100644 index 732bb075..00000000 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNetCore.Mvc/Versioning/ODataVersioningFeature.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.AspNetCore.Mvc.Versioning -{ - using System; - using System.Collections.Generic; - - sealed class ODataVersioningFeature : IODataVersioningFeature - { - public IDictionary MatchingRoutes { get; } = new Dictionary(); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/IODataBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/IODataBuilderExtensions.cs index 031836ca..fd93259b 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/IODataBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/IODataBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Interfaces; using Microsoft.AspNet.OData.Routing; + using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -18,7 +19,7 @@ /// Provides extension methods for the interface. /// [CLSCompliant( false )] - public static class IODataBuilderExtensions + public static partial class IODataBuilderExtensions { /// /// Enables service API versioning for the specified OData configuration. @@ -77,6 +78,7 @@ static void AddODataServices( IServiceCollection services ) services.AddTransient(); services.AddSingleton( ODataActionDescriptorChangeProvider.Instance ); services.TryAddEnumerable( Transient() ); + services.AddTransient(); services.AddModelConfigurationsAsServices( partManager ); } diff --git a/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/RaiseVersionedODataRoutesMapped.cs b/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/RaiseVersionedODataRoutesMapped.cs new file mode 100644 index 00000000..18a9df5e --- /dev/null +++ b/src/Microsoft.AspNetCore.OData.Versioning/Extensions.DependencyInjection/RaiseVersionedODataRoutesMapped.cs @@ -0,0 +1,28 @@ +#pragma warning disable CA1812 + +namespace Microsoft.Extensions.DependencyInjection +{ + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc; + using System; + + sealed class RaiseVersionedODataRoutesMapped : IStartupFilter + { + public Action Configure( Action next ) + { + return app => + { + // execute the next configuration, which should make a call to MapVersionedODataRoute. using a IStartupFilter + // this way will reduce the number of times we need to re-evaluate the OData action descriptions to just once + next( app ); + + // note: we don't have the required information necessary to build the odata route information + // until one or more routes have been mapped. if anyone has subscribed changes, notify them now. + // this might do unnecessary work, but we assume that if you're using api versioning and odata, then + // at least one call to MapVersionedODataRoute or some other means added a route to the IODataRouteCollection + ODataActionDescriptorChangeProvider.Instance.NotifyChanged(); + }; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/Microsoft.AspNetCore.OData.Versioning.csproj b/src/Microsoft.AspNetCore.OData.Versioning/Microsoft.AspNetCore.OData.Versioning.csproj index 64dc644f..6c76c8c2 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/Microsoft.AspNetCore.OData.Versioning.csproj +++ b/src/Microsoft.AspNetCore.OData.Versioning/Microsoft.AspNetCore.OData.Versioning.csproj @@ -1,8 +1,8 @@  - 4.1.1 - 4.1.0.0 + 5.0.0 + 5.0.0.0 netcoreapp3.1 Microsoft ASP.NET Core API Versioning for OData v4.0 A service API versioning library for Microsoft ASP.NET Core and OData v4.0. @@ -20,7 +20,7 @@ - + diff --git a/src/Microsoft.AspNetCore.OData.Versioning/OData/IContainerBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData.Versioning/OData/IContainerBuilderExtensions.cs new file mode 100644 index 00000000..9b28436a --- /dev/null +++ b/src/Microsoft.AspNetCore.OData.Versioning/OData/IContainerBuilderExtensions.cs @@ -0,0 +1,67 @@ +namespace Microsoft.OData +{ + using Microsoft.AspNet.OData.Extensions; + using Microsoft.AspNet.OData.Routing.Conventions; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Microsoft.OData.Edm; + using System; + using System.Collections.Generic; + using System.Linq; + using static Microsoft.AspNet.OData.Routing.VersionedODataRoutingConventions; + using static Microsoft.OData.ServiceLifetime; + + /// + /// Provides extension methods for the interface. + /// + public static class IContainerBuilderExtensions + { + /// + /// Adds service API versioning to the specified container builder. + /// + /// The extended container builder. + /// The name of the route to add API versioning to. + /// The sequence of EDM models to use for parsing OData paths. + /// The associated service provider. + /// The original . + public static IContainerBuilder AddApiVersioning( this IContainerBuilder builder, string routeName, IEnumerable models, IServiceProvider serviceProvider ) => + builder + .AddService( Transient, sp => sp.GetRequiredService().SelectModel( sp ) ) + .AddService( + Singleton, + child => child.WithParent( + serviceProvider, + sp => (IEdmModelSelector) new EdmModelSelector( + models, + sp.GetRequiredService>().Value.DefaultApiVersion ) ) ) + .AddService( + Singleton, + child => child.WithParent( serviceProvider, sp => CreateDefaultWithAttributeRouting( routeName, sp ).AsEnumerable() ) ); + + /// + /// Adds service API versioning to the specified container builder. + /// + /// The extended container builder. + /// The sequence of EDM models to use for parsing OData paths. + /// The OData routing conventions to use for controller and action selection. + /// The associated service provider. + /// The original . + [CLSCompliant( false )] + public static IContainerBuilder AddApiVersioning( + this IContainerBuilder builder, + IEnumerable models, + IEnumerable routingConventions, + IServiceProvider serviceProvider ) => + builder + .AddService( Transient, sp => sp.GetRequiredService().SelectModel( sp ) ) + .AddService( + Singleton, + child => child.WithParent( + serviceProvider, + sp => (IEdmModelSelector) new EdmModelSelector( + models, + sp.GetRequiredService>().Value.DefaultApiVersion ) ) ) + .AddService( Singleton, child => child.WithParent( serviceProvider, sp => AddOrUpdate( routingConventions.ToList() ).AsEnumerable() ) ); + } +} \ No newline at end of file diff --git a/test/Acceptance.Test.Shared/AcceptanceTest.cs b/test/Acceptance.Test.Shared/AcceptanceTest.cs index 4a9f70a9..b69d1a3f 100644 --- a/test/Acceptance.Test.Shared/AcceptanceTest.cs +++ b/test/Acceptance.Test.Shared/AcceptanceTest.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc #if WEBAPI [Trait( "Framework", "Web API" )] #else - [Trait( "Framework", "ASP.NET Core" )] + [Trait( "Framework", "Core" )] #endif public abstract partial class AcceptanceTest { diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Description/TestConfigurations.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Description/TestConfigurations.cs index 6afa96de..211d2247 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Description/TestConfigurations.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Description/TestConfigurations.cs @@ -47,7 +47,7 @@ public static HttpConfiguration NewOrdersConfiguration() }; var models = builder.GetEdmModels(); - configuration.MapVersionedODataRoutes( "odata", "api", models ); + configuration.MapVersionedODataRoute( "odata", "api", models ); return configuration; } @@ -69,7 +69,7 @@ public static HttpConfiguration NewPeopleConfiguration() }; var models = builder.GetEdmModels(); - configuration.MapVersionedODataRoutes( "odata", "api/v{apiVersion}", models ); + configuration.MapVersionedODataRoute( "odata", "api/v{apiVersion}", models ); return configuration; } @@ -86,7 +86,7 @@ public static HttpConfiguration NewProductAndSupplierConfiguration() var builder = new VersionedODataModelBuilder( configuration ) { - DefaultModelConfiguration = ( b, v ) => + DefaultModelConfiguration = ( b, v, r ) => { b.EntitySet( "Products" ).EntityType.HasKey( p => p.Id ); b.EntitySet( "Suppliers" ).EntityType.HasKey( s => s.Id ); @@ -94,7 +94,7 @@ public static HttpConfiguration NewProductAndSupplierConfiguration() }; var models = builder.GetEdmModels(); - configuration.MapVersionedODataRoutes( "odata", "api", models ); + configuration.MapVersionedODataRoute( "odata", "api", models ); return configuration; } diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/Configuration/OrderModelConfiguration.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/Configuration/OrderModelConfiguration.cs index 5301cca8..48b17a30 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/Configuration/OrderModelConfiguration.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/Configuration/OrderModelConfiguration.cs @@ -6,6 +6,6 @@ public class OrderModelConfiguration : IModelConfiguration { - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) => builder.EntitySet( "Orders" ); + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) => builder.EntitySet( "Orders" ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/Configuration/PersonModelConfiguration.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/Configuration/PersonModelConfiguration.cs index 7fed0c86..12816bdf 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/Configuration/PersonModelConfiguration.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/Configuration/PersonModelConfiguration.cs @@ -6,6 +6,6 @@ public class PersonModelConfiguration : IModelConfiguration { - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) => builder.EntitySet( "People" ); + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) => builder.EntitySet( "People" ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.OData.Versioning.Tests/Builder/VersionedODataModelBuilderTest.cs b/test/Microsoft.AspNet.OData.Versioning.Tests/Builder/VersionedODataModelBuilderTest.cs index cc770f60..0de21df2 100644 --- a/test/Microsoft.AspNet.OData.Versioning.Tests/Builder/VersionedODataModelBuilderTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.Tests/Builder/VersionedODataModelBuilderTest.cs @@ -3,6 +3,8 @@ using FluentAssertions; using Microsoft.OData.Edm; using Microsoft.Web.Http; + using Microsoft.Web.Http.Dispatcher; + using Microsoft.Web.Http.Versioning; using Moq; using System; using System.Collections.Generic; @@ -13,26 +15,22 @@ public class VersionedODataModelBuilderTest { - [ApiVersion( "1.0" )] - sealed class ControllerV1 : ODataController { } - [Fact] public void get_edm_models_should_return_expected_results() { // arrange var configuration = new HttpConfiguration(); var controllerTypeResolver = new Mock(); - var controllerTypes = new List() { typeof( ControllerV1 ) }; + var controllerTypes = new List() { typeof( TestController ) }; controllerTypeResolver.Setup( ctr => ctr.GetControllerTypes( It.IsAny() ) ).Returns( controllerTypes ); configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver.Object ); - var defaultConfiguration = new Mock>(); var modelCreated = new Mock>(); var apiVersion = new ApiVersion( 1, 0 ); var builder = new VersionedODataModelBuilder( configuration ) { - DefaultModelConfiguration = defaultConfiguration.Object, + DefaultModelConfiguration = ( builder, version, prefix ) => builder.EntitySet( "Tests" ), OnModelCreated = modelCreated.Object }; @@ -41,8 +39,53 @@ public void get_edm_models_should_return_expected_results() // assert model.GetAnnotationValue( model ).ApiVersion.Should().Be( apiVersion ); - defaultConfiguration.Verify( f => f( It.IsAny(), apiVersion ), Times.Once() ); modelCreated.Verify( f => f( It.IsAny(), model ), Times.Once() ); } + + [Fact] + public void get_edm_models_should_split_models_between_routes() + { + // arrange + var configuration = new HttpConfiguration(); + var controllerTypeResolver = new Mock(); + var controllerTypes = new List() { typeof( TestController ), typeof( OtherTestController ) }; + var options = new ApiVersioningOptions(); + + controllerTypeResolver.Setup( ctr => ctr.GetControllerTypes( It.IsAny() ) ).Returns( controllerTypes ); + configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver.Object ); + configuration.Services.Replace( typeof( IHttpControllerSelector ), new ApiVersionControllerSelector( configuration, options ) ); + + var defaultConfiguration = new Mock>(); + var modelCreated = new Mock>(); + var builder = new VersionedODataModelBuilder( configuration ) + { + DefaultModelConfiguration = ( builder, version, prefix ) => + { + if ( prefix == "api2" ) + { + builder.EntitySet( "Tests" ); + } + }, + }; + + // act + var models = builder.GetEdmModels( "api2" ); + + // assert + models.Should().HaveCount( 2 ); + models.ElementAt( 1 ).FindDeclaredEntitySet( "Tests" ).Should().NotBeNull(); + } + } + + [ApiVersion( "1.0" )] + public sealed class TestController : ODataController + { + public IHttpActionResult Get() => Ok(); + } + + [ApiVersion( "2.0" )] + public sealed class OtherTestController : ODataController + { + public IHttpActionResult Get() => Ok(); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.OData.Versioning.Tests/HttpConfigurationExtensionsTest.cs b/test/Microsoft.AspNet.OData.Versioning.Tests/HttpConfigurationExtensionsTest.cs index c6591d63..031af10d 100644 --- a/test/Microsoft.AspNet.OData.Versioning.Tests/HttpConfigurationExtensionsTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.Tests/HttpConfigurationExtensionsTest.cs @@ -1,11 +1,11 @@ namespace Microsoft.AspNet.OData { using FluentAssertions; - using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Batch; using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Routing; using Microsoft.AspNet.OData.Routing.Conventions; + using Microsoft.Extensions.DependencyInjection; using Microsoft.OData.Edm; using Microsoft.Web.Http; using Moq; @@ -20,93 +20,43 @@ public class HttpConfigurationExtensionsTest { - [Fact] - public void map_versioned_odata_route_should_return_expected_result() + [Theory] + [InlineData( null )] + [InlineData( "api" )] + [InlineData( "v{apiVersion}" )] + public void map_versioned_odata_route_should_return_expected_result( string routePrefix ) { // arrange var configuration = new HttpConfiguration(); var httpServer = new HttpServer( configuration ); var routeName = "odata"; - var routePrefix = "api/v3"; - var model = new ODataModelBuilder().GetEdmModel(); - var apiVersion = new ApiVersion( 3, 0 ); + var batchTemplate = "$batch"; var batchHandler = new DefaultODataBatchHandler( httpServer ); + var models = CreateModels( configuration ); + + if ( !string.IsNullOrEmpty( routePrefix ) ) + { + batchTemplate = routePrefix + "/" + batchTemplate; + } // act - var route = configuration.MapVersionedODataRoute( routeName, routePrefix, model, apiVersion, batchHandler ); - var constraint = route.PathRouteConstraint; - var routingConventions = GetRoutingConventions( configuration, route ); + var route = configuration.MapVersionedODataRoute( routeName, routePrefix, models, batchHandler ); var batchRoute = configuration.Routes["odataBatch"]; // assert + var selector = GetODataRootContainer( configuration, routeName ).GetRequiredService(); + var routingConventions = GetRoutingConventions( configuration, route ); + + selector.ApiVersions.Should().Equal( new[] { new ApiVersion( 1, 0 ), new ApiVersion( 2, 0 ) } ); routingConventions[0].Should().BeOfType(); routingConventions[1].Should().BeOfType(); routingConventions.OfType().Should().BeEmpty(); - constraint.RouteName.Should().Be( routeName ); + route.PathRouteConstraint.RouteName.Should().Be( routeName ); route.RoutePrefix.Should().Be( routePrefix ); batchRoute.Handler.Should().Be( batchHandler ); - batchRoute.RouteTemplate.Should().Be( "api/v3/$batch" ); - } - - [Fact] - public void map_versioned_odata_routes_should_return_expected_results() - { - // arrange - var configuration = new HttpConfiguration(); - var httpServer = new HttpServer( configuration ); - var routeName = "odata"; - var routePrefix = "api"; - var batchHandler = new DefaultODataBatchHandler( httpServer ); - var models = CreateModels( configuration ); - - // act - var routes = configuration.MapVersionedODataRoutes( routeName, routePrefix, models, batchHandler ); - var batchRoute = configuration.Routes["odataBatch"]; - - // assert - foreach ( var route in routes ) - { - if ( !( route.PathRouteConstraint is VersionedODataPathRouteConstraint constraint ) ) - { - continue; - } - - var apiVersion = constraint.ApiVersion; - var routingConventions = GetRoutingConventions( configuration, route ); - var versionedRouteName = routeName + "-" + apiVersion.ToString(); - - routingConventions[0].Should().BeOfType(); - routingConventions[1].Should().BeOfType(); - routingConventions.OfType().Should().BeEmpty(); - constraint.RouteName.Should().Be( versionedRouteName ); - route.RoutePrefix.Should().Be( routePrefix ); - } - - batchRoute.Handler.Should().Be( batchHandler ); - batchRoute.RouteTemplate.Should().Be( "api/$batch" ); - } - - [Theory] - [InlineData( 0, "1.0" )] - [InlineData( 1, "2.0" )] - public void get_edm_model_should_retrieve_configured_model_by_api_version( int modelIndex, string apiVersionValue ) - { - // arrange - var apiVersion = ApiVersion.Parse( apiVersionValue ); - var configuration = new HttpConfiguration(); - var models = CreateModels( configuration ).ToArray(); - - configuration.MapVersionedODataRoutes( "odata", "api", models ); - - // act - var model = configuration.GetEdmModel( apiVersion ); - - // assert - model.Should().BeSameAs( models[modelIndex] ); + batchRoute.RouteTemplate.Should().Be( batchTemplate ); } - const string RootContainerMappingsKey = "Microsoft.AspNet.OData.RootContainerMappingsKey"; - static IEnumerable CreateModels( HttpConfiguration configuration ) { var controllerTypeResolver = new Mock(); @@ -116,11 +66,21 @@ static IEnumerable CreateModels( HttpConfiguration configuration ) configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver.Object ); configuration.AddApiVersioning(); - var builder = new VersionedODataModelBuilder( configuration ); + var builder = new VersionedODataModelBuilder( configuration ) + { + DefaultModelConfiguration = ( b, v, r ) => b.EntitySet( "Tests" ), + }; return builder.GetEdmModels(); } + static IServiceProvider GetODataRootContainer( HttpConfiguration configuration, string routeName ) + { + const string RootContainerMappingsKey = "Microsoft.AspNet.OData.RootContainerMappingsKey"; + var serviceProviders = (ConcurrentDictionary) configuration.Properties[RootContainerMappingsKey]; + return serviceProviders[routeName]; + } + static IReadOnlyList GetRoutingConventions( HttpConfiguration configuration, ODataRoute route ) { var routes = configuration.Routes; @@ -129,8 +89,8 @@ static IReadOnlyList GetRoutingConventions( HttpConfigu routes.CopyTo( pairs, 0 ); var key = pairs.Single( p => p.Value == route ).Key; - var serviceProviders = (ConcurrentDictionary) configuration.Properties[RootContainerMappingsKey]; - var routingConventions = (IEnumerable) serviceProviders[key].GetService( typeof( IEnumerable ) ); + var serviceProvider = GetODataRootContainer( configuration, key ); + var routingConventions = (IEnumerable) serviceProvider.GetService( typeof( IEnumerable ) ); return routingConventions.ToArray(); } diff --git a/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedAttributeRoutingConventionTest.cs b/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedAttributeRoutingConventionTest.cs index 90dda2d2..417362d7 100644 --- a/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedAttributeRoutingConventionTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedAttributeRoutingConventionTest.cs @@ -10,24 +10,18 @@ public class VersionedAttributeRoutingConventionTest { - [ApiVersionNeutral] - sealed class NeutralController : ODataController { } - - [ApiVersion( "1.0" )] - sealed class ControllerV1 : ODataController { } - [Fact] public void should_map_controller_should_return_true_for_versionX2Dneutral_controller() { // arrange var configuration = new HttpConfiguration(); var controller = new HttpControllerDescriptor( configuration, string.Empty, typeof( NeutralController ) ); - var convention = new VersionedAttributeRoutingConvention( "Tests", configuration, new ApiVersion( 1, 0 ) ); + var convention = new VersionedAttributeRoutingConvention( "Tests", configuration ); controller.Properties[typeof( ApiVersionModel )] = ApiVersionModel.Neutral; // act - var result = convention.ShouldMapController( controller ); + var result = convention.ShouldMapController( controller, new ApiVersion( 1, 0 ) ); // assert result.Should().BeTrue(); @@ -41,15 +35,21 @@ public void should_map_controller_should_return_expected_result_for_controller_v // arrange var configuration = new HttpConfiguration(); var controller = new HttpControllerDescriptor( configuration, string.Empty, typeof( ControllerV1 ) ); - var convention = new VersionedAttributeRoutingConvention( "Tests", configuration, new ApiVersion( majorVersion, 0 ) ); + var convention = new VersionedAttributeRoutingConvention( "Tests", configuration ); - controller.Properties[typeof( ApiVersionModel )] = new ApiVersionModel( new ApiVersion( 1, 0 ) ); + controller.Properties[typeof( ApiVersionModel )] = new ApiVersionModel( new ApiVersion( majorVersion, 0 ) ); // act - var result = convention.ShouldMapController( controller ); + var result = convention.ShouldMapController( controller, new ApiVersion( majorVersion, 0 ) ); // assert result.Should().BeTrue(); } + + [ApiVersionNeutral] + sealed class NeutralController : ODataController { } + + [ApiVersion( "1.0" )] + sealed class ControllerV1 : ODataController { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedMetadataRoutingConventionTest.cs b/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedMetadataRoutingConventionTest.cs index b03e07b5..a5d5905c 100644 --- a/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedMetadataRoutingConventionTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedMetadataRoutingConventionTest.cs @@ -4,6 +4,7 @@ using Microsoft.AspNet.OData; using Microsoft.OData; using Microsoft.OData.Edm; + using Microsoft.Web.Http; using Moq; using System; using System.Collections.Generic; @@ -15,6 +16,42 @@ public class VersionedMetadataRoutingConventionTest { + [Theory] + [MemberData( nameof( SelectControllerData ) )] + public void select_controller_should_return_expected_name( string requestUrl, string expected ) + { + // arrange + var odataPath = ParseUrl( requestUrl ); + var request = new HttpRequestMessage(); + var routingConvention = new VersionedMetadataRoutingConvention(); + + SetRequestContainer( request ); + + // act + var controllerName = routingConvention.SelectController( odataPath, request ); + + // assert + controllerName.Should().Be( expected ); + } + + [Theory] + [MemberData( nameof( SelectActionData ) )] + public void select_action_should_return_expected_name( string requestUrl, string verb, string expected ) + { + // arrange + var odataPath = ParseUrl( requestUrl ); + var request = new HttpRequestMessage( new HttpMethod( verb ), "http://localhost/$metadata" ); + var controllerContext = new HttpControllerContext() { Request = request }; + var actionMap = new Mock>().Object; + var routingConvention = new VersionedMetadataRoutingConvention(); + + // act + var actionName = routingConvention.SelectAction( odataPath, controllerContext, actionMap ); + + // assert + actionName.Should().Be( expected ); + } + readonly IODataPathHandler pathHandler = new DefaultODataPathHandler(); readonly IServiceProvider serviceProvider; @@ -29,6 +66,17 @@ public VersionedMetadataRoutingConventionTest() ODataPath ParseUrl( string odataPath ) => pathHandler.Parse( "http://localhost", odataPath, serviceProvider ); + static void SetRequestContainer(HttpRequestMessage request) + { + const string RequestContainerKey = "Microsoft.AspNet.OData.RequestContainer"; + var selector = new Mock(); + var serviceProvider = new Mock(); + + selector.SetupGet( s => s.ApiVersions ).Returns( new[] { ApiVersion.Default } ); + serviceProvider.Setup( sp => sp.GetService( typeof( IEdmModelSelector ) ) ).Returns( selector.Object ); + request.Properties[RequestContainerKey] = serviceProvider.Object; + } + public static IEnumerable SelectControllerData { get @@ -36,7 +84,7 @@ public static IEnumerable SelectControllerData yield return new object[] { "", "VersionedMetadata" }; yield return new object[] { "$metadata", "VersionedMetadata" }; yield return new object[] { "Tests", null }; - yield return new object[] { "Tests(42)", null }; + yield return new object[] { "Tests/42", null }; } } @@ -48,42 +96,8 @@ public static IEnumerable SelectActionData yield return new object[] { "$metadata", "GET", "GetMetadata" }; yield return new object[] { "$metadata", "OPTIONS", "GetOptions" }; yield return new object[] { "Tests", "GET", null }; - yield return new object[] { "Tests(42)", "GET", null }; + yield return new object[] { "Tests/42", "GET", null }; } } - - [Theory] - [MemberData( nameof( SelectControllerData ) )] - public void select_controller_should_return_expected_name( string requestUrl, string expected ) - { - // arrange - var odataPath = ParseUrl( requestUrl ); - var request = new HttpRequestMessage(); - var routingConvention = new VersionedMetadataRoutingConvention(); - - // act - var controllerName = routingConvention.SelectController( odataPath, request ); - - // assert - controllerName.Should().Be( expected ); - } - - [Theory] - [MemberData( nameof( SelectActionData ) )] - public void select_action_should_return_expected_name( string requestUrl, string verb, string expected ) - { - // arrange - var odataPath = ParseUrl( requestUrl ); - var request = new HttpRequestMessage( new HttpMethod( verb ), "http://localhost/$metadata" ); - var controllerContext = new HttpControllerContext() { Request = request }; - var actionMap = new Mock>().Object; - var routingConvention = new VersionedMetadataRoutingConvention(); - - // act - var actionName = routingConvention.SelectAction( odataPath, controllerContext, actionMap ); - - // assert - actionName.Should().Be( expected ); - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedODataPathRouteConstraintTest.cs b/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedODataPathRouteConstraintTest.cs deleted file mode 100644 index 57b14e32..00000000 --- a/test/Microsoft.AspNet.OData.Versioning.Tests/Routing/VersionedODataPathRouteConstraintTest.cs +++ /dev/null @@ -1,136 +0,0 @@ -namespace Microsoft.AspNet.OData.Routing -{ - using FluentAssertions; - using Microsoft.AspNet.OData.Routing.Conventions; - using Microsoft.OData.Edm; - using Microsoft.Web.Http; - using Microsoft.Web.Http.Versioning; - using Moq; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; - using System.Web.Http; - using System.Web.Http.Routing; - using Xunit; - using static Microsoft.Web.Http.ApiVersion; - using static System.Net.Http.HttpMethod; - using static System.Net.HttpStatusCode; - using static System.Web.Http.Routing.HttpRouteDirection; - - public class VersionedODataPathRouteConstraintTest - { - [Fact] - public void match_should_always_return_true_for_uri_resolution() - { - // arrange - var request = new HttpRequestMessage(); - var route = new Mock().Object; - var parameterName = (string) null; - var values = new Dictionary(); - var routeDirection = UriGeneration; - var constraint = new VersionedODataPathRouteConstraint( "odata", Default ); - - // act - var result = constraint.Match( request, route, parameterName, values, routeDirection ); - - // assert - result.Should().BeTrue(); - } - - [Theory] - [InlineData( "2.0" )] - [InlineData( "3.0" )] - public void match_should_be_true_when_api_version_is_requested_in_query_string( string apiVersion ) - { - // arrange - var request = new HttpRequestMessage( Get, $"http://localhost/Tests(1)?api-version={apiVersion}" ); - var values = new Dictionary() { ["odataPath"] = "Tests(1)" }; - var constraint = NewVersionedODataPathRouteConstraint( request, Test.Model, Parse( apiVersion ) ); - - // act - var result = constraint.Match( request, null, null, values, UriResolution ); - - // assert - result.Should().BeTrue(); - } - - [Theory] - [InlineData( "http://localhost?api-version=2.0", null )] - [InlineData( "http://localhost/$metadata?api-version=2.0", "$metadata" )] - public void match_should_return_expected_result_for_service_and_metadata_document( string requestUri, string odataPath ) - { - // arrange - var apiVersion = new ApiVersion( 2, 0 ); - var request = new HttpRequestMessage( Get, requestUri ); - var values = new Dictionary() { [nameof( odataPath )] = odataPath }; - var constraint = NewVersionedODataPathRouteConstraint( request, Test.EmptyModel, apiVersion ); - - // act - var result = constraint.Match( request, null, null, values, UriResolution ); - - // assert - result.Should().BeTrue(); - } - - [Theory] - [InlineData( "http://localhost/", null )] - [InlineData( "http://localhost/$metadata", "$metadata" )] - [InlineData( "http://localhost/Tests(1)", "Tests(1)" )] - public void match_should_return_expected_result_when_controller_is_implicitly_versioned( string requestUri, string odataPath ) - { - // arrange - var apiVersion = new ApiVersion( 2, 0 ); - var request = new HttpRequestMessage( Get, requestUri ); - var values = new Dictionary() { [nameof( odataPath )] = odataPath }; - var constraint = NewVersionedODataPathRouteConstraint( - request, - Test.Model, - apiVersion, - options => options.AssumeDefaultVersionWhenUnspecified = true ); - - // act - var result = constraint.Match( request, null, null, values, UriResolution ); - - // assert - result.Should().BeFalse(); - request.ODataApiVersionProperties().MatchingRoutes.Should().Equal( new Dictionary() { [apiVersion] = "odata" } ); - } - - [Fact] - public void match_should_return_400_when_requested_api_version_is_ambiguous() - { - // arrange - var request = new HttpRequestMessage( Get, $"http://localhost/Tests(1)?api-version=1.0&api-version=2.0" ); - var values = new Dictionary() { ["odataPath"] = "Tests(1)" }; - var constraint = NewVersionedODataPathRouteConstraint( request, Test.Model, new ApiVersion( 1, 0 ) ); - - // act - Action match = () => constraint.Match( request, null, null, values, UriResolution ); - - // assert - match.Should().Throw().And.Response.StatusCode.Should().Be( BadRequest ); - } - - static VersionedODataPathRouteConstraint NewVersionedODataPathRouteConstraint( - HttpRequestMessage request, - IEdmModel model, - ApiVersion apiVersion, - Action configure = default, - string routePrefix = default ) - { - var pathHandler = new DefaultODataPathHandler(); - var conventions = ODataRoutingConventions.CreateDefault(); - var configuration = new HttpConfiguration(); - var routingConventions = Enumerable.Empty(); - var constraint = new VersionedODataPathRouteConstraint( "odata", apiVersion ); - - configuration.AddApiVersioning( configure ?? new Action( _ => { } ) ); - configuration.MapVersionedODataRoute( "odata", routePrefix, model, apiVersion ); - request.SetConfiguration( configuration ); - configuration.EnsureInitialized(); - - return constraint; - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.OData.Versioning.Tests/VersionedMetadataControllerTest.cs b/test/Microsoft.AspNet.OData.Versioning.Tests/VersionedMetadataControllerTest.cs index 5432e5c6..cf938b2a 100644 --- a/test/Microsoft.AspNet.OData.Versioning.Tests/VersionedMetadataControllerTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.Tests/VersionedMetadataControllerTest.cs @@ -19,8 +19,11 @@ public class VersionedMetadataControllerTest public async Task options_should_return_expected_headers() { // arrange - var configuration = new HttpConfiguration(); - var builder = new VersionedODataModelBuilder( configuration ); + var configuration = new HttpConfiguration() { IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always }; + var builder = new VersionedODataModelBuilder( configuration ) + { + DefaultModelConfiguration = (b, v, r) => b.EntitySet( "Tests" ), + }; var metadata = new VersionedMetadataController() { Configuration = configuration }; var controllerTypeResolver = new Mock(); var controllerTypes = new List() { typeof( Controller1 ), typeof( Controller2 ), typeof( VersionedMetadataController ) }; @@ -31,18 +34,17 @@ public async Task options_should_return_expected_headers() var models = builder.GetEdmModels(); var request = new HttpRequestMessage( new HttpMethod( "OPTIONS" ), "http://localhost/$metadata" ); - var response = default( HttpResponseMessage ); - configuration.MapVersionedODataRoutes( "odata", null, models ); + configuration.MapVersionedODataRoute( "odata", null, models ); - using ( var server = new HttpServer( configuration ) ) - using ( var client = new HttpClient( server ) ) - { - // act - response = ( await client.SendAsync( request ) ).EnsureSuccessStatusCode(); - } + using var server = new HttpServer( configuration ); + using var client = new HttpClient( server ); + + // act + var response = await client.SendAsync( request ); // assert + response.EnsureSuccessStatusCode(); response.Headers.GetValues( "OData-Version" ).Single().Should().Be( "4.0" ); response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); response.Headers.GetValues( "api-deprecated-versions" ).Single().Should().Be( "3.0-Beta" ); diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpServerFixture.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpServerFixture.cs index 4c1c4e7d..edb7c1c1 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpServerFixture.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpServerFixture.cs @@ -1,6 +1,7 @@ namespace Microsoft.Web { using System; + using System.Diagnostics; using System.Net.Http; using System.Web.Http; using System.Web.Http.Dispatcher; @@ -13,7 +14,7 @@ protected HttpServerFixture() { Configuration.IncludeErrorDetailPolicy = Always; Configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), FilteredControllerTypes ); - Configuration.Services.Replace( typeof( ITraceWriter ), new TraceWriter() ); + Configuration.Services.Replace( typeof( ITraceWriter ), Debugger.IsAttached ? TraceWriter.Debug : TraceWriter.None ); Server = new HttpServer( Configuration ); Client = new HttpClient( new HttpSimulatorHandler( Server ) ) { BaseAddress = new Uri( "http://localhost" ) }; } diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/AdvancedFixture.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/AdvancedFixture.cs index f6231ec2..4673f427 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/AdvancedFixture.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/AdvancedFixture.cs @@ -3,13 +3,10 @@ using Microsoft.AspNet.OData.Advanced.Controllers; using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Configuration; - using Microsoft.AspNet.OData.Routing; using Microsoft.OData; - using Microsoft.OData.UriParser; using Microsoft.Web.Http; using Microsoft.Web.Http.Versioning; using System.Web.Http; - using static Microsoft.OData.ServiceLifetime; using static System.Web.Http.RouteParameter; public class AdvancedFixture : ODataFixture @@ -42,15 +39,10 @@ public AdvancedFixture() }; var models = modelBuilder.GetEdmModels(); - Configuration.MapVersionedODataRoutes( "odata", "api", models, OnConfigureContainer ); + Configuration.MapVersionedODataRoute( "odata", "api", models ); Configuration.Routes.MapHttpRoute( "orders", "api/{controller}/{key}", new { key = Optional } ); + Configuration.Formatters.Remove( Configuration.Formatters.XmlFormatter ); Configuration.EnsureInitialized(); } - - void OnConfigureContainer( IContainerBuilder builder ) - { - builder.AddService( Singleton, typeof( ODataUriResolver ), sp => UriResolver ); - builder.AddService( Singleton, typeof( IODataPathHandler ), sp => PathHandler ); - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs index 09862b27..166d9e63 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs @@ -16,8 +16,8 @@ public class Orders2Controller : ODataController public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } } ); - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IHttpActionResult Get( int key, ODataQueryOptions options ) => Ok( new Order() { Id = key, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs index 6c3e6cac..34b08175 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs @@ -16,8 +16,8 @@ public class People2Controller : ODataController public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IHttpActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs index 2203874c..a4489629 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs @@ -16,13 +16,13 @@ public class PeopleController : ODataController public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IHttpActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); [MapToApiVersion( "2.0" )] - [ODataRoute( "({key})" )] - public IHttpActionResult Patch( [FromODataUri] int key, Delta delta, ODataQueryOptions options ) + [ODataRoute( "{key}" )] + public IHttpActionResult Patch( int key, Delta delta, ODataQueryOptions options ) { if ( !ModelState.IsValid ) { diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when orders is v2.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when orders is v2.cs index adbbd84c..4dfbfb71 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when orders is v2.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when orders is v2.cs @@ -31,7 +31,7 @@ public async Task then_get_with_key_should_return_200() // act - var response = await Client.GetAsync( "api/orders(42)?api-version=2.0" ); + var response = await Client.GetAsync( "api/orders/42?api-version=2.0" ); var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( new { id = 0, customer = "" } ); // assert diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs index 368db47f..29f79315 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs @@ -19,7 +19,7 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() var person = new { lastName = "Me" }; // act - var response = await PatchAsync( $"api/people(42)?api-version=4.0", person ); + var response = await PatchAsync( $"api/people/42?api-version=4.0", person ); var content = await response.Content.ReadAsAsync(); // assert diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v1.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v1.cs index 822ca81a..c9ab41b4 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v1.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v1.cs @@ -31,8 +31,8 @@ public async Task then_get_should_return_200( string requestUrl ) } [Theory] - [InlineData( "api/people(42)" )] - [InlineData( "api/people(42)?api-version=1.0" )] + [InlineData( "api/people/42" )] + [InlineData( "api/people/42?api-version=1.0" )] public async Task then_get_with_key_should_return_200( string requestUrl ) { // arrange @@ -55,7 +55,7 @@ public async Task then_patch_should_return_405_if_supported_in_any_version() var person = new { lastName = "Me" }; // act - var response = await PatchAsync( $"api/people(42)?api-version=1.0", person ); + var response = await PatchAsync( $"api/people/42?api-version=1.0", person ); var content = await response.Content.ReadAsAsync(); // assert diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v2.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v2.cs index e26c0f29..d147bd0e 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v2.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v2.cs @@ -34,7 +34,7 @@ public async Task then_get_with_key_should_return_200() var example = new { id = 0, firstName = "", lastName = "", email = "" }; // act - var response = await Client.GetAsync( "api/people(42)?api-version=2.0" ); + var response = await Client.GetAsync( "api/people/42?api-version=2.0" ); var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example ); // assert @@ -50,7 +50,7 @@ public async Task then_patch_should_return_204() var person = new { email = "bmei@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + var response = await PatchAsync( "api/people/42?api-version=2.0", person ); // assert response.StatusCode.Should().Be( NoContent ); @@ -63,7 +63,7 @@ public async Task then_patch_should_return_400_while_updating_member_that_does_n var person = new { phone = "bmei@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + var response = await PatchAsync( "api/people/42?api-version=2.0", person ); // assert response.StatusCode.Should().Be( BadRequest ); diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v3.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v3.cs index f3d9eeb8..2edffec1 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v3.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is v3.cs @@ -35,7 +35,7 @@ public async Task then_get_with_key_should_return_200() var example = new { id = 0, firstName = "", lastName = "", email = "", phone = "" }; // act - var response = await Client.GetAsync( "api/people(42)?api-version=3.0" ); + var response = await Client.GetAsync( "api/people/42?api-version=3.0" ); var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example ); // assert @@ -51,7 +51,7 @@ public async Task then_patch_should_return_405_if_supported_in_any_version() var person = new { lastName = "Me" }; // act - var response = await PatchAsync( $"api/people(42)?api-version=3.0", person ); + var response = await PatchAsync( $"api/people/42?api-version=3.0", person ); var content = await response.Content.ReadAsAsync(); // assert diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/BasicFixture.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/BasicFixture.cs index 4f85ec44..17b0fee1 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/BasicFixture.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/BasicFixture.cs @@ -3,9 +3,7 @@ using Microsoft.AspNet.OData.Basic.Controllers; using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Configuration; - using Microsoft.OData.UriParser; using System.Web.Http; - using static Microsoft.OData.ServiceLifetime; public class BasicFixture : ODataFixture { @@ -29,8 +27,8 @@ public BasicFixture() }; var models = modelBuilder.GetEdmModels(); - Configuration.MapVersionedODataRoutes( "odata", "api", models, builder => builder.AddService( Singleton, typeof( ODataUriResolver ), sp => UriResolver ) ); - Configuration.MapVersionedODataRoutes( "odata-bypath", "v{apiVersion}", models, builder => builder.AddService( Singleton, typeof( ODataUriResolver ), sp => UriResolver ) ); + Configuration.MapVersionedODataRoute( "odata", "api", models ); + Configuration.MapVersionedODataRoute( "odata-bypath", "v{apiVersion}", models ); Configuration.EnsureInitialized(); } } diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/CustomersController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/CustomersController.cs index fd4fbbb8..4271936f 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/CustomersController.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/CustomersController.cs @@ -19,7 +19,7 @@ public class CustomersController : ODataController [ApiVersion( "1.0" )] [ApiVersion( "2.0" )] [ApiVersion( "3.0" )] - public IHttpActionResult Get( [FromODataUri] int key ) => Ok(); + public IHttpActionResult Get( int key ) => Ok(); [ODataRoute] [ApiVersion( "1.0" )] @@ -31,12 +31,12 @@ public IHttpActionResult Post( [FromBody] Customer customer ) return Created( customer ); } - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ApiVersion( "3.0" )] - public IHttpActionResult Put( [FromODataUri] int key, [FromBody] Customer customer ) => StatusCode( NoContent ); + public IHttpActionResult Put( int key, [FromBody] Customer customer ) => StatusCode( NoContent ); - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ApiVersionNeutral] - public IHttpActionResult Delete( [FromODataUri] int key ) => StatusCode( NoContent ); + public IHttpActionResult Delete( int key ) => StatusCode( NoContent ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs index 5d89ad3b..f6571818 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs @@ -15,8 +15,8 @@ public class OrdersController : ODataController public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IHttpActionResult Get( int key, ODataQueryOptions options ) => Ok( new Order() { Id = key, Customer = "Bill Mei" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs index ea582b8d..be09643b 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs @@ -16,8 +16,8 @@ public class People2Controller : ODataController public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IHttpActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs index 0a143270..fc48d6da 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs @@ -16,13 +16,13 @@ public class PeopleController : ODataController public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IHttpActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); [MapToApiVersion( "2.0" )] - [ODataRoute( "({key})" )] - public IHttpActionResult Patch( [FromODataUri] int key, Delta delta, ODataQueryOptions options ) + [ODataRoute( "{key}" )] + public IHttpActionResult Patch( int key, Delta delta, ODataQueryOptions options ) { if ( !ModelState.IsValid ) { diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs index 1a3c5fec..95d60ec1 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs @@ -13,11 +13,11 @@ public class when_using_a_query_string_and_split_into_two_types : BasicAcceptanc { [Theory] [InlineData( "api/people?api-version=1.0" )] - [InlineData( "api/people(42)?api-version=1.0" )] + [InlineData( "api/people/42?api-version=1.0" )] [InlineData( "api/people?api-version=2.0" )] - [InlineData( "api/people(42)?api-version=2.0" )] + [InlineData( "api/people/42?api-version=2.0" )] [InlineData( "api/people?api-version=3.0" )] - [InlineData( "api/people(42)?api-version=3.0" )] + [InlineData( "api/people/42?api-version=3.0" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -52,15 +52,15 @@ public async Task then_patch_should_return_204() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + var response = await PatchAsync( "api/people/42?api-version=2.0", person ); // assert response.StatusCode.Should().Be( NoContent ); } [Theory] - [InlineData( "api/people(42)?api-version=1.0" )] - [InlineData( "api/people(42)?api-version=3.0" )] + [InlineData( "api/people/42?api-version=1.0" )] + [InlineData( "api/people/42?api-version=3.0" )] public async Task then_patch_should_return_405_if_supported_in_any_version( string requestUrl ) { // arrange @@ -82,7 +82,7 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=4.0", person ); + var response = await PatchAsync( "api/people/42?api-version=4.0", person ); var content = await response.Content.ReadAsAsync(); // assert diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs index 68ac4612..208447ae 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs @@ -13,7 +13,7 @@ public class when_using_a_query_string : BasicAcceptanceTest { [Theory] [InlineData( "api/orders?api-version=1.0" )] - [InlineData( "api/orders(42)?api-version=1.0" )] + [InlineData( "api/orders/42?api-version=1.0" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment and split into two types.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment and split into two types.cs index 7f88f123..df183b51 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment and split into two types.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment and split into two types.cs @@ -13,11 +13,11 @@ public class when_using_a_url_segment_and_split_into_two_types : BasicAcceptance { [Theory] [InlineData( "v1/people" )] - [InlineData( "v1/people(42)" )] + [InlineData( "v1/people/42" )] [InlineData( "v2/people" )] - [InlineData( "v2/people(42)" )] + [InlineData( "v2/people/42" )] [InlineData( "v3/people" )] - [InlineData( "v3/people(42)" )] + [InlineData( "v3/people/42" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -37,15 +37,15 @@ public async Task then_patch_should_return_204() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "v2/people(42)", person ); + var response = await PatchAsync( "v2/people/42", person ); // assert response.StatusCode.Should().Be( NoContent ); } [Theory] - [InlineData( "v1/people(42)" )] - [InlineData( "v3/people(42)" )] + [InlineData( "v1/people/42" )] + [InlineData( "v3/people/42" )] public async Task then_patch_should_return_405_if_supported_in_any_version( string requestUrl ) { // arrange @@ -67,7 +67,7 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "v4/people(42)", person ); + var response = await PatchAsync( "v4/people/42", person ); var content = await response.Content.ReadAsAsync(); // assert diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment.cs index bb68cc8c..873ce06a 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment.cs @@ -13,7 +13,7 @@ public class when_using_a_url_segment : BasicAcceptanceTest { [Theory] [InlineData( "v1/orders" )] - [InlineData( "v1/orders(42)" )] + [InlineData( "v1/orders/42" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using an action.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using an action.cs index 18f2e14a..ea985309 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using an action.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using an action.cs @@ -10,9 +10,9 @@ public class when_using_an_action : BasicAcceptanceTest { [Theory] - [InlineData( "api/customers(42)?api-version=1.0" )] - [InlineData( "api/customers(42)?api-version=2.0" )] - [InlineData( "api/customers(42)?api-version=3.0" )] + [InlineData( "api/customers/42?api-version=1.0" )] + [InlineData( "api/customers/42?api-version=2.0" )] + [InlineData( "api/customers/42?api-version=3.0" )] [InlineData( "api/customers?api-version=2.0" )] [InlineData( "api/customers?api-version=3.0" )] public async Task then_get_should_return_200( string requestUrl ) @@ -40,6 +40,7 @@ public async Task then_post_should_return_201( string requestUrl ) // assert response.StatusCode.Should().Be( Created ); + // BUG: https://github.com/OData/WebApi/issues/1137 response.Headers.Location.Should().Be( new Uri( "http://localhost/api/Customers(42)" ) ); } @@ -47,7 +48,7 @@ public async Task then_post_should_return_201( string requestUrl ) public async Task then_put_should_return_204() { // arrange - var requestUrl = "api/customers(42)?api-version=3.0"; + var requestUrl = "api/customers/42?api-version=3.0"; var customer = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act @@ -58,10 +59,10 @@ public async Task then_put_should_return_204() } [Theory] - [InlineData( "api/customers(42)" )] - [InlineData( "api/customers(42)?api-version=1.0" )] - [InlineData( "api/customers(42)?api-version=2.0" )] - [InlineData( "api/customers(42)?api-version=3.0" )] + [InlineData( "api/customers/42" )] + [InlineData( "api/customers/42?api-version=1.0" )] + [InlineData( "api/customers/42?api-version=2.0" )] + [InlineData( "api/customers/42?api-version=3.0" )] public async Task then_delete_should_return_204( string requestUrl ) { // arrange diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs index 3d10115a..7dde12ce 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs @@ -22,7 +22,7 @@ EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) return customer; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { switch ( apiVersion.MajorVersion ) { diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs index 83f1962f..5307e17a 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs @@ -6,10 +6,9 @@ public class OrderModelConfiguration : IModelConfiguration { - static readonly ApiVersion V1 = new ApiVersion( 1, 0 ); readonly ApiVersion supportedApiVersion; - public OrderModelConfiguration() : this( V1 ) { } + public OrderModelConfiguration() { } public OrderModelConfiguration( ApiVersion supportedApiVersion ) => this.supportedApiVersion = supportedApiVersion; @@ -20,9 +19,9 @@ EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) return order; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { - if ( supportedApiVersion == apiVersion ) + if ( supportedApiVersion == null || supportedApiVersion == apiVersion ) { ConfigureCurrent( builder ); } diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs index 6ad4af34..3d57471b 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs @@ -22,7 +22,7 @@ EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) return person; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { switch ( apiVersion.MajorVersion ) { diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/CustomersController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/CustomersController.cs index 45146090..0804b051 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/CustomersController.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/CustomersController.cs @@ -2,30 +2,23 @@ { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Models; - using Microsoft.AspNet.OData.Routing; using System.Web.Http; using static System.Net.HttpStatusCode; - [ODataRoutePrefix( "Customers" )] public class CustomersController : ODataController { - [ODataRoute] public IHttpActionResult Get() => Ok(); - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int key ) => Ok(); + public IHttpActionResult Get( int key ) => Ok(); - [ODataRoute] - public IHttpActionResult Post( [FromBody] Customer customer ) + public IHttpActionResult Post( Customer customer ) { customer.Id = 42; return Created( customer ); } - [ODataRoute( "({key})" )] - public IHttpActionResult Put( [FromODataUri] int key, [FromBody] Customer customer ) => StatusCode( NoContent ); + public IHttpActionResult Put( int key, [FromBody] Customer customer ) => StatusCode( NoContent ); - [ODataRoute( "({key})" )] - public IHttpActionResult Delete( [FromODataUri] int key ) => StatusCode( NoContent ); + public IHttpActionResult Delete( int key ) => StatusCode( NoContent ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs index fb677ee6..eecb0ed9 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs @@ -3,18 +3,14 @@ using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Models; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using System.Web.Http; - [ODataRoutePrefix( "Orders" )] public class OrdersController : ODataController { - [ODataRoute] public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + public IHttpActionResult Get( int key, ODataQueryOptions options ) => Ok( new Order() { Id = key, Customer = "Bill Mei" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs index e3c375a9..7afe983a 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs @@ -3,20 +3,16 @@ using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Models; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.Web.Http; using System.Web.Http; [ControllerName( "People" )] - [ODataRoutePrefix( "People" )] public class People2Controller : ODataController { - [ODataRoute] public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + public IHttpActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs index bc88b8ea..14e57b3c 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs @@ -1,26 +1,21 @@ namespace Microsoft.AspNet.OData.Conventions.Controllers { - using Microsoft.Web.Http; - using System.Web.Http; using Microsoft.AspNet.OData; - using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.AspNet.OData.Models; + using Microsoft.AspNet.OData.Query; + using Microsoft.Web.Http; + using System.Web.Http; - [ODataRoutePrefix( "People" )] public class PeopleController : ODataController { - [ODataRoute] public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + public IHttpActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); [MapToApiVersion( "2.0" )] - [ODataRoute( "({key})" )] - public IHttpActionResult Patch( [FromODataUri] int key, Delta delta, ODataQueryOptions options ) + public IHttpActionResult Patch( int key, Delta delta, ODataQueryOptions options ) { if ( !ModelState.IsValid ) { diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/ConventionsFixture.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/ConventionsFixture.cs index 12d506e5..77246c19 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/ConventionsFixture.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/ConventionsFixture.cs @@ -4,10 +4,8 @@ using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Configuration; using Microsoft.AspNet.OData.Conventions.Controllers; - using Microsoft.OData.UriParser; using Microsoft.Web.Http.Versioning.Conventions; using System.Web.Http; - using static Microsoft.OData.ServiceLifetime; public class ConventionsFixture : ODataFixture { @@ -50,8 +48,8 @@ public ConventionsFixture() }; var models = modelBuilder.GetEdmModels(); - Configuration.MapVersionedODataRoutes( "odata", "api", models, builder => builder.AddService( Singleton, typeof( ODataUriResolver ), sp => UriResolver ) ); - Configuration.MapVersionedODataRoutes( "odata-bypath", "v{apiVersion}", models, builder => builder.AddService( Singleton, typeof( ODataUriResolver ), sp => UriResolver ) ); + Configuration.MapVersionedODataRoute( "odata", "api", models ); + Configuration.MapVersionedODataRoute( "odata-bypath", "v{apiVersion}", models ); Configuration.EnsureInitialized(); } } diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs index bb5b206e..13c7c250 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs @@ -15,11 +15,11 @@ public class when_using_a_query_string_and_split_into_two_types : ODataAcceptanc { [Theory] [InlineData( "api/people?api-version=1.0" )] - [InlineData( "api/people(42)?api-version=1.0" )] + [InlineData( "api/people/42?api-version=1.0" )] [InlineData( "api/people?api-version=2.0" )] - [InlineData( "api/people(42)?api-version=2.0" )] + [InlineData( "api/people/42?api-version=2.0" )] [InlineData( "api/people?api-version=3.0" )] - [InlineData( "api/people(42)?api-version=3.0" )] + [InlineData( "api/people/42?api-version=3.0" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -54,15 +54,15 @@ public async Task then_patch_should_return_204() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + var response = await PatchAsync( "api/people/42?api-version=2.0", person ); // assert response.StatusCode.Should().Be( NoContent ); } [Theory] - [InlineData( "api/people(42)?api-version=1.0" )] - [InlineData( "api/people(42)?api-version=3.0" )] + [InlineData( "api/people/42?api-version=1.0" )] + [InlineData( "api/people/42?api-version=3.0" )] public async Task then_patch_should_return_405_if_supported_in_any_version( string requestUrl ) { // arrange @@ -84,7 +84,7 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=4.0", person ); + var response = await PatchAsync( "api/people/42?api-version=4.0", person ); var content = await response.Content.ReadAsAsync(); // assert diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string.cs index 0ec0c2b7..6c4efe59 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string.cs @@ -15,7 +15,7 @@ public class when_using_a_query_string : ODataAcceptanceTest { [Theory] [InlineData( "api/orders?api-version=1.0" )] - [InlineData( "api/orders(42)?api-version=1.0" )] + [InlineData( "api/orders/42?api-version=1.0" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment and split into two types.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment and split into two types.cs index 1c1bf942..5d31f0ac 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment and split into two types.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment and split into two types.cs @@ -15,11 +15,11 @@ public class when_using_a_url_segment_and_split_into_two_types : ODataAcceptance { [Theory] [InlineData( "v1/people" )] - [InlineData( "v1/people(42)" )] + [InlineData( "v1/people/42" )] [InlineData( "v2/people" )] - [InlineData( "v2/people(42)" )] + [InlineData( "v2/people/42" )] [InlineData( "v3/people" )] - [InlineData( "v3/people(42)" )] + [InlineData( "v3/people/42" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -39,15 +39,15 @@ public async Task then_patch_should_return_204() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "v2/people(42)", person ); + var response = await PatchAsync( "v2/people/42", person ); // assert response.StatusCode.Should().Be( NoContent ); } [Theory] - [InlineData( "v1/people(42)" )] - [InlineData( "v3/people(42)" )] + [InlineData( "v1/people/42" )] + [InlineData( "v3/people/42" )] public async Task then_patch_should_return_405_if_supported_in_any_version( string requestUrl ) { // arrange @@ -69,7 +69,7 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "v4/people(42)", person ); + var response = await PatchAsync( "v4/people/42", person ); var content = await response.Content.ReadAsAsync(); // assert diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment.cs index 3671252e..c80b65de 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment.cs @@ -15,7 +15,7 @@ public class when_using_a_url_segment : ODataAcceptanceTest { [Theory] [InlineData( "v1/orders" )] - [InlineData( "v1/orders(42)" )] + [InlineData( "v1/orders/42" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using an action.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using an action.cs index ab9b3151..f66d8bd6 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using an action.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using an action.cs @@ -12,9 +12,9 @@ public class when_using_an_action : ODataAcceptanceTest { [Theory] - [InlineData( "api/customers(42)?api-version=1.0" )] - [InlineData( "api/customers(42)?api-version=2.0" )] - [InlineData( "api/customers(42)?api-version=3.0" )] + [InlineData( "api/customers/42?api-version=1.0" )] + [InlineData( "api/customers/42?api-version=2.0" )] + [InlineData( "api/customers/42?api-version=3.0" )] [InlineData( "api/customers?api-version=2.0" )] [InlineData( "api/customers?api-version=3.0" )] public async Task then_get_should_return_200( string requestUrl ) @@ -42,6 +42,7 @@ public async Task then_post_should_return_201( string requestUrl ) // assert response.StatusCode.Should().Be( Created ); + // BUG: https://github.com/OData/WebApi/issues/1137 response.Headers.Location.Should().Be( new Uri( "http://localhost/api/Customers(42)" ) ); } @@ -49,7 +50,7 @@ public async Task then_post_should_return_201( string requestUrl ) public async Task then_put_should_return_204() { // arrange - var requestUrl = "api/customers(42)?api-version=3.0"; + var requestUrl = "api/customers/42?api-version=3.0"; var customer = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act @@ -60,10 +61,10 @@ public async Task then_put_should_return_204() } [Theory] - [InlineData( "api/customers(42)" )] - [InlineData( "api/customers(42)?api-version=1.0" )] - [InlineData( "api/customers(42)?api-version=2.0" )] - [InlineData( "api/customers(42)?api-version=3.0" )] + [InlineData( "api/customers/42" )] + [InlineData( "api/customers/42?api-version=1.0" )] + [InlineData( "api/customers/42?api-version=2.0" )] + [InlineData( "api/customers/42?api-version=3.0" )] public async Task then_delete_should_return_204( string requestUrl ) { // arrange diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/ODataFixture.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/ODataFixture.cs index 2ece9b45..c6d6d57b 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/ODataFixture.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/ODataFixture.cs @@ -1,16 +1,9 @@ namespace Microsoft.AspNet.OData { - using Microsoft.AspNet.OData.Routing; - using Microsoft.OData.UriParser; using Microsoft.Web; - using static Microsoft.OData.ODataUrlKeyDelimiter; public abstract class ODataFixture : HttpServerFixture { protected ODataFixture() => FilteredControllerTypes.Add( typeof( VersionedMetadataController ) ); - - public IODataPathHandler PathHandler { get; } = new DefaultODataPathHandler() { UrlKeyDelimiter = Parentheses }; - - public ODataUriResolver UriResolver { get; } = new UnqualifiedCallAndEnumPrefixFreeResolver() { EnableCaseInsensitive = true }; } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/TraceWriter.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/TraceWriter.cs index e57cebda..da3a6064 100644 --- a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/TraceWriter.cs +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/TraceWriter.cs @@ -3,9 +3,32 @@ using System; using System.Net.Http; using System.Web.Http.Tracing; + using static System.Diagnostics.Debug; public sealed class TraceWriter : ITraceWriter { + TraceWriter() { } + + public static ITraceWriter None { get; } = new TraceWriter(); + + public static ITraceWriter Debug { get; } = new DebugTraceWriter(); + public void Trace( HttpRequestMessage request, string category, TraceLevel level, Action traceAction ) { } + + sealed class DebugTraceWriter : ITraceWriter + { + public void Trace( HttpRequestMessage request, string category, TraceLevel level, Action traceAction ) + { + var record = new TraceRecord( request, category, level ); + traceAction?.Invoke( record ); + + WriteLine( $"[{nameof( record.Category )}={record.Category},{nameof( record.Operator )}={record.Operator},{nameof( record.Operation )}={record.Operation}] {record.Message}" ); + + if ( record.Exception != null ) + { + WriteLine( record.Exception ); + } + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/HttpServerFixture.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/HttpServerFixture.cs index 8b32d4a9..98b71ffc 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/HttpServerFixture.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/HttpServerFixture.cs @@ -65,13 +65,11 @@ protected virtual void OnConfigureServices( IServiceCollection services ) { } protected virtual void OnConfigureRoutes( IRouteBuilder routeBuilder ) { } -#if !NET461 protected virtual void OnConfigureEndpoints( IEndpointRouteBuilder routeBuilder ) { routeBuilder.MapControllers(); routeBuilder.MapDefaultControllerRoute(); } -#endif TestServer CreateServer() { @@ -103,9 +101,6 @@ void OnDefaultConfigureServices( IServiceCollection services ) void OnConfigureApplication( IApplicationBuilder app ) { -#if NET461 - app.UseMvc( OnConfigureRoutes ).UseMvcWithDefaultRoute(); -#else if ( EnableEndpointRouting ) { app.UseRouting(); @@ -115,7 +110,6 @@ void OnConfigureApplication( IApplicationBuilder app ) { app.UseMvc( OnConfigureRoutes ).UseMvcWithDefaultRoute(); } -#endif } string GetContentRoot() diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/UIFixture.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/UIFixture.cs index fd5afe17..bd1a4b57 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/UIFixture.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/UIFixture.cs @@ -14,9 +14,7 @@ protected override void OnConfigurePartManager( ApplicationPartManager partManag partManager.ApplicationParts.Add( new AssemblyPart( GetType().Assembly ) ); } -#if !NET461 protected override void OnConfigureServices( IServiceCollection services ) => services.AddControllersWithViews().AddRazorRuntimeCompilation(); -#endif } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral Controller/when no version is specified.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral Controller/when no version is specified.cs index 9d2cb000..a2375380 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral Controller/when no version is specified.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral Controller/when no version is specified.cs @@ -8,6 +8,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( BasicCollection ) )] public class when_no_version_is_specified : AcceptanceTest { @@ -85,6 +86,7 @@ public async Task then_unsupported_api_version_error_should_only_contain_the_pat public when_no_version_is_specified( BasicFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] [Collection( nameof( BasicEndpointCollection ) )] public class when_no_version_is_specified_ : when_no_version_is_specified { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral UI Controller/when accessing a view using attribute routing.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral UI Controller/when accessing a view using attribute routing.cs index 379846cf..4b310679 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral UI Controller/when accessing a view using attribute routing.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral UI Controller/when accessing a view using attribute routing.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Xunit; + [Trait( "Routing", "Classic" )] public class when_accessing_a_view_using_attribute_routing : AcceptanceTest, IClassFixture { [Theory] @@ -35,6 +36,7 @@ public when_accessing_a_view_using_attribute_routing( UIFixture fixture ) : base } } + [Trait( "Routing", "Endpoint" )] public class when_accessing_a_view_using_attribute_routing_ : when_accessing_a_view_using_attribute_routing, IClassFixture { public when_accessing_a_view_using_attribute_routing_( UIEndpointFixture fixture ) : base( fixture ) { } diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral UI Controller/when accessing a view using convention routing.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral UI Controller/when accessing a view using convention routing.cs index d4d3bc96..7c5d72f7 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral UI Controller/when accessing a view using convention routing.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a version-neutral UI Controller/when accessing a view using convention routing.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Xunit; + [Trait( "Routing", "Classic" )] public class when_accessing_a_view_using_convention_routing : AcceptanceTest, IClassFixture { [Theory] @@ -34,6 +35,7 @@ public when_accessing_a_view_using_convention_routing( UIFixture fixture ) : bas } } + [Trait( "Routing", "Endpoint" )] public class when_accessing_a_view_using_convention_routing_ : when_accessing_a_view_using_convention_routing, IClassFixture { public when_accessing_a_view_using_convention_routing_( UIEndpointFixture fixture ) : base( fixture ) { } diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when two route templates overlap.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when two route templates overlap.cs index 4a4a5314..2b3f44c5 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when two route templates overlap.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when two route templates overlap.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Xunit; + [Trait( "Routing", "Classic" )] public class when_two_route_templates_overlap : AcceptanceTest, IClassFixture { [Fact] @@ -71,6 +72,7 @@ public async Task then_the_higher_precedence_route_should_result_in_ambiguous_ac public when_two_route_templates_overlap( OverlappingRouteTemplateFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] public class when_two_route_templates_overlap_ : when_two_route_templates_overlap, IClassFixture { public when_two_route_templates_overlap_( OverlappingRouteTemplateEndpointFixture fixture ) : base( fixture ) { } diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a query string and split into two types.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a query string and split into two types.cs index 87f9163f..da8d7b9c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a query string and split into two types.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a query string and split into two types.cs @@ -10,6 +10,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( BasicCollection ) )] public class when_using_a_query_string_and_split_into_two_types : AcceptanceTest { @@ -66,11 +67,16 @@ public async Task then_get_returns_400_with_invalid_id() // arrange var requestUrl = "api/values/abc?api-version=2.0"; + // note: the classic routing mechanism cannot disambiguate 400 vs 405 + // because the route constraint {id:int} completely eliminates the + // candidate; however, endpoint routing works as expected + var statusCode = UsingEndpointRouting ? BadRequest : MethodNotAllowed; + // act var response = await GetAsync( requestUrl ); // assert - response.StatusCode.Should().Be( BadRequest ); + response.StatusCode.Should().Be( statusCode ); } [Theory] @@ -138,6 +144,7 @@ public async Task then_action_segment_should_not_be_ambiguous_with_route_paramet public when_using_a_query_string_and_split_into_two_types( BasicFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] [Collection( nameof( BasicEndpointCollection ) )] public class when_using_a_query_string_and_split_into_two_types_ : when_using_a_query_string_and_split_into_two_types { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a url segment.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a url segment.cs index a6898aa8..f1e318b0 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a url segment.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using a url segment.cs @@ -11,6 +11,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( BasicCollection ) )] public class when_using_a_url_segment : AcceptanceTest { @@ -70,11 +71,16 @@ public async Task then_get_returns_400_with_invalid_id() // arrange var requestUrl = "api/v2/helloworld/abc"; + // note: the classic routing mechanism cannot disambiguate 400 vs 405 + // because the route constraint {id:int} completely eliminates the + // candidate; however, endpoint routing works as expected + var statusCode = UsingEndpointRouting ? BadRequest : MethodNotAllowed; + // act var response = await GetAsync( requestUrl ); // asserts - response.StatusCode.Should().Be( BadRequest ); + response.StatusCode.Should().Be( statusCode ); } [Theory] @@ -126,6 +132,7 @@ public async Task then_action_segment_should_not_be_ambiguous_with_route_paramet public when_using_a_url_segment( BasicFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] [Collection( nameof( BasicEndpointCollection ) )] public class when_using_a_url_segment_ : when_using_a_url_segment { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using an action.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using an action.cs index 7f87216e..61c86320 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using an action.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Basic/given a versioned Controller/when using an action.cs @@ -8,6 +8,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( BasicCollection ) )] public class when_using_an_action : AcceptanceTest { @@ -77,6 +78,7 @@ public async Task then_delete_should_return_204( string requestUrl ) public when_using_an_action( BasicFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] [Collection( nameof( BasicEndpointCollection ) )] public class when_using_an_action_ : when_using_an_action { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/AgreementsEndpointFixture.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/AgreementsEndpointFixture.cs index 670b829c..174d948a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/AgreementsEndpointFixture.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/AgreementsEndpointFixture.cs @@ -7,12 +7,10 @@ public class AgreementsEndpointFixture : AgreementsFixture { public AgreementsEndpointFixture() => EnableEndpointRouting = true; -#if !NET461 protected override void OnConfigureEndpoints( IEndpointRouteBuilder routeBuilder ) { routeBuilder.MapControllerRoute( "VersionedQueryString", "api/{controller}/{accountId}/{action=Get}" ); routeBuilder.MapControllerRoute( "VersionedUrl", "v{version:apiVersion}/{controller}/{accountId}/{action=Get}" ); } -#endif } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using a query string.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using a query string.cs index 5546464c..e54aad33 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using a query string.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using a query string.cs @@ -9,6 +9,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( AgreementsCollection ) )] public class when_using_a_query_string : AcceptanceTest { @@ -63,6 +64,7 @@ public async Task then_get_should_return_400_for_an_unspecified_version() public when_using_a_query_string( AgreementsFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] [Collection( nameof( AgreementsEndpointCollection ) )] public class when_using_a_query_string_ : when_using_a_query_string { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using a url segment.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using a url segment.cs index 5149db59..a501a2a8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using a url segment.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using a url segment.cs @@ -9,6 +9,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( AgreementsCollection ) )] public class when_using_a_url_segment : AcceptanceTest { @@ -48,6 +49,7 @@ public async Task then_get_should_return_400_for_an_unsupported_version() public when_using_a_url_segment( AgreementsFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] [Collection( nameof( AgreementsEndpointCollection ) )] public class when_using_a_url_segment_ : when_using_a_url_segment { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using an action.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using an action.cs index 37837c96..f8b1487a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using an action.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/ByNamespace/given a versioned Controller per namespace/when using an action.cs @@ -8,6 +8,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] public class when_using_an_action : AcceptanceTest, IClassFixture { [Theory] @@ -73,6 +74,7 @@ public async Task then_delete_should_return_204( string requestUrl ) public when_using_an_action( OrdersFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] public class when_using_an_action_ : when_using_an_action, IClassFixture { public when_using_an_action_( OrdersEndpointFixture fixture ) : base( fixture ) { } diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using a query string and split into two types.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using a query string and split into two types.cs index 912596e3..10864ac7 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using a query string and split into two types.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using a query string and split into two types.cs @@ -10,6 +10,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( ConventionsCollection ) )] public class when_using_a_query_string_and_split_into_two_types : AcceptanceTest { @@ -64,6 +65,7 @@ public async Task then_get_should_return_400_for_an_unspecified_version() public when_using_a_query_string_and_split_into_two_types( ConventionsFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] [Collection( nameof( ConventionsEndpointCollection ) )] public class when_using_a_query_string_and_split_into_two_types_ : when_using_a_query_string_and_split_into_two_types { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using a url segment.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using a url segment.cs index 21c0084f..b6921dd4 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using a url segment.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using a url segment.cs @@ -10,6 +10,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( ConventionsCollection ) )] public class when_using_a_url_segment : AcceptanceTest { @@ -58,7 +59,7 @@ public async Task then_get_should_return_400_for_an_unsupported_version() // act - var response = await GetAsync( "api/v4/helloworld" ); + var response = await GetAsync( "api/v42/helloworld" ); var content = await response.Content.ReadAsAsync(); // assert @@ -69,6 +70,7 @@ public async Task then_get_should_return_400_for_an_unsupported_version() public when_using_a_url_segment( ConventionsFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] [Collection( nameof( ConventionsEndpointCollection ) )] public class when_using_a_url_segment_ : when_using_a_url_segment { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using an action.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using an action.cs index f4b5bc2a..443e1113 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using an action.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/Conventions/given a versioned Controller using conventions/when using an action.cs @@ -8,6 +8,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( ConventionsCollection ) )] public class when_using_an_action : AcceptanceTest { @@ -77,6 +78,7 @@ public async Task then_delete_should_return_204( string requestUrl ) public when_using_an_action( ConventionsFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] [Collection( nameof( ConventionsEndpointCollection ) )] public class when_using_an_action_ : when_using_an_action { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/MediaTypeNegotiation/given a versioned Controller/when using media type negotiation.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/MediaTypeNegotiation/given a versioned Controller/when using media type negotiation.cs index 52f98475..c7d1b14a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/MediaTypeNegotiation/given a versioned Controller/when using media type negotiation.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Mvc/MediaTypeNegotiation/given a versioned Controller/when using media type negotiation.cs @@ -13,6 +13,7 @@ using static System.Net.HttpStatusCode; using static System.Text.Encoding; + [Trait( "Routing", "Classic" )] public class when_using_media_type_negotiation : AcceptanceTest, IClassFixture { [Theory] @@ -93,6 +94,7 @@ public async Task then_post_should_return_201() public when_using_media_type_negotiation( MediaTypeNegotiationFixture fixture ) : base( fixture ) { } } + [Trait( "Routing", "Endpoint" )] public class when_using_media_type_negotiation_ : when_using_media_type_negotiation, IClassFixture { public when_using_media_type_negotiation_( MediaTypeNegotiationEndpointFixture fixture ) : base( fixture ) { } diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedEndpointFixture.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedEndpointFixture.cs new file mode 100644 index 00000000..c03c88b1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedEndpointFixture.cs @@ -0,0 +1,46 @@ +namespace Microsoft.AspNetCore.OData.Advanced +{ + using Microsoft.AspNet.OData.Builder; + using Microsoft.AspNet.OData.Extensions; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.AspNetCore.OData.Advanced.Controllers.Endpoint; + using Microsoft.AspNetCore.OData.Configuration; + using Microsoft.AspNetCore.Routing; + using Microsoft.Extensions.DependencyInjection; + using System.Reflection; + + public class AdvancedEndpointFixture : ODataFixture + { + public AdvancedEndpointFixture() + { + EnableEndpointRouting = true; + FilteredControllerTypes.Add( typeof( OrdersController ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( Orders2Controller ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( Orders3Controller ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( PeopleController ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( People2Controller ).GetTypeInfo() ); + } + + protected override void OnAddApiVersioning( ApiVersioningOptions options ) + { + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = ApiVersionReader.Combine( + new QueryStringApiVersionReader(), + new HeaderApiVersionReader( "api-version", "x-ms-version" ) ); + } + + protected override void OnConfigureEndpoints( IEndpointRouteBuilder routeBuilder ) + { + base.OnConfigureEndpoints( routeBuilder ); + + var modelBuilder = routeBuilder.ServiceProvider.GetRequiredService(); + + modelBuilder.ModelConfigurations.Clear(); + modelBuilder.ModelConfigurations.Add( new PersonModelConfiguration() ); + modelBuilder.ModelConfigurations.Add( new OrderModelConfiguration( supportedApiVersion: new ApiVersion( 2, 0 ) ) ); + routeBuilder.MapVersionedODataRoute( "odata", "api", modelBuilder.GetEdmModels() ); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedFixture.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedFixture.cs index d0d94f74..84618ae7 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedFixture.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedFixture.cs @@ -4,7 +4,7 @@ using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Versioning; - using Microsoft.AspNetCore.OData.Advanced.Controllers; + using Microsoft.AspNetCore.OData.Advanced.Controllers.Classic; using Microsoft.AspNetCore.OData.Configuration; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -37,10 +37,7 @@ protected override void OnConfigureRoutes( IRouteBuilder routeBuilder ) modelBuilder.ModelConfigurations.Clear(); modelBuilder.ModelConfigurations.Add( new PersonModelConfiguration() ); modelBuilder.ModelConfigurations.Add( new OrderModelConfiguration( supportedApiVersion: new ApiVersion( 2, 0 ) ) ); - - var models = modelBuilder.GetEdmModels(); - - routeBuilder.MapVersionedODataRoutes( "odata", "api", models ); + routeBuilder.MapVersionedODataRoute( "odata", "api", modelBuilder.GetEdmModels() ); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedODataEndpointCollection.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedODataEndpointCollection.cs new file mode 100644 index 00000000..34461a19 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/AdvancedODataEndpointCollection.cs @@ -0,0 +1,10 @@ +namespace Microsoft.AspNetCore.OData.Advanced +{ + using System; + using Xunit; + + [CollectionDefinition( nameof( AdvancedODataEndpointCollection ) )] + public sealed class AdvancedODataEndpointCollection : ICollectionFixture + { + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/Orders2Controller.cs similarity index 66% rename from test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs rename to test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/Orders2Controller.cs index a4ef0282..f01e9285 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/Orders2Controller.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNetCore.OData.Advanced.Controllers +namespace Microsoft.AspNetCore.OData.Advanced.Controllers.Classic { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; @@ -12,11 +12,11 @@ public class Orders2Controller : ODataController { [ODataRoute] - public IActionResult Get( ODataQueryOptions options ) => - Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{HttpContext.GetRequestedApiVersion()}" } } ); + public IActionResult Get( ODataQueryOptions options, ApiVersion apiVersion ) => + Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{apiVersion}" } } ); - [ODataRoute( "({key})" )] - public IActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => - Ok( new Order() { Id = key, Customer = $"Customer v{HttpContext.GetRequestedApiVersion()}" } ); + [ODataRoute( "{key}" )] + public IActionResult Get( int key, ODataQueryOptions options, ApiVersion apiVersion ) => + Ok( new Order() { Id = key, Customer = $"Customer v{apiVersion}" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/Orders3Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/Orders3Controller.cs new file mode 100644 index 00000000..eba6658e --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/Orders3Controller.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNetCore.OData.Advanced.Controllers.Classic +{ + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [ApiController] + [ApiVersion( "3.0" )] + [ControllerName( "Orders" )] + [Route( "api/orders" )] + public class Orders3Controller : ControllerBase + { + [HttpGet] + public IActionResult Get( ApiVersion apiVersion ) => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{apiVersion}" } } ); + + [HttpGet( "{key}" )] + public IActionResult Get( int key, ApiVersion apiVersion ) => Ok( new Order() { Id = key, Customer = $"Customer v{apiVersion}" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/OrdersController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/OrdersController.cs new file mode 100644 index 00000000..c7aa2255 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/OrdersController.cs @@ -0,0 +1,16 @@ +namespace Microsoft.AspNetCore.OData.Advanced.Controllers.Classic +{ + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [ApiController] + [Route( "api/orders" )] + public class OrdersController : ControllerBase + { + [HttpGet] + public IActionResult Get( ApiVersion apiVersion ) => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{apiVersion}" } } ); + + [HttpGet( "{key}" )] + public IActionResult Get( int key, ApiVersion apiVersion ) => Ok( new Order() { Id = key, Customer = $"Customer v{apiVersion}" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/People2Controller.cs similarity index 79% rename from test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs rename to test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/People2Controller.cs index 2674f9f4..be34ed09 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/People2Controller.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNetCore.OData.Basic.Controllers +namespace Microsoft.AspNetCore.OData.Advanced.Controllers.Classic { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; @@ -15,8 +15,8 @@ public class People2Controller : ODataController public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/PeopleController.cs similarity index 76% rename from test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs rename to test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/PeopleController.cs index 9b0289e7..34e6a6ee 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Classic/PeopleController.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNetCore.OData.Basic.Controllers +namespace Microsoft.AspNetCore.OData.Advanced.Controllers.Classic { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; @@ -15,13 +15,13 @@ public class PeopleController : ODataController public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); [MapToApiVersion( "2.0" )] - [ODataRoute( "({key})" )] - public IActionResult Patch( [FromODataUri] int key, Delta delta, ODataQueryOptions options ) + [ODataRoute( "{key}" )] + public IActionResult Patch( int key, Delta delta, ODataQueryOptions options ) { if ( !ModelState.IsValid ) { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/Orders2Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/Orders2Controller.cs new file mode 100644 index 00000000..ec90a68f --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/Orders2Controller.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNetCore.OData.Advanced.Controllers.Endpoint +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [ApiVersion( "2.0" )] + [ControllerName( "Orders" )] + public class Orders2Controller : ODataController + { + public IActionResult Get( ODataQueryOptions options, ApiVersion apiVersion ) => + Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{apiVersion}" } } ); + + public IActionResult Get( int key, ODataQueryOptions options, ApiVersion apiVersion ) => + Ok( new Order() { Id = key, Customer = $"Customer v{apiVersion}" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/Orders3Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/Orders3Controller.cs new file mode 100644 index 00000000..1de3dfa1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/Orders3Controller.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNetCore.OData.Advanced.Controllers.Endpoint +{ + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [ApiController] + [ApiVersion( "3.0" )] + [ControllerName( "Orders" )] + [Route( "api/orders" )] + public class Orders3Controller : ControllerBase + { + [HttpGet] + public IActionResult Get( ApiVersion apiVersion ) => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{apiVersion}" } } ); + + [HttpGet( "{key}" )] + public IActionResult Get( int key, ApiVersion apiVersion ) => Ok( new Order() { Id = key, Customer = $"Customer v{apiVersion}" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/OrdersController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/OrdersController.cs new file mode 100644 index 00000000..7e4c904b --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/OrdersController.cs @@ -0,0 +1,16 @@ +namespace Microsoft.AspNetCore.OData.Advanced.Controllers.Endpoint +{ + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [ApiController] + [Route( "api/orders" )] + public class OrdersController : ControllerBase + { + [HttpGet] + public IActionResult Get( ApiVersion apiVersion ) => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{apiVersion}" } } ); + + [HttpGet( "{key}" )] + public IActionResult Get( int key, ApiVersion apiVersion ) => Ok( new Order() { Id = key, Customer = $"Customer v{apiVersion}" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/People2Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/People2Controller.cs new file mode 100644 index 00000000..338a549b --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/People2Controller.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNetCore.OData.Advanced.Controllers.Endpoint +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [ApiVersion( "3.0" )] + [ControllerName( "People" )] + public class People2Controller : ODataController + { + public IActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + public IActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/PeopleController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/PeopleController.cs new file mode 100644 index 00000000..ba9c8244 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Endpoint/PeopleController.cs @@ -0,0 +1,33 @@ +namespace Microsoft.AspNetCore.OData.Advanced.Controllers.Endpoint +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [ApiVersion( "1.0" )] + [ApiVersion( "2.0" )] + public class PeopleController : ODataController + { + public IActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + public IActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + + [MapToApiVersion( "2.0" )] + public IActionResult Patch( int key, Delta delta, ODataQueryOptions options ) + { + if ( !ModelState.IsValid ) + { + return BadRequest( ModelState ); + } + + var person = new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" }; + + delta.Patch( person ); + + return Updated( person ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Orders3Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Orders3Controller.cs deleted file mode 100644 index 9158245e..00000000 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/Orders3Controller.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Microsoft.AspNetCore.OData.Advanced.Controllers -{ - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.OData.Models; - - [ApiController] - [ApiVersion( "3.0" )] - [Route( "api/orders" )] - public class Orders3Controller : ControllerBase - { - [HttpGet] - public IActionResult Get() => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{HttpContext.GetRequestedApiVersion()}" } } ); - - [HttpGet( "{key}" )] - public IActionResult Get( int key ) => Ok( new Order() { Id = key, Customer = $"Customer v{HttpContext.GetRequestedApiVersion()}" } ); - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/OrdersController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/OrdersController.cs deleted file mode 100644 index db27a2f0..00000000 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/OrdersController.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Microsoft.AspNetCore.OData.Advanced.Controllers -{ - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.OData.Models; - - [ApiController] - [Route( "api/orders" )] - public class OrdersController : ControllerBase - { - [HttpGet] - public IActionResult Get() => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{HttpContext.GetRequestedApiVersion()}" } } ); - - [HttpGet( "{key}" )] - public IActionResult Get( int key ) => Ok( new Order() { Id = key, Customer = $"Customer v{HttpContext.GetRequestedApiVersion()}" } ); - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ControllerBase mixed with OData controllers/when orders is v1.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ControllerBase mixed with OData controllers/when orders is v1.cs index 96a3dab9..56d5dc54 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ControllerBase mixed with OData controllers/when orders is v1.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ControllerBase mixed with OData controllers/when orders is v1.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Xunit; + [Trait( "Routing", "Classic" )] [Collection( nameof( AdvancedODataCollection ) )] public class when_orders_is_v1 : ODataAcceptanceTest { @@ -67,5 +68,14 @@ public async Task then_get_with_key_should_return_200() } public when_orders_is_v1( AdvancedFixture fixture ) : base( fixture ) { } + + protected when_orders_is_v1( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( AdvancedODataEndpointCollection ) )] + public class when_orders_is_v1_ : when_orders_is_v1 + { + public when_orders_is_v1_( AdvancedEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ControllerBase mixed with OData controllers/when orders is v3.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ControllerBase mixed with OData controllers/when orders is v3.cs index 072bdf2a..fabff6c6 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ControllerBase mixed with OData controllers/when orders is v3.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ControllerBase mixed with OData controllers/when orders is v3.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Xunit; + [Trait( "Routing", "Classic" )] [Collection( nameof( AdvancedODataCollection ) )] public class when_orders_is_v3 : ODataAcceptanceTest { @@ -39,5 +40,14 @@ public async Task then_get_with_key_should_return_200_for_an_unspecified_version } public when_orders_is_v3( AdvancedFixture fixture ) : base( fixture ) { } + + protected when_orders_is_v3( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( AdvancedODataEndpointCollection ) )] + public class when_orders_is_v3_ : when_orders_is_v3 + { + public when_orders_is_v3_( AdvancedEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when orders is v2.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when orders is v2.cs index 5020da7f..22d9f170 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when orders is v2.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when orders is v2.cs @@ -7,8 +7,9 @@ using System.Threading.Tasks; using Xunit; + [Trait( "Routing", "Classic" )] [Collection( nameof( AdvancedODataCollection ) )] - public class when_using_OData_for_orders_in_v2 : ODataAcceptanceTest + public class when_using_odata_for_orders_in_v2 : ODataAcceptanceTest { [Fact] public async Task then_get_should_return_200() @@ -31,13 +32,22 @@ public async Task then_get_with_key_should_return_200() // act - var response = await Client.GetAsync( "api/orders(42)?api-version=2.0" ); + var response = await Client.GetAsync( "api/orders/42?api-version=2.0" ); var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( new { id = 0, customer = "" } ); // assert order.Should().BeEquivalentTo( new { id = 42, customer = "Customer v2.0" }, options => options.ExcludingMissingMembers() ); } - public when_using_OData_for_orders_in_v2( AdvancedFixture fixture ) : base( fixture ) { } + public when_using_odata_for_orders_in_v2( AdvancedFixture fixture ) : base( fixture ) { } + + protected when_using_odata_for_orders_in_v2( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( AdvancedODataEndpointCollection ) )] + public class when_using_odata_for_orders_in_v2_ : when_using_odata_for_orders_in_v2 + { + public when_using_odata_for_orders_in_v2_( AdvancedEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs index b1afa95b..3b4116c0 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs @@ -9,6 +9,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( AdvancedODataCollection ) )] public class when_people_is_any_version : ODataAcceptanceTest { @@ -19,7 +20,7 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() var person = new { lastName = "Me" }; // act - var response = await PatchAsync( $"api/people(42)?api-version=4.0", person ); + var response = await PatchAsync( $"api/people/42?api-version=4.0", person ); var content = await response.Content.ReadAsAsync(); // assert @@ -28,5 +29,14 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() } public when_people_is_any_version( AdvancedFixture fixture ) : base( fixture ) { } + + protected when_people_is_any_version( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( AdvancedODataEndpointCollection ) )] + public class when_people_is_any_version_ : when_people_is_any_version + { + public when_people_is_any_version_( AdvancedEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v1.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v1.cs index 1e9697a4..76923fdf 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v1.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v1.cs @@ -9,6 +9,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( AdvancedODataCollection ) )] public class when_people_is_v1 : ODataAcceptanceTest { @@ -31,8 +32,8 @@ public async Task then_get_should_return_200( string requestUrl ) } [Theory] - [InlineData( "api/people(42)" )] - [InlineData( "api/people(42)?api-version=1.0" )] + [InlineData( "api/people/42" )] + [InlineData( "api/people/42?api-version=1.0" )] public async Task then_get_with_key_should_return_200( string requestUrl ) { // arrange @@ -55,7 +56,7 @@ public async Task then_patch_should_return_405_if_supported_in_any_version() var person = new { lastName = "Me" }; // act - var response = await PatchAsync( $"api/people(42)?api-version=1.0", person ); + var response = await PatchAsync( $"api/people/42?api-version=1.0", person ); var content = await response.Content.ReadAsAsync(); // assert @@ -64,5 +65,14 @@ public async Task then_patch_should_return_405_if_supported_in_any_version() } public when_people_is_v1( AdvancedFixture fixture ) : base( fixture ) { } + + protected when_people_is_v1( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( AdvancedODataEndpointCollection ) )] + public class when_people_is_v1_ : when_people_is_v1 + { + public when_people_is_v1_( AdvancedEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v2.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v2.cs index 4849da24..d2ebd499 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v2.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v2.cs @@ -8,6 +8,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( AdvancedODataCollection ) )] public class when_people_is_v2 : ODataAcceptanceTest { @@ -34,7 +35,7 @@ public async Task then_get_with_key_should_return_200() var example = new { id = 0, firstName = "", lastName = "", email = "" }; // act - var response = await Client.GetAsync( "api/people(42)?api-version=2.0" ); + var response = await Client.GetAsync( "api/people/42?api-version=2.0" ); var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example ); // assert @@ -50,7 +51,7 @@ public async Task then_patch_should_return_204() var person = new { email = "bmei@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + var response = await PatchAsync( "api/people/42?api-version=2.0", person ); // assert response.StatusCode.Should().Be( NoContent ); @@ -63,12 +64,21 @@ public async Task then_patch_should_return_400_while_updating_member_that_does_n var person = new { phone = "bmei@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + var response = await PatchAsync( "api/people/42?api-version=2.0", person ); // assert response.StatusCode.Should().Be( BadRequest ); } public when_people_is_v2( AdvancedFixture fixture ) : base( fixture ) { } + + protected when_people_is_v2( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( AdvancedODataEndpointCollection ) )] + public class when_people_is_v2_ : when_people_is_v2 + { + public when_people_is_v2_( AdvancedEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v3.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v3.cs index 46ed7a91..5549e497 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v3.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is v3.cs @@ -9,6 +9,7 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( AdvancedODataCollection ) )] public class when_people_is_v3 : ODataAcceptanceTest { @@ -35,7 +36,7 @@ public async Task then_get_with_key_should_return_200() var example = new { id = 0, firstName = "", lastName = "", email = "", phone = "" }; // act - var response = await Client.GetAsync( "api/people(42)?api-version=3.0" ); + var response = await Client.GetAsync( "api/people/42?api-version=3.0" ); var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example ); // assert @@ -51,7 +52,7 @@ public async Task then_patch_should_return_405_if_supported_in_any_version() var person = new { lastName = "Me" }; // act - var response = await PatchAsync( $"api/people(42)?api-version=3.0", person ); + var response = await PatchAsync( $"api/people/42?api-version=3.0", person ); var content = await response.Content.ReadAsAsync(); // assert @@ -60,5 +61,14 @@ public async Task then_patch_should_return_405_if_supported_in_any_version() } public when_people_is_v3( AdvancedFixture fixture ) : base( fixture ) { } + + protected when_people_is_v3( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( AdvancedODataEndpointCollection ) )] + public class when_people_is_v3_ : when_people_is_v3 + { + public when_people_is_v3_( AdvancedEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicAcceptanceTest.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicAcceptanceTest.cs index 68557a5a..e9472445 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicAcceptanceTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicAcceptanceTest.cs @@ -7,7 +7,6 @@ using Xunit; using static System.Net.HttpStatusCode; - [Collection( nameof( BasicODataCollection ) )] public abstract class BasicAcceptanceTest : ODataAcceptanceTest { [Fact] @@ -56,6 +55,6 @@ public async Task then_the_service_document_should_return_only_path_for_an_unsup content.Error.Message.Should().NotContain( "?api-version=1.0" ); } - protected BasicAcceptanceTest( BasicFixture fixture ) : base( fixture ) { } + protected BasicAcceptanceTest( ODataFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicEndpointFixture.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicEndpointFixture.cs new file mode 100644 index 00000000..ef110b04 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicEndpointFixture.cs @@ -0,0 +1,35 @@ +namespace Microsoft.AspNetCore.OData.Basic +{ + using Microsoft.AspNet.OData.Builder; + using Microsoft.AspNet.OData.Extensions; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.AspNetCore.OData.Basic.Controllers.Endpoint; + using Microsoft.AspNetCore.Routing; + using Microsoft.Extensions.DependencyInjection; + using System.Reflection; + + public class BasicEndpointFixture : ODataFixture + { + public BasicEndpointFixture() + { + EnableEndpointRouting = true; + FilteredControllerTypes.Add( typeof( OrdersController ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( PeopleController ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( People2Controller ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( CustomersController ).GetTypeInfo() ); + } + + protected override void OnAddApiVersioning( ApiVersioningOptions options ) => options.ReportApiVersions = true; + + protected override void OnConfigureEndpoints( IEndpointRouteBuilder routeBuilder ) + { + base.OnConfigureEndpoints( routeBuilder ); + + var modelBuilder = routeBuilder.ServiceProvider.GetRequiredService(); + var models = modelBuilder.GetEdmModels(); + + routeBuilder.MapVersionedODataRoute( "odata", "api", models ); + routeBuilder.MapVersionedODataRoute( "odata-bypath", "v{version:apiVersion}", models ); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicFixture.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicFixture.cs index cf9134d6..5230ec6c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicFixture.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicFixture.cs @@ -3,7 +3,7 @@ using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNetCore.Mvc.Versioning; - using Microsoft.AspNetCore.OData.Basic.Controllers; + using Microsoft.AspNetCore.OData.Basic.Controllers.Classic; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using System.Reflection; @@ -25,8 +25,8 @@ protected override void OnConfigureRoutes( IRouteBuilder routeBuilder ) var modelBuilder = routeBuilder.ServiceProvider.GetRequiredService(); var models = modelBuilder.GetEdmModels(); - routeBuilder.MapVersionedODataRoutes( "odata", "api", models ); - routeBuilder.MapVersionedODataRoutes( "odata-bypath", "v{version:apiVersion}", models ); + routeBuilder.MapVersionedODataRoute( "odata", "api", models ); + routeBuilder.MapVersionedODataRoute( "odata-bypath", "v{version:apiVersion}", models ); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicODataEndpointCollection.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicODataEndpointCollection.cs new file mode 100644 index 00000000..17d62e47 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/BasicODataEndpointCollection.cs @@ -0,0 +1,10 @@ +namespace Microsoft.AspNetCore.OData.Basic +{ + using System; + using Xunit; + + [CollectionDefinition( nameof( BasicODataEndpointCollection ) )] + public sealed class BasicODataEndpointCollection : ICollectionFixture + { + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/CustomersController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/CustomersController.cs similarity index 66% rename from test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/CustomersController.cs rename to test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/CustomersController.cs index 036712fe..0cec08c1 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/CustomersController.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/CustomersController.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNetCore.OData.Basic.Controllers +namespace Microsoft.AspNetCore.OData.Basic.Controllers.Classic { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Routing; @@ -13,11 +13,11 @@ public class CustomersController : ODataController [ApiVersion( "3.0" )] public IActionResult Get() => Ok(); - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ApiVersion( "1.0" )] [ApiVersion( "2.0" )] [ApiVersion( "3.0" )] - public IActionResult Get( [FromODataUri] int key ) => Ok(); + public IActionResult Get( int key ) => Ok(); [ODataRoute] [ApiVersion( "1.0" )] @@ -29,12 +29,12 @@ public IActionResult Post( [FromBody] Customer customer ) return Created( customer ); } - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ApiVersion( "3.0" )] - public IActionResult Put( [FromODataUri] int key, [FromBody] Customer customer ) => NoContent(); + public IActionResult Put( int key, [FromBody] Customer customer ) => NoContent(); - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ApiVersionNeutral] - public IActionResult Delete( [FromODataUri] int key ) => NoContent(); + public IActionResult Delete( int key ) => NoContent(); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/OrdersController.cs similarity index 74% rename from test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs rename to test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/OrdersController.cs index a5c98e60..77f0b046 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/OrdersController.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNetCore.OData.Basic.Controllers +namespace Microsoft.AspNetCore.OData.Basic.Controllers.Classic { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; @@ -14,8 +14,8 @@ public class OrdersController : ODataController public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); - [ODataRoute( "({key})" )] - public IActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IActionResult Get( int key, ODataQueryOptions options ) => Ok( new Order() { Id = key, Customer = "Bill Mei" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/People2Controller.cs similarity index 79% rename from test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs rename to test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/People2Controller.cs index 4b3c590c..97388267 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/People2Controller.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNetCore.OData.Advanced.Controllers +namespace Microsoft.AspNetCore.OData.Basic.Controllers.Classic { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; @@ -15,8 +15,8 @@ public class People2Controller : ODataController public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/PeopleController.cs similarity index 76% rename from test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs rename to test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/PeopleController.cs index a82b31e4..48274b32 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Classic/PeopleController.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNetCore.OData.Advanced.Controllers +namespace Microsoft.AspNetCore.OData.Basic.Controllers.Classic { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; @@ -15,13 +15,13 @@ public class PeopleController : ODataController public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + [ODataRoute( "{key}" )] + public IActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); [MapToApiVersion( "2.0" )] - [ODataRoute( "({key})" )] - public IActionResult Patch( [FromODataUri] int key, Delta delta, ODataQueryOptions options ) + [ODataRoute( "{key}" )] + public IActionResult Patch( int key, Delta delta, ODataQueryOptions options ) { if ( !ModelState.IsValid ) { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/CustomersController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/CustomersController.cs new file mode 100644 index 00000000..a5a2e0fb --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/CustomersController.cs @@ -0,0 +1,39 @@ +namespace Microsoft.AspNetCore.OData.Basic.Controllers.Endpoint +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [Route( "api/[controller]" )] + public class CustomersController : ODataController + { + [HttpGet] + [ApiVersion( "2.0" )] + [ApiVersion( "3.0" )] + public IActionResult Get() => Ok(); + + [HttpGet( "{key}" )] + [ApiVersion( "1.0" )] + [ApiVersion( "2.0" )] + [ApiVersion( "3.0" )] + public IActionResult Get( int key ) => Ok(); + + [HttpPost] + [ApiVersion( "1.0" )] + [ApiVersion( "2.0" )] + [ApiVersion( "3.0" )] + public IActionResult Post( [FromBody] Customer customer ) + { + customer.Id = 42; + return Created( customer ); + } + + [HttpPut( "{key}" )] + [ApiVersion( "3.0" )] + public IActionResult Put( int key, [FromBody] Customer customer ) => NoContent(); + + [HttpDelete( "{key}" )] + [ApiVersionNeutral] + public IActionResult Delete( int key ) => NoContent(); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/OrdersController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/OrdersController.cs new file mode 100644 index 00000000..f75593ee --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/OrdersController.cs @@ -0,0 +1,20 @@ +namespace Microsoft.AspNetCore.OData.Basic.Controllers.Endpoint +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [ApiVersion( "1.0" )] + [Route( "api/[controller]" )] + public class OrdersController : ODataController + { + [HttpGet] + public IActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); + + [HttpGet( "{key}" )] + public IActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Order() { Id = key, Customer = "Bill Mei" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/People2Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/People2Controller.cs new file mode 100644 index 00000000..50da1ba5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/People2Controller.cs @@ -0,0 +1,21 @@ +namespace Microsoft.AspNetCore.OData.Basic.Controllers.Endpoint +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [ApiVersion( "3.0" )] + [ControllerName( "People" )] + [Route( "api/people" )] + public class People2Controller : ODataController + { + [HttpGet] + public IActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + [HttpGet( "{key}" )] + public IActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/PeopleController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/PeopleController.cs new file mode 100644 index 00000000..1de244f2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/Controllers/Endpoint/PeopleController.cs @@ -0,0 +1,37 @@ +namespace Microsoft.AspNetCore.OData.Basic.Controllers.Endpoint +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData.Models; + + [ApiVersion( "1.0" )] + [ApiVersion( "2.0" )] + [Route( "api/[controller]" )] + public class PeopleController : ODataController + { + [HttpGet] + public IActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + [HttpGet( "{key}" )] + public IActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + + [MapToApiVersion( "2.0" )] + [HttpPatch( "{key}" )] + public IActionResult Patch( int key, Delta delta, ODataQueryOptions options ) + { + if ( !ModelState.IsValid ) + { + return BadRequest( ModelState ); + } + + var person = new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" }; + + delta.Patch( person ); + + return Updated( person ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs index 44cd9ba1..fa3b2bcf 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs @@ -2,6 +2,7 @@ { using FluentAssertions; using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Basic; using System.Linq; using System.Net.Http; @@ -9,15 +10,17 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] + [Collection( nameof( BasicODataCollection ) )] public class when_using_a_query_string_and_split_into_two_types : BasicAcceptanceTest { [Theory] [InlineData( "api/people?api-version=1.0" )] - [InlineData( "api/people(42)?api-version=1.0" )] + [InlineData( "api/people/42?api-version=1.0" )] [InlineData( "api/people?api-version=2.0" )] - [InlineData( "api/people(42)?api-version=2.0" )] + [InlineData( "api/people/42?api-version=2.0" )] [InlineData( "api/people?api-version=3.0" )] - [InlineData( "api/people(42)?api-version=3.0" )] + [InlineData( "api/people/42?api-version=3.0" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -52,15 +55,15 @@ public async Task then_patch_should_return_204() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + var response = await PatchAsync( "api/people/42?api-version=2.0", person ); // assert response.StatusCode.Should().Be( NoContent ); } [Theory] - [InlineData( "api/people(42)?api-version=1.0" )] - [InlineData( "api/people(42)?api-version=3.0" )] + [InlineData( "api/people/42?api-version=1.0" )] + [InlineData( "api/people/42?api-version=3.0" )] public async Task then_patch_should_return_405_if_supported_in_any_version( string requestUrl ) { // arrange @@ -82,7 +85,7 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=4.0", person ); + var response = await PatchAsync( "api/people/42?api-version=4.0", person ); var content = await response.Content.ReadAsAsync(); // assert @@ -106,5 +109,14 @@ public async Task then_get_should_return_400_for_an_unspecified_version() } public when_using_a_query_string_and_split_into_two_types( BasicFixture fixture ) : base( fixture ) { } + + protected when_using_a_query_string_and_split_into_two_types( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( BasicODataEndpointCollection ) )] + public class when_using_a_query_string_and_split_into_two_types_ : when_using_a_query_string_and_split_into_two_types + { + public when_using_a_query_string_and_split_into_two_types_( BasicEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs index 8a0cae75..5dd0259f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs @@ -2,6 +2,7 @@ { using FluentAssertions; using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Basic; using System.Linq; using System.Net.Http; @@ -9,11 +10,13 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] + [Collection( nameof( BasicODataCollection ) )] public class when_using_a_query_string : BasicAcceptanceTest { [Theory] [InlineData( "api/orders?api-version=1.0" )] - [InlineData( "api/orders(42)?api-version=1.0" )] + [InlineData( "api/orders/42?api-version=1.0" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -57,5 +60,14 @@ public async Task then_get_should_return_400_for_an_unspecified_version() } public when_using_a_query_string( BasicFixture fixture ) : base( fixture ) { } + + protected when_using_a_query_string( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( BasicODataEndpointCollection ) )] + public class when_using_a_query_string_ : when_using_a_query_string + { + public when_using_a_query_string_( BasicEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment and split into two types.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment and split into two types.cs index 1d62678b..b6430c68 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment and split into two types.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment and split into two types.cs @@ -2,6 +2,7 @@ { using FluentAssertions; using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Basic; using System.Linq; using System.Net.Http; @@ -9,15 +10,17 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] + [Collection( nameof( BasicODataCollection ) )] public class when_using_a_url_segment_and_split_into_two_types : BasicAcceptanceTest { [Theory] [InlineData( "v1/people" )] - [InlineData( "v1/people(42)" )] + [InlineData( "v1/people/42" )] [InlineData( "v2/people" )] - [InlineData( "v2/people(42)" )] + [InlineData( "v2/people/42" )] [InlineData( "v3/people" )] - [InlineData( "v3/people(42)" )] + [InlineData( "v3/people/42" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -37,15 +40,15 @@ public async Task then_patch_should_return_204() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "v2/people(42)", person ); + var response = await PatchAsync( "v2/people/42", person ); // assert response.StatusCode.Should().Be( NoContent ); } [Theory] - [InlineData( "v1/people(42)" )] - [InlineData( "v3/people(42)" )] + [InlineData( "v1/people/42" )] + [InlineData( "v3/people/42" )] public async Task then_patch_should_return_405_if_supported_in_any_version( string requestUrl ) { // arrange @@ -67,7 +70,7 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "v4/people(42)", person ); + var response = await PatchAsync( "v4/people/42", person ); var content = await response.Content.ReadAsAsync(); // assert @@ -76,5 +79,14 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() } public when_using_a_url_segment_and_split_into_two_types( BasicFixture fixture ) : base( fixture ) { } + + protected when_using_a_url_segment_and_split_into_two_types( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( BasicODataEndpointCollection ) )] + public class when_using_a_url_segment_and_split_into_two_types_ : when_using_a_url_segment_and_split_into_two_types + { + public when_using_a_url_segment_and_split_into_two_types_( BasicEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment.cs index 8874ef16..84fc99a4 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a url segment.cs @@ -2,6 +2,7 @@ { using FluentAssertions; using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Basic; using System.Linq; using System.Net.Http; @@ -9,11 +10,13 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] + [Collection( nameof( BasicODataCollection ) )] public class when_using_a_url_segment : BasicAcceptanceTest { [Theory] [InlineData( "v1/orders" )] - [InlineData( "v1/orders(42)" )] + [InlineData( "v1/orders/42" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -42,5 +45,14 @@ public async Task then_get_should_return_400_for_an_unsupported_version() } public when_using_a_url_segment( BasicFixture fixture ) : base( fixture ) { } + + protected when_using_a_url_segment( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( BasicODataEndpointCollection ) )] + public class when_using_a_url_segment_ : when_using_a_url_segment + { + public when_using_a_url_segment_( BasicEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using an action.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using an action.cs index fe9dbb8a..202f2d14 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using an action.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using an action.cs @@ -1,17 +1,20 @@ namespace given_a_versioned_ODataController { using FluentAssertions; + using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Basic; using System.Threading.Tasks; using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] + [Collection( nameof( BasicODataCollection ) )] public class when_using_an_action : BasicAcceptanceTest { [Theory] - [InlineData( "api/customers(42)?api-version=1.0" )] - [InlineData( "api/customers(42)?api-version=2.0" )] - [InlineData( "api/customers(42)?api-version=3.0" )] + [InlineData( "api/customers/42?api-version=1.0" )] + [InlineData( "api/customers/42?api-version=2.0" )] + [InlineData( "api/customers/42?api-version=3.0" )] [InlineData( "api/customers?api-version=2.0" )] [InlineData( "api/customers?api-version=3.0" )] public async Task then_get_should_return_200( string requestUrl ) @@ -45,7 +48,7 @@ public async Task then_post_should_return_201( string requestUrl ) public async Task then_put_should_return_204() { // arrange - var requestUrl = "api/customers(42)?api-version=3.0"; + var requestUrl = "api/customers/42?api-version=3.0"; var customer = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act @@ -56,10 +59,10 @@ public async Task then_put_should_return_204() } [Theory] - [InlineData( "api/customers(42)" )] - [InlineData( "api/customers(42)?api-version=1.0" )] - [InlineData( "api/customers(42)?api-version=2.0" )] - [InlineData( "api/customers(42)?api-version=3.0" )] + [InlineData( "api/customers/42" )] + [InlineData( "api/customers/42?api-version=1.0" )] + [InlineData( "api/customers/42?api-version=2.0" )] + [InlineData( "api/customers/42?api-version=3.0" )] public async Task then_delete_should_return_204( string requestUrl ) { // arrange @@ -72,5 +75,14 @@ public async Task then_delete_should_return_204( string requestUrl ) } public when_using_an_action( BasicFixture fixture ) : base( fixture ) { } + + protected when_using_an_action( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( BasicODataEndpointCollection ) )] + public class when_using_an_action_ : when_using_an_action + { + public when_using_an_action_( BasicEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs index b74db2c3..dcfb3ac8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs @@ -22,7 +22,7 @@ EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) return customer; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { switch ( apiVersion.MajorVersion ) { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs index 30de57f3..ffab8e9c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs @@ -6,10 +6,9 @@ public class OrderModelConfiguration : IModelConfiguration { - static readonly ApiVersion V1 = new ApiVersion( 1, 0 ); readonly ApiVersion supportedApiVersion; - public OrderModelConfiguration() : this( V1 ) { } + public OrderModelConfiguration() { } public OrderModelConfiguration( ApiVersion supportedApiVersion ) => this.supportedApiVersion = supportedApiVersion; @@ -20,9 +19,9 @@ EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) return order; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { - if ( supportedApiVersion == apiVersion ) + if ( supportedApiVersion == null || supportedApiVersion == apiVersion ) { ConfigureCurrent( builder ); } diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs index d2d94d81..814da606 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs @@ -22,7 +22,7 @@ EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) return person; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { switch ( apiVersion.MajorVersion ) { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/CustomersController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/CustomersController.cs index b2338cf1..a03d1109 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/CustomersController.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/CustomersController.cs @@ -1,30 +1,23 @@ namespace Microsoft.AspNetCore.OData.Conventions.Controllers { using Microsoft.AspNet.OData; - using Microsoft.AspNet.OData.Routing; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Models; - [ODataRoutePrefix( "Customers" )] public class CustomersController : ODataController { - [ODataRoute] public IActionResult Get() => Ok(); - [ODataRoute( "({key})" )] - public IActionResult Get( [FromODataUri] int key ) => Ok(); + public IActionResult Get( int key ) => Ok(); - [ODataRoute] public IActionResult Post( [FromBody] Customer customer ) { customer.Id = 42; return Created( customer ); } - [ODataRoute( "({key})" )] - public IActionResult Put( [FromODataUri] int key, [FromBody] Customer customer ) => NoContent(); + public IActionResult Put( int key, [FromBody] Customer customer ) => NoContent(); - [ODataRoute( "({key})" )] - public IActionResult Delete( [FromODataUri] int key ) => NoContent(); + public IActionResult Delete( int key ) => NoContent(); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs index 4a210d85..deb2cd96 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs @@ -2,19 +2,15 @@ { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Models; - [ODataRoutePrefix( "Orders" )] public class OrdersController : ODataController { - [ODataRoute] public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); - [ODataRoute( "({key})" )] - public IActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + public IActionResult Get( int key, ODataQueryOptions options ) => Ok( new Order() { Id = key, Customer = "Bill Mei" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs index 52eb6e05..65d8dde5 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs @@ -2,20 +2,16 @@ { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Models; [ControllerName( "People" )] - [ODataRoutePrefix( "People" )] public class People2Controller : ODataController { - [ODataRoute] public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + public IActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs index dc6a8ab8..0b7dac86 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs @@ -2,24 +2,19 @@ { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Models; - [ODataRoutePrefix( "People" )] public class PeopleController : ODataController { - [ODataRoute] public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - [ODataRoute( "({key})" )] - public IActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + public IActionResult Get( int key, ODataQueryOptions options ) => Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); [MapToApiVersion( "2.0" )] - [ODataRoute( "({key})" )] - public IActionResult Patch( [FromODataUri] int key, Delta delta, ODataQueryOptions options ) + public IActionResult Patch( int key, Delta delta, ODataQueryOptions options ) { if ( !ModelState.IsValid ) { diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsEndpointFixture.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsEndpointFixture.cs new file mode 100644 index 00000000..88fd83a9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsEndpointFixture.cs @@ -0,0 +1,52 @@ +namespace Microsoft.AspNetCore.OData.Conventions +{ + using Microsoft.AspNet.OData.Builder; + using Microsoft.AspNet.OData.Extensions; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.AspNetCore.Mvc.Versioning.Conventions; + using Microsoft.AspNetCore.OData.Conventions.Controllers; + using Microsoft.AspNetCore.Routing; + using Microsoft.Extensions.DependencyInjection; + using System.Reflection; + + public class ConventionsEndpointFixture : ODataFixture + { + public ConventionsEndpointFixture() + { + EnableEndpointRouting = true; + FilteredControllerTypes.Add( typeof( OrdersController ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( PeopleController ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( People2Controller ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( CustomersController ).GetTypeInfo() ); + } + + protected override void OnAddApiVersioning( ApiVersioningOptions options ) + { + options.ReportApiVersions = true; + options.Conventions.Controller() + .HasApiVersion( 1, 0 ); + options.Conventions.Controller() + .HasApiVersion( 1, 0 ) + .HasApiVersion( 2, 0 ) + .Action( c => c.Patch( default, null, null ) ).MapToApiVersion( 2, 0 ); + options.Conventions.Controller() + .HasApiVersion( 3, 0 ); + options.Conventions.Controller() + .Action( c => c.Get() ).HasApiVersion( 2, 0 ).HasApiVersion( 3, 0 ) + .Action( c => c.Get( default ) ).HasApiVersion( 1, 0 ).HasApiVersion( 2, 0 ).HasApiVersion( 3, 0 ) + .Action( c => c.Post( default ) ).HasApiVersion( 1, 0 ).HasApiVersion( 2, 0 ).HasApiVersion( 3, 0 ) + .Action( c => c.Put( default, default ) ).HasApiVersion( 3, 0 ) + .Action( c => c.Delete( default ) ).IsApiVersionNeutral(); + } + + protected override void OnConfigureEndpoints( IEndpointRouteBuilder routeBuilder ) + { + var modelBuilder = routeBuilder.ServiceProvider.GetRequiredService(); + var models = modelBuilder.GetEdmModels(); + + routeBuilder.MapVersionedODataRoute( "odata", "api", models ); + routeBuilder.MapVersionedODataRoute( "odata-bypath", "v{version:apiVersion}", models ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsFixture.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsFixture.cs index b83ddd8e..5a577171 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsFixture.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsFixture.cs @@ -44,8 +44,8 @@ protected override void OnConfigureRoutes( IRouteBuilder routeBuilder ) var modelBuilder = routeBuilder.ServiceProvider.GetRequiredService(); var models = modelBuilder.GetEdmModels(); - routeBuilder.MapVersionedODataRoutes( "odata", "api", models ); - routeBuilder.MapVersionedODataRoutes( "odata-bypath", "v{version:apiVersion}", models ); + routeBuilder.MapVersionedODataRoute( "odata", "api", models ); + routeBuilder.MapVersionedODataRoute( "odata-bypath", "v{version:apiVersion}", models ); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsODataEndpointCollection.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsODataEndpointCollection.cs new file mode 100644 index 00000000..db6ec7f2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/ConventionsODataEndpointCollection.cs @@ -0,0 +1,10 @@ +namespace Microsoft.AspNetCore.OData.Conventions +{ + using System; + using Xunit; + + [CollectionDefinition( nameof( ConventionsODataEndpointCollection ) )] + public sealed class ConventionsODataEndpointCollection : ICollectionFixture + { + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs index 349c6afe..d959162e 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs @@ -10,16 +10,17 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( ConventionsODataCollection ) )] public class when_using_a_query_string_and_split_into_two_types : ODataAcceptanceTest { [Theory] [InlineData( "api/people?api-version=1.0" )] - [InlineData( "api/people(42)?api-version=1.0" )] + [InlineData( "api/people/42?api-version=1.0" )] [InlineData( "api/people?api-version=2.0" )] - [InlineData( "api/people(42)?api-version=2.0" )] + [InlineData( "api/people/42?api-version=2.0" )] [InlineData( "api/people?api-version=3.0" )] - [InlineData( "api/people(42)?api-version=3.0" )] + [InlineData( "api/people/42?api-version=3.0" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -54,15 +55,15 @@ public async Task then_patch_should_return_204() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + var response = await PatchAsync( "api/people/42?api-version=2.0", person ); // assert response.StatusCode.Should().Be( NoContent ); } [Theory] - [InlineData( "api/people(42)?api-version=1.0" )] - [InlineData( "api/people(42)?api-version=3.0" )] + [InlineData( "api/people/42?api-version=1.0" )] + [InlineData( "api/people/42?api-version=3.0" )] public async Task then_patch_should_return_405_if_supported_in_any_version( string requestUrl ) { // arrange @@ -84,7 +85,7 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "api/people(42)?api-version=4.0", person ); + var response = await PatchAsync( "api/people/42?api-version=4.0", person ); var content = await response.Content.ReadAsAsync(); // assert @@ -108,5 +109,14 @@ public async Task then_get_should_return_400_for_an_unspecified_version() } public when_using_a_query_string_and_split_into_two_types( ConventionsFixture fixture ) : base( fixture ) { } + + protected when_using_a_query_string_and_split_into_two_types( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( ConventionsODataEndpointCollection ) )] + public class when_using_a_query_string_and_split_into_two_types_ : when_using_a_query_string_and_split_into_two_types + { + public when_using_a_query_string_and_split_into_two_types_( ConventionsEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string.cs index a8742f4a..03429d92 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a query string.cs @@ -10,12 +10,13 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( ConventionsODataCollection ) )] public class when_using_a_query_string : ODataAcceptanceTest { [Theory] [InlineData( "api/orders?api-version=1.0" )] - [InlineData( "api/orders(42)?api-version=1.0" )] + [InlineData( "api/orders/42?api-version=1.0" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -59,5 +60,14 @@ public async Task then_get_should_return_400_for_an_unspecified_version() } public when_using_a_query_string( ConventionsFixture fixture ) : base( fixture ) { } + + protected when_using_a_query_string( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( ConventionsODataEndpointCollection ) )] + public class when_using_a_query_string_ : when_using_a_query_string + { + public when_using_a_query_string_( ConventionsEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment and split into two types.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment and split into two types.cs index 1f293e62..454db441 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment and split into two types.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment and split into two types.cs @@ -10,16 +10,17 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( ConventionsODataCollection ) )] public class when_using_a_url_segment_and_split_into_two_types : ODataAcceptanceTest { [Theory] [InlineData( "v1/people" )] - [InlineData( "v1/people(42)" )] + [InlineData( "v1/people/42" )] [InlineData( "v2/people" )] - [InlineData( "v2/people(42)" )] + [InlineData( "v2/people/42" )] [InlineData( "v3/people" )] - [InlineData( "v3/people(42)" )] + [InlineData( "v3/people/42" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -39,15 +40,15 @@ public async Task then_patch_should_return_204() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "v2/people(42)", person ); + var response = await PatchAsync( "v2/people/42", person ); // assert response.StatusCode.Should().Be( NoContent ); } [Theory] - [InlineData( "v1/people(42)" )] - [InlineData( "v3/people(42)" )] + [InlineData( "v1/people/42" )] + [InlineData( "v3/people/42" )] public async Task then_patch_should_return_405_if_supported_in_any_version( string requestUrl ) { // arrange @@ -69,7 +70,7 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act - var response = await PatchAsync( "v4/people(42)", person ); + var response = await PatchAsync( "v4/people/42", person ); var content = await response.Content.ReadAsAsync(); // assert @@ -78,5 +79,14 @@ public async Task then_patch_should_return_400_for_an_unsupported_version() } public when_using_a_url_segment_and_split_into_two_types( ConventionsFixture fixture ) : base( fixture ) { } + + protected when_using_a_url_segment_and_split_into_two_types( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( ConventionsODataEndpointCollection ) )] + public class when_using_a_url_segment_and_split_into_two_types_ : when_using_a_url_segment_and_split_into_two_types + { + public when_using_a_url_segment_and_split_into_two_types_( ConventionsEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment.cs index 21aeb46b..619033d1 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using a url segment.cs @@ -10,12 +10,13 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( ConventionsODataCollection ) )] public class when_using_a_url_segment : ODataAcceptanceTest { [Theory] [InlineData( "v1/orders" )] - [InlineData( "v1/orders(42)" )] + [InlineData( "v1/orders/42" )] public async Task then_get_should_return_200( string requestUrl ) { // arrange @@ -44,5 +45,14 @@ public async Task then_get_should_return_400_for_an_unsupported_version() } public when_using_a_url_segment( ConventionsFixture fixture ) : base( fixture ) { } + + protected when_using_a_url_segment( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( ConventionsODataEndpointCollection ) )] + public class when_using_a_url_segment_ : when_using_a_url_segment + { + public when_using_a_url_segment_( ConventionsEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using an action.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using an action.cs index 80db7ed1..4d67d940 100644 --- a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using an action.cs +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OData/Conventions/given a versioned ODataController using conventions/when using an action.cs @@ -7,13 +7,14 @@ using Xunit; using static System.Net.HttpStatusCode; + [Trait( "Routing", "Classic" )] [Collection( nameof( ConventionsODataCollection ) )] public class when_using_an_action : ODataAcceptanceTest { [Theory] - [InlineData( "api/customers(42)?api-version=1.0" )] - [InlineData( "api/customers(42)?api-version=2.0" )] - [InlineData( "api/customers(42)?api-version=3.0" )] + [InlineData( "api/customers/42?api-version=1.0" )] + [InlineData( "api/customers/42?api-version=2.0" )] + [InlineData( "api/customers/42?api-version=3.0" )] [InlineData( "api/customers?api-version=2.0" )] [InlineData( "api/customers?api-version=3.0" )] public async Task then_get_should_return_200( string requestUrl ) @@ -47,7 +48,7 @@ public async Task then_post_should_return_201( string requestUrl ) public async Task then_put_should_return_204() { // arrange - var requestUrl = "api/customers(42)?api-version=3.0"; + var requestUrl = "api/customers/42?api-version=3.0"; var customer = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; // act @@ -58,10 +59,10 @@ public async Task then_put_should_return_204() } [Theory] - [InlineData( "api/customers(42)" )] - [InlineData( "api/customers(42)?api-version=1.0" )] - [InlineData( "api/customers(42)?api-version=2.0" )] - [InlineData( "api/customers(42)?api-version=3.0" )] + [InlineData( "api/customers/42" )] + [InlineData( "api/customers/42?api-version=1.0" )] + [InlineData( "api/customers/42?api-version=2.0" )] + [InlineData( "api/customers/42?api-version=3.0" )] public async Task then_delete_should_return_204( string requestUrl ) { // arrange @@ -74,5 +75,14 @@ public async Task then_delete_should_return_204( string requestUrl ) } public when_using_an_action( ConventionsFixture fixture ) : base( fixture ) { } + + protected when_using_an_action( ODataFixture fixture ) : base( fixture ) { } + } + + [Trait( "Routing", "Endpoint" )] + [Collection( nameof( ConventionsODataEndpointCollection ) )] + public class when_using_an_action_ : when_using_an_action + { + public when_using_an_action_( ConventionsEndpointFixture fixture ) : base( fixture ) { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/AllConfigurations.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/AllConfigurations.cs index 3102e142..11cc6030 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/AllConfigurations.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/AllConfigurations.cs @@ -8,12 +8,8 @@ /// public class AllConfigurations : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { builder.Function( "GetSalesTaxRate" ).Returns().Parameter( "PostalCode" ); } diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/OrderModelConfiguration.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/OrderModelConfiguration.cs index 87ac2c25..348cbf81 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/OrderModelConfiguration.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/OrderModelConfiguration.cs @@ -9,12 +9,8 @@ /// public class OrderModelConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { var order = builder.EntitySet( "Orders" ).EntityType.HasKey( o => o.Id ); diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/PersonModelConfiguration.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/PersonModelConfiguration.cs index 13f7fbe2..2a392aba 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/PersonModelConfiguration.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/PersonModelConfiguration.cs @@ -10,12 +10,8 @@ /// public class PersonModelConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { var person = builder.EntitySet( "People" ).EntityType.HasKey( p => p.Id ); diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/ProductConfiguration.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/ProductConfiguration.cs index 108b9492..c22e9daf 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/ProductConfiguration.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/ProductConfiguration.cs @@ -10,12 +10,8 @@ /// public class ProductConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { if ( apiVersion < ApiVersions.V3 ) { diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/SupplierConfiguration.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/SupplierConfiguration.cs index 1744e697..207a7a2e 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/SupplierConfiguration.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/Configuration/SupplierConfiguration.cs @@ -10,12 +10,8 @@ /// public class SupplierConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { if ( apiVersion < ApiVersions.V3 ) { diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V1/OrdersController.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V1/OrdersController.cs index c8ac5086..d5f92882 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V1/OrdersController.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V1/OrdersController.cs @@ -24,7 +24,7 @@ public class OrdersController : ODataController [Produces( "application/json" )] [ProducesResponseType( typeof( Order ), Status200OK )] [ProducesResponseType( Status404NotFound )] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] public IActionResult Get( int key ) => Ok( new Order() { Id = key, Customer = "John Doe" } ); /// diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V2/OrdersController.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V2/OrdersController.cs index 2a586561..434d5452 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V2/OrdersController.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V2/OrdersController.cs @@ -45,7 +45,7 @@ public IActionResult Get() [Produces( "application/json" )] [ProducesResponseType( typeof( Order ), Status200OK )] [ProducesResponseType( Status404NotFound )] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] public IActionResult Get( int key ) => Ok( new Order() { Id = key, Customer = "John Doe" } ); /// @@ -81,7 +81,7 @@ public IActionResult Post( [FromBody] Order order ) [ProducesResponseType( typeof( Order ), Status204NoContent )] [ProducesResponseType( Status400BadRequest )] [ProducesResponseType( Status404NotFound )] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] public IActionResult Patch( int key, Delta delta ) { if ( !ModelState.IsValid ) @@ -119,7 +119,7 @@ public IActionResult Patch( int key, Delta delta ) [ProducesResponseType( Status200OK )] [ProducesResponseType( Status400BadRequest )] [ProducesResponseType( Status404NotFound )] - [ODataRoute( "({key})/Rate" )] + [ODataRoute( "{key}/Rate" )] public IActionResult Rate( int key, ODataActionParameters parameters ) { if ( !ModelState.IsValid ) diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V3/OrdersController.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V3/OrdersController.cs index 7717c3aa..8ace50f8 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V3/OrdersController.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V3/OrdersController.cs @@ -45,7 +45,7 @@ public IActionResult Get() [Produces( "application/json" )] [ProducesResponseType( typeof( Order ), Status200OK )] [ProducesResponseType( Status404NotFound )] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] public IActionResult Get( int key ) => Ok( new Order() { Id = key, Customer = "John Doe" } ); /// @@ -80,7 +80,7 @@ public IActionResult Post( [FromBody] Order order ) [ProducesResponseType( typeof( Order ), Status204NoContent )] [ProducesResponseType( Status400BadRequest )] [ProducesResponseType( Status404NotFound )] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] public IActionResult Patch( int key, Delta delta ) { if ( !ModelState.IsValid ) @@ -104,7 +104,7 @@ public IActionResult Patch( int key, Delta delta ) /// The order was successfully canceled. [ProducesResponseType( Status204NoContent )] [ProducesResponseType( Status404NotFound )] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] public IActionResult Delete( int key, bool suspendOnly ) => NoContent(); /// @@ -130,7 +130,7 @@ public IActionResult Patch( int key, Delta delta ) [ProducesResponseType( Status200OK )] [ProducesResponseType( Status400BadRequest )] [ProducesResponseType( Status404NotFound )] - [ODataRoute( "({key})/Rate" )] + [ODataRoute( "{key}/Rate" )] public IActionResult Rate( int key, ODataActionParameters parameters ) { if ( !ModelState.IsValid ) diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProviderTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProviderTest.cs index e29770f5..1345c84b 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProviderTest.cs @@ -29,7 +29,7 @@ public void odata_api_explorer_should_group_and_order_descriptions_on_providers_ app => { var modelBuilder = app.ApplicationServices.GetRequiredService(); - app.UseMvc( routeBuilder => routeBuilder.MapVersionedODataRoutes( "odata", "api", modelBuilder.GetEdmModels() ) ); + app.UseMvc( rb => rb.MapVersionedODataRoute( "odata", "api", modelBuilder.GetEdmModels() ) ); } ); var server = new TestServer( builder ); var serviceProvider = server.Host.Services; @@ -58,7 +58,7 @@ private void AssertVersion0_9( ApiDescriptionGroup group ) new[] { new { HttpMethod = "GET", GroupName, RelativePath = "api/GetSalesTaxRate(PostalCode={postalCode})" }, - new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders({key})" }, + new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/{key}" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" }, }, options => options.ExcludingMissingMembers() ); @@ -73,7 +73,7 @@ private void AssertVersion1( ApiDescriptionGroup group ) new[] { new { HttpMethod = "GET", GroupName, RelativePath = "api/GetSalesTaxRate(PostalCode={postalCode})" }, - new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders({key})" }, + new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/{key}" }, new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/MostExpensive" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" }, @@ -91,11 +91,11 @@ private void AssertVersion2( ApiDescriptionGroup group ) { new { HttpMethod = "GET", GroupName, RelativePath = "api/GetSalesTaxRate(PostalCode={postalCode})" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders" }, - new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders({key})" }, + new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/{key}" }, new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders" }, - new { HttpMethod = "PATCH", GroupName, RelativePath = "api/Orders({key})" }, + new { HttpMethod = "PATCH", GroupName, RelativePath = "api/Orders/{key}" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/MostExpensive" }, - new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders({key})/Rate" }, + new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders/{key}/Rate" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/People" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/People/NewHires(Since={since})" }, @@ -113,12 +113,12 @@ private void AssertVersion3( ApiDescriptionGroup group ) { new { HttpMethod = "GET", GroupName, RelativePath = "api/GetSalesTaxRate(PostalCode={postalCode})" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders" }, - new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders({key})" }, + new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/{key}" }, new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders" }, - new { HttpMethod = "PATCH", GroupName, RelativePath = "api/Orders({key})" }, - new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Orders({key})?suspendOnly={suspendOnly}" }, + new { HttpMethod = "PATCH", GroupName, RelativePath = "api/Orders/{key}" }, + new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Orders/{key}?suspendOnly={suspendOnly}" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/MostExpensive" }, - new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders({key})/Rate" }, + new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders/{key}/Rate" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/People" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" }, new { HttpMethod = "POST", GroupName, RelativePath = "api/People" }, diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Builder/ModelConfigurationFeatureProviderTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Builder/ModelConfigurationFeatureProviderTest.cs index 65e5acb7..84a3273b 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Builder/ModelConfigurationFeatureProviderTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Builder/ModelConfigurationFeatureProviderTest.cs @@ -37,27 +37,27 @@ namespace ModelConfigurations { struct ValueTypeModelConfiguration : IModelConfiguration { - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) { } + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } } class PrivateModelConfiguration : IModelConfiguration { - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) { } + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } } public abstract class AbstractModelConfiguration : IModelConfiguration { - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) { } + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } } public class GenericModelConfiguration : IModelConfiguration { - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) { } + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } } public class PublicModelConfiguration : IModelConfiguration { - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) { } + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Builder/VersionedODataModelBuilderTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Builder/VersionedODataModelBuilderTest.cs index 7b7c7d6b..01c5e4bd 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Builder/VersionedODataModelBuilderTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Builder/VersionedODataModelBuilderTest.cs @@ -21,14 +21,13 @@ public class VersionedODataModelBuilderTest public void get_edm_models_should_return_expected_results() { // arrange - var actionDescriptorCollectionProvider = NewActionDescriptorCollectionProvider(); var apiVersion = new ApiVersion( 1, 0 ); + var actionDescriptorCollectionProvider = NewActionDescriptorCollectionProvider( new[] { apiVersion } ); var options = Options.Create( new ApiVersioningOptions() { DefaultApiVersion = apiVersion } ); - var defaultConfiguration = new Mock>(); var modelCreated = new Mock>(); var builder = new VersionedODataModelBuilder( actionDescriptorCollectionProvider, options ) { - DefaultModelConfiguration = defaultConfiguration.Object, + DefaultModelConfiguration = ( b, v, r ) => b.EntitySet( "Tests" ), OnModelCreated = modelCreated.Object }; @@ -37,24 +36,53 @@ public void get_edm_models_should_return_expected_results() // assert model.GetAnnotationValue( model ).ApiVersion.Should().Be( apiVersion ); - defaultConfiguration.Verify( f => f( It.IsAny(), apiVersion ), Once() ); modelCreated.Verify( f => f( It.IsAny(), model ), Once() ); } - static IActionDescriptorCollectionProvider NewActionDescriptorCollectionProvider() + [Fact] + public void get_edm_models_should_split_models_between_routes() { - var provider = new Mock(); - var items = new[] + // arrange + var apiVersion = new ApiVersion( 1, 0 ); + var actionDescriptorCollectionProvider = NewActionDescriptorCollectionProvider( new[] { apiVersion, new ApiVersion( 2, 0 ) } ); + var options = Options.Create( new ApiVersioningOptions() { DefaultApiVersion = apiVersion } ); + var modelCreated = new Mock>(); + var builder = new VersionedODataModelBuilder( actionDescriptorCollectionProvider, options ) { - new ControllerActionDescriptor() + DefaultModelConfiguration = ( builder, version, prefix ) => { - ControllerTypeInfo = typeof( ODataController ).GetTypeInfo(), - Properties = new Dictionary() + if ( prefix == "api2" ) { - [typeof( ApiVersionModel )] = new ApiVersionModel( new ApiVersion( 1, 0 ) ), - }, + builder.EntitySet( "Tests" ); + } }, }; + + // act + var models = builder.GetEdmModels( "api2" ); + + // assert + models.Should().HaveCount( 2 ); + models.ElementAt( 1 ).FindDeclaredEntitySet( "Tests" ).Should().NotBeNull(); + } + + static IActionDescriptorCollectionProvider NewActionDescriptorCollectionProvider( IReadOnlyList versions ) + { + var provider = new Mock(); + var items = new List( capacity: versions.Count ); + + for ( var i = 0; i < versions.Count; i++ ) + { + items.Add( + new ControllerActionDescriptor() + { + ControllerTypeInfo = typeof( ODataController ).GetTypeInfo(), + Properties = new Dictionary() + { + [typeof( ApiVersionModel )] = new ApiVersionModel( versions[i] ), + }, + } ); + } var collection = new ActionDescriptorCollection( items, 0 ); provider.SetupGet( p => p.ActionDescriptors ).Returns( collection ); diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Extensions/IRouteBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Extensions/IRouteBuilderExtensionsTest.cs index e5e23f28..367ece3c 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Extensions/IRouteBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Extensions/IRouteBuilderExtensionsTest.cs @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; - using Microsoft.Extensions.Options; + using Microsoft.OData.Edm; using Microsoft.Simulators; using System.Collections.Generic; using System.Diagnostics; @@ -19,83 +19,49 @@ public class IRouteBuilderExtensionsTest { - [Fact] - public void map_versioned_odata_route_should_return_expected_result() + [Theory] + [InlineData( null )] + [InlineData( "api" )] + [InlineData( "v{version:apiVersion}" )] + public void map_versioned_odata_route_should_return_expected_result( string routePrefix ) { // arrange var routeName = "odata"; - var routePrefix = "api/v3"; - var modelBuilder = new ODataModelBuilder(); - var modelConfiguration = new TestModelConfiguration(); - var apiVersion = new ApiVersion( 3, 0 ); - var batchHandler = new DefaultODataBatchHandler(); var app = NewApplicationBuilder(); var route = default( ODataRoute ); + var perRequestContainer = app.ApplicationServices.GetRequiredService(); + var modelBuilder = app.ApplicationServices.GetRequiredService(); - modelConfiguration.Apply( modelBuilder, apiVersion ); + modelBuilder.ModelConfigurations.Add( new TestModelConfiguration() ); - var model = modelBuilder.GetEdmModel(); + var models = modelBuilder.GetEdmModels(); // act - app.UseMvc( r => route = r.MapVersionedODataRoute( routeName, routePrefix, model, apiVersion, batchHandler ) ); - - var perRequestContainer = app.ApplicationServices.GetRequiredService(); - var serviceProvider = perRequestContainer.GetODataRootContainer( route.Name ); - var routingConventions = serviceProvider.GetRequiredService>().ToArray(); - var constraint = (VersionedODataPathRouteConstraint) route.PathRouteConstraint; + app.UseMvc( rb => route = rb.MapVersionedODataRoute( routeName, routePrefix, modelBuilder.GetEdmModels(), new DefaultODataBatchHandler() ) ); // assert + var rootContainer = perRequestContainer.GetODataRootContainer( routeName ); + var selector = rootContainer.GetRequiredService(); + var routingConventions = rootContainer.GetRequiredService>().ToArray(); + var batchHandler = perRequestContainer.GetODataRootContainer( routeName ).GetRequiredService(); + + selector.ApiVersions.Should().Equal( + new[] + { + new ApiVersion( 1, 0 ), + new ApiVersion( 2, 0 ), + new ApiVersion( 3, 0, "Beta" ), + new ApiVersion( 3, 0 ) + } ); routingConventions[0].Should().BeOfType(); routingConventions[1].Should().BeOfType(); routingConventions.OfType().Should().BeEmpty(); - constraint.RouteName.Should().Be( routeName ); + route.PathRouteConstraint.RouteName.Should().Be( routeName ); route.RoutePrefix.Should().Be( routePrefix ); batchHandler.ODataRoute.Should().NotBeNull(); batchHandler.ODataRouteName.Should().Be( routeName ); } - [Fact] - public void map_versioned_odata_routes_should_return_expected_results() - { - // arrange - var routeName = "odata"; - var routePrefix = "api"; - var app = NewApplicationBuilder(); - var routes = default( IReadOnlyList ); - var perRequestContainer = app.ApplicationServices.GetRequiredService(); - var modelBuilder = app.ApplicationServices.GetRequiredService(); - - modelBuilder.ModelConfigurations.Add( new TestModelConfiguration() ); - - var models = modelBuilder.GetEdmModels(); - - // act - app.UseMvc( r => routes = r.MapVersionedODataRoutes( routeName, routePrefix, models, () => new DefaultODataBatchHandler() ) ); - - // assert - foreach ( var route in routes ) - { - if ( !( route.PathRouteConstraint is VersionedODataPathRouteConstraint constraint ) ) - { - continue; - } - - var apiVersion = constraint.ApiVersion; - var versionedRouteName = routeName + "-" + apiVersion.ToString(); - var rootContainer = perRequestContainer.GetODataRootContainer( versionedRouteName ); - var routingConventions = rootContainer.GetRequiredService>().ToArray(); - var batchHandler = perRequestContainer.GetODataRootContainer( versionedRouteName ).GetRequiredService(); - - routingConventions[0].Should().BeOfType(); - routingConventions[1].Should().BeOfType(); - routingConventions.OfType().Should().BeEmpty(); - constraint.RouteName.Should().Be( versionedRouteName ); - route.RoutePrefix.Should().Be( routePrefix ); - batchHandler.ODataRoute.Should().NotBeNull(); - batchHandler.ODataRouteName.Should().Be( versionedRouteName ); - } - } - static ApplicationBuilder NewApplicationBuilder() { var services = new ServiceCollection(); diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedAttributeRoutingConventionTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedAttributeRoutingConventionTest.cs index d0751e15..67f3b7f4 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedAttributeRoutingConventionTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedAttributeRoutingConventionTest.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; + using Microsoft.Extensions.Primitives; using Microsoft.Simulators; using Moq; using System; @@ -34,9 +35,9 @@ public class VersionedAttributeRoutingConventionTest public void select_action_should_return_true_for_versionX2Dneutral_controller() { // arrange - var routeContext = NewRouteContext( "http://localhost/NeutralTests(1)", typeof( VersionNeutralController ) ); + var routeContext = NewRouteContext( "http://localhost/NeutralTests/1", typeof( VersionNeutralController ) ); var serviceProvider = routeContext.HttpContext.RequestServices; - var convention = NewRoutingConvention( serviceProvider, new ApiVersion( 1, 0 ) ); + var convention = new VersionedAttributeRoutingConvention( "odata", serviceProvider ); // act var result = convention.SelectAction( routeContext ); @@ -51,9 +52,10 @@ public void select_action_should_return_true_for_versionX2Dneutral_controller() public void select_action_should_return_expected_result_for_controller_version( int majorVersion, string expected ) { // arrange - var routeContext = NewRouteContext( "http://localhost/Tests(1)?api-version=1.0", typeof( TestsController ) ); + var apiVersion = majorVersion + ".0"; + var routeContext = NewRouteContext( $"http://localhost/Tests(1)?api-version={apiVersion}", typeof( TestsController ), apiVersion ); var serviceProvider = routeContext.HttpContext.RequestServices; - var convention = NewRoutingConvention( serviceProvider, new ApiVersion( majorVersion, 0 ) ); + var convention = new VersionedAttributeRoutingConvention( "odata", serviceProvider ); // act var actionName = convention.SelectAction( routeContext )?.SingleOrDefault()?.ActionName; @@ -62,9 +64,6 @@ public void select_action_should_return_expected_result_for_controller_version( actionName.Should().Be( expected ); } - static VersionedAttributeRoutingConvention NewRoutingConvention( IServiceProvider serviceProvider, ApiVersion apiVersion ) => - new VersionedAttributeRoutingConvention( "odata", serviceProvider, new DefaultODataPathHandler(), apiVersion ); - static IActionDescriptorCollectionProvider NewActionDescriptorProvider( MethodInfo method ) { var controllerType = method.DeclaringType.GetTypeInfo(); @@ -89,9 +88,10 @@ static IActionDescriptorCollectionProvider NewActionDescriptorProvider( MethodIn return provider.Object; } - static RouteContext NewRouteContext( string requestUri, Type controllerType ) + static RouteContext NewRouteContext( string requestUri, Type controllerType, string apiVersion = default ) { var url = new Uri( requestUri ); + var store = new Dictionary( capacity: 1 ); var features = new Mock(); var odataFeature = Mock.Of(); var entitySet = Test.Model.EntityContainer.FindEntitySet( "Tests" ); @@ -99,6 +99,11 @@ static RouteContext NewRouteContext( string requestUri, Type controllerType ) var httpContext = new Mock(); var services = new ServiceCollection(); + if ( !string.IsNullOrEmpty( apiVersion ) ) + { + store["api-version"] = new StringValues( apiVersion ); + } + services.AddLogging(); services.Add( Singleton( new DiagnosticListener( "test" ) ) ); services.AddMvcCore( options => options.EnableEndpointRouting = false ); @@ -111,18 +116,24 @@ static RouteContext NewRouteContext( string requestUri, Type controllerType ) var modelBuilder = serviceProvider.GetRequiredService(); modelBuilder.ModelConfigurations.Add( new TestModelConfiguration() ); - app.UseMvc( rb => rb.MapVersionedODataRoute( "odata", null, modelBuilder.GetEdmModels().First(), new ApiVersion( 1, 0 ) ) ); + app.UseMvc( rb => rb.MapVersionedODataRoute( "odata", null, modelBuilder ) ); + + var rootContainer = serviceProvider.GetRequiredService().GetODataRootContainer( "odata" ); + odataFeature.Path = new DefaultODataPathHandler().Parse( url.GetLeftPart( UriPartial.Authority ), url.GetComponents( Path, Unescaped ), - serviceProvider.GetRequiredService().GetODataRootContainer( "odata" ) ); + rootContainer ); odataFeature.RoutingConventionsStore = new Dictionary(); + odataFeature.RequestContainer = rootContainer; features.SetupGet( f => f[typeof( IODataFeature )] ).Returns( odataFeature ); features.Setup( f => f.Get() ).Returns( odataFeature ); + features.Setup( f => f.Get() ).Returns( () => new ApiVersioningFeature( httpContext.Object ) ); httpContext.SetupGet( c => c.Features ).Returns( features.Object ); httpContext.SetupProperty( c => c.RequestServices, serviceProvider ); httpContext.SetupGet( c => c.Request ).Returns( () => httpRequest.Object ); httpRequest.SetupGet( r => r.HttpContext ).Returns( () => httpContext.Object ); + httpRequest.SetupProperty( r => r.Query, new QueryCollection( store ) ); httpRequest.SetupProperty( r => r.Method, "GET" ); httpRequest.SetupProperty( r => r.Protocol, url.Scheme ); httpRequest.SetupProperty( r => r.Host, new HostString( url.Host ) ); diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedMetadataRoutingConventionTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedMetadataRoutingConventionTest.cs index 7e5ef46b..0a7e1e12 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedMetadataRoutingConventionTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedMetadataRoutingConventionTest.cs @@ -4,9 +4,11 @@ using Microsoft.AspNet.OData.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; + using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; + using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.AspNetCore.Routing; using Microsoft.OData; using Microsoft.OData.Edm; @@ -29,7 +31,7 @@ public void select_action_should_return_expected_name( string requestUrl, string var features = new Mock(); var actionDescriptorCollectionProvider = new Mock(); var serviceProvider = new Mock(); - var request = Mock.Of(); + var request = new Mock(); var httpContext = new Mock(); var routingConvention = new VersionedMetadataRoutingConvention(); var items = new ActionDescriptor[] @@ -40,13 +42,34 @@ public void select_action_should_return_expected_name( string requestUrl, string }; feature.SetupProperty( f => f.Path, odataPath ); + feature.SetupGet( f => f.RequestContainer ).Returns( () => + { + var sp = new Mock(); + var selector = new Mock(); + + selector.SetupGet( s => s.ApiVersions ).Returns( new[] { ApiVersion.Default } ); + sp.Setup( sp => sp.GetService( typeof( IEdmModelSelector ) ) ).Returns( selector.Object ); + + return sp.Object; + } ); + features.Setup( f => f.Get() ).Returns( () => new ApiVersioningFeature( httpContext.Object ) ); features.Setup( f => f.Get() ).Returns( feature.Object ); actionDescriptorCollectionProvider.SetupGet( p => p.ActionDescriptors ).Returns( new ActionDescriptorCollection( items, 0 ) ); serviceProvider.Setup( sp => sp.GetService( typeof( IActionDescriptorCollectionProvider ) ) ).Returns( actionDescriptorCollectionProvider.Object ); - request.Method = verb; + serviceProvider.Setup( sp => sp.GetService( typeof( IApiVersionSelector ) ) ).Returns( Mock.Of ); + serviceProvider.Setup( sp => sp.GetService( typeof( IODataRouteCollectionProvider ) ) ) + .Returns( () => + { + var provider = new Mock(); + provider.SetupGet( p => p.Items ).Returns( Mock.Of ); + return provider.Object; + } ); + request.SetupProperty( r => r.Method, verb ); + request.SetupGet( r => r.HttpContext ).Returns( () => httpContext.Object ); + request.SetupProperty( r => r.Query, Mock.Of() ); httpContext.SetupGet( c => c.Features ).Returns( features.Object ); httpContext.SetupProperty( c => c.RequestServices, serviceProvider.Object ); - httpContext.SetupGet( c => c.Request ).Returns( request ); + httpContext.SetupGet( c => c.Request ).Returns( request.Object ); // act var actionName = routingConvention.SelectAction( new RouteContext( httpContext.Object ) )?.SingleOrDefault()?.ActionName; @@ -77,7 +100,7 @@ public static IEnumerable SelectActionData yield return new object[] { "$metadata", "GET", "GetMetadata" }; yield return new object[] { "$metadata", "OPTIONS", "GetOptions" }; yield return new object[] { "Tests", "GET", null }; - yield return new object[] { "Tests(42)", "GET", null }; + yield return new object[] { "Tests/42", "GET", null }; } } } diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedODataPathRouteConstraintTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedODataPathRouteConstraintTest.cs deleted file mode 100644 index a9c4291d..00000000 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/Routing/VersionedODataPathRouteConstraintTest.cs +++ /dev/null @@ -1,212 +0,0 @@ -namespace Microsoft.AspNet.OData.Routing -{ - using FluentAssertions; - using Microsoft.AspNet.OData.Builder; - using Microsoft.AspNet.OData.Extensions; - using Microsoft.AspNet.OData.Interfaces; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Http.Features; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.Versioning; - using Microsoft.AspNetCore.Routing; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; - using Microsoft.Extensions.Options; - using Microsoft.Extensions.Primitives; - using Microsoft.OData; - using Microsoft.Simulators; - using Moq; - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using Xunit; - using static Microsoft.AspNetCore.Mvc.ApiVersion; - using static Microsoft.AspNetCore.Routing.RouteDirection; - using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; - using static System.UriComponents; - using static System.UriFormat; - - public class VersionedODataPathRouteConstraintTest - { - [Fact] - public void match_should_always_return_true_for_uri_resolution() - { - // arrange - var url = new Uri( "http://localhost" ); - var httpContext = NewHttpContext( url, "1.0" ); - var route = Mock.Of(); - var routeKey = (string) null; - var values = new RouteValueDictionary(); - var constraint = new VersionedODataPathRouteConstraint( "odata", Default ); - - // act - var result = constraint.Match( httpContext, route, routeKey, values, UrlGeneration ); - - // assert - result.Should().BeTrue(); - } - - [Theory] - [InlineData( "2.0" )] - [InlineData( "3.0" )] - public void match_should_be_true_when_api_version_is_requested_in_query_string( string apiVersionValue ) - { - // arrange - var url = new Uri( "http://localhost/Tests(1)?api-version=" + apiVersionValue ); - var apiVersion = Parse( apiVersionValue ); - var context = NewHttpContext( url, apiVersionValue ); - var route = Mock.Of(); - var routeKey = (string) null; - var values = new RouteValueDictionary() { ["odataPath"] = "Tests(1)" }; - var constraint = new VersionedODataPathRouteConstraint( "odata", apiVersion ); - - // act - var result = constraint.Match( context, route, routeKey, values, IncomingRequest ); - - // assert - result.Should().BeTrue(); - } - - [Theory] - [InlineData( "http://localhost", null, "1.0", true )] - [InlineData( "http://localhost", null, "2.0", false )] - [InlineData( "http://localhost/$metadata", "$metadata", "1.0", true )] - [InlineData( "http://localhost/$metadata", "$metadata", "2.0", false )] - public void match_should_return_expected_result_for_service_and_metadata_document( string requestUri, string odataPath, string apiVersionValue, bool expected ) - { - // arrange - const string rawApiVersion = default; - var url = new Uri( requestUri ); - var apiVersion = Parse( apiVersionValue ); - var context = NewHttpContext( url, rawApiVersion ); - var route = Mock.Of(); - var routeKey = (string) null; - var values = new RouteValueDictionary() { [nameof( odataPath )] = odataPath }; - var constraint = new VersionedODataPathRouteConstraint( "odata", apiVersion ); - - // act - var result = constraint.Match( context, route, routeKey, values, IncomingRequest ); - - // assert - result.Should().Be( expected ); - } - - [Theory] - [InlineData( "http://localhost/", null, false )] - [InlineData( "http://localhost/$metadata", "$metadata", false )] - [InlineData( "http://localhost/Tests(1)", "Tests(1)", true )] - public void match_should_return_expected_result_when_controller_is_implicitly_versioned( string requestUri, string odataPath, bool allowImplicitVersioning ) - { - // arrange - const string rawApiVersion = default; - var apiVersion = new ApiVersion( 2, 0 ); - - void OnConfigure( ApiVersioningOptions options ) - { - options.DefaultApiVersion = apiVersion; - options.AssumeDefaultVersionWhenUnspecified = allowImplicitVersioning; - } - - var url = new Uri( requestUri ); - var context = NewHttpContext( url, rawApiVersion, configure: OnConfigure ); - var route = Mock.Of(); - var routeKey = (string) null; - var values = new RouteValueDictionary() { [nameof( odataPath )] = odataPath }; - var constraint = new VersionedODataPathRouteConstraint( "odata", apiVersion ); - - // act - var result = constraint.Match( context, route, routeKey, values, IncomingRequest ); - - // assert - result.Should().BeTrue(); - } - - [Theory] - [InlineData( "Tests(1)", true )] - [InlineData( "NonExistent(1)", false )] - public void match_should_return_expected_result_when_requested_api_version_is_ambiguous( string odataPath, bool expected ) - { - // arrange - var url = new Uri( $"http://localhost/{odataPath}?api-version=1.0&api-version=2.0" ); - var apiVersion = new ApiVersion( 1, 0 ); - var route = Mock.Of(); - var routeKey = (string) null; - var values = new RouteValueDictionary() { [nameof( odataPath )] = odataPath }; - var context = NewHttpContext( url, "1.0" ); - var constraint = new VersionedODataPathRouteConstraint( "odata", apiVersion ); - - // act - var result = constraint.Match( context, route, routeKey, values, IncomingRequest ); - - // assert - result.Should().Be( expected ); - } - - static HttpContext NewHttpContext( Uri url, string rawApiVersion, string routePrefix = null, Action configure = null ) - { - - var features = new Mock(); - var odataFeature = Mock.Of(); - var apiVersioningFeature = Mock.Of(); - var query = new Mock(); - var httpRequest = new Mock(); - var httpContext = new Mock(); - var services = new ServiceCollection(); - var queryValues = new Dictionary( StringComparer.OrdinalIgnoreCase ); - - if ( !string.IsNullOrEmpty( url.Query ) ) - { - foreach ( var values in from item in url.Query.TrimStart( '?' ).Split( '&' ) - let parts = item.Split( '=' ) - group parts[1] by parts[0] ) - { - queryValues.Add( values.Key, new StringValues( values.ToArray() ) ); - } - } - - services.AddLogging(); - services.Add( Singleton( new DiagnosticListener( "test" ) ) ); - services.AddMvcCore( options => options.EnableEndpointRouting = false ) - .ConfigureApplicationPartManager( apm => apm.ApplicationParts.Add( new TestApplicationPart( typeof( TestsController ) ) ) ); - services.AddApiVersioning( configure ?? ( _ => { } ) ); - services.AddOData().EnableApiVersioning(); - - var serviceProvider = services.BuildServiceProvider(); - var app = new ApplicationBuilder( serviceProvider ); - var modelBuilder = serviceProvider.GetRequiredService(); - - modelBuilder.ModelConfigurations.Add( new TestModelConfiguration() ); - - var model = modelBuilder.GetEdmModels().Single(); - - if ( !TryParse( rawApiVersion, out var apiVersion ) ) - { - apiVersion = serviceProvider.GetRequiredService>().Value.DefaultApiVersion; - } - - app.UseMvc( rb => rb.MapVersionedODataRoute( "odata", routePrefix, model, apiVersion ) ); - apiVersioningFeature.RawRequestedApiVersion = rawApiVersion; - apiVersioningFeature.RequestedApiVersion = apiVersion; - features.SetupGet( f => f[typeof( IODataFeature )] ).Returns( odataFeature ); - features.Setup( f => f.Get() ).Returns( odataFeature ); - features.Setup( f => f.Get() ).Returns( apiVersioningFeature ); - query.Setup( q => q[It.IsAny()] ).Returns( ( string k ) => queryValues.TryGetValue( k, out var v ) ? v : StringValues.Empty ); - httpContext.SetupGet( c => c.Features ).Returns( features.Object ); - httpContext.SetupProperty( c => c.RequestServices, serviceProvider ); - httpContext.SetupProperty( c => c.Items, new Dictionary() ); - httpContext.SetupGet( c => c.Request ).Returns( () => httpRequest.Object ); - httpRequest.SetupGet( r => r.HttpContext ).Returns( () => httpContext.Object ); - httpRequest.SetupProperty( r => r.Method, "GET" ); - httpRequest.SetupProperty( r => r.Scheme, url.Scheme ); - httpRequest.SetupProperty( r => r.Host, new HostString( url.Host ) ); - httpRequest.SetupProperty( r => r.PathBase, new PathString() ); - httpRequest.SetupProperty( r => r.Path, new PathString( '/' + url.GetComponents( Path, Unescaped ) ) ); - httpRequest.SetupProperty( r => r.QueryString, new QueryString( url.Query ) ); - httpRequest.SetupGet( r => r.Query ).Returns( query.Object ); - - return httpContext.Object; - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/VersionedMetadataControllerTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/VersionedMetadataControllerTest.cs index f3701a0b..8f164f7f 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/VersionedMetadataControllerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNet.OData/VersionedMetadataControllerTest.cs @@ -21,17 +21,15 @@ public async Task options_should_return_expected_headers() { // arrange var request = new HttpRequestMessage( new HttpMethod( "OPTIONS" ), "http://localhost/$metadata" ); - var response = default( HttpResponseMessage ); var hostBuilder = new WebHostBuilder().UseStartup(); - using ( var server = new TestServer( hostBuilder ) ) - using ( var client = server.CreateClient() ) - { - client.BaseAddress = new Uri( "http://localhost" ); + using var server = new TestServer( hostBuilder ); + using var client = server.CreateClient(); - // act - response = ( await client.SendAsync( request ) ).EnsureSuccessStatusCode(); - } + client.BaseAddress = new Uri( "http://localhost" ); + + // act + var response = ( await client.SendAsync( request ) ).EnsureSuccessStatusCode(); // assert response.Headers.GetValues( "OData-Version" ).Single().Should().Be( "4.0" ); @@ -60,7 +58,7 @@ public void ConfigureServices( IServiceCollection services ) public void Configure( IApplicationBuilder app, VersionedODataModelBuilder builder ) { - app.UseMvc( r => r.MapVersionedODataRoutes( "odata", null, builder.GetEdmModels() ) ); + app.UseMvc( rb => rb.MapVersionedODataRoute( "odata", null, builder ) ); } } } diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/ODataApiVersionActionSelectorTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/ODataApiVersionActionSelectorTest.cs index f8769b3a..fa31870b 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/ODataApiVersionActionSelectorTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/ODataApiVersionActionSelectorTest.cs @@ -23,12 +23,11 @@ public async Task select_best_candidate_should_return_correct_versionedX2C_attri // arrange using var server = new WebServer(); - await server.Client.GetAsync( $"api/tests?api-version={version}" ); - // act - var action = ( (TestODataApiVersionActionSelector) server.Services.GetRequiredService() ).SelectedCandidate; + await server.Client.GetAsync( $"api/tests?api-version={version}" ); // assert + var action = ( (TestODataApiVersionActionSelector) server.Services.GetRequiredService() ).SelectedCandidate; action.GetProperty().SupportedApiVersions.Should().Contain( Parse( version ) ); action.As().ControllerTypeInfo.Should().Be( controllerType.GetTypeInfo() ); } @@ -42,12 +41,11 @@ public async Task select_best_candidate_should_return_correct_versionX2DneutralX var controllerType = typeof( VersionNeutralController ).GetTypeInfo(); using var server = new WebServer(); - await server.Client.GetAsync( requestUri ); - // act - var action = ( (TestODataApiVersionActionSelector) server.Services.GetRequiredService() ).SelectedCandidate; + await server.Client.GetAsync( requestUri ); // assert + var action = ( (TestODataApiVersionActionSelector) server.Services.GetRequiredService() ).SelectedCandidate; action.As().ControllerTypeInfo.Should().Be( controllerType ); } } diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/TestODataApiVersionActionSelector.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/TestODataApiVersionActionSelector.cs index c5c9cd6b..745b246e 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/TestODataApiVersionActionSelector.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/TestODataApiVersionActionSelector.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Infrastructure; + using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; @@ -16,8 +17,21 @@ public TestODataApiVersionActionSelector( IEnumerable actionConstraintProviders, IOptions options, ILoggerFactory loggerFactory, - IApiVersionRoutePolicy routePolicy ) - : base( actionDescriptorCollectionProvider, actionConstraintProviders, options, loggerFactory, routePolicy ) { } + IApiVersionRoutePolicy routePolicy, + IModelBinderFactory modelBinderFactory, + IModelMetadataProvider modelMetadataProvider, + IOptions mvcOptions ) + : base( + actionDescriptorCollectionProvider, + actionConstraintProviders, + options, + loggerFactory, + routePolicy, + modelBinderFactory, + modelMetadataProvider, + mvcOptions ) + { + } public override ActionDescriptor SelectBestCandidate( RouteContext context, IReadOnlyList candidates ) => SelectedCandidate = base.SelectBestCandidate( context, candidates ); diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/WebServer.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/WebServer.cs index 7bd6d121..d0c52659 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/WebServer.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/AspNetCore.Mvc/Versioning/WebServer.cs @@ -40,7 +40,7 @@ public WebServer( Action setupApiVersioning = null, Action var modelBuilder = app.ApplicationServices.GetRequiredService(); app.UseMvc( setupRoutes ); - app.UseMvc( routeBuilder => routeBuilder.MapVersionedODataRoutes( "odata", "api", modelBuilder.GetEdmModels() ) ); + app.UseMvc( routeBuilder => routeBuilder.MapVersionedODataRoute( "odata", "api", modelBuilder.GetEdmModels() ) ); } ); server = new TestServer( hostBuilder ); diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController.cs index af9ec6cd..1155969f 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController.cs @@ -11,7 +11,7 @@ public class TestsController : ODataController [ODataRoute] public IActionResult Get() => Ok( new[] { new TestEntity() { Id = 1 }, new TestEntity() { Id = 2 }, new TestEntity() { Id = 3 } } ); - [ODataRoute( "({id})" )] + [ODataRoute( "{id}" )] public IActionResult Get( int id ) => Ok( new TestEntity() { Id = id } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController2.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController2.cs index 5f74cf23..47cb9113 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController2.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController2.cs @@ -12,7 +12,7 @@ public class TestsController2 : ODataController [ODataRoute] public IActionResult Get() => Ok( new [] { new TestEntity() { Id = 1 }, new TestEntity() { Id = 2 }, new TestEntity() { Id = 3 } } ); - [ODataRoute( "({id})" )] + [ODataRoute( "{id}" )] public IActionResult Get( int id ) => Ok( new TestEntity() { Id = id } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController3.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController3.cs index 6a26acbe..5f77cc8c 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController3.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/TestsController3.cs @@ -13,7 +13,7 @@ public class TestsController3 : ODataController [ODataRoute] public IActionResult Get() => Ok( new[] { new TestEntity() { Id = 1 }, new TestEntity() { Id = 2 }, new TestEntity() { Id = 3 } } ); - [ODataRoute( "({id})" )] + [ODataRoute( "{id}" )] public IActionResult Get( int id ) => Ok( new TestEntity() { Id = id } ); } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/VersionNeutralController.cs b/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/VersionNeutralController.cs index 82672904..395ae2d9 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/VersionNeutralController.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.Tests/Simulators/VersionNeutralController.cs @@ -3,16 +3,16 @@ using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Routing; using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.Versioning; [ApiVersionNeutral] + [ControllerName( "NeutralTests" )] [ODataRoutePrefix( "NeutralTests" )] public class VersionNeutralController : ODataController { [ODataRoute] public IActionResult Get() => Ok( new[] { new TestNeutralEntity() { Id = 1 }, new TestNeutralEntity() { Id = 2 }, new TestNeutralEntity() { Id = 3 } } ); - [ODataRoute( "({id})" )] + [ODataRoute( "{id}" )] public IActionResult Get( int id ) => Ok( new TestNeutralEntity() { Id = id } ); } } diff --git a/test/OData.Test.Shared/TestModelConfiguration.cs b/test/OData.Test.Shared/TestModelConfiguration.cs index b7203a77..66d94b26 100644 --- a/test/OData.Test.Shared/TestModelConfiguration.cs +++ b/test/OData.Test.Shared/TestModelConfiguration.cs @@ -10,7 +10,7 @@ public class TestModelConfiguration : IModelConfiguration { - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { var tests = builder.EntitySet( "Tests" ).EntityType; var neutralTests = builder.EntitySet( "NeutralTests" ).EntityType; From f261bace285cf27fc601d55d4aedeef0ae2f56c3 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 4 Oct 2020 14:24:40 -0700 Subject: [PATCH 08/13] Update OData examples --- ApiVersioning.sln | 14 ++++ .../AdvancedODataSample.csproj | 12 ++++ .../Configuration/OrderModelConfiguration.cs | 29 +++++++++ .../Configuration/PersonModelConfiguration.cs | 43 ++++++++++++ .../Controllers/Orders2Controller.cs | 22 +++++++ .../Controllers/Orders3Controller.cs | 20 ++++++ .../Controllers/OrdersController.cs | 22 +++++++ .../Controllers/People2Controller.cs | 22 +++++++ .../Controllers/PeopleController.cs | 41 ++++++++++++ .../AdvancedODataSample/Models/Order.cs | 20 ++++++ .../AdvancedODataSample/Models/Person.cs | 23 +++++++ .../aspnetcore/AdvancedODataSample/Program.cs | 16 +++++ .../Properties/launchSettings.json | 28 ++++++++ .../aspnetcore/AdvancedODataSample/Startup.cs | 51 +++++++++++++++ .../AdvancedODataSample/appsettings.json | 8 +++ .../aspnetcore/AdvancedODataSample/web.config | 14 ++++ .../Configuration/OrderModelConfiguration.cs | 34 ++++++++++ .../Configuration/PersonModelConfiguration.cs | 48 ++++++++++++++ .../Controllers/OrdersController.cs | 18 +++++ .../Controllers/People2Controller.cs | 19 ++++++ .../Controllers/PeopleController.cs | 33 ++++++++++ .../ConventionsODataSample.csproj | 12 ++++ .../ConventionsODataSample/Models/Order.cs | 20 ++++++ .../ConventionsODataSample/Models/Person.cs | 23 +++++++ .../ConventionsODataSample/Program.cs | 16 +++++ .../Properties/launchSettings.json | 28 ++++++++ .../ConventionsODataSample/Startup.cs | 65 +++++++++++++++++++ .../ConventionsODataSample/appsettings.json | 8 +++ .../ConventionsODataSample/web.config | 12 ++++ .../Configuration/OrderModelConfiguration.cs | 7 +- .../Configuration/PersonModelConfiguration.cs | 7 +- .../Controllers/OrdersController.cs | 12 ++-- .../Controllers/People2Controller.cs | 12 ++-- .../Controllers/PeopleController.cs | 21 ++---- .../Properties/launchSettings.json | 2 +- .../aspnetcore/ODataBasicSample/Startup.cs | 31 ++++----- .../Configuration/AllConfigurations.cs | 8 +-- .../Configuration/OrderModelConfiguration.cs | 8 +-- .../Configuration/PersonModelConfiguration.cs | 8 +-- .../Configuration/ProductConfiguration.cs | 8 +-- .../Configuration/SupplierConfiguration.cs | 8 +-- .../aspnetcore/SwaggerODataSample/Startup.cs | 2 +- .../Configuration/OrderModelConfiguration.cs | 2 +- .../Configuration/PersonModelConfiguration.cs | 2 +- .../Controllers/Orders2Controller.cs | 14 ++-- .../Controllers/Orders3Controller.cs | 8 +-- .../Controllers/OrdersController.cs | 13 ++-- .../Controllers/People2Controller.cs | 10 +-- .../Controllers/PeopleController.cs | 20 +++--- .../AdvancedODataWebApiSample/Startup.cs | 19 +----- .../Configuration/OrderModelConfiguration.cs | 7 +- .../Configuration/PersonModelConfiguration.cs | 7 +- .../Controllers/OrdersController.cs | 10 ++- .../Controllers/People2Controller.cs | 8 +-- .../Controllers/PeopleController.cs | 17 ++--- .../webapi/BasicODataWebApiSample/Startup.cs | 24 ++----- samples/webapi/BasicWebApiSample/Startup.cs | 2 +- .../Configuration/OrderModelConfiguration.cs | 7 +- .../Configuration/PersonModelConfiguration.cs | 7 +- .../Controllers/OrdersController.cs | 14 ++-- .../Controllers/People2Controller.cs | 12 ++-- .../Controllers/PeopleController.cs | 21 ++---- .../ConventionsODataWebApiSample/Startup.cs | 24 ++----- .../Configuration/AllConfigurations.cs | 8 +-- .../Configuration/OrderModelConfiguration.cs | 8 +-- .../Configuration/PersonModelConfiguration.cs | 8 +-- .../Configuration/ProductConfiguration.cs | 10 ++- .../Configuration/SupplierConfiguration.cs | 8 +-- .../SwaggerODataWebApiSample/Startup.cs | 21 ++---- .../V1/OrdersController.cs | 4 +- .../V2/OrdersController.cs | 8 +-- .../V3/OrdersController.cs | 10 +-- .../V3/ProductsController.cs | 19 ++++++ 73 files changed, 930 insertions(+), 277 deletions(-) create mode 100644 samples/aspnetcore/AdvancedODataSample/AdvancedODataSample.csproj create mode 100644 samples/aspnetcore/AdvancedODataSample/Configuration/OrderModelConfiguration.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/Configuration/PersonModelConfiguration.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/Controllers/Orders2Controller.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/Controllers/Orders3Controller.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/Controllers/OrdersController.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/Controllers/People2Controller.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/Controllers/PeopleController.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/Models/Order.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/Models/Person.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/Program.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/Properties/launchSettings.json create mode 100644 samples/aspnetcore/AdvancedODataSample/Startup.cs create mode 100644 samples/aspnetcore/AdvancedODataSample/appsettings.json create mode 100644 samples/aspnetcore/AdvancedODataSample/web.config create mode 100644 samples/aspnetcore/ConventionsODataSample/Configuration/OrderModelConfiguration.cs create mode 100644 samples/aspnetcore/ConventionsODataSample/Configuration/PersonModelConfiguration.cs create mode 100644 samples/aspnetcore/ConventionsODataSample/Controllers/OrdersController.cs create mode 100644 samples/aspnetcore/ConventionsODataSample/Controllers/People2Controller.cs create mode 100644 samples/aspnetcore/ConventionsODataSample/Controllers/PeopleController.cs create mode 100644 samples/aspnetcore/ConventionsODataSample/ConventionsODataSample.csproj create mode 100644 samples/aspnetcore/ConventionsODataSample/Models/Order.cs create mode 100644 samples/aspnetcore/ConventionsODataSample/Models/Person.cs create mode 100644 samples/aspnetcore/ConventionsODataSample/Program.cs create mode 100644 samples/aspnetcore/ConventionsODataSample/Properties/launchSettings.json create mode 100644 samples/aspnetcore/ConventionsODataSample/Startup.cs create mode 100644 samples/aspnetcore/ConventionsODataSample/appsettings.json create mode 100644 samples/aspnetcore/ConventionsODataSample/web.config diff --git a/ApiVersioning.sln b/ApiVersioning.sln index eec68c5a..2d5ca8f2 100644 --- a/ApiVersioning.sln +++ b/ApiVersioning.sln @@ -141,6 +141,10 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Common.OData.ApiExplorer", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests", "test\Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests\Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests.csproj", "{23BC896B-A4CC-4C82-B98B-CE71239C2EB8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConventionsODataSample", "samples\aspnetcore\ConventionsODataSample\ConventionsODataSample.csproj", "{992B6D9F-F007-441A-9ED9-6A0669993A70}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedODataSample", "samples\aspnetcore\AdvancedODataSample\AdvancedODataSample.csproj", "{DDC53D03-C461-4477-84E2-4C31DD3C6B13}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Common.OData.ApiExplorer\Common.OData.ApiExplorer.projitems*{0d6519ae-20d2-4c98-97aa-ed3622043936}*SharedItemsImports = 5 @@ -306,6 +310,14 @@ Global {23BC896B-A4CC-4C82-B98B-CE71239C2EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU {23BC896B-A4CC-4C82-B98B-CE71239C2EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU {23BC896B-A4CC-4C82-B98B-CE71239C2EB8}.Release|Any CPU.Build.0 = Release|Any CPU + {992B6D9F-F007-441A-9ED9-6A0669993A70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {992B6D9F-F007-441A-9ED9-6A0669993A70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {992B6D9F-F007-441A-9ED9-6A0669993A70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {992B6D9F-F007-441A-9ED9-6A0669993A70}.Release|Any CPU.Build.0 = Release|Any CPU + {DDC53D03-C461-4477-84E2-4C31DD3C6B13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDC53D03-C461-4477-84E2-4C31DD3C6B13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDC53D03-C461-4477-84E2-4C31DD3C6B13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDC53D03-C461-4477-84E2-4C31DD3C6B13}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -353,6 +365,8 @@ Global {0D6519AE-20D2-4C98-97AA-ED3622043936} = {4D5F5F21-0CB7-4B4E-A42F-732BD4AFD0FF} {C0C766F3-A2D6-461E-ADFF-27496600EA9C} = {4D5F5F21-0CB7-4B4E-A42F-732BD4AFD0FF} {23BC896B-A4CC-4C82-B98B-CE71239C2EB8} = {0987757E-4D09-4523-B9C9-65B1E8832AA1} + {992B6D9F-F007-441A-9ED9-6A0669993A70} = {900DD210-8500-4D89-A05D-C9526935A719} + {DDC53D03-C461-4477-84E2-4C31DD3C6B13} = {900DD210-8500-4D89-A05D-C9526935A719} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5A38B7FA-17BC-4D3C-977F-7379653DC67C} diff --git a/samples/aspnetcore/AdvancedODataSample/AdvancedODataSample.csproj b/samples/aspnetcore/AdvancedODataSample/AdvancedODataSample.csproj new file mode 100644 index 00000000..f7889e26 --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/AdvancedODataSample.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + Microsoft.Examples + + + + + + + \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Configuration/OrderModelConfiguration.cs b/samples/aspnetcore/AdvancedODataSample/Configuration/OrderModelConfiguration.cs new file mode 100644 index 00000000..5c6a9ca6 --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Configuration/OrderModelConfiguration.cs @@ -0,0 +1,29 @@ +namespace Microsoft.Examples.Configuration +{ + using Microsoft.AspNet.OData.Builder; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Examples.Models; + + public class OrderModelConfiguration : IModelConfiguration + { + private static readonly ApiVersion V2 = new ApiVersion( 2, 0 ); + + private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + { + var order = builder.EntitySet( "Orders" ).EntityType; + + order.HasKey( p => p.Id ); + + return order; + } + + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) + { + // note: the EDM for orders is only available in version 2.0 + if ( apiVersion == V2 ) + { + ConfigureCurrent( builder ); + } + } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Configuration/PersonModelConfiguration.cs b/samples/aspnetcore/AdvancedODataSample/Configuration/PersonModelConfiguration.cs new file mode 100644 index 00000000..7638158e --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Configuration/PersonModelConfiguration.cs @@ -0,0 +1,43 @@ +namespace Microsoft.Examples.Configuration +{ + using Microsoft.AspNet.OData.Builder; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Examples.Models; + + public class PersonModelConfiguration : IModelConfiguration + { + private void ConfigureV1( ODataModelBuilder builder ) + { + var person = ConfigureCurrent( builder ); + person.Ignore( p => p.Email ); + person.Ignore( p => p.Phone ); + } + + private void ConfigureV2( ODataModelBuilder builder ) => ConfigureCurrent( builder ).Ignore( p => p.Phone ); + + private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + { + var person = builder.EntitySet( "People" ).EntityType; + + person.HasKey( p => p.Id ); + + return person; + } + + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) + { + switch ( apiVersion.MajorVersion ) + { + case 1: + ConfigureV1( builder ); + break; + case 2: + ConfigureV2( builder ); + break; + default: + ConfigureCurrent( builder ); + break; + } + } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Controllers/Orders2Controller.cs b/samples/aspnetcore/AdvancedODataSample/Controllers/Orders2Controller.cs new file mode 100644 index 00000000..9bbc5434 --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Controllers/Orders2Controller.cs @@ -0,0 +1,22 @@ +namespace Microsoft.Examples.Controllers +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Examples.Models; + + [ApiVersion( "2.0" )] + [ControllerName( "Orders" )] + public class Orders2Controller : ODataController + { + // GET ~/api/orders?api-version=2.0 + [HttpGet] + public IActionResult Get( ODataQueryOptions options, ApiVersion version ) => + Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{version}" } } ); + + // GET ~/api/orders/{id}?api-version=2.0 + [HttpGet( "{id}" )] + public IActionResult Get( int id, ODataQueryOptions options, ApiVersion version ) => + Ok( new Order() { Id = id, Customer = $"Customer v{version}" } ); + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Controllers/Orders3Controller.cs b/samples/aspnetcore/AdvancedODataSample/Controllers/Orders3Controller.cs new file mode 100644 index 00000000..de1d2a24 --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Controllers/Orders3Controller.cs @@ -0,0 +1,20 @@ +namespace Microsoft.Examples.Controllers +{ + using Microsoft.AspNetCore.Mvc; + using Microsoft.Examples.Models; + + [ApiController] + [ApiVersion( "3.0" )] + [ControllerName( "Orders" )] + [Route( "api/orders" )] + public class Orders3Controller : ControllerBase + { + // GET ~/api/orders?api-version=3.0 + [HttpGet] + public IActionResult Get( ApiVersion version ) => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{version}" } } ); + + // GET ~/api/orders/{id}?api-version=3.0 + [HttpGet( "{id}" )] + public IActionResult Get( int id, ApiVersion version ) => Ok( new Order() { Id = id, Customer = $"Customer v{version}" } ); + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Controllers/OrdersController.cs b/samples/aspnetcore/AdvancedODataSample/Controllers/OrdersController.cs new file mode 100644 index 00000000..dce47a48 --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Controllers/OrdersController.cs @@ -0,0 +1,22 @@ +namespace Microsoft.Examples.Controllers +{ + using Microsoft.AspNetCore.Mvc; + using Microsoft.Examples.Models; + + // note: since the application is configured with AssumeDefaultVersionWhenUnspecified, this controller + // is implicitly versioned to the DefaultApiVersion, which has the default value 1.0. + [ApiController] + [Route( "api/orders" )] + public class OrdersController : ControllerBase + { + // GET ~/api/orders + // GET ~/api/orders?api-version=1.0 + [HttpGet] + public IActionResult Get( ApiVersion version ) => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{version}" } } ); + + // GET ~/api/orders/{id} + // GET ~/api/orders/{id}?api-version=1.0 + [HttpGet( "{id}" )] + public IActionResult Get( int id, ApiVersion version ) => Ok( new Order() { Id = id, Customer = $"Customer v{version}" } ); + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Controllers/People2Controller.cs b/samples/aspnetcore/AdvancedODataSample/Controllers/People2Controller.cs new file mode 100644 index 00000000..dbbbbfab --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Controllers/People2Controller.cs @@ -0,0 +1,22 @@ +namespace Microsoft.Examples.Controllers +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Examples.Models; + + [ApiVersion( "3.0" )] + [ControllerName( "People" )] + public class People2Controller : ODataController + { + // GET ~/api/people?api-version=3.0 + [HttpGet] + public IActionResult Get( ODataQueryOptions options, ApiVersion version ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + // GET ~/api/people/{id}?api-version=3.0 + [HttpGet( "{id}" )] + public IActionResult Get( int id, ODataQueryOptions options, ApiVersion version ) => + Ok( new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Controllers/PeopleController.cs b/samples/aspnetcore/AdvancedODataSample/Controllers/PeopleController.cs new file mode 100644 index 00000000..9833cd8f --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Controllers/PeopleController.cs @@ -0,0 +1,41 @@ +namespace Microsoft.Examples.Controllers +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Examples.Models; + + // note: since the application is configured with AssumeDefaultVersionWhenUnspecified, this controller + // is resolved without or without an API version, even though it is explicitly versioned + [ApiVersion( "1.0" )] + [ApiVersion( "2.0" )] + public class PeopleController : ODataController + { + // GET ~/api/people + // GET ~/api/people?api-version=[1.0|2.0] + [HttpGet] + public IActionResult Get( ODataQueryOptions options, ApiVersion version ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + // GET ~/api/people/{id} + // GET ~/api/people/{id}?api-version=[1.0|2.0] + [HttpGet( "{id}" )] + public IActionResult Get( int id, ODataQueryOptions options, ApiVersion version ) => + Ok( new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + + // PATCH ~/api/people/{id}?api-version=2.0 + [HttpPatch( "{id}" )] + [MapToApiVersion( "2.0" )] + public IActionResult Patch( int id, Delta delta, ODataQueryOptions options, ApiVersion version ) + { + if ( !ModelState.IsValid ) + return BadRequest( ModelState ); + + var person = new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" }; + + delta.Patch( person ); + + return Updated( person ); + } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Models/Order.cs b/samples/aspnetcore/AdvancedODataSample/Models/Order.cs new file mode 100644 index 00000000..db06e016 --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Models/Order.cs @@ -0,0 +1,20 @@ +namespace Microsoft.Examples.Models +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.Linq; + using System.Web; + + public class Order + { + public int Id { get; set; } + + public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.Now; + + public DateTimeOffset EffectiveDate { get; set; } = DateTimeOffset.Now; + + [Required] + public string Customer { get; set; } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Models/Person.cs b/samples/aspnetcore/AdvancedODataSample/Models/Person.cs new file mode 100644 index 00000000..682aa36b --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Models/Person.cs @@ -0,0 +1,23 @@ +namespace Microsoft.Examples.Models +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + + public class Person + { + public int Id { get; set; } + + [Required] + [StringLength( 25 )] + public string FirstName { get; set; } + + [Required] + [StringLength( 25 )] + public string LastName { get; set; } + + public string Email { get; set; } + + public string Phone { get; set; } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Program.cs b/samples/aspnetcore/AdvancedODataSample/Program.cs new file mode 100644 index 00000000..3ef6eb9c --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Program.cs @@ -0,0 +1,16 @@ +namespace Microsoft.Examples +{ + using Microsoft.AspNetCore; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + + public static class Program + { + public static void Main( string[] args ) => + CreateWebHostBuilder( args ).Build().Run(); + + public static IWebHostBuilder CreateWebHostBuilder( string[] args ) => + WebHost.CreateDefaultBuilder( args ) + .UseStartup(); + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Properties/launchSettings.json b/samples/aspnetcore/AdvancedODataSample/Properties/launchSettings.json new file mode 100644 index 00000000..c53e584f --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:1237/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "AdvancedODataSample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5000/api", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/Startup.cs b/samples/aspnetcore/AdvancedODataSample/Startup.cs new file mode 100644 index 00000000..c8a23e03 --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/Startup.cs @@ -0,0 +1,51 @@ +namespace Microsoft.Examples +{ + using Microsoft.AspNet.OData.Builder; + using Microsoft.AspNet.OData.Extensions; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Mvc.Versioning; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + + public class Startup + { + public Startup( IConfiguration configuration ) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices( IServiceCollection services ) + { + services.AddControllers(); + services.AddApiVersioning( + options => + { + // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions" + options.ReportApiVersions = true; + + // allows a client to make a request without specifying an api version. the value of + // options.DefaultApiVersion will be 'assumed'; this is meant to grandfather in legacy apis + options.AssumeDefaultVersionWhenUnspecified = true; + + // allow multiple locations to request an api version + options.ApiVersionReader = ApiVersionReader.Combine( + new QueryStringApiVersionReader(), + new HeaderApiVersionReader( "api-version", "x-ms-version" ) ); + } ); + services.AddOData().EnableApiVersioning(); + } + + public void Configure( IApplicationBuilder app, VersionedODataModelBuilder modelBuilder ) + { + app.UseRouting(); + app.UseEndpoints( + endpoints => + { + endpoints.MapControllers(); + endpoints.MapVersionedODataRoute( "odata", "api", modelBuilder.GetEdmModels() ); + } ); + } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/appsettings.json b/samples/aspnetcore/AdvancedODataSample/appsettings.json new file mode 100644 index 00000000..d713e815 --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/samples/aspnetcore/AdvancedODataSample/web.config b/samples/aspnetcore/AdvancedODataSample/web.config new file mode 100644 index 00000000..dc0514fc --- /dev/null +++ b/samples/aspnetcore/AdvancedODataSample/web.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/samples/aspnetcore/ConventionsODataSample/Configuration/OrderModelConfiguration.cs b/samples/aspnetcore/ConventionsODataSample/Configuration/OrderModelConfiguration.cs new file mode 100644 index 00000000..a8a7cd1d --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/Configuration/OrderModelConfiguration.cs @@ -0,0 +1,34 @@ +namespace Microsoft.Examples.Configuration +{ + using Microsoft.AspNet.OData.Builder; + using Microsoft.AspNetCore.Mvc; + using Models; + + public class OrderModelConfiguration : IModelConfiguration + { + private static readonly ApiVersion V1 = new ApiVersion( 1, 0 ); + + private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + { + var order = builder.EntitySet( "Orders" ).EntityType; + + order.HasKey( p => p.Id ); + + return order; + } + + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) + { + if ( routePrefix != "api/v{version:apiVersion}" ) + { + return; + } + + // note: the EDM for orders is only available in version 1.0 + if ( apiVersion == V1 ) + { + ConfigureCurrent( builder ); + } + } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/Configuration/PersonModelConfiguration.cs b/samples/aspnetcore/ConventionsODataSample/Configuration/PersonModelConfiguration.cs new file mode 100644 index 00000000..a5e46ac2 --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/Configuration/PersonModelConfiguration.cs @@ -0,0 +1,48 @@ +namespace Microsoft.Examples.Configuration +{ + using Microsoft.AspNet.OData.Builder; + using Microsoft.AspNetCore.Mvc; + using Models; + + public class PersonModelConfiguration : IModelConfiguration + { + private void ConfigureV1( ODataModelBuilder builder ) + { + var person = ConfigureCurrent( builder ); + person.Ignore( p => p.Email ); + person.Ignore( p => p.Phone ); + } + + private void ConfigureV2( ODataModelBuilder builder ) => ConfigureCurrent( builder ).Ignore( p => p.Phone ); + + private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + { + var person = builder.EntitySet( "People" ).EntityType; + + person.HasKey( p => p.Id ); + + return person; + } + + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) + { + if ( routePrefix != "api" ) + { + return; + } + + switch ( apiVersion.MajorVersion ) + { + case 1: + ConfigureV1( builder ); + break; + case 2: + ConfigureV2( builder ); + break; + default: + ConfigureCurrent( builder ); + break; + } + } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/Controllers/OrdersController.cs b/samples/aspnetcore/ConventionsODataSample/Controllers/OrdersController.cs new file mode 100644 index 00000000..f774bcb3 --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/Controllers/OrdersController.cs @@ -0,0 +1,18 @@ +namespace Microsoft.Examples.Controllers +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Models; + + public class OrdersController : ODataController + { + // GET ~/v1/orders + public IActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); + + // GET ~/api/v1/orders/{key}?api-version=1.0 + public IActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Order() { Id = key, Customer = "Bill Mei" } ); + } +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/Controllers/People2Controller.cs b/samples/aspnetcore/ConventionsODataSample/Controllers/People2Controller.cs new file mode 100644 index 00000000..41c7cb95 --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/Controllers/People2Controller.cs @@ -0,0 +1,19 @@ +namespace Microsoft.Examples.Controllers +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Models; + + [ControllerName( "People" )] + public class People2Controller : ODataController + { + // GET ~/api/people?api-version=3.0 + public IActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + // GET ~/api/people/{key}?api-version=3.0 + public IActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + } +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/Controllers/PeopleController.cs b/samples/aspnetcore/ConventionsODataSample/Controllers/PeopleController.cs new file mode 100644 index 00000000..91c26f9c --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/Controllers/PeopleController.cs @@ -0,0 +1,33 @@ +namespace Microsoft.Examples.Controllers +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Query; + using Microsoft.AspNetCore.Mvc; + using Models; + + public class PeopleController : ODataController + { + // GET ~/api/people?api-version=[1.0|2.0] + public IActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + // GET ~/api/people/{key}?api-version=[1.0|2.0] + public IActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + + // PATCH ~/api/people/{key}?api-version=2.0 + public IActionResult Patch( int key, Delta delta, ODataQueryOptions options ) + { + if ( !ModelState.IsValid ) + { + return BadRequest( ModelState ); + } + + var person = new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" }; + + delta.Patch( person ); + + return Updated( person ); + } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/ConventionsODataSample.csproj b/samples/aspnetcore/ConventionsODataSample/ConventionsODataSample.csproj new file mode 100644 index 00000000..cf016599 --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/ConventionsODataSample.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + Microsoft.Examples + + + + + + + \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/Models/Order.cs b/samples/aspnetcore/ConventionsODataSample/Models/Order.cs new file mode 100644 index 00000000..db06e016 --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/Models/Order.cs @@ -0,0 +1,20 @@ +namespace Microsoft.Examples.Models +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.Linq; + using System.Web; + + public class Order + { + public int Id { get; set; } + + public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.Now; + + public DateTimeOffset EffectiveDate { get; set; } = DateTimeOffset.Now; + + [Required] + public string Customer { get; set; } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/Models/Person.cs b/samples/aspnetcore/ConventionsODataSample/Models/Person.cs new file mode 100644 index 00000000..682aa36b --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/Models/Person.cs @@ -0,0 +1,23 @@ +namespace Microsoft.Examples.Models +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + + public class Person + { + public int Id { get; set; } + + [Required] + [StringLength( 25 )] + public string FirstName { get; set; } + + [Required] + [StringLength( 25 )] + public string LastName { get; set; } + + public string Email { get; set; } + + public string Phone { get; set; } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/Program.cs b/samples/aspnetcore/ConventionsODataSample/Program.cs new file mode 100644 index 00000000..3ef6eb9c --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/Program.cs @@ -0,0 +1,16 @@ +namespace Microsoft.Examples +{ + using Microsoft.AspNetCore; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + + public static class Program + { + public static void Main( string[] args ) => + CreateWebHostBuilder( args ).Build().Run(); + + public static IWebHostBuilder CreateWebHostBuilder( string[] args ) => + WebHost.CreateDefaultBuilder( args ) + .UseStartup(); + } +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/Properties/launchSettings.json b/samples/aspnetcore/ConventionsODataSample/Properties/launchSettings.json new file mode 100644 index 00000000..b4c0b17f --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:1238/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/People?api-version=1.0", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ConventionsODataSample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5000/api/People?api-version=1.0", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/Startup.cs b/samples/aspnetcore/ConventionsODataSample/Startup.cs new file mode 100644 index 00000000..147af4a2 --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/Startup.cs @@ -0,0 +1,65 @@ +namespace Microsoft.Examples +{ + using Microsoft.AspNet.OData.Builder; + using Microsoft.AspNet.OData.Extensions; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Mvc.Versioning.Conventions; + using Microsoft.Examples.Controllers; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + + public class Startup + { + public Startup( IConfiguration configuration ) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices( IServiceCollection services ) + { + services.AddMvc( options => options.EnableEndpointRouting = false ); + services.AddApiVersioning( + options => + { + // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions" + options.ReportApiVersions = true; + + // apply api versions using conventions rather than attributes + options.Conventions.Controller() + .HasApiVersion( 1, 0 ); + + options.Conventions.Controller() + .HasApiVersion( 1, 0 ) + .HasApiVersion( 2, 0 ) + .Action( c => c.Patch( default, default, default ) ).MapToApiVersion( 2, 0 ); + + options.Conventions.Controller() + .HasApiVersion( 3, 0 ); + } ); + services.AddOData().EnableApiVersioning(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure( IApplicationBuilder app, VersionedODataModelBuilder modelBuilder ) + { + app.UseMvc( + routes => + { + // INFO: you do NOT and should NOT use both the query string and url segment methods together. + // this configuration is merely illustrating that they can coexist and allows you to easily + // experiment with either configuration. one of these would be removed in a real application. + // + // INFO: only pass the route prefix to GetEdmModels if you want to split the models; otherwise, both routes contain all models + + // WHEN VERSIONING BY: query string, header, or media type + routes.MapVersionedODataRoute( "odata", "api", modelBuilder ); + + // WHEN VERSIONING BY: url segment + routes.MapVersionedODataRoute( "odata-bypath", "api/v{version:apiVersion}", modelBuilder ); + } ); + } + } +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/appsettings.json b/samples/aspnetcore/ConventionsODataSample/appsettings.json new file mode 100644 index 00000000..d713e815 --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/samples/aspnetcore/ConventionsODataSample/web.config b/samples/aspnetcore/ConventionsODataSample/web.config new file mode 100644 index 00000000..8700b60c --- /dev/null +++ b/samples/aspnetcore/ConventionsODataSample/web.config @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/aspnetcore/ODataBasicSample/Configuration/OrderModelConfiguration.cs b/samples/aspnetcore/ODataBasicSample/Configuration/OrderModelConfiguration.cs index 2d8ec4e3..a8a7cd1d 100644 --- a/samples/aspnetcore/ODataBasicSample/Configuration/OrderModelConfiguration.cs +++ b/samples/aspnetcore/ODataBasicSample/Configuration/OrderModelConfiguration.cs @@ -17,8 +17,13 @@ private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder build return order; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + if ( routePrefix != "api/v{version:apiVersion}" ) + { + return; + } + // note: the EDM for orders is only available in version 1.0 if ( apiVersion == V1 ) { diff --git a/samples/aspnetcore/ODataBasicSample/Configuration/PersonModelConfiguration.cs b/samples/aspnetcore/ODataBasicSample/Configuration/PersonModelConfiguration.cs index 8b296cf3..a5e46ac2 100644 --- a/samples/aspnetcore/ODataBasicSample/Configuration/PersonModelConfiguration.cs +++ b/samples/aspnetcore/ODataBasicSample/Configuration/PersonModelConfiguration.cs @@ -24,8 +24,13 @@ private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder buil return person; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + if ( routePrefix != "api" ) + { + return; + } + switch ( apiVersion.MajorVersion ) { case 1: diff --git a/samples/aspnetcore/ODataBasicSample/Controllers/OrdersController.cs b/samples/aspnetcore/ODataBasicSample/Controllers/OrdersController.cs index 969a7840..53ba9b64 100644 --- a/samples/aspnetcore/ODataBasicSample/Controllers/OrdersController.cs +++ b/samples/aspnetcore/ODataBasicSample/Controllers/OrdersController.cs @@ -2,24 +2,20 @@ { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.AspNetCore.Mvc; using Models; [ApiVersion( "1.0" )] - [ODataRoutePrefix( "Orders" )] public class OrdersController : ODataController { // GET ~/v1/orders - // GET ~/api/orders?api-version=1.0 - [ODataRoute] + [HttpGet] public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); - // GET ~/v1/orders(1) - // GET ~/api/orders(1)?api-version=1.0 - [ODataRoute( "({id})" )] - public IActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => + // GET ~/api/v1/orders/{id}?api-version=1.0 + [HttpGet( "{id:int}" )] + public IActionResult Get( int id, ODataQueryOptions options ) => Ok( new Order() { Id = id, Customer = "Bill Mei" } ); } } \ No newline at end of file diff --git a/samples/aspnetcore/ODataBasicSample/Controllers/People2Controller.cs b/samples/aspnetcore/ODataBasicSample/Controllers/People2Controller.cs index a9393bb3..4fab4c9a 100644 --- a/samples/aspnetcore/ODataBasicSample/Controllers/People2Controller.cs +++ b/samples/aspnetcore/ODataBasicSample/Controllers/People2Controller.cs @@ -2,25 +2,21 @@ { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.AspNetCore.Mvc; using Models; [ApiVersion( "3.0" )] [ControllerName( "People" )] - [ODataRoutePrefix( "People" )] public class People2Controller : ODataController { - // GET ~/v3/people // GET ~/api/people?api-version=3.0 - [ODataRoute] + [HttpGet] public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - // GET ~/v3/people(1) - // GET ~/api/people(1)?api-version=3.0 - [ODataRoute( "({id})" )] - public IActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => + // GET ~/api/people/{id}?api-version=3.0 + [HttpGet( "{id:int}" )] + public IActionResult Get( int id, ODataQueryOptions options ) => Ok( new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); } } \ No newline at end of file diff --git a/samples/aspnetcore/ODataBasicSample/Controllers/PeopleController.cs b/samples/aspnetcore/ODataBasicSample/Controllers/PeopleController.cs index 02e674e9..dfc3668b 100644 --- a/samples/aspnetcore/ODataBasicSample/Controllers/PeopleController.cs +++ b/samples/aspnetcore/ODataBasicSample/Controllers/PeopleController.cs @@ -2,34 +2,27 @@ { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.AspNetCore.Mvc; using Models; [ApiVersion( "1.0" )] [ApiVersion( "2.0" )] - [ODataRoutePrefix( "People" )] public class PeopleController : ODataController { - // GET ~/v1/people - // GET ~/v2/people // GET ~/api/people?api-version=[1.0|2.0] - [ODataRoute] + [HttpGet] public IActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - // GET ~/v1/people(1) - // GET ~/v2/people(1) - // GET ~/api/people(1)?api-version=[1.0|2.0] - [ODataRoute( "({id})" )] - public IActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => + // GET ~/api/people/{id}?api-version=[1.0|2.0] + [HttpGet( "{id:int}" )] + public IActionResult Get( int id, ODataQueryOptions options ) => Ok( new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); - // PATCH ~/v2/people(1) - // PATCH ~/api/people(1)?api-version=2.0 + // PATCH ~/api/people/{id}?api-version=2.0 [MapToApiVersion( "2.0" )] - [ODataRoute( "({id})" )] - public IActionResult Patch( [FromODataUri] int id, Delta delta, ODataQueryOptions options ) + [HttpPatch( "{id:int}" )] + public IActionResult Patch( int id, Delta delta, ODataQueryOptions options ) { if ( !ModelState.IsValid ) { diff --git a/samples/aspnetcore/ODataBasicSample/Properties/launchSettings.json b/samples/aspnetcore/ODataBasicSample/Properties/launchSettings.json index e50fb671..68ad0581 100644 --- a/samples/aspnetcore/ODataBasicSample/Properties/launchSettings.json +++ b/samples/aspnetcore/ODataBasicSample/Properties/launchSettings.json @@ -16,7 +16,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "BasicSample": { + "ODataBasicSample": { "commandName": "Project", "launchBrowser": true, "launchUrl": "http://localhost:5000/api/People?api-version=1.0", diff --git a/samples/aspnetcore/ODataBasicSample/Startup.cs b/samples/aspnetcore/ODataBasicSample/Startup.cs index 0a6ab01e..f98d2a8d 100644 --- a/samples/aspnetcore/ODataBasicSample/Startup.cs +++ b/samples/aspnetcore/ODataBasicSample/Startup.cs @@ -1,13 +1,10 @@ namespace Microsoft.Examples { - using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; - using static Microsoft.AspNetCore.Mvc.CompatibilityVersion; - using static Microsoft.OData.ODataUrlKeyDelimiter; public class Startup { @@ -21,9 +18,7 @@ public Startup( IConfiguration configuration ) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices( IServiceCollection services ) { - // the sample application always uses the latest version, but you may want an explicit version such as Version_2_2 - // note: Endpoint Routing is enabled by default; however, it is unsupported by OData and MUST be false - services.AddMvc( options => options.EnableEndpointRouting = false ).SetCompatibilityVersion( Latest ); + services.AddControllers(); services.AddApiVersioning( options => { @@ -36,17 +31,23 @@ public void ConfigureServices( IServiceCollection services ) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure( IApplicationBuilder app, VersionedODataModelBuilder modelBuilder ) { - app.UseMvc( - routeBuilder => + app.UseRouting(); + app.UseEndpoints( + endpoints => { - var models = modelBuilder.GetEdmModels(); + endpoints.MapControllers(); - // the following will not work as expected - // BUG: https://github.com/OData/WebApi/issues/1837 - // routeBuilder.SetDefaultODataOptions( new ODataOptions() { UrlKeyDelimiter = Parentheses } ); - routeBuilder.ServiceProvider.GetRequiredService().UrlKeyDelimiter = Parentheses; - routeBuilder.MapVersionedODataRoutes( "odata", "api", models ); - routeBuilder.MapVersionedODataRoutes( "odata-bypath", "v{version:apiVersion}", models ); + // INFO: you do NOT and should NOT use both the query string and url segment methods together. + // this configuration is merely illustrating that they can coexist and allows you to easily + // experiment with either configuration. one of these would be removed in a real application. + // + // INFO: only pass the route prefix to GetEdmModels if you want to split the models; otherwise, both routes contain all models + + // WHEN VERSIONING BY: query string, header, or media type + endpoints.MapVersionedODataRoute( "odata", "api", modelBuilder ); + + // WHEN VERSIONING BY: url segment + endpoints.MapVersionedODataRoute( "odata-bypath", "api/v{version:apiVersion}", modelBuilder ); } ); } } diff --git a/samples/aspnetcore/SwaggerODataSample/Configuration/AllConfigurations.cs b/samples/aspnetcore/SwaggerODataSample/Configuration/AllConfigurations.cs index bac5a21c..c1713bab 100644 --- a/samples/aspnetcore/SwaggerODataSample/Configuration/AllConfigurations.cs +++ b/samples/aspnetcore/SwaggerODataSample/Configuration/AllConfigurations.cs @@ -8,12 +8,8 @@ /// public class AllConfigurations : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { builder.Function( "GetSalesTaxRate" ).Returns().Parameter( "PostalCode" ); } diff --git a/samples/aspnetcore/SwaggerODataSample/Configuration/OrderModelConfiguration.cs b/samples/aspnetcore/SwaggerODataSample/Configuration/OrderModelConfiguration.cs index 652dd3ee..e43c4e94 100644 --- a/samples/aspnetcore/SwaggerODataSample/Configuration/OrderModelConfiguration.cs +++ b/samples/aspnetcore/SwaggerODataSample/Configuration/OrderModelConfiguration.cs @@ -9,12 +9,8 @@ /// public class OrderModelConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { var order = builder.EntitySet( "Orders" ).EntityType.HasKey( o => o.Id ); var lineItem = builder.EntityType().HasKey( li => li.Number ); diff --git a/samples/aspnetcore/SwaggerODataSample/Configuration/PersonModelConfiguration.cs b/samples/aspnetcore/SwaggerODataSample/Configuration/PersonModelConfiguration.cs index c2c55bfe..da8c619b 100644 --- a/samples/aspnetcore/SwaggerODataSample/Configuration/PersonModelConfiguration.cs +++ b/samples/aspnetcore/SwaggerODataSample/Configuration/PersonModelConfiguration.cs @@ -10,12 +10,8 @@ /// public class PersonModelConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { var person = builder.EntitySet( "People" ).EntityType; var address = builder.EntityType
().HasKey( a => a.Id ); diff --git a/samples/aspnetcore/SwaggerODataSample/Configuration/ProductConfiguration.cs b/samples/aspnetcore/SwaggerODataSample/Configuration/ProductConfiguration.cs index b0f5c1d5..a417a848 100644 --- a/samples/aspnetcore/SwaggerODataSample/Configuration/ProductConfiguration.cs +++ b/samples/aspnetcore/SwaggerODataSample/Configuration/ProductConfiguration.cs @@ -10,12 +10,8 @@ ///
public class ProductConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { if ( apiVersion < ApiVersions.V3 ) { diff --git a/samples/aspnetcore/SwaggerODataSample/Configuration/SupplierConfiguration.cs b/samples/aspnetcore/SwaggerODataSample/Configuration/SupplierConfiguration.cs index b97836e5..42696239 100644 --- a/samples/aspnetcore/SwaggerODataSample/Configuration/SupplierConfiguration.cs +++ b/samples/aspnetcore/SwaggerODataSample/Configuration/SupplierConfiguration.cs @@ -9,12 +9,8 @@ ///
public class SupplierConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { if ( apiVersion < ApiVersions.V3 ) { diff --git a/samples/aspnetcore/SwaggerODataSample/Startup.cs b/samples/aspnetcore/SwaggerODataSample/Startup.cs index 7361bcde..e24224a0 100644 --- a/samples/aspnetcore/SwaggerODataSample/Startup.cs +++ b/samples/aspnetcore/SwaggerODataSample/Startup.cs @@ -86,7 +86,7 @@ public void Configure( IApplicationBuilder app, VersionedODataModelBuilder model // global odata query options routeBuilder.Count(); - routeBuilder.MapVersionedODataRoutes( "odata", "api", modelBuilder.GetEdmModels() ); + routeBuilder.MapVersionedODataRoute( "odata", "api", modelBuilder ); } ); app.UseSwagger(); app.UseSwaggerUI( diff --git a/samples/webapi/AdvancedODataWebApiSample/Configuration/OrderModelConfiguration.cs b/samples/webapi/AdvancedODataWebApiSample/Configuration/OrderModelConfiguration.cs index 90c06d07..2a0bd73d 100644 --- a/samples/webapi/AdvancedODataWebApiSample/Configuration/OrderModelConfiguration.cs +++ b/samples/webapi/AdvancedODataWebApiSample/Configuration/OrderModelConfiguration.cs @@ -17,7 +17,7 @@ private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder build return order; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { // note: the EDM for orders is only available in version 2.0 if ( apiVersion == V2 ) diff --git a/samples/webapi/AdvancedODataWebApiSample/Configuration/PersonModelConfiguration.cs b/samples/webapi/AdvancedODataWebApiSample/Configuration/PersonModelConfiguration.cs index 971a60fa..9987ee6e 100644 --- a/samples/webapi/AdvancedODataWebApiSample/Configuration/PersonModelConfiguration.cs +++ b/samples/webapi/AdvancedODataWebApiSample/Configuration/PersonModelConfiguration.cs @@ -24,7 +24,7 @@ private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder buil return person; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { switch ( apiVersion.MajorVersion ) { diff --git a/samples/webapi/AdvancedODataWebApiSample/Controllers/Orders2Controller.cs b/samples/webapi/AdvancedODataWebApiSample/Controllers/Orders2Controller.cs index df2dce2a..a3c9eab7 100644 --- a/samples/webapi/AdvancedODataWebApiSample/Controllers/Orders2Controller.cs +++ b/samples/webapi/AdvancedODataWebApiSample/Controllers/Orders2Controller.cs @@ -12,14 +12,14 @@ [ODataRoutePrefix( "Orders" )] public class Orders2Controller : ODataController { - // GET ~/orders?api-version=2.0 + // GET ~/api/orders?api-version=2.0 [ODataRoute] - public IHttpActionResult Get( ODataQueryOptions options ) => - Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } } ); + public IHttpActionResult Get( ODataQueryOptions options, ApiVersion version ) => + Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{version}" } } ); - // GET ~/orders({id})?api-version=2.0 - [ODataRoute( "({id})" )] - public IHttpActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => - Ok( new Order() { Id = id, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } ); + // GET ~/api/orders/{id}?api-version=2.0 + [ODataRoute( "{id}" )] + public IHttpActionResult Get( int id, ODataQueryOptions options, ApiVersion version ) => + Ok( new Order() { Id = id, Customer = $"Customer v{version}" } ); } } \ No newline at end of file diff --git a/samples/webapi/AdvancedODataWebApiSample/Controllers/Orders3Controller.cs b/samples/webapi/AdvancedODataWebApiSample/Controllers/Orders3Controller.cs index 4b73c2bc..690d93cb 100644 --- a/samples/webapi/AdvancedODataWebApiSample/Controllers/Orders3Controller.cs +++ b/samples/webapi/AdvancedODataWebApiSample/Controllers/Orders3Controller.cs @@ -8,10 +8,10 @@ [ControllerName( "Orders" )] public class Orders3Controller : ApiController { - // GET ~/orders?api-version=3.0 - public IHttpActionResult Get() => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } } ); + // GET ~/api/orders?api-version=3.0 + public IHttpActionResult Get( ApiVersion version ) => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{version}" } } ); - // GET ~/orders/{id}?api-version=3.0 - public IHttpActionResult Get( int id ) => Ok( new Order() { Id = id, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } ); + // GET ~/api/orders/{id}?api-version=3.0 + public IHttpActionResult Get( int id, ApiVersion version ) => Ok( new Order() { Id = id, Customer = $"Customer v{version}" } ); } } \ No newline at end of file diff --git a/samples/webapi/AdvancedODataWebApiSample/Controllers/OrdersController.cs b/samples/webapi/AdvancedODataWebApiSample/Controllers/OrdersController.cs index db5f1f66..847389b7 100644 --- a/samples/webapi/AdvancedODataWebApiSample/Controllers/OrdersController.cs +++ b/samples/webapi/AdvancedODataWebApiSample/Controllers/OrdersController.cs @@ -1,18 +1,19 @@ namespace Microsoft.Examples.Controllers { using Microsoft.Examples.Models; + using Microsoft.Web.Http; using System.Web.Http; // note: since the application is configured with AssumeDefaultVersionWhenUnspecified, this controller // is implicitly versioned to the DefaultApiVersion, which has the default value 1.0. public class OrdersController : ApiController { - // GET ~/orders - // GET ~/orders?api-version=1.0 - public IHttpActionResult Get() => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } } ); + // GET ~/api/orders + // GET ~/api/orders?api-version=1.0 + public IHttpActionResult Get( ApiVersion version ) => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{version}" } } ); - // GET ~/orders/{id} - // GET ~/orders/{id}?api-version=1.0 - public IHttpActionResult Get( int id ) => Ok( new Order() { Id = id, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } ); + // GET ~/api/orders/{id} + // GET ~/api/orders/{id}?api-version=1.0 + public IHttpActionResult Get( int id, ApiVersion version ) => Ok( new Order() { Id = id, Customer = $"Customer v{version}" } ); } } \ No newline at end of file diff --git a/samples/webapi/AdvancedODataWebApiSample/Controllers/People2Controller.cs b/samples/webapi/AdvancedODataWebApiSample/Controllers/People2Controller.cs index d2104bba..dd0f1328 100644 --- a/samples/webapi/AdvancedODataWebApiSample/Controllers/People2Controller.cs +++ b/samples/webapi/AdvancedODataWebApiSample/Controllers/People2Controller.cs @@ -12,14 +12,14 @@ [ODataRoutePrefix( "People" )] public class People2Controller : ODataController { - // GET ~/people?api-version=3.0 + // GET ~/api/people?api-version=3.0 [ODataRoute] - public IHttpActionResult Get( ODataQueryOptions options ) => + public IHttpActionResult Get( ODataQueryOptions options, ApiVersion version ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - // GET ~/people({id})?api-version=3.0 - [ODataRoute( "({id})" )] - public IHttpActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => + // GET ~/api/people/{id}?api-version=3.0 + [ODataRoute( "{id}" )] + public IHttpActionResult Get( int id, ODataQueryOptions options, ApiVersion version ) => Ok( new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); } } \ No newline at end of file diff --git a/samples/webapi/AdvancedODataWebApiSample/Controllers/PeopleController.cs b/samples/webapi/AdvancedODataWebApiSample/Controllers/PeopleController.cs index 2a8a70e4..05ec1aee 100644 --- a/samples/webapi/AdvancedODataWebApiSample/Controllers/PeopleController.cs +++ b/samples/webapi/AdvancedODataWebApiSample/Controllers/PeopleController.cs @@ -14,22 +14,22 @@ [ODataRoutePrefix( "People" )] public class PeopleController : ODataController { - // GET ~/people - // GET ~/people?api-version=[1.0|2.0] + // GET ~/api/people + // GET ~/api/people?api-version=[1.0|2.0] [ODataRoute] - public IHttpActionResult Get( ODataQueryOptions options ) => + public IHttpActionResult Get( ODataQueryOptions options, ApiVersion version ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - // GET ~/people({id}) - // GET ~/people({id})?api-version=[1.0|2.0] - [ODataRoute( "({id})" )] - public IHttpActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => + // GET ~/api/people/{id} + // GET ~/api/people/{id}?api-version=[1.0|2.0] + [ODataRoute( "{id}" )] + public IHttpActionResult Get( int id, ODataQueryOptions options, ApiVersion version ) => Ok( new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); - // PATCH ~/people({id})?api-version=2.0 + // PATCH ~/api/people/{id}?api-version=2.0 [MapToApiVersion( "2.0" )] - [ODataRoute( "({id})" )] - public IHttpActionResult Patch( [FromODataUri] int id, Delta delta, ODataQueryOptions options ) + [ODataRoute( "{id}" )] + public IHttpActionResult Patch( int id, Delta delta, ODataQueryOptions options, ApiVersion version ) { if ( !ModelState.IsValid ) return BadRequest( ModelState ); diff --git a/samples/webapi/AdvancedODataWebApiSample/Startup.cs b/samples/webapi/AdvancedODataWebApiSample/Startup.cs index 708cd1fa..d2e2d3ca 100644 --- a/samples/webapi/AdvancedODataWebApiSample/Startup.cs +++ b/samples/webapi/AdvancedODataWebApiSample/Startup.cs @@ -3,18 +3,12 @@ namespace Microsoft.Examples { 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; using System.Web.Http; - using static Microsoft.OData.ODataUrlKeyDelimiter; - using static Microsoft.OData.ServiceLifetime; using static System.Web.Http.RouteParameter; public class Startup @@ -48,24 +42,17 @@ public void Configuration( IAppBuilder appBuilder ) new OrderModelConfiguration() } }; - var models = modelBuilder.GetEdmModels(); - var batchHandler = new DefaultODataBatchHandler( httpServer ); // NOTE: when you mix OData and non-Data controllers in Web API, it's RECOMMENDED to only use // convention-based routing. using attribute routing may not work as expected due to limitations // in the underlying routing system. the order of route registration is important as well. // // DO NOT use configuration.MapHttpAttributeRoutes(); - configuration.MapVersionedODataRoutes( "odata", "api", models, ConfigureContainer, batchHandler ); + configuration.MapVersionedODataRoute( "odata", "api", modelBuilder.GetEdmModels() ); configuration.Routes.MapHttpRoute( "orders", "api/{controller}/{id}", new { id = Optional } ); - - appBuilder.UseWebApi( httpServer ); - } - static void ConfigureContainer( IContainerBuilder builder ) - { - builder.AddService( Singleton, sp => new DefaultODataPathHandler() { UrlKeyDelimiter = Parentheses } ); - builder.AddService( Singleton, sp => new UnqualifiedCallAndEnumPrefixFreeResolver() { EnableCaseInsensitive = true } ); + configuration.Formatters.Remove( configuration.Formatters.XmlFormatter ); + appBuilder.UseWebApi( httpServer ); } public static string ContentRootPath diff --git a/samples/webapi/BasicODataWebApiSample/Configuration/OrderModelConfiguration.cs b/samples/webapi/BasicODataWebApiSample/Configuration/OrderModelConfiguration.cs index 8787c17b..1f98f8bf 100644 --- a/samples/webapi/BasicODataWebApiSample/Configuration/OrderModelConfiguration.cs +++ b/samples/webapi/BasicODataWebApiSample/Configuration/OrderModelConfiguration.cs @@ -17,8 +17,13 @@ private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder build return order; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + if ( routePrefix != "api/v{apiVersion}" ) + { + return; + } + // note: the EDM for orders is only available in version 1.0 if ( apiVersion == V1 ) { diff --git a/samples/webapi/BasicODataWebApiSample/Configuration/PersonModelConfiguration.cs b/samples/webapi/BasicODataWebApiSample/Configuration/PersonModelConfiguration.cs index ee0dfcaa..4ef82a80 100644 --- a/samples/webapi/BasicODataWebApiSample/Configuration/PersonModelConfiguration.cs +++ b/samples/webapi/BasicODataWebApiSample/Configuration/PersonModelConfiguration.cs @@ -24,8 +24,13 @@ private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder buil return person; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + if ( routePrefix != "api" ) + { + return; + } + switch ( apiVersion.MajorVersion ) { case 1: diff --git a/samples/webapi/BasicODataWebApiSample/Controllers/OrdersController.cs b/samples/webapi/BasicODataWebApiSample/Controllers/OrdersController.cs index b7c84429..39dd10ea 100644 --- a/samples/webapi/BasicODataWebApiSample/Controllers/OrdersController.cs +++ b/samples/webapi/BasicODataWebApiSample/Controllers/OrdersController.cs @@ -11,16 +11,14 @@ [ODataRoutePrefix( "Orders" )] public class OrdersController : ODataController { - // GET ~/v1/orders - // GET ~/api/orders?api-version=1.0 + // GET ~/api/v1/orders [ODataRoute] public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); - // GET ~/v1/orders(1) - // GET ~/api/orders(1)?api-version=1.0 - [ODataRoute( "({id})" )] - public IHttpActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => + // GET ~/api/v1/orders/{id} + [ODataRoute( "{id}" )] + public IHttpActionResult Get( int id, ODataQueryOptions options ) => Ok( new Order() { Id = id, Customer = "Bill Mei" } ); } } \ No newline at end of file diff --git a/samples/webapi/BasicODataWebApiSample/Controllers/People2Controller.cs b/samples/webapi/BasicODataWebApiSample/Controllers/People2Controller.cs index 3c92fec3..782d9484 100644 --- a/samples/webapi/BasicODataWebApiSample/Controllers/People2Controller.cs +++ b/samples/webapi/BasicODataWebApiSample/Controllers/People2Controller.cs @@ -12,16 +12,14 @@ [ODataRoutePrefix( "People" )] public class People2Controller : ODataController { - // GET ~/v3/people // GET ~/api/people?api-version=3.0 [ODataRoute] public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - // GET ~/v3/people(1) - // GET ~/api/people(1)?api-version=3.0 - [ODataRoute( "({id})" )] - public IHttpActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => + // GET ~/api/people/{id}?api-version=3.0 + [ODataRoute( "{id}" )] + public IHttpActionResult Get( int id, ODataQueryOptions options ) => Ok( new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); } } \ No newline at end of file diff --git a/samples/webapi/BasicODataWebApiSample/Controllers/PeopleController.cs b/samples/webapi/BasicODataWebApiSample/Controllers/PeopleController.cs index c93c390e..85ee9c79 100644 --- a/samples/webapi/BasicODataWebApiSample/Controllers/PeopleController.cs +++ b/samples/webapi/BasicODataWebApiSample/Controllers/PeopleController.cs @@ -12,25 +12,20 @@ [ODataRoutePrefix( "People" )] public class PeopleController : ODataController { - // GET ~/v1/people - // GET ~/v2/people // GET ~/api/people?api-version=[1.0|2.0] [ODataRoute] public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - // GET ~/v1/people(1) - // GET ~/v2/people(1) - // GET ~/api/people(1)?api-version=[1.0|2.0] - [ODataRoute( "({id})" )] - public IHttpActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => + // GET ~/api/people/{id}?api-version=[1.0|2.0] + [ODataRoute( "{id}" )] + public IHttpActionResult Get( int id, ODataQueryOptions options ) => Ok( new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); - // PATCH ~/v2/people(1) - // PATCH ~/api/people(1)?api-version=2.0 + // PATCH ~/api/people/{id}?api-version=2.0 [MapToApiVersion( "2.0" )] - [ODataRoute( "({id})" )] - public IHttpActionResult Patch( [FromODataUri] int id, Delta delta, ODataQueryOptions options ) + [ODataRoute( "{id}" )] + public IHttpActionResult Patch( int id, Delta delta, ODataQueryOptions options ) { if ( !ModelState.IsValid ) { diff --git a/samples/webapi/BasicODataWebApiSample/Startup.cs b/samples/webapi/BasicODataWebApiSample/Startup.cs index 42f4b164..6d320cad 100644 --- a/samples/webapi/BasicODataWebApiSample/Startup.cs +++ b/samples/webapi/BasicODataWebApiSample/Startup.cs @@ -3,17 +3,11 @@ namespace Microsoft.Examples { 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; using System.Web.Http; - using static Microsoft.OData.ODataUrlKeyDelimiter; - using static Microsoft.OData.ServiceLifetime; public class Startup { @@ -33,22 +27,18 @@ public void Configuration( IAppBuilder appBuilder ) new OrderModelConfiguration() } }; - var models = modelBuilder.GetEdmModels(); - var batchHandler = new DefaultODataBatchHandler( httpServer ); - // NOTE: you do NOT and should NOT use both the query string and url segment methods together. + // INFO: you do NOT and should NOT use both the query string and url segment methods together. // this configuration is merely illustrating that they can coexist and allows you to easily // experiment with either configuration. one of these would be removed in a real application. - configuration.MapVersionedODataRoutes( "odata", "api", models, ConfigureContainer, batchHandler ); - configuration.MapVersionedODataRoutes( "odata-bypath", "api/v{apiVersion}", models, ConfigureContainer ); - appBuilder.UseWebApi( httpServer ); - } + // WHEN VERSIONING BY: query string, header, or media type + configuration.MapVersionedODataRoute( "odata", "api", modelBuilder ); - static void ConfigureContainer( IContainerBuilder builder ) - { - builder.AddService( Singleton, sp => new DefaultODataPathHandler() { UrlKeyDelimiter = Parentheses } ); - builder.AddService( Singleton, sp => new UnqualifiedCallAndEnumPrefixFreeResolver() { EnableCaseInsensitive = true } ); + // WHEN VERSIONING BY: url segment + configuration.MapVersionedODataRoute( "odata-bypath", "api/v{apiVersion}", modelBuilder ); + + appBuilder.UseWebApi( httpServer ); } public static string ContentRootPath diff --git a/samples/webapi/BasicWebApiSample/Startup.cs b/samples/webapi/BasicWebApiSample/Startup.cs index 77632525..9f5912b8 100644 --- a/samples/webapi/BasicWebApiSample/Startup.cs +++ b/samples/webapi/BasicWebApiSample/Startup.cs @@ -12,7 +12,7 @@ public class Startup { public void Configuration( IAppBuilder builder ) { - // we only need to change the default constraint resolver for services that want urls with versioning like: ~/v{version}/{controller} + // we only need to change the default constraint resolver for services that want urls with versioning like: ~/v{apiVersion}/{controller} var constraintResolver = new DefaultInlineConstraintResolver() { ConstraintMap = { ["apiVersion"] = typeof( ApiVersionRouteConstraint ) } }; var configuration = new HttpConfiguration(); var httpServer = new HttpServer( configuration ); diff --git a/samples/webapi/ConventionsODataWebApiSample/Configuration/OrderModelConfiguration.cs b/samples/webapi/ConventionsODataWebApiSample/Configuration/OrderModelConfiguration.cs index 0a2540f6..805883ee 100644 --- a/samples/webapi/ConventionsODataWebApiSample/Configuration/OrderModelConfiguration.cs +++ b/samples/webapi/ConventionsODataWebApiSample/Configuration/OrderModelConfiguration.cs @@ -17,8 +17,13 @@ private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder build return order; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + if ( routePrefix != "api/v{apiVersion}" ) + { + return; + } + // note: the EDM for orders is only available in version 1.0 if ( apiVersion == V1 ) { diff --git a/samples/webapi/ConventionsODataWebApiSample/Configuration/PersonModelConfiguration.cs b/samples/webapi/ConventionsODataWebApiSample/Configuration/PersonModelConfiguration.cs index 971a60fa..5d940e5f 100644 --- a/samples/webapi/ConventionsODataWebApiSample/Configuration/PersonModelConfiguration.cs +++ b/samples/webapi/ConventionsODataWebApiSample/Configuration/PersonModelConfiguration.cs @@ -24,8 +24,13 @@ private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder buil return person; } - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + if ( routePrefix != "api" ) + { + return; + } + switch ( apiVersion.MajorVersion ) { case 1: diff --git a/samples/webapi/ConventionsODataWebApiSample/Controllers/OrdersController.cs b/samples/webapi/ConventionsODataWebApiSample/Controllers/OrdersController.cs index f8bd9c67..5fb3825a 100644 --- a/samples/webapi/ConventionsODataWebApiSample/Controllers/OrdersController.cs +++ b/samples/webapi/ConventionsODataWebApiSample/Controllers/OrdersController.cs @@ -2,23 +2,17 @@ { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.Examples.Models; using System.Web.Http; - [ODataRoutePrefix( "Orders" )] public class OrdersController : ODataController { - // GET ~/v1/orders - // GET ~/orders?api-version=1.0 - [ODataRoute] + // GET ~/api/v1/orders public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); - // GET ~/v1/orders(1) - // GET ~/orders(1)?api-version=1.0 - [ODataRoute( "({id})" )] - public IHttpActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => - Ok( new Order() { Id = id, Customer = "Bill Mei" } ); + // GET ~/api/v1/orders/{key} + public IHttpActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Order() { Id = key, Customer = "Bill Mei" } ); } } \ No newline at end of file diff --git a/samples/webapi/ConventionsODataWebApiSample/Controllers/People2Controller.cs b/samples/webapi/ConventionsODataWebApiSample/Controllers/People2Controller.cs index c9118dfb..da725f28 100644 --- a/samples/webapi/ConventionsODataWebApiSample/Controllers/People2Controller.cs +++ b/samples/webapi/ConventionsODataWebApiSample/Controllers/People2Controller.cs @@ -2,23 +2,19 @@ { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.Examples.Models; using Microsoft.Web.Http; using System.Web.Http; [ControllerName( "People" )] - [ODataRoutePrefix( "People" )] public class People2Controller : ODataController { - // GET ~/people?api-version=3.0 - [ODataRoute] + // GET ~/api/people?api-version=3.0 public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - // GET ~/people(1)?api-version=3.0 - [ODataRoute( "({key})" )] - public IHttpActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => - Ok( new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + // GET ~/api/people/{key}?api-version=3.0 + public IHttpActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); } } \ No newline at end of file diff --git a/samples/webapi/ConventionsODataWebApiSample/Controllers/PeopleController.cs b/samples/webapi/ConventionsODataWebApiSample/Controllers/PeopleController.cs index e2b7c1a7..13037ce8 100644 --- a/samples/webapi/ConventionsODataWebApiSample/Controllers/PeopleController.cs +++ b/samples/webapi/ConventionsODataWebApiSample/Controllers/PeopleController.cs @@ -2,37 +2,30 @@ { using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Query; - using Microsoft.AspNet.OData.Routing; using Microsoft.Examples.Models; using Microsoft.Web.Http; using System.Web.Http; - [ODataRoutePrefix( "People" )] public class PeopleController : ODataController { - // GET ~/v1/people - // GET ~/people?api-version=[1.0|2.0] - [ODataRoute] + // GET ~/api/people?api-version=[1.0|2.0] public IHttpActionResult Get( ODataQueryOptions options ) => Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); - // GET ~/v1/people(1) - // GET ~/people(1)?api-version=[1.0|2.0] - [ODataRoute( "({id})" )] - public IHttpActionResult Get( [FromODataUri] int id, ODataQueryOptions options ) => - Ok( new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + // GET ~/api/people/{key}?api-version=[1.0|2.0] + public IHttpActionResult Get( int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); - // PATCH ~/people(1)?api-version=2.0 + // PATCH ~/api/people/{key}?api-version=2.0 [MapToApiVersion( "2.0" )] - [ODataRoute( "({id})" )] - public IHttpActionResult Patch( [FromODataUri] int id, Delta delta, ODataQueryOptions options ) + public IHttpActionResult Patch( int key, Delta delta, ODataQueryOptions options ) { if ( !ModelState.IsValid ) { return BadRequest( ModelState ); } - var person = new Person() { Id = id, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" }; + var person = new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" }; delta.Patch( person ); diff --git a/samples/webapi/ConventionsODataWebApiSample/Startup.cs b/samples/webapi/ConventionsODataWebApiSample/Startup.cs index 191a7d1b..80c8d36e 100644 --- a/samples/webapi/ConventionsODataWebApiSample/Startup.cs +++ b/samples/webapi/ConventionsODataWebApiSample/Startup.cs @@ -3,19 +3,13 @@ namespace Microsoft.Examples { 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; using System.Web.Http; - using static Microsoft.OData.ODataUrlKeyDelimiter; - using static Microsoft.OData.ServiceLifetime; public class Startup { @@ -51,22 +45,18 @@ public void Configuration( IAppBuilder appBuilder ) new OrderModelConfiguration() } }; - var models = modelBuilder.GetEdmModels(); - var batchHandler = new DefaultODataBatchHandler( httpServer ); - // NOTE: you do NOT and should NOT use both the query string and url segment methods together. + // INFO: you do NOT and should NOT use both the query string and url segment methods together. // this configuration is merely illustrating that they can coexist and allows you to easily // experiment with either configuration. one of these would be removed in a real application. - configuration.MapVersionedODataRoutes( "odata", "api", models, ConfigureContainer, batchHandler ); - configuration.MapVersionedODataRoutes( "odata-bypath", "api/v{apiVersion}", models, ConfigureContainer ); - appBuilder.UseWebApi( httpServer ); - } + // WHEN VERSIONING BY: query string, header, or media type + configuration.MapVersionedODataRoute( "odata", "api", modelBuilder ); - static void ConfigureContainer( IContainerBuilder builder ) - { - builder.AddService( Singleton, sp => new DefaultODataPathHandler() { UrlKeyDelimiter = Parentheses } ); - builder.AddService( Singleton, sp => new UnqualifiedCallAndEnumPrefixFreeResolver() { EnableCaseInsensitive = true } ); + // WHEN VERSIONING BY: url segment + configuration.MapVersionedODataRoute( "odata-bypath", "api/v{apiVersion}", modelBuilder ); + + appBuilder.UseWebApi( httpServer ); } public static string ContentRootPath diff --git a/samples/webapi/SwaggerODataWebApiSample/Configuration/AllConfigurations.cs b/samples/webapi/SwaggerODataWebApiSample/Configuration/AllConfigurations.cs index d44fc011..53648ae8 100644 --- a/samples/webapi/SwaggerODataWebApiSample/Configuration/AllConfigurations.cs +++ b/samples/webapi/SwaggerODataWebApiSample/Configuration/AllConfigurations.cs @@ -8,12 +8,8 @@ ///
public class AllConfigurations : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { builder.Function( "GetSalesTaxRate" ).Returns().Parameter( "PostalCode" ); } diff --git a/samples/webapi/SwaggerODataWebApiSample/Configuration/OrderModelConfiguration.cs b/samples/webapi/SwaggerODataWebApiSample/Configuration/OrderModelConfiguration.cs index c452952b..d8199556 100644 --- a/samples/webapi/SwaggerODataWebApiSample/Configuration/OrderModelConfiguration.cs +++ b/samples/webapi/SwaggerODataWebApiSample/Configuration/OrderModelConfiguration.cs @@ -9,12 +9,8 @@ ///
public class OrderModelConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { var order = builder.EntitySet( "Orders" ).EntityType.HasKey( o => o.Id ); var lineItem = builder.EntityType().HasKey( li => li.Number ); diff --git a/samples/webapi/SwaggerODataWebApiSample/Configuration/PersonModelConfiguration.cs b/samples/webapi/SwaggerODataWebApiSample/Configuration/PersonModelConfiguration.cs index 14e77410..01ef80de 100644 --- a/samples/webapi/SwaggerODataWebApiSample/Configuration/PersonModelConfiguration.cs +++ b/samples/webapi/SwaggerODataWebApiSample/Configuration/PersonModelConfiguration.cs @@ -10,12 +10,8 @@ ///
public class PersonModelConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { var person = builder.EntitySet( "People" ).EntityType; var address = builder.EntityType
().HasKey( a => a.Id ); diff --git a/samples/webapi/SwaggerODataWebApiSample/Configuration/ProductConfiguration.cs b/samples/webapi/SwaggerODataWebApiSample/Configuration/ProductConfiguration.cs index f79c7b4b..f5fbbe3c 100644 --- a/samples/webapi/SwaggerODataWebApiSample/Configuration/ProductConfiguration.cs +++ b/samples/webapi/SwaggerODataWebApiSample/Configuration/ProductConfiguration.cs @@ -9,12 +9,8 @@ ///
public class ProductConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { if ( apiVersion < ApiVersions.V3 ) { @@ -22,6 +18,8 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) } var product = builder.EntitySet( "Products" ).EntityType.HasKey( p => p.Id ); + + product.Action( "Rate" ).Parameter( "stars" ); } } } \ No newline at end of file diff --git a/samples/webapi/SwaggerODataWebApiSample/Configuration/SupplierConfiguration.cs b/samples/webapi/SwaggerODataWebApiSample/Configuration/SupplierConfiguration.cs index a8841129..3d64d074 100644 --- a/samples/webapi/SwaggerODataWebApiSample/Configuration/SupplierConfiguration.cs +++ b/samples/webapi/SwaggerODataWebApiSample/Configuration/SupplierConfiguration.cs @@ -9,12 +9,8 @@ ///
public class SupplierConfiguration : IModelConfiguration { - /// - /// Applies model configurations using the provided builder for the specified API version. - /// - /// The builder used to apply configurations. - /// The API version associated with the . - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + /// + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { if ( apiVersion < ApiVersions.V3 ) { diff --git a/samples/webapi/SwaggerODataWebApiSample/Startup.cs b/samples/webapi/SwaggerODataWebApiSample/Startup.cs index fed1da79..82c895d3 100644 --- a/samples/webapi/SwaggerODataWebApiSample/Startup.cs +++ b/samples/webapi/SwaggerODataWebApiSample/Startup.cs @@ -3,13 +3,10 @@ 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; @@ -18,8 +15,6 @@ namespace Microsoft.Examples using System.Web.Http; using System.Web.Http.Description; using static Microsoft.AspNet.OData.Query.AllowedQueryOptions; - using static Microsoft.OData.ODataUrlKeyDelimiter; - using static Microsoft.OData.ServiceLifetime; /// /// Represents the startup process for the application. @@ -57,13 +52,17 @@ public void Configuration( IAppBuilder builder ) // global odata query options configuration.Count(); - // INFO: while you can use both, you should choose only ONE of the following; comment, uncomment, or remove as necessary + // INFO: you do NOT and should NOT use both the query string and url segment methods together. + // this configuration is merely illustrating that they can coexist and allows you to easily + // experiment with either configuration. one of these would be removed in a real application. + // + // INFO: only pass the route prefix to GetEdmModels if you want to split the models; otherwise, both routes contain all models // WHEN VERSIONING BY: query string, header, or media type - configuration.MapVersionedODataRoutes( "odata", "api", models, ConfigureContainer ); + configuration.MapVersionedODataRoute( "odata", "api", models ); // WHEN VERSIONING BY: url segment - // configuration.MapVersionedODataRoutes( "odata-bypath", "api/v{apiVersion}", models, ConfigureContainer ); + // configuration.MapVersionedODataRoute( "odata-bypath", "api/v{apiVersion}", models ); // 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]" @@ -154,11 +153,5 @@ static string XmlCommentsFilePath return Path.Combine( ContentRootPath, 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/V1/OrdersController.cs b/samples/webapi/SwaggerODataWebApiSample/V1/OrdersController.cs index 89e00132..4d04fe0b 100644 --- a/samples/webapi/SwaggerODataWebApiSample/V1/OrdersController.cs +++ b/samples/webapi/SwaggerODataWebApiSample/V1/OrdersController.cs @@ -26,7 +26,7 @@ public class OrdersController : ODataController /// The order was successfully retrieved. /// The order does not exist. [HttpGet] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ResponseType( typeof( Order ) )] [EnableQuery( AllowedQueryOptions = Select )] public SingleResult Get( int key ) => SingleResult.Create( new[] { new Order() { Id = key, Customer = "John Doe" } }.AsQueryable() ); @@ -75,7 +75,7 @@ public IHttpActionResult Post( [FromBody] Order order ) /// The line items were successfully retrieved. /// The order does not exist. [HttpGet] - [ODataRoute( "({key})/LineItems" )] + [ODataRoute( "{key}/LineItems" )] [ResponseType( typeof( ODataValue> ) )] [EnableQuery( AllowedQueryOptions = Select )] public IHttpActionResult LineItems( int key ) diff --git a/samples/webapi/SwaggerODataWebApiSample/V2/OrdersController.cs b/samples/webapi/SwaggerODataWebApiSample/V2/OrdersController.cs index 6dd3cca7..1808b3f0 100644 --- a/samples/webapi/SwaggerODataWebApiSample/V2/OrdersController.cs +++ b/samples/webapi/SwaggerODataWebApiSample/V2/OrdersController.cs @@ -48,7 +48,7 @@ public IQueryable Get() /// The order was successfully retrieved. /// The order does not exist. [HttpGet] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ResponseType( typeof( Order ) )] [EnableQuery( AllowedQueryOptions = Select )] public SingleResult Get( int key ) => SingleResult.Create( new[] { new Order() { Id = key, Customer = "John Doe" } }.AsQueryable() ); @@ -84,7 +84,7 @@ public IHttpActionResult Post( [FromBody] Order order ) /// The order was successfully updated. /// The order does not exist. [HttpPatch] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ResponseType( typeof( Order ) )] public IHttpActionResult Patch( int key, Delta delta ) { @@ -120,7 +120,7 @@ public IHttpActionResult Patch( int key, Delta delta ) /// None /// The order was successfully rated. [HttpPost] - [ODataRoute( "({key})/Rate" )] + [ODataRoute( "{key}/Rate" )] public IHttpActionResult Rate( int key, ODataActionParameters parameters ) { if ( !ModelState.IsValid ) @@ -140,7 +140,7 @@ public IHttpActionResult Rate( int key, ODataActionParameters parameters ) /// The line items were successfully retrieved. /// The order does not exist. [HttpGet] - [ODataRoute( "({key})/LineItems" )] + [ODataRoute( "{key}/LineItems" )] [ResponseType( typeof( ODataValue> ) )] [EnableQuery( AllowedQueryOptions = Select )] public IHttpActionResult LineItems( int key ) diff --git a/samples/webapi/SwaggerODataWebApiSample/V3/OrdersController.cs b/samples/webapi/SwaggerODataWebApiSample/V3/OrdersController.cs index 2c4bfed5..0da17cf2 100644 --- a/samples/webapi/SwaggerODataWebApiSample/V3/OrdersController.cs +++ b/samples/webapi/SwaggerODataWebApiSample/V3/OrdersController.cs @@ -49,7 +49,7 @@ public IQueryable Get() /// The order was successfully retrieved. /// The order does not exist. [HttpGet] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ResponseType( typeof( Order ) )] [EnableQuery( AllowedQueryOptions = Select )] public SingleResult Get( int key ) => SingleResult.Create( new[] { new Order() { Id = key, Customer = "John Doe" } }.AsQueryable() ); @@ -85,7 +85,7 @@ public IHttpActionResult Post( [FromBody] Order order ) /// The order was successfully updated. /// The order does not exist. [HttpPatch] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ResponseType( typeof( Order ) )] public IHttpActionResult Patch( int key, Delta delta ) { @@ -109,7 +109,7 @@ public IHttpActionResult Patch( int key, Delta delta ) /// None /// The order was successfully canceled. [HttpDelete] - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] public IHttpActionResult Delete( int key, bool suspendOnly ) => StatusCode( NoContent ); /// @@ -132,7 +132,7 @@ public IHttpActionResult Patch( int key, Delta delta ) /// None /// The order was successfully rated. [HttpPost] - [ODataRoute( "({key})/Rate" )] + [ODataRoute( "{key}/Rate" )] public IHttpActionResult Rate( int key, ODataActionParameters parameters ) { if ( !ModelState.IsValid ) @@ -152,7 +152,7 @@ public IHttpActionResult Rate( int key, ODataActionParameters parameters ) /// The line items were successfully retrieved. /// The order does not exist. [HttpGet] - [ODataRoute( "({key})/LineItems" )] + [ODataRoute( "{key}/LineItems" )] [ResponseType( typeof( ODataValue> ) )] [EnableQuery( AllowedQueryOptions = Select )] public IHttpActionResult LineItems( int key ) diff --git a/samples/webapi/SwaggerODataWebApiSample/V3/ProductsController.cs b/samples/webapi/SwaggerODataWebApiSample/V3/ProductsController.cs index ddd4f1b9..de90e97c 100644 --- a/samples/webapi/SwaggerODataWebApiSample/V3/ProductsController.cs +++ b/samples/webapi/SwaggerODataWebApiSample/V3/ProductsController.cs @@ -125,6 +125,25 @@ public IHttpActionResult Put( [FromODataUri] int key, [FromBody] Product update [ResponseType( typeof( Supplier ) )] public SingleResult GetSupplier( [FromODataUri] int key ) => SingleResult.Create( products.Where( p => p.Id == key ).Select( p => p.Supplier ) ); + /// + /// Rates a product. + /// + /// The requested product identifier. + /// The action parameters. + /// None + /// The product was successfully rated. + [HttpPost] + public IHttpActionResult Rate( int key, ODataActionParameters parameters ) + { + if ( !ModelState.IsValid ) + { + return BadRequest( ModelState ); + } + + var stars = (int) parameters["stars"]; + return StatusCode( NoContent ); + } + /// /// Gets the link to the associated supplier, if any. /// From c93fb91720f409cb2d584e45b91501e076a9b25f Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 4 Oct 2020 14:25:08 -0700 Subject: [PATCH 09/13] Preserve API group name when explicitly set --- .../VersionedApiDescriptionProvider.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs index cdda8514..8092c716 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/VersionedApiDescriptionProvider.cs @@ -108,7 +108,11 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) var groupResult = result.Clone(); - groupResult.GroupName = groupName; + if ( string.IsNullOrEmpty( groupResult.GroupName ) ) + { + groupResult.GroupName = groupName; + } + groupResult.SetApiVersion( version ); PopulateApiVersionParameters( groupResult, version ); groupResult.TryUpdateRelativePathAndRemoveApiVersionParameter( Options ); From a6ca972a557b5f73909fde0793339a97b3406d70 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sat, 10 Oct 2020 00:35:47 -0700 Subject: [PATCH 10/13] Fix OData API exploration. Fixes #529, #599, #573, #610. --- .../Routing/ODataRouteActionType.cs | 1 + .../AspNet.OData/Routing/ODataRouteBuilder.cs | 182 ++++++++++---- .../Routing/ODataRouteBuilderContext.cs | 127 ++++++---- .../AspNet.OData/Routing/ODataRouteBuilder.cs | 38 +-- .../Routing/ODataRouteBuilderContext.cs | 19 +- .../Web.Http.Description/ODataApiExplorer.cs | 56 ++++- .../AspNet.OData/Routing/ODataRouteBuilder.cs | 50 +--- .../Routing/ODataRouteBuilderContext.cs | 9 +- .../ODataApiDescriptionProvider.cs | 74 +++--- .../Routing/ActionParameterContext.cs | 58 ++++- .../AspNet.OData/Routing/ActionTemplates.cs | 17 ++ .../ODataRouteBindingInfoConvention.cs | 222 +++++++++--------- .../Routing/ODataRouteBuilder.Core.cs | 2 +- .../Routing/ODataRouteBuilderContext.Core.cs | 5 +- .../Description/ODataApiExplorerTest.cs | 6 +- .../Simulators/V3/ProductsController.cs | 6 +- .../Simulators/V3/ProductsController.cs | 6 +- .../ODataApiDescriptionProviderTest.cs | 6 +- 18 files changed, 546 insertions(+), 338 deletions(-) create mode 100644 src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionTemplates.cs diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteActionType.cs b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteActionType.cs index c80c79e3..22b26b26 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteActionType.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteActionType.cs @@ -6,5 +6,6 @@ enum ODataRouteActionType EntitySet, BoundOperation, UnboundOperation, + Singleton, } } \ 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 01d27d30..f665ea09 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs @@ -49,24 +49,81 @@ sealed partial class ODataRouteBuilder internal ODataRouteBuilder( ODataRouteBuilderContext context ) => Context = context; + internal bool IsNavigationPropertyLink { get; private set; } + + ODataRouteBuilderContext Context { get; } + internal string Build() { var builder = new StringBuilder(); + IsNavigationPropertyLink = false; BuildPath( builder ); BuildQuery( builder ); return builder.ToString(); } - ODataRouteBuilderContext Context { get; } + internal string GetRoutePrefix() => + IsNullOrEmpty( Context.RoutePrefix ) ? string.Empty : RemoveRouteConstraints( Context.RoutePrefix! ); + + internal IReadOnlyList ExpandNavigationPropertyLinkTemplate( string template ) + { + if ( IsNullOrEmpty( template ) ) + { +#if WEBAPI + return new string[0]; +#else + return Array.Empty(); +#endif + } + + var token = Concat( "{", NavigationProperty, "}" ); + + if ( template.IndexOf( token, OrdinalIgnoreCase ) < 0 ) + { + return new[] { template }; + } + + IEdmEntityType entity; + + switch ( Context.ActionType ) + { + case EntitySet: + entity = Context.EntitySet.EntityType(); + break; + case Singleton: + entity = Context.Singleton.EntityType(); + break; + default: +#if WEBAPI + return new string[0]; +#else + return Array.Empty(); +#endif + } + + var properties = entity.NavigationProperties().ToArray(); + var refLinks = new string[properties.Length]; + + for ( var i = 0; i < properties.Length; i++ ) + { +#if WEBAPI + refLinks[i] = template.Replace( token, properties[i].Name ); +#else + refLinks[i] = template.Replace( token, properties[i].Name, OrdinalIgnoreCase ); +#endif + } + + return refLinks; + } void BuildPath( StringBuilder builder ) { var segments = new List(); AppendRoutePrefix( segments ); - AppendEntitySetOrOperation( segments ); + AppendPath( segments ); builder.Append( Join( "/", segments ) ); } @@ -84,7 +141,7 @@ void AppendRoutePrefix( IList segments ) segments.Add( prefix ); } - void AppendEntitySetOrOperation( IList segments ) + void AppendPath( IList segments ) { #if WEBAPI var controllerDescriptor = Context.ActionDescriptor.ControllerDescriptor; @@ -95,19 +152,21 @@ void AppendEntitySetOrOperation( IList segments ) if ( Context.IsAttributeRouted ) { #if WEBAPI - var prefix = controllerDescriptor.GetCustomAttributes().FirstOrDefault()?.Prefix?.Trim( '/' ); + var attributes = controllerDescriptor.GetCustomAttributes(); #else - var prefix = controllerDescriptor.ControllerTypeInfo.GetCustomAttributes().FirstOrDefault()?.Prefix?.Trim( '/' ); + var attributes = controllerDescriptor.ControllerTypeInfo.GetCustomAttributes(); #endif - AppendEntitySetOrOperationFromAttributes( segments, prefix ); + var prefix = attributes.FirstOrDefault()?.Prefix?.Trim( '/' ); + + AppendPathFromAttributes( segments, prefix ); } else { - AppendEntitySetOrOperationFromConvention( segments, controllerDescriptor.ControllerName ); + AppendPathFromConventions( segments, controllerDescriptor.ControllerName ); } } - void AppendEntitySetOrOperationFromAttributes( IList segments, string? prefix ) + void AppendPathFromAttributes( IList segments, string? prefix ) { var template = Context.RouteTemplate; @@ -141,7 +200,7 @@ void AppendEntitySetOrOperationFromAttributes( IList segments, string? p } } - void AppendEntitySetOrOperationFromConvention( IList segments, string controllerName ) + void AppendPathFromConventions( IList segments, string controllerName ) { var builder = new StringBuilder(); @@ -150,7 +209,11 @@ void AppendEntitySetOrOperationFromConvention( IList segments, string co case EntitySet: builder.Append( controllerName ); AppendEntityKeysFromConvention( builder ); - AppendNavigationPropertyFromConvention( builder ); + AppendNavigationPropertyFromConvention( builder, Context.EntitySet.EntityType() ); + break; + case Singleton: + builder.Append( controllerName ); + AppendNavigationPropertyFromConvention( builder, Context.Singleton.EntityType() ); break; case BoundOperation: builder.Append( controllerName ); @@ -175,10 +238,21 @@ void AppendEntitySetOrOperationFromConvention( IList segments, string co void AppendEntityKeysFromConvention( StringBuilder builder ) { // REF: http://odata.github.io/WebApi/#13-06-KeyValueBinding - var entityKeys = ( Context.EntitySet?.EntityType().Key() ?? Empty() ).ToArray(); + if ( Context.EntitySet == null ) + { + return; + } + + var entityKeys = Context.EntitySet.EntityType().Key().ToArray(); + + if ( entityKeys.Length == 0 ) + { + return; + } + var parameterKeys = Context.ParameterDescriptions.Where( p => p.Name.StartsWith( Key, OrdinalIgnoreCase ) ).ToArray(); - if ( entityKeys.Length == 0 || entityKeys.Length != parameterKeys.Length ) + if ( entityKeys.Length != parameterKeys.Length ) { return; } @@ -219,18 +293,22 @@ void AppendEntityKeysFromConvention( StringBuilder builder ) } } - void AppendNavigationPropertyFromConvention( StringBuilder builder ) + void AppendNavigationPropertyFromConvention( StringBuilder builder, IEdmEntityType entityType ) { var actionName = Context.ActionDescriptor.ActionName; - var navigationProperties = new Lazy( Context.EntitySet.EntityType().NavigationProperties().ToArray ); #if API_EXPLORER - var refLink = TryAppendNavigationPropertyLink( builder, actionName, navigationProperties ); + var navigationProperties = entityType.NavigationProperties().ToArray(); + + IsNavigationPropertyLink = TryAppendNavigationPropertyLink( builder, actionName, navigationProperties ); #else - var refLink = TryAppendNavigationPropertyLink( builder, actionName ); + IsNavigationPropertyLink = TryAppendNavigationPropertyLink( builder, actionName ); #endif - if ( !refLink ) + if ( !IsNavigationPropertyLink ) { +#if !API_EXPLORER + var navigationProperties = entityType.NavigationProperties().ToArray(); +#endif TryAppendNavigationProperty( builder, actionName, navigationProperties ); } } @@ -494,12 +572,11 @@ IList GetQueryParameters( IList navigationProperties ) + bool TryAppendNavigationProperty( StringBuilder builder, string name, IReadOnlyList navigationProperties ) { // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/PropertyRoutingConvention.cs - const string NavigationPropertyPrefix = @"(?:Get|(?:Post|Put|Delete|Patch)To)(\w+)"; - const string NavigationProperty = "^" + NavigationPropertyPrefix + "$"; - const string NavigationPropertyFromDeclaringType = "^" + NavigationPropertyPrefix + @"From(\w+)$"; + const string NavigationProperty = @"(?:Get|(?:Post|Put|Delete|Patch)To)(\w+)"; + const string NavigationPropertyFromDeclaringType = NavigationProperty + @"From(\w+)"; var match = Regex.Match( name, NavigationPropertyFromDeclaringType, RegexOptions.Singleline ); if ( !match.Success ) @@ -519,7 +596,7 @@ bool TryAppendNavigationProperty( StringBuilder builder, string name, Lazy p.Name.Equals( navigationPropertyName, OrdinalIgnoreCase ) ); + var navigationProperty = navigationProperties.First( p => p.Name.Equals( navigationPropertyName, OrdinalIgnoreCase ) ); builder.Append( navigationProperty.Type.ShortQualifiedName() ); } else @@ -535,19 +612,22 @@ bool TryAppendNavigationProperty( StringBuilder builder, string name, Lazy navigationProperties ) + bool TryAppendNavigationPropertyLink( StringBuilder builder, string name, IReadOnlyList navigationProperties ) #else - static bool TryAppendNavigationPropertyLink( StringBuilder builder, string name ) + bool TryAppendNavigationPropertyLink( StringBuilder builder, string name ) #endif { // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/RefRoutingConvention.cs - const string NavigationPropertyLinkPrefix = "(?:Create|Delete|Get)Ref"; - const string NavigationPropertyLink = "^" + NavigationPropertyLinkPrefix + "$"; - const string NavigationPropertyLinkTo = "^" + NavigationPropertyLinkPrefix + @"To(\w+)$"; - const string NavigationPropertyLinkFrom = "^" + NavigationPropertyLinkPrefix + @"To(\w+)From(\w+)$"; - var patterns = new[] { NavigationPropertyLinkFrom, NavigationPropertyLinkTo, NavigationPropertyLink }; + const int Link = 1; + const int LinkTo = 2; + const int LinkFrom = 3; + const string NavigationPropertyLink = "(?:Create|Delete|Get)Ref"; + const string NavigationPropertyLinkTo = NavigationPropertyLink + @"To(\w+)"; + const string NavigationPropertyLinkFrom = NavigationPropertyLinkTo + @"From(\w+)"; var i = 0; + var patterns = new[] { NavigationPropertyLinkFrom, NavigationPropertyLinkTo, NavigationPropertyLink }; var match = Regex.Match( name, patterns[i], RegexOptions.Singleline ); while ( !match.Success && ++i < patterns.Length ) @@ -560,49 +640,39 @@ static bool TryAppendNavigationPropertyLink( StringBuilder builder, string name return false; } + var convention = match.Groups.Count; var propertyName = match.Groups[1].Value; builder.Append( '/' ); - switch ( match.Groups.Count ) + switch ( convention ) { - case 1: + case Link: builder.Append( '{' ).Append( NavigationProperty ).Append( '}' ); #if API_EXPLORER - AddOrReplaceNavigationPropertyParameter(); + RemoveNavigationPropertyParameter(); #endif break; - case 2: - case 3: + case LinkTo: + case LinkFrom: builder.Append( propertyName ); -#if API_EXPLORER - var parameters = Context.ParameterDescriptions; - - for ( i = 0; i < parameters.Count; i++ ) - { - if ( parameters[i].Name.Equals( NavigationProperty, OrdinalIgnoreCase ) ) - { - parameters.RemoveAt( i ); - break; - } - } -#endif + RemoveNavigationPropertyParameter(); break; } builder.Append( "/$ref" ); #if API_EXPLORER - if ( name.StartsWith( "DeleteRef", OrdinalIgnoreCase ) ) + if ( name.StartsWith( "DeleteRef", Ordinal ) && !IsNullOrEmpty( propertyName ) ) { - var property = navigationProperties.Value.First( p => p.Name.Equals( propertyName, OrdinalIgnoreCase ) ); + var property = navigationProperties.First( p => p.Name.Equals( propertyName, OrdinalIgnoreCase ) ); if ( property.TargetMultiplicity() == EdmMultiplicity.Many ) { AddOrReplaceRefIdQueryParameter(); } } - else if ( name.StartsWith( "CreateRef", OrdinalIgnoreCase ) ) + else if ( name.StartsWith( "CreateRef", Ordinal ) ) { AddOrReplaceIdBodyParameter(); } @@ -610,6 +680,20 @@ static bool TryAppendNavigationPropertyLink( StringBuilder builder, string name return true; } + void RemoveNavigationPropertyParameter() + { + var parameters = Context.ParameterDescriptions; + + for ( var i = 0; i < parameters.Count; i++ ) + { + if ( parameters[i].Name.Equals( NavigationProperty, OrdinalIgnoreCase ) ) + { + parameters.RemoveAt( i ); + break; + } + } + } + static string GetRouteParameterName( IReadOnlyDictionary actionParameters, string name ) { if ( !actionParameters.TryGetValue( name, out var parameter ) ) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs index c87e03fc..7304e343 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs @@ -18,8 +18,6 @@ using System; using System.Collections.Generic; using System.Linq; - using System.Reflection; - using System.Text.RegularExpressions; #if WEBAPI using System.Web.Http.Description; using System.Web.Http.Dispatcher; @@ -32,6 +30,7 @@ sealed partial class ODataRouteBuilderContext { readonly ODataRouteAttribute? routeAttribute; + IODataPathTemplateHandler? templateHandler; internal IServiceProvider Services { get; } @@ -61,6 +60,8 @@ sealed partial class ODataRouteBuilderContext internal IEdmEntitySet? EntitySet { get; } + internal IEdmSingleton? Singleton { get; } + internal IEdmOperation? Operation { get; } internal ODataRouteActionType ActionType { get; } @@ -73,47 +74,45 @@ sealed partial class ODataRouteBuilderContext internal bool IsOperation => Operation != null; - internal bool IsBound => IsOperation && EntitySet != null; + internal bool IsBound => IsOperation && ( EntitySet != null || Singleton != null ); internal bool AllowUnqualifiedEnum => Services.GetRequiredService() is StringAsEnumResolver; - internal -#if !WEBAPI - static -#endif - ODataRouteActionType GetActionType( IEdmEntitySet? entitySet, IEdmOperation? operation, ControllerActionDescriptor action ) + internal ODataRouteActionType GetActionType( ControllerActionDescriptor action ) { - if ( entitySet == null ) + if ( EntitySet == null && Singleton == null ) { - if ( operation == null ) + if ( Operation == null ) { return ODataRouteActionType.Unknown; } - else if ( !operation.IsBound ) + else if ( !Operation.IsBound ) { return ODataRouteActionType.UnboundOperation; } } - else + else if ( Operation == null ) { - if ( operation == null ) + if ( IsActionOrFunction( EntitySet, Singleton, action.ActionName, GetHttpMethods( action ) ) ) { - if ( IsActionOrFunction( entitySet, action.ActionName, GetHttpMethods( action ) ) ) - { - return ODataRouteActionType.Unknown; - } - else - { - return ODataRouteActionType.EntitySet; - } + return ODataRouteActionType.Unknown; + } + else if ( Singleton == null ) + { + return ODataRouteActionType.EntitySet; } - else if ( operation.IsBound ) + else { - return ODataRouteActionType.BoundOperation; + return ODataRouteActionType.Singleton; } } - return ODataRouteActionType.Unknown; + if ( Operation.IsBound ) + { + return ODataRouteActionType.BoundOperation; + } + + return ODataRouteActionType.UnboundOperation; } // Slash became the default 4/18/2018 @@ -124,7 +123,8 @@ ODataRouteActionType GetActionType( IEdmEntitySet? entitySet, IEdmOperation? ope // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/FunctionRoutingConvention.cs // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntityRoutingConvention.cs - static bool IsActionOrFunction( IEdmEntitySet? entitySet, string actionName, IEnumerable methods ) + // REF: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/SingletonRoutingConvention.cs + static bool IsActionOrFunction( IEdmEntitySet? entitySet, IEdmSingleton? singleton, string actionName, IEnumerable methods ) { using var iterator = methods.GetEnumerator(); @@ -140,50 +140,73 @@ static bool IsActionOrFunction( IEdmEntitySet? entitySet, string actionName, IEn return false; } + if ( entitySet == null && singleton == null ) + { + return true; + } + const string ActionMethod = "Post"; const string FunctionMethod = "Get"; if ( ActionMethod.Equals( method, OrdinalIgnoreCase ) && actionName != ActionMethod ) { - if ( entitySet == null ) + if ( actionName.StartsWith( "CreateRef", Ordinal ) || + ( entitySet != null && actionName == ( ActionMethod + entitySet.Name ) ) ) { - return true; + return false; } - return actionName != ( ActionMethod + entitySet.Name ) && - actionName != ( ActionMethod + entitySet.EntityType().Name ) && - !actionName.StartsWith( "CreateRef", Ordinal ); + return !IsNavigationPropertyLink( entitySet, singleton, actionName, ActionMethod ); } else if ( FunctionMethod.Equals( method, OrdinalIgnoreCase ) && actionName != FunctionMethod ) { - if ( entitySet == null ) + if ( actionName.StartsWith( "GetRef", Ordinal ) || + ( entitySet != null && actionName == ( ActionMethod + entitySet.Name ) ) ) { - // TODO: could be a singleton here - return true; + return false; } - if ( actionName == ( ActionMethod + entitySet.Name ) || - actionName.StartsWith( "GetRef", Ordinal ) ) + return !IsNavigationPropertyLink( entitySet, singleton, actionName, FunctionMethod ); + } + + return false; + } + + static bool IsNavigationPropertyLink( IEdmEntitySet? entitySet, IEdmSingleton? singleton, string actionName, string method ) + { + var entities = new List( capacity: 2 ); + + if ( entitySet != null ) + { + entities.Add( entitySet.EntityType() ); + } + + if ( singleton != null ) + { + var entity = singleton.EntityType(); + + if ( entities.Count == 0 || !entities[0].Equals( entity ) ) { - return false; + entities.Add( entity ); } + } - var entity = entitySet.EntityType(); + for ( var i = 0; i < entities.Count; i++ ) + { + var entity = entities[i]; - if ( actionName == ( ActionMethod + entity.Name ) ) + if ( actionName == ( method + entity.Name ) ) { - return false; + return true; } foreach ( var property in entity.NavigationProperties() ) { - if ( actionName.StartsWith( FunctionMethod + property.Name, OrdinalIgnoreCase ) ) + if ( actionName.StartsWith( method + property.Name, OrdinalIgnoreCase ) ) { - return false; + return true; } } - - return true; } return false; @@ -199,10 +222,26 @@ static bool IsActionOrFunction( IEdmEntitySet? entitySet, string actionName, IEn } var qualifiedName = container.Namespace + "." + name; + var entities = new List( capacity: 2 ); + + if ( Singleton != null ) + { + entities.Add( Singleton.EntityType() ); + } if ( EntitySet != null ) { - var operation = EdmModel.FindBoundOperations( qualifiedName, EntitySet.EntityType() ).SingleOrDefault(); + var entity = EntitySet.EntityType(); + + if ( entities.Count == 0 || !entities[0].Equals( entity ) ) + { + entities.Add( entity ); + } + } + + for ( var i = 0; i < entities.Count; i++ ) + { + var operation = EdmModel.FindBoundOperations( qualifiedName, entities[i] ).SingleOrDefault(); if ( operation != null ) { 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 21bb9cbc..311c90c8 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 @@ -11,34 +11,7 @@ partial class ODataRouteBuilder { - void AddOrReplaceNavigationPropertyParameter() - { - var parameters = Context.ParameterDescriptions; - - for ( var i = 0; i < parameters.Count; i++ ) - { - if ( parameters[i].Name.Equals( NavigationProperty, OrdinalIgnoreCase ) ) - { - return; - } - } - - var descriptor = new ODataParameterDescriptor( NavigationProperty, typeof( string ) ) - { - ActionDescriptor = Context.ActionDescriptor, - Configuration = Context.ActionDescriptor.Configuration, - }; - var parameter = new ApiParameterDescription() - { - Name = descriptor.ParameterName, - Source = FromUri, - ParameterDescriptor = descriptor, - }; - - parameters.Add( parameter ); - } - - void AddOrReplaceRefIdQueryParameter() + internal void AddOrReplaceRefIdQueryParameter() { var parameters = Context.ParameterDescriptions; var parameter = default( ApiParameterDescription ); @@ -86,14 +59,15 @@ void AddOrReplaceIdBodyParameter() for ( var i = parameters.Count - 1; i >= 0; i-- ) { - var param = parameters[i]; + parameter = parameters[i]; - if ( param.ParameterDescriptor.ParameterType == type && - param.Source == FromBody ) + if ( parameter.Source == FromBody && + parameter.ParameterDescriptor?.ParameterType == type ) { - parameter = param; break; } + + parameter = default; } if ( parameter == null ) diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs index 9d3ba809..ad03b989 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs @@ -1,6 +1,7 @@ namespace Microsoft.AspNet.OData.Routing { using Microsoft.AspNet.OData; + using Microsoft.AspNet.OData.Routing.Conventions; using Microsoft.Extensions.DependencyInjection; using Microsoft.OData; using Microsoft.OData.Edm; @@ -53,8 +54,9 @@ internal ODataRouteBuilderContext( EdmModel = model; Services = new FixedEdmModelServiceProviderDecorator( Services, model ); EntitySet = container.FindEntitySet( controllerName ); + Singleton = container.FindSingleton( controllerName ); Operation = ResolveOperation( container, actionDescriptor.ActionName ); - ActionType = GetActionType( EntitySet, Operation, actionDescriptor ); + ActionType = GetActionType( actionDescriptor ); IsRouteExcluded = ActionType == ODataRouteActionType.Unknown; if ( Operation?.IsAction() == true ) @@ -63,6 +65,21 @@ internal ODataRouteBuilderContext( } } + internal IODataPathTemplateHandler PathTemplateHandler + { + get + { + if ( templateHandler == null ) + { + var conventions = Services.GetRequiredService>(); + var attribute = conventions.OfType().FirstOrDefault(); + templateHandler = attribute?.ODataPathTemplateHandler ?? new DefaultODataPathHandler(); + } + + return templateHandler; + } + } + IEnumerable GetHttpMethods( HttpActionDescriptor action ) => action.GetHttpMethods( route ).Select( m => m.Method ); void ConvertODataActionParametersToTypedModel( IModelTypeBuilder modelTypeBuilder, IEdmAction action, string controllerName ) diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Web.Http.Description/ODataApiExplorer.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Web.Http.Description/ODataApiExplorer.cs index 938a148a..10afbe1e 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Web.Http.Description/ODataApiExplorer.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/Web.Http.Description/ODataApiExplorer.cs @@ -4,6 +4,7 @@ using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Formatter; using Microsoft.AspNet.OData.Routing; + using Microsoft.AspNet.OData.Routing.Template; using Microsoft.Extensions.DependencyInjection; using Microsoft.OData.Edm; using Microsoft.OData.UriParser; @@ -21,6 +22,7 @@ using System.Web.Http.Routing; using System.Web.Http.Services; using System.Web.Http.ValueProviders; + using static System.StringComparison; using static System.Text.RegularExpressions.RegexOptions; using static System.Web.Http.Description.ApiParameterSource; @@ -258,9 +260,59 @@ void ExploreRouteActions( continue; } - var relativePath = new ODataRouteBuilder( context ).Build(); + var routeBuilder = new ODataRouteBuilder( context ); + var relativePath = routeBuilder.Build(); - PopulateActionDescriptions( action, route, context, relativePath, apiDescriptions, apiVersion ); + if ( routeBuilder.IsNavigationPropertyLink ) + { + var routeTemplates = routeBuilder.ExpandNavigationPropertyLinkTemplate( relativePath ); + var afterPrefix = string.IsNullOrEmpty( context.RoutePrefix ) ? 0 : context.RoutePrefix!.Length + 1; + + for ( var i = 0; i < routeTemplates.Count; i++ ) + { + relativePath = routeTemplates[i]; + + var queryParamAdded = false; + + if ( action.ActionName.StartsWith( "DeleteRef", Ordinal ) ) + { + var handler = context.PathTemplateHandler; + var pathTemplate = handler.ParseTemplate( relativePath.Substring( afterPrefix ), context.Services ); + var template = pathTemplate?.Segments.OfType().FirstOrDefault(); + + if ( template != null ) + { + var property = template.Segment.NavigationProperty; + + if ( property.TargetMultiplicity() == EdmMultiplicity.Many ) + { + routeBuilder.AddOrReplaceRefIdQueryParameter(); + queryParamAdded = true; + } + } + } + + PopulateActionDescriptions( action, route, context, relativePath, apiDescriptions, apiVersion ); + + if ( queryParamAdded ) + { + for ( var j = 0; j < context.ParameterDescriptions.Count; j++ ) + { + var parameter = context.ParameterDescriptions[j]; + + if ( parameter.Name == "$id" || parameter.Name == "id" ) + { + context.ParameterDescriptions.RemoveAt( j ); + break; + } + } + } + } + } + else + { + PopulateActionDescriptions( action, route, context, relativePath, apiDescriptions, apiVersion ); + } } } } 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 e2177672..bf5599e9 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 @@ -17,48 +17,11 @@ partial class ODataRouteBuilder internal string BuildPath() { var segments = new List(); - AppendEntitySetOrOperation( segments ); + AppendPath( segments ); return Join( "/", segments ); } - void AddOrReplaceNavigationPropertyParameter() - { - var parameters = Context.ParameterDescriptions; - var parameter = default( ApiParameterDescription ); - - for ( var i = 0; i < parameters.Count; i++ ) - { - if ( parameters[i].Name.Equals( NavigationProperty, OrdinalIgnoreCase ) ) - { - break; - } - } - - if ( parameter == null ) - { - var type = typeof( string ); - - parameter = new ApiParameterDescription() - { - DefaultValue = default( string ), - IsRequired = true, - ModelMetadata = new ODataQueryOptionModelMetadata( Context.ModelMetadataProvider!, type, description: Empty ), - Name = NavigationProperty, - ParameterDescriptor = new ParameterDescriptor() - { - Name = NavigationProperty, - ParameterType = type, - }, - Type = type, - }; - - parameters.Add( parameter ); - } - - parameter.Source = Path; - } - - void AddOrReplaceRefIdQueryParameter() + internal void AddOrReplaceRefIdQueryParameter() { var parameters = Context.ParameterDescriptions; var parameter = default( ApiParameterDescription ); @@ -101,12 +64,15 @@ void AddOrReplaceIdBodyParameter() for ( var i = parameters.Count - 1; i >= 0; i-- ) { - if ( parameters[i].ParameterDescriptor.ParameterType == type && - parameters[i].Source == Body ) + parameter = parameters[i]; + + if ( parameter.Source == Body && + parameter.ParameterDescriptor?.ParameterType == type ) { - parameter = parameters[i]; break; } + + parameter = default; } type = typeof( ODataId ); diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs index df58ceca..17be6ee8 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs @@ -15,8 +15,6 @@ partial class ODataRouteBuilderContext { - private IODataPathTemplateHandler? templateHandler; - internal ODataRouteBuilderContext( ODataRouteMapping routeMapping, ApiVersion apiVersion, @@ -48,16 +46,17 @@ internal ODataRouteBuilderContext( EdmModel = model; Services = new FixedEdmModelServiceProviderDecorator( Services, model ); EntitySet = container.FindEntitySet( actionDescriptor.ControllerName ); + Singleton = container.FindSingleton( actionDescriptor.ControllerName ); Operation = ResolveOperation( container, actionDescriptor.ActionName ); - ActionType = GetActionType( EntitySet, Operation, actionDescriptor ); + ActionType = GetActionType( actionDescriptor ); IsRouteExcluded = ActionType == ODataRouteActionType.Unknown; } - static IEnumerable GetHttpMethods( ControllerActionDescriptor action ) => action.GetHttpMethods(); - internal IODataPathTemplateHandler PathTemplateHandler => templateHandler ??= Services.GetRequiredService(); + static IEnumerable GetHttpMethods( ControllerActionDescriptor action ) => action.GetHttpMethods(); + internal IModelMetadataProvider? ModelMetadataProvider { get; set; } } } \ 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 9d05201e..744fdda0 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 @@ -27,6 +27,7 @@ using static Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource; using static System.Linq.Enumerable; using static System.StringComparison; + using static Microsoft.AspNet.OData.Routing.ODataRouteConstants; /// /// Represents an API explorer that provides API descriptions for actions represented by @@ -301,11 +302,35 @@ static bool IsMappedTo( ControllerActionDescriptor action, ODataRouteMapping map { var relativePath = action.AttributeRouteInfo?.Template; - // note: if path happens to be built ahead of time, it's expected to be qualified; rebuild it as necessary - if ( string.IsNullOrEmpty( relativePath ) || !routeContext.Options.UseQualifiedNames ) + if ( !string.IsNullOrEmpty( relativePath ) && routeContext.Options.UseQualifiedNames ) { - var builder = new ODataRouteBuilder( routeContext ); - relativePath = builder.Build(); + return relativePath; + } + + var builder = new ODataRouteBuilder( routeContext ); + + relativePath = builder.Build(); + + if ( builder.IsNavigationPropertyLink && action.AttributeRouteInfo is ODataAttributeRouteInfo info ) + { + var template = info.ODataTemplate?.Segments.OfType().FirstOrDefault(); + + if ( template == null ) + { + return relativePath; + } + + var key = string.Concat( "{", NavigationProperty, "}" ); + var property = template.Segment.NavigationProperty; + var value = property.Name; + + relativePath = relativePath.Replace( key, value, OrdinalIgnoreCase ); + + if ( action.ActionName.StartsWith( "DeleteRef", Ordinal ) && + property.TargetMultiplicity() == EdmMultiplicity.Many ) + { + builder.AddOrReplaceRefIdQueryParameter(); + } } return relativePath; @@ -530,40 +555,31 @@ static void UpdateBindingInfo( ApiParameterContext context, ParameterDescriptor var parameterType = parameter.ParameterType; var bindingInfo = parameter.BindingInfo; - static bool IsSpecialBindingSource( BindingInfo info, Type type ) + if ( bindingInfo == null ) { - if ( info == null ) - { - return false; - } - - if ( ( type.IsODataQueryOptions() || type.IsODataPath() ) && info.BindingSource == Custom ) - { - info.BindingSource = Special; - return true; - } - - return false; + parameter.BindingInfo = bindingInfo = new BindingInfo() { BindingSource = metadata.BindingSource }; } - - if ( IsSpecialBindingSource( bindingInfo, parameterType ) ) + else if ( bindingInfo.BindingSource == null ) { - return; + bindingInfo.BindingSource = metadata.BindingSource; } - if ( bindingInfo == null ) + if ( bindingInfo.BindingSource == Custom ) { - parameter.BindingInfo = bindingInfo = new BindingInfo() { BindingSource = metadata.BindingSource }; - - if ( IsSpecialBindingSource( bindingInfo, parameterType ) ) + if ( parameterType.IsODataQueryOptions() || parameterType.IsODataPath() ) { - return; + bindingInfo.BindingSource = Special; } } + if ( bindingInfo.BindingSource != null ) + { + return; + } + var key = default( IEdmNamedElement ); var paramName = parameter.Name; - var source = bindingInfo.BindingSource; + var source = Query; switch ( context.RouteContext.ActionType ) { @@ -615,14 +631,10 @@ static bool IsSpecialBindingSource( BindingInfo info, Type type ) source = Path; } - break; - default: - source = Query; break; } - bindingInfo.BindingSource = source ?? Query; - parameter.BindingInfo = bindingInfo; + bindingInfo.BindingSource = source; } IReadOnlyList GetApiResponseTypes( diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionParameterContext.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionParameterContext.cs index f697420b..bf05dcf3 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionParameterContext.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionParameterContext.cs @@ -1,26 +1,68 @@ namespace Microsoft.AspNet.OData.Routing { - using Microsoft.AspNet.OData.Routing.Template; using System; + using System.Collections.Generic; sealed class ActionParameterContext { - readonly ODataPathTemplate? pathTemplate; - internal ActionParameterContext( ODataRouteBuilder routeBuilder, ODataRouteBuilderContext routeContext ) { - var odataPathTemplate = routeBuilder.BuildPath( includePrefix: false ); - RouteContext = routeContext; - pathTemplate = RouteContext.PathTemplateHandler.SafeParseTemplate( odataPathTemplate, Services ); + + var template = routeBuilder.BuildPath( includePrefix: false ); + var prefix = routeBuilder.GetRoutePrefix(); + + if ( routeBuilder.IsNavigationPropertyLink ) + { + var routeTemplates = routeBuilder.ExpandNavigationPropertyLinkTemplate( template ); + var templates = new List( capacity: routeTemplates.Count ); + + for ( var i = 0; i < routeTemplates.Count; i++ ) + { + var routeTemplate = routeTemplates[i]; + var pathTemplate = routeContext.PathTemplateHandler.SafeParseTemplate( routeTemplate, Services ); + + if ( pathTemplate == null ) + { + continue; + } + + if ( !string.IsNullOrEmpty( prefix ) ) + { + routeTemplate = string.Concat( prefix, "/", routeTemplate ); + } + + templates.Add( new ActionTemplates( routeTemplate, pathTemplate ) ); + } + + Templates = templates.ToArray(); + } + else + { + var pathTemplate = routeContext.PathTemplateHandler.SafeParseTemplate( template, Services ); + + if ( pathTemplate == null ) + { + Templates = Array.Empty(); + } + else + { + if ( !string.IsNullOrEmpty( prefix ) ) + { + template = string.Concat( prefix, "/", template ); + } + + Templates = new[] { new ActionTemplates( template, pathTemplate ) }; + } + } } internal ODataRouteBuilderContext RouteContext { get; } internal IServiceProvider Services => RouteContext.Services; - internal ODataPathTemplate PathTemplate => pathTemplate ?? throw new NotSupportedException(); + internal IReadOnlyList Templates { get; } - internal bool IsSupported => pathTemplate != null; + internal bool IsSupported => Templates.Count > 0; } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionTemplates.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionTemplates.cs new file mode 100644 index 00000000..f3fd9aef --- /dev/null +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ActionTemplates.cs @@ -0,0 +1,17 @@ +namespace Microsoft.AspNet.OData.Routing +{ + using Microsoft.AspNet.OData.Routing.Template; + + sealed class ActionTemplates + { + internal ActionTemplates( string routeTemplate, ODataPathTemplate pathTemplate ) + { + RouteTemplate = routeTemplate; + PathTemplate = pathTemplate; + } + + internal string RouteTemplate { get; } + + internal ODataPathTemplate PathTemplate { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBindingInfoConvention.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBindingInfoConvention.cs index c6223d88..70dc1fb7 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBindingInfoConvention.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBindingInfoConvention.cs @@ -40,6 +40,8 @@ internal ODataRouteBindingInfoConvention( ODataApiVersioningOptions Options => options.Value; + ODataAttributeRouteInfoComparer Comparer { get; } = new ODataAttributeRouteInfoComparer(); + public void Apply( ActionDescriptorProviderContext context, ControllerActionDescriptor action ) { var model = action.GetApiVersionModel( Explicit | Implicit ); @@ -56,67 +58,60 @@ public void Apply( ActionDescriptorProviderContext context, ControllerActionDesc } } - IEnumerable ExpandVersionedActions( ControllerActionDescriptor action, ApiVersionModel model ) + static ControllerActionDescriptor Clone( ControllerActionDescriptor action, AttributeRouteInfo attributeRouteInfo ) { - var mappings = RouteCollectionProvider.Items; - var routeInfos = new HashSet( new ODataAttributeRouteInfoComparer() ); - var declaredVersions = model.DeclaredApiVersions; - var metadata = action.ControllerTypeInfo.IsMetadataController(); - - for ( var i = 0; i < declaredVersions.Count; i++ ) + var clone = new ControllerActionDescriptor() { - for ( var j = 0; j < mappings.Count; j++ ) - { - var mapping = mappings[j]; - var selector = mapping.ModelSelector; - - if ( !selector.Contains( declaredVersions[i] ) ) - { - continue; - } + ActionConstraints = action.ActionConstraints, + ActionName = action.ActionName, + AttributeRouteInfo = attributeRouteInfo, + BoundProperties = action.BoundProperties, + ControllerName = action.ControllerName, + ControllerTypeInfo = action.ControllerTypeInfo, + DisplayName = action.DisplayName, + FilterDescriptors = action.FilterDescriptors, + MethodInfo = action.MethodInfo, + Parameters = action.Parameters, + Properties = action.Properties, + RouteValues = action.RouteValues, + }; - if ( metadata ) - { - UpdateBindingInfo( action, mapping, routeInfos ); - } - else - { - var mappedVersions = selector.ApiVersions; + return clone; + } - for ( var k = 0; k < mappedVersions.Count; k++ ) - { - UpdateBindingInfo( action, mappedVersions[k], mapping, routeInfos ); - } - } - } + static void UpdateControllerName( ControllerActionDescriptor action ) + { + if ( !action.RouteValues.TryGetValue( "controller", out var key ) ) + { + key = action.ControllerName; } - return routeInfos; + action.ControllerName = TrimTrailingNumbers( key ); } - IEnumerable ExpandVersionNeutralActions( ControllerActionDescriptor action ) + static string TrimTrailingNumbers( string name ) { - var mappings = RouteCollectionProvider.Items; - var routeInfos = new HashSet( new ODataAttributeRouteInfoComparer() ); - var visited = new HashSet(); - - for ( var i = 0; i < mappings.Count; i++ ) + if ( string.IsNullOrEmpty( name ) ) { - var mapping = mappings[i]; - var mappedVersions = mapping.ModelSelector.ApiVersions; + return name; + } - for ( var j = 0; j < mappedVersions.Count; j++ ) - { - var apiVersion = mappedVersions[j]; + var last = name.Length - 1; - if ( visited.Add( apiVersion ) ) + for ( var i = last; i >= 0; i-- ) + { + if ( !char.IsNumber( name[i] ) ) + { + if ( i < last ) { - UpdateBindingInfo( action, apiVersion, mapping, routeInfos ); + return name.Substring( 0, i + 1 ); } + + return name; } } - return routeInfos; + return name; } static void UpdateBindingInfo( @@ -187,15 +182,21 @@ void UpdateBindingInfo( UpdateBindingInfo( parameterContext, action.Parameters[i] ); } - var routeInfo = new ODataAttributeRouteInfo() + var templates = parameterContext.Templates; + + for ( var i = 0; i < templates.Count; i++ ) { - Name = mapping.RouteName, - Template = routeBuilder.BuildPath( includePrefix: true ), - ODataTemplate = parameterContext.PathTemplate, - RoutePrefix = mapping.RoutePrefix, - }; + var template = templates[i]; + var routeInfo = new ODataAttributeRouteInfo() + { + Name = mapping.RouteName, + Template = template.RouteTemplate, + ODataTemplate = template.PathTemplate, + RoutePrefix = mapping.RoutePrefix, + }; - routeInfos.Add( routeInfo ); + routeInfos.Add( routeInfo ); + } } void UpdateBindingInfo( ActionParameterContext context, ParameterDescriptor parameter ) @@ -203,27 +204,30 @@ void UpdateBindingInfo( ActionParameterContext context, ParameterDescriptor para var parameterType = parameter.ParameterType; var bindingInfo = parameter.BindingInfo; - if ( bindingInfo != null ) + if ( bindingInfo == null || bindingInfo.BindingSource == null ) { - if ( ( parameterType.IsODataQueryOptions() || parameterType.IsODataPath() ) && bindingInfo.BindingSource == Custom ) + var metadata = ModelMetadataProvider.GetMetadataForType( parameterType ); + + if ( bindingInfo == null ) { - bindingInfo.BindingSource = Special; + parameter.BindingInfo = bindingInfo = new BindingInfo() { BindingSource = metadata.BindingSource }; + } + else + { + bindingInfo.BindingSource = metadata.BindingSource; } - - return; } - var metadata = ModelMetadataProvider.GetMetadataForType( parameterType ); - - parameter.BindingInfo = bindingInfo = new BindingInfo() { BindingSource = metadata.BindingSource }; - - if ( bindingInfo.BindingSource != null ) + if ( bindingInfo.BindingSource == Custom ) { - if ( ( parameterType.IsODataQueryOptions() || parameterType.IsODataPath() ) && bindingInfo.BindingSource == Custom ) + if ( parameterType.IsODataQueryOptions() || parameterType.IsODataPath() ) { bindingInfo.BindingSource = Special; } + } + if ( bindingInfo.BindingSource != null ) + { return; } @@ -241,16 +245,12 @@ void UpdateBindingInfo( ActionParameterContext context, ParameterDescriptor para if ( key == null ) { - var template = context.PathTemplate; + var template = context.Templates[0].PathTemplate; + var segments = template.Segments.OfType(); - if ( template != null ) + if ( segments.SelectMany( s => s.ParameterMappings.Values ).Any( name => name.Equals( paramName, OrdinalIgnoreCase ) ) ) { - var segments = template.Segments.OfType(); - - if ( segments.SelectMany( s => s.ParameterMappings.Values ).Any( name => name.Equals( paramName, OrdinalIgnoreCase ) ) ) - { - source = Path; - } + source = Path; } } else @@ -287,63 +287,69 @@ void UpdateBindingInfo( ActionParameterContext context, ParameterDescriptor para } bindingInfo.BindingSource = source; - parameter.BindingInfo = bindingInfo; } - static ControllerActionDescriptor Clone( ControllerActionDescriptor action, AttributeRouteInfo attributeRouteInfo ) + IEnumerable ExpandVersionedActions( ControllerActionDescriptor action, ApiVersionModel model ) { - var clone = new ControllerActionDescriptor() + var mappings = RouteCollectionProvider.Items; + var routeInfos = new HashSet( Comparer ); + var declaredVersions = model.DeclaredApiVersions; + var metadata = action.ControllerTypeInfo.IsMetadataController(); + + for ( var i = 0; i < declaredVersions.Count; i++ ) { - ActionConstraints = action.ActionConstraints, - ActionName = action.ActionName, - AttributeRouteInfo = attributeRouteInfo, - BoundProperties = action.BoundProperties, - ControllerName = action.ControllerName, - ControllerTypeInfo = action.ControllerTypeInfo, - DisplayName = action.DisplayName, - FilterDescriptors = action.FilterDescriptors, - MethodInfo = action.MethodInfo, - Parameters = action.Parameters, - Properties = action.Properties, - RouteValues = action.RouteValues, - }; + for ( var j = 0; j < mappings.Count; j++ ) + { + var mapping = mappings[j]; + var selector = mapping.ModelSelector; - return clone; - } + if ( !selector.Contains( declaredVersions[i] ) ) + { + continue; + } - static void UpdateControllerName( ControllerActionDescriptor action ) - { - if ( !action.RouteValues.TryGetValue( "controller", out var key ) ) - { - key = action.ControllerName; + if ( metadata ) + { + UpdateBindingInfo( action, mapping, routeInfos ); + } + else + { + var mappedVersions = selector.ApiVersions; + + for ( var k = 0; k < mappedVersions.Count; k++ ) + { + UpdateBindingInfo( action, mappedVersions[k], mapping, routeInfos ); + } + } + } } - action.ControllerName = TrimTrailingNumbers( key ); + return routeInfos; } - static string TrimTrailingNumbers( string name ) + IEnumerable ExpandVersionNeutralActions( ControllerActionDescriptor action ) { - if ( string.IsNullOrEmpty( name ) ) - { - return name; - } - - var last = name.Length - 1; + var mappings = RouteCollectionProvider.Items; + var routeInfos = new HashSet( Comparer ); + var visited = new HashSet(); - for ( var i = last; i >= 0; i-- ) + for ( var i = 0; i < mappings.Count; i++ ) { - if ( !char.IsNumber( name[i] ) ) + var mapping = mappings[i]; + var mappedVersions = mapping.ModelSelector.ApiVersions; + + for ( var j = 0; j < mappedVersions.Count; j++ ) { - if ( i < last ) + var apiVersion = mappedVersions[j]; + + if ( visited.Add( apiVersion ) ) { - return name.Substring( 0, i + 1 ); + UpdateBindingInfo( action, apiVersion, mapping, routeInfos ); } - - return name; } } - return name; + return routeInfos; } sealed class ODataAttributeRouteInfoComparer : IEqualityComparer 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 e325128c..17a6860a 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 @@ -15,7 +15,7 @@ internal string BuildPath( bool includePrefix ) AppendRoutePrefix( segments ); } - AppendEntitySetOrOperation( segments ); + AppendPath( segments ); return Join( "/", segments ); } diff --git a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilderContext.Core.cs b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilderContext.Core.cs index e118b8e9..906c098b 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilderContext.Core.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning/AspNet.OData/Routing/ODataRouteBuilderContext.Core.cs @@ -14,8 +14,6 @@ partial class ODataRouteBuilderContext { - private IODataPathTemplateHandler? templateHandler; - internal ODataRouteBuilderContext( ApiVersion apiVersion, ODataRouteMapping routeMapping, @@ -45,8 +43,9 @@ internal ODataRouteBuilderContext( EdmModel = model; Services = new FixedEdmModelServiceProviderDecorator( Services, model ); EntitySet = container.FindEntitySet( actionDescriptor.ControllerName ); + Singleton = container.FindSingleton( actionDescriptor.ControllerName ); Operation = ResolveOperation( container, actionDescriptor.ActionName ); - ActionType = GetActionType( EntitySet, Operation, actionDescriptor ); + ActionType = GetActionType( actionDescriptor ); IsRouteExcluded = ActionType == ODataRouteActionType.Unknown; } diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Description/ODataApiExplorerTest.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Description/ODataApiExplorerTest.cs index c5a33009..0d66e216 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Description/ODataApiExplorerTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Description/ODataApiExplorerTest.cs @@ -157,10 +157,10 @@ public void api_description_group_should_explore_navigation_properties() new { HttpMethod = Get, Version, RelativePath = "api/Suppliers/{key}" }, new { HttpMethod = Get, Version, RelativePath = "api/Products/{key}/Supplier" }, new { HttpMethod = Get, Version, RelativePath = "api/Suppliers/{key}/Products" }, - new { HttpMethod = Get, Version, RelativePath = "api/Products/{key}/Supplier/$ref" }, + new { HttpMethod = Get, Version, RelativePath = "api/Products/{key}/supplier/$ref" }, new { HttpMethod = Put, Version, RelativePath = "api/Products/{key}" }, new { HttpMethod = Put, Version, RelativePath = "api/Suppliers/{key}" }, - new { HttpMethod = Put, Version, RelativePath = "api/Products/{key}/Supplier/$ref" }, + new { HttpMethod = Put, Version, RelativePath = "api/Products/{key}/supplier/$ref" }, new { HttpMethod = Post, Version, RelativePath = "api/Products" }, new { HttpMethod = Post, Version, RelativePath = "api/Suppliers" }, new { HttpMethod = Post, Version, RelativePath = "api/Suppliers/{key}/Products/$ref" }, @@ -168,7 +168,7 @@ public void api_description_group_should_explore_navigation_properties() new { HttpMethod = Patch, Version, RelativePath = "api/Suppliers/{key}" }, new { HttpMethod = Delete, Version, RelativePath = "api/Products/{key}" }, new { HttpMethod = Delete, Version, RelativePath = "api/Suppliers/{key}" }, - new { HttpMethod = Delete, Version, RelativePath = "api/Products/{key}/Supplier/$ref" }, + new { HttpMethod = Delete, Version, RelativePath = "api/Products/{key}/supplier/$ref" }, new { HttpMethod = Delete, Version, RelativePath = "api/Suppliers/{key}/Products/$ref?$id={$id}" }, }, options => options.ExcludingMissingMembers() ); diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/V3/ProductsController.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/V3/ProductsController.cs index 65e34d3e..2764f363 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/V3/ProductsController.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/Web.Http/Simulators/V3/ProductsController.cs @@ -133,7 +133,7 @@ public IHttpActionResult Put( [FromODataUri] int key, [FromBody] Product update /// The supplier to link. /// The supplier link. [ResponseType( typeof( ODataId ) )] - public IHttpActionResult GetRefToSupplier( [FromODataUri] int key, string navigationProperty ) + public IHttpActionResult GetRef( [FromODataUri] int key, string navigationProperty ) { var routeName = Request.ODataProperties().RouteName; var url = Request.RequestUri; @@ -164,7 +164,7 @@ public IHttpActionResult GetRefToSupplier( [FromODataUri] int key, string naviga /// The supplier identifier. /// None [HttpPut] - public IHttpActionResult CreateRefToSupplier( [FromODataUri] int key, string navigationProperty, [FromBody] Uri link ) => StatusCode( NoContent ); + public IHttpActionResult CreateRef( [FromODataUri] int key, string navigationProperty, [FromBody] Uri link ) => StatusCode( NoContent ); /// /// Unlinks a supplier from a product. @@ -172,7 +172,7 @@ public IHttpActionResult GetRefToSupplier( [FromODataUri] int key, string naviga /// The product identifier. /// The supplier to unlink. /// None - public IHttpActionResult DeleteRefToSupplier( [FromODataUri] int key, string navigationProperty ) => StatusCode( NoContent ); + public IHttpActionResult DeleteRef( [FromODataUri] int key, string navigationProperty ) => StatusCode( NoContent ); static Product NewProduct( int id ) => new Product() diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V3/ProductsController.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V3/ProductsController.cs index c59ab560..cd382b5f 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V3/ProductsController.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Simulators/V3/ProductsController.cs @@ -149,7 +149,7 @@ public IActionResult Put( [FromODataUri] int key, [FromBody] Product update ) [Produces( "application/json" )] [ProducesResponseType( typeof( ODataId ), Status200OK )] [ProducesResponseType( Status404NotFound )] - public IActionResult GetRefToSupplier( [FromODataUri] int key, [FromODataUri] string navigationProperty ) + public IActionResult GetRef( [FromODataUri] int key, [FromODataUri] string navigationProperty ) { var segments = Request.ODataFeature().Path.Segments.ToArray(); var entitySet = ( (EntitySetSegment) segments[0] ).EntitySet; @@ -172,7 +172,7 @@ public IActionResult GetRefToSupplier( [FromODataUri] int key, [FromODataUri] st [HttpPut] [ProducesResponseType( Status204NoContent )] [ProducesResponseType( Status404NotFound )] - public IActionResult CreateRefToSupplier( [FromODataUri] int key, [FromODataUri] string navigationProperty, [FromBody] Uri link ) => NoContent(); + public IActionResult CreateRef( [FromODataUri] int key, [FromODataUri] string navigationProperty, [FromBody] Uri link ) => NoContent(); /// /// Unlinks a supplier from a product. @@ -182,7 +182,7 @@ public IActionResult GetRefToSupplier( [FromODataUri] int key, [FromODataUri] st /// None [ProducesResponseType( Status204NoContent )] [ProducesResponseType( Status404NotFound )] - public IActionResult DeleteRefToSupplier( [FromODataUri] int key, [FromODataUri] string navigationProperty ) => NoContent(); + public IActionResult DeleteRef( [FromODataUri] int key, [FromODataUri] string navigationProperty ) => NoContent(); static Product NewProduct( int id ) => new Product() diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProviderTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProviderTest.cs index 1345c84b..f1990fb5 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNetCore.Mvc.ApiExplorer/ODataApiDescriptionProviderTest.cs @@ -131,9 +131,9 @@ private void AssertVersion3( ApiDescriptionGroup group ) new { HttpMethod = "PUT", GroupName, RelativePath = "api/Products/{key}" }, new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Products/{key}" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/Products/{key}/Supplier" }, - new { HttpMethod = "GET", GroupName, RelativePath = "api/Products/{key}/Supplier/$ref" }, - new { HttpMethod = "PUT", GroupName, RelativePath = "api/Products/{key}/Supplier/$ref" }, - new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Products/{key}/Supplier/$ref" }, + new { HttpMethod = "GET", GroupName, RelativePath = "api/Products/{key}/supplier/$ref" }, + new { HttpMethod = "PUT", GroupName, RelativePath = "api/Products/{key}/supplier/$ref" }, + new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Products/{key}/supplier/$ref" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/Suppliers" }, new { HttpMethod = "GET", GroupName, RelativePath = "api/Suppliers/{key}" }, new { HttpMethod = "POST", GroupName, RelativePath = "api/Suppliers" }, From c228bce20c5fb87916b9c75166c7328c8e44b1dd Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sat, 10 Oct 2020 00:36:49 -0700 Subject: [PATCH 11/13] Update OData Swagger samples --- .../aspnetcore/SwaggerODataSample/Startup.cs | 23 +++++-------------- .../SwaggerODataSample/V1/OrdersController.cs | 4 ++-- .../SwaggerODataSample/V2/OrdersController.cs | 8 +++---- .../SwaggerODataSample/V3/OrdersController.cs | 10 ++++---- .../V3/SuppliersController.cs | 2 +- .../V3/SuppliersController.cs | 2 +- 6 files changed, 19 insertions(+), 30 deletions(-) diff --git a/samples/aspnetcore/SwaggerODataSample/Startup.cs b/samples/aspnetcore/SwaggerODataSample/Startup.cs index e24224a0..6048ba46 100644 --- a/samples/aspnetcore/SwaggerODataSample/Startup.cs +++ b/samples/aspnetcore/SwaggerODataSample/Startup.cs @@ -1,6 +1,5 @@ namespace Microsoft.Examples { - using Microsoft.AspNet.OData; using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNetCore.Builder; @@ -12,8 +11,6 @@ using System.IO; using System.Reflection; using static Microsoft.AspNet.OData.Query.AllowedQueryOptions; - using static Microsoft.AspNetCore.Mvc.CompatibilityVersion; - using static Microsoft.OData.ODataUrlKeyDelimiter; /// /// Represents the startup process for the application. @@ -26,9 +23,7 @@ public class Startup /// The collection of services to configure the application with. public void ConfigureServices( IServiceCollection services ) { - // the sample application always uses the latest version, but you may want an explicit version such as Version_2_2 - // note: Endpoint Routing is enabled by default; however, it is unsupported by OData and MUST be false - services.AddMvc( options => options.EnableEndpointRouting = false ).SetCompatibilityVersion( Latest ); + services.AddControllers(); services.AddApiVersioning( options => options.ReportApiVersions = true ); services.AddOData().EnableApiVersioning(); services.AddODataApiExplorer( @@ -75,18 +70,12 @@ public void ConfigureServices( IServiceCollection services ) /// The API version descriptor provider used to enumerate defined API versions. public void Configure( IApplicationBuilder app, VersionedODataModelBuilder modelBuilder, IApiVersionDescriptionProvider provider ) { - app.UseMvc( - routeBuilder => + app.UseRouting(); + app.UseEndpoints( + endpoints => { - // the following will not work as expected - // BUG: https://github.com/OData/WebApi/issues/1837 - // routeBuilder.SetDefaultODataOptions( new ODataOptions() { UrlKeyDelimiter = Parentheses } ); - routeBuilder.ServiceProvider.GetRequiredService().UrlKeyDelimiter = Parentheses; - - // global odata query options - routeBuilder.Count(); - - routeBuilder.MapVersionedODataRoute( "odata", "api", modelBuilder ); + endpoints.Count(); + endpoints.MapVersionedODataRoute( "odata", "api", modelBuilder ); } ); app.UseSwagger(); app.UseSwaggerUI( diff --git a/samples/aspnetcore/SwaggerODataSample/V1/OrdersController.cs b/samples/aspnetcore/SwaggerODataSample/V1/OrdersController.cs index 848f2323..bcd4b242 100644 --- a/samples/aspnetcore/SwaggerODataSample/V1/OrdersController.cs +++ b/samples/aspnetcore/SwaggerODataSample/V1/OrdersController.cs @@ -24,7 +24,7 @@ public class OrdersController : ODataController /// The requested order. /// The order was successfully retrieved. /// The order does not exist. - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [Produces( "application/json" )] [ProducesResponseType( typeof( Order ), Status200OK )] [ProducesResponseType( Status404NotFound )] @@ -78,7 +78,7 @@ public IActionResult Post( [FromBody] Order order ) /// The line items were successfully retrieved. /// The order does not exist. [HttpGet] - [ODataRoute( "({key})/LineItems" )] + [ODataRoute( "{key}/LineItems" )] [Produces( "application/json" )] [ProducesResponseType( typeof( ODataValue> ), Status200OK )] [ProducesResponseType( Status404NotFound )] diff --git a/samples/aspnetcore/SwaggerODataSample/V2/OrdersController.cs b/samples/aspnetcore/SwaggerODataSample/V2/OrdersController.cs index d2ebf83c..4c6c8ff5 100644 --- a/samples/aspnetcore/SwaggerODataSample/V2/OrdersController.cs +++ b/samples/aspnetcore/SwaggerODataSample/V2/OrdersController.cs @@ -45,7 +45,7 @@ public IQueryable Get() /// The requested order. /// The order was successfully retrieved. /// The order does not exist. - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [Produces( "application/json" )] [ProducesResponseType( typeof( Order ), Status200OK )] [ProducesResponseType( Status404NotFound )] @@ -84,7 +84,7 @@ public IActionResult Post( [FromBody] Order order ) /// The order was successfully updated. /// The order is invalid. /// The order does not exist. - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [Produces( "application/json" )] [ProducesResponseType( typeof( Order ), Status200OK )] [ProducesResponseType( Status204NoContent )] @@ -128,7 +128,7 @@ public IActionResult Patch( int key, Delta delta ) /// The parameters are invalid. /// The order does not exist. [HttpPost] - [ODataRoute( "({key})/Rate" )] + [ODataRoute( "{key}/Rate" )] [ProducesResponseType( Status204NoContent )] [ProducesResponseType( Status400BadRequest )] [ProducesResponseType( Status404NotFound )] @@ -151,7 +151,7 @@ public IActionResult Rate( int key, ODataActionParameters parameters ) /// The line items were successfully retrieved. /// The order does not exist. [HttpGet] - [ODataRoute( "({key})/LineItems" )] + [ODataRoute( "{key}/LineItems" )] [Produces( "application/json" )] [ProducesResponseType( typeof( ODataValue> ), Status200OK )] [ProducesResponseType( Status404NotFound )] diff --git a/samples/aspnetcore/SwaggerODataSample/V3/OrdersController.cs b/samples/aspnetcore/SwaggerODataSample/V3/OrdersController.cs index ac53f6ea..9b6270fb 100644 --- a/samples/aspnetcore/SwaggerODataSample/V3/OrdersController.cs +++ b/samples/aspnetcore/SwaggerODataSample/V3/OrdersController.cs @@ -46,7 +46,7 @@ public IQueryable Get() /// The requested order. /// The order was successfully retrieved. /// The order does not exist. - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [Produces( "application/json" )] [ProducesResponseType( typeof( Order ), Status200OK )] [ProducesResponseType( Status404NotFound )] @@ -84,7 +84,7 @@ public IActionResult Post( [FromBody] Order order ) /// The order was successfully updated. /// The order is invalid. /// The order does not exist. - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [Produces( "application/json" )] [ProducesResponseType( typeof( Order), Status200OK )] [ProducesResponseType( Status204NoContent )] @@ -112,7 +112,7 @@ public IActionResult Patch( int key, Delta delta ) /// None /// The order was successfully canceled. /// The order does not exist. - [ODataRoute( "({key})" )] + [ODataRoute( "{key}" )] [ProducesResponseType( Status204NoContent )] [ProducesResponseType( Status404NotFound )] public IActionResult Delete( int key, bool suspendOnly ) => NoContent(); @@ -141,7 +141,7 @@ public IActionResult Patch( int key, Delta delta ) /// The parameters are invalid. /// The order does not exist. [HttpPost] - [ODataRoute( "({key})/Rate" )] + [ODataRoute( "{key}/Rate" )] [ProducesResponseType( Status204NoContent )] [ProducesResponseType( Status400BadRequest )] [ProducesResponseType( Status404NotFound )] @@ -164,7 +164,7 @@ public IActionResult Rate( int key, ODataActionParameters parameters ) /// The line items were successfully retrieved. /// The order does not exist. [HttpGet] - [ODataRoute( "({key})/LineItems" )] + [ODataRoute( "{key}/LineItems" )] [Produces( "application/json" )] [ProducesResponseType( typeof( ODataValue> ), Status200OK )] [ProducesResponseType( Status404NotFound )] diff --git a/samples/aspnetcore/SwaggerODataSample/V3/SuppliersController.cs b/samples/aspnetcore/SwaggerODataSample/V3/SuppliersController.cs index bd865488..6802d62e 100644 --- a/samples/aspnetcore/SwaggerODataSample/V3/SuppliersController.cs +++ b/samples/aspnetcore/SwaggerODataSample/V3/SuppliersController.cs @@ -20,7 +20,7 @@ public class SuppliersController : ODataController /// Retrieves all suppliers. /// /// All available suppliers. - /// Products successfully retrieved. + /// Suppliers successfully retrieved. [EnableQuery] [Produces( "application/json" )] [ProducesResponseType( typeof( ODataValue> ), Status200OK )] diff --git a/samples/webapi/SwaggerODataWebApiSample/V3/SuppliersController.cs b/samples/webapi/SwaggerODataWebApiSample/V3/SuppliersController.cs index 1fcf7e09..38a4ec33 100644 --- a/samples/webapi/SwaggerODataWebApiSample/V3/SuppliersController.cs +++ b/samples/webapi/SwaggerODataWebApiSample/V3/SuppliersController.cs @@ -22,7 +22,7 @@ public class SuppliersController : ODataController /// Retrieves all suppliers. /// /// All available suppliers. - /// Products successfully retrieved. + /// Suppliers were successfully retrieved. [EnableQuery] [ResponseType( typeof( ODataValue> ) )] public IQueryable Get() => suppliers; From f6e198520e04c632a863418c6c77ada2d8236c7c Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sat, 10 Oct 2020 00:37:34 -0700 Subject: [PATCH 12/13] Add singleton example --- .../Configuration/SupplierConfiguration.cs | 3 +- .../SwaggerODataSample/V3/AcmeController.cs | 75 +++++++++++++++++++ .../Configuration/SupplierConfiguration.cs | 3 +- .../V3/AcmeController.cs | 70 +++++++++++++++++ 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 samples/aspnetcore/SwaggerODataSample/V3/AcmeController.cs create mode 100644 samples/webapi/SwaggerODataWebApiSample/V3/AcmeController.cs diff --git a/samples/aspnetcore/SwaggerODataSample/Configuration/SupplierConfiguration.cs b/samples/aspnetcore/SwaggerODataSample/Configuration/SupplierConfiguration.cs index 42696239..c25bf345 100644 --- a/samples/aspnetcore/SwaggerODataSample/Configuration/SupplierConfiguration.cs +++ b/samples/aspnetcore/SwaggerODataSample/Configuration/SupplierConfiguration.cs @@ -17,7 +17,8 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string rout return; } - var supplier = builder.EntitySet( "Suppliers" ).EntityType.HasKey( p => p.Id ); + builder.EntitySet( "Suppliers" ).EntityType.HasKey( p => p.Id ); + builder.Singleton( "Acme" ); } } } \ No newline at end of file diff --git a/samples/aspnetcore/SwaggerODataSample/V3/AcmeController.cs b/samples/aspnetcore/SwaggerODataSample/V3/AcmeController.cs new file mode 100644 index 00000000..5766d06e --- /dev/null +++ b/samples/aspnetcore/SwaggerODataSample/V3/AcmeController.cs @@ -0,0 +1,75 @@ +namespace Microsoft.Examples.V3 +{ + using Microsoft.AspNet.OData; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Examples.Models; + using System; + using System.Collections.Generic; + using System.Linq; + using static Microsoft.AspNetCore.Http.StatusCodes; + + /// + /// Represents a RESTful service for the ACME supplier. + /// + [ApiVersion( "3.0" )] + public class AcmeController : ODataController + { + // + /// Retrieves the ACME supplier. + /// + /// All available suppliers. + /// The supplier successfully retrieved. + [EnableQuery] + [Produces( "application/json" )] + [ProducesResponseType( typeof( ODataValue ), Status200OK )] + public IActionResult Get() => Ok( NewSupplier() ); + + /// + /// Gets the products associated with the supplier. + /// + /// The associated supplier products. + [EnableQuery] + public IQueryable GetProducts() => NewSupplier().Products.AsQueryable(); + + /// + /// Links a product to a supplier. + /// + /// The product to link. + /// The product identifier. + /// None + [HttpPost] + [ProducesResponseType( Status204NoContent )] + [ProducesResponseType( Status404NotFound )] + public IActionResult CreateRef( [FromODataUri] string navigationProperty, [FromBody] Uri link ) => NoContent(); + + // TODO: OData doesn't seem to currently support this action in ASP.NET Core, but it works in Web API + + /// + /// Unlinks a product from a supplier. + /// + /// The related product identifier. + /// The product to unlink. + /// None + [ProducesResponseType( Status204NoContent )] + [ProducesResponseType( Status404NotFound )] + public IActionResult DeleteRef( [FromODataUri] string relatedKey, string navigationProperty ) => NoContent(); + + private static Supplier NewSupplier() => + new Supplier() + { + Id = 42, + Name = "Acme", + Products = new List() + { + new Product() + { + Id = 42, + Name = "Product 42", + Category = "Test", + Price = 42, + SupplierId = 42, + } + }, + }; + } +} diff --git a/samples/webapi/SwaggerODataWebApiSample/Configuration/SupplierConfiguration.cs b/samples/webapi/SwaggerODataWebApiSample/Configuration/SupplierConfiguration.cs index 3d64d074..79b1efdd 100644 --- a/samples/webapi/SwaggerODataWebApiSample/Configuration/SupplierConfiguration.cs +++ b/samples/webapi/SwaggerODataWebApiSample/Configuration/SupplierConfiguration.cs @@ -17,7 +17,8 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string rout return; } - var supplier = builder.EntitySet( "Suppliers" ).EntityType.HasKey( p => p.Id ); + builder.EntitySet( "Suppliers" ).EntityType.HasKey( p => p.Id ); + builder.Singleton( "Acme" ); } } } \ No newline at end of file diff --git a/samples/webapi/SwaggerODataWebApiSample/V3/AcmeController.cs b/samples/webapi/SwaggerODataWebApiSample/V3/AcmeController.cs new file mode 100644 index 00000000..97c4c1a6 --- /dev/null +++ b/samples/webapi/SwaggerODataWebApiSample/V3/AcmeController.cs @@ -0,0 +1,70 @@ +namespace Microsoft.Examples.V3 +{ + using Microsoft.AspNet.OData; + using Microsoft.Examples.Models; + using Microsoft.Web.Http; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Web.Http; + using System.Web.Http.Description; + using static System.Net.HttpStatusCode; + + /// + /// Represents a RESTful service for the ACME supplier. + /// + [ApiVersion( "3.0" )] + public class AcmeController : ODataController + { + /// + /// Retrieves the ACME supplier. + /// + /// The ACME supplier. + /// The supplier was successfully retrieved. + [EnableQuery] + [ResponseType( typeof( ODataValue ) )] + public IHttpActionResult Get() => Ok( NewSupplier() ); + + /// + /// Gets the products associated with the supplier. + /// + /// The associated supplier products. + [EnableQuery] + public IQueryable GetProducts() => NewSupplier().Products.AsQueryable(); + + /// + /// Links a product to a supplier. + /// + /// The product to link. + /// The product identifier. + /// None + [HttpPost] + public IHttpActionResult CreateRef( string navigationProperty, [FromBody] Uri link ) => StatusCode( NoContent ); + + /// + /// Unlinks a product from a supplier. + /// + /// The related product identifier. + /// The product to unlink. + /// None + public IHttpActionResult DeleteRef( [FromODataUri] string relatedKey, string navigationProperty ) => StatusCode( NoContent ); + + private static Supplier NewSupplier() => + new Supplier() + { + Id = 42, + Name = "Acme", + Products = new List() + { + new Product() + { + Id = 42, + Name = "Product 42", + Category = "Test", + Price = 42, + SupplierId = 42, + } + }, + }; + } +} \ No newline at end of file From 0ea88aaa6be4e4fbd2634c3f4f529f0fabf02b02 Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sat, 10 Oct 2020 01:02:09 -0700 Subject: [PATCH 13/13] Update version numbers and release notes --- .../ReleaseNotes.txt | 2 +- src/Microsoft.AspNet.WebApi.Versioning/ReleaseNotes.txt | 3 ++- .../Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.csproj | 4 ++-- .../ReleaseNotes.txt | 2 +- .../Microsoft.AspNetCore.Mvc.Versioning.csproj | 4 ++-- src/Microsoft.AspNetCore.Mvc.Versioning/ReleaseNotes.txt | 4 +++- .../ReleaseNotes.txt | 2 +- src/Microsoft.AspNetCore.OData.Versioning/ReleaseNotes.txt | 2 +- 8 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/ReleaseNotes.txt b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/ReleaseNotes.txt index 5f282702..d9a561f3 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/ReleaseNotes.txt +++ b/src/Microsoft.AspNet.WebApi.Versioning.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ - \ No newline at end of file +None \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebApi.Versioning/ReleaseNotes.txt b/src/Microsoft.AspNet.WebApi.Versioning/ReleaseNotes.txt index 63cacfaf..d69a9eca 100644 --- a/src/Microsoft.AspNet.WebApi.Versioning/ReleaseNotes.txt +++ b/src/Microsoft.AspNet.WebApi.Versioning/ReleaseNotes.txt @@ -1 +1,2 @@ -HttpControllerDescriptorExtensions.AsEnumerable is now public \ No newline at end of file +CORS preflight request results in error (#619) +HttpControllerDescriptorExtensions.AsEnumerable is now public \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.csproj b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.csproj index 9f5fc1b7..57bdb6b4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.csproj +++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 4.1.1 - 4.1.0.0 + 4.2.0 + 4.2.0.0 netcoreapp3.1 Microsoft ASP.NET Core API Versioning ASP.NET Core MVC API explorer functionality for discovering metadata such as the list of API-versioned controllers and actions, and their URLs and allowed HTTP methods. diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ReleaseNotes.txt b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ReleaseNotes.txt index b2593c79..d9a561f3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ReleaseNotes.txt +++ b/src/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ -Drop support for .NET Core 2.2 \ No newline at end of file +None \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.AspNetCore.Mvc.Versioning.csproj b/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.AspNetCore.Mvc.Versioning.csproj index 8d3ba913..7707c1c1 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.AspNetCore.Mvc.Versioning.csproj +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.AspNetCore.Mvc.Versioning.csproj @@ -1,8 +1,8 @@  - 4.1.1 - 4.1.0.0 + 4.2.0 + 4.2.0.0 netcoreapp3.1 Microsoft ASP.NET Core API Versioning A service API versioning library for Microsoft ASP.NET Core. diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/ReleaseNotes.txt b/src/Microsoft.AspNetCore.Mvc.Versioning/ReleaseNotes.txt index b2593c79..47ef0348 100644 --- a/src/Microsoft.AspNetCore.Mvc.Versioning/ReleaseNotes.txt +++ b/src/Microsoft.AspNetCore.Mvc.Versioning/ReleaseNotes.txt @@ -1 +1,3 @@ -Drop support for .NET Core 2.2 \ No newline at end of file +ApiVersionMatcherPolicy doesn't check candidate validity (#600) +Fix NuGET Part URI cannot start with two forward slashes (#637) +Error response provider should return ProblemDetails (#612) \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/ReleaseNotes.txt b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/ReleaseNotes.txt index b2593c79..5f282702 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/ReleaseNotes.txt +++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ -Drop support for .NET Core 2.2 \ No newline at end of file + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Versioning/ReleaseNotes.txt b/src/Microsoft.AspNetCore.OData.Versioning/ReleaseNotes.txt index b2593c79..5f282702 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning/ReleaseNotes.txt +++ b/src/Microsoft.AspNetCore.OData.Versioning/ReleaseNotes.txt @@ -1 +1 @@ -Drop support for .NET Core 2.2 \ No newline at end of file + \ No newline at end of file