Skip to content

Commit 5740e00

Browse files
committed
Added .NET 3.1 OData + OpenAPI example to reproduce issue dotnet#850
1 parent f5cd8e5 commit 5740e00

File tree

12 files changed

+365
-0
lines changed

12 files changed

+365
-0
lines changed

asp.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SomeODataOpenApiExample", "
196196
EndProject
197197
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SomeOpenApiODataWebApiExample", "examples\AspNet\OData\SomeOpenApiODataWebApiExample\SomeOpenApiODataWebApiExample.csproj", "{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}"
198198
EndProject
199+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetCoreApp3Dot1Example", "examples\AspNetCore\OData\NetCoreApp3Dot1Example\NetCoreApp3Dot1Example.csproj", "{AE1764E7-0B67-4C95-A736-96B3AC80AEC2}"
200+
EndProject
199201
Global
200202
GlobalSection(SolutionConfigurationPlatforms) = preSolution
201203
Debug|Any CPU = Debug|Any CPU
@@ -386,6 +388,10 @@ Global
386388
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
387389
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
388390
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}.Release|Any CPU.Build.0 = Release|Any CPU
391+
{AE1764E7-0B67-4C95-A736-96B3AC80AEC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
392+
{AE1764E7-0B67-4C95-A736-96B3AC80AEC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
393+
{AE1764E7-0B67-4C95-A736-96B3AC80AEC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
394+
{AE1764E7-0B67-4C95-A736-96B3AC80AEC2}.Release|Any CPU.Build.0 = Release|Any CPU
389395
EndGlobalSection
390396
GlobalSection(SolutionProperties) = preSolution
391397
HideSolutionNode = FALSE
@@ -470,6 +476,7 @@ Global
470476
{124C18D1-F72A-4380-AE40-E7511AC16C62} = {E0E64F6F-FB0C-4534-B815-2217700B50BA}
471477
{94A9AF81-A7BE-4E6C-81B1-8BFF4B6E1B78} = {49EA6476-901C-4D4F-8E45-98BC8A2780EB}
472478
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5} = {7BB01633-6E2C-4837-B618-C7F09B18E99E}
479+
{AE1764E7-0B67-4C95-A736-96B3AC80AEC2} = {49EA6476-901C-4D4F-8E45-98BC8A2780EB}
473480
EndGlobalSection
474481
GlobalSection(ExtensibilityGlobals) = postSolution
475482
SolutionGuid = {91FE116A-CEFB-4304-A8A6-CFF021C7453A}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using ApiVersioning.Examples.Models;
2+
using Asp.Versioning;
3+
using Asp.Versioning.OData;
4+
using Microsoft.OData.ModelBuilder;
5+
6+
namespace ApiVersioning.Examples.Configuration
7+
{
8+
public class PersonModelConfiguration : IModelConfiguration
9+
{
10+
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix )
11+
{
12+
builder.EntitySet<Person>( "People" );
13+
}
14+
}
15+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using ApiVersioning.Examples.Models;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.AspNetCore.OData.Routing.Controllers;
4+
5+
namespace ApiVersioning.Examples.Controllers
6+
{
7+
[Route("[controller]")]
8+
public class PeopleController : ODataController
9+
{
10+
[HttpGet]
11+
public IEnumerable<Person> Get()
12+
{
13+
return Enumerable.Range(1, 5).Select(index => new Person
14+
{
15+
PersonId = index,
16+
FirstName = $"First{index}",
17+
LastName = $"Last{index}"
18+
})
19+
.ToArray();
20+
}
21+
}
22+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace ApiVersioning.Examples.Models
4+
{
5+
public class Person
6+
{
7+
[Key]
8+
public int PersonId { get; set; }
9+
public string FirstName { get; set; }
10+
public string LastName { get; set; }
11+
}
12+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.3.0" />
9+
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.3.0" />
10+
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.3.0" />
11+
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\..\..\..\src\AspNetCore\OData\src\Asp.Versioning.OData.ApiExplorer\Asp.Versioning.OData.ApiExplorer.csproj" />
16+
<ProjectReference Include="..\..\..\..\src\AspNetCore\OData\src\Asp.Versioning.OData\Asp.Versioning.OData.csproj" />
17+
<ProjectReference Include="..\..\..\..\src\AspNetCore\WebApi\src\Asp.Versioning.Mvc.ApiExplorer\Asp.Versioning.Mvc.ApiExplorer.csproj" />
18+
<ProjectReference Include="..\..\..\..\src\AspNetCore\WebApi\src\Asp.Versioning.Mvc\Asp.Versioning.Mvc.csproj" />
19+
</ItemGroup>
20+
21+
22+
</Project>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace ApiVersioning.Examples
2+
{
3+
public class Program
4+
{
5+
public static void Main(string[] args)
6+
{
7+
CreateHostBuilder(args).Build().Run();
8+
}
9+
10+
public static IHostBuilder CreateHostBuilder(string[] args) =>
11+
Host.CreateDefaultBuilder(args)
12+
.ConfigureWebHostDefaults(webBuilder =>
13+
{
14+
webBuilder.UseStartup<Startup>();
15+
});
16+
}
17+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$schema": "http://json.schemastore.org/launchsettings.json",
3+
"iisSettings": {
4+
"windowsAuthentication": false,
5+
"anonymousAuthentication": true,
6+
"iisExpress": {
7+
"applicationUrl": "http://localhost:43047",
8+
"sslPort": 0
9+
}
10+
},
11+
"profiles": {
12+
"IIS Express": {
13+
"commandName": "IISExpress",
14+
"launchBrowser": true,
15+
"launchUrl": "swagger",
16+
"environmentVariables": {
17+
"ASPNETCORE_ENVIRONMENT": "Development"
18+
}
19+
},
20+
"NetCoreApp3Dot1Example": {
21+
"commandName": "Project",
22+
"launchBrowser": true,
23+
"launchUrl": "swagger",
24+
"applicationUrl": "http://localhost:5000",
25+
"environmentVariables": {
26+
"ASPNETCORE_ENVIRONMENT": "Development"
27+
}
28+
}
29+
}
30+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using ApiVersioning.Examples.Swagger;
2+
using Asp.Versioning;
3+
using Asp.Versioning.ApiExplorer;
4+
using Microsoft.AspNetCore.OData;
5+
using Microsoft.Extensions.Options;
6+
using Swashbuckle.AspNetCore.SwaggerGen;
7+
8+
namespace ApiVersioning.Examples
9+
{
10+
public class Startup
11+
{
12+
public Startup(IConfiguration configuration)
13+
{
14+
Configuration = configuration;
15+
}
16+
17+
public IConfiguration Configuration { get; }
18+
19+
// This method gets called by the runtime. Use this method to add services to the container.
20+
public void ConfigureServices(IServiceCollection services)
21+
{
22+
services.AddControllers()
23+
.AddOData( options =>
24+
{
25+
options.EnableQueryFeatures();
26+
} );
27+
28+
// allow a client to call you without specifying an api version
29+
services
30+
.AddApiVersioning(options =>
31+
{
32+
options.AssumeDefaultVersionWhenUnspecified = true;
33+
options.DefaultApiVersion = new ApiVersion( 1, 0 );
34+
options.ReportApiVersions = false;
35+
})
36+
.AddApiExplorer(options =>
37+
{
38+
options.GroupNameFormat = "'v'VVV";
39+
options.AssumeDefaultVersionWhenUnspecified = true;
40+
options.DefaultApiVersion = new ApiVersion( 1, 0 );
41+
} )
42+
.AddOData( options =>
43+
{
44+
// versioning by: query string, header, or media type
45+
options.AddRouteComponents("api"); // route prefix
46+
} )
47+
.AddODataApiExplorer( options =>
48+
{
49+
// format the version as "'v'major[.minor][-status]"
50+
options.GroupNameFormat = "'v'VVV";
51+
} );
52+
53+
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
54+
services.AddSwaggerGen( options =>
55+
{
56+
// add a custom operation filter which sets default values
57+
options.OperationFilter<SwaggerDefaultValues>();
58+
} );
59+
60+
// explicit opt-in - needs to be placed after AddSwaggerGen()
61+
services.AddSwaggerGenNewtonsoftSupport();
62+
}
63+
64+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
65+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
66+
{
67+
if (env.IsDevelopment())
68+
{
69+
app.UseDeveloperExceptionPage();
70+
}
71+
72+
app.UseRouting();
73+
74+
app.UseAuthorization();
75+
76+
77+
// Enable middleware to serve generated Swagger as a JSON endpoint.
78+
app.UseSwagger();
79+
80+
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
81+
// specifying the Swagger JSON endpoint.
82+
app.UseSwaggerUI( options =>
83+
{
84+
// build a swagger endpoint for each discovered API version
85+
foreach ( var description in provider.ApiVersionDescriptions )
86+
{
87+
var url = $"/swagger/{description.GroupName}/swagger.json";
88+
var name = description.GroupName.ToUpperInvariant();
89+
options.SwaggerEndpoint( url, name );
90+
}
91+
} );
92+
93+
app.UseEndpoints(endpoints =>
94+
{
95+
endpoints.MapControllers();
96+
});
97+
}
98+
}
99+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Asp.Versioning.ApiExplorer;
2+
using Microsoft.Extensions.Options;
3+
using Microsoft.OpenApi.Models;
4+
using Swashbuckle.AspNetCore.SwaggerGen;
5+
using System.Text;
6+
7+
namespace ApiVersioning.Examples.Swagger
8+
{
9+
/// <summary>
10+
/// Configures the Swagger generation options.
11+
/// </summary>
12+
/// <remarks>This allows API versioning to define a Swagger document per API version after the
13+
/// <see cref="IApiVersionDescriptionProvider"/> service has been resolved from the service container.</remarks>
14+
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
15+
{
16+
private readonly IApiVersionDescriptionProvider provider;
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="ConfigureSwaggerOptions"/> class.
20+
/// </summary>
21+
/// <param name="provider">The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger documents.</param>
22+
public ConfigureSwaggerOptions( IApiVersionDescriptionProvider provider ) => this.provider = provider;
23+
24+
/// <inheritdoc />
25+
public void Configure( SwaggerGenOptions options )
26+
{
27+
// add a swagger document for each discovered API version
28+
// note: you might choose to skip or document deprecated API versions differently
29+
foreach ( var description in provider.ApiVersionDescriptions )
30+
{
31+
options.SwaggerDoc( description.GroupName, CreateInfoForApiVersion( description ) );
32+
}
33+
}
34+
35+
private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription description )
36+
{
37+
var text = new StringBuilder( "An example application with OpenAPI, Swashbuckle, and API versioning." );
38+
var info = new OpenApiInfo()
39+
{
40+
Title = "Example API",
41+
Version = description.ApiVersion.ToString(),
42+
Contact = new OpenApiContact() { Name = "Bill Mei", Email = "[email protected]" },
43+
License = new OpenApiLicense() { Name = "MIT", Url = new Uri( "https://opensource.org/licenses/MIT" ) }
44+
};
45+
46+
if ( description.IsDeprecated )
47+
{
48+
text.Append( " This API version has been deprecated." );
49+
}
50+
51+
info.Description = text.ToString();
52+
53+
return info;
54+
}
55+
}
56+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using Microsoft.AspNetCore.Mvc.ApiExplorer;
2+
using Microsoft.OpenApi.Models;
3+
using Swashbuckle.AspNetCore.SwaggerGen;
4+
using System.Text.Json;
5+
6+
namespace ApiVersioning.Examples.Swagger
7+
{
8+
/// <summary>
9+
/// Represents the OpenAPI/Swashbuckle operation filter used to document information provided, but not used.
10+
/// </summary>
11+
/// <remarks>This <see cref="IOperationFilter"/> is only required due to bugs in the <see cref="SwaggerGenerator"/>.
12+
/// Once they are fixed and published, this class can be removed.</remarks>
13+
public class SwaggerDefaultValues : IOperationFilter
14+
{
15+
/// <inheritdoc />
16+
public void Apply( OpenApiOperation operation, OperationFilterContext context )
17+
{
18+
var apiDescription = context.ApiDescription;
19+
20+
operation.Deprecated |= apiDescription.IsDeprecated();
21+
22+
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077
23+
foreach ( var responseType in context.ApiDescription.SupportedResponseTypes )
24+
{
25+
// based on internals of SwaggerGenerator
26+
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387
27+
var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
28+
var response = operation.Responses[responseKey];
29+
30+
// remove media types not supported by the API
31+
foreach ( var contentType in response.Content.Keys )
32+
{
33+
if ( !responseType.ApiResponseFormats.Any( x => x.MediaType == contentType ) )
34+
{
35+
response.Content.Remove( contentType );
36+
}
37+
}
38+
}
39+
40+
if ( operation.Parameters == null )
41+
{
42+
return;
43+
}
44+
45+
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
46+
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
47+
foreach ( var parameter in operation.Parameters )
48+
{
49+
var description = apiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name );
50+
51+
if ( parameter.Description == null )
52+
{
53+
parameter.Description = description.ModelMetadata?.Description;
54+
}
55+
56+
if ( parameter.Schema.Default == null && description.DefaultValue != null )
57+
{
58+
// REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330
59+
var json = JsonSerializer.Serialize( description.DefaultValue, description.ModelMetadata.ModelType );
60+
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson( json );
61+
}
62+
63+
parameter.Required |= description.IsRequired;
64+
}
65+
}
66+
}
67+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft": "Warning",
6+
"Microsoft.Hosting.Lifetime": "Information"
7+
}
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}

0 commit comments

Comments
 (0)