Skip to content

Commit c8528ba

Browse files
authored
Merge branch 'develop' into feature/batch-deserialize-event-records
2 parents edf318a + 356fada commit c8528ba

File tree

11 files changed

+336
-63
lines changed

11 files changed

+336
-63
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
run: dotnet test --no-restore --filter "Category!=E2E" --collect:"XPlat Code Coverage" --results-directory ./codecov --verbosity normal
4141

4242
- name: Codecov
43-
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # 5.4.3
43+
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # 5.5.0
4444
with:
4545
token: ${{ secrets.CODECOV_TOKEN }}
4646
fail_ci_if_error: false

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@ AWS.Lambda.Powertools.sln.DotSettings.user
2525

2626
dist/
2727
site/
28-
samconfig.toml
28+
samconfig.toml
29+
30+
.kiro
31+
.claude
32+
.amazonq
33+
.github/instructions

docs/index.md

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -154,23 +154,6 @@ There are many ways you can help us gain future investments to improve everyone'
154154
155155
</div>
156156
157-
### Becoming a reference customer
158-
159-
Knowing which companies are using this library is important to help prioritize the project internally. The following companies, among others, use Powertools:
160-
161-
<div class="grid" style="text-align:center;" markdown>
162-
163-
[**Caylent**](https://caylent.com/){target="_blank" rel="nofollow"}
164-
{ .card }
165-
166-
[**Instil Software**](https://instil.co/){target="_blank" rel="nofollow"}
167-
{ .card }
168-
169-
[**Pushpay**](https://pushpay.com/){target="_blank" rel="nofollow"}
170-
{ .card }
171-
172-
</div>
173-
174157
## Tenets
175158
176159
These are our core principles to guide our decision making.

docs/utilities/idempotency.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,50 @@ Data would then be stored in DynamoDB like this:
846846
| idempotency#MyLambdaFunction | f091d2527ad1c78f05d54cc3f363be80 | 1636549585 | IN_PROGRESS | |
847847

848848

849+
### Manipulating the Idempotent Response
850+
851+
You can set up a response hook in the Idempotency configuration to manipulate the returned data when an operation is idempotent. The hook function will be called with the current deserialized response object and the Idempotency `DataRecord`.
852+
853+
#### Using Response Hooks
854+
855+
The example below shows how to append HTTP headers to an `APIGatewayProxyResponse`:
856+
857+
```csharp
858+
Idempotency.Config()
859+
.WithConfig(IdempotencyOptions.Builder()
860+
.WithEventKeyJmesPath("powertools_json(body).address")
861+
.WithResponseHook((responseData, dataRecord) => {
862+
if (responseData is APIGatewayProxyResponse proxyResponse)
863+
{
864+
proxyResponse.Headers ??= new Dictionary<string, string>();
865+
proxyResponse.Headers["x-idempotency-response"] = "true";
866+
proxyResponse.Headers["x-idempotency-expiration"] = dataRecord.ExpiryTimestamp.ToString();
867+
return proxyResponse;
868+
}
869+
return responseData;
870+
})
871+
.Build())
872+
.WithPersistenceStore(DynamoDBPersistenceStore.Builder()
873+
.WithTableName(Environment.GetEnvironmentVariable("IDEMPOTENCY_TABLE"))
874+
.Build())
875+
.Configure();
876+
```
877+
878+
???+ info "Info: Using custom de-serialization?"
879+
880+
The response hook is called after de-serialization so the payload you process will be the de-serialized C# object.
881+
882+
#### Being a good citizen
883+
884+
When using response hooks to manipulate returned data from idempotent operations, it's important to follow best practices to avoid introducing complexity or issues. Keep these guidelines in mind:
885+
886+
1. **Response hook works exclusively when operations are idempotent.** The hook will not be called when an operation is not idempotent, or when the idempotent logic fails.
887+
888+
2. **Catch and Handle Exceptions.** Your response hook code should catch and handle any exceptions that may arise from your logic. Unhandled exceptions will cause the Lambda function to fail unexpectedly.
889+
890+
3. **Keep Hook Logic Simple** Response hooks should consist of minimal and straightforward logic for manipulating response data. Avoid complex conditional branching and aim for hooks that are easy to reason about.
891+
892+
849893
## AOT Support
850894

851895
Native AOT trims your application code as part of the compilation to ensure that the binary is as small as possible. .NET 8 for Lambda provides improved trimming support compared to previous versions of .NET.

examples/Event Handler/BedrockAgentFunction/infra/package-lock.json

Lines changed: 11 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
* permissions and limitations under the License.
1414
*/
1515

16+
using System;
17+
1618
namespace AWS.Lambda.Powertools.Idempotency;
1719

1820
/// <summary>
@@ -57,32 +59,24 @@ public class IdempotencyOptions
5759
/// as supported by <see cref="System.Security.Cryptography.HashAlgorithm"/> (eg. SHA1, SHA-256, ...)
5860
/// </summary>
5961
public string HashFunction { get; }
62+
/// <summary>
63+
/// Delegate for manipulating idempotent responses.
64+
/// </summary>
65+
public Func<object, Persistence.DataRecord, object> ResponseHook { get; }
6066

6167
/// <summary>
6268
/// Constructor of <see cref="IdempotencyOptions"/>.
6369
/// </summary>
64-
/// <param name="eventKeyJmesPath"></param>
65-
/// <param name="payloadValidationJmesPath"></param>
66-
/// <param name="throwOnNoIdempotencyKey"></param>
67-
/// <param name="useLocalCache"></param>
68-
/// <param name="localCacheMaxItems"></param>
69-
/// <param name="expirationInSeconds"></param>
70-
/// <param name="hashFunction"></param>
71-
internal IdempotencyOptions(
72-
string eventKeyJmesPath,
73-
string payloadValidationJmesPath,
74-
bool throwOnNoIdempotencyKey,
75-
bool useLocalCache,
76-
int localCacheMaxItems,
77-
long expirationInSeconds,
78-
string hashFunction)
70+
/// <param name="builder">The builder containing the configuration values</param>
71+
internal IdempotencyOptions(IdempotencyOptionsBuilder builder)
7972
{
80-
EventKeyJmesPath = eventKeyJmesPath;
81-
PayloadValidationJmesPath = payloadValidationJmesPath;
82-
ThrowOnNoIdempotencyKey = throwOnNoIdempotencyKey;
83-
UseLocalCache = useLocalCache;
84-
LocalCacheMaxItems = localCacheMaxItems;
85-
ExpirationInSeconds = expirationInSeconds;
86-
HashFunction = hashFunction;
73+
EventKeyJmesPath = builder.EventKeyJmesPath;
74+
PayloadValidationJmesPath = builder.PayloadValidationJmesPath;
75+
ThrowOnNoIdempotencyKey = builder.ThrowOnNoIdempotencyKey;
76+
UseLocalCache = builder.UseLocalCache;
77+
LocalCacheMaxItems = builder.LocalCacheMaxItems;
78+
ExpirationInSeconds = builder.ExpirationInSeconds;
79+
HashFunction = builder.HashFunction;
80+
ResponseHook = builder.ResponseHook;
8781
}
8882
}

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

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,59 @@ public class IdempotencyOptionsBuilder
4343
private string _hashFunction = "MD5";
4444

4545
/// <summary>
46-
/// Initialize and return an instance of IdempotencyConfig.
46+
/// Response hook function
47+
/// </summary>
48+
private Func<object, AWS.Lambda.Powertools.Idempotency.Persistence.DataRecord, object> _responseHook;
49+
50+
/// <summary>
51+
/// Gets the event key JMESPath expression.
52+
/// </summary>
53+
internal string EventKeyJmesPath => _eventKeyJmesPath;
54+
55+
/// <summary>
56+
/// Gets the payload validation JMESPath expression.
57+
/// </summary>
58+
internal string PayloadValidationJmesPath => _payloadValidationJmesPath;
59+
60+
/// <summary>
61+
/// Gets whether to throw exception if no idempotency key is found.
62+
/// </summary>
63+
internal bool ThrowOnNoIdempotencyKey => _throwOnNoIdempotencyKey;
64+
65+
/// <summary>
66+
/// Gets whether local cache is enabled.
67+
/// </summary>
68+
internal bool UseLocalCache => _useLocalCache;
69+
70+
/// <summary>
71+
/// Gets the maximum number of items in the local cache.
72+
/// </summary>
73+
internal int LocalCacheMaxItems => _localCacheMaxItems;
74+
75+
/// <summary>
76+
/// Gets the expiration in seconds.
77+
/// </summary>
78+
internal long ExpirationInSeconds => _expirationInSeconds;
79+
80+
/// <summary>
81+
/// Gets the hash function.
82+
/// </summary>
83+
internal string HashFunction => _hashFunction;
84+
85+
/// <summary>
86+
/// Gets the response hook function.
87+
/// </summary>
88+
internal Func<object, AWS.Lambda.Powertools.Idempotency.Persistence.DataRecord, object> ResponseHook => _responseHook;
89+
90+
/// <summary>
91+
/// Initialize and return an instance of IdempotencyOptions.
4792
/// Example:
48-
/// IdempotencyConfig.Builder().WithUseLocalCache().Build();
49-
/// This instance must then be passed to the Idempotency.Config:
50-
/// Idempotency.Config().WithConfig(config).Configure();
93+
/// new IdempotencyOptionsBuilder().WithUseLocalCache().Build();
94+
/// This instance can then be passed to Idempotency.Configure:
95+
/// Idempotency.Configure(builder => builder.WithOptions(options));
5196
/// </summary>
52-
/// <returns>an instance of IdempotencyConfig</returns>
53-
public IdempotencyOptions Build() =>
54-
new(_eventKeyJmesPath,
55-
_payloadValidationJmesPath,
56-
_throwOnNoIdempotencyKey,
57-
_useLocalCache,
58-
_localCacheMaxItems,
59-
_expirationInSeconds,
60-
_hashFunction);
97+
/// <returns>an instance of IdempotencyOptions</returns>
98+
public IdempotencyOptions Build() => new(this);
6199

62100
/// <summary>
63101
/// A JMESPath expression to extract the idempotency key from the event record.
@@ -133,4 +171,15 @@ public IdempotencyOptionsBuilder WithHashFunction(string hashFunction)
133171
#endif
134172
return this;
135173
}
174+
175+
/// <summary>
176+
/// Set a response hook function, to be called with the response and the data record.
177+
/// </summary>
178+
/// <param name="hook">The response hook function</param>
179+
/// <returns>the instance of the builder (to chain operations)</returns>
180+
public IdempotencyOptionsBuilder WithResponseHook(Func<object, AWS.Lambda.Powertools.Idempotency.Persistence.DataRecord, object> hook)
181+
{
182+
_responseHook = hook;
183+
return this;
184+
}
136185
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,24 @@ private Task<T> HandleForStatus(DataRecord record)
208208
{
209209
throw new IdempotencyPersistenceLayerException("Unable to cast function response as " + typeof(T).Name);
210210
}
211+
// Response hook logic
212+
var responseHook = Idempotency.Instance.IdempotencyOptions?.ResponseHook;
213+
if (responseHook != null)
214+
{
215+
try
216+
{
217+
var hooked = responseHook(result, record);
218+
if (hooked is T typedHooked)
219+
{
220+
return Task.FromResult(typedHooked);
221+
}
222+
// If hook returns wrong type, fallback to original result
223+
}
224+
catch (Exception)
225+
{
226+
// If hook throws, fallback to original result
227+
}
228+
}
211229
return Task.FromResult(result);
212230
}
213231
catch (Exception e)

libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,9 +370,9 @@ public class DynamoDBPersistenceStoreBuilder
370370
private AmazonDynamoDBClient _dynamoDbClient;
371371

372372
/// <summary>
373-
/// Initialize and return a new instance of {@link DynamoDBPersistenceStore}.
373+
/// Initialize and return a new instance of <see cref="DynamoDBPersistenceStore"/>.
374374
/// Example:
375-
/// DynamoDBPersistenceStore.builder().withTableName("idempotency_store").build();
375+
/// new DynamoDBPersistenceStoreBuilder().WithTableName("idempotency_store").Build();
376376
/// </summary>
377377
/// <returns></returns>
378378
/// <exception cref="ArgumentNullException"></exception>

0 commit comments

Comments
 (0)