Skip to content

Expose ILambdaSerializer on ILambdaContext#2378

Draft
GarrettBeatty wants to merge 2 commits into
devfrom
GarrettBeatty/expose-serializer-on-context
Draft

Expose ILambdaSerializer on ILambdaContext#2378
GarrettBeatty wants to merge 2 commits into
devfrom
GarrettBeatty/expose-serializer-on-context

Conversation

@GarrettBeatty
Copy link
Copy Markdown
Contributor

@GarrettBeatty GarrettBeatty commented May 14, 2026

Summary

Adds a preview Serializer property to ILambdaContext so user code can reuse the ILambdaSerializer that the Lambda function registered with the runtime — either via LambdaBootstrapBuilder.Create(handler, serializer) (custom runtime) or [assembly: LambdaSerializer(typeof(...))] (class-library mode) — without re-instantiating it.

This is a small additive change to enable downstream features — most immediately, simplifying the AOT story for Amazon.Lambda.DurableExecution (follow-up PR), but the property is generally useful for any user-side library that wants to honor the function's configured serialization choice.

Preview API. The property is marked [Experimental("AWSLAMBDA001")]. Class-library mode requires an updated managed Lambda runtime to populate it; until that managed-runtime release ships, class-library users will see context.Serializer == null. Custom-runtime / NativeAOT users get the full behavior immediately.

Motivation

Today the ILambdaSerializer registered by the function lives entirely inside HandlerWrapper's closure (custom runtime) or inside UserCodeLoader._invokeDelegate (class-library mode) — neither path makes it discoverable to downstream code. Libraries that want to (de)serialize within a handler are forced to either:

  1. Take an ILambdaSerializer as an explicit constructor/method argument, duplicating registration.
  2. Hard-code their own serializer instance (e.g., new DefaultLambdaJsonSerializer()), which diverges from the function's actual configuration and silently breaks AOT scenarios.

Exposing the registered serializer via ILambdaContext removes the duplication and lets libraries inherit the function's AOT-aware serializer choice automatically.

Design

Amazon.Lambda.Core

Adds ILambdaSerializer Serializer { get { return null; } } to ILambdaContext, inside the existing #if NET8_0_OR_GREATER block, matching the precedent set by TenantId and TraceId.

  • Default-implemented → no breaking change for any user-implemented ILambdaContext.
  • netstandard2.0 consumers see no change.
  • Marked [Experimental("AWSLAMBDA001")] so callers explicitly opt in (#pragma warning disable AWSLAMBDA001).

Amazon.Lambda.RuntimeSupport

Two registration paths funnel into the same per-invocation setter:

Path 1 — custom runtime:

  • HandlerWrapper.Serializer exposes the serializer the user passed to GetHandlerWrapper(handler, serializer). All 20 serializer-taking overloads populate this field.
  • LambdaBootstrap constructors that take a HandlerWrapper forward handlerWrapper.Serializer into a private _serializer field.

Path 2 — class-library mode:

  • UserCodeLoader.CustomerSerializerInstance (new) exposes the serializer constructed from [assembly: LambdaSerializer].
  • RuntimeSupportInitializer wraps UserCodeInitializer.InitializeAsync so that, after init runs, it calls bootstrap.SetSerializer(userCodeLoader.CustomerSerializerInstance). This is the only place the new internal LambdaBootstrap.SetSerializer setter is intended to be used.

Common per-invocation hookup:

  • LambdaBootstrap.InvokeOnceAsync calls SetSerializerOnContext(impl) after GetNextInvocationAsync returns the per-invocation LambdaContext.
  • That helper delegates to LambdaContextSerializerIsolated.TrySetSerializer — a one-line static shim that mirrors the existing TraceProviderIsolated, SnapstartHelperCopySnapshotCallbacksIsolated, etc. patterns.
  • The shim's call site is wrapped in try { ... } catch (TypeLoadException) { ... } catch (MissingMethodException) { ... }. If the user's function references an older Amazon.Lambda.Core that doesn't declare ILambdaContext.Serializer, the JIT throws when the Isolated method is JIT'd; the catch logs once and sets a _disableSerializerOnContext flag so subsequent invocations short-circuit instead of repeatedly throwing.

LambdaContext.Serializer is a public read-only property with an internal setter ({ get; internal set; }). Constructor injection was deliberately avoided so LambdaContext can still be constructed by older RuntimeSupport callers without signature breakage.

Amazon.Lambda.TestUtilities

TestLambdaContext.Serializer is a writable property so tests can mirror the production wiring.

Usage

From an annotated function

#pragma warning disable AWSLAMBDA001  // ILambdaContext.Serializer is preview
public class Functions
{
    public async Task<MyOutput> Handler(MyInput input, ILambdaContext context)
    {
        // Reuse the registered serializer for an ad-hoc round trip
        // (e.g., to forward a payload to another service).
        using var ms = new MemoryStream();
        context.Serializer.Serialize(input, ms);
        ms.Position = 0;
        var roundTripped = context.Serializer.Deserialize<MyInput>(ms);

        return new MyOutput();
    }
}

From a custom runtime

#pragma warning disable AWSLAMBDA001
var serializer = new SourceGeneratorLambdaJsonSerializer<MyJsonContext>();
await LambdaBootstrapBuilder
    .Create<MyInput, MyOutput>(Handler, serializer)
    .Build()
    .RunAsync();

static Task<MyOutput> Handler(MyInput input, ILambdaContext context)
{
    // context.Serializer is the same SourceGeneratorLambdaJsonSerializer<MyJsonContext>
    // instance — AOT-safe because trim analysis follows the user's registered type.
    Assert.Same(serializer, context.Serializer);
    return Task.FromResult(new MyOutput());
}

From a downstream library

#pragma warning disable AWSLAMBDA001
public static class MyHelper
{
    public static T DeserializeUsingContext<T>(string json, ILambdaContext context)
    {
        if (context.Serializer is null)
            throw new InvalidOperationException(
                "No serializer registered. Pass one to LambdaBootstrapBuilder.Create " +
                "or use [assembly: LambdaSerializer(...)].");

        var bytes = Encoding.UTF8.GetBytes(json);
        using var ms = new MemoryStream(bytes);
        return context.Serializer.Deserialize<T>(ms);
    }
}

Compatibility

  • Amazon.Lambda.Core — Minor version bump. Additive; default-implemented interface member only available on net8.0+.
  • Amazon.Lambda.RuntimeSupport — Minor version bump. New public HandlerWrapper.Serializer property; new internal LambdaBootstrap.SetSerializer; new LambdaContextSerializerIsolated shim. No removed or changed signatures.
  • Amazon.Lambda.TestUtilities — Minor version bump. New public TestLambdaContext.Serializer property.

No removed APIs. No changed signatures. netstandard2.0 consumers see no change. Functions referencing an older Amazon.Lambda.Core continue to run; they just see ILambdaContext.Serializer == null because the Isolated shim catches the TypeLoadException from the missing interface member.

Tests

  • Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/.../LambdaContextSerializerTests.cs — 8 tests covering:
    • LambdaContext.Serializer defaults to null when not set.
    • LambdaContextSerializerIsolated.TrySetSerializer populates the property.
    • LambdaContextSerializerIsolated.TrySetSerializer is null-tolerant on the context argument.
    • HandlerWrapper.Serializer is populated for typed-input/output overloads.
    • HandlerWrapper.Serializer is null for raw-stream overloads.
    • All 7 sample serializer-taking overloads propagate the field (regression guard).
    • End-to-end via LambdaBootstrap.InvokeOnceAsync: a handler invoked through the wrapper sees context.Serializer == registered serializer.
    • End-to-end raw-stream handler: context.Serializer stays null.
  • Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs — 2 tests covering the TestLambdaContext.Serializer round-trip.

All 275 existing Amazon.Lambda.RuntimeSupport.UnitTests continue to pass, including NativeAOTTests.EnsureNoTrimWarningsDuringPublish which dotnet publishes NativeAOTFunction and asserts zero trim/AOT warnings.

Related

This is the first of two PRs. The follow-up will use ILambdaContext.Serializer to delete ICheckpointSerializer<T>, ReflectionJsonCheckpointSerializer<T>, and the JsonSerializerContext overloads in Amazon.Lambda.DurableExecution — collapsing 8 WrapAsync overloads into 4 and removing all [RequiresUnreferencedCode]/[RequiresDynamicCode] attributes from that package.

Test plan

  • Build all touched projects in Debug & Release
  • All 275 existing Amazon.Lambda.RuntimeSupport.UnitTests pass
  • All 8 existing Amazon.Lambda.Core.Tests pass
  • 10 new tests pass
  • NativeAOTTests.EnsureNoTrimWarningsDuringPublish passes (no new trim warnings)
  • AOT publish of NativeAOTFunction completes with zero IL2026/IL3050 warnings
  • CI green

@GarrettBeatty GarrettBeatty force-pushed the GarrettBeatty/expose-serializer-on-context branch from 1080325 to 3971b6f Compare May 14, 2026 22:34
@GarrettBeatty GarrettBeatty changed the title Garrett beatty/expose serializer on context Expose ILambdaSerializer on ILambdaContext May 14, 2026
Adds an optional `Serializer` property to `ILambdaContext` (default-implemented
to `null` on net8.0+, matching the existing TenantId/TraceId pattern), and has
RuntimeSupport propagate the serializer registered with `HandlerWrapper` /
`LambdaBootstrapBuilder.Create` through `RuntimeApiClient` to the per-invocation
`LambdaContext`. User code can now reuse the configured serializer for
ad-hoc serialization without re-instantiating it.
@GarrettBeatty GarrettBeatty force-pushed the GarrettBeatty/expose-serializer-on-context branch from 3971b6f to 69f9619 Compare May 14, 2026 22:35
Copy link
Copy Markdown
Member

@normj normj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional things that have to be done to make this work:

  • UserCodeLoader is used for the class library programming model and it takes care of grabbing the ILambdaSerializer via the LambdaSerializerAttribute assembly attribute. Some of that instance needs to be communicate back to your code setting the serializer on the context.
  • Because in class library mode you might have an old Amazon.Lambda.Core that doesn't have the code changes to hold on to the ILambdaSerializer you have to make the code extra defensive. Don't change the constructor of ILambdaContext and make an internal setter. RuntimeSupport has access to internal methods. Then put the code setting the serializer in a separate method and make the calling method do a try/catch around the RuntimeSupport method for setting. You can see examples in RuntimeSupport with classes that have the "Isolated" suffix.
  • Mark the property in Amazon.Lambda.Core for getting the ILambdaSerializer as preview because it won't work correctly for class library mode till the changes are deployed to the managed runtime.

Comment thread Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs Outdated
- Doc wording: clarify that the Lambda function (not "the runtime") registers
  the serializer, and call out both registration paths (LambdaBootstrapBuilder
  for custom runtimes, [assembly: LambdaSerializer] for class-library mode).
- Mark ILambdaContext.Serializer as preview ([Experimental("AWSLAMBDA001")])
  since class-library mode requires an updated managed runtime to populate it.
- Plumb [assembly: LambdaSerializer] for class-library mode: UserCodeLoader
  exposes the constructed serializer; RuntimeSupportInitializer pushes it onto
  LambdaBootstrap inside the wrapped initializer callback.
- Defensive forward-compat: replace constructor injection with an internal
  setter on LambdaContext, and route the assignment through a new
  LambdaContextSerializerIsolated shim. The Isolated pattern (mirroring
  TraceProviderIsolated and the SnapStart helpers) defers JIT resolution of
  the new ILambdaContext.Serializer member to a single one-line method, so a
  TypeLoadException from a stale Amazon.Lambda.Core in the user's function
  can be caught at the call site without crashing the invoke loop. After the
  first failure, _disableSerializerOnContext short-circuits subsequent attempts.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a preview serializer surface to Lambda context plumbing so handlers and downstream libraries can reuse the runtime-registered ILambdaSerializer without creating a separate serializer instance.

Changes:

  • Adds ILambdaContext.Serializer as a preview API in Amazon.Lambda.Core.
  • Propagates registered serializers through HandlerWrapper, LambdaBootstrap, and class-library initialization.
  • Adds TestLambdaContext.Serializer and unit coverage for the new serializer behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs Adds preview Serializer property to the context interface.
Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs Stores serializers on serializer-based handler wrappers.
Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs Carries the serializer into each invocation context.
Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs Exposes the class-library serializer instance after initialization.
Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs Adds serializer storage on the runtime context implementation.
Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/LambdaContextSerializerIsolated.cs Adds isolated helper for setting the serializer on context.
Libraries/src/Amazon.Lambda.RuntimeSupport/RuntimeSupportInitializer.cs Wires class-library serializer initialization into bootstrap setup.
Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs Adds writable serializer property for tests.
Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs Adds RuntimeSupport serializer propagation tests.
Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs Adds TestLambdaContext serializer tests.
.autover/changes/6e13a012-1f93-4e55-90b5-d2dd480d086c.json Records minor-version changelog entries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 142 to +143
var customerSerializerInstance = GetSerializerObject(customerAssembly);
CustomerSerializerInstance = customerSerializerInstance;
/// to mirror the serializer that the Lambda runtime support library would attach
/// in production.
/// </summary>
public ILambdaSerializer Serializer { get; set; }
Comment on lines +104 to +108
public void HandlerWrapper_AllSerializerOverloads_PropagateSerializer()
{
// One sample per overload family (Func/Action × Task/non-Task × in/out × ILambdaContext)
// is enough — they share the same field-assignment line. This guards against future
// overloads being added without setting Serializer.
Comment on lines +54 to +56
/// references — the cross-version <see cref="ILambdaSerializer"/> identity is
/// not guaranteed. The Isolated shim that exposes the value via
/// <see cref="ILambdaContext.Serializer"/> handles the cross-version cast.
{
/// <summary>
/// Wrapper around the call that sets <see cref="LambdaContext.Serializer"/>.
/// The user's Lambda function may reference an older <see cref="Amazon.Lambda.Core"/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants