Skip to content

OpenAPI tweaks and fixes #1748

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 1, 2025
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
24 changes: 4 additions & 20 deletions src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureMvcOptions.cs
Original file line number Diff line number Diff line change
@@ -1,45 +1,29 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace JsonApiDotNetCore.OpenApi.Swashbuckle;

internal sealed class ConfigureMvcOptions : IConfigureOptions<MvcOptions>
{
private readonly IJsonApiRoutingConvention _jsonApiRoutingConvention;
private readonly JsonApiRequestFormatMetadataProvider _jsonApiRequestFormatMetadataProvider;
private readonly IJsonApiOptions _jsonApiOptions;
private readonly JsonApiOptions _jsonApiOptions;

public ConfigureMvcOptions(IJsonApiRoutingConvention jsonApiRoutingConvention, JsonApiRequestFormatMetadataProvider jsonApiRequestFormatMetadataProvider,
IJsonApiOptions jsonApiOptions)
public ConfigureMvcOptions(JsonApiRequestFormatMetadataProvider jsonApiRequestFormatMetadataProvider, IJsonApiOptions jsonApiOptions)
{
ArgumentNullException.ThrowIfNull(jsonApiRoutingConvention);
ArgumentNullException.ThrowIfNull(jsonApiRequestFormatMetadataProvider);
ArgumentNullException.ThrowIfNull(jsonApiOptions);

_jsonApiRoutingConvention = jsonApiRoutingConvention;
_jsonApiRequestFormatMetadataProvider = jsonApiRequestFormatMetadataProvider;
_jsonApiOptions = jsonApiOptions;
_jsonApiOptions = (JsonApiOptions)jsonApiOptions;
}

public void Configure(MvcOptions options)
{
ArgumentNullException.ThrowIfNull(options);

AddSwashbuckleCliCompatibility(options);

options.InputFormatters.Add(_jsonApiRequestFormatMetadataProvider);

((JsonApiOptions)_jsonApiOptions).IncludeExtensions(OpenApiMediaTypeExtension.OpenApi, OpenApiMediaTypeExtension.RelaxedOpenApi);
}

private void AddSwashbuckleCliCompatibility(MvcOptions options)
{
if (!options.Conventions.Any(convention => convention is IJsonApiRoutingConvention))
{
// See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1957 for why this is needed.
options.Conventions.Insert(0, _jsonApiRoutingConvention);
}
_jsonApiOptions.IncludeExtensions(OpenApiMediaTypeExtension.OpenApi, OpenApiMediaTypeExtension.RelaxedOpenApi);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Net;
using System.Reflection;
using JsonApiDotNetCore.Configuration;
Expand Down Expand Up @@ -36,8 +37,10 @@ internal sealed partial class JsonApiActionDescriptorCollectionProvider : IActio
private readonly JsonApiEndpointMetadataProvider _jsonApiEndpointMetadataProvider;
private readonly IJsonApiOptions _options;
private readonly ILogger<JsonApiActionDescriptorCollectionProvider> _logger;
private readonly ConcurrentDictionary<int, Lazy<ActionDescriptorCollection>> _versionedActionDescriptorCache = new();

public ActionDescriptorCollection ActionDescriptors => GetActionDescriptors();
public ActionDescriptorCollection ActionDescriptors =>
_versionedActionDescriptorCache.GetOrAdd(_defaultProvider.ActionDescriptors.Version, LazyGetActionDescriptors).Value;

public JsonApiActionDescriptorCollectionProvider(IActionDescriptorCollectionProvider defaultProvider, IControllerResourceMapping controllerResourceMapping,
JsonApiEndpointMetadataProvider jsonApiEndpointMetadataProvider, IJsonApiOptions options, ILogger<JsonApiActionDescriptorCollectionProvider> logger)
Expand All @@ -55,7 +58,13 @@ public JsonApiActionDescriptorCollectionProvider(IActionDescriptorCollectionProv
_logger = logger;
}

private ActionDescriptorCollection GetActionDescriptors()
private Lazy<ActionDescriptorCollection> LazyGetActionDescriptors(int version)
{
// https://andrewlock.net/making-getoradd-on-concurrentdictionary-thread-safe-using-lazy/
return new Lazy<ActionDescriptorCollection>(() => GetActionDescriptors(version), LazyThreadSafetyMode.ExecutionAndPublication);
}

private ActionDescriptorCollection GetActionDescriptors(int version)
{
List<ActionDescriptor> descriptors = [];

Expand Down Expand Up @@ -106,8 +115,7 @@ private ActionDescriptorCollection GetActionDescriptors()
descriptors.Add(descriptor);
}

int descriptorVersion = _defaultProvider.ActionDescriptors.Version;
return new ActionDescriptorCollection(descriptors.AsReadOnly(), descriptorVersion);
return new ActionDescriptorCollection(descriptors.AsReadOnly(), version);
}

internal static bool IsVisibleEndpoint(ActionDescriptor descriptor)
Expand Down Expand Up @@ -221,9 +229,9 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor, Buil
{
Dictionary<RelationshipAttribute, ActionDescriptor> descriptorsByRelationship = [];

JsonApiEndpointMetadata? endpointMetadata = _jsonApiEndpointMetadataProvider.Get(descriptor);
JsonApiEndpointMetadata endpointMetadata = _jsonApiEndpointMetadataProvider.Get(descriptor);

switch (endpointMetadata?.RequestMetadata)
switch (endpointMetadata.RequestMetadata)
{
case AtomicOperationsRequestMetadata atomicOperationsRequestMetadata:
{
Expand Down Expand Up @@ -259,7 +267,7 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor, Buil
}
}

switch (endpointMetadata?.ResponseMetadata)
switch (endpointMetadata.ResponseMetadata)
{
case AtomicOperationsResponseMetadata atomicOperationsResponseMetadata:
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@ public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerReso
_nonPrimaryDocumentTypeFactory = nonPrimaryDocumentTypeFactory;
}

public JsonApiEndpointMetadata? Get(ActionDescriptor descriptor)
public JsonApiEndpointMetadata Get(ActionDescriptor descriptor)
{
ArgumentNullException.ThrowIfNull(descriptor);

var actionMethod = OpenApiActionMethod.Create(descriptor);
JsonApiEndpointMetadata? metadata = null;

switch (actionMethod)
{
case AtomicOperationsActionMethod:
{
return new JsonApiEndpointMetadata(AtomicOperationsRequestMetadata.Instance, AtomicOperationsResponseMetadata.Instance);
metadata = new JsonApiEndpointMetadata(AtomicOperationsRequestMetadata.Instance, AtomicOperationsResponseMetadata.Instance);
break;
}
case JsonApiActionMethod jsonApiActionMethod:
{
Expand All @@ -45,13 +47,13 @@ public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerReso

IJsonApiRequestMetadata? requestMetadata = GetRequestMetadata(jsonApiActionMethod.Endpoint, primaryResourceType);
IJsonApiResponseMetadata? responseMetadata = GetResponseMetadata(jsonApiActionMethod.Endpoint, primaryResourceType);
return new JsonApiEndpointMetadata(requestMetadata, responseMetadata);
}
default:
{
return null;
metadata = new JsonApiEndpointMetadata(requestMetadata, responseMetadata);
break;
}
}

ConsistencyGuard.ThrowIf(metadata == null);
return metadata;
}

private IJsonApiRequestMetadata? GetRequestMetadata(JsonApiEndpoints endpoint, ResourceType primaryResourceType)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Formatters;

Expand All @@ -10,12 +11,14 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
internal sealed class JsonApiRequestFormatMetadataProvider : IInputFormatter, IApiRequestFormatMetadataProvider
{
/// <inheritdoc />
[ExcludeFromCodeCoverage]
public bool CanRead(InputFormatterContext context)
{
return false;
}

/// <inheritdoc />
[ExcludeFromCodeCoverage]
public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
throw new UnreachableException();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -87,7 +88,7 @@ private static string GetSchemaTypeName(Type type)

private sealed partial class SchemaGenerationTraceScope : ISchemaGenerationTraceScope
{
private static readonly AsyncLocal<int> RecursionDepthAsyncLocal = new();
private static readonly AsyncLocal<StrongBox<int>> RecursionDepthAsyncLocal = new();

private readonly ILogger _logger;
private readonly string _schemaTypeName;
Expand All @@ -101,8 +102,10 @@ public SchemaGenerationTraceScope(ILogger logger, string schemaTypeName)
_logger = logger;
_schemaTypeName = schemaTypeName;

RecursionDepthAsyncLocal.Value++;
LogStarted(RecursionDepthAsyncLocal.Value, _schemaTypeName);
RecursionDepthAsyncLocal.Value ??= new StrongBox<int>(0);
int depth = Interlocked.Increment(ref RecursionDepthAsyncLocal.Value.Value);

LogStarted(depth, _schemaTypeName);
}

public void TraceSucceeded(string schemaId)
Expand All @@ -112,16 +115,18 @@ public void TraceSucceeded(string schemaId)

public void Dispose()
{
int depth = RecursionDepthAsyncLocal.Value!.Value;

if (_schemaId != null)
{
LogSucceeded(RecursionDepthAsyncLocal.Value, _schemaTypeName, _schemaId);
LogSucceeded(depth, _schemaTypeName, _schemaId);
}
else
{
LogFailed(RecursionDepthAsyncLocal.Value, _schemaTypeName);
LogFailed(depth, _schemaTypeName);
}

RecursionDepthAsyncLocal.Value--;
Interlocked.Decrement(ref RecursionDepthAsyncLocal.Value.Value);
}

[LoggerMessage(Level = LogLevel.Trace, SkipEnabledCheck = true, Message = "({Depth:D2}) Started for {SchemaTypeName}.")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private static void AddCustomApiExplorer(IServiceCollection services)

AddApiExplorer(services);

services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>());
}

private static void AddApiExplorer(IServiceCollection services)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,6 @@ public static void UseJsonApi(this IApplicationBuilder builder)
inverseNavigationResolver.Resolve();
}

var jsonApiApplicationBuilder = builder.ApplicationServices.GetRequiredService<IJsonApiApplicationBuilder>();

jsonApiApplicationBuilder.ConfigureMvcOptions = options =>
{
var inputFormatter = builder.ApplicationServices.GetRequiredService<IJsonApiInputFormatter>();
options.InputFormatters.Insert(0, inputFormatter);

var outputFormatter = builder.ApplicationServices.GetRequiredService<IJsonApiOutputFormatter>();
options.OutputFormatters.Insert(0, outputFormatter);

var routingConvention = builder.ApplicationServices.GetRequiredService<IJsonApiRoutingConvention>();
options.Conventions.Insert(0, routingConvention);
};

builder.UseMiddleware<JsonApiMiddleware>();
}

Expand Down
38 changes: 38 additions & 0 deletions src/JsonApiDotNetCore/Configuration/ConfigureMvcOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using JsonApiDotNetCore.Middleware;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace JsonApiDotNetCore.Configuration;

internal sealed class ConfigureMvcOptions : IConfigureOptions<MvcOptions>
{
private readonly IJsonApiInputFormatter _inputFormatter;
private readonly IJsonApiOutputFormatter _outputFormatter;
private readonly IJsonApiRoutingConvention _routingConvention;

public ConfigureMvcOptions(IJsonApiInputFormatter inputFormatter, IJsonApiOutputFormatter outputFormatter, IJsonApiRoutingConvention routingConvention)
{
ArgumentNullException.ThrowIfNull(inputFormatter);
ArgumentNullException.ThrowIfNull(outputFormatter);
ArgumentNullException.ThrowIfNull(routingConvention);

_inputFormatter = inputFormatter;
_outputFormatter = outputFormatter;
_routingConvention = routingConvention;
}

public void Configure(MvcOptions options)
{
ArgumentNullException.ThrowIfNull(options);

options.EnableEndpointRouting = true;

options.InputFormatters.Insert(0, _inputFormatter);
options.OutputFormatters.Insert(0, _outputFormatter);
options.Conventions.Insert(0, _routingConvention);

options.Filters.AddService<IAsyncJsonApiExceptionFilter>();
options.Filters.AddService<IAsyncQueryStringActionFilter>();
options.Filters.AddService<IAsyncConvertEmptyActionResultFilter>();
}
}

This file was deleted.

16 changes: 3 additions & 13 deletions src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,21 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace JsonApiDotNetCore.Configuration;

/// <summary>
/// A utility class that builds a JSON:API application. It registers all required services and allows the user to override parts of the startup
/// configuration.
/// </summary>
internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder
internal sealed class JsonApiApplicationBuilder
{
private readonly IServiceCollection _services;
private readonly IMvcCoreBuilder _mvcBuilder;
private readonly JsonApiOptions _options = new();
private readonly ResourceDescriptorAssemblyCache _assemblyCache = new();

public Action<MvcOptions>? ConfigureMvcOptions { get; set; }

public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder)
{
ArgumentNullException.ThrowIfNull(services);
Expand Down Expand Up @@ -105,15 +104,6 @@ public void ConfigureResourceGraph(ICollection<Type> dbContextTypes, Action<Reso
/// </summary>
public void ConfigureMvc()
{
_mvcBuilder.AddMvcOptions(options =>
{
options.EnableEndpointRouting = true;
options.Filters.AddService<IAsyncJsonApiExceptionFilter>();
options.Filters.AddService<IAsyncQueryStringActionFilter>();
options.Filters.AddService<IAsyncConvertEmptyActionResultFilter>();
ConfigureMvcOptions?.Invoke(options);
});

if (_options.ValidateModelState)
{
_mvcBuilder.AddDataAnnotations();
Expand Down Expand Up @@ -175,14 +165,14 @@ public void ConfigureServiceContainer(ICollection<Type> dbContextTypes)
private void AddMiddlewareLayer()
{
_services.TryAddSingleton<IJsonApiOptions>(_options);
_services.TryAddSingleton<IJsonApiApplicationBuilder>(this);
_services.TryAddSingleton<IExceptionHandler, ExceptionHandler>();
_services.TryAddScoped<IAsyncJsonApiExceptionFilter, AsyncJsonApiExceptionFilter>();
_services.TryAddScoped<IAsyncQueryStringActionFilter, AsyncQueryStringActionFilter>();
_services.TryAddScoped<IAsyncConvertEmptyActionResultFilter, AsyncConvertEmptyActionResultFilter>();
_services.TryAddSingleton<IJsonApiInputFormatter, JsonApiInputFormatter>();
_services.TryAddSingleton<IJsonApiOutputFormatter, JsonApiOutputFormatter>();
_services.TryAddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
_services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>());
_services.TryAddSingleton<IControllerResourceMapping>(provider => provider.GetRequiredService<IJsonApiRoutingConvention>());
_services.TryAddSingleton<IJsonApiEndpointFilter, AlwaysEnabledJsonApiEndpointFilter>();
_services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Expand Down
Loading
Loading