diff --git a/src/Abstractions/Entities/TaskEntity.cs b/src/Abstractions/Entities/TaskEntity.cs index bad549fa..c3c33d85 100644 --- a/src/Abstractions/Entities/TaskEntity.cs +++ b/src/Abstractions/Entities/TaskEntity.cs @@ -72,6 +72,14 @@ public interface ITaskEntity /// Entity state will be hydrated into the property. See for /// more information. /// +/// +/// Implicit Operations +/// +/// This class supports some operations implicitly. Implicit operations have the lowest priority, after entity and state +/// method dispatching. To override an implicit operation, implement a public method of the same name. Throw +/// from the method to indicate the implicit operation is not supported at all. +/// +/// delete: deletes the entity state from storage. /// public abstract class TaskEntity : ITaskEntity { @@ -120,6 +128,12 @@ public abstract class TaskEntity : ITaskEntity if (!operation.TryDispatch(this, out object? result, out Type returnType) && !this.TryDispatchState(operation, out result, out returnType)) { + if (TryDispatchImplicit(operation, out ValueTask task)) + { + // We do not go into UnwrapAsync for implicit tasks + return task; + } + throw new NotSupportedException($"No suitable method found for entity operation '{operation}'."); } @@ -144,6 +158,20 @@ protected virtual TState InitializeState() return Activator.CreateInstance(); } + static bool TryDispatchImplicit(TaskEntityOperation operation, out ValueTask result) + { + // We do not implement implicit operations via methods because then they would supersede state-dispatching. + // As such, implicit operations are manually implemented here. + result = default; + if (string.Equals(operation.Name, "delete", StringComparison.OrdinalIgnoreCase)) + { + operation.State.SetState(null); + return true; + } + + return false; + } + bool TryDispatchState(TaskEntityOperation operation, out object? result, out Type returnType) { if (!this.AllowStateDispatch) diff --git a/test/Abstractions.Tests/Entities/EntityTaskEntityTests.cs b/test/Abstractions.Tests/Entities/EntityTaskEntityTests.cs index 2f37b03d..4ca908b4 100644 --- a/test/Abstractions.Tests/Entities/EntityTaskEntityTests.cs +++ b/test/Abstractions.Tests/Entities/EntityTaskEntityTests.cs @@ -125,6 +125,34 @@ public async Task DefaultValue_Input_Succeeds() result.Should().BeOfType().Which.Should().Be("not-default"); } + [Theory] + [InlineData("delete")] + [InlineData("Delete")] + public async Task ImplicitDelete_ClearsState(string op) + { + TestEntityOperation operation = new(op, 10, default); + TestEntity entity = new(); + + object? result = await entity.RunAsync(operation); + + result.Should().BeNull(); + operation.State.GetState(typeof(object)).Should().BeNull(); + } + + [Theory] + [InlineData("delete")] + [InlineData("Delete")] + public async Task ExplicitDelete_Overridden(string op) + { + TestEntityOperation operation = new(op, 10, default); + DeleteEntity entity = new(); + + object? result = await entity.RunAsync(operation); + + result.Should().BeNull(); + operation.State.GetState(typeof(int)).Should().Be(0); + } + #pragma warning disable CA1822 // Mark members as static #pragma warning disable IDE0060 // Remove unused parameter class TestEntity : TaskEntity @@ -215,6 +243,11 @@ int Get(Optional context) return this.State; } } + + class DeleteEntity : TaskEntity + { + public void Delete() => this.State = 0; + } #pragma warning restore IDE0060 // Remove unused parameter #pragma warning restore CA1822 // Mark members as static } diff --git a/test/Abstractions.Tests/Entities/StateTaskEntityTests.cs b/test/Abstractions.Tests/Entities/StateTaskEntityTests.cs index 2250c925..e778ff20 100644 --- a/test/Abstractions.Tests/Entities/StateTaskEntityTests.cs +++ b/test/Abstractions.Tests/Entities/StateTaskEntityTests.cs @@ -158,6 +158,20 @@ public async Task DefaultValue_Input_Succeeds() result.Should().BeOfType().Which.Should().Be("not-default"); } + [Theory] + [InlineData("delete")] + [InlineData("Delete")] + public async Task ExplicitDelete_Overridden(string op) + { + TestEntityOperation operation = new(op, State(10), default); + TestEntity entity = new(); + + object? result = await entity.RunAsync(operation); + + result.Should().BeNull(); + operation.State.GetState(typeof(TestState)).Should().BeOfType().Which.Value.Should().Be(0); + } + static TestState State(int value) => new() { Value = value }; class NullStateEntity : TestEntity @@ -187,6 +201,8 @@ class TestState public static string StaticMethod() => throw new NotImplementedException(); + public void Delete() => this.Value = 0; + public int Precedence() => 10; public int Add0(int value) => this.Add(value, default);