Skip to content

Commit 55f84fe

Browse files
author
Hossam Barakat
committed
Support Idempotent attribute on any method
1 parent 10505e4 commit 55f84fe

File tree

9 files changed

+433
-3
lines changed

9 files changed

+433
-3
lines changed

docs/utilities/idempotency.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,38 @@ You can quickly start by configuring `Idempotency` and using it with the `Idempo
122122
}
123123
```
124124

125+
#### Idempotent attribute on another method
126+
127+
You can use the `Idempotent` attribute for any .NET function, not only the Lambda handlers.
128+
129+
When using `Idempotent` attribute on another method, you must tell which parameter in the method signature has the data we should use:
130+
131+
- If the method only has one parameter, it will be used by default.
132+
- If there are 2 or more parameters, you must set the `IdempotencyKey` attribute on the parameter to use.
133+
134+
!!! info "The parameter must be serializable in JSON. We use `System.Text.Json` internally to (de)serialize objects"
135+
136+
```csharp
137+
public class Function
138+
{
139+
public Function()
140+
{
141+
Idempotency.Configure(builder => builder.UseDynamoDb("idempotency_table"));
142+
}
143+
144+
public Task<string> FunctionHandler(string input, ILambdaContext context)
145+
{
146+
dummpy("hello", "world")
147+
return Task.FromResult(input.ToUpper());
148+
}
149+
150+
[Idempotent]
151+
private string dummy(string argOne, [IdempotencyKey] string argTwo) {
152+
return "something";
153+
}
154+
}
155+
```
156+
125157
### Choosing a payload subset for idempotency
126158

127159
!!! tip "Tip: Dealing with always changing payloads"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using System;
17+
18+
namespace AWS.Lambda.Powertools.Idempotency;
19+
20+
/// <summary>
21+
/// IdempotencyKey is used to signal that a method parameter is used as a key for idempotency.
22+
/// Must be used in conjunction with the Idempotency attribute.
23+
///
24+
/// Example:
25+
///
26+
/// [Idempotent]
27+
/// private Basket SubMethod([IdempotencyKey]string magicProduct, Product p) { ... }
28+
/// Note: This annotation is not needed when the method only has one parameter.
29+
/// </summary>
30+
[AttributeUsage(AttributeTargets.Parameter)]
31+
public class IdempotencyKeyAttribute: Attribute
32+
{
33+
34+
}

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515

1616
using System;
1717
using System.Linq;
18+
using System.Reflection;
1819
using System.Text.Json;
1920
using System.Threading.Tasks;
21+
using Amazon.Lambda.Core;
2022
using AspectInjector.Broker;
2123
using AWS.Lambda.Powertools.Common;
2224
using AWS.Lambda.Powertools.Idempotency.Exceptions;
@@ -74,8 +76,12 @@ protected internal sealed override T WrapSync<T>(Func<object[], T> target, objec
7476
{
7577
return base.WrapSync(target, args, eventArgs);
7678
}
77-
78-
var payload = args is not null && args.Any() ? JsonDocument.Parse(JsonSerializer.Serialize(args[0])) : null;
79+
if (eventArgs.ReturnType == typeof(void))
80+
{
81+
throw new IdempotencyConfigurationException("The annotated method doesn't return anything. Unable to perform idempotency on void return type");
82+
}
83+
84+
var payload = GetPayload<T>(eventArgs);
7985
if (payload == null)
8086
{
8187
throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey");
@@ -107,8 +113,13 @@ protected internal sealed override async Task<T> WrapAsync<T>(
107113
{
108114
return await base.WrapAsync(target, args, eventArgs);
109115
}
116+
117+
if (eventArgs.ReturnType == typeof(void))
118+
{
119+
throw new IdempotencyConfigurationException("The annotated method doesn't return anything. Unable to perform idempotency on void return type");
120+
}
110121

111-
var payload = args is not null && args.Any() ? JsonDocument.Parse(JsonSerializer.Serialize(args[0])) : null;
122+
var payload = GetPayload<T>(eventArgs);
112123
if (payload == null)
113124
{
114125
throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey");
@@ -124,4 +135,41 @@ protected internal sealed override async Task<T> WrapAsync<T>(
124135
var result = await idempotencyHandler.Handle();
125136
return result;
126137
}
138+
139+
/// <summary>
140+
/// Retrieve the payload from the annotated method parameter
141+
/// </summary>
142+
/// <param name="eventArgs">The <see cref="AspectEventArgs" /> instance containing the event data.</param>
143+
/// <typeparam name="T"></typeparam>
144+
/// <returns>The payload</returns>
145+
private static JsonDocument GetPayload<T>(AspectEventArgs eventArgs)
146+
{
147+
JsonDocument payload = null;
148+
var eventArgsMethod = eventArgs.Method;
149+
var args = eventArgs.Args;
150+
var isPlacedOnRequestHandler = IsPlacedOnRequestHandler(eventArgsMethod);
151+
// Use the first argument if IdempotentAttribute placed on handler or number of arguments is 1
152+
if (isPlacedOnRequestHandler || args.Count == 1)
153+
{
154+
payload = args is not null && args.Any() ? JsonDocument.Parse(JsonSerializer.Serialize(args[0])) : null;
155+
}
156+
else
157+
{
158+
//Find the first parameter in eventArgsMethod with attribute IdempotencyKeyAttribute
159+
var parameter = eventArgsMethod.GetParameters().FirstOrDefault(p => p.GetCustomAttribute<IdempotencyKeyAttribute>() != null);
160+
if (parameter != null)
161+
{
162+
// set payload to the value of the parameter
163+
payload = JsonDocument.Parse(JsonSerializer.Serialize(args[Array.IndexOf(eventArgsMethod.GetParameters(), parameter)]));
164+
}
165+
}
166+
167+
return payload;
168+
}
169+
170+
private static bool IsPlacedOnRequestHandler(MethodBase method)
171+
{
172+
//Check if method has two arguments and the second one is of type ILambdaContext
173+
return method.GetParameters().Length == 2 && method.GetParameters()[1].ParameterType == typeof(ILambdaContext);
174+
}
127175
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using Amazon.Lambda.Core;
17+
using AWS.Lambda.Powertools.Idempotency.Tests.Model;
18+
19+
namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers;
20+
21+
/// <summary>
22+
/// Simple Lambda function with Idempotent attribute on a sub method (not the Lambda handler one)
23+
/// </summary>
24+
public class IdempotencyInternalFunction
25+
{
26+
public Basket HandleRequest(Product input, ILambdaContext context)
27+
{
28+
return CreateBasket("fake", input);
29+
}
30+
31+
[Idempotent]
32+
private Basket CreateBasket([IdempotencyKey]string magicProduct, Product p)
33+
{
34+
IsSubMethodCalled = true;
35+
Basket b = new Basket(p);
36+
b.Add(new Product(0, magicProduct, 0));
37+
return b;
38+
}
39+
40+
public bool IsSubMethodCalled { get; private set; } = false;
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using Amazon.Lambda.Core;
17+
using AWS.Lambda.Powertools.Idempotency.Tests.Model;
18+
19+
namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers;
20+
21+
/// <summary>
22+
/// Simple Lambda function with Idempotent attribute on a sub method (not the Lambda handler one)
23+
/// </summary>
24+
public class IdempotencyInternalFunctionInternalKey
25+
{
26+
public Basket HandleRequest(Product input, ILambdaContext context)
27+
{
28+
return CreateBasket(input);
29+
}
30+
31+
[Idempotent]
32+
private Basket CreateBasket(Product p)
33+
{
34+
return new Basket(p);
35+
}
36+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using Amazon.Lambda.Core;
17+
using AWS.Lambda.Powertools.Idempotency.Tests.Model;
18+
19+
namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers;
20+
21+
/// <summary>
22+
/// Simple Lambda function with Idempotent attribute on a sub method.
23+
/// This one is invalid as there are two parameters and IdempotencyKey attribute
24+
/// is not used to specify which one will be used as a key for persistence.
25+
/// </summary>
26+
public class IdempotencyInternalFunctionInvalid
27+
{
28+
public Basket HandleRequest(Product input, ILambdaContext context)
29+
{
30+
return CreateBasket("fake", input);
31+
}
32+
33+
[Idempotent]
34+
private Basket CreateBasket(string magicProduct, Product p)
35+
{
36+
Basket b = new Basket(p);
37+
b.Add(new Product(0, magicProduct, 0));
38+
return b;
39+
}
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using Amazon.Lambda.Core;
17+
using AWS.Lambda.Powertools.Idempotency.Tests.Model;
18+
19+
namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers;
20+
21+
/// <summary>
22+
/// Simple Lambda function with Idempotent attribute a sub method.
23+
/// This one is invalid because the annotated method return type is void, thus we cannot store any response.
24+
/// </summary>
25+
public class IdempotencyInternalFunctionVoid
26+
{
27+
public Basket HandleRequest(Product input, ILambdaContext context)
28+
{
29+
Basket b = new Basket(input);
30+
AddProduct("fake", b);
31+
return b;
32+
}
33+
34+
[Idempotent]
35+
private void AddProduct([IdempotencyKey] string productName, Basket b)
36+
{
37+
b.Add(new Product(0, productName, 0));
38+
}
39+
40+
}

0 commit comments

Comments
 (0)