diff --git a/ApiVersioning.sln b/ApiVersioning.sln
index c370c882..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,8 +365,10 @@ 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}
EndGlobalSection
-EndGlobal
\ No newline at end of file
+EndGlobal
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/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/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..c25bf345 100644
--- a/samples/aspnetcore/SwaggerODataSample/Configuration/SupplierConfiguration.cs
+++ b/samples/aspnetcore/SwaggerODataSample/Configuration/SupplierConfiguration.cs
@@ -9,19 +9,16 @@
///
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 )
{
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/Startup.cs b/samples/aspnetcore/SwaggerODataSample/Startup.cs
index 7361bcde..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.MapVersionedODataRoutes( "odata", "api", modelBuilder.GetEdmModels() );
+ 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/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/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/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/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..79b1efdd 100644
--- a/samples/webapi/SwaggerODataWebApiSample/Configuration/SupplierConfiguration.cs
+++ b/samples/webapi/SwaggerODataWebApiSample/Configuration/SupplierConfiguration.cs
@@ -9,19 +9,16 @@
///
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 )
{
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/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/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
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.
///
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;
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/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/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 23b26971..f665ea09 100644
--- a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
+++ b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
@@ -49,31 +49,88 @@ 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 ) );
}
void AppendRoutePrefix( IList segments )
{
- var prefix = Context.Route.RoutePrefix?.Trim( '/' );
+ var prefix = Context.RoutePrefix;
if ( IsNullOrEmpty( prefix ) )
{
@@ -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 5fd96a1a..7304e343 100644
--- a/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs
+++ b/src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs
@@ -17,7 +17,7 @@
#endif
using System;
using System.Collections.Generic;
- using System.Reflection;
+ using System.Linq;
#if WEBAPI
using System.Web.Http.Description;
using System.Web.Http.Dispatcher;
@@ -25,10 +25,12 @@
#endif
using static Microsoft.OData.ODataUrlKeyDelimiter;
using static ODataRouteTemplateGenerationKind;
+ using static System.StringComparison;
sealed partial class ODataRouteBuilderContext
{
readonly ODataRouteAttribute? routeAttribute;
+ IODataPathTemplateHandler? templateHandler;
internal IServiceProvider Services { get; }
@@ -52,12 +54,14 @@ sealed partial class ODataRouteBuilderContext
internal string? RouteTemplate { get; }
- internal ODataRoute Route { get; }
+ internal string? RoutePrefix { get; }
internal ControllerActionDescriptor ActionDescriptor { get; }
internal IEdmEntitySet? EntitySet { get; }
+ internal IEdmSingleton? Singleton { get; }
+
internal IEdmOperation? Operation { get; }
internal ODataRouteActionType ActionType { get; }
@@ -70,40 +74,197 @@ 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 static ODataRouteActionType GetActionType( IEdmEntitySet entitySet, IEdmOperation operation )
+ 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 ) ) )
+ {
+ 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
// 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
+ // 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();
+
+ if ( !iterator.MoveNext() )
+ {
+ return false;
+ }
+
+ var method = iterator.Current;
+
+ if ( iterator.MoveNext() )
+ {
+ 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 ( actionName.StartsWith( "CreateRef", Ordinal ) ||
+ ( entitySet != null && actionName == ( ActionMethod + entitySet.Name ) ) )
+ {
+ return false;
+ }
+
+ return !IsNavigationPropertyLink( entitySet, singleton, actionName, ActionMethod );
+ }
+ else if ( FunctionMethod.Equals( method, OrdinalIgnoreCase ) && actionName != FunctionMethod )
+ {
+ if ( actionName.StartsWith( "GetRef", Ordinal ) ||
+ ( entitySet != null && actionName == ( ActionMethod + entitySet.Name ) ) )
+ {
+ return false;
+ }
+
+ 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 ) )
+ {
+ entities.Add( entity );
+ }
+ }
+
+ for ( var i = 0; i < entities.Count; i++ )
+ {
+ var entity = entities[i];
+
+ if ( actionName == ( method + entity.Name ) )
+ {
+ return true;
+ }
+
+ foreach ( var property in entity.NavigationProperties() )
+ {
+ if ( actionName.StartsWith( method + property.Name, OrdinalIgnoreCase ) )
+ {
+ 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;
+ var entities = new List( capacity: 2 );
+
+ if ( Singleton != null )
+ {
+ entities.Add( Singleton.EntityType() );
+ }
+
+ if ( EntitySet != null )
+ {
+ 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 )
+ {
+ 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/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.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.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 ddf004ec..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;
@@ -15,6 +16,8 @@
partial class ODataRouteBuilderContext
{
+ readonly ODataRoute route;
+
internal ODataRouteBuilderContext(
HttpConfiguration configuration,
ApiVersion apiVersion,
@@ -24,40 +27,63 @@ 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 );
+ Singleton = container.FindSingleton( controllerName );
+ Operation = ResolveOperation( container, actionDescriptor.ActionName );
+ ActionType = GetActionType( actionDescriptor );
+ IsRouteExcluded = ActionType == ODataRouteActionType.Unknown;
if ( Operation?.IsAction() == true )
{
- ConvertODataActionParametersToTypedModel( modelTypeBuilder, (IEdmAction) Operation, actionDescriptor.ControllerDescriptor.ControllerName );
+ ConvertODataActionParametersToTypedModel( modelTypeBuilder, (IEdmAction) Operation, controllerName );
}
}
- void ConvertODataActionParametersToTypedModel( IModelTypeBuilder modelTypeBuilder, IEdmAction action, string controllerName )
+ internal IODataPathTemplateHandler PathTemplateHandler
{
- var apiVersion = new Lazy( () => EdmModel.GetAnnotationValue( EdmModel ).ApiVersion );
+ 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 )
+ {
for ( var i = 0; i < ParameterDescriptions.Count; i++ )
{
var description = ParameterDescriptions[i];
@@ -65,7 +91,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..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;
@@ -158,10 +160,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;
}
@@ -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 );
+ }
}
}
}
@@ -355,7 +407,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 9896200c..00000000
--- a/src/Microsoft.AspNet.OData.Versioning/Routing/VersionedODataPathRouteConstraint.cs
+++ /dev/null
@@ -1,137 +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;
- }
-
- if ( ApiVersion == requestedVersion )
- {
- DecorateUrlHelperWithApiVersionRouteValueIfNecessary( request, values );
- return true;
- }
-
- return false;
- }
-
- 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 ) );
- }
- }
-
- 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.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 @@
-