Skip to content

Fix versioning error when using composite key #889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions asp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.ComponentModel.DataAnnotations;

namespace ApiVersioning.Examples;

/// <summary>
/// Represents a book.
/// </summary>
public class Book
{
/// <summary>
/// Gets or sets the book identifier.
/// </summary>
/// <value>The International Standard Book Number (ISBN).</value>
[Key]
public string IdFirst { get; set; }
[Key]
public string IdSecond { get; set; }

/// <summary>
/// Gets or sets the book author.
/// </summary>
/// <value>The author of the book.</value>
public string Author { get; set; }

/// <summary>
/// Gets or sets the book title.
/// </summary>
/// <value>The title of the book.</value>
public string Title { get; set; }

/// <summary>
/// Gets or sets the book publication year.
/// </summary>
/// <value>The year the book was first published.</value>
public int Published { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a RESTful service of books.
/// </summary>
[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 },
};

/// <summary>
/// Gets all books.
/// </summary>
/// <returns>All available books.</returns>
/// <response code="200">The successfully retrieved books.</response>
[Produces( "application/json" )]
[ProducesResponseType( typeof( IEnumerable<Book> ), Status200OK )]
public IActionResult Get() =>
Ok( books.AsQueryable() );

/// <summary>
/// Gets a single book.
/// </summary>
/// <param name="key">The requested book identifier.</param>
/// <returns>The requested book.</returns>
/// <response code="200">The book was successfully retrieved.</response>
/// <response code="404">The book does not exist.</response>
//[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);
//}

/// <summary>
/// Gets a single book.
/// </summary>
[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 );
}
}
Original file line number Diff line number Diff line change
@@ -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<Book> ConfigureCurrent(ODataModelBuilder builder)
{
var model = builder.EntitySet<Book>("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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Configures the Swagger generation options.
/// </summary>
/// <remarks>This allows API versioning to define a Swagger document per API version after the
/// <see cref="IApiVersionDescriptionProvider"/> service has been resolved from the service container.</remarks>
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider provider;

/// <summary>
/// Initializes a new instance of the <see cref="ConfigureSwaggerOptions"/> class.
/// </summary>
/// <param name="provider">The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger documents.</param>
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider;

/// <inheritdoc />
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 = "[email protected]" },
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;
}
}
Original file line number Diff line number Diff line change
@@ -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<JsonOptions>(
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<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen(
options =>
{
// add a custom operation filter which sets default values
options.OperationFilter<SwaggerDefaultValues>();

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();
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"profiles": {
"SomeODataOpenApiExample": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:64762;http://localhost:64763"
}
}
}
Loading