Verify icon indicating copy to clipboard operation
Verify copied to clipboard

VerifyJson throws PlatformNotSupportedException in AOT (Dynamic code generation is not supported)

Open 7amou3 opened this issue 3 months ago • 12 comments

Description:

When running tests against an AOT build, calling await VerifyJson(responseBody); fails with:

System.PlatformNotSupportedException : Dynamic code generation is not supported on this platform.

This appears to come from Verify’s default JSON serialization, which relies on dynamic code generation (Emit). That path isn’t available under NativeAOT or trimming.

Repro steps:

  • Create a test project referencing Verify.Xunit and Verify.Json.
  • Enable AOT:
    <PropertyGroup>
        <PublishAot>true</PublishAot>
     </PropertyGroup>
    
  • Run a test:
    [Fact]
    public Task Sample()
    {
        var responseBody = "{\"hello\":\"world\"}";
        return VerifyJson(responseBody);
    }
    
  • Run under AOT → crash with PlatformNotSupportedException.

7amou3 avatar Sep 02 '25 18:09 7amou3

how do i "Run under AOT" ?

SimonCropp avatar Sep 02 '25 21:09 SimonCropp

better yet. can you submit a pull request with a new test project that illustrates the issue

SimonCropp avatar Sep 03 '25 00:09 SimonCropp

here is a sample that produce the error: https://github.com/7amou3/TestProject

7amou3 avatar Sep 04 '25 20:09 7amou3

what is the logic behind AOTing and Trimming a test assembly?

SimonCropp avatar Sep 05 '25 05:09 SimonCropp

ie what is the value of it?

SimonCropp avatar Sep 05 '25 05:09 SimonCropp

I Trim/AOT tests to simulate production runtime conditions, catching reflection, serialization, and dynamic code issues before the customers do. Here’s the logic behind AOTing and trimming test assemblies:

  • Validation of Production Build Compatibility - running tests under the same constraints ensures my code and dependencies are compatible
  • Confidence in Linker/Trimmer Configuration - If trimming removes a type I actually need at runtime, the tests will fail
  • Ensuring Deterministic Builds - Running tests with the same publish settings as production ensures my binary is what I expect

7amou3 avatar Sep 05 '25 07:09 7amou3

ie what is the value of it?

@SimonCropp this is something we've been looking at for Sentry as well. Motivation described in this issue:

  • https://github.com/getsentry/sentry-dotnet/issues/3930

Since we've already got thousands of tests, it's going to be a bit of a journey to get decent AOT coverage for the codebase. Currently we've got some integration tests that run in Powershell / Pester which build some apps AOT, run these and then run regex expressions on the console output to verify expectations. These tests are pretty painful to build, run and maintain though, so the coverage of these isn't very good and we'd like to have something more comprehensive and easier to maintain for this.

I wrote an article that might be useful, describing some of the initial challenges we ran into building AOT support for Sentry, including the serialisation issues described above. If you read through that post, you'll see there are pros and cons to this... so if you did build something like that into Verify, you might want to make it opt in (or only use that serialisation technique when you know the test library is being compiled AOT).

Actually detecting whether the code has been compiled AOT or not is it's own challenge... at runtime there aren't really any easy ways to do it. You can reliably detect whether Trimming has been enabled, which is probably just as good... since it's the trimming that really messes with reflection and serialisation.

jamescrosswell avatar Sep 06 '25 00:09 jamescrosswell

@7amou3 @jamescrosswell given you both seem to have better understanding of this than me, would either of you like to work on a pull request

SimonCropp avatar Sep 11 '25 13:09 SimonCropp

I’d love to, but I’m not sure where to start. One idea could be to extract the serializer logic and let users choose their preferred default serializer, similar to how Refit does it.

7amou3 avatar Sep 14 '25 11:09 7amou3

I'm not going to have bandwidth in the near future... the guts of what we did in Sentry (where we have to support both serialisation techniques) is here:

    private static void InternalSerialize(Utf8JsonWriter writer, object value, bool preserveReferences = false)
    {
#if NET8_0_OR_GREATER
        if (JsonSerializer.IsReflectionEnabledByDefault)
        {
            JitSerialize();
        }
        else
        {
            var context = GetSerializerContext(value.GetType(), preserveReferences);
            JsonSerializer.Serialize(writer, value, value.GetType(), context);
        }
#else
        JitSerialize();
#endif
        return;

        [UnconditionalSuppressMessage("AOT", "IL3050: RequiresDynamicCode", Justification = AotHelper.AvoidAtRuntime)]
        [UnconditionalSuppressMessage("Trimming", "IL2026: RequiresUnreferencedCode", Justification = AotHelper.AvoidAtRuntime)]
        void JitSerialize()
        {
            var options = preserveReferences ? AltSerializerOptions : SerializerOptions;
            JsonSerializer.Serialize(writer, value, options);
        }
    }

So at runtime, it will select the appropriate serialiser... and then there are some UnconditionalSuppressMessage attributes that disable the warnings (since we know this code never gets run for AOT applications at runtime).

I'm not sure how easily/well that maps to Verify...

jamescrosswell avatar Sep 18 '25 07:09 jamescrosswell

@jamescrosswell i also dont think i will get to this any time soon

@7amou3 do you want to have a go at a pull request?

SimonCropp avatar Sep 29 '25 11:09 SimonCropp

Running into the same issue. Naively I would ask, can the issue simply be resolve by passing a set of JsonTypeInfo?

Odonno avatar Dec 03 '25 11:12 Odonno