aws / aws-lambda-dotnet

Libraries, samples and tools to help .NET Core developers develop AWS Lambda functions.
Apache License 2.0
1.57k stars 478 forks source link

JsonSerializer.Deserialize<T> returns invalid object when called by TypeConverter within AWS Lambda Test Tool #1453

Open alex-bolenok-centralreach opened 1 year ago

alex-bolenok-centralreach commented 1 year ago

Describe the bug

When I'm running some code in the test tool, I come across an issue, when System.Text.Json.JsonSerializer.Deserialize returns different results depending on whether it's being called from the top level or from a System.ComponentModel.TypeConverter method.

This is best illustrated with code (see attachment)

lambda-test-tool.zip

Run:

dotnet tool restore
dotnet build
dotnet dotnet-lambda-test-tool-6.0 --no-ui --function-handler Lambda::Main.Lambda::Handler --pause-exit false

Output:

AWS .NET Core 6.0 Mock Lambda Test Tool (0.12.7)
Loaded local Lambda runtime from project output /home/alex.bolenok/Projects/lambda-test-tool/bin/Debug/net6.0
Executing Lambda function without web interface
... Using function handler Lambda::Main.Lambda::Handler
... No payload configured. If a payload is required set the --payload switch to a file path or a JSON document.
... Setting AWS_PROFILE environment variable to default.
... No default AWS region configured. The --region switch can be used to configure an AWS Region.
Captured Log information:
test
True
Main.S3Bucket
Main.S3Bucket
>>>> False <<<<

Request executed successfully
Response:

This issue seems to be specific to the test tool (when I run this code in an actual Lambda, or in a console app, it works as expected).

Expected Behavior

TypeDescriptor.GetConverter(typeof T).ConvertFrom and JsonSerializer.Deserialize<T> both return objects of the same type T.

Current Behavior

TypeDescriptor.GetConverter(typeof T).ConvertFrom and JsonSerializer.Deserialize<T> return objects of different types.

Reproduction Steps

Run:

dotnet tool restore
dotnet build
dotnet dotnet-lambda-test-tool-6.0 --no-ui --function-handler Lambda::Main.Lambda::Handler --pause-exit false

Output:

AWS .NET Core 6.0 Mock Lambda Test Tool (0.12.7)
Loaded local Lambda runtime from project output /home/alex.bolenok/Projects/lambda-test-tool/bin/Debug/net6.0
Executing Lambda function without web interface
... Using function handler Lambda::Main.Lambda::Handler
... No payload configured. If a payload is required set the --payload switch to a file path or a JSON document.
... Setting AWS_PROFILE environment variable to default.
... No default AWS region configured. The --region switch can be used to configure an AWS Region.
Captured Log information:
test
True
Main.S3Bucket
Main.S3Bucket
>>>> False <<<<

Request executed successfully
Response:

Possible Solution

There is something weird in the way the test tool calles the framework methods. The actual lambda runtime works fine.

Additional Information/Context

No response

AWS .NET SDK and/or Package version used

{
    "version": 1,
    "isRoot": true,
    "tools": {
        "amazon.lambda.testtool-6.0": {
            "version": "0.12.7",
            "commands": [
                "dotnet-lambda-test-tool-6.0"
            ]
        }
    }
}
    <PackageReference Include="Amazon.Lambda.Core" Version="2.1.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.3.1" />
    <PackageReference Include="Amazon.Lambda.SQSEvents" Version="2.1.0" />

Targeted .NET Platform

.NET 6

Operating System and version

Amazon Linux

ashishdhingra commented 1 year ago

Reproducible using below code in test tool (code it ported to Lambda project type for easy debugging): .csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
    <AWSProjectType>Lambda</AWSProjectType>
    <!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. -->
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    <!-- Generate ready to run images during publishing to improve cold start time. -->
    <PublishReadyToRun>true</PublishReadyToRun>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="2.1.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.3.1" />
    <PackageReference Include="Amazon.Lambda.SQSEvents" Version="2.1.0" />
    <PackageReference Include="JetBrains.Annotations" Version="2022.3.1" />
  </ItemGroup>
</Project>

Function.cs

using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
using Amazon.Lambda.Core;
using Amazon.Lambda.SQSEvents;
using JetBrains.Annotations;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace LambdaNet6Test;

public static class JsonOptions
{
    public static readonly JsonSerializerOptions JsonSerializerOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };
}

internal class JsonTypeConverterAdapter<T> : TypeConverter
{
    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
        => value is string stringValue
            ? JsonSerializer.Deserialize<T>(stringValue, JsonOptions.JsonSerializerOptions)
            : base.ConvertFrom(context, culture, value);

    public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value,
        Type destinationType)
        => value is T tValue && destinationType == typeof(string)
            ? JsonSerializer.Serialize(tValue)
            : base.ConvertTo(context, culture, value, destinationType);

    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
        => sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);

    public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
        => destinationType == typeof(T) || base.CanConvertTo(context, destinationType);
}

[TypeConverter(typeof(JsonTypeConverterAdapter<S3Bucket>))]
[Serializable]
internal class S3Bucket
{
    public string Bucket { get; set; } = string.Empty;
}

public class Function
{

    [PublicAPI]
    [SuppressMessage("Performance", "CA1822", Justification = "Lambda API")]
    [SuppressMessage("Style", "IDE0060", Justification = "Lambda API")]
    public string FunctionHandler(SQSEvent sqsEvent, ILambdaContext lambdaContext)
    {
        const string json = "{\"bucket\":\"test\"}";

        var s3BucketWorks = JsonSerializer.Deserialize<S3Bucket>(json, JsonOptions.JsonSerializerOptions);
        if (s3BucketWorks == null)
        {
            return string.Empty;
        }
        Console.WriteLine(s3BucketWorks.Bucket);
        Console.WriteLine((s3BucketWorks.GetType() == typeof(S3Bucket)).ToString());

        var s3BucketFails = TypeDescriptor.GetConverter(typeof(S3Bucket)).ConvertFrom(json);
        if (s3BucketFails == null)
        {
            return string.Empty;
        }
        Console.WriteLine(s3BucketFails.GetType());
        Console.WriteLine(typeof(S3Bucket));
        Console.WriteLine((s3BucketFails is S3Bucket).ToString());
        return string.Empty;
    }
}

While executing under Lambda Test tool, the statement (s3BucketFails is S3Bucket).ToString() returns False. Executing the same statement in immediate window when breakpoint is hit during debugging using Lambda Test tool returns correct value of True.