Expose ILambdaSerializer on ILambdaContext#2378
Conversation
1080325 to
3971b6f
Compare
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.
3971b6f to
69f9619
Compare
normj
left a comment
There was a problem hiding this comment.
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
LambdaSerializerAttributeassembly 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.
- 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.
There was a problem hiding this comment.
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.Serializeras a preview API inAmazon.Lambda.Core. - Propagates registered serializers through
HandlerWrapper,LambdaBootstrap, and class-library initialization. - Adds
TestLambdaContext.Serializerand 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.
- LambdaContextSerializerIsolated: replace cref pointing at the
Amazon.Lambda.Core namespace with plain text so XML doc generation
doesn't trip the warnings-as-errors build.
- UserCodeLoader.CustomerSerializerInstance: rewrite the doc comment to
match the implementation — the cross-version cast happens in
RuntimeSupportInitializer, not in the Isolated shim.
- TestLambdaContext.Serializer: apply [Experimental("AWSLAMBDA001")] so
the test-utility surface mirrors the preview opt-in on
ILambdaContext.Serializer.
- Tests: rename HandlerWrapper_AllSerializerOverloads_PropagateSerializer
to HandlerWrapper_SerializerOverloadFamilies_PropagateSerializer to
reflect that it samples one overload per family rather than asserting
every overload signature. Add two tests covering the class-library
serializer path: UserCodeLoader.Init resolving an [assembly:
LambdaSerializer] attribute, and LambdaBootstrap.SetSerializer flowing
that instance to ILambdaContext.Serializer through the invoke loop.
|
i also deployed a custom container to test. Serializer-on-context: real Lambda deploymentEnd-to-end verification of Inside the deployed LambdaTwo functions, one container image at The image
What
|
| Function | Image config CMD | Env |
|---|---|---|
serializer-verify-classlib |
HandlerLib::HandlerLib.Function::Handler (Lambda passes this as argv[0], which sets _HANDLER) |
SERIALIZER_TEST_MODE=classlib, DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true |
serializer-verify-custom |
unused (custom mode ignores _HANDLER but provided.al2023 requires something) |
SERIALIZER_TEST_MODE=custom, DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true |
Both: provided.al2023, x86_64, 512 MB, 30 s timeout, IAM role
serializer-verify-exec with just AWSLambdaBasicExecutionRole.
The actual responses
Region us-east-1, payload {"Name":"hello","Value":42}:
serializer-verify-classlib:
{"Mode":"classlib",
"SerializerType":"...SourceGeneratorLambdaJsonSerializer`1[[HandlerLib.SerializerContext, HandlerLib, ...]]",
"Echoed":"hello",
"RoundTripOk":true}
serializer-verify-custom:
{"Mode":"custom",
"SerializerType":"...SourceGeneratorLambdaJsonSerializer`1[[SerializerVerifyBootstrap.CustomCtx, bootstrap, ...]]",
"Echoed":"hello",
"RoundTripOk":true}
The two SerializerTypes being different (and matching the type
each path registered) is the part that proves the wiring: classlib
found the type from HandlerLib's [assembly: LambdaSerializer]
attribute via reflection, custom found the one constructed inline in
Program.Main. RoundTripOk:true proves context.Serializer isn't
just non-null but actually functional in-handler.v to the bootstrap, which
in turn populates _HANDLER.
4. provided.al2023 requires a CMD even in custom-runtime mode.
Set Command=[unused] on the custom-mode function.
| "Name": "Amazon.Lambda.Core", | ||
| "Type": "Minor", | ||
| "ChangelogMessages": [ | ||
| "Add ILambdaSerializer Serializer property to ILambdaContext (default-implemented to null on net8.0+) so user code can access the serializer registered with the runtime" |
There was a problem hiding this comment.
Call out the property is preview. There will be another release where we take the preview flag off once the managed runtime has been deployed.
| /// <see cref="ILambdaContext"/>, allowing user code to reuse it. | ||
| /// Null for handlers that don't take a typed input/output. | ||
| /// </summary> | ||
| public ILambdaSerializer Serializer { get; private set; } |
There was a problem hiding this comment.
This should be internal. No reason to expand our public API contract HandlerWrapper.
Summary
Adds a preview
Serializerproperty toILambdaContextso user code can reuse theILambdaSerializerthat the Lambda function registered with the runtime — either viaLambdaBootstrapBuilder.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.Motivation
Today the
ILambdaSerializerregistered by the function lives entirely insideHandlerWrapper's closure (custom runtime) or insideUserCodeLoader._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:ILambdaSerializeras an explicit constructor/method argument, duplicating registration.new DefaultLambdaJsonSerializer()), which diverges from the function's actual configuration and silently breaks AOT scenarios.Exposing the registered serializer via
ILambdaContextremoves the duplication and lets libraries inherit the function's AOT-aware serializer choice automatically.Design
Amazon.Lambda.CoreAdds
ILambdaSerializer Serializer { get { return null; } }toILambdaContext, inside the existing#if NET8_0_OR_GREATERblock, matching the precedent set byTenantIdandTraceId.ILambdaContext.netstandard2.0consumers see no change.[Experimental("AWSLAMBDA001")]so callers explicitly opt in (#pragma warning disable AWSLAMBDA001).Amazon.Lambda.RuntimeSupportTwo registration paths funnel into the same per-invocation setter:
Path 1 — custom runtime:
HandlerWrapper.Serializerexposes the serializer the user passed toGetHandlerWrapper(handler, serializer). All 20 serializer-taking overloads populate this field.LambdaBootstrapconstructors that take aHandlerWrapperforwardhandlerWrapper.Serializerinto a private_serializerfield.Path 2 — class-library mode:
UserCodeLoader.CustomerSerializerInstance(new) exposes the serializer constructed from[assembly: LambdaSerializer].RuntimeSupportInitializerwrapsUserCodeInitializer.InitializeAsyncso that, after init runs, it callsbootstrap.SetSerializer(userCodeLoader.CustomerSerializerInstance). This is the only place the new internalLambdaBootstrap.SetSerializersetter is intended to be used.Common per-invocation hookup:
LambdaBootstrap.InvokeOnceAsynccallsSetSerializerOnContext(impl)afterGetNextInvocationAsyncreturns the per-invocationLambdaContext.LambdaContext.Serializeris a public read-only property with aninternalsetter ({ get; internal set; }). Constructor injection was deliberately avoided soLambdaContextcan still be constructed by older RuntimeSupport callers without signature breakage.Amazon.Lambda.TestUtilitiesTestLambdaContext.Serializeris a writable property so tests can mirror the production wiring.Usage
From an annotated function
From a custom runtime
From a downstream library
Compatibility
Amazon.Lambda.Core— Minor version bump. Additive; default-implemented interface member only available on net8.0+.Amazon.Lambda.RuntimeSupport— Minor version bump. New publicHandlerWrapper.Serializerproperty; new internalLambdaBootstrap.SetSerializer; newLambdaContextSerializerIsolatedshim. No removed or changed signatures.Amazon.Lambda.TestUtilities— Minor version bump. New publicTestLambdaContext.Serializerproperty.No removed APIs. No changed signatures.
netstandard2.0consumers see no change. Functions referencing an olderAmazon.Lambda.Corecontinue to run; they just seeILambdaContext.Serializer == nullbecause the Isolated shim catches theTypeLoadExceptionfrom the missing interface member.Tests
Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/.../LambdaContextSerializerTests.cs— 8 tests covering:LambdaContext.Serializerdefaults tonullwhen not set.LambdaContextSerializerIsolated.TrySetSerializerpopulates the property.LambdaContextSerializerIsolated.TrySetSerializeris null-tolerant on the context argument.HandlerWrapper.Serializeris populated for typed-input/output overloads.HandlerWrapper.Serializerisnullfor raw-stream overloads.LambdaBootstrap.InvokeOnceAsync: a handler invoked through the wrapper seescontext.Serializer == registered serializer.context.Serializerstaysnull.Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs— 2 tests covering theTestLambdaContext.Serializerround-trip.All 275 existing
Amazon.Lambda.RuntimeSupport.UnitTestscontinue to pass, includingNativeAOTTests.EnsureNoTrimWarningsDuringPublishwhichdotnet publishesNativeAOTFunctionand asserts zero trim/AOT warnings.Related
This is the first of two PRs. The follow-up will use
ILambdaContext.Serializerto deleteICheckpointSerializer<T>,ReflectionJsonCheckpointSerializer<T>, and theJsonSerializerContextoverloads inAmazon.Lambda.DurableExecution— collapsing 8WrapAsyncoverloads into 4 and removing all[RequiresUnreferencedCode]/[RequiresDynamicCode]attributes from that package.Test plan
Amazon.Lambda.RuntimeSupport.UnitTestspassAmazon.Lambda.Core.TestspassNativeAOTTests.EnsureNoTrimWarningsDuringPublishpasses (no new trim warnings)NativeAOTFunctioncompletes with zero IL2026/IL3050 warnings