Skip to content

Commit fb7e6c3

Browse files
authored
Merge pull request #653 from hjgraca/aot(idempotency|jmespath)-aot-support
chore: AOT support for Idempotency
2 parents f4f00e6 + af59143 commit fb7e6c3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1217
-306
lines changed

docs/utilities/idempotency.md

+67
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The idempotency utility provides a simple solution to convert your Lambda functi
1414
* Select a subset of the event as the idempotency key using [JMESPath](https://jmespath.org/) expressions
1515
* Set a time window in which records with the same payload should be considered duplicates
1616
* Expires in-progress executions if the Lambda function times out halfway through
17+
* Ahead-of-Time compilation to native code support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.3.0
1718

1819
## Terminology
1920

@@ -821,10 +822,76 @@ Data would then be stored in DynamoDB like this:
821822
| idempotency#MyLambdaFunction | 2b2cdb5f86361e97b4383087c1ffdf27 | 1636549571 | COMPLETED | {"id": 527212, "message": "success"} |
822823
| idempotency#MyLambdaFunction | f091d2527ad1c78f05d54cc3f363be80 | 1636549585 | IN_PROGRESS | |
823824

825+
826+
## AOT Support
827+
828+
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.
829+
830+
### WithJsonSerializationContext()
831+
832+
To use Idempotency utility with AOT support you first need to add `WithJsonSerializationContext()` to your `Idempotency` configuration.
833+
834+
This ensures that when serializing your payload, the utility uses the correct serialization context.
835+
836+
In the example below, we use the default `LambdaFunctionJsonSerializerContext`:
837+
838+
```csharp
839+
Idempotency.Configure(builder =>
840+
builder.WithJsonSerializationContext(LambdaFunctionJsonSerializerContext.Default)));
841+
842+
```
843+
844+
Full example:
845+
846+
```csharp hl_lines="8"
847+
public static class Function
848+
{
849+
private static async Task Main()
850+
{
851+
var tableName = Environment.GetEnvironmentVariable("IDEMPOTENCY_TABLE_NAME");
852+
Idempotency.Configure(builder =>
853+
builder
854+
.WithJsonSerializationContext(LambdaFunctionJsonSerializerContext.Default)
855+
.WithOptions(optionsBuilder => optionsBuilder
856+
.WithExpiration(TimeSpan.FromHours(1)))
857+
.UseDynamoDb(storeBuilder => storeBuilder
858+
.WithTableName(tableName)
859+
));
860+
861+
Func<APIGatewayProxyRequest, ILambdaContext, APIGatewayProxyResponse> handler = FunctionHandler;
862+
await LambdaBootstrapBuilder.Create(handler,
863+
new SourceGeneratorLambdaJsonSerializer<LambdaFunctionJsonSerializerContext>())
864+
.Build()
865+
.RunAsync();
866+
}
867+
868+
[Idempotent]
869+
public static APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent,
870+
ILambdaContext context)
871+
{
872+
return new APIGatewayProxyResponse
873+
{
874+
Body = JsonSerializer.Serialize(response, typeof(Response), LambdaFunctionJsonSerializerContext.Default),
875+
StatusCode = 200,
876+
Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
877+
};
878+
}
879+
}
880+
881+
[JsonSerializable(typeof(APIGatewayProxyRequest))]
882+
[JsonSerializable(typeof(APIGatewayProxyResponse))]
883+
[JsonSerializable(typeof(Response))]
884+
public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext
885+
{
886+
}
887+
```
888+
824889
## Testing your code
825890

826891
The idempotency utility provides several routes to test your code.
827892

893+
You can check our Integration tests which use [TestContainers](https://testcontainers.com/modules/dynamodb/){:target="_blank"} with a local DynamoDB instance to test the idempotency utility. Or our end-to-end tests which use the AWS SDK to interact with a real DynamoDB table.
894+
828895
### Disabling the idempotency utility
829896
When testing your code, you may wish to disable the idempotency logic altogether and focus on testing your business logic. To do this, you can set the environment variable `POWERTOOLS_IDEMPOTENCY_DISABLED` to true.
830897

libraries/AWS.Lambda.Powertools.sln

+44-14
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Function", "tests\e2e\funct
9191
EndProject
9292
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Function.Tests", "tests\e2e\functions\idempotency\Function\test\Function.Tests\Function.Tests.csproj", "{FBCE2C8A-2F64-4B62-8CF1-D4A14C19A5CC}"
9393
EndProject
94-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-Function", "tests\e2e\functions\idempotency\AOT-Function\src\AOT-Function\AOT-Function.csproj", "{56DFC68A-3994-43CD-A17C-323495F1709C}"
94+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-FunctionPayloadSubsetTest", "tests\e2e\functions\idempotency\AOT-Function\src\AOT-FunctionPayloadSubsetTest\AOT-FunctionPayloadSubsetTest.csproj", "{ACA789EA-BD38-490B-A7F8-6A3A86985025}"
95+
EndProject
96+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-FunctionHandlerTest", "tests\e2e\functions\idempotency\AOT-Function\src\AOT-FunctionHandlerTest\AOT-FunctionHandlerTest.csproj", "{E71C48D2-AD56-4177-BBD7-6BB859A40C92}"
97+
EndProject
98+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-FunctionMethodAttributeTest", "tests\e2e\functions\idempotency\AOT-Function\src\AOT-FunctionMethodAttributeTest\AOT-FunctionMethodAttributeTest.csproj", "{CC8CFF43-DC72-464C-A42D-55E023DE8500}"
9599
EndProject
96100
Global
97101
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -478,18 +482,42 @@ Global
478482
{FBCE2C8A-2F64-4B62-8CF1-D4A14C19A5CC}.Release|x64.Build.0 = Release|Any CPU
479483
{FBCE2C8A-2F64-4B62-8CF1-D4A14C19A5CC}.Release|x86.ActiveCfg = Release|Any CPU
480484
{FBCE2C8A-2F64-4B62-8CF1-D4A14C19A5CC}.Release|x86.Build.0 = Release|Any CPU
481-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
482-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|Any CPU.Build.0 = Debug|Any CPU
483-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|x64.ActiveCfg = Debug|Any CPU
484-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|x64.Build.0 = Debug|Any CPU
485-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|x86.ActiveCfg = Debug|Any CPU
486-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|x86.Build.0 = Debug|Any CPU
487-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|Any CPU.ActiveCfg = Release|Any CPU
488-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|Any CPU.Build.0 = Release|Any CPU
489-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|x64.ActiveCfg = Release|Any CPU
490-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|x64.Build.0 = Release|Any CPU
491-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|x86.ActiveCfg = Release|Any CPU
492-
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|x86.Build.0 = Release|Any CPU
485+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
486+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|Any CPU.Build.0 = Debug|Any CPU
487+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|x64.ActiveCfg = Debug|Any CPU
488+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|x64.Build.0 = Debug|Any CPU
489+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|x86.ActiveCfg = Debug|Any CPU
490+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|x86.Build.0 = Debug|Any CPU
491+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|Any CPU.ActiveCfg = Release|Any CPU
492+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|Any CPU.Build.0 = Release|Any CPU
493+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|x64.ActiveCfg = Release|Any CPU
494+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|x64.Build.0 = Release|Any CPU
495+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|x86.ActiveCfg = Release|Any CPU
496+
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|x86.Build.0 = Release|Any CPU
497+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
498+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|Any CPU.Build.0 = Debug|Any CPU
499+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|x64.ActiveCfg = Debug|Any CPU
500+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|x64.Build.0 = Debug|Any CPU
501+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|x86.ActiveCfg = Debug|Any CPU
502+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|x86.Build.0 = Debug|Any CPU
503+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|Any CPU.ActiveCfg = Release|Any CPU
504+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|Any CPU.Build.0 = Release|Any CPU
505+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|x64.ActiveCfg = Release|Any CPU
506+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|x64.Build.0 = Release|Any CPU
507+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|x86.ActiveCfg = Release|Any CPU
508+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|x86.Build.0 = Release|Any CPU
509+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
510+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|Any CPU.Build.0 = Debug|Any CPU
511+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|x64.ActiveCfg = Debug|Any CPU
512+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|x64.Build.0 = Debug|Any CPU
513+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|x86.ActiveCfg = Debug|Any CPU
514+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|x86.Build.0 = Debug|Any CPU
515+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|Any CPU.ActiveCfg = Release|Any CPU
516+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|Any CPU.Build.0 = Release|Any CPU
517+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x64.ActiveCfg = Release|Any CPU
518+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x64.Build.0 = Release|Any CPU
519+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x86.ActiveCfg = Release|Any CPU
520+
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x86.Build.0 = Release|Any CPU
493521
EndGlobalSection
494522

495523
GlobalSection(NestedProjects) = preSolution
@@ -532,6 +560,8 @@ Global
532560
{FB2C7DA3-6FCE-429D-86F9-5775D0231EC6} = {CDAE55EB-9438-4F54-B7ED-931D64324D5F}
533561
{9AF99F6D-E8E7-443F-A965-D55B8E388836} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
534562
{FBCE2C8A-2F64-4B62-8CF1-D4A14C19A5CC} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
535-
{56DFC68A-3994-43CD-A17C-323495F1709C} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
563+
{ACA789EA-BD38-490B-A7F8-6A3A86985025} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
564+
{E71C48D2-AD56-4177-BBD7-6BB859A40C92} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
565+
{CC8CFF43-DC72-464C-A42D-55E023DE8500} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
536566
EndGlobalSection
537567
EndGlobal

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

+22-6
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
/*
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3-
*
3+
*
44
* Licensed under the Apache License, Version 2.0 (the "License").
55
* You may not use this file except in compliance with the License.
66
* A copy of the License is located at
7-
*
7+
*
88
* http://aws.amazon.com/apache2.0
9-
*
9+
*
1010
* or in the "license" file accompanying this file. This file is distributed
1111
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
1212
* express or implied. See the License for the specific language governing
1313
* permissions and limitations under the License.
1414
*/
1515

1616
using System;
17+
using System.Text.Json.Serialization;
1718
using Amazon.Lambda.Core;
1819
using AWS.Lambda.Powertools.Common;
20+
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;
1921
using AWS.Lambda.Powertools.Idempotency.Persistence;
2022

2123
namespace AWS.Lambda.Powertools.Idempotency;
@@ -27,7 +29,7 @@ namespace AWS.Lambda.Powertools.Idempotency;
2729
/// Use it before the function handler get called.
2830
/// Example: Idempotency.Configure(builder => builder.WithPersistenceStore(...));
2931
/// </summary>
30-
public sealed class Idempotency
32+
public sealed class Idempotency
3133
{
3234
/// <summary>
3335
/// The general configurations for the idempotency
@@ -47,6 +49,7 @@ internal Idempotency(IPowertoolsConfigurations powertoolsConfigurations)
4749
{
4850
powertoolsConfigurations.SetExecutionEnvironment(this);
4951
}
52+
5053
/// <summary>
5154
/// Set Idempotency options
5255
/// </summary>
@@ -68,7 +71,7 @@ private void SetPersistenceStore(BasePersistenceStore persistenceStore)
6871
/// <summary>
6972
/// Holds the idempotency Instance:
7073
/// </summary>
71-
public static Idempotency Instance { get; } = new(PowertoolsConfigurations.Instance);
74+
internal static Idempotency Instance { get; } = new(PowertoolsConfigurations.Instance);
7275

7376
/// <summary>
7477
/// Use this method to configure persistence layer (mandatory) and idempotency options (optional)
@@ -90,7 +93,7 @@ public static void Configure(Action<IdempotencyBuilder> configurationAction)
9093
/// Holds ILambdaContext
9194
/// </summary>
9295
public ILambdaContext LambdaContext { get; private set; }
93-
96+
9497
/// <summary>
9598
/// Can be used in a method which is not the handler to capture the Lambda context,
9699
/// to calculate the remaining time before the invocation times out.
@@ -177,5 +180,18 @@ public IdempotencyBuilder WithOptions(IdempotencyOptions options)
177180
Options = options;
178181
return this;
179182
}
183+
184+
#if NET8_0_OR_GREATER
185+
/// <summary>
186+
/// Set Customer JsonSerializerContext to append to IdempotencySerializationContext
187+
/// </summary>
188+
/// <param name="context"></param>
189+
/// <returns>IdempotencyBuilder</returns>
190+
public IdempotencyBuilder WithJsonSerializationContext(JsonSerializerContext context)
191+
{
192+
IdempotencySerializer.AddTypeInfoResolver(context);
193+
return this;
194+
}
195+
#endif
180196
}
181197
}

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,32 @@ public class IdempotencyOptionsBuilder
1111
/// Default maximum number of items in the local cache.
1212
/// </summary>
1313
private readonly int _localCacheMaxItems = 256;
14+
1415
/// <summary>
1516
/// Local cache enabled
1617
/// </summary>
1718
private bool _useLocalCache;
19+
1820
/// <summary>
1921
/// Default expiration in seconds.
2022
/// </summary>
2123
private long _expirationInSeconds = 60 * 60; // 1 hour
24+
2225
/// <summary>
2326
/// Event key JMESPath expression.
2427
/// </summary>
2528
private string _eventKeyJmesPath;
29+
2630
/// <summary>
2731
/// Payload validation JMESPath expression.
2832
/// </summary>
2933
private string _payloadValidationJmesPath;
34+
3035
/// <summary>
3136
/// Throw exception if no idempotency key is found.
3237
/// </summary>
3338
private bool _throwOnNoIdempotencyKey;
39+
3440
/// <summary>
3541
/// Default Hash function
3642
/// </summary>
@@ -107,7 +113,7 @@ public IdempotencyOptionsBuilder WithThrowOnNoIdempotencyKey(bool throwOnNoIdemp
107113
/// <returns>the instance of the builder (to chain operations)</returns>
108114
public IdempotencyOptionsBuilder WithExpiration(TimeSpan duration)
109115
{
110-
_expirationInSeconds = (long) duration.TotalSeconds;
116+
_expirationInSeconds = (long)duration.TotalSeconds;
111117
return this;
112118
}
113119

@@ -116,9 +122,15 @@ public IdempotencyOptionsBuilder WithExpiration(TimeSpan duration)
116122
/// </summary>
117123
/// <param name="hashFunction">Can be any algorithm supported by HashAlgorithm.Create</param>
118124
/// <returns>the instance of the builder (to chain operations)</returns>
125+
#if NET8_0_OR_GREATER
126+
[Obsolete("Idempotency uses MD5 and does not support other hash algorithms.")]
127+
#endif
119128
public IdempotencyOptionsBuilder WithHashFunction(string hashFunction)
120129
{
130+
#if NET6_0
131+
// for backward compability keep this code in .net 6
121132
_hashFunction = hashFunction;
133+
#endif
122134
return this;
123135
}
124136
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using AWS.Lambda.Powertools.Common;
2424
using AWS.Lambda.Powertools.Idempotency.Exceptions;
2525
using AWS.Lambda.Powertools.Idempotency.Internal;
26+
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;
2627

2728
namespace AWS.Lambda.Powertools.Idempotency;
2829

@@ -151,7 +152,7 @@ private static JsonDocument GetPayload<T>(AspectEventArgs eventArgs)
151152
// Use the first argument if IdempotentAttribute placed on handler or number of arguments is 1
152153
if (isPlacedOnRequestHandler || args.Count == 1)
153154
{
154-
payload = args is not null && args.Any() ? JsonDocument.Parse(JsonSerializer.Serialize(args[0])) : null;
155+
payload = args is not null && args.Any() ? JsonDocument.Parse(IdempotencySerializer.Serialize(args[0], typeof(object))) : null;
155156
}
156157
else
157158
{
@@ -160,7 +161,7 @@ private static JsonDocument GetPayload<T>(AspectEventArgs eventArgs)
160161
if (parameter != null)
161162
{
162163
// set payload to the value of the parameter
163-
payload = JsonDocument.Parse(JsonSerializer.Serialize(args[Array.IndexOf(eventArgsMethod.GetParameters(), parameter)]));
164+
payload = JsonDocument.Parse(IdempotencySerializer.Serialize(args[Array.IndexOf(eventArgsMethod.GetParameters(), parameter)], typeof(object)));
164165
}
165166
}
166167

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System.Threading.Tasks;
1919
using Amazon.Lambda.Core;
2020
using AWS.Lambda.Powertools.Idempotency.Exceptions;
21+
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;
2122
using AWS.Lambda.Powertools.Idempotency.Persistence;
2223

2324
namespace AWS.Lambda.Powertools.Idempotency.Internal;
@@ -184,7 +185,7 @@ private Task<T> HandleForStatus(DataRecord record)
184185
default:
185186
try
186187
{
187-
var result = JsonSerializer.Deserialize<T>(record.ResponseData!);
188+
var result = IdempotencySerializer.Deserialize<T>(record.ResponseData!);
188189
if (result is null)
189190
{
190191
throw new IdempotencyPersistenceLayerException("Unable to cast function response as " + typeof(T).Name);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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.Text.Json.Serialization;
17+
18+
namespace AWS.Lambda.Powertools.Idempotency.Internal.Serializers;
19+
20+
#if NET8_0_OR_GREATER
21+
22+
23+
/// <summary>
24+
/// The source generated JsonSerializerContext to be used to Serialize Idempotency types
25+
/// </summary>
26+
[JsonSerializable(typeof(string))]
27+
[JsonSerializable(typeof(object))]
28+
public partial class IdempotencySerializationContext : JsonSerializerContext
29+
{
30+
31+
}
32+
#endif

0 commit comments

Comments
 (0)