OpenApi: Lowercase document name on registration to match AddOpenApi internal behaviour (closes #23210) (#23212)
* Lowercase OpenAPI document name on registration to match AddOpenApi internal behaviour AddOpenApi lowercases the document name when registering its keyed services, so ReplaceOpenApiSchemaService must receive the same lowercased key or the lookup throws. BackOfficeOpenApiDocumentBuilder now computes a normalised registration name and uses it for all DI calls, while keeping DocumentName in its original casing. ShouldInclude matches [MapToApi] case-insensitively to align with how documents are registered, and the UI dropdown label falls back to DocumentName (original casing) rather than the lowercased registration key. AddUmbracoOpenApiDocument applies the same normalisation for its apiName parameter. * Add regression tests for mixed-case OpenAPI document name registration Covers the bug scenario where AddBackOfficeOpenApiDocument with a mixed-case name and WithJsonOptions threw InvalidOperationException at startup, and verifies that ShouldInclude matches [MapToApi] case-insensitively.
This commit is contained in:
@@ -59,6 +59,7 @@ public static class UmbracoBuilderApiExtensions
|
||||
string? jsonOptionsName = null)
|
||||
where TConfigureOptions : ConfigureUmbracoOpenApiOptionsBase
|
||||
{
|
||||
apiName = apiName.ToLowerInvariant();
|
||||
builder.Services.AddOpenApi(apiName);
|
||||
builder.Services.ConfigureOptions<TConfigureOptions>();
|
||||
builder.Services.AddOpenApiDocumentToUi(apiName, apiTitle);
|
||||
|
||||
@@ -2,9 +2,9 @@ using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Common.Attributes;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
@@ -116,12 +116,20 @@ public sealed class BackOfficeOpenApiDocumentBuilder
|
||||
/// <param name="builder">The Umbraco builder to register services against.</param>
|
||||
internal void Build(IUmbracoBuilder builder)
|
||||
{
|
||||
// AddOpenApi lowercases the document name when registering its keyed services (https://github.com/dotnet/aspnetcore/blob/v10.0.9/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs#L64),
|
||||
// so we must normalise here to keep AddOpenApiDocumentToUi and ReplaceOpenApiSchemaService in sync.
|
||||
string lowercasedDocumentName = DocumentName.ToLowerInvariant();
|
||||
|
||||
builder.Services.AddOpenApi(
|
||||
DocumentName,
|
||||
lowercasedDocumentName,
|
||||
options =>
|
||||
{
|
||||
// ShouldInclude matches [MapToApi] case-insensitively to align with how documents are registered.
|
||||
options.ShouldInclude = apiDescription =>
|
||||
apiDescription.ActionDescriptor.HasMapToApiAttribute(DocumentName);
|
||||
apiDescription.ActionDescriptor.EndpointMetadata
|
||||
?.OfType<MapToApiAttribute>()
|
||||
.Any(a => a.ApiName.Equals(DocumentName, StringComparison.OrdinalIgnoreCase))
|
||||
?? false;
|
||||
|
||||
options.CreateSchemaReferenceId = UmbracoSchemaIdGenerator.CreateSchemaReferenceId;
|
||||
|
||||
@@ -158,12 +166,12 @@ public sealed class BackOfficeOpenApiDocumentBuilder
|
||||
|
||||
if (_includedInUi)
|
||||
{
|
||||
builder.Services.AddOpenApiDocumentToUi(DocumentName, _uiTitle ?? _title);
|
||||
builder.Services.AddOpenApiDocumentToUi(lowercasedDocumentName, _uiTitle ?? _title ?? DocumentName);
|
||||
}
|
||||
|
||||
if (_httpJsonOptionsFactory is not null)
|
||||
{
|
||||
builder.Services.ReplaceOpenApiSchemaService(DocumentName, _httpJsonOptionsFactory);
|
||||
builder.Services.ReplaceOpenApiSchemaService(lowercasedDocumentName, _httpJsonOptionsFactory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-1
@@ -9,6 +9,7 @@ using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using Umbraco.Cms.Api.Common.Attributes;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
@@ -117,6 +118,26 @@ public class BackOfficeOpenApiDocumentBuilderTests
|
||||
Assert.IsTrue(urls is null || urls.All(url => url.Name != "My API" && url.Name != DocumentName));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Build_With_Mixed_Case_Name_And_WithJsonOptions_Does_Not_Throw()
|
||||
{
|
||||
// AddOpenApi lowercases the document name internally; our registration logic must account for that.
|
||||
Assert.DoesNotThrow(() => Build("MixedCaseDocument", b => b.WithJsonOptions(new JsonOptions())));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShouldInclude_Matches_MapToApi_Case_Insensitively_When_Document_Name_Is_Mixed_Case()
|
||||
{
|
||||
// ShouldInclude matches [MapToApi] case-insensitively so callers are not forced to use exact casing.
|
||||
ServiceCollection services = Build("MixedCaseDocument");
|
||||
ServiceProvider provider = services.BuildServiceProvider();
|
||||
OpenApiOptions options = provider.GetRequiredService<IOptionsMonitor<OpenApiOptions>>().Get("mixedcasedocument");
|
||||
|
||||
Assert.IsTrue(options.ShouldInclude!(CreateApiDescription(new MapToApiAttribute("MixedCaseDocument"))));
|
||||
Assert.IsTrue(options.ShouldInclude!(CreateApiDescription(new MapToApiAttribute("MIXEDCASEDOCUMENT"))));
|
||||
Assert.IsTrue(options.ShouldInclude!(CreateApiDescription(new MapToApiAttribute("mixedcasedocument"))));
|
||||
}
|
||||
|
||||
private static OpenApiOptions BuildAndResolveOptions(Action<BackOfficeOpenApiDocumentBuilder>? configure = null)
|
||||
{
|
||||
ServiceCollection services = Build(configure);
|
||||
@@ -132,10 +153,13 @@ public class BackOfficeOpenApiDocumentBuilderTests
|
||||
}
|
||||
|
||||
private static ServiceCollection Build(Action<BackOfficeOpenApiDocumentBuilder>? configure = null)
|
||||
=> Build(DocumentName, configure);
|
||||
|
||||
private static ServiceCollection Build(string documentName, Action<BackOfficeOpenApiDocumentBuilder>? configure = null)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
IUmbracoBuilder umbracoBuilder = Mock.Of<IUmbracoBuilder>(b => b.Services == services);
|
||||
var documentBuilder = new BackOfficeOpenApiDocumentBuilder(DocumentName);
|
||||
var documentBuilder = new BackOfficeOpenApiDocumentBuilder(documentName);
|
||||
configure?.Invoke(documentBuilder);
|
||||
documentBuilder.Build(umbracoBuilder);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user