Skip to content

Commit 43c1a4b

Browse files
authored
Merge pull request #1628 from json-api-dotnet/fix-operations-inheritance
Fix resource inheritance with atomic operations
2 parents 661afb3 + aa763bf commit 43c1a4b

File tree

7 files changed

+229
-9
lines changed

7 files changed

+229
-9
lines changed

src/JsonApiDotNetCore/AtomicOperations/DefaultOperationFilter.cs

+27-3
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,37 @@
77
namespace JsonApiDotNetCore.AtomicOperations;
88

99
/// <inheritdoc />
10-
internal sealed class DefaultOperationFilter : IAtomicOperationFilter
10+
public class DefaultOperationFilter : IAtomicOperationFilter
1111
{
1212
/// <inheritdoc />
13-
public bool IsEnabled(ResourceType resourceType, WriteOperationKind writeOperation)
13+
public virtual bool IsEnabled(ResourceType resourceType, WriteOperationKind writeOperation)
1414
{
15+
ArgumentGuard.NotNull(resourceType);
16+
17+
// To match the behavior of non-operations controllers:
18+
// If an operation is enabled on a base type, it is implicitly enabled on all derived types.
19+
ResourceType currentResourceType = resourceType;
20+
21+
while (true)
22+
{
23+
JsonApiEndpoints? endpoints = GetJsonApiEndpoints(currentResourceType);
24+
bool isEnabled = endpoints != null && Contains(endpoints.Value, writeOperation);
25+
26+
if (isEnabled || currentResourceType.BaseType == null)
27+
{
28+
return isEnabled;
29+
}
30+
31+
currentResourceType = currentResourceType.BaseType;
32+
}
33+
}
34+
35+
protected virtual JsonApiEndpoints? GetJsonApiEndpoints(ResourceType resourceType)
36+
{
37+
ArgumentGuard.NotNull(resourceType);
38+
1539
var resourceAttribute = resourceType.ClrType.GetCustomAttribute<ResourceAttribute>();
16-
return resourceAttribute != null && Contains(resourceAttribute.GenerateControllerEndpoints, writeOperation);
40+
return resourceAttribute?.GenerateControllerEndpoints;
1741
}
1842

1943
private static bool Contains(JsonApiEndpoints endpoints, WriteOperationKind writeOperation)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System.Net;
2+
using FluentAssertions;
3+
using JsonApiDotNetCore.Serialization.Objects;
4+
using JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
5+
using JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.TablePerHierarchy;
6+
using TestBuildingBlocks;
7+
using Xunit;
8+
9+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance;
10+
11+
public sealed class AtomicOperationTests : IClassFixture<IntegrationTestContext<TestableStartup<TablePerHierarchyDbContext>, TablePerHierarchyDbContext>>
12+
{
13+
private readonly IntegrationTestContext<TestableStartup<TablePerHierarchyDbContext>, TablePerHierarchyDbContext> _testContext;
14+
private readonly ResourceInheritanceFakers _fakers = new();
15+
16+
public AtomicOperationTests(IntegrationTestContext<TestableStartup<TablePerHierarchyDbContext>, TablePerHierarchyDbContext> testContext)
17+
{
18+
_testContext = testContext;
19+
20+
testContext.UseController<OperationsController>();
21+
}
22+
23+
[Fact]
24+
public async Task When_operation_is_enabled_on_base_type_it_is_implicitly_enabled_on_derived_types()
25+
{
26+
// Arrange
27+
AlwaysMovingTandem newMovingTandem = _fakers.AlwaysMovingTandem.GenerateOne();
28+
29+
var requestBody = new
30+
{
31+
atomic__operations = new[]
32+
{
33+
new
34+
{
35+
op = "add",
36+
data = new
37+
{
38+
type = "alwaysMovingTandems",
39+
attributes = new
40+
{
41+
weight = newMovingTandem.Weight,
42+
requiresDriverLicense = newMovingTandem.RequiresDriverLicense,
43+
gearCount = newMovingTandem.GearCount
44+
}
45+
}
46+
}
47+
}
48+
};
49+
50+
const string route = "/operations";
51+
52+
// Act
53+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
54+
55+
// Assert
56+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);
57+
58+
responseDocument.Results.ShouldHaveCount(1);
59+
60+
responseDocument.Results[0].Data.SingleValue.ShouldNotBeNull().With(resource =>
61+
{
62+
resource.Type.Should().Be("alwaysMovingTandems");
63+
resource.Attributes.ShouldContainKey("weight").With(value => value.Should().Be(newMovingTandem.Weight));
64+
resource.Attributes.ShouldContainKey("requiresDriverLicense").With(value => value.Should().Be(newMovingTandem.RequiresDriverLicense));
65+
resource.Attributes.ShouldContainKey("gearCount").With(value => value.Should().Be(newMovingTandem.GearCount));
66+
resource.Relationships.Should().BeNull();
67+
});
68+
69+
long newMovingTandemId = long.Parse(responseDocument.Results[0].Data.SingleValue!.Id.ShouldNotBeNull());
70+
71+
await _testContext.RunOnDatabaseAsync(async dbContext =>
72+
{
73+
AlwaysMovingTandem movingTandemInDatabase = await dbContext.AlwaysMovingTandems.FirstWithIdAsync(newMovingTandemId);
74+
75+
movingTandemInDatabase.Weight.Should().Be(newMovingTandem.Weight);
76+
movingTandemInDatabase.RequiresDriverLicense.Should().Be(newMovingTandem.RequiresDriverLicense);
77+
movingTandemInDatabase.GearCount.Should().Be(newMovingTandem.GearCount);
78+
});
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
using JetBrains.Annotations;
2-
using JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
32
using Microsoft.EntityFrameworkCore;
43

54
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.ChangeTracking;
65

76
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
87
public sealed class ChangeTrackingDbContext(DbContextOptions<ChangeTrackingDbContext> options)
9-
: ResourceInheritanceDbContext(options)
10-
{
11-
public DbSet<AlwaysMovingTandem> AlwaysMovingTandems => Set<AlwaysMovingTandem>();
12-
}
8+
: ResourceInheritanceDbContext(options);

test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/Models/AlwaysMovingTandem.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
using System.ComponentModel.DataAnnotations.Schema;
22
using JetBrains.Annotations;
3+
using JsonApiDotNetCore.Controllers;
34
using JsonApiDotNetCore.Resources.Annotations;
45

56
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
67

78
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8-
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
9+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance", GenerateControllerEndpoints = JsonApiEndpoints.None)]
910
public sealed class AlwaysMovingTandem : Bike
1011
{
1112
[NotMapped]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using JsonApiDotNetCore.AtomicOperations;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Controllers;
4+
using JsonApiDotNetCore.Middleware;
5+
using JsonApiDotNetCore.Resources;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance;
9+
10+
public sealed class OperationsController(
11+
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request,
12+
ITargetedFields targetedFields, IAtomicOperationFilter operationFilter)
13+
: JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields, operationFilter);

test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/ResourceInheritanceDbContext.cs

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public abstract class ResourceInheritanceDbContext(DbContextOptions options)
1212
public DbSet<Vehicle> Vehicles => Set<Vehicle>();
1313
public DbSet<Bike> Bikes => Set<Bike>();
1414
public DbSet<Tandem> Tandems => Set<Tandem>();
15+
public DbSet<AlwaysMovingTandem> AlwaysMovingTandems => Set<AlwaysMovingTandem>();
1516
public DbSet<MotorVehicle> MotorVehicles => Set<MotorVehicle>();
1617
public DbSet<Car> Cars => Set<Car>();
1718
public DbSet<Truck> Trucks => Set<Truck>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using FluentAssertions;
2+
using JetBrains.Annotations;
3+
using JsonApiDotNetCore;
4+
using JsonApiDotNetCore.AtomicOperations;
5+
using JsonApiDotNetCore.Configuration;
6+
using JsonApiDotNetCore.Controllers;
7+
using JsonApiDotNetCore.Middleware;
8+
using JsonApiDotNetCore.Resources;
9+
using Microsoft.Extensions.Logging.Abstractions;
10+
using Xunit;
11+
12+
namespace JsonApiDotNetCoreTests.UnitTests.Controllers;
13+
14+
public sealed class DefaultOperationFilterTests
15+
{
16+
// @formatter:wrap_chained_method_calls chop_always
17+
// @formatter:wrap_before_first_method_call true
18+
19+
private static readonly IResourceGraph ResourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance)
20+
.Add<AbstractBaseType, long>()
21+
.Add<ConcreteBaseType, long>()
22+
.Add<ConcreteDerivedType, long>()
23+
.Build();
24+
25+
// @formatter:wrap_before_first_method_call restore
26+
// @formatter:wrap_chained_method_calls restore
27+
28+
[Theory]
29+
[InlineData(WriteOperationKind.CreateResource)]
30+
[InlineData(WriteOperationKind.UpdateResource)]
31+
[InlineData(WriteOperationKind.DeleteResource)]
32+
[InlineData(WriteOperationKind.SetRelationship)]
33+
[InlineData(WriteOperationKind.AddToRelationship)]
34+
[InlineData(WriteOperationKind.RemoveFromRelationship)]
35+
public void Operations_enabled_on_abstract_base_type_are_implicitly_enabled_on_derived_types(WriteOperationKind writeOperation)
36+
{
37+
// Arrange
38+
ResourceType abstractBaseType = ResourceGraph.GetResourceType<AbstractBaseType>();
39+
ResourceType concreteBaseType = ResourceGraph.GetResourceType<ConcreteBaseType>();
40+
ResourceType concreteDerivedType = ResourceGraph.GetResourceType<ConcreteDerivedType>();
41+
42+
var filter = new FakeOperationFilter(resourceType => resourceType.Equals(abstractBaseType));
43+
44+
// Act
45+
bool abstractBaseIsEnabled = filter.IsEnabled(abstractBaseType, writeOperation);
46+
bool concreteBaseIsEnabled = filter.IsEnabled(concreteBaseType, writeOperation);
47+
bool concreteDerivedIsEnabled = filter.IsEnabled(concreteDerivedType, writeOperation);
48+
49+
// Assert
50+
abstractBaseIsEnabled.Should().BeTrue();
51+
concreteBaseIsEnabled.Should().BeTrue();
52+
concreteDerivedIsEnabled.Should().BeTrue();
53+
}
54+
55+
[Theory]
56+
[InlineData(WriteOperationKind.CreateResource)]
57+
[InlineData(WriteOperationKind.UpdateResource)]
58+
[InlineData(WriteOperationKind.DeleteResource)]
59+
[InlineData(WriteOperationKind.SetRelationship)]
60+
[InlineData(WriteOperationKind.AddToRelationship)]
61+
[InlineData(WriteOperationKind.RemoveFromRelationship)]
62+
public void Operations_enabled_on_concrete_base_type_are_implicitly_enabled_on_derived_types(WriteOperationKind writeOperation)
63+
{
64+
// Arrange
65+
ResourceType abstractBaseType = ResourceGraph.GetResourceType<AbstractBaseType>();
66+
ResourceType concreteBaseType = ResourceGraph.GetResourceType<ConcreteBaseType>();
67+
ResourceType concreteDerivedType = ResourceGraph.GetResourceType<ConcreteDerivedType>();
68+
69+
var filter = new FakeOperationFilter(resourceType => resourceType.Equals(concreteBaseType));
70+
71+
// Act
72+
bool abstractBaseIsEnabled = filter.IsEnabled(abstractBaseType, writeOperation);
73+
bool concreteBaseIsEnabled = filter.IsEnabled(concreteBaseType, writeOperation);
74+
bool concreteDerivedIsEnabled = filter.IsEnabled(concreteDerivedType, writeOperation);
75+
76+
// Assert
77+
abstractBaseIsEnabled.Should().BeFalse();
78+
concreteBaseIsEnabled.Should().BeTrue();
79+
concreteDerivedIsEnabled.Should().BeTrue();
80+
}
81+
82+
private sealed class FakeOperationFilter : DefaultOperationFilter
83+
{
84+
private readonly Func<ResourceType, bool> _isResourceTypeEnabled;
85+
86+
public FakeOperationFilter(Func<ResourceType, bool> isResourceTypeEnabled)
87+
{
88+
ArgumentGuard.NotNull(isResourceTypeEnabled);
89+
90+
_isResourceTypeEnabled = isResourceTypeEnabled;
91+
}
92+
93+
protected override JsonApiEndpoints? GetJsonApiEndpoints(ResourceType resourceType)
94+
{
95+
return _isResourceTypeEnabled(resourceType) ? JsonApiEndpoints.All : JsonApiEndpoints.None;
96+
}
97+
}
98+
99+
private abstract class AbstractBaseType : Identifiable<long>;
100+
101+
private class ConcreteBaseType : AbstractBaseType;
102+
103+
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
104+
private sealed class ConcreteDerivedType : ConcreteBaseType;
105+
}

0 commit comments

Comments
 (0)