Skip to content

Commit 45c746a

Browse files
committed
Refactor. Enabled Idempotency on Sync handlers. Refactor unit tests with Theory for both async and sync handlers.
1 parent 983234f commit 45c746a

File tree

5 files changed

+116
-35
lines changed

5 files changed

+116
-35
lines changed

libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,27 @@ public class IdempotentAttribute : UniversalWrapperAttribute
6969
/// <returns>T.</returns>
7070
protected internal sealed override T WrapSync<T>(Func<object[], T> target, object[] args, AspectEventArgs eventArgs)
7171
{
72-
throw new IdempotencyConfigurationException("Idempotent attribute can be used on async methods only");
72+
if (PowertoolsConfigurations.Instance.IdempotencyDisabled)
73+
{
74+
return base.WrapSync(target, args, eventArgs);
75+
}
76+
var payload = JsonDocument.Parse(JsonSerializer.Serialize(args[0]));
77+
if (payload == null)
78+
{
79+
throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey");
80+
}
81+
82+
Task<T> ResultDelegate() => Task.FromResult(target(args));
83+
84+
var idempotencyHandler = new IdempotencyAspectHandler<T>(ResultDelegate, eventArgs.Method.Name, payload);
85+
if (idempotencyHandler == null)
86+
{
87+
throw new Exception("Failed to create an instance of IdempotencyAspectHandler");
88+
}
89+
var result = idempotencyHandler.Handle().GetAwaiter().GetResult();
90+
return result;
7391
}
74-
92+
7593
/// <summary>
7694
/// Wrap as an asynchronous operation.
7795
/// </summary>
@@ -93,7 +111,9 @@ protected internal sealed override async Task<T> WrapAsync<T>(
93111
throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey");
94112
}
95113

96-
var idempotencyHandler = new IdempotencyAspectHandler<T>(target, args, eventArgs.Method.Name, payload);
114+
Task<T> ResultDelegate() => target(args);
115+
116+
var idempotencyHandler = new IdempotencyAspectHandler<T>(ResultDelegate, eventArgs.Method.Name, payload);
97117
if (idempotencyHandler == null)
98118
{
99119
throw new Exception("Failed to create an instance of IdempotencyAspectHandler");

libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,17 @@ internal class IdempotencyAspectHandler<T>
2626
{
2727
private const int MaxRetries = 2;
2828

29-
private readonly Func<object[], object> _target;
30-
private readonly object[] _args;
29+
private readonly Func<Task<T>> _target;
3130
private readonly JsonDocument _data;
3231
private readonly BasePersistenceStore _persistenceStore;
3332
private readonly ILog _log;
3433

3534
public IdempotencyAspectHandler(
36-
Func<object[], object> target,
37-
object[] args,
35+
Func<Task<T>> target,
3836
string functionName,
3937
JsonDocument payload)
4038
{
4139
_target = target;
42-
_args = args;
4340
_data = payload;
4441
_persistenceStore = Idempotency.Instance.PersistenceStore;
4542
_persistenceStore.Configure(Idempotency.Instance.IdempotencyOptions, functionName);
@@ -182,7 +179,7 @@ private async Task<T> GetFunctionResponse()
182179
T response;
183180
try
184181
{
185-
response = await (Task<T>)_target(_args);
182+
response = await _target();
186183
}
187184
catch(Exception handlerException)
188185
{

libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,50 @@
1919

2020
namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers;
2121

22-
public class IdempotencyEnabledFunction
22+
23+
public interface IIdempotencyEnabledFunction
2324
{
24-
public bool HandlerExecuted;
25+
public bool HandlerExecuted { get; set; }
26+
Task<Basket> HandleTest(Product input, ILambdaContext context);
27+
}
2528

29+
public class IdempotencyEnabledFunction : IIdempotencyEnabledFunction
30+
{
2631
[Idempotent]
27-
public Task<Basket> Handle(Product input, ILambdaContext context)
32+
public async Task<Basket> Handle(Product input, ILambdaContext context)
2833
{
2934
HandlerExecuted = true;
3035
var basket = new Basket();
3136
basket.Add(input);
3237
var result = Task.FromResult(basket);
3338

34-
return result;
39+
return await result;
40+
}
41+
42+
public bool HandlerExecuted { get; set; }
43+
44+
public Task<Basket> HandleTest(Product input, ILambdaContext context)
45+
{
46+
return Handle(input, context);
47+
}
48+
}
49+
50+
public class IdempotencyEnabledSyncFunction : IIdempotencyEnabledFunction
51+
{
52+
[Idempotent]
53+
public Basket Handle(Product input, ILambdaContext context)
54+
{
55+
HandlerExecuted = true;
56+
var basket = new Basket();
57+
basket.Add(input);
58+
59+
return basket;
60+
}
61+
62+
public bool HandlerExecuted { get; set; }
63+
64+
public Task<Basket> HandleTest(Product input, ILambdaContext context)
65+
{
66+
return Task.FromResult(Handle(input, context));
3567
}
3668
}

libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyWithErrorFunction.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,31 @@
2020

2121
namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers;
2222

23-
public class IdempotencyWithErrorFunction
23+
public interface IIdempotencyWithErrorFunction
24+
{
25+
Task<Basket> HandleTest(Product input, ILambdaContext context);
26+
}
27+
28+
public class IdempotencyWithErrorFunction : IIdempotencyWithErrorFunction
2429
{
2530
[Idempotent]
2631
public Task<Basket> Handle(Product input, ILambdaContext context)
2732
=> throw new IndexOutOfRangeException("Fake exception");
33+
34+
public Task<Basket> HandleTest(Product input, ILambdaContext context)
35+
{
36+
return Handle(input, context);
37+
}
38+
}
39+
40+
public class IdempotencyWithErrorSyncFunction : IIdempotencyWithErrorFunction
41+
{
42+
[Idempotent]
43+
public Basket Handle(Product input, ILambdaContext context)
44+
=> throw new IndexOutOfRangeException("Fake exception");
45+
46+
public Task<Basket> HandleTest(Product input, ILambdaContext context)
47+
{
48+
return Task.FromResult(Handle(input, context));
49+
}
2850
}

libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal;
3333
[Collection("Sequential")]
3434
public class IdempotentAspectTests : IDisposable
3535
{
36-
[Fact]
37-
public async Task Handle_WhenFirstCall_ShouldPutInStore()
36+
[Theory]
37+
[InlineData(typeof(IdempotencyEnabledFunction))]
38+
[InlineData(typeof(IdempotencyEnabledSyncFunction))]
39+
public async Task Handle_WhenFirstCall_ShouldPutInStore(Type type)
3840
{
3941
//Arrange
4042
var store = new Mock<BasePersistenceStore>();
@@ -44,11 +46,11 @@ public async Task Handle_WhenFirstCall_ShouldPutInStore()
4446
.WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id"))
4547
);
4648

47-
var function = new IdempotencyEnabledFunction();
49+
var function = Activator.CreateInstance(type) as IIdempotencyEnabledFunction;
4850
var product = new Product(42, "fake product", 12);
4951

5052
//Act
51-
var basket = await function.Handle(product, new TestLambdaContext());
53+
var basket = await function!.HandleTest(product, new TestLambdaContext());
5254

5355
//Assert
5456
basket.Products.Count.Should().Be(1);
@@ -61,8 +63,10 @@ public async Task Handle_WhenFirstCall_ShouldPutInStore()
6163
.Verify(x=>x.SaveSuccess(It.IsAny<JsonDocument>(), It.Is<Basket>(y => y.Equals(basket)), It.IsAny<DateTimeOffset>()));
6264
}
6365

64-
[Fact]
65-
public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore()
66+
[Theory]
67+
[InlineData(typeof(IdempotencyEnabledFunction))]
68+
[InlineData(typeof(IdempotencyEnabledSyncFunction))]
69+
public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore(Type type)
6670
{
6771
//Arrange
6872
var store = new Mock<BasePersistenceStore>();
@@ -87,18 +91,20 @@ public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore()
8791
store.Setup(x=>x.GetRecord(It.IsAny<JsonDocument>(), It.IsAny<DateTimeOffset>()))
8892
.ReturnsAsync(record);
8993

90-
var function = new IdempotencyEnabledFunction();
94+
var function = Activator.CreateInstance(type) as IIdempotencyEnabledFunction;
9195

9296
// Act
93-
var resultBasket = await function.Handle(product, new TestLambdaContext());
97+
var resultBasket = await function!.HandleTest(product, new TestLambdaContext());
9498

9599
// Assert
96100
resultBasket.Should().Be(basket);
97101
function.HandlerExecuted.Should().BeFalse();
98102
}
99103

100-
[Fact]
101-
public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempotencyAlreadyInProgressException()
104+
[Theory]
105+
[InlineData(typeof(IdempotencyEnabledFunction))]
106+
[InlineData(typeof(IdempotencyEnabledSyncFunction))]
107+
public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempotencyAlreadyInProgressException(Type type)
102108
{
103109
// Arrange
104110
var store = new Mock<BasePersistenceStore>();
@@ -123,15 +129,17 @@ public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempoten
123129
.ReturnsAsync(record);
124130

125131
// Act
126-
var function = new IdempotencyEnabledFunction();
127-
Func<Task> act = async () => await function.Handle(product, new TestLambdaContext());
132+
var function = Activator.CreateInstance(type) as IIdempotencyEnabledFunction;
133+
Func<Task> act = async () => await function!.HandleTest(product, new TestLambdaContext());
128134

129135
// Assert
130136
await act.Should().ThrowAsync<IdempotencyAlreadyInProgressException>();
131137
}
132138

133-
[Fact]
134-
public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionException()
139+
[Theory]
140+
[InlineData(typeof(IdempotencyWithErrorFunction))]
141+
[InlineData(typeof(IdempotencyWithErrorSyncFunction))]
142+
public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionException(Type type)
135143
{
136144
// Arrange
137145
var store = new Mock<BasePersistenceStore>();
@@ -142,20 +150,22 @@ public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionE
142150
.WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id"))
143151
);
144152

145-
var function = new IdempotencyWithErrorFunction();
153+
var function = Activator.CreateInstance(type) as IIdempotencyWithErrorFunction;
146154
var product = new Product(42, "fake product", 12);
147155

148156
// Act
149-
Func<Task> act = async () => await function.Handle(product, new TestLambdaContext());
157+
Func<Task> act = async () => await function!.HandleTest(product, new TestLambdaContext());
150158

151159
// Assert
152160
await act.Should().ThrowAsync<IndexOutOfRangeException>();
153161
store.Verify(
154162
x => x.DeleteRecord(It.IsAny<JsonDocument>(), It.IsAny<IndexOutOfRangeException>()));
155163
}
156164

157-
[Fact]
158-
public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction()
165+
[Theory]
166+
[InlineData(typeof(IdempotencyEnabledFunction))]
167+
[InlineData(typeof(IdempotencyEnabledSyncFunction))]
168+
public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction(Type type)
159169
{
160170

161171
// Arrange
@@ -169,18 +179,18 @@ public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction()
169179
.WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id"))
170180
);
171181

172-
var function = new IdempotencyEnabledFunction();
182+
var function = Activator.CreateInstance(type) as IIdempotencyEnabledFunction;
173183
var product = new Product(42, "fake product", 12);
174184

175185
// Act
176-
var basket = await function.Handle(product, new TestLambdaContext());
186+
var basket = await function!.HandleTest(product, new TestLambdaContext());
177187

178188
// Assert
179189
store.Invocations.Count.Should().Be(0);
180190
basket.Products.Count.Should().Be(1);
181191
function.HandlerExecuted.Should().BeTrue();
182192
}
183-
193+
184194
[Fact]
185195
public void Idempotency_Set_Execution_Environment_Context()
186196
{

0 commit comments

Comments
 (0)