diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/JsonApiDotNetCore.OpenApi.Client.NSwag.csproj b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/JsonApiDotNetCore.OpenApi.Client.NSwag.csproj
index 0a5d7b7263..89c3e65f1c 100644
--- a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/JsonApiDotNetCore.OpenApi.Client.NSwag.csproj
+++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/JsonApiDotNetCore.OpenApi.Client.NSwag.csproj
@@ -27,6 +27,7 @@
+
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureSwaggerGenOptions.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureSwaggerGenOptions.cs
index de34115b65..00e24965b5 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureSwaggerGenOptions.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureSwaggerGenOptions.cs
@@ -45,6 +45,8 @@ public ConfigureSwaggerGenOptions(OpenApiOperationIdSelector operationIdSelector
public void Configure(SwaggerGenOptions options)
{
+ ArgumentNullException.ThrowIfNull(options);
+
options.SupportNonNullableReferenceTypes();
options.UseAllOfToExtendReferenceSchemas();
@@ -57,10 +59,10 @@ public void Configure(SwaggerGenOptions options)
options.CustomOperationIds(_operationIdSelector.GetOpenApiOperationId);
options.CustomSchemaIds(_schemaIdSelector.GetSchemaId);
+ options.OperationFilter();
options.DocumentFilter();
options.DocumentFilter();
options.DocumentFilter();
- options.OperationFilter();
options.DocumentFilter();
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs
index c99384f7b8..d6a47f1ebc 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs
@@ -58,7 +58,7 @@ .. AddJsonApiMetadataToAction(endpoint, endpointMetadataContainer.ResponseMetada
if (replacementDescriptorsForEndpoint.Count > 0)
{
- newDescriptors.InsertRange(newDescriptors.IndexOf(endpoint) - 1, replacementDescriptorsForEndpoint);
+ newDescriptors.InsertRange(newDescriptors.IndexOf(endpoint), replacementDescriptorsForEndpoint);
newDescriptors.Remove(endpoint);
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs
index 8a1d8d19a2..514372db11 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs
@@ -280,5 +280,10 @@ public static JsonApiEndpointWrapper FromActionModel(ActionModel actionModel)
JsonApiEndpoints endpoint = EndpointResolver.Instance.GetEndpoint(actionModel.ActionMethod);
return new JsonApiEndpointWrapper(false, endpoint);
}
+
+ public override string ToString()
+ {
+ return IsAtomicOperationsEndpoint ? "PostOperations" : Value.ToString();
+ }
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Bodies/BodySchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Bodies/BodySchemaGenerator.cs
index 237d8f5e2f..28110a36fc 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Bodies/BodySchemaGenerator.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Bodies/BodySchemaGenerator.cs
@@ -45,18 +45,19 @@ public OpenApiSchema GenerateSchema(Type bodyType, SchemaRepository schemaReposi
_linksVisibilitySchemaGenerator.UpdateSchemaForTopLevel(bodyType, fullSchema, schemaRepository);
- SetJsonApiVersion(fullSchema);
+ SetJsonApiVersion(fullSchema, schemaRepository);
return referenceSchema;
}
protected abstract OpenApiSchema GenerateBodySchema(Type bodyType, SchemaRepository schemaRepository);
- private void SetJsonApiVersion(OpenApiSchema fullSchema)
+ private void SetJsonApiVersion(OpenApiSchema fullSchema, SchemaRepository schemaRepository)
{
if (fullSchema.Properties.ContainsKey(JsonApiPropertyName.Jsonapi) && !_options.IncludeJsonApiVersion)
{
fullSchema.Properties.Remove(JsonApiPropertyName.Jsonapi);
+ schemaRepository.Schemas.Remove(JsonApiPropertyName.Jsonapi);
}
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Components/LinksVisibilitySchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Components/LinksVisibilitySchemaGenerator.cs
index 606a5d1f42..660e4ee94d 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Components/LinksVisibilitySchemaGenerator.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Components/LinksVisibilitySchemaGenerator.cs
@@ -110,14 +110,17 @@ public void UpdateSchemaForRelationship(Type modelType, OpenApiSchema fullSchema
private void UpdateLinksProperty(OpenApiSchema fullSchemaForLinksContainer, LinkTypes visibleLinkTypes, LinkTypes possibleLinkTypes,
SchemaRepository schemaRepository)
{
+ OpenApiSchema referenceSchemaForLinks = fullSchemaForLinksContainer.Properties[JsonApiPropertyName.Links].UnwrapLastExtendedSchema();
+
if ((visibleLinkTypes & possibleLinkTypes) == 0)
{
fullSchemaForLinksContainer.Required.Remove(JsonApiPropertyName.Links);
fullSchemaForLinksContainer.Properties.Remove(JsonApiPropertyName.Links);
+
+ schemaRepository.Schemas.Remove(referenceSchemaForLinks.Reference.Id);
}
else if (visibleLinkTypes != possibleLinkTypes)
{
- OpenApiSchema referenceSchemaForLinks = fullSchemaForLinksContainer.Properties[JsonApiPropertyName.Links].UnwrapLastExtendedSchema();
string linksSchemaId = referenceSchemaForLinks.Reference.Id;
if (schemaRepository.Schemas.TryGetValue(linksSchemaId, out OpenApiSchema? fullSchemaForLinks))
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/StringEnumOrderingFilter.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/StringEnumOrderingFilter.cs
index 0a610bba27..2342d4feec 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/StringEnumOrderingFilter.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/StringEnumOrderingFilter.cs
@@ -27,18 +27,15 @@ private sealed class OpenApiEnumVisitor : OpenApiVisitorBase
{
public override void Visit(OpenApiSchema schema)
{
- if (schema.Enum.Count > 0)
+ if (HasSortAnnotation(schema))
{
- if (HasSortAnnotation(schema))
+ if (schema.Enum.Count > 1)
{
- if (schema.Enum.Count > 1)
- {
- OrderEnumMembers(schema);
- }
+ OrderEnumMembers(schema);
}
-
- schema.Extensions.Remove(RequiresSortKey);
}
+
+ schema.Extensions.Remove(RequiresSortKey);
}
private static bool HasSortAnnotation(OpenApiSchema schema)
diff --git a/test/OpenApiTests/AtomicOperations/OperationsTests.cs b/test/OpenApiTests/AtomicOperations/OperationsTests.cs
index 749b3d804d..5eee4b8e9e 100644
--- a/test/OpenApiTests/AtomicOperations/OperationsTests.cs
+++ b/test/OpenApiTests/AtomicOperations/OperationsTests.cs
@@ -29,8 +29,10 @@ public OperationsTests(OpenApiTestContext, O
[Fact]
public async Task Operations_endpoint_is_exposed()
{
+ // Act
JsonElement document = await _testContext.GetSwaggerDocumentAsync();
+ // Assert
document.Should().ContainPath("paths./operations.post").Should().BeJson("""
{
"tags": [
@@ -125,8 +127,10 @@ public async Task Operations_endpoint_is_exposed()
[Fact]
public async Task Operations_request_component_schemas_are_exposed()
{
+ // Act
JsonElement document = await _testContext.GetSwaggerDocumentAsync();
+ // Assert
document.Should().ContainPath("components.schemas").With(schemasElement =>
{
schemasElement.Should().ContainPath("operationsRequestDocument").Should().BeJson("""
@@ -241,8 +245,10 @@ public async Task Operations_request_component_schemas_are_exposed()
[Fact]
public async Task Operations_response_component_schemas_are_exposed()
{
+ // Act
JsonElement document = await _testContext.GetSwaggerDocumentAsync();
+ // Assert
document.Should().ContainPath("components.schemas").With(schemasElement =>
{
schemasElement.Should().ContainPath("operationsResponseDocument").Should().BeJson("""
@@ -314,8 +320,10 @@ public async Task Operations_response_component_schemas_are_exposed()
[Fact]
public async Task Course_operation_component_schemas_are_exposed()
{
+ // Act
JsonElement document = await _testContext.GetSwaggerDocumentAsync();
+ // Assert
document.Should().ContainPath("components.schemas").With(schemasElement =>
{
// resource operations
@@ -782,8 +790,10 @@ public async Task Course_operation_component_schemas_are_exposed()
[Fact]
public async Task Student_operation_component_schemas_are_exposed()
{
+ // Act
JsonElement document = await _testContext.GetSwaggerDocumentAsync();
+ // Assert
document.Should().ContainPath("components.schemas").With(schemasElement =>
{
// resource operations
@@ -1342,8 +1352,10 @@ public async Task Student_operation_component_schemas_are_exposed()
[Fact]
public async Task Teacher_operation_component_schemas_are_exposed()
{
+ // Act
JsonElement document = await _testContext.GetSwaggerDocumentAsync();
+ // Assert
document.Should().ContainPath("components.schemas").With(schemasElement =>
{
// resource operations
@@ -1846,8 +1858,10 @@ public async Task Teacher_operation_component_schemas_are_exposed()
[Fact]
public async Task Enrollment_operation_component_schemas_are_exposed()
{
+ // Act
JsonElement document = await _testContext.GetSwaggerDocumentAsync();
+ // Assert
document.Should().ContainPath("components.schemas").With(schemasElement =>
{
// resource operations
diff --git a/test/OpenApiTests/JsonPathBuilder.cs b/test/OpenApiTests/JsonPathBuilder.cs
new file mode 100644
index 0000000000..b3c16e4a65
--- /dev/null
+++ b/test/OpenApiTests/JsonPathBuilder.cs
@@ -0,0 +1,76 @@
+using System.Collections.ObjectModel;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Resources.Annotations;
+
+#pragma warning disable AV1008 // Class should not be static
+
+namespace OpenApiTests;
+
+internal static class JsonPathBuilder
+{
+ public static readonly IReadOnlyCollection KnownEndpoints =
+ [
+ JsonApiEndpoints.GetCollection,
+ JsonApiEndpoints.GetSingle,
+ JsonApiEndpoints.GetSecondary,
+ JsonApiEndpoints.GetRelationship,
+ JsonApiEndpoints.Post,
+ JsonApiEndpoints.PostRelationship,
+ JsonApiEndpoints.Patch,
+ JsonApiEndpoints.PatchRelationship,
+ JsonApiEndpoints.Delete,
+ JsonApiEndpoints.DeleteRelationship
+ ];
+
+ public static IReadOnlyDictionary> GetEndpointPaths(ResourceType resourceType)
+ {
+ var endpointToPathMap = new Dictionary>
+ {
+ [JsonApiEndpoints.GetCollection] =
+ [
+ $"paths./{resourceType.PublicName}.get",
+ $"paths./{resourceType.PublicName}.head"
+ ],
+ [JsonApiEndpoints.GetSingle] =
+ [
+ $"paths./{resourceType.PublicName}/{{id}}.get",
+ $"paths./{resourceType.PublicName}/{{id}}.head"
+ ],
+ [JsonApiEndpoints.GetSecondary] = [],
+ [JsonApiEndpoints.GetRelationship] = [],
+ [JsonApiEndpoints.Post] = [$"paths./{resourceType.PublicName}.post"],
+ [JsonApiEndpoints.PostRelationship] = [],
+ [JsonApiEndpoints.Patch] = [$"paths./{resourceType.PublicName}/{{id}}.patch"],
+ [JsonApiEndpoints.PatchRelationship] = [],
+ [JsonApiEndpoints.Delete] = [$"paths./{resourceType.PublicName}/{{id}}.delete"],
+ [JsonApiEndpoints.DeleteRelationship] = []
+ };
+
+ foreach (RelationshipAttribute relationship in resourceType.Relationships)
+ {
+ endpointToPathMap[JsonApiEndpoints.GetSecondary].AddRange([
+ $"paths./{resourceType.PublicName}/{{id}}/{relationship.PublicName}.get",
+ $"paths./{resourceType.PublicName}/{{id}}/{relationship.PublicName}.head"
+ ]);
+
+ endpointToPathMap[JsonApiEndpoints.GetRelationship].AddRange([
+ $"paths./{resourceType.PublicName}/{{id}}/relationships/{relationship.PublicName}.get",
+ $"paths./{resourceType.PublicName}/{{id}}/relationships/{relationship.PublicName}.head"
+ ]);
+
+ endpointToPathMap[JsonApiEndpoints.PatchRelationship].Add($"paths./{resourceType.PublicName}/{{id}}/relationships/{relationship.PublicName}.patch");
+
+ if (relationship is HasManyAttribute)
+ {
+ endpointToPathMap[JsonApiEndpoints.PostRelationship].Add(
+ $"paths./{resourceType.PublicName}/{{id}}/relationships/{relationship.PublicName}.post");
+
+ endpointToPathMap[JsonApiEndpoints.DeleteRelationship].Add(
+ $"paths./{resourceType.PublicName}/{{id}}/relationships/{relationship.PublicName}.delete");
+ }
+ }
+
+ return endpointToPathMap.ToDictionary(pair => pair.Key, pair => pair.Value.AsReadOnly()).AsReadOnly();
+ }
+}
diff --git a/test/OpenApiTests/QueryStrings/QueryStringTests.cs b/test/OpenApiTests/QueryStrings/QueryStringTests.cs
index f26f3833f5..61ffc007ab 100644
--- a/test/OpenApiTests/QueryStrings/QueryStringTests.cs
+++ b/test/OpenApiTests/QueryStrings/QueryStringTests.cs
@@ -39,6 +39,7 @@ public async Task Endpoints_have_query_string_parameter(string endpointPath)
// Act
JsonElement document = await _testContext.GetSwaggerDocumentAsync();
+ // Assert
document.Should().ContainPath($"paths.{endpointPath}").With(verbElement =>
{
verbElement.Should().ContainPath("parameters").With(parametersElement =>
@@ -76,6 +77,7 @@ public async Task Endpoints_do_not_have_query_string_parameter(string endpointPa
// Act
JsonElement document = await _testContext.GetSwaggerDocumentAsync();
+ // Assert
document.Should().ContainPath($"paths.{endpointPath}").With(verbElement =>
{
verbElement.Should().ContainPath("parameters").With(parametersElement =>
diff --git a/test/OpenApiTests/RestrictedControllers/RestrictionTests.cs b/test/OpenApiTests/RestrictedControllers/RestrictionTests.cs
index ef52a5529e..2938f6867e 100644
--- a/test/OpenApiTests/RestrictedControllers/RestrictionTests.cs
+++ b/test/OpenApiTests/RestrictedControllers/RestrictionTests.cs
@@ -1,29 +1,15 @@
+using System.Collections.ObjectModel;
using System.Text.Json;
-using Humanizer;
+using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
+using Microsoft.Extensions.DependencyInjection;
using TestBuildingBlocks;
using Xunit;
-#pragma warning disable AV1532 // Loop statement contains nested loop
-
namespace OpenApiTests.RestrictedControllers;
public sealed class RestrictionTests : IClassFixture, RestrictionDbContext>>
{
- private static readonly JsonApiEndpoints[] KnownEndpoints =
- [
- JsonApiEndpoints.GetCollection,
- JsonApiEndpoints.GetSingle,
- JsonApiEndpoints.GetSecondary,
- JsonApiEndpoints.GetRelationship,
- JsonApiEndpoints.Post,
- JsonApiEndpoints.PostRelationship,
- JsonApiEndpoints.Patch,
- JsonApiEndpoints.PatchRelationship,
- JsonApiEndpoints.Delete,
- JsonApiEndpoints.DeleteRelationship
- ];
-
private readonly OpenApiTestContext, RestrictionDbContext> _testContext;
public RestrictionTests(OpenApiTestContext, RestrictionDbContext> testContext)
@@ -48,69 +34,27 @@ public RestrictionTests(OpenApiTestContext,
public async Task Only_expected_endpoints_are_exposed(Type resourceClrType, JsonApiEndpoints expected)
{
// Arrange
- string resourceName = resourceClrType.Name.Camelize().Pluralize();
-
- var endpointToPathMap = new Dictionary
- {
- [JsonApiEndpoints.GetCollection] =
- [
- $"/{resourceName}.get",
- $"/{resourceName}.head"
- ],
- [JsonApiEndpoints.GetSingle] =
- [
- $"/{resourceName}/{{id}}.get",
- $"/{resourceName}/{{id}}.head"
- ],
- [JsonApiEndpoints.GetSecondary] =
- [
- $"/{resourceName}/{{id}}/audioStreams.get",
- $"/{resourceName}/{{id}}/audioStreams.head",
- $"/{resourceName}/{{id}}/ultraHighDefinitionVideoStream.get",
- $"/{resourceName}/{{id}}/ultraHighDefinitionVideoStream.head",
- $"/{resourceName}/{{id}}/videoStream.get",
- $"/{resourceName}/{{id}}/videoStream.head"
- ],
- [JsonApiEndpoints.GetRelationship] =
- [
- $"/{resourceName}/{{id}}/relationships/audioStreams.get",
- $"/{resourceName}/{{id}}/relationships/audioStreams.head",
- $"/{resourceName}/{{id}}/relationships/ultraHighDefinitionVideoStream.get",
- $"/{resourceName}/{{id}}/relationships/ultraHighDefinitionVideoStream.head",
- $"/{resourceName}/{{id}}/relationships/videoStream.get",
- $"/{resourceName}/{{id}}/relationships/videoStream.head"
- ],
- [JsonApiEndpoints.Post] = [$"/{resourceName}.post"],
- [JsonApiEndpoints.PostRelationship] = [$"/{resourceName}/{{id}}/relationships/audioStreams.post"],
- [JsonApiEndpoints.Patch] = [$"/{resourceName}/{{id}}.patch"],
- [JsonApiEndpoints.PatchRelationship] =
- [
- $"/{resourceName}/{{id}}/relationships/audioStreams.patch",
- $"/{resourceName}/{{id}}/relationships/ultraHighDefinitionVideoStream.patch",
- $"/{resourceName}/{{id}}/relationships/videoStream.patch"
- ],
- [JsonApiEndpoints.Delete] = [$"/{resourceName}/{{id}}.delete"],
- [JsonApiEndpoints.DeleteRelationship] = [$"/{resourceName}/{{id}}/relationships/audioStreams.delete"]
- };
+ var resourceGraph = _testContext.Factory.Services.GetRequiredService();
+ ResourceType resourceType = resourceGraph.GetResourceType(resourceClrType);
+ IReadOnlyDictionary> endpointToPathMap = JsonPathBuilder.GetEndpointPaths(resourceType);
// Act
JsonElement document = await _testContext.GetSwaggerDocumentAsync();
- foreach (JsonApiEndpoints endpoint in KnownEndpoints.Where(value => expected.HasFlag(value)))
- {
- string[] pathsExpected = endpointToPathMap[endpoint];
- string[] pathsNotExpected = endpointToPathMap.Values.SelectMany(paths => paths).Except(pathsExpected).ToArray();
+ // Assert
+ string[] pathsExpected = JsonPathBuilder.KnownEndpoints.Where(endpoint => expected.HasFlag(endpoint))
+ .SelectMany(endpoint => endpointToPathMap[endpoint]).ToArray();
- // Assert
- foreach (string path in pathsExpected)
- {
- document.Should().ContainPath($"paths.{path}");
- }
+ string[] pathsNotExpected = endpointToPathMap.Values.SelectMany(paths => paths).Except(pathsExpected).ToArray();
- foreach (string path in pathsNotExpected)
- {
- document.Should().NotContainPath($"paths{path}");
- }
+ foreach (string path in pathsExpected)
+ {
+ document.Should().ContainPath(path);
+ }
+
+ foreach (string path in pathsNotExpected)
+ {
+ document.Should().NotContainPath(path);
}
}
}
diff --git a/test/TestBuildingBlocks/IntegrationTestContext.cs b/test/TestBuildingBlocks/IntegrationTestContext.cs
index c895765a3e..4876388c3a 100644
--- a/test/TestBuildingBlocks/IntegrationTestContext.cs
+++ b/test/TestBuildingBlocks/IntegrationTestContext.cs
@@ -106,17 +106,32 @@ private static void SetDbContextDebugOptions(DbContextOptionsBuilder options)
public void ConfigureLogging(Action loggingConfiguration)
{
+ if (_loggingConfiguration != null && _loggingConfiguration != loggingConfiguration)
+ {
+ throw new InvalidOperationException($"Do not call {nameof(ConfigureLogging)} multiple times.");
+ }
+
_loggingConfiguration = loggingConfiguration;
}
public void ConfigureServices(Action configureServices)
{
+ if (_configureServices != null && _configureServices != configureServices)
+ {
+ throw new InvalidOperationException($"Do not call {nameof(ConfigureServices)} multiple times.");
+ }
+
_configureServices = configureServices;
}
- public void PostConfigureServices(Action configureServices)
+ public void PostConfigureServices(Action postConfigureServices)
{
- _postConfigureServices = configureServices;
+ if (_postConfigureServices != null && _postConfigureServices != postConfigureServices)
+ {
+ throw new InvalidOperationException($"Do not call {nameof(PostConfigureServices)} multiple times.");
+ }
+
+ _postConfigureServices = postConfigureServices;
}
public async Task RunOnDatabaseAsync(Func asyncAction)