Skip to content

Commit 617f8ce

Browse files
Unify and simplify configuring versioned OpenAPI options
1 parent 33213e3 commit 617f8ce

File tree

7 files changed

+187
-89
lines changed

7 files changed

+187
-89
lines changed

examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
// can also be used to control the format of the API version in route templates
3535
options.SubstituteApiVersionInUrl = true;
3636
} )
37-
.AddOpenApi( ( _, options ) => options.AddScalarTransformers() )
37+
.AddOpenApi( options => options.Document.AddScalarTransformers() )
3838
// this enables binding ApiVersion as a endpoint callback parameter. if you don't use it, then
3939
// you should remove this configuration.
4040
.EnableApiVersionBinding();

examples/AspNetCore/WebApi/OpenApiExample/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
// can also be used to control the format of the API version in route templates
3535
options.SubstituteApiVersionInUrl = true;
3636
} )
37-
.AddOpenApi( ( _, options ) => options.AddScalarTransformers() );
37+
.AddOpenApi( options => options.Document.AddScalarTransformers() );
3838

3939
var app = builder.Build();
4040

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/Configuration/ConfigureOpenApiOptions.cs

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Asp.Versioning.OpenApi;
66

77
using Asp.Versioning.ApiExplorer;
8+
using Asp.Versioning.OpenApi.Configuration;
89
using Asp.Versioning.OpenApi.Reflection;
910
using Asp.Versioning.OpenApi.Transformers;
1011
using Microsoft.AspNetCore.OpenApi;
@@ -14,11 +15,10 @@ namespace Asp.Versioning.OpenApi;
1415
internal sealed class ConfigureOpenApiOptions(
1516
XmlCommentsFile file,
1617
IApiVersionDescriptionProvider provider,
17-
IOptions<OpenApiDocumentDescriptionOptions> descriptionOptions,
18-
[FromKeyedServices( typeof( ApiVersion ) )] Action<ApiVersionDescription, OpenApiOptions> configure )
19-
: IConfigureNamedOptions<OpenApiOptions>
18+
VersionedOpenApiOptionsFactory factory )
19+
: IPostConfigureOptions<OpenApiOptions>
2020
{
21-
public void Configure( string? name, OpenApiOptions options )
21+
public void PostConfigure( string? name, OpenApiOptions options )
2222
{
2323
var comparer = StringComparer.OrdinalIgnoreCase;
2424
var descriptions = provider.ApiVersionDescriptions;
@@ -33,26 +33,33 @@ public void Configure( string? name, OpenApiOptions options )
3333
continue;
3434
}
3535

36-
var apiExplorer = new ApiExplorerTransformer( description, descriptionOptions );
37-
38-
options.SetDocumentName( description.GroupName );
39-
options.AddDocumentTransformer( apiExplorer );
40-
options.AddSchemaTransformer( apiExplorer );
41-
options.AddOperationTransformer( apiExplorer );
42-
43-
if ( !xmlComments.IsEmpty )
36+
var context = new VersionedOpenApiOptionsFactory.Context()
4437
{
45-
options.AddSchemaTransformer( xmlComments );
46-
options.AddOperationTransformer( xmlComments );
47-
}
38+
Name = name,
39+
Description = description,
40+
Options = options,
41+
OnCreated = versionedOptions => Configure( versionedOptions, xmlComments ),
42+
};
4843

49-
configure( description, options );
44+
factory.CreateAndConfigure( context );
5045
break;
5146
}
5247
}
5348

54-
public void Configure( OpenApiOptions options )
49+
private static void Configure( VersionedOpenApiOptions versionedOptions, XmlCommentsTransformer xmlComments )
5550
{
56-
// intentionally empty; all options must be named
51+
var options = versionedOptions.Document;
52+
var apiExplorer = new ApiExplorerTransformer( versionedOptions );
53+
54+
options.SetDocumentName( versionedOptions.Description.GroupName );
55+
options.AddDocumentTransformer( apiExplorer );
56+
options.AddSchemaTransformer( apiExplorer );
57+
options.AddOperationTransformer( apiExplorer );
58+
59+
if ( !xmlComments.IsEmpty )
60+
{
61+
options.AddSchemaTransformer( xmlComments );
62+
options.AddOperationTransformer( xmlComments );
63+
}
5764
}
5865
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
#pragma warning disable CA1812
4+
5+
namespace Asp.Versioning.OpenApi.Configuration;
6+
7+
using Asp.Versioning.ApiExplorer;
8+
using Microsoft.AspNetCore.OpenApi;
9+
using Microsoft.Extensions.Options;
10+
using System.Collections.Generic;
11+
12+
// OpenApiOptions is sealed so we can't inherit from it, but we need to get in front of it. this factory allows
13+
// configuring VersionedOpenApiOptions registered when the services are added. IOptions<VersionedOpenApiOptions> isn't
14+
// ever directly resolved and never uses a name. whenever OpenApiOptions are created and configured, this factory is
15+
// invoked to create the VersionedOpenApiOptions, which will be passed down to transformers, etc.
16+
internal sealed class VersionedOpenApiOptionsFactory(
17+
IEnumerable<IConfigureOptions<VersionedOpenApiOptions>> setups,
18+
IEnumerable<IPostConfigureOptions<VersionedOpenApiOptions>> postConfigures,
19+
IEnumerable<IValidateOptions<VersionedOpenApiOptions>> validations )
20+
: IOptionsFactory<VersionedOpenApiOptions>
21+
{
22+
private readonly IConfigureOptions<VersionedOpenApiOptions>[] setups = [.. setups];
23+
private readonly IPostConfigureOptions<VersionedOpenApiOptions>[] postConfigures = [.. postConfigures];
24+
private readonly IValidateOptions<VersionedOpenApiOptions>[] validations = [.. validations];
25+
private Context? context;
26+
27+
internal VersionedOpenApiOptions CreateAndConfigure( Context newContext )
28+
{
29+
context = newContext;
30+
var instance = Create( newContext.Name );
31+
context = default;
32+
return instance;
33+
}
34+
35+
public VersionedOpenApiOptions Create( string name )
36+
{
37+
if ( string.IsNullOrEmpty( name ) || context is null )
38+
{
39+
return DefaultOptions();
40+
}
41+
42+
if ( name != context.Name )
43+
{
44+
return DefaultOptions();
45+
}
46+
47+
var options = new VersionedOpenApiOptions()
48+
{
49+
Description = context.Description,
50+
Document = context.Options,
51+
DocumentDescription = new(),
52+
};
53+
54+
context.OnCreated( options );
55+
56+
for ( var i = 0; i < setups.Length; i++ )
57+
{
58+
setups[i].Configure( options );
59+
}
60+
61+
for ( var i = 0; i < postConfigures.Length; i++ )
62+
{
63+
postConfigures[i].PostConfigure( Options.DefaultName, options );
64+
}
65+
66+
if ( validations.Length > 0 )
67+
{
68+
var failures = new List<string>();
69+
70+
for ( var i = 0; i < validations.Length; i++ )
71+
{
72+
var result = validations[i].Validate( Options.DefaultName, options );
73+
74+
if ( result is not null && result.Failed )
75+
{
76+
failures.AddRange( result.Failures );
77+
}
78+
}
79+
80+
if ( failures.Count > 0 )
81+
{
82+
throw new OptionsValidationException( name, typeof( VersionedOpenApiOptions ), failures );
83+
}
84+
}
85+
86+
return options;
87+
}
88+
89+
private static VersionedOpenApiOptions DefaultOptions() => new()
90+
{
91+
Description = new( ApiVersion.Neutral, string.Empty ),
92+
Document = new(),
93+
DocumentDescription = new(),
94+
};
95+
96+
internal sealed class Context
97+
{
98+
public required string Name { get; init; }
99+
100+
public required ApiVersionDescription Description { get; init; }
101+
102+
public required OpenApiOptions Options { get; init; }
103+
104+
public required Action<VersionedOpenApiOptions> OnCreated { get; init; }
105+
}
106+
}

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/DependencyInjection/IApiVersioningBuilderExtensions.cs

Lines changed: 9 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Microsoft.Extensions.DependencyInjection;
77
using Asp.Versioning;
88
using Asp.Versioning.ApiExplorer;
99
using Asp.Versioning.OpenApi;
10+
using Asp.Versioning.OpenApi.Configuration;
1011
using Asp.Versioning.OpenApi.Reflection;
1112
using Asp.Versioning.OpenApi.Transformers;
1213
using Microsoft.AspNetCore.Http.Json;
@@ -16,6 +17,7 @@ namespace Microsoft.Extensions.DependencyInjection;
1617
using Microsoft.Extensions.Options;
1718
using System.Reflection;
1819
using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
20+
using EM = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions;
1921

2022
/// <summary>
2123
/// Provides OpenAPI specific extension methods for <see cref="IApiVersioningBuilder"/>.
@@ -34,60 +36,22 @@ public IApiVersioningBuilder AddOpenApi()
3436
ArgumentNullException.ThrowIfNull( builder );
3537

3638
AddOpenApiServices( builder, GetAssemblies( Assembly.GetCallingAssembly() ) );
37-
builder.Services.TryAddKeyedTransient( typeof( ApiVersion ), NoOptions );
3839

3940
return builder;
4041
}
4142

4243
/// <summary>
4344
/// Adds OpenAPI support for API versioning.
4445
/// </summary>
45-
/// <param name="configureOptions">The function used to configure the target <see cref="OpenApiOptions">options</see>.</param>
46+
/// <param name="configureOptions">The function used to configure the
47+
/// <see cref="VersionedOpenApiOptions">versioned OpenAPI options</see>.</param>
4648
/// <returns>The original <see cref="IApiVersioningBuilder">builder</see>.</returns>
47-
public IApiVersioningBuilder AddOpenApi( Action<ApiVersionDescription, OpenApiOptions> configureOptions )
49+
public IApiVersioningBuilder AddOpenApi( Action<VersionedOpenApiOptions> configureOptions )
4850
{
4951
ArgumentNullException.ThrowIfNull( builder );
5052

5153
AddOpenApiServices( builder, GetAssemblies( Assembly.GetCallingAssembly() ) );
52-
builder.Services.TryAddKeyedTransient( typeof( ApiVersion ), ( _, _ ) => configureOptions );
53-
54-
return builder;
55-
}
56-
57-
/// <summary>
58-
/// Adds OpenAPI support for API versioning.
59-
/// </summary>
60-
/// <param name="descriptionOptions">The function used to configure the target
61-
/// <see cref="OpenApiDocumentDescriptionOptions">title options</see>.</param>
62-
/// <returns>The original <see cref="IApiVersioningBuilder">builder</see>.</returns>
63-
public IApiVersioningBuilder AddOpenApi( Action<OpenApiDocumentDescriptionOptions> descriptionOptions )
64-
{
65-
ArgumentNullException.ThrowIfNull( builder );
66-
67-
AddOpenApiServices( builder, GetAssemblies( Assembly.GetCallingAssembly() ) );
68-
builder.Services.Configure( descriptionOptions );
69-
builder.Services.TryAddKeyedTransient( typeof( ApiVersion ), NoOptions );
70-
71-
return builder;
72-
}
73-
74-
/// <summary>
75-
/// Adds OpenAPI support for API versioning.
76-
/// </summary>
77-
/// <param name="configureOptions">The function used to configure the target
78-
/// <see cref="OpenApiOptions">OpenAPI options</see>.</param>
79-
/// <param name="descriptionOptions">The function used to configure the target
80-
/// <see cref="OpenApiDocumentDescriptionOptions">title options</see>.</param>
81-
/// <returns>The original <see cref="IApiVersioningBuilder">builder</see>.</returns>
82-
public IApiVersioningBuilder AddOpenApi(
83-
Action<ApiVersionDescription, OpenApiOptions> configureOptions,
84-
Action<OpenApiDocumentDescriptionOptions> descriptionOptions )
85-
{
86-
ArgumentNullException.ThrowIfNull( builder );
87-
88-
AddOpenApiServices( builder, GetAssemblies( Assembly.GetCallingAssembly() ) );
89-
builder.Services.Configure( descriptionOptions );
90-
builder.Services.TryAddKeyedTransient( typeof( ApiVersion ), ( _, _ ) => configureOptions );
54+
builder.Services.Configure( configureOptions );
9155

9256
return builder;
9357
}
@@ -102,9 +66,9 @@ private static void AddOpenApiServices( IApiVersioningBuilder builder, Assembly[
10266

10367
services.AddTransient( NewRequestServices );
10468
services.Add( Singleton( Type.IDocumentProvider, ResolveDocumentProvider ) );
105-
services.AddOptions<OpenApiDocumentDescriptionOptions>();
106-
services.Add( Transient<ConfigureOpenApiOptions, ConfigureOpenApiOptions>() );
107-
services.TryAddEnumerable( Singleton<IConfigureOptions<OpenApiOptions>, ConfigureOpenApiOptions>( static sp => sp.GetRequiredService<ConfigureOpenApiOptions>() ) );
69+
services.AddSingleton<VersionedOpenApiOptionsFactory>();
70+
services.TryAddEnumerable( Transient<IPostConfigureOptions<OpenApiOptions>, ConfigureOpenApiOptions>() );
71+
services.TryAdd( Singleton<IOptionsFactory<VersionedOpenApiOptions>>( EM.GetRequiredService<VersionedOpenApiOptionsFactory> ) );
10872
builder.Services.AddSingleton( sp => new XmlCommentsFile( assemblies, sp.GetRequiredService<IHostEnvironment>() ) );
10973

11074
if ( GetJsonConfiguration() is { } descriptor )
@@ -138,17 +102,12 @@ private static Assembly[] GetAssemblies( Assembly callingAssembly )
138102
return services.SingleOrDefault( sd => sd.ServiceType == typeof( IConfigureOptions<JsonOptions> ) );
139103
}
140104

141-
#pragma warning disable IDE0060
142-
143-
private static Action<ApiVersionDescription, OpenApiOptions> NoOptions( IServiceProvider provider, object key ) => static ( _, _ ) => { };
144-
145105
private static object ResolveDocumentProvider( IServiceProvider provider ) =>
146106
provider.GetRequiredService<KeyedServiceContainer>().GetRequiredService( Type.IDocumentProvider );
147107

148108
[UnconditionalSuppressMessage( "ILLink", "IL3050" )]
149109
private static KeyedServiceContainer NewRequestServices( IServiceProvider services )
150110
{
151-
var configure = services.GetRequiredKeyedService<Action<ApiVersionDescription, OpenApiOptions>>( typeof( ApiVersion ) );
152111
var provider = services.GetRequiredService<IApiVersionDescriptionProvider>();
153112
var keyedServices = new KeyedServiceContainer( services );
154113
var names = new List<string>();

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/Transformers/ApiExplorerTransformer.cs

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ namespace Asp.Versioning.OpenApi.Transformers;
55
using Asp.Versioning.ApiExplorer;
66
using Microsoft.AspNetCore.Mvc.ApiExplorer;
77
using Microsoft.AspNetCore.OpenApi;
8-
using Microsoft.Extensions.Options;
98
using Microsoft.Extensions.Primitives;
109
using Microsoft.OpenApi;
1110
using System.Reflection;
@@ -24,22 +23,18 @@ public class ApiExplorerTransformer :
2423
IOpenApiDocumentTransformer,
2524
IOpenApiOperationTransformer
2625
{
27-
private readonly ApiVersionDescription apiVersionDescription;
28-
private readonly IOptions<OpenApiDocumentDescriptionOptions> descriptionOptions;
29-
3026
/// <summary>
3127
/// Initializes a new instance of the <see cref="ApiExplorerTransformer"/> class.
3228
/// </summary>
33-
/// <param name="apiVersionDescription">The <see cref="ApiVersionDescription">metadata</see> to apply.</param>
34-
/// <param name="descriptionOptions">The <see cref="OpenApiDocumentDescriptionOptions">options</see> applied
29+
/// <param name="options">The <see cref="VersionedOpenApiOptions">options</see> applied
3530
/// to OpenAPI document descriptions.</param>
36-
public ApiExplorerTransformer(
37-
ApiVersionDescription apiVersionDescription,
38-
IOptions<OpenApiDocumentDescriptionOptions> descriptionOptions )
39-
{
40-
this.apiVersionDescription = apiVersionDescription;
41-
this.descriptionOptions = descriptionOptions;
42-
}
31+
public ApiExplorerTransformer( VersionedOpenApiOptions options ) => Options = options;
32+
33+
/// <summary>
34+
/// Gets the associated, versioned OpenAPI options.
35+
/// </summary>
36+
/// <value>The associated <see cref="VersionedOpenApiOptions">options</see>.</value>
37+
protected VersionedOpenApiOptions Options { get; }
4338

4439
/// <summary>
4540
/// Gets or sets the OpenApi extension name.
@@ -74,14 +69,12 @@ public Task TransformAsync(
7469
ArgumentNullException.ThrowIfNull( document );
7570
ArgumentNullException.ThrowIfNull( context );
7671

77-
var options = descriptionOptions.Value;
78-
79-
UpdateFromAssemblyInfo( document, apiVersionDescription );
72+
UpdateFromAssemblyInfo( document, Options.Description );
8073

81-
document.Info.Version = apiVersionDescription.ApiVersion.ToString();
74+
document.Info.Version = Options.Description.ApiVersion.ToString();
8275

83-
UpdateDescriptionToMarkdown( document, apiVersionDescription, options );
84-
AddLinkExtensions( document, apiVersionDescription );
76+
UpdateDescriptionToMarkdown( document, Options.Description, Options.DocumentDescription );
77+
AddLinkExtensions( document, Options.Description );
8578

8679
return Task.CompletedTask;
8780
}

0 commit comments

Comments
 (0)