diff --git a/asp.sln b/asp.sln
index 57868d94..e5a1c95e 100644
--- a/asp.sln
+++ b/asp.sln
@@ -197,6 +197,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SomeODataOpenApiExample", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SomeOpenApiODataWebApiExample", "examples\AspNet\OData\SomeOpenApiODataWebApiExample\SomeOpenApiODataWebApiExample.csproj", "{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SomeODataOpenApiExampleCompositeKey", "examples\AspNetCore\OData\SomeODataOpenApiExampleCompositeKey\SomeODataOpenApiExampleCompositeKey.csproj", "{452D7A02-C198-48AA-8F99-0445653F4792}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -387,6 +389,10 @@ Global
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {452D7A02-C198-48AA-8F99-0445653F4792}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {452D7A02-C198-48AA-8F99-0445653F4792}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {452D7A02-C198-48AA-8F99-0445653F4792}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {452D7A02-C198-48AA-8F99-0445653F4792}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -471,6 +477,7 @@ Global
{124C18D1-F72A-4380-AE40-E7511AC16C62} = {E0E64F6F-FB0C-4534-B815-2217700B50BA}
{94A9AF81-A7BE-4E6C-81B1-8BFF4B6E1B78} = {49EA6476-901C-4D4F-8E45-98BC8A2780EB}
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5} = {7BB01633-6E2C-4837-B618-C7F09B18E99E}
+ {452D7A02-C198-48AA-8F99-0445653F4792} = {49EA6476-901C-4D4F-8E45-98BC8A2780EB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {91FE116A-CEFB-4304-A8A6-CFF021C7453A}
diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/Book.cs b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/Book.cs
new file mode 100644
index 00000000..ff29035a
--- /dev/null
+++ b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/Book.cs
@@ -0,0 +1,36 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace ApiVersioning.Examples;
+
+///
+/// Represents a book.
+///
+public class Book
+{
+ ///
+ /// Gets or sets the book identifier.
+ ///
+ /// The International Standard Book Number (ISBN).
+ [Key]
+ public string IdFirst { get; set; }
+ [Key]
+ public string IdSecond { get; set; }
+
+ ///
+ /// Gets or sets the book author.
+ ///
+ /// The author of the book.
+ public string Author { get; set; }
+
+ ///
+ /// Gets or sets the book title.
+ ///
+ /// The title of the book.
+ public string Title { get; set; }
+
+ ///
+ /// Gets or sets the book publication year.
+ ///
+ /// The year the book was first published.
+ public int Published { get; set; }
+}
\ No newline at end of file
diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/BooksController.cs b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/BooksController.cs
new file mode 100644
index 00000000..d3cd14aa
--- /dev/null
+++ b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/BooksController.cs
@@ -0,0 +1,77 @@
+namespace ApiVersioning.Examples;
+
+using Asp.Versioning;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.OData.Query;
+using Microsoft.AspNetCore.OData.Routing.Controllers;
+using System.Collections.Generic;
+using System.Linq;
+using static Microsoft.AspNetCore.Http.StatusCodes;
+
+///
+/// Represents a RESTful service of books.
+///
+[Asp.Versioning.ApiVersion( "1.0" )]
+[Asp.Versioning.ApiVersion( "2.0" )]
+public class BooksController : ODataController
+{
+ private static readonly Book[] books = new Book[]
+ {
+ new() { IdFirst = "9781847490599",IdSecond = "9781847490599", Title = "Anna Karenina", Author = "Leo Tolstoy", Published = 1878 },
+ new() { IdFirst = "9780198800545",IdSecond = "9780198800545", Title = "War and Peace", Author = "Leo Tolstoy", Published = 1869 },
+ new() { IdFirst = "9780684801520",IdSecond = "9780684801520", Title = "The Great Gatsby", Author = "F. Scott Fitzgerald", Published = 1925 },
+ new() { IdFirst = "9780486280615",IdSecond = "9780486280615", Title = "The Adventures of Huckleberry Finn", Author = "Mark Twain", Published = 1884 },
+ new() { IdFirst = "9780140430820",IdSecond = "9780140430820", Title = "Moby Dick", Author = "Herman Melville", Published = 1851 },
+ new() { IdFirst = "9780060934347",IdSecond = "9780060934347", Title = "Don Quixote", Author = "Miguel de Cervantes", Published = 1605 },
+ };
+
+ ///
+ /// Gets all books.
+ ///
+ /// All available books.
+ /// The successfully retrieved books.
+ [Produces( "application/json" )]
+ [ProducesResponseType( typeof( IEnumerable ), Status200OK )]
+ public IActionResult Get() =>
+ Ok( books.AsQueryable() );
+
+ ///
+ /// Gets a single book.
+ ///
+ /// The requested book identifier.
+ /// The requested book.
+ /// The book was successfully retrieved.
+ /// The book does not exist.
+ //[Produces("application/json")]
+ //[ProducesResponseType(typeof(Book), Status200OK)]
+ //[ProducesResponseType(Status404NotFound)]
+ //public IActionResult Get(string key)
+ //{
+ // var book = books.FirstOrDefault(book => book.Id == key);
+
+ // if (book == null)
+ // {
+ // return NotFound();
+ // }
+
+ // return Ok(book);
+ //}
+
+ ///
+ /// Gets a single book.
+ ///
+ [Produces( "application/json" )]
+ [ProducesResponseType( typeof( Book ), Status200OK )]
+ [ProducesResponseType( Status404NotFound )]
+ public IActionResult Get( string keyidFirst, string keyidSecond )
+ {
+ var book = books.FirstOrDefault( book => book.IdFirst == keyidFirst && book.IdSecond == keyidSecond );
+
+ if ( book == null )
+ {
+ return NotFound();
+ }
+
+ return Ok( book );
+ }
+}
\ No newline at end of file
diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/BooksModelConfiguration.cs b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/BooksModelConfiguration.cs
new file mode 100644
index 00000000..5d7550f9
--- /dev/null
+++ b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/BooksModelConfiguration.cs
@@ -0,0 +1,33 @@
+using Asp.Versioning;
+using Asp.Versioning.OData;
+using Microsoft.OData.ModelBuilder;
+
+namespace ApiVersioning.Examples
+{
+ public class BooksModelConfiguration : IModelConfiguration
+ {
+ private void ConfigureV1(ODataModelBuilder builder) => ConfigureCurrent(builder);
+
+ private EntityTypeConfiguration ConfigureCurrent(ODataModelBuilder builder)
+ {
+ var model = builder.EntitySet("Books").EntityType;
+ model.HasKey(p => p.IdFirst);
+ model.HasKey(p => p.IdSecond);
+
+ return model;
+ }
+
+ public void Apply(ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix)
+ {
+ switch (apiVersion.MajorVersion)
+ {
+ case 1:
+ ConfigureV1(builder);
+ break;
+ default:
+ ConfigureCurrent(builder);
+ break;
+ }
+ }
+ }
+}
diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/ConfigureSwaggerOptions.cs b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/ConfigureSwaggerOptions.cs
new file mode 100644
index 00000000..b8e59193
--- /dev/null
+++ b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/ConfigureSwaggerOptions.cs
@@ -0,0 +1,90 @@
+namespace ApiVersioning.Examples;
+
+using Asp.Versioning;
+using Asp.Versioning.ApiExplorer;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using System;
+using System.Text;
+
+///
+/// Configures the Swagger generation options.
+///
+/// This allows API versioning to define a Swagger document per API version after the
+/// service has been resolved from the service container.
+public class ConfigureSwaggerOptions : IConfigureOptions
+{
+ private readonly IApiVersionDescriptionProvider provider;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The provider used to generate Swagger documents.
+ public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider;
+
+ ///
+ public void Configure(SwaggerGenOptions options)
+ {
+ // add a swagger document for each discovered API version
+ // note: you might choose to skip or document deprecated API versions differently
+ foreach (var description in provider.ApiVersionDescriptions)
+ {
+ options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
+ }
+ }
+
+ private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
+ {
+ var text = new StringBuilder("An example application with some OData, OpenAPI, Swashbuckle, and API versioning.");
+ var info = new OpenApiInfo()
+ {
+ Title = "Sample API",
+ Version = description.ApiVersion.ToString(),
+ Contact = new OpenApiContact() { Name = "Bill Mei", Email = "bill.mei@somewhere.com" },
+ License = new OpenApiLicense() { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
+ };
+
+ if (description.IsDeprecated)
+ {
+ text.Append(" This API version has been deprecated.");
+ }
+
+ if (description.SunsetPolicy is SunsetPolicy policy)
+ {
+ if (policy.Date is DateTimeOffset when)
+ {
+ text.Append(" The API will be sunset on ")
+ .Append(when.Date.ToShortDateString())
+ .Append('.');
+ }
+
+ if (policy.HasLinks)
+ {
+ text.AppendLine();
+
+ for (var i = 0; i < policy.Links.Count; i++)
+ {
+ var link = policy.Links[i];
+
+ if (link.Type == "text/html")
+ {
+ text.AppendLine();
+
+ if (link.Title.HasValue)
+ {
+ text.Append(link.Title.Value).Append(": ");
+ }
+
+ text.Append(link.LinkTarget.OriginalString);
+ }
+ }
+ }
+ }
+
+ info.Description = text.ToString();
+
+ return info;
+ }
+}
\ No newline at end of file
diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/Program.cs b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/Program.cs
new file mode 100644
index 00000000..aca8ef13
--- /dev/null
+++ b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/Program.cs
@@ -0,0 +1,103 @@
+using ApiVersioning.Examples;
+using Asp.Versioning;
+using Asp.Versioning.Conventions;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.OData;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+using Microsoft.OData.ModelBuilder;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using System;
+using System.IO;
+using static Microsoft.AspNetCore.OData.Query.AllowedQueryOptions;
+using static System.Text.Json.JsonNamingPolicy;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+// note: this example application intentionally only illustrates the
+// bare minimum configuration for OpenAPI with partial OData support.
+// see the OpenAPI or OData OpenAPI examples for additional options.
+
+builder.Services.Configure(
+ options =>
+ {
+ // odata projection operations (ex: $select) use a dictionary, but for good
+ // measure set the default property naming policy for any other use cases
+ options.JsonSerializerOptions.PropertyNamingPolicy = CamelCase;
+ options.JsonSerializerOptions.DictionaryKeyPolicy = CamelCase;
+ });
+
+builder.Services
+ .AddControllers()
+ .AddOData((options, serviceProvider) =>
+ {
+ options.SetMaxTop(null).Select().Expand().Filter().OrderBy().Count();
+ });
+builder.Services.AddApiVersioning((options) =>
+{
+ options.ReportApiVersions = true;
+ options.DefaultApiVersion = new ApiVersion(1, 0);
+ options.AssumeDefaultVersionWhenUnspecified = true;
+})
+ .AddOData(options =>
+ {
+ options.AddRouteComponents("v{version:apiVersion}");
+ // options.ModelBuilder.ModelBuilderFactory = () => new ODataConventionModelBuilder();
+ })
+ .AddODataApiExplorer(options =>
+ {
+ options.GroupNameFormat = "'v'VV";
+ options.SubstituteApiVersionInUrl = true;
+ });
+
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddTransient, ConfigureSwaggerOptions>();
+builder.Services.AddSwaggerGen(
+ options =>
+ {
+ // add a custom operation filter which sets default values
+ options.OperationFilter();
+
+ var fileName = typeof(Program).Assembly.GetName().Name + ".xml";
+ var filePath = Path.Combine(AppContext.BaseDirectory, fileName);
+
+ // integrate xml comments
+ options.IncludeXmlComments(filePath);
+ });
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseDeveloperExceptionPage();
+ // navigate to ~/$odata to determine whether any endpoints did not match an odata route template
+ app.UseODataRouteDebug();
+}
+
+// Configure the HTTP request pipeline.
+
+app.UseSwagger();
+app.UseSwaggerUI(
+ options =>
+ {
+ var descriptions = app.DescribeApiVersions();
+
+ // build a swagger endpoint for each discovered API version
+ foreach (var description in descriptions)
+ {
+ var url = $"/swagger/{description.GroupName}/swagger.json";
+ var name = description.GroupName.ToUpperInvariant();
+ options.SwaggerEndpoint(url, name);
+ }
+ });
+
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.MapControllers();
+app.Run();
\ No newline at end of file
diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/Properties/launchSettings.json b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/Properties/launchSettings.json
new file mode 100644
index 00000000..3c74a485
--- /dev/null
+++ b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "profiles": {
+ "SomeODataOpenApiExample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:64762;http://localhost:64763"
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/SomeODataOpenApiExampleCompositeKey.csproj b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/SomeODataOpenApiExampleCompositeKey.csproj
new file mode 100644
index 00000000..01a3b626
--- /dev/null
+++ b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/SomeODataOpenApiExampleCompositeKey.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net6.0
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/SwaggerDefaultValues.cs b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/SwaggerDefaultValues.cs
new file mode 100644
index 00000000..00a33340
--- /dev/null
+++ b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/SwaggerDefaultValues.cs
@@ -0,0 +1,69 @@
+namespace ApiVersioning.Examples;
+
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using System.Linq;
+using System.Text.Json;
+
+///
+/// Represents the OpenAPI/Swashbuckle operation filter used to document the implicit API version parameter.
+///
+/// This is only required due to bugs in the .
+/// Once they are fixed and published, this class can be removed.
+public class SwaggerDefaultValues : IOperationFilter
+{
+ ///
+ /// Applies the filter to the specified operation using the given context.
+ ///
+ /// The operation to apply the filter to.
+ /// The current operation filter context.
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ var apiDescription = context.ApiDescription;
+
+ operation.Deprecated |= apiDescription.IsDeprecated();
+
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077
+ foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
+ {
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387
+ var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
+ var response = operation.Responses[responseKey];
+
+ foreach (var contentType in response.Content.Keys)
+ {
+ if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType))
+ {
+ response.Content.Remove(contentType);
+ }
+ }
+ }
+
+ if (operation.Parameters == null)
+ {
+ return;
+ }
+
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
+ foreach (var parameter in operation.Parameters)
+ {
+ var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
+
+ if (parameter.Description == null)
+ {
+ parameter.Description = description.ModelMetadata?.Description;
+ }
+
+ if (parameter.Schema.Default == null && description.DefaultValue != null)
+ {
+ // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330
+ var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType);
+ parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
+ }
+
+ parameter.Required |= description.IsRequired;
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/appsettings.json b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/appsettings.json
new file mode 100644
index 00000000..ec04bc12
--- /dev/null
+++ b/examples/AspNetCore/OData/SomeODataOpenApiExampleCompositeKey/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
\ No newline at end of file
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs
index ce2d828b..8b4cdb47 100644
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs
@@ -194,7 +194,22 @@ private static void CopyApiVersionEndpointMetadata( IList contr
continue;
}
- var metadata = selectors[0].EndpointMetadata.OfType().FirstOrDefault();
+ ApiVersionMetadata? metadata = null;
+ for ( var m = 0; m < selectors.Count; m++ )
+ {
+ metadata = selectors[m].EndpointMetadata.OfType().FirstOrDefault();
+ if ( metadata != null )
+ {
+ if ( m != 0 )
+ {
+ var tmpSelector = selectors[m];
+ selectors[m] = selectors[0];
+ selectors[0] = tmpSelector;
+ }
+
+ break;
+ }
+ }
if ( metadata is null )
{