Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f2d637c
feat(minimal-lambda): add `LifetimeStopwatch` to track application li…
j-d-ha Dec 16, 2025
fb0f48f
feat(minimal-lambda): add `ILambdaLifecycleContext` interface for Lam…
j-d-ha Dec 16, 2025
5fda15e
feat(minimal-lambda): implement factory and context for Lambda lifecycle
j-d-ha Dec 16, 2025
db7b8ea
test(minimal-lambda): add unit tests for lifecycle context and factory
j-d-ha Dec 16, 2025
8a74825
feat(minimal-lambda): update Lambda initialization for enhanced lifec…
j-d-ha Dec 16, 2025
c7f14d2
feat(minimal-lambda): enhance lifecycle context and Lambda output for…
j-d-ha Dec 16, 2025
2b141cd
feat(source-generators): refactor handler cast generation logic
j-d-ha Dec 16, 2025
807f227
fix(unit-tests): make `OnInit` handler argument optional in tests
j-d-ha Dec 16, 2025
5ed3532
feat(source-generators): refactor `OnInit` handling with `ILambdaLife…
j-d-ha Dec 16, 2025
4538693
feat(minimal-lambda): unify `LambdaInitDelegate` with `ILambdaLifecyc…
j-d-ha Dec 16, 2025
838d783
refactor(minimal-lambda): replace `LambdaInitDelegate2` with `LambdaI…
j-d-ha Dec 16, 2025
8332593
feat(minimal-lambda): introduce `ILifetimeStopwatch` and refactor lif…
j-d-ha Dec 16, 2025
ed6b8c2
refactor(unit-tests): simplify `CreateHostWithServices` using `Lambda…
j-d-ha Dec 16, 2025
e842f97
feat(minimal-lambda): enhance shutdown handling with `ILambdaLifecycl…
j-d-ha Dec 16, 2025
adf0abc
feat(minimal-lambda): update `OnInit` and `OnShutdown` for improved l…
j-d-ha Dec 16, 2025
6b19eb6
feat(minimal-lambda): enhance `OnShutdown` with context factory and s…
j-d-ha Dec 16, 2025
657f88b
feat(opentelemetry): update shutdown handlers and add project references
j-d-ha Dec 17, 2025
3ddfefb
feat(source-generators): update `OnShutdown` syntax provider to use `…
j-d-ha Dec 17, 2025
72aa4cf
refactor(unit-tests): streamline `LambdaOnShutdownBuilder` test methods
j-d-ha Dec 17, 2025
5e14df5
test(unit-tests): add `ILambdaLifecycleContextFactory` to shutdown fa…
j-d-ha Dec 17, 2025
3a4f070
refactor(unit-tests): simplify `LambdaShutdownDelegate` usage in shut…
j-d-ha Dec 17, 2025
cc75ba1
refactor(unit-tests): remove unused parameters from `LambdaShutdownDe…
j-d-ha Dec 17, 2025
5a101cd
refactor(unit-tests): replace `IServiceProvider` with `ILambdaLifecyc…
j-d-ha Dec 17, 2025
27d6ea2
refactor(unit-tests): update `LambdaShutdownDelegate` in shutdown tests
j-d-ha Dec 17, 2025
ee30de2
refactor(source-generators): remove commented location hints from tem…
j-d-ha Dec 17, 2025
5ae7965
feat(minimal-lambda): add shared properties support for Init and Shut…
j-d-ha Dec 17, 2025
e6894de
feat(docs): improve lifecycle management and DI guides
j-d-ha Dec 17, 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
11 changes: 11 additions & 0 deletions docs/getting-started/core-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ lambda.OnShutdown(async (
});
```

### Lifecycle Context Access

Both OnInit and OnShutdown handlers support rich dependency injection patterns:

- **Inject services directly**: `(ICache cache, ILogger logger, CancellationToken ct)` – Recommended for most scenarios
- **Access AWS metadata**: `(ILambdaLifecycleContext context)` – When you need environment information like function name, region, memory size, or initialization type
- **Combine both**: `(ILambdaLifecycleContext context, ICache cache)` – Mix context access with direct service injection
- **Use keyed services**: `([FromKeyedServices("key")] IService service)` – Resolve services registered with specific keys

The `ILambdaLifecycleContext` interface provides AWS environment metadata, a `Properties` dictionary for sharing state between handlers, and access to the scoped `ServiceProvider`. See [Lifecycle Management](../guides/lifecycle-management.md) for detailed patterns and examples.

### Lifecycle Timeline

```mermaid
Expand Down
54 changes: 47 additions & 7 deletions docs/guides/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,23 +74,63 @@ If your handler doesn't need the Lambda payload, omit the `[FromEvent]` paramete
remaining time when creating the token.
- Always pass it down to outbound SDK calls and database queries so you can stop work cleanly.

## Middleware and Lifecycle Hooks
## Middleware and Lifecycle Hooks: Source-Generated DI

- Middleware receives the invocation scope via the `ILambdaHostContext` argument. Resolve services with
`context.ServiceProvider` or create reusable middleware classes with constructor injection.
- `OnInit` and `OnShutdown` handlers run outside the normal invocation scope but each one executes
inside its own scoped service provider so you can warm caches, seed connections, or flush telemetry
without leaking per-invocation services.
- `OnInit` and `OnShutdown` handlers now use the same source-generated dependency injection as your main
handlers. Each executes inside its own scoped service provider so you can warm caches, seed connections,
or flush telemetry without leaking per-invocation services.

```csharp title="OnInit"
lambda.OnInit(async (services, ct) =>
OnInit and OnShutdown handlers support multiple dependency injection patterns:

```csharp title="Pattern 1: Direct DI (Recommended)"
lambda.OnInit(async (ICache cache, ILogger<Program> logger, CancellationToken ct) =>
{
var cache = services.GetRequiredService<ICache>();
logger.LogInformation("Warming cache during cold start");
await cache.WarmUpAsync(ct);
return true;
});
```

Each handler runs in its own scoped service provider, so you can safely resolve scoped services even
outside the invocation pipeline.

```csharp title="Pattern 2: Using ILambdaLifecycleContext"
lambda.OnInit(async (ILambdaLifecycleContext context, ICache cache) =>
{
var logger = context.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogInformation(
"Init type: {Type}, Function: {Name}, Memory: {MB}MB",
context.InitializationType,
context.FunctionName,
context.FunctionMemorySize
);

await cache.WarmUpAsync(context.CancellationToken);
return true;
});
```

Use `ILambdaLifecycleContext` when you need AWS environment metadata (region, function name, memory size,
initialization type) or want to share data between handlers via the `Properties` dictionary.

```csharp title="Pattern 3: Keyed Services"
builder.Services.AddKeyedSingleton<ICache, RedisCache>("redis");
builder.Services.AddKeyedSingleton<ICache, MemoryCache>("memory");

lambda.OnInit(async (
[FromKeyedServices("redis")] ICache primaryCache,
[FromKeyedServices("memory")] ICache fallbackCache,
CancellationToken ct
) =>
{
await primaryCache.WarmUpAsync(ct);
await fallbackCache.WarmUpAsync(ct);
return true;
});
```

## Configuration and Options

Use the standard options pattern for configuration:
Expand Down
102 changes: 96 additions & 6 deletions docs/guides/lifecycle-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,112 @@ Both handlers are awaited simultaneously. Keep shutdown work smallβ€”only the re

OnInit and OnShutdown handlers support the same source-generated dependency injection experience as middleware and handlers:

- Request only what you need. The generated delegate resolves typed parameters (services, keyed services, `ILambdaHostContext`, `CancellationToken`, etc.).
- Request only what you need. The generated delegate resolves typed parameters (services, keyed services, `ILambdaLifecycleContext`, `CancellationToken`, etc.).
- `MinimalLambda` creates a new `IServiceScope` for every handler invocation, ensuring scoped services (database units of work, caches) are isolated even though you are outside the invocation pipeline.
- The `IServiceProvider` parameter gives you direct access to the scope for manual resolution when needed.
- Inject `ILambdaLifecycleContext` when you need AWS environment metadata or want to share state via the `Properties` dictionary. For simple scenarios, inject services directly.

```csharp title="Program.cs - Keyed Services"
builder.Services.AddKeyedSingleton<IMyClient, PrimaryClient>("primary");
builder.Services.AddKeyedSingleton<IMyClient, SecondaryClient>("secondary");

```csharp title="Program.cs"
lambda.OnInit(async (
IServiceProvider scope,
KeyedService<MyClient>("primary"),
[FromKeyedServices("primary")] IMyClient primaryClient,
[FromKeyedServices("secondary")] IMyClient secondaryClient,
CancellationToken ct
) =>
{
await MyWarmupAsync(scope, MyClient, ct);
await primaryClient.WarmupAsync(ct);
await secondaryClient.WarmupAsync(ct);
return true;
});
```

## Accessing AWS Environment Information

When you need AWS environment metadata during initialization or shutdown, inject `ILambdaLifecycleContext`. This interface provides rich context information about the Lambda execution environment beyond what's available in the standard invocation pipeline.

### Using ILambdaLifecycleContext

```csharp title="Program.cs - AWS Environment Metadata"
lambda.OnInit(async (ILambdaLifecycleContext context, ILogger<Program> logger) =>
{
logger.LogInformation(
"Initializing {FunctionName} v{Version} in {Region}",
context.FunctionName,
context.FunctionVersion,
context.Region
);

logger.LogInformation(
"Memory: {Memory}MB, Init type: {InitType}, Elapsed: {Elapsed}ms",
context.FunctionMemorySize,
context.InitializationType,
context.ElapsedTime.TotalMilliseconds
);

return true;
});
```

### Sharing State via Properties Dictionary

The `Properties` dictionary lets you share data between OnInit handlers or pass information from initialization to your invocation handlers.

```csharp title="Program.cs - State Sharing"
lambda.OnInit(async (ILambdaLifecycleContext context) =>
{
var initStartTime = DateTimeOffset.UtcNow;
context.Properties["InitStartTime"] = initStartTime;
context.Properties["EnvironmentType"] = context.InitializationType;

return true;
});

lambda.OnInit(async (ILambdaLifecycleContext context, ILogger<Program> logger) =>
{
if (context.Properties.TryGetValue("InitStartTime", out var startTime))
{
logger.LogInformation("First handler started at {Time}", startTime);
}

return true;
});
```

The `Properties` dictionary is backed by a thread-safe `ConcurrentDictionary<string, object?>` and is shared across all OnInit handlers. Properties set during OnInit are also available to invocation handlers via `ILambdaHostContext.Properties`.

### Available Context Properties

`ILambdaLifecycleContext` exposes the following properties:

| Property | Type | Description |
|----------|------|-------------|
| `CancellationToken` | `CancellationToken` | Signals cancellation when `InitTimeout` (OnInit) or `ShutdownDuration - ShutdownDurationBuffer` (OnShutdown) elapses, or when SIGTERM is received |
| `ServiceProvider` | `IServiceProvider` | The scoped service container for this handler invocation. Use for manual service resolution when direct injection isn't sufficient |
| `Properties` | `IDictionary<string, object?>` | Thread-safe dictionary for sharing data between handlers or from OnInit to invocation handlers |
| `ElapsedTime` | `TimeSpan` | Time elapsed since the Lambda execution environment started |
| `Region` | `string?` | AWS region where the function is running (from `AWS_REGION` env var) |
| `ExecutionEnvironment` | `string?` | Runtime identifier like `AWS_Lambda_dotnet8` (from `AWS_EXECUTION_ENV` env var) |
| `FunctionName` | `string?` | Name of the Lambda function (from `AWS_LAMBDA_FUNCTION_NAME` env var) |
| `FunctionMemorySize` | `int?` | Memory allocated to the function in MB (from `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` env var) |
| `FunctionVersion` | `string?` | Version of the function being executed (from `AWS_LAMBDA_FUNCTION_VERSION` env var) |
| `InitializationType` | `string?` | Type of initialization: `on-demand`, `provisioned-concurrency`, `snap-start`, or `lambda-managed-instances` (from `AWS_LAMBDA_INITIALIZATION_TYPE` env var) |
| `LogGroupName` | `string?` | CloudWatch Logs group name (from `AWS_LAMBDA_LOG_GROUP_NAME` env var, not available in SnapStart) |
| `LogStreamName` | `string?` | CloudWatch Logs stream name (from `AWS_LAMBDA_LOG_STREAM_NAME` env var, not available in SnapStart) |
| `TaskRoot` | `string?` | Path to the Lambda function code (from `LAMBDA_TASK_ROOT` env var) |

All AWS environment properties are nullable because they depend on environment variables set by the Lambda runtime. Most are available during normal execution, but some (like `LogGroupName` and `LogStreamName`) are unavailable in SnapStart functions.

### When to Use ILambdaLifecycleContext

Use `ILambdaLifecycleContext` when you need:

- **AWS environment metadata** – Access region, function name, memory size, or initialization type for logging, metrics, or conditional logic based on the execution environment
- **State sharing** – Pass data between OnInit handlers or from OnInit to invocation handlers via the `Properties` dictionary
- **Manual service resolution** – Access `ServiceProvider` directly for advanced scenarios where parameter injection isn't sufficient

For simple dependency injection scenarios, prefer injecting services directly as parameters instead of using `ILambdaLifecycleContext.ServiceProvider`.

## Configuring Lifecycle Behavior

Use `ConfigureLambdaHostOptions` to shape lifecycle behavior centrally or bind the same settings from configuration:
Expand Down
22 changes: 11 additions & 11 deletions examples/MinimalLambda.Example.Lifecycle/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MinimalLambda.Builder;

var builder = LambdaApplication.CreateBuilder();
Expand All @@ -20,22 +21,21 @@

lambda.UseClearLambdaOutputFormatting();

lambda.OnInit(
Task<bool> (IServiceCollection services, CancellationToken cancellationToken) =>
{
Console.WriteLine("Initializing...");
return Task.FromResult(true);
}
);
lambda.OnInit(() =>
{
Console.WriteLine("Initializing...");
return Task.FromResult(true);
});

lambda.OnInit(
async Task<bool> (IServiceCollection services, CancellationToken cancellationToken) =>
async Task<bool> (ILogger<Program> logger, CancellationToken cancellationToken) =>
{
var stopwatch = Stopwatch.StartNew();
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine(
$"Waiting for init to timeout. {stopwatch.ElapsedMilliseconds}ms elapsed"
logger.LogInformation(
"Waiting for init to timeout. {ElapsedMilliseconds}ms elapsed",
stopwatch.ElapsedMilliseconds
);
try
{
Expand All @@ -58,7 +58,7 @@ async Task<bool> (IServiceCollection services, CancellationToken cancellationTok
});

lambda.OnShutdown(
(IServiceCollection services, CancellationToken token) =>
(CancellationToken token) =>
{
Console.WriteLine("Shutting down...");
return Task.CompletedTask;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"AWS_LAMBDA_RUNTIME_API": "localhost:5050",
"DOTNET_ENVIRONMENT": "Development",
"AWS_LAMBDA_LOG_FORMAT": "JSON",
"AWS_LAMBDA_LOG_LEVEL": "DEBUG"
"AWS_LAMBDA_LOG_LEVEL": "DEBUG",
"AWS_LAMBDA_FUNCTION_NAME": "MinimalLambda.Example.Lifecycle"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Concurrent;

namespace MinimalLambda.Builder;

/// <summary>Builder for composing Lambda init handlers that execute during the Init phase.</summary>
Expand All @@ -18,6 +20,9 @@ public interface ILambdaOnInitBuilder
/// <summary>Gets the read-only list of registered Init handlers.</summary>
IReadOnlyList<LambdaInitDelegate> InitHandlers { get; }

/// <summary>Gets a dictionary for storing state that is shared between handlers.</summary>
ConcurrentDictionary<string, object?> Properties { get; }

/// <summary>Gets the service provider for dependency injection.</summary>
IServiceProvider Services { get; }

Expand All @@ -39,5 +44,5 @@ public interface ILambdaOnInitBuilder
/// A function that accepts a <see cref="CancellationToken" /> and executes all registered
/// handlers concurrently. Ready for the Lambda Init phase.
/// </returns>
Func<CancellationToken, Task<bool>> Build();
Func<CancellationToken, Task<bool>>? Build();
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Concurrent;

namespace MinimalLambda.Builder;

/// <summary>Builder for composing Lambda shutdown handlers that execute during the Shutdown phase.</summary>
Expand All @@ -17,6 +19,9 @@ public interface ILambdaOnShutdownBuilder
/// <summary>Gets the service provider for dependency injection.</summary>
IServiceProvider Services { get; }

/// <summary>Gets a dictionary for storing state that is shared between handlers.</summary>
ConcurrentDictionary<string, object?> Properties { get; }

/// <summary>Gets the read-only list of registered shutdown handlers.</summary>
IReadOnlyList<LambdaShutdownDelegate> ShutdownHandlers { get; }

Expand Down
Loading
Loading