From ca2cda086777c420cccfba03470d6b8fc1cf6b73 Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Wed, 27 Nov 2024 16:29:30 +0100
Subject: [PATCH 1/4] IJsonApiEndpointFilter: remove controller action methods
at runtime
---
.../IAtomicOperationFilter.cs | 2 +-
.../JsonApiApplicationBuilder.cs | 1 +
.../AlwaysEnabledJsonApiEndpointFilter.cs | 13 +++
.../HttpMethodAttributeExtensions.cs | 56 ++++++++++
.../Middleware/IJsonApiEndpointFilter.cs | 24 +++++
.../Middleware/JsonApiRoutingConvention.cs | 100 +++++++++++-------
.../ObfuscatedIdentifiableController.cs | 4 +
.../EndpointFilterTests.cs | 61 +++++++++++
.../Controllers/GetJsonApiEndpointTests.cs | 43 ++++++++
9 files changed, 263 insertions(+), 41 deletions(-)
create mode 100644 src/JsonApiDotNetCore/Middleware/AlwaysEnabledJsonApiEndpointFilter.cs
create mode 100644 src/JsonApiDotNetCore/Middleware/HttpMethodAttributeExtensions.cs
create mode 100644 src/JsonApiDotNetCore/Middleware/IJsonApiEndpointFilter.cs
create mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/EndpointFilterTests.cs
create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/Controllers/GetJsonApiEndpointTests.cs
diff --git a/src/JsonApiDotNetCore/AtomicOperations/IAtomicOperationFilter.cs b/src/JsonApiDotNetCore/AtomicOperations/IAtomicOperationFilter.cs
index 240efbf936..47d534c5b5 100644
--- a/src/JsonApiDotNetCore/AtomicOperations/IAtomicOperationFilter.cs
+++ b/src/JsonApiDotNetCore/AtomicOperations/IAtomicOperationFilter.cs
@@ -6,7 +6,7 @@
namespace JsonApiDotNetCore.AtomicOperations;
///
-/// Determines whether an operation in an atomic:operations request can be used.
+/// Determines whether an operation in an atomic:operations request can be used. For non-operations requests, see .
///
///
/// The default implementation relies on the usage of . If you're using explicit
diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
index 8f94195580..8dc8f47b77 100644
--- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
+++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
@@ -184,6 +184,7 @@ private void AddMiddlewareLayer()
_services.TryAddSingleton();
_services.TryAddSingleton();
_services.TryAddSingleton(provider => provider.GetRequiredService());
+ _services.TryAddSingleton();
_services.TryAddSingleton();
_services.TryAddSingleton();
_services.TryAddScoped();
diff --git a/src/JsonApiDotNetCore/Middleware/AlwaysEnabledJsonApiEndpointFilter.cs b/src/JsonApiDotNetCore/Middleware/AlwaysEnabledJsonApiEndpointFilter.cs
new file mode 100644
index 0000000000..c3918d7462
--- /dev/null
+++ b/src/JsonApiDotNetCore/Middleware/AlwaysEnabledJsonApiEndpointFilter.cs
@@ -0,0 +1,13 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+
+namespace JsonApiDotNetCore.Middleware;
+
+internal sealed class AlwaysEnabledJsonApiEndpointFilter : IJsonApiEndpointFilter
+{
+ ///
+ public bool IsEnabled(ResourceType resourceType, JsonApiEndpoints endpoint)
+ {
+ return true;
+ }
+}
diff --git a/src/JsonApiDotNetCore/Middleware/HttpMethodAttributeExtensions.cs b/src/JsonApiDotNetCore/Middleware/HttpMethodAttributeExtensions.cs
new file mode 100644
index 0000000000..e00cdd326d
--- /dev/null
+++ b/src/JsonApiDotNetCore/Middleware/HttpMethodAttributeExtensions.cs
@@ -0,0 +1,56 @@
+using JsonApiDotNetCore.Controllers;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Routing;
+
+namespace JsonApiDotNetCore.Middleware;
+
+internal static class HttpMethodAttributeExtensions
+{
+ private const string IdTemplate = "{id}";
+ private const string RelationshipNameTemplate = "{relationshipName}";
+ private const string SecondaryEndpointTemplate = $"{IdTemplate}/{RelationshipNameTemplate}";
+ private const string RelationshipEndpointTemplate = $"{IdTemplate}/relationships/{RelationshipNameTemplate}";
+
+ public static JsonApiEndpoints GetJsonApiEndpoint(this IEnumerable httpMethods)
+ {
+ ArgumentGuard.NotNull(httpMethods);
+
+ HttpMethodAttribute[] nonHeadAttributes = httpMethods.Where(attribute => attribute is not HttpHeadAttribute).ToArray();
+
+ return nonHeadAttributes.Length == 1 ? ResolveJsonApiEndpoint(nonHeadAttributes[0]) : JsonApiEndpoints.None;
+ }
+
+ private static JsonApiEndpoints ResolveJsonApiEndpoint(HttpMethodAttribute httpMethod)
+ {
+ return httpMethod switch
+ {
+ HttpGetAttribute httpGet => httpGet.Template switch
+ {
+ null => JsonApiEndpoints.GetCollection,
+ IdTemplate => JsonApiEndpoints.GetSingle,
+ SecondaryEndpointTemplate => JsonApiEndpoints.GetSecondary,
+ RelationshipEndpointTemplate => JsonApiEndpoints.GetRelationship,
+ _ => JsonApiEndpoints.None
+ },
+ HttpPostAttribute httpPost => httpPost.Template switch
+ {
+ null => JsonApiEndpoints.Post,
+ RelationshipEndpointTemplate => JsonApiEndpoints.PostRelationship,
+ _ => JsonApiEndpoints.None
+ },
+ HttpPatchAttribute httpPatch => httpPatch.Template switch
+ {
+ IdTemplate => JsonApiEndpoints.Patch,
+ RelationshipEndpointTemplate => JsonApiEndpoints.PatchRelationship,
+ _ => JsonApiEndpoints.None
+ },
+ HttpDeleteAttribute httpDelete => httpDelete.Template switch
+ {
+ IdTemplate => JsonApiEndpoints.Delete,
+ RelationshipEndpointTemplate => JsonApiEndpoints.DeleteRelationship,
+ _ => JsonApiEndpoints.None
+ },
+ _ => JsonApiEndpoints.None
+ };
+ }
+}
diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiEndpointFilter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiEndpointFilter.cs
new file mode 100644
index 0000000000..6dbf81bce3
--- /dev/null
+++ b/src/JsonApiDotNetCore/Middleware/IJsonApiEndpointFilter.cs
@@ -0,0 +1,24 @@
+using JetBrains.Annotations;
+using JsonApiDotNetCore.AtomicOperations;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+
+namespace JsonApiDotNetCore.Middleware;
+
+///
+/// Enables to remove JSON:API controller action methods at startup. For atomic:operation requests, see .
+///
+[PublicAPI]
+public interface IJsonApiEndpointFilter
+{
+ ///
+ /// Determines whether to remove the associated controller action method.
+ ///
+ ///
+ /// The primary resource type of the endpoint.
+ ///
+ ///
+ /// The JSON:API endpoint. Despite being a enum, a single value is always passed here.
+ ///
+ bool IsEnabled(ResourceType resourceType, JsonApiEndpoints endpoint);
+}
diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs
index 72cac28daa..8403109d19 100644
--- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs
+++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs
@@ -7,43 +7,48 @@
using JsonApiDotNetCore.Resources;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
+using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.Logging;
namespace JsonApiDotNetCore.Middleware;
///
-/// The default routing convention registers the name of the resource as the route using the serializer naming convention. The default for this is a
-/// camel case formatter. If the controller directly inherits from and there is no resource directly associated, it
-/// uses the name of the controller instead of the name of the type.
+/// Registers routes based on the JSON:API resource name, which defaults to camel-case pluralized form of the resource CLR type name. If unavailable (for
+/// example, when a controller directly inherits from ), the serializer naming convention is applied on the
+/// controller type name (camel-case by default).
///
/// { } // => /someResources/relationship/relatedResource
+/// // controller name is ignored when resource type is available:
+/// public class RandomNameController : JsonApiController { } // => /someResources
///
-/// public class RandomNameController : JsonApiController { } // => /someResources/relationship/relatedResource
+/// // when using kebab-case naming convention in options:
+/// public class RandomNameController : JsonApiController { } // => /some-resources
///
-/// // when using kebab-case naming convention:
-/// public class SomeResourceController : JsonApiController { } // => /some-resources/relationship/related-resource
-///
-/// public class SomeVeryCustomController : CoreJsonApiController { } // => /someVeryCustoms/relationship/relatedResource
+/// // unable to determine resource type:
+/// public class SomeVeryCustomController : CoreJsonApiController { } // => /someVeryCustom
/// ]]>
[PublicAPI]
public sealed partial class JsonApiRoutingConvention : IJsonApiRoutingConvention
{
private readonly IJsonApiOptions _options;
private readonly IResourceGraph _resourceGraph;
+ private readonly IJsonApiEndpointFilter _jsonApiEndpointFilter;
private readonly ILogger _logger;
private readonly Dictionary _registeredControllerNameByTemplate = [];
private readonly Dictionary _resourceTypePerControllerTypeMap = [];
private readonly Dictionary _controllerPerResourceTypeMap = [];
- public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resourceGraph, ILogger logger)
+ public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resourceGraph, IJsonApiEndpointFilter jsonApiEndpointFilter,
+ ILogger logger)
{
ArgumentGuard.NotNull(options);
ArgumentGuard.NotNull(resourceGraph);
+ ArgumentGuard.NotNull(jsonApiEndpointFilter);
ArgumentGuard.NotNull(logger);
_options = options;
_resourceGraph = resourceGraph;
+ _jsonApiEndpointFilter = jsonApiEndpointFilter;
_logger = logger;
}
@@ -106,6 +111,8 @@ public void Apply(ApplicationModel application)
$"Multiple controllers found for resource type '{resourceType}': '{existingModel.ControllerType}' and '{controller.ControllerType}'.");
}
+ RemoveDisabledActionMethods(controller, resourceType);
+
_resourceTypePerControllerTypeMap.Add(controller.ControllerType, resourceType);
_controllerPerResourceTypeMap.Add(resourceType, controller);
}
@@ -148,34 +155,10 @@ private static bool HasApiControllerAttribute(ControllerModel controller)
return controller.ControllerType.GetCustomAttribute() != null;
}
- private static bool IsRoutingConventionDisabled(ControllerModel controller)
- {
- return controller.ControllerType.GetCustomAttribute(true) != null;
- }
-
- ///
- /// Derives a template from the resource type, and checks if this template was already registered.
- ///
- private string? TemplateFromResource(ControllerModel model)
- {
- if (_resourceTypePerControllerTypeMap.TryGetValue(model.ControllerType, out ResourceType? resourceType))
- {
- return $"{_options.Namespace}/{resourceType.PublicName}";
- }
-
- return null;
- }
-
- ///
- /// Derives a template from the controller name, and checks if this template was already registered.
- ///
- private string TemplateFromController(ControllerModel model)
+ private static bool IsOperationsController(Type type)
{
- string controllerName = _options.SerializerOptions.PropertyNamingPolicy == null
- ? model.ControllerName
- : _options.SerializerOptions.PropertyNamingPolicy.ConvertName(model.ControllerName);
-
- return $"{_options.Namespace}/{controllerName}";
+ Type baseControllerType = typeof(BaseJsonApiOperationsController);
+ return baseControllerType.IsAssignableFrom(type);
}
///
@@ -213,10 +196,47 @@ private string TemplateFromController(ControllerModel model)
return currentType?.GetGenericArguments().First();
}
- private static bool IsOperationsController(Type type)
+ private void RemoveDisabledActionMethods(ControllerModel controller, ResourceType resourceType)
{
- Type baseControllerType = typeof(BaseJsonApiOperationsController);
- return baseControllerType.IsAssignableFrom(type);
+ foreach (ActionModel actionModel in controller.Actions.ToArray())
+ {
+ JsonApiEndpoints endpoint = actionModel.Attributes.OfType().GetJsonApiEndpoint();
+
+ if (endpoint != JsonApiEndpoints.None && !_jsonApiEndpointFilter.IsEnabled(resourceType, endpoint))
+ {
+ controller.Actions.Remove(actionModel);
+ }
+ }
+ }
+
+ private static bool IsRoutingConventionDisabled(ControllerModel controller)
+ {
+ return controller.ControllerType.GetCustomAttribute(true) != null;
+ }
+
+ ///
+ /// Derives a template from the resource type, and checks if this template was already registered.
+ ///
+ private string? TemplateFromResource(ControllerModel model)
+ {
+ if (_resourceTypePerControllerTypeMap.TryGetValue(model.ControllerType, out ResourceType? resourceType))
+ {
+ return $"{_options.Namespace}/{resourceType.PublicName}";
+ }
+
+ return null;
+ }
+
+ ///
+ /// Derives a template from the controller name, and checks if this template was already registered.
+ ///
+ private string TemplateFromController(ControllerModel model)
+ {
+ string controllerName = _options.SerializerOptions.PropertyNamingPolicy == null
+ ? model.ControllerName
+ : _options.SerializerOptions.PropertyNamingPolicy.ConvertName(model.ControllerName);
+
+ return $"{_options.Namespace}/{controllerName}";
}
[LoggerMessage(Level = LogLevel.Warning,
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs
index d82342a0bc..ffe73f7b9a 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs
@@ -18,12 +18,14 @@ public abstract class ObfuscatedIdentifiableController(
private readonly HexadecimalCodec _codec = new();
[HttpGet]
+ [HttpHead]
public override Task GetAsync(CancellationToken cancellationToken)
{
return base.GetAsync(cancellationToken);
}
[HttpGet("{id}")]
+ [HttpHead("{id}")]
public Task GetAsync([Required] string id, CancellationToken cancellationToken)
{
int idValue = _codec.Decode(id);
@@ -31,6 +33,7 @@ public Task GetAsync([Required] string id, CancellationToken canc
}
[HttpGet("{id}/{relationshipName}")]
+ [HttpHead("{id}/{relationshipName}")]
public Task GetSecondaryAsync([Required] string id, [Required] [PreserveEmptyString] string relationshipName,
CancellationToken cancellationToken)
{
@@ -39,6 +42,7 @@ public Task GetSecondaryAsync([Required] string id, [Required] [P
}
[HttpGet("{id}/relationships/{relationshipName}")]
+ [HttpHead("{id}/relationships/{relationshipName}")]
public Task GetRelationshipAsync([Required] string id, [Required] [PreserveEmptyString] string relationshipName,
CancellationToken cancellationToken)
{
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/EndpointFilterTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/EndpointFilterTests.cs
new file mode 100644
index 0000000000..87289ab555
--- /dev/null
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/EndpointFilterTests.cs
@@ -0,0 +1,61 @@
+using System.Net;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Middleware;
+using Microsoft.Extensions.DependencyInjection;
+using TestBuildingBlocks;
+using Xunit;
+
+namespace JsonApiDotNetCoreTests.IntegrationTests.RestrictedControllers;
+
+public sealed class EndpointFilterTests : IClassFixture, RestrictionDbContext>>
+{
+ private readonly IntegrationTestContext, RestrictionDbContext> _testContext;
+ private readonly RestrictionFakers _fakers = new();
+
+ public EndpointFilterTests(IntegrationTestContext, RestrictionDbContext> testContext)
+ {
+ _testContext = testContext;
+
+ testContext.UseController();
+
+ testContext.ConfigureServices(services => services.AddSingleton());
+ }
+
+ [Fact]
+ public async Task Cannot_get_relationship()
+ {
+ // Arrange
+ Bed bed = _fakers.Bed.GenerateOne();
+
+ await _testContext.RunOnDatabaseAsync(async dbContext =>
+ {
+ dbContext.Beds.Add(bed);
+ await dbContext.SaveChangesAsync();
+ });
+
+ string route = $"/beds/{bed.StringId}/relationships/pillows";
+
+ // Act
+ (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound);
+ }
+
+ private sealed class NoRelationshipsAtBedJsonApiEndpointFilter : IJsonApiEndpointFilter
+ {
+ public bool IsEnabled(ResourceType resourceType, JsonApiEndpoints endpoint)
+ {
+ return !IsGetRelationshipAtBed(endpoint, resourceType);
+ }
+
+ private static bool IsGetRelationshipAtBed(JsonApiEndpoints endpoint, ResourceType resourceType)
+ {
+ bool isRelationshipEndpoint = endpoint is JsonApiEndpoints.GetRelationship or JsonApiEndpoints.PostRelationship or
+ JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship;
+
+ return isRelationshipEndpoint && resourceType.ClrType == typeof(Bed);
+ }
+ }
+}
diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Controllers/GetJsonApiEndpointTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Controllers/GetJsonApiEndpointTests.cs
new file mode 100644
index 0000000000..f78edebe49
--- /dev/null
+++ b/test/JsonApiDotNetCoreTests/UnitTests/Controllers/GetJsonApiEndpointTests.cs
@@ -0,0 +1,43 @@
+using FluentAssertions;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Middleware;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Xunit;
+
+namespace JsonApiDotNetCoreTests.UnitTests.Controllers;
+
+public sealed class GetJsonApiEndpointTests
+{
+ [Theory]
+ [InlineData("GET", null, JsonApiEndpoints.GetCollection)]
+ [InlineData("GET", "{id}", JsonApiEndpoints.GetSingle)]
+ [InlineData("GET", "{id}/{relationshipName}", JsonApiEndpoints.GetSecondary)]
+ [InlineData("GET", "{id}/relationships/{relationshipName}", JsonApiEndpoints.GetRelationship)]
+ [InlineData("POST", null, JsonApiEndpoints.Post)]
+ [InlineData("POST", "{id}/relationships/{relationshipName}", JsonApiEndpoints.PostRelationship)]
+ [InlineData("PATCH", "{id}", JsonApiEndpoints.Patch)]
+ [InlineData("PATCH", "{id}/relationships/{relationshipName}", JsonApiEndpoints.PatchRelationship)]
+ [InlineData("DELETE", "{id}", JsonApiEndpoints.Delete)]
+ [InlineData("DELETE", "{id}/relationships/{relationshipName}", JsonApiEndpoints.DeleteRelationship)]
+ [InlineData("PUT", null, JsonApiEndpoints.None)]
+ public void Can_identify_endpoint_from_http_method_and_route_template(string httpMethod, string? routeTemplate, JsonApiEndpoints expected)
+ {
+ // Arrange
+ HttpMethodAttribute attribute = httpMethod switch
+ {
+ "GET" => routeTemplate == null ? new HttpGetAttribute() : new HttpGetAttribute(routeTemplate),
+ "POST" => routeTemplate == null ? new HttpPostAttribute() : new HttpPostAttribute(routeTemplate),
+ "PATCH" => routeTemplate == null ? new HttpPatchAttribute() : new HttpPatchAttribute(routeTemplate),
+ "DELETE" => routeTemplate == null ? new HttpDeleteAttribute() : new HttpDeleteAttribute(routeTemplate),
+ "PUT" => routeTemplate == null ? new HttpPutAttribute() : new HttpPutAttribute(routeTemplate),
+ _ => throw new ArgumentOutOfRangeException(nameof(httpMethod), httpMethod, null)
+ };
+
+ // Act
+ JsonApiEndpoints endpoint = HttpMethodAttributeExtensions.GetJsonApiEndpoint([attribute]);
+
+ // Assert
+ endpoint.Should().Be(expected);
+ }
+}
From d98a732f130a23e278301e3148587e49af4c376b Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Wed, 27 Nov 2024 16:30:50 +0100
Subject: [PATCH 2/4] Rename for consistency
---
.../BaseJsonApiOperationsController.cs | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
index 1ed6afec83..3c8ebac01a 100644
--- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
+++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
@@ -136,11 +136,11 @@ protected virtual void ValidateEnabledOperations(IList opera
for (int operationIndex = 0; operationIndex < operations.Count; operationIndex++)
{
IJsonApiRequest operationRequest = operations[operationIndex].Request;
- WriteOperationKind operationKind = operationRequest.WriteOperation!.Value;
+ WriteOperationKind writeOperation = operationRequest.WriteOperation!.Value;
- if (operationRequest.Relationship != null && !_operationFilter.IsEnabled(operationRequest.Relationship.LeftType, operationKind))
+ if (operationRequest.Relationship != null && !_operationFilter.IsEnabled(operationRequest.Relationship.LeftType, writeOperation))
{
- string operationCode = GetOperationCodeText(operationKind);
+ string operationCode = GetOperationCodeText(writeOperation);
errors.Add(new ErrorObject(HttpStatusCode.Forbidden)
{
@@ -153,9 +153,9 @@ protected virtual void ValidateEnabledOperations(IList opera
}
});
}
- else if (operationRequest.PrimaryResourceType != null && !_operationFilter.IsEnabled(operationRequest.PrimaryResourceType, operationKind))
+ else if (operationRequest.PrimaryResourceType != null && !_operationFilter.IsEnabled(operationRequest.PrimaryResourceType, writeOperation))
{
- string operationCode = GetOperationCodeText(operationKind);
+ string operationCode = GetOperationCodeText(writeOperation);
errors.Add(new ErrorObject(HttpStatusCode.Forbidden)
{
@@ -175,9 +175,9 @@ protected virtual void ValidateEnabledOperations(IList opera
}
}
- private static string GetOperationCodeText(WriteOperationKind operationKind)
+ private static string GetOperationCodeText(WriteOperationKind writeOperation)
{
- AtomicOperationCode operationCode = operationKind switch
+ AtomicOperationCode operationCode = writeOperation switch
{
WriteOperationKind.CreateResource => AtomicOperationCode.Add,
WriteOperationKind.UpdateResource => AtomicOperationCode.Update,
@@ -185,7 +185,7 @@ private static string GetOperationCodeText(WriteOperationKind operationKind)
WriteOperationKind.AddToRelationship => AtomicOperationCode.Add,
WriteOperationKind.SetRelationship => AtomicOperationCode.Update,
WriteOperationKind.RemoveFromRelationship => AtomicOperationCode.Remove,
- _ => throw new NotSupportedException($"Unknown operation kind '{operationKind}'.")
+ _ => throw new NotSupportedException($"Unknown operation kind '{writeOperation}'.")
};
return operationCode.ToString().ToLowerInvariant();
From 5dd48c42835cae3be062c11a7a9862037d6a556f Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Wed, 27 Nov 2024 16:31:21 +0100
Subject: [PATCH 3/4] Clarify documenation
---
src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs b/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs
index d8d5d63f3e..07817698e0 100644
--- a/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs
+++ b/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs
@@ -217,7 +217,7 @@ public RelationshipAttribute GetRelationshipByPropertyName(string propertyName)
}
///
- /// Returns all directly and indirectly non-abstract resource types that derive from this resource type.
+ /// Returns all non-abstract resource types that directly or indirectly derive from this resource type.
///
public IReadOnlySet GetAllConcreteDerivedTypes()
{
From 11aafa435a96c99926122b9a7f63179e34b1b1db Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Wed, 27 Nov 2024 18:07:37 +0100
Subject: [PATCH 4/4] Remove JsonApiEndpoint, reuse logic from base library
---
...onApiActionDescriptorCollectionProvider.cs | 8 +-
.../JsonApiEndpoint.cs | 16 --
.../JsonApiMetadata/EndpointResolver.cs | 60 ++-----
.../JsonApiEndpointMetadataProvider.cs | 43 +++--
.../JsonApiPathParameter.cs | 8 -
.../JsonApiRoutingTemplate.cs | 12 --
.../OpenApiEndpointConvention.cs | 155 +++++++++++-------
.../ServiceCollectionExtensions.cs | 1 -
.../EndpointOrderingFilter.cs | 4 +-
9 files changed, 132 insertions(+), 175 deletions(-)
delete mode 100644 src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiEndpoint.cs
delete mode 100644 src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiPathParameter.cs
delete mode 100644 src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiRoutingTemplate.cs
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs
index f8fb49d6bf..486d261490 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs
@@ -163,7 +163,7 @@ private static List Expand(ActionDescriptor genericEndpoint, N
ActionDescriptor expandedEndpoint = Clone(genericEndpoint);
- RemovePathParameter(expandedEndpoint.Parameters, JsonApiPathParameter.RelationshipName);
+ RemovePathParameter(expandedEndpoint.Parameters, "relationshipName");
ExpandTemplate(expandedEndpoint.AttributeRouteInfo!, relationshipName);
@@ -212,12 +212,12 @@ private static FilterDescriptor Clone(FilterDescriptor descriptor)
private static void RemovePathParameter(ICollection parameters, string parameterName)
{
- ParameterDescriptor relationshipName = parameters.Single(parameterDescriptor => parameterDescriptor.Name == parameterName);
- parameters.Remove(relationshipName);
+ ParameterDescriptor descriptor = parameters.Single(parameterDescriptor => parameterDescriptor.Name == parameterName);
+ parameters.Remove(descriptor);
}
private static void ExpandTemplate(AttributeRouteInfo route, string expansionParameter)
{
- route.Template = route.Template!.Replace(JsonApiRoutingTemplate.RelationshipNameRoutePlaceholder, expansionParameter);
+ route.Template = route.Template!.Replace("{relationshipName}", expansionParameter);
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiEndpoint.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiEndpoint.cs
deleted file mode 100644
index 570a97699b..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiEndpoint.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
-
-internal enum JsonApiEndpoint
-{
- GetCollection,
- GetSingle,
- GetSecondary,
- GetRelationship,
- PostResource,
- PostRelationship,
- PatchResource,
- PatchRelationship,
- DeleteResource,
- DeleteRelationship,
- PostOperations
-}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/EndpointResolver.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/EndpointResolver.cs
index fe5da000c9..6107956d78 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/EndpointResolver.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/EndpointResolver.cs
@@ -1,72 +1,38 @@
using System.Reflection;
using JsonApiDotNetCore.Controllers;
-using Microsoft.AspNetCore.Mvc;
+using JsonApiDotNetCore.Middleware;
using Microsoft.AspNetCore.Mvc.Routing;
namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
internal sealed class EndpointResolver
{
- public JsonApiEndpoint? Get(MethodInfo controllerAction)
+ public static EndpointResolver Instance { get; } = new();
+
+ private EndpointResolver()
+ {
+ }
+
+ public JsonApiEndpoints GetEndpoint(MethodInfo controllerAction)
{
ArgumentGuard.NotNull(controllerAction);
if (!IsJsonApiController(controllerAction))
{
- return null;
- }
-
- if (IsAtomicOperationsController(controllerAction))
- {
- return JsonApiEndpoint.PostOperations;
+ return JsonApiEndpoints.None;
}
- HttpMethodAttribute? method = Attribute.GetCustomAttributes(controllerAction, true).OfType().FirstOrDefault();
-
- return ResolveJsonApiEndpoint(method);
+ IEnumerable httpMethodAttributes = controllerAction.GetCustomAttributes(true);
+ return httpMethodAttributes.GetJsonApiEndpoint();
}
- private static bool IsJsonApiController(MethodInfo controllerAction)
+ private bool IsJsonApiController(MethodInfo controllerAction)
{
return typeof(CoreJsonApiController).IsAssignableFrom(controllerAction.ReflectedType);
}
- private static bool IsAtomicOperationsController(MethodInfo controllerAction)
+ public bool IsAtomicOperationsController(MethodInfo controllerAction)
{
return typeof(BaseJsonApiOperationsController).IsAssignableFrom(controllerAction.ReflectedType);
}
-
- private static JsonApiEndpoint? ResolveJsonApiEndpoint(HttpMethodAttribute? httpMethod)
- {
- return httpMethod switch
- {
- HttpGetAttribute attr => attr.Template switch
- {
- null => JsonApiEndpoint.GetCollection,
- JsonApiRoutingTemplate.PrimaryEndpoint => JsonApiEndpoint.GetSingle,
- JsonApiRoutingTemplate.SecondaryEndpoint => JsonApiEndpoint.GetSecondary,
- JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.GetRelationship,
- _ => null
- },
- HttpPostAttribute attr => attr.Template switch
- {
- null => JsonApiEndpoint.PostResource,
- JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.PostRelationship,
- _ => null
- },
- HttpPatchAttribute attr => attr.Template switch
- {
- JsonApiRoutingTemplate.PrimaryEndpoint => JsonApiEndpoint.PatchResource,
- JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.PatchRelationship,
- _ => null
- },
- HttpDeleteAttribute attr => attr.Template switch
- {
- JsonApiRoutingTemplate.PrimaryEndpoint => JsonApiEndpoint.DeleteResource,
- JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.DeleteRelationship,
- _ => null
- },
- _ => null
- };
- }
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs
index 471c490b2d..1c22b1505e 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Documents;
using JsonApiDotNetCore.Resources.Annotations;
@@ -12,18 +13,14 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
///
internal sealed class JsonApiEndpointMetadataProvider
{
- private readonly EndpointResolver _endpointResolver;
private readonly IControllerResourceMapping _controllerResourceMapping;
private readonly NonPrimaryDocumentTypeFactory _nonPrimaryDocumentTypeFactory;
- public JsonApiEndpointMetadataProvider(EndpointResolver endpointResolver, IControllerResourceMapping controllerResourceMapping,
- NonPrimaryDocumentTypeFactory nonPrimaryDocumentTypeFactory)
+ public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerResourceMapping, NonPrimaryDocumentTypeFactory nonPrimaryDocumentTypeFactory)
{
- ArgumentGuard.NotNull(endpointResolver);
ArgumentGuard.NotNull(controllerResourceMapping);
ArgumentGuard.NotNull(nonPrimaryDocumentTypeFactory);
- _endpointResolver = endpointResolver;
_controllerResourceMapping = controllerResourceMapping;
_nonPrimaryDocumentTypeFactory = nonPrimaryDocumentTypeFactory;
}
@@ -32,16 +29,16 @@ public JsonApiEndpointMetadataContainer Get(MethodInfo controllerAction)
{
ArgumentGuard.NotNull(controllerAction);
- JsonApiEndpoint? endpoint = _endpointResolver.Get(controllerAction);
-
- if (endpoint == null)
+ if (EndpointResolver.Instance.IsAtomicOperationsController(controllerAction))
{
- throw new NotSupportedException($"Unable to provide metadata for non-JSON:API endpoint '{controllerAction.ReflectedType!.FullName}'.");
+ return new JsonApiEndpointMetadataContainer(AtomicOperationsRequestMetadata.Instance, AtomicOperationsResponseMetadata.Instance);
}
- if (endpoint == JsonApiEndpoint.PostOperations)
+ JsonApiEndpoints endpoint = EndpointResolver.Instance.GetEndpoint(controllerAction);
+
+ if (endpoint == JsonApiEndpoints.None)
{
- return new JsonApiEndpointMetadataContainer(AtomicOperationsRequestMetadata.Instance, AtomicOperationsResponseMetadata.Instance);
+ throw new NotSupportedException($"Unable to provide metadata for non-JSON:API endpoint '{controllerAction.ReflectedType!.FullName}'.");
}
ResourceType? primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(controllerAction.ReflectedType);
@@ -51,19 +48,19 @@ public JsonApiEndpointMetadataContainer Get(MethodInfo controllerAction)
throw new UnreachableCodeException();
}
- IJsonApiRequestMetadata? requestMetadata = GetRequestMetadata(endpoint.Value, primaryResourceType);
- IJsonApiResponseMetadata? responseMetadata = GetResponseMetadata(endpoint.Value, primaryResourceType);
+ IJsonApiRequestMetadata? requestMetadata = GetRequestMetadata(endpoint, primaryResourceType);
+ IJsonApiResponseMetadata? responseMetadata = GetResponseMetadata(endpoint, primaryResourceType);
return new JsonApiEndpointMetadataContainer(requestMetadata, responseMetadata);
}
- private IJsonApiRequestMetadata? GetRequestMetadata(JsonApiEndpoint endpoint, ResourceType primaryResourceType)
+ private IJsonApiRequestMetadata? GetRequestMetadata(JsonApiEndpoints endpoint, ResourceType primaryResourceType)
{
return endpoint switch
{
- JsonApiEndpoint.PostResource => GetPostResourceRequestMetadata(primaryResourceType.ClrType),
- JsonApiEndpoint.PatchResource => GetPatchResourceRequestMetadata(primaryResourceType.ClrType),
- JsonApiEndpoint.PostRelationship or JsonApiEndpoint.PatchRelationship or JsonApiEndpoint.DeleteRelationship => GetRelationshipRequestMetadata(
- primaryResourceType.Relationships, endpoint != JsonApiEndpoint.PatchRelationship),
+ JsonApiEndpoints.Post => GetPostResourceRequestMetadata(primaryResourceType.ClrType),
+ JsonApiEndpoints.Patch => GetPatchResourceRequestMetadata(primaryResourceType.ClrType),
+ JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship => GetRelationshipRequestMetadata(
+ primaryResourceType.Relationships, endpoint != JsonApiEndpoints.PatchRelationship),
_ => null
};
}
@@ -92,14 +89,14 @@ private RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable
- GetPrimaryResponseMetadata(primaryResourceType.ClrType, endpoint == JsonApiEndpoint.GetCollection),
- JsonApiEndpoint.GetSecondary => GetSecondaryResponseMetadata(primaryResourceType.Relationships),
- JsonApiEndpoint.GetRelationship => GetRelationshipResponseMetadata(primaryResourceType.Relationships),
+ JsonApiEndpoints.GetCollection or JsonApiEndpoints.GetSingle or JsonApiEndpoints.Post or JsonApiEndpoints.Patch => GetPrimaryResponseMetadata(
+ primaryResourceType.ClrType, endpoint == JsonApiEndpoints.GetCollection),
+ JsonApiEndpoints.GetSecondary => GetSecondaryResponseMetadata(primaryResourceType.Relationships),
+ JsonApiEndpoints.GetRelationship => GetRelationshipResponseMetadata(primaryResourceType.Relationships),
_ => null
};
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiPathParameter.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiPathParameter.cs
deleted file mode 100644
index 9edc5af9eb..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiPathParameter.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-#pragma warning disable AV1008 // Class should not be static
-
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
-
-internal static class JsonApiPathParameter
-{
- public const string RelationshipName = "relationshipName";
-}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiRoutingTemplate.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiRoutingTemplate.cs
deleted file mode 100644
index 62d118ea56..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiRoutingTemplate.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-#pragma warning disable AV1008 // Class should not be static
-
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
-
-internal static class JsonApiRoutingTemplate
-{
- public const string RelationshipNameRoutePlaceholder = "{" + JsonApiPathParameter.RelationshipName + "}";
- public const string RelationshipsPart = "relationships";
- public const string PrimaryEndpoint = "{id}";
- public const string SecondaryEndpoint = "{id}/" + RelationshipNameRoutePlaceholder;
- public const string RelationshipEndpoint = "{id}/" + RelationshipsPart + "/" + RelationshipNameRoutePlaceholder;
-}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs
index c055d2753e..6f202daca8 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs
@@ -17,17 +17,14 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
internal sealed class OpenApiEndpointConvention : IActionModelConvention
{
private readonly IControllerResourceMapping _controllerResourceMapping;
- private readonly EndpointResolver _endpointResolver;
private readonly IJsonApiOptions _options;
- public OpenApiEndpointConvention(IControllerResourceMapping controllerResourceMapping, EndpointResolver endpointResolver, IJsonApiOptions options)
+ public OpenApiEndpointConvention(IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options)
{
ArgumentGuard.NotNull(controllerResourceMapping);
- ArgumentGuard.NotNull(endpointResolver);
ArgumentGuard.NotNull(options);
_controllerResourceMapping = controllerResourceMapping;
- _endpointResolver = endpointResolver;
_options = options;
}
@@ -35,9 +32,9 @@ public void Apply(ActionModel action)
{
ArgumentGuard.NotNull(action);
- JsonApiEndpoint? endpoint = _endpointResolver.Get(action.ActionMethod);
+ JsonApiEndpointWrapper endpoint = JsonApiEndpointWrapper.FromActionModel(action);
- if (endpoint == null)
+ if (endpoint.IsUnknown)
{
// Not a JSON:API controller, or a non-standard action method in a JSON:API controller.
// None of these are yet implemented, so hide them to avoid downstream crashes.
@@ -47,36 +44,36 @@ public void Apply(ActionModel action)
ResourceType? resourceType = _controllerResourceMapping.GetResourceTypeForController(action.Controller.ControllerType);
- if (ShouldSuppressEndpoint(endpoint.Value, resourceType))
+ if (ShouldSuppressEndpoint(endpoint, resourceType))
{
action.ApiExplorer.IsVisible = false;
return;
}
- SetResponseMetadata(action, endpoint.Value, resourceType);
- SetRequestMetadata(action, endpoint.Value);
+ SetResponseMetadata(action, endpoint, resourceType);
+ SetRequestMetadata(action, endpoint);
}
- private bool ShouldSuppressEndpoint(JsonApiEndpoint endpoint, ResourceType? resourceType)
+ private bool ShouldSuppressEndpoint(JsonApiEndpointWrapper endpoint, ResourceType? resourceType)
{
if (resourceType == null)
{
return false;
}
- if (!IsEndpointAvailable(endpoint, resourceType))
+ if (!IsEndpointAvailable(endpoint.Value, resourceType))
{
return true;
}
- if (IsSecondaryOrRelationshipEndpoint(endpoint))
+ if (IsSecondaryOrRelationshipEndpoint(endpoint.Value))
{
if (resourceType.Relationships.Count == 0)
{
return true;
}
- if (endpoint is JsonApiEndpoint.DeleteRelationship or JsonApiEndpoint.PostRelationship)
+ if (endpoint.Value is JsonApiEndpoints.DeleteRelationship or JsonApiEndpoints.PostRelationship)
{
return !resourceType.Relationships.OfType().Any();
}
@@ -85,7 +82,7 @@ private bool ShouldSuppressEndpoint(JsonApiEndpoint endpoint, ResourceType? reso
return false;
}
- private static bool IsEndpointAvailable(JsonApiEndpoint endpoint, ResourceType resourceType)
+ private static bool IsEndpointAvailable(JsonApiEndpoints endpoint, ResourceType resourceType)
{
JsonApiEndpoints availableEndpoints = GetGeneratedControllerEndpoints(resourceType);
@@ -101,16 +98,16 @@ private static bool IsEndpointAvailable(JsonApiEndpoint endpoint, ResourceType r
// Otherwise, it is considered to be an action method that throws because the endpoint is unavailable.
return endpoint switch
{
- JsonApiEndpoint.GetCollection => availableEndpoints.HasFlag(JsonApiEndpoints.GetCollection),
- JsonApiEndpoint.GetSingle => availableEndpoints.HasFlag(JsonApiEndpoints.GetSingle),
- JsonApiEndpoint.GetSecondary => availableEndpoints.HasFlag(JsonApiEndpoints.GetSecondary),
- JsonApiEndpoint.GetRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.GetRelationship),
- JsonApiEndpoint.PostResource => availableEndpoints.HasFlag(JsonApiEndpoints.Post),
- JsonApiEndpoint.PostRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.PostRelationship),
- JsonApiEndpoint.PatchResource => availableEndpoints.HasFlag(JsonApiEndpoints.Patch),
- JsonApiEndpoint.PatchRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.PatchRelationship),
- JsonApiEndpoint.DeleteResource => availableEndpoints.HasFlag(JsonApiEndpoints.Delete),
- JsonApiEndpoint.DeleteRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.DeleteRelationship),
+ JsonApiEndpoints.GetCollection => availableEndpoints.HasFlag(JsonApiEndpoints.GetCollection),
+ JsonApiEndpoints.GetSingle => availableEndpoints.HasFlag(JsonApiEndpoints.GetSingle),
+ JsonApiEndpoints.GetSecondary => availableEndpoints.HasFlag(JsonApiEndpoints.GetSecondary),
+ JsonApiEndpoints.GetRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.GetRelationship),
+ JsonApiEndpoints.Post => availableEndpoints.HasFlag(JsonApiEndpoints.Post),
+ JsonApiEndpoints.PostRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.PostRelationship),
+ JsonApiEndpoints.Patch => availableEndpoints.HasFlag(JsonApiEndpoints.Patch),
+ JsonApiEndpoints.PatchRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.PatchRelationship),
+ JsonApiEndpoints.Delete => availableEndpoints.HasFlag(JsonApiEndpoints.Delete),
+ JsonApiEndpoints.DeleteRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.DeleteRelationship),
_ => throw new UnreachableCodeException()
};
}
@@ -121,13 +118,13 @@ private static JsonApiEndpoints GetGeneratedControllerEndpoints(ResourceType res
return resourceAttribute?.GenerateControllerEndpoints ?? JsonApiEndpoints.None;
}
- private static bool IsSecondaryOrRelationshipEndpoint(JsonApiEndpoint endpoint)
+ private static bool IsSecondaryOrRelationshipEndpoint(JsonApiEndpoints endpoint)
{
- return endpoint is JsonApiEndpoint.GetSecondary or JsonApiEndpoint.GetRelationship or JsonApiEndpoint.PostRelationship or
- JsonApiEndpoint.PatchRelationship or JsonApiEndpoint.DeleteRelationship;
+ return endpoint is JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship or JsonApiEndpoints.PostRelationship or
+ JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship;
}
- private void SetResponseMetadata(ActionModel action, JsonApiEndpoint endpoint, ResourceType? resourceType)
+ private void SetResponseMetadata(ActionModel action, JsonApiEndpointWrapper endpoint, ResourceType? resourceType)
{
JsonApiMediaType mediaType = GetMediaTypeForEndpoint(endpoint);
action.Filters.Add(new ProducesAttribute(mediaType.ToString()));
@@ -144,57 +141,73 @@ private void SetResponseMetadata(ActionModel action, JsonApiEndpoint endpoint, R
}
}
- private JsonApiMediaType GetMediaTypeForEndpoint(JsonApiEndpoint endpoint)
+ private JsonApiMediaType GetMediaTypeForEndpoint(JsonApiEndpointWrapper endpoint)
{
- return endpoint == JsonApiEndpoint.PostOperations ? JsonApiMediaType.RelaxedAtomicOperations : JsonApiMediaType.Default;
+ return endpoint.IsAtomicOperationsEndpoint ? JsonApiMediaType.RelaxedAtomicOperations : JsonApiMediaType.Default;
}
- private static HttpStatusCode[] GetSuccessStatusCodesForEndpoint(JsonApiEndpoint endpoint)
+ private static HttpStatusCode[] GetSuccessStatusCodesForEndpoint(JsonApiEndpointWrapper endpoint)
{
- return endpoint switch
+ if (endpoint.IsAtomicOperationsEndpoint)
{
- JsonApiEndpoint.GetCollection or JsonApiEndpoint.GetSingle or JsonApiEndpoint.GetSecondary or JsonApiEndpoint.GetRelationship =>
+ return
+ [
+ HttpStatusCode.OK,
+ HttpStatusCode.NoContent
+ ];
+ }
+
+ return endpoint.Value switch
+ {
+ JsonApiEndpoints.GetCollection or JsonApiEndpoints.GetSingle or JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship =>
[
HttpStatusCode.OK,
HttpStatusCode.NotModified
],
- JsonApiEndpoint.PostResource =>
+ JsonApiEndpoints.Post =>
[
HttpStatusCode.Created,
HttpStatusCode.NoContent
],
- JsonApiEndpoint.PatchResource =>
+ JsonApiEndpoints.Patch =>
[
HttpStatusCode.OK,
HttpStatusCode.NoContent
],
- JsonApiEndpoint.DeleteResource or JsonApiEndpoint.PostRelationship or JsonApiEndpoint.PatchRelationship or JsonApiEndpoint.DeleteRelationship =>
+ JsonApiEndpoints.Delete or JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship =>
[
HttpStatusCode.NoContent
],
- JsonApiEndpoint.PostOperations =>
- [
- HttpStatusCode.OK,
- HttpStatusCode.NoContent
- ],
_ => throw new UnreachableCodeException()
};
}
- private HttpStatusCode[] GetErrorStatusCodesForEndpoint(JsonApiEndpoint endpoint, ResourceType? resourceType)
+ private HttpStatusCode[] GetErrorStatusCodesForEndpoint(JsonApiEndpointWrapper endpoint, ResourceType? resourceType)
{
+ if (endpoint.IsAtomicOperationsEndpoint)
+ {
+ return
+ [
+ HttpStatusCode.BadRequest,
+ HttpStatusCode.Forbidden,
+ HttpStatusCode.NotFound,
+ HttpStatusCode.Conflict,
+ HttpStatusCode.UnprocessableEntity
+ ];
+ }
+
// Condition doesn't apply to atomic operations, because Forbidden is also used when an operation is not accessible.
ClientIdGenerationMode clientIdGeneration = resourceType?.ClientIdGeneration ?? _options.ClientIdGeneration;
- return endpoint switch
+ return endpoint.Value switch
{
- JsonApiEndpoint.GetCollection => [HttpStatusCode.BadRequest],
- JsonApiEndpoint.GetSingle or JsonApiEndpoint.GetSecondary or JsonApiEndpoint.GetRelationship =>
+ JsonApiEndpoints.GetCollection => [HttpStatusCode.BadRequest],
+ JsonApiEndpoints.GetSingle or JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship =>
[
HttpStatusCode.BadRequest,
HttpStatusCode.NotFound
],
- JsonApiEndpoint.PostResource when clientIdGeneration == ClientIdGenerationMode.Forbidden =>
+ JsonApiEndpoints.Post when clientIdGeneration == ClientIdGenerationMode.Forbidden =>
[
HttpStatusCode.BadRequest,
HttpStatusCode.Forbidden,
@@ -202,40 +215,32 @@ private HttpStatusCode[] GetErrorStatusCodesForEndpoint(JsonApiEndpoint endpoint
HttpStatusCode.Conflict,
HttpStatusCode.UnprocessableEntity
],
- JsonApiEndpoint.PostResource =>
+ JsonApiEndpoints.Post =>
[
HttpStatusCode.BadRequest,
HttpStatusCode.NotFound,
HttpStatusCode.Conflict,
HttpStatusCode.UnprocessableEntity
],
- JsonApiEndpoint.PatchResource =>
+ JsonApiEndpoints.Patch =>
[
HttpStatusCode.BadRequest,
HttpStatusCode.NotFound,
HttpStatusCode.Conflict,
HttpStatusCode.UnprocessableEntity
],
- JsonApiEndpoint.DeleteResource => [HttpStatusCode.NotFound],
- JsonApiEndpoint.PostRelationship or JsonApiEndpoint.PatchRelationship or JsonApiEndpoint.DeleteRelationship =>
+ JsonApiEndpoints.Delete => [HttpStatusCode.NotFound],
+ JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship =>
[
HttpStatusCode.BadRequest,
HttpStatusCode.NotFound,
HttpStatusCode.Conflict
],
- JsonApiEndpoint.PostOperations =>
- [
- HttpStatusCode.BadRequest,
- HttpStatusCode.Forbidden,
- HttpStatusCode.NotFound,
- HttpStatusCode.Conflict,
- HttpStatusCode.UnprocessableEntity
- ],
_ => throw new UnreachableCodeException()
};
}
- private void SetRequestMetadata(ActionModel action, JsonApiEndpoint endpoint)
+ private void SetRequestMetadata(ActionModel action, JsonApiEndpointWrapper endpoint)
{
if (RequiresRequestBody(endpoint))
{
@@ -244,9 +249,35 @@ private void SetRequestMetadata(ActionModel action, JsonApiEndpoint endpoint)
}
}
- private static bool RequiresRequestBody(JsonApiEndpoint endpoint)
+ private static bool RequiresRequestBody(JsonApiEndpointWrapper endpoint)
+ {
+ return endpoint.IsAtomicOperationsEndpoint || endpoint.Value is JsonApiEndpoints.Post or JsonApiEndpoints.Patch or JsonApiEndpoints.PostRelationship or
+ JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship;
+ }
+
+ private sealed class JsonApiEndpointWrapper
{
- return endpoint is JsonApiEndpoint.PostResource or JsonApiEndpoint.PatchResource or JsonApiEndpoint.PostRelationship or
- JsonApiEndpoint.PatchRelationship or JsonApiEndpoint.DeleteRelationship or JsonApiEndpoint.PostOperations;
+ private static readonly JsonApiEndpointWrapper AtomicOperations = new(true, JsonApiEndpoints.None);
+
+ public bool IsAtomicOperationsEndpoint { get; }
+ public JsonApiEndpoints Value { get; }
+ public bool IsUnknown => !IsAtomicOperationsEndpoint && Value == JsonApiEndpoints.None;
+
+ private JsonApiEndpointWrapper(bool isAtomicOperationsEndpoint, JsonApiEndpoints value)
+ {
+ IsAtomicOperationsEndpoint = isAtomicOperationsEndpoint;
+ Value = value;
+ }
+
+ public static JsonApiEndpointWrapper FromActionModel(ActionModel actionModel)
+ {
+ if (EndpointResolver.Instance.IsAtomicOperationsController(actionModel.ActionMethod))
+ {
+ return AtomicOperations;
+ }
+
+ JsonApiEndpoints endpoint = EndpointResolver.Instance.GetEndpoint(actionModel.ActionMethod);
+ return new JsonApiEndpointWrapper(false, endpoint);
+ }
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ServiceCollectionExtensions.cs
index f7c26217d1..e6507042f6 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ServiceCollectionExtensions.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ServiceCollectionExtensions.cs
@@ -36,7 +36,6 @@ private static void AddCustomApiExplorer(IServiceCollection services)
{
services.TryAddSingleton();
services.TryAddSingleton();
- services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddSingleton();
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/EndpointOrderingFilter.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/EndpointOrderingFilter.cs
index d5a5398cb4..48fdcfdbc8 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/EndpointOrderingFilter.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/EndpointOrderingFilter.cs
@@ -12,7 +12,7 @@ internal sealed class EndpointOrderingFilter : IDocumentFilter
internal sealed partial class EndpointOrderingFilter : IDocumentFilter
#endif
{
- private const string PatternText = $@".*{JsonApiRoutingTemplate.PrimaryEndpoint}/(?>{JsonApiRoutingTemplate.RelationshipsPart}\/)?(?\w+)";
+ private const string PatternText = @".*{id}/(?>relationships\/)?(?\w+)";
#if NET6_0
private const RegexOptions RegexOptionsNet60 = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture;
@@ -42,7 +42,7 @@ private static string GetPrimaryResourcePublicName(KeyValuePair entry)
{
- return entry.Key.Contains($"/{JsonApiRoutingTemplate.RelationshipsPart}");
+ return entry.Key.Contains("/relationships");
}
private static string GetRelationshipName(KeyValuePair entry)