Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
823311e
refactor(core): enhance null safety in DefaultFeatureCollectionFactory
j-d-ha Nov 29, 2025
5027f32
refactor(core): enhance null safety in DefaultFeatureCollectionFactory
j-d-ha Nov 29, 2025
5c775ae
Merge remote-tracking branch 'origin/feature/#189-support-multiple-ma…
j-d-ha Dec 2, 2025
c63db6a
feat(core): add support for feature provider factories
j-d-ha Dec 2, 2025
1ca12fa
feat(core): add XML documentation to feature provider factories
j-d-ha Dec 2, 2025
56d37ff
refactor(core): change feature provider classes to internal
j-d-ha Dec 2, 2025
16c7e78
feat(core): extend `IFeatureCollectionFactory` with overload for cust…
j-d-ha Dec 2, 2025
f1cc520
refactor(core): convert concatenated feature providers to array in fa…
j-d-ha Dec 2, 2025
4b80f3b
feat(core): support custom feature providers in LambdaHostContextFactory
j-d-ha Dec 2, 2025
16d118e
refactor(core): remove unused `Create` method from `DefaultFeatureCol…
j-d-ha Dec 2, 2025
fb20c62
refactor(core): simplify `DefaultFeatureCollectionFactory` constructor
j-d-ha Dec 2, 2025
850d453
feat(core): enable feature providers in `MapHandlerLambdaApplicationE…
j-d-ha Dec 3, 2025
a377f9a
feat(source-generators): extend `MapHandler` template for multiple fe…
j-d-ha Dec 3, 2025
e71f283
refactor(diagnostics): remove `LH0001` rule for duplicate `MapHandler…
j-d-ha Dec 3, 2025
9d2bdc2
refactor(source-generators): simplify `Predicate` logic in `MapHandle…
j-d-ha Dec 3, 2025
aef4ea5
docs(source-generators): add README and update release tracking
j-d-ha Dec 3, 2025
9613186
feat(opentelemetry): add OpenTelemetry tracing middleware for AwsLambda
j-d-ha Dec 3, 2025
b5c899f
feat(source-generators): support multiple MapHandler invocations
j-d-ha Dec 3, 2025
1c4791e
fix(source-generators): handle diagnostics with errors before code ge…
j-d-ha Dec 3, 2025
279d30c
feat(source-generators): add feature requirements for event and respo…
j-d-ha Dec 3, 2025
08b8b88
feat(source-generators): refine feature provider registration in MapH…
j-d-ha Dec 3, 2025
a287d21
fix(opentelemetry): simplify `TraceAsync` usage in middleware
j-d-ha Dec 3, 2025
5070a26
feat(lambda-host): refactor feature provider initialization and impro…
j-d-ha Dec 3, 2025
3c00f9a
refactor(lambda-host): simplify feature provider resolution in `Lambd…
j-d-ha Dec 3, 2025
d9053c9
chore(settings): update code style and cleanup settings
j-d-ha Dec 3, 2025
268b58d
feat(opentelemetry): remove OpenTelemetry integrations for AwsLambda
j-d-ha Dec 3, 2025
b842d7a
feat(opentelemetry): add unit tests for `UseOpenTelemetryTracing` mid…
j-d-ha Dec 3, 2025
4082280
feat(source-generators): remove OpenTelemetry tracing tests and snaps…
j-d-ha Dec 3, 2025
ae20420
feat(source-generators): refine MapHandler templates and improve feat…
j-d-ha Dec 3, 2025
dcaea5c
feat(lambda-host): update handler to support request-response model
j-d-ha Dec 3, 2025
f41b988
refactor(source-generators): remove unused imports in MapHandler temp…
j-d-ha Dec 3, 2025
0c08a27
refactor(source-generators): rename MapHandler class to clarify its p…
j-d-ha Dec 3, 2025
c430bef
refactor(source-generators): remove OpenTelemetry tracing template
j-d-ha Dec 3, 2025
6f869ab
refactor(source-generators): rename extension classes for consistency
j-d-ha Dec 3, 2025
9bc882e
refactor(source-generators): remove unused import in GenericHandler t…
j-d-ha Dec 3, 2025
fbc8ffd
refactor(source-generators): reorder OnShutdown generator for consist…
j-d-ha Dec 3, 2025
06ccf78
test(lambda-host): update LambdaHostContextFactoryTests to validate f…
j-d-ha Dec 3, 2025
952cb2c
test(lambda-host): refactor DefaultFeatureCollectionFactoryTests to u…
j-d-ha Dec 3, 2025
a64e7ed
test(lambda-host): update AddLambdaHostCoreServices test to use new s…
j-d-ha Dec 3, 2025
8260830
feat(docs): add type-safe feature access guide to middleware document…
j-d-ha Dec 3, 2025
503ea6c
feat(docs): enhance OpenTelemetry middleware documentation
j-d-ha Dec 3, 2025
fead5f4
feat(docs): clarify `MapHandler` usage and runtime restrictions
j-d-ha Dec 3, 2025
aa641cb
chore(opentelemetry): add back LambdaOpenTelemetryServiceProviderExte…
j-d-ha Dec 3, 2025
ee9f5b4
refactor(core): remove unused #region directives for improved readabi…
j-d-ha Dec 3, 2025
168f008
Merge branch 'main' into feature/#189-support-multiple-map-handler
j-d-ha Dec 4, 2025
da5ba7d
refactor(lambda-host): replace ICollection with List in LambdaHostCon…
j-d-ha Dec 4, 2025
cbb85c1
test(lambda-host): add unit test for feature collection creation in L…
j-d-ha Dec 4, 2025
6c3e7ff
test(lambda-host): add unit tests for feature provider factories
j-d-ha Dec 4, 2025
2d87fee
Merge branch 'main' into feature/#189-support-multiple-map-handler
j-d-ha Dec 4, 2025
0b11052
Merge branch 'main' into feature/#189-support-multiple-map-handler
j-d-ha Dec 4, 2025
5d93cf2
docs(opentelemetry): remove redundant section on middleware flexibility
j-d-ha Dec 4, 2025
197243e
docs(middleware): improve table formatting for type-safe feature methods
j-d-ha Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions docs/features/open_telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,21 @@ internal record Response(string Message);

---

## How It Works: Source Generation and Interceptors
## How It Works: Feature-Based Middleware

`AwsLambda.Host` relies on C# source generators to avoid runtime reflection and improve performance. The `UseOpenTelemetryTracing()` method is a key part of this system.
The `UseOpenTelemetryTracing()` method adds middleware to the Lambda invocation pipeline that automatically instruments your handlers with distributed tracing.

Here's the step-by-step process:

1. The `UseOpenTelemetryTracing()` method itself is an empty placeholder.
2. At compile time, a source generator finds all calls to this method.
3. For each call it finds, it emits a C# 12 Interceptor. This interceptor replaces the empty method call with code that adds a middleware delegate to the Lambda invocation pipeline.
4. This generated middleware calls an adapter in the `AwsLambda.Host.OpenTelemetry` package, which in turn uses the official `OpenTelemetry.Instrumentation.AWSLambda` package to wrap the Lambda handler execution in a root trace span.
1. The `UseOpenTelemetryTracing()` middleware is registered in the invocation pipeline when you call the method on your Lambda application.
2. During each Lambda invocation, the middleware executes before your handler runs.
3. The middleware dynamically detects the event and response types by reading from the feature collection:
- It checks for an `IEventFeature` to get the incoming event object
- It checks for an `IResponseFeature` to get the outgoing response object
4. These event and response objects are passed to the official `OpenTelemetry.Instrumentation.AWSLambda` package, which wraps the handler execution in a root trace span.
5. The OpenTelemetry instrumentation automatically extracts trace context from supported event types (like API Gateway requests), ensuring proper distributed trace propagation across services.

This entire process happens during compilation, resulting in highly optimized code that instruments your handler without any reflection overhead at runtime.

In contrast, the shutdown helpers (`OnShutdownFlushOpenTelemetry`, `OnShutdownFlushTracer`, and `OnShutdownFlushMeter`) are regular extension methods. They execute as-is at runtime and use the registered `TracerProvider`/`MeterProvider` instances to force-flush telemetry before Lambda freezes the environment.
The shutdown helpers (`OnShutdownFlushOpenTelemetry`, `OnShutdownFlushTracer`, and `OnShutdownFlushMeter`) are regular extension methods that execute at runtime and use the registered `TracerProvider`/`MeterProvider` instances to force-flush telemetry before Lambda freezes the environment.

---

Expand All @@ -148,9 +149,13 @@ lambda.UseOpenTelemetryTracing();
// ... MapHandler, OnShutdown, etc. ...
```

This method call acts as a compile-time trigger for a source generator. The generator intercepts the call and injects middleware into the request pipeline. This middleware is responsible for creating the root trace span for each Lambda invocation.
This method registers middleware in the invocation pipeline that wraps each handler execution in a root trace span. The middleware is responsible for:

- Retrieving the event object from the `IEventFeature` in the feature collection (if present)
- Executing the handler and capturing the response from `IResponseFeature` (if present)
- Passing both the event and response to the underlying `OpenTelemetry.Instrumentation.AWSLambda` package

Under the covers, the source generator performs a critical task. It inspects the delegate you provided to `MapHandler` to determine the specific input and output types of your function (e.g., `APIGatewayProxyRequest`, `SQSEvent`). It then uses these types to generate a call to a generic helper method. This ensures that the underlying `OpenTelemetry.Instrumentation.AWSLambda` package receives a strongly-typed request object. By preserving the specific event type, the OpenTelemetry instrumentation can correctly extract context and attributes, such as trace parent headers from an API Gateway request, ensuring proper distributed trace propagation.
Because the middleware reads event and response types dynamically from the feature collection, it works seamlessly with any Lambda event source type. The OpenTelemetry instrumentation can correctly extract context and attributes from strongly-typed event objects (such as trace parent headers from an API Gateway request), ensuring proper distributed trace propagation across your services.


### Gracefully Shutdown & Cleaning Up
Expand Down
61 changes: 53 additions & 8 deletions docs/guides/handler-registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,55 @@ interface IGreetingService
}
```

- Register middleware (via `lambda.Use(...)`) before calling `MapHandler`; the handler is always the last piece of the pipeline.
- Only one handler can be mapped. If you call `MapHandler` twice, the generator emits error `LH0001` so you catch the issue before publishing.
- While you can have multiple `MapHandler` calls in your code, only one can be registered at runtime (see [Multiple MapHandler Calls](#multiple-maphandler-calls) below).
- The generated handler feeds into the normal invocation builder, so all middleware, features, and diagnostics apply equally to handlers created via `Handle` or `MapHandler`.

## Multiple MapHandler Calls

You can have multiple `MapHandler` calls in your code, but **only one handler can be registered per Lambda execution**. If more than one `MapHandler` is called at runtime, an exception will be thrown.

This design enables several useful patterns:

### Use Cases

**Library-Provided Handlers**

Libraries can expose pre-configured handlers that applications can opt into:

```csharp title="MyLibrary.cs" linenums="1"
namespace MyLibrary;

public static class OrderHandlers
{
public static void MapOrderHandler(this ILambdaInvocationBuilder app)
{
app.MapHandler(([Event] OrderRequest req, IOrderService orders) =>
orders.ProcessAsync(req)
);
}
}
```

```csharp title="Program.cs" linenums="1"
using MyLibrary;

var builder = LambdaApplication.CreateBuilder();
builder.Services.AddOrderServices(); // Library-provided services

var lambda = builder.Build();

// Use the library's handler
lambda.MapOrderHandler();

await lambda.RunAsync();
```

### Important Notes

- Only **one** `MapHandler` can execute at runtime—calling multiple will throw an `InvalidOperationException`
- Feature providers are registered when `MapHandler` is called, so ensure the registered handler matches your expected event/response types
- This pattern is designed for **conditional registration**, not for handling multiple event types in a single Lambda (which requires a custom routing solution)

## Handler Signatures and the `[Event]` Parameter

Handlers that receive an incoming payload must identify exactly one parameter with `[Event]`. The generator uses that marker to synthesize deserialization logic (JSON by default, or whatever envelope/serializer is active). If your Lambda does **not** expect input (e.g., scheduled jobs, health checks, etc.), you can omit the `[Event]` attribute entirely—just define a handler with no payload parameter and `AwsLambda.Host` skips the event binding phase.
Expand Down Expand Up @@ -133,11 +178,15 @@ lambda.MapHandler(async (
`MapHandler` is decorated as a C# 12 interceptor target. During compilation the generator:

1. Ensures the project is built with C# 11+ so interceptors are available (otherwise `LH0004`).
2. Verifies there is exactly one handler and, when a payload parameter exists, exactly one `[Event]` annotation.
2. When a payload parameter exists, verifies exactly one `[Event]` annotation is present.
3. Validates keyed service metadata so the requested key matches the DI container's capabilities (`LH0003` when the key uses an unsupported type such as arrays).
4. Emits a strongly typed `Handle` call that deserializes the payload (if any), resolves services via generated code, sets up features, and serializes the response.

At runtime the stub `MapHandler` method would throw if invoked, but the interception step guarantees that never happens. You get ahead-of-time compatible, reflection-free code with compile-time errors if the signature is invalid.
At runtime:

- The stub `MapHandler` method would throw if invoked, but the interception step guarantees that never happens
- Only one handler can be registered—calling `MapHandler` multiple times in the same execution throws an `InvalidOperationException`
- You get ahead-of-time compatible, reflection-free code with compile-time errors if the signature is invalid

## Patterns and Best Practices

Expand Down Expand Up @@ -174,10 +223,6 @@ At runtime the stub `MapHandler` method would throw if invoked, but the intercep

## Troubleshooting

**`LH0001: Multiple handlers registered`**

Make sure you call `MapHandler` only once. If you need to branch by trigger type, create separate Lambda projects or use envelope dispatching.

**`LH0002: No parameter marked with [Event]`**

Add a `[Event]` attribute when your handler accepts an input payload. This diagnostic does **not** appear for payload-less handlers because no event parameter is required in that case.
Expand Down
46 changes: 46 additions & 0 deletions docs/guides/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,52 @@ Common features:
- Handlers remain free of middleware-specific dependencies; they just work with the event/response types.
- Custom features are easy to add—register an implementation of `IFeatureProvider` and it becomes available to all middleware.

### Type-Safe Feature Access

The framework provides convenient extension methods on `ILambdaHostContext` for type-safe event and response access, simplifying the feature access pattern shown above:

```csharp title="Program.cs"
lambda.UseMiddleware(async (context, next) =>
{
// Nullable access - returns null if not found
var request = context.GetEvent<OrderRequest>();
if (request is not null)
Console.WriteLine($"Processing order {request.OrderId}");

// Try pattern - safe null checking
if (context.TryGetEvent<OrderRequest>(out var order))
{
// Use order safely without additional null checks
Console.WriteLine($"Order {order.OrderId} has {order.Items.Count} items");
}

await next(context);

// Required access - throws if not found
var response = context.GetRequiredResponse<OrderResponse>();
Console.WriteLine($"Status: {response.Status}");
});
```

**Available Methods:**

| Method | Description | Returns |
|----------------------------|-----------------------------------------|------------------------------------------|
| `GetEvent<T>()` | Returns event or `null` if not found | `T?` |
| `GetResponse<T>()` | Returns response or `null` if not found | `T?` |
| `TryGetEvent<T>(out T)` | Try-pattern for safe event access | `bool` |
| `TryGetResponse<T>(out T)` | Try-pattern for safe response access | `bool` |
| `GetRequiredEvent<T>()` | Returns event or throws | `T` (throws `InvalidOperationException`) |
| `GetRequiredResponse<T>()` | Returns response or throws | `T` (throws `InvalidOperationException`) |

**When to use each:**

- **Nullable methods** (`GetEvent<T>()`) – When the event/response might not exist and you'll handle null gracefully
- **Try pattern** (`TryGetEvent<T>()`) – When you want explicit null checking without additional conditionals
- **Required methods** (`GetRequiredEvent<T>()`) – When the event/response must exist and missing it is an error condition

These methods are equivalent to calling `context.Features.Get<IEventFeature<T>>()` and accessing the event/response, but provide cleaner syntax and better null-safety annotations.

### Feature Providers in Practice

When `context.Features.Get<T>()` runs, `AwsLambda.Host` walks through every registered `IFeatureProvider`
Expand Down
13 changes: 8 additions & 5 deletions examples/AwsLambda.Host.Example.HelloWorld/Program.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
#region

using System;
using AwsLambda.Host.Builder;
using Microsoft.Extensions.Hosting;

#endregion

// Create the application builder
var builder = LambdaApplication.CreateBuilder();

// Build the Lambda application
var lambda = builder.Build();

// Map your handler - the event is automatically injected
lambda.MapHandler(([Event] string name) => $"Hello {name}!");
lambda.MapHandler(
([Event] Request request) => new Response($"Hello {request.Name}!", DateTime.UtcNow)
);

// Run the Lambda
await lambda.RunAsync();

internal record Response(string Message, DateTime TimestampUtc);

internal record Request(string Name);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace AwsLambda.Host.Core;

/// <summary>Creates feature providers for Lambda event deserialization.</summary>
/// <remarks>
/// <para>
/// <see cref="IEventFeatureProviderFactory" /> creates <see cref="IFeatureProvider" />
/// instances for specific event types during Lambda invocations. The factory enables
/// lazy registration of event feature providers. This factory is registered automatically
/// at startup.
/// </para>
/// </remarks>
public interface IEventFeatureProviderFactory
{
/// <summary>Creates a feature provider for the specified event type.</summary>
/// <typeparam name="T">The type of event object to create a provider for.</typeparam>
/// <returns>
/// An <see cref="IFeatureProvider" /> that can create <see cref="IEventFeature" />
/// instances for the specified type.
/// </returns>
IFeatureProvider Create<T>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace AwsLambda.Host.Core;

/// <summary>Creates feature providers for Lambda response serialization.</summary>
/// <remarks>
/// <para>
/// <see cref="IResponseFeatureProviderFactory" /> creates <see cref="IFeatureProvider" />
/// instances for specific response types during Lambda invocations. The factory enables
/// lazy registration of response feature providers. This factory is registered automatically
/// at startup.
/// </para>
/// </remarks>
public interface IResponseFeatureProviderFactory
{
/// <summary>Creates a feature provider for the specified response type.</summary>
/// <typeparam name="T">The type of response object to create a provider for.</typeparam>
/// <returns>
/// An <see cref="IFeatureProvider" /> that can create <see cref="IResponseFeature" />
/// instances for the specified type.
/// </returns>
IFeatureProvider Create<T>();
}
12 changes: 12 additions & 0 deletions src/AwsLambda.Host.OpenTelemetry/LambdaOpenTelemetryAdapters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public static class LambdaOpenTelemetryServiceProviderExtensions
/// <typeparamref name="TResponse" />, or if a <see cref="TracerProvider" /> instance is not
/// registered in the dependency injection container.
/// </exception>
[Obsolete(
"This method will be removed in v2.0.0 Use UseOpenTelemetryTracing directly instead."
)]
public Func<LambdaInvocationDelegate, LambdaInvocationDelegate> GetOpenTelemetryTracer<
TEvent,
TResponse
Expand Down Expand Up @@ -107,6 +110,9 @@ is not TResponse responseT
/// Thrown if the context response is not of type
/// <typeparamref name="TResponse" />.
/// </exception>
[Obsolete(
"This method will be removed in v2.0.0 Use UseOpenTelemetryTracing directly instead."
)]
public Func<
LambdaInvocationDelegate,
LambdaInvocationDelegate
Expand Down Expand Up @@ -154,6 +160,9 @@ is not TResponse responseT
/// Thrown if the context event is not of type
/// <typeparamref name="TEvent" />.
/// </exception>
[Obsolete(
"This method will be removed in v2.0.0 Use UseOpenTelemetryTracing directly instead."
)]
public Func<
LambdaInvocationDelegate,
LambdaInvocationDelegate
Expand Down Expand Up @@ -194,6 +203,9 @@ async Task (_, _) => await next(context),
/// <inheritdoc cref="GetOpenTelemetryTracer{TEvent,TResponse}" path="/remarks" /> Neither
/// event nor response types are relevant or known when using this overload.
/// </remarks>
[Obsolete(
"This method will be removed in v2.0.0 Use UseOpenTelemetryTracing directly instead."
)]
public Func<
LambdaInvocationDelegate,
LambdaInvocationDelegate
Expand Down
Loading
Loading