Dependency Injection¶
aws-lambda-host uses the same dependency injection container as ASP.NET Core
(Microsoft.Extensions.DependencyInjection). If you're new to DI in .NET, start with the
official documentation
and then come back for Lambda-specific guidance. This guide focuses on what changes (and what stays the
same) when you run inside AWS Lambda.
How the Container Is Created¶
var builder = LambdaApplication.CreateBuilder();
builder.Services.AddSingleton<ICache, MemoryCache>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddTransient<IValidator<OrderRequest>, OrderValidator>();
var lambda = builder.Build();
builder.Servicesis the sameIServiceCollectionyou use everywhere else in .NET.- All registrations must happen before
builder.Build(). - Keep supporting types (records, services, options classes) at the bottom of
Program.cs; keep the pipeline (DI, middleware, handlers, run) at the top so cold-start work stays easy to read.
Service Lifetimes in Lambda¶
Lambda containers live across multiple invocations. Map the standard lifetimes to Lambda's lifecycle:
| Lifetime | When it's created | When it's disposed | Use for |
|---|---|---|---|
| Singleton | During OnInit (first cold start) | When the execution environment shuts down | HttpClient, caches, telemetry, config |
| Scoped | Once per invocation | After the invocation completes | DbContext, repositories, per-request state |
| Transient | Every time it's requested | After the requesting scope is disposed | Lightweight helpers, pure functions |
Tips:
- Scoped services are the default choice for anything that shouldn't leak state between invocations.
- Transients work the same as in ASP.NET Core, but prefer Scoped unless you truly need a new instance every time a constructor runs.
Invocation Scope and ILambdaHostContext¶
Every invocation gets its own scope. You can access it via the ILambdaHostContext and it is shared
across middleware and handlers:
lambda.MapHandler(async (
[Event] OrderRequest request,
IOrderService orders, // scoped service
ILambdaHostContext context, // framework context
CancellationToken cancellation // host-managed token
) =>
{
context.Items["RequestId"] = request.Id;
return await orders.ProcessAsync(request, cancellation);
});
ILambdaHostContext exposes:
ServiceProvider– the scoped service provider for the invocationCancellationToken– automatically linked to Lambda remaining timeItems– per-invocation storage shared by middleware/handlersProperties– cross-invocation storage backed by the singleton containerFeatures– typed feature collections (advanced scenarios)
If your handler doesn't need the Lambda payload, omit the [Event] parameter entirely and inject only services.
Cancellation buffers
The cancellation token fires slightly before AWS kills the process:
- The runtime subtracts
LambdaHostOptions.InvocationCancellationBuffer(default 3s) from the 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 receives the invocation scope via the
ILambdaHostContextargument. Resolve services withcontext.ServiceProvideror create reusable middleware classes with constructor injection. OnInitandOnShutdownhandlers 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.
lambda.OnInit(async (services, ct) =>
{
var cache = services.GetRequiredService<ICache>();
await cache.WarmUpAsync(ct);
return true;
});
Configuration and Options¶
Use the standard options pattern for configuration:
builder.Services.Configure<OrderProcessingOptions>(
builder.Configuration.GetSection("OrderProcessing"));
Prefer IOptions<T> inside handlers/services so the value is captured once per cold start. Snapshot/Monitor
variants work but rarely matter in Lambda because configuration usually ships with the deployment package.
Patterns That Work Well¶
- Constructor injection everywhere – middleware, handlers, lifecycle hooks can all resolve services directly. Avoid service locator patterns unless you truly need dynamic lookups.
- Decorator pattern – use
builder.Services.Decorate<TService, TDecorator>()(from Scrutor) to add caching, logging, or retry behavior without touching core services. - Keyed services – register multiple implementations with
AddKeyed{Lifetime}and inject the one you need via[FromKeyedServices].
Keyed Services in Practice¶
builder.Services.AddKeyedSingleton<INotifier, EmailNotifier>("email");
builder.Services.AddKeyedSingleton<INotifier, SmsNotifier>("sms");
lambda.MapHandler((
[Event] Order order,
[FromKeyedServices("sms")] INotifier notifier
) => notifier.NotifyAsync(order));
- Keys can be strings, enums, numeric types, or even
Typeinstances. - Optional services are supported by making the parameter nullable.
- The generated code throws a descriptive exception if the service provider doesn't support keyed services (e.g., if you run on an older DI container).
Host-Specific Pitfalls¶
| Pitfall | Impact | Fix |
|---|---|---|
| Singleton depends on a scoped service | Scoped instance from first invocation leaks forever | Inject IServiceProvider, create a scope, resolve the scoped service inside the method |
| Storing scoped services in singletons | ObjectDisposedException on later invocations |
Keep scoped dependencies scoped; pass data instead of services |
| Over-injecting handlers | Hard-to-test functions with 8+ services | Move orchestration into services; keep handlers thin |
| Forgetting cancellation tokens | Lambda kills the environment mid-work | Always inject CancellationToken and pass it down |
Key Takeaways¶
- Register everything before
builder.Build()so the container is ready for cold starts. - Map lifetimes to the Lambda lifecycle: singleton for shared resources, scoped for per-invocation work, transient only when necessary.
- Always pass the host-provided
CancellationToken; adjustInvocationCancellationBufferwhen you need more time to wind down. - Prefer constructor injection in handlers/middleware/lifecycle hooks—avoid service locator patterns.
- Use the options pattern for config and keyed services for multiple implementations.
- For fundamentals, refer back to the official DI docs.
With these patterns, AwsLambda.Host feels just like ASP.NET Core, but tuned for the Lambda lifecycle.