Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ internal static void AddUmbracoOpenApiDocument<TConfigureOptions>(
string? jsonOptionsName = null)
where TConfigureOptions : ConfigureUmbracoOpenApiOptionsBase
{
apiName = apiName.ToLowerInvariant();
builder.Services.AddOpenApi(apiName);
builder.Services.ConfigureOptions<TConfigureOptions>();
builder.Services.AddOpenApiDocumentToUi(apiName, apiTitle);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
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;

Expand Down Expand Up @@ -116,12 +116,20 @@ public BackOfficeOpenApiDocumentBuilder WithJsonOptions(Func<IServiceProvider, J
/// <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;

Expand Down Expand Up @@ -158,12 +166,12 @@ internal void Build(IUmbracoBuilder builder)

if (_includedInUi)
{
builder.Services.AddOpenApiDocumentToUi(DocumentName, _uiTitle ?? _title);
builder.Services.AddOpenApiDocumentToUi(lowercasedDocumentName, _uiTitle ?? _title ?? DocumentName);
}
Comment thread
lauraneto marked this conversation as resolved.

if (_httpJsonOptionsFactory is not null)
{
builder.Services.ReplaceOpenApiSchemaService(DocumentName, _httpJsonOptionsFactory);
builder.Services.ReplaceOpenApiSchemaService(lowercasedDocumentName, _httpJsonOptionsFactory);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;

Expand Down Expand Up @@ -117,6 +118,26 @@ public void ExcludeFromUi_Skips_Swagger_Dropdown_Registration()
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);
Expand All @@ -132,10 +153,13 @@ private static SwaggerUIOptions BuildAndResolveSwaggerOptions(Action<BackOfficeO
}

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);

Expand Down
Loading