Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: AOT support for Idempotency #653

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
28 changes: 22 additions & 6 deletions libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
*
* http://aws.amazon.com/apache2.0
*
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

using System;
using System.Text.Json.Serialization;
using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.Common;
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;
using AWS.Lambda.Powertools.Idempotency.Persistence;

namespace AWS.Lambda.Powertools.Idempotency;
Expand All @@ -27,7 +29,7 @@ namespace AWS.Lambda.Powertools.Idempotency;
/// Use it before the function handler get called.
/// Example: Idempotency.Configure(builder => builder.WithPersistenceStore(...));
/// </summary>
public sealed class Idempotency
public sealed class Idempotency
{
/// <summary>
/// The general configurations for the idempotency
Expand All @@ -47,6 +49,7 @@ internal Idempotency(IPowertoolsConfigurations powertoolsConfigurations)
{
powertoolsConfigurations.SetExecutionEnvironment(this);
}

/// <summary>
/// Set Idempotency options
/// </summary>
Expand All @@ -68,7 +71,7 @@ private void SetPersistenceStore(BasePersistenceStore persistenceStore)
/// <summary>
/// Holds the idempotency Instance:
/// </summary>
public static Idempotency Instance { get; } = new(PowertoolsConfigurations.Instance);
internal static Idempotency Instance { get; } = new(PowertoolsConfigurations.Instance);

/// <summary>
/// Use this method to configure persistence layer (mandatory) and idempotency options (optional)
Expand All @@ -90,7 +93,7 @@ public static void Configure(Action<IdempotencyBuilder> configurationAction)
/// Holds ILambdaContext
/// </summary>
public ILambdaContext LambdaContext { get; private set; }

/// <summary>
/// Can be used in a method which is not the handler to capture the Lambda context,
/// to calculate the remaining time before the invocation times out.
Expand Down Expand Up @@ -177,5 +180,18 @@ public IdempotencyBuilder WithOptions(IdempotencyOptions options)
Options = options;
return this;
}

#if NET8_0_OR_GREATER
/// <summary>
/// Set Customer JsonSerializerContext to append to IdempotencySerializationContext
/// </summary>
/// <param name="context"></param>
/// <returns>IdempotencyBuilder</returns>
public IdempotencyBuilder WithJsonSerializationContext(JsonSerializerContext context)
{
IdempotencySerializer.AddTypeInfoResolver(context);
return this;
}
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,32 @@ public class IdempotencyOptionsBuilder
/// Default maximum number of items in the local cache.
/// </summary>
private readonly int _localCacheMaxItems = 256;

/// <summary>
/// Local cache enabled
/// </summary>
private bool _useLocalCache;

/// <summary>
/// Default expiration in seconds.
/// </summary>
private long _expirationInSeconds = 60 * 60; // 1 hour

/// <summary>
/// Event key JMESPath expression.
/// </summary>
private string _eventKeyJmesPath;

/// <summary>
/// Payload validation JMESPath expression.
/// </summary>
private string _payloadValidationJmesPath;

/// <summary>
/// Throw exception if no idempotency key is found.
/// </summary>
private bool _throwOnNoIdempotencyKey;

/// <summary>
/// Default Hash function
/// </summary>
Expand Down Expand Up @@ -107,7 +113,7 @@ public IdempotencyOptionsBuilder WithThrowOnNoIdempotencyKey(bool throwOnNoIdemp
/// <returns>the instance of the builder (to chain operations)</returns>
public IdempotencyOptionsBuilder WithExpiration(TimeSpan duration)
{
_expirationInSeconds = (long) duration.TotalSeconds;
_expirationInSeconds = (long)duration.TotalSeconds;
return this;
}

Expand All @@ -116,9 +122,15 @@ public IdempotencyOptionsBuilder WithExpiration(TimeSpan duration)
/// </summary>
/// <param name="hashFunction">Can be any algorithm supported by HashAlgorithm.Create</param>
/// <returns>the instance of the builder (to chain operations)</returns>
#if NET8_0_OR_GREATER
[Obsolete("Idempotency uses MD5 and does not support other hash algorithms.")]
#endif
public IdempotencyOptionsBuilder WithHashFunction(string hashFunction)
{
#if NET6_0
// for backward compability keep this code in .net 6
_hashFunction = hashFunction;
#endif
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using AWS.Lambda.Powertools.Common;
using AWS.Lambda.Powertools.Idempotency.Exceptions;
using AWS.Lambda.Powertools.Idempotency.Internal;
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;

namespace AWS.Lambda.Powertools.Idempotency;

Expand Down Expand Up @@ -151,7 +152,7 @@ private static JsonDocument GetPayload<T>(AspectEventArgs eventArgs)
// Use the first argument if IdempotentAttribute placed on handler or number of arguments is 1
if (isPlacedOnRequestHandler || args.Count == 1)
{
payload = args is not null && args.Any() ? JsonDocument.Parse(JsonSerializer.Serialize(args[0])) : null;
payload = args is not null && args.Any() ? JsonDocument.Parse(IdempotencySerializer.Serialize(args[0], typeof(object))) : null;
}
else
{
Expand All @@ -160,7 +161,7 @@ private static JsonDocument GetPayload<T>(AspectEventArgs eventArgs)
if (parameter != null)
{
// set payload to the value of the parameter
payload = JsonDocument.Parse(JsonSerializer.Serialize(args[Array.IndexOf(eventArgsMethod.GetParameters(), parameter)]));
payload = JsonDocument.Parse(IdempotencySerializer.Serialize(args[Array.IndexOf(eventArgsMethod.GetParameters(), parameter)], typeof(object)));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using System.Threading.Tasks;
using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.Idempotency.Exceptions;
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;
using AWS.Lambda.Powertools.Idempotency.Persistence;

namespace AWS.Lambda.Powertools.Idempotency.Internal;
Expand Down Expand Up @@ -184,7 +185,7 @@ private Task<T> HandleForStatus(DataRecord record)
default:
try
{
var result = JsonSerializer.Deserialize<T>(record.ResponseData!);
var result = IdempotencySerializer.Deserialize<T>(record.ResponseData!);
if (result is null)
{
throw new IdempotencyPersistenceLayerException("Unable to cast function response as " + typeof(T).Name);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

using System.Text.Json.Serialization;

namespace AWS.Lambda.Powertools.Idempotency.Internal.Serializers;

#if NET8_0_OR_GREATER


/// <summary>
/// The source generated JsonSerializerContext to be used to Serialize Idempotency types
/// </summary>
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(object))]
public partial class IdempotencySerializationContext : JsonSerializerContext
{

}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

using System;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using AWS.Lambda.Powertools.Common.Utils;

namespace AWS.Lambda.Powertools.Idempotency.Internal.Serializers;

/// <summary>
/// Serializer for Idempotency.
/// </summary>
internal static class IdempotencySerializer
{
private static JsonSerializerOptions _jsonOptions;

static IdempotencySerializer()
{
BuildDefaultOptions();
}

/// <summary>
/// Builds the default JsonSerializerOptions.
/// </summary>
private static void BuildDefaultOptions()
{
_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
#if NET8_0_OR_GREATER
if (!RuntimeFeatureWrapper.IsDynamicCodeSupported)
{
_jsonOptions.TypeInfoResolverChain.Add(IdempotencySerializationContext.Default);
}

Check warning on line 50 in libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Serializers/IdempotencySerializer.cs

View check run for this annotation

Codecov / codecov/patch

libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Serializers/IdempotencySerializer.cs#L48-L50

Added lines #L48 - L50 were not covered by tests
#endif
}

#if NET8_0_OR_GREATER

/// <summary>
/// Adds a JsonTypeInfoResolver to the JsonSerializerOptions.
/// </summary>
/// <param name="context">The JsonTypeInfoResolver to add.</param>
/// <remarks>
/// This method is only available in .NET 8.0 and later versions.
/// </remarks>
internal static void AddTypeInfoResolver(JsonSerializerContext context)
{
BuildDefaultOptions();
_jsonOptions.TypeInfoResolverChain.Add(context);
}

/// <summary>
/// Gets the JsonTypeInfo for a given type.
/// </summary>
/// <param name="type">The type to get information for.</param>
/// <returns>The JsonTypeInfo for the specified type, or null if not found.</returns>
internal static JsonTypeInfo GetTypeInfo(Type type)
{
var typeInfo = _jsonOptions.TypeInfoResolver?.GetTypeInfo(type, _jsonOptions);
if (typeInfo == null)
{
throw new SerializationException(
$"Type {type} is not known to the serializer. Ensure it's included in the JsonSerializerContext.");
}

return typeInfo;
}

Check warning on line 84 in libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Serializers/IdempotencySerializer.cs

View check run for this annotation

Codecov / codecov/patch

libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Serializers/IdempotencySerializer.cs#L83-L84

Added lines #L83 - L84 were not covered by tests
#endif

/// <summary>
/// Serializes the specified object to a JSON string.
/// </summary>
/// <param name="value">The object to serialize.</param>
/// <param name="inputType">The type of the object to serialize.</param>
/// <returns>A JSON string representation of the object.</returns>
internal static string Serialize(object value, Type inputType)
{
#if NET6_0
return JsonSerializer.Serialize(value, _jsonOptions);
#else
if (RuntimeFeatureWrapper.IsDynamicCodeSupported)
{
#pragma warning disable
return JsonSerializer.Serialize(value, _jsonOptions);
}

return JsonSerializer.Serialize(value, GetTypeInfo(inputType));

Check warning on line 104 in libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Serializers/IdempotencySerializer.cs

View check run for this annotation

Codecov / codecov/patch

libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Serializers/IdempotencySerializer.cs#L104

Added line #L104 was not covered by tests
#endif
}

/// <summary>
/// Deserializes the specified JSON string to an object of type T.
/// </summary>
/// <typeparam name="T">The type of the object to deserialize to.</typeparam>
/// <param name="value">The JSON string to deserialize.</param>
/// <returns>An object of type T represented by the JSON string.</returns>
internal static T Deserialize<T>(string value)
{
#if NET6_0
return JsonSerializer.Deserialize<T>(value,_jsonOptions);
#else
if (RuntimeFeatureWrapper.IsDynamicCodeSupported)
{
#pragma warning disable
return JsonSerializer.Deserialize<T>(value, _jsonOptions);
}

return (T)JsonSerializer.Deserialize(value, GetTypeInfo(typeof(T)));

Check warning on line 125 in libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Serializers/IdempotencySerializer.cs

View check run for this annotation

Codecov / codecov/patch

libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Serializers/IdempotencySerializer.cs#L125

Added line #L125 was not covered by tests
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using AWS.Lambda.Powertools.Common;
using AWS.Lambda.Powertools.Idempotency.Exceptions;
using AWS.Lambda.Powertools.Idempotency.Internal;
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;
using AWS.Lambda.Powertools.JMESPath;

namespace AWS.Lambda.Powertools.Idempotency.Persistence;
Expand Down Expand Up @@ -95,7 +96,7 @@ internal void Configure(IdempotencyOptions options, string functionName, LRUCach
/// <param name="now">The current date time</param>
public virtual async Task SaveSuccess(JsonDocument data, object result, DateTimeOffset now)
{
var responseJson = JsonSerializer.Serialize(result);
var responseJson = IdempotencySerializer.Serialize(result, typeof(object));
var record = new DataRecord(
GetHashedIdempotencyKey(data),
DataRecord.DataRecordStatus.COMPLETED,
Expand Down Expand Up @@ -323,7 +324,13 @@ private static bool IsMissingIdempotencyKey(JsonElement data)
/// <exception cref="ArgumentException"></exception>
internal string GenerateHash(JsonElement data)
{
#if NET8_0_OR_GREATER
// starting .NET 8 no option to change hash algorithm
using var hashAlgorithm = MD5.Create();
#else

using var hashAlgorithm = HashAlgorithm.Create(_idempotencyOptions.HashFunction);
#endif
if (hashAlgorithm == null)
{
throw new ArgumentException("Invalid HashAlgorithm");
Expand Down
Loading
Loading