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/3] 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/3] 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/3] 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()
{