Skip to content

Commit 920ac53

Browse files
Fix DI registration and make API Explorer transformer public
1 parent d014631 commit 920ac53

3 files changed

Lines changed: 117 additions & 58 deletions

File tree

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

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@ public IApiVersioningBuilder AddOpenApi()
2929
{
3030
ArgumentNullException.ThrowIfNull( builder );
3131

32-
var services = builder.Services;
33-
34-
AddOpenApiServices( services );
35-
services.TryAddKeyedTransient( typeof( ApiVersion ), NoOptions );
32+
AddOpenApiServices( builder );
33+
builder.Services.TryAddKeyedTransient( typeof( ApiVersion ), NoOptions );
3634

3735
return builder;
3836
}
@@ -46,10 +44,8 @@ public IApiVersioningBuilder AddOpenApi( Action<ApiVersionDescription, OpenApiOp
4644
{
4745
ArgumentNullException.ThrowIfNull( builder );
4846

49-
var services = builder.Services;
50-
51-
AddOpenApiServices( services );
52-
services.TryAddKeyedTransient( typeof( ApiVersion ), ( _, _ ) => configureOptions );
47+
AddOpenApiServices( builder );
48+
builder.Services.TryAddKeyedTransient( typeof( ApiVersion ), ( _, _ ) => configureOptions );
5349

5450
return builder;
5551
}
@@ -64,11 +60,9 @@ public IApiVersioningBuilder AddOpenApi( Action<OpenApiDocumentDescriptionOption
6460
{
6561
ArgumentNullException.ThrowIfNull( builder );
6662

67-
var services = builder.Services;
68-
69-
AddOpenApiServices( services );
70-
services.Configure( descriptionOptions );
71-
services.TryAddKeyedTransient( typeof( ApiVersion ), NoOptions );
63+
AddOpenApiServices( builder );
64+
builder.Services.Configure( descriptionOptions );
65+
builder.Services.TryAddKeyedTransient( typeof( ApiVersion ), NoOptions );
7266

7367
return builder;
7468
}
@@ -87,18 +81,21 @@ public IApiVersioningBuilder AddOpenApi(
8781
{
8882
ArgumentNullException.ThrowIfNull( builder );
8983

90-
var services = builder.Services;
91-
92-
AddOpenApiServices( services );
93-
services.Configure( descriptionOptions );
94-
services.TryAddKeyedTransient( typeof( ApiVersion ), ( _, _ ) => configureOptions );
84+
AddOpenApiServices( builder );
85+
builder.Services.Configure( descriptionOptions );
86+
builder.Services.TryAddKeyedTransient( typeof( ApiVersion ), ( _, _ ) => configureOptions );
9587

9688
return builder;
9789
}
9890
}
9991

100-
private static void AddOpenApiServices( IServiceCollection services )
92+
[UnconditionalSuppressMessage( "ILLink", "IL2026" )]
93+
private static void AddOpenApiServices( IApiVersioningBuilder builder )
10194
{
95+
builder.AddApiExplorer();
96+
97+
var services = builder.Services;
98+
10299
services.AddOptions<OpenApiDocumentDescriptionOptions>();
103100
services.Add( Singleton<ConfigureOpenApiOptions, ConfigureOpenApiOptions>() );
104101
services.TryAddEnumerable( Singleton<IConfigureOptions<OpenApiOptions>, ConfigureOpenApiOptions>( static sp => sp.GetRequiredService<ConfigureOpenApiOptions>() ) );

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public static class IEndpointConventionBuilderExtensions
2626
/// Enables generating one OpenAPI document per APi Version for the associated endpoint builder.
2727
/// </summary>
2828
/// <remarks>
29-
/// This method is only intended to apply API Versioning conventions the the OpenAPI endpoint. Applying this
29+
/// This method is only intended to apply API Versioning conventions the OpenAPI endpoint. Applying this
3030
/// method to other endpoints may have unintended effects.
3131
/// </remarks>
3232
/// <returns>The original <see cref="IEndpointConventionBuilder">endpoint convention builder</see>.</returns>

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

Lines changed: 100 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,48 @@ namespace Asp.Versioning.OpenApi.Transformers;
1414
using System.Threading;
1515
using System.Threading.Tasks;
1616

17-
internal sealed class ApiExplorerTransformer(
18-
ApiVersionDescription apiVersionDescription,
19-
IOptions<OpenApiDocumentDescriptionOptions> descriptionOptions ) :
17+
/// <summary>
18+
/// Represents a <see cref="IOpenApiDocumentTransformer">transformer</see> used to apply API Explorer metadata to an
19+
/// OpenAPI document.
20+
/// </summary>
21+
[CLSCompliant( false )]
22+
public class ApiExplorerTransformer :
2023
IOpenApiSchemaTransformer,
2124
IOpenApiDocumentTransformer,
2225
IOpenApiOperationTransformer
2326
{
27+
private readonly ApiVersionDescription apiVersionDescription;
28+
private readonly IOptions<OpenApiDocumentDescriptionOptions> descriptionOptions;
29+
30+
/// <summary>
31+
/// Initializes a new instance of the <see cref="ApiExplorerTransformer"/> class.
32+
/// </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
35+
/// 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+
}
43+
44+
/// <summary>
45+
/// Gets or sets the OpenApi extension name.
46+
/// </summary>
47+
/// <value>The OpenAPI extension name. The default value is <c>x-api-versioning</c>.</value>
48+
protected string ExtensionName { get; set; } = "x-api-versioning";
49+
50+
/// <inheritdoc />
2451
public Task TransformAsync(
2552
OpenApiSchema schema,
2653
OpenApiSchemaTransformerContext context,
2754
CancellationToken cancellationToken )
2855
{
56+
ArgumentNullException.ThrowIfNull( schema );
57+
ArgumentNullException.ThrowIfNull( context );
58+
2959
if ( schema.Default is null
3060
&& context.ParameterDescription?.DefaultValue is string value )
3161
{
@@ -35,11 +65,15 @@ public Task TransformAsync(
3565
return Task.CompletedTask;
3666
}
3767

68+
/// <inheritdoc />
3869
public Task TransformAsync(
3970
OpenApiDocument document,
4071
OpenApiDocumentTransformerContext context,
4172
CancellationToken cancellationToken )
4273
{
74+
ArgumentNullException.ThrowIfNull( document );
75+
ArgumentNullException.ThrowIfNull( context );
76+
4377
var options = descriptionOptions.Value;
4478

4579
UpdateFromAssemblyInfo( document, apiVersionDescription );
@@ -52,6 +86,7 @@ public Task TransformAsync(
5286
return Task.CompletedTask;
5387
}
5488

89+
/// <inheritdoc />
5590
public Task TransformAsync(
5691
OpenApiOperation operation,
5792
OpenApiOperationTransformerContext context,
@@ -115,7 +150,7 @@ private static void UpdateFromAssemblyInfo( OpenApiDocument document, ApiVersion
115150
}
116151
}
117152

118-
private static void UpdateDescriptionToMarkdown(
153+
private void UpdateDescriptionToMarkdown(
119154
OpenApiDocument document,
120155
ApiVersionDescription api,
121156
OpenApiDocumentDescriptionOptions options )
@@ -170,48 +205,68 @@ private static void UpdateDescriptionToMarkdown(
170205
document.Info.Description = description.ToString();
171206
}
172207

173-
private static bool IsHtml( LinkHeaderValue link ) =>
174-
StringSegmentComparer.OrdinalIgnoreCase.Equals( link.Type, "text/html" );
175-
176-
private static void AddMarkdownLinks( StringBuilder markdown, IList<LinkHeaderValue> links )
208+
/// <summary>
209+
/// Determines if the specified link should be rendered.
210+
/// </summary>
211+
/// <param name="link">The <see cref="LinkHeaderValue">link</see> to evaluate.</param>
212+
/// <returns>True if the link should be rendered; otherwise, false.</returns>
213+
/// <remarks>The default implementation only renders <c>text/html</c> links.</remarks>
214+
protected virtual bool ShouldRenderLink( LinkHeaderValue link )
177215
{
178-
for ( var i = 0; i < links.Count; i++ )
179-
{
180-
var link = links[i];
216+
ArgumentNullException.ThrowIfNull( link );
217+
return StringSegmentComparer.OrdinalIgnoreCase.Equals( link.Type, "text/html" );
218+
}
181219

182-
if ( !IsHtml( link ) )
183-
{
184-
continue;
185-
}
220+
/// <summary>
221+
/// Renders the specified link as markdown.
222+
/// </summary>
223+
/// <param name="markdown">The <see cref="StringBuilder">builder</see> to render the Markdown into.</param>
224+
/// <param name="link">The <see cref="LinkHeaderValue">link</see> to render.</param>
225+
protected virtual void RenderLink( StringBuilder markdown, LinkHeaderValue link )
226+
{
227+
ArgumentNullException.ThrowIfNull( markdown );
228+
ArgumentNullException.ThrowIfNull( link );
186229

187-
if ( StringSegment.IsNullOrEmpty( link.Title ) )
230+
if ( StringSegment.IsNullOrEmpty( link.Title ) )
231+
{
232+
if ( link.LinkTarget.IsAbsoluteUri )
188233
{
189-
if ( link.LinkTarget.IsAbsoluteUri )
190-
{
191-
markdown.Append( "- " ).AppendLine( link.LinkTarget.OriginalString );
192-
}
193-
else
194-
{
195-
markdown.Append( "- <a href=\"" )
196-
.Append( link.LinkTarget.OriginalString )
197-
.Append( "\">" )
198-
.Append( link.LinkTarget.OriginalString )
199-
.AppendLine( "</a>" );
200-
}
234+
markdown.Append( "- " ).AppendLine( link.LinkTarget.OriginalString );
201235
}
202236
else
203237
{
204-
markdown.Append( "- [" )
205-
.Append( link.Title.ToString() )
206-
.Append( "](" )
238+
markdown.Append( "- <a href=\"" )
239+
.Append( link.LinkTarget.OriginalString )
240+
.Append( "\">" )
207241
.Append( link.LinkTarget.OriginalString )
208-
.Append( ')' )
209-
.AppendLine();
242+
.AppendLine( "</a>" );
243+
}
244+
}
245+
else
246+
{
247+
markdown.Append( "- [" )
248+
.Append( link.Title.ToString() )
249+
.Append( "](" )
250+
.Append( link.LinkTarget.OriginalString )
251+
.Append( ')' )
252+
.AppendLine();
253+
}
254+
}
255+
256+
private void AddMarkdownLinks( StringBuilder markdown, IList<LinkHeaderValue> links )
257+
{
258+
for ( var i = 0; i < links.Count; i++ )
259+
{
260+
var link = links[i];
261+
262+
if ( ShouldRenderLink( link ) )
263+
{
264+
RenderLink( markdown, link );
210265
}
211266
}
212267
}
213268

214-
private static void AddLinkExtensions( OpenApiDocument document, ApiVersionDescription api )
269+
private void AddLinkExtensions( OpenApiDocument document, ApiVersionDescription api )
215270
{
216271
var array = new JsonArray();
217272

@@ -223,22 +278,29 @@ private static void AddLinkExtensions( OpenApiDocument document, ApiVersionDescr
223278
if ( array.Count > 0 )
224279
{
225280
var extensions = document.Extensions ??= new Dictionary<string, IOpenApiExtension>();
226-
extensions["x-api-versioning"] = new JsonNodeExtension( array );
281+
extensions[ExtensionName] = new JsonNodeExtension( array );
227282
}
228283
}
229284

230285
[UnconditionalSuppressMessage( "ILLink", "IL2026" )]
231286
[UnconditionalSuppressMessage( "ILLink", "IL3050" )]
232-
private static void AddLinks( JsonArray array, IList<LinkHeaderValue> links )
287+
private void AddLinks( JsonArray array, IList<LinkHeaderValue> links )
233288
{
234289
for ( var i = 0; i < links.Count; i++ )
235290
{
236-
array.Add( LinkToJson( links[i] ) );
291+
array.Add( ToJson( links[i] ) );
237292
}
238293
}
239294

240-
private static JsonObject LinkToJson( LinkHeaderValue link )
295+
/// <summary>
296+
/// Converts the specified link into JSON as an OpenAPI extension.
297+
/// </summary>
298+
/// <param name="link">The <see cref="LinkHeaderValue">link</see> to convert.</param>
299+
/// <returns>The OpenAPI extension <see cref="JsonObject">JSON</see> node.</returns>
300+
protected virtual JsonObject ToJson( LinkHeaderValue link )
241301
{
302+
ArgumentNullException.ThrowIfNull( link );
303+
242304
var obj = new JsonObject();
243305

244306
if ( link.Title.HasValue )

0 commit comments

Comments
 (0)