From 0a7ffdd1ee4f5dbdcccfa69be0a62258a47dec1d Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 21 Dec 2025 13:23:57 -0500 Subject: [PATCH 1/4] feat(middleware): add support for factory-based middleware creation - Introduced `ILambdaMiddlewareFactory` for custom middleware construction. - Added `UseMiddleware()` extension method in `MiddlewareLambdaApplicationExtensions`. - Enhanced DI container integration for resolving middleware factories. - Included proper handling and disposal for `IAsyncDisposable` and `IDisposable` middlewares. --- .../Core/ILambdaMiddlewareFactory.cs | 16 ++++++ .../MiddlewareLambdaApplicationExtensions.cs | 57 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/MinimalLambda.Abstractions/Core/ILambdaMiddlewareFactory.cs diff --git a/src/MinimalLambda.Abstractions/Core/ILambdaMiddlewareFactory.cs b/src/MinimalLambda.Abstractions/Core/ILambdaMiddlewareFactory.cs new file mode 100644 index 00000000..3778cc18 --- /dev/null +++ b/src/MinimalLambda.Abstractions/Core/ILambdaMiddlewareFactory.cs @@ -0,0 +1,16 @@ +namespace MinimalLambda; + +/// Defines a factory for creating middleware instances. +/// +/// +/// Use an when middleware construction needs to be +/// customized or deferred. Register the factory in the DI container and use +/// UseMiddleware<TFactory>() to resolve it per invocation. +/// +/// +public interface ILambdaMiddlewareFactory +{ + /// Creates a middleware instance. + /// The instance to execute in the pipeline. + ILambdaMiddleware Create(); +} diff --git a/src/MinimalLambda/Builder/Extensions/MiddlewareLambdaApplicationExtensions.cs b/src/MinimalLambda/Builder/Extensions/MiddlewareLambdaApplicationExtensions.cs index e03233a6..96d1ab9e 100644 --- a/src/MinimalLambda/Builder/Extensions/MiddlewareLambdaApplicationExtensions.cs +++ b/src/MinimalLambda/Builder/Extensions/MiddlewareLambdaApplicationExtensions.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.DependencyInjection; + namespace MinimalLambda.Builder; /// Provides extension methods for adding middleware to the Lambda invocation pipeline. @@ -41,5 +43,60 @@ Func middleware return application; } + + /// Adds middleware created by a factory resolved from the invocation service provider. + /// + /// + /// The factory is resolved per invocation from and + /// used to create a middleware instance. If the middleware implements + /// or , it is disposed after + /// invocation. + /// + /// + /// The factory type implementing . + /// The current instance for method chaining. + /// + /// Thrown when is + /// null. + /// + /// + /// + public ILambdaInvocationBuilder UseMiddleware() + where TFactory : ILambdaMiddlewareFactory + { + ArgumentNullException.ThrowIfNull(application); + + application.Use(next => + { + return async context => + { + var factory = context.ServiceProvider.GetRequiredService(); + var middleware = factory.Create(); + + switch (middleware) + { + case IAsyncDisposable asyncDisposable: + { + await using (asyncDisposable) + await middleware.InvokeAsync(context, next); + + break; + } + case IDisposable disposable: + { + using (disposable) + await middleware.InvokeAsync(context, next); + + break; + } + default: + await middleware.InvokeAsync(context, next); + break; + } + }; + }); + + return application; + } } } From 8a5084150913e39045208ac51eab161797b0f127 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 21 Dec 2025 13:26:27 -0500 Subject: [PATCH 2/4] test(middleware): add unit tests for factory-based middleware creation - Added tests for `UseMiddlewareFactory` with null application argument. - Verified middleware registration and execution using `TestMiddlewareFactory`. - Implemented tests for proper handling of disposable and async disposable middlewares. - Included scenarios tracking middleware creation, invocation, and disposal. --- ...dlewareLambdaApplicationExtensionsTests.cs | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/tests/MinimalLambda.UnitTests/Builder/Extensions/MiddlewareLambdaApplicationExtensionsTests.cs b/tests/MinimalLambda.UnitTests/Builder/Extensions/MiddlewareLambdaApplicationExtensionsTests.cs index b2d57843..b1ccae08 100644 --- a/tests/MinimalLambda.UnitTests/Builder/Extensions/MiddlewareLambdaApplicationExtensionsTests.cs +++ b/tests/MinimalLambda.UnitTests/Builder/Extensions/MiddlewareLambdaApplicationExtensionsTests.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace MinimalLambda.UnitTests.Application.Extensions; @@ -108,4 +109,188 @@ public async Task UseMiddleware_CallsMiddlewareWithContextAndNext() capturedContext.Should().Be(mockContext); capturedNext.Should().NotBeNull(); } + + [Fact] + public void UseMiddlewareFactory_WithNullApplication_ThrowsArgumentNullException() + { + // Arrange + ILambdaInvocationBuilder? application = null; + + // Act + var act = () => application!.UseMiddleware(); + + // Assert + act.Should().ThrowExactly(); + } + + [Fact] + public void UseMiddlewareFactory_WithValidFactory_AddsMiddlewareToApplication() + { + // Arrange + var builder = new LambdaApplicationBuilder(new LambdaApplicationOptions()); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + var host = builder.Build(); + var app = new LambdaApplication(host); + + // Act + app.UseMiddleware(); + + // Assert + app.Middlewares.Should().HaveCount(1); + } + + [Fact] + public async Task UseMiddlewareFactory_ResolvesFactoryAndInvokesMiddleware() + { + // Arrange + var builder = new LambdaApplicationBuilder(new LambdaApplicationOptions()); + var tracker = new MiddlewareTracker(); + builder.Services.AddSingleton(tracker); + builder.Services.AddTransient(); + var host = builder.Build(); + var app = new LambdaApplication(host); + app.UseMiddleware(); + app.Handle(_ => Task.CompletedTask); + var pipeline = app.Build(); + var context = Substitute.For(); + context.ServiceProvider.Returns(host.Services); + + // Act + await pipeline(context); + + // Assert + tracker.CreateCount.Should().Be(1); + tracker.InvokeCount.Should().Be(1); + } + + [Fact] + public async Task UseMiddlewareFactory_DisposesDisposableMiddleware() + { + // Arrange + var builder = new LambdaApplicationBuilder(new LambdaApplicationOptions()); + var tracker = new MiddlewareTracker(); + builder.Services.AddSingleton(tracker); + builder.Services.AddTransient(); + var host = builder.Build(); + var app = new LambdaApplication(host); + app.UseMiddleware(); + app.Handle(_ => Task.CompletedTask); + var pipeline = app.Build(); + var context = Substitute.For(); + context.ServiceProvider.Returns(host.Services); + + // Act + await pipeline(context); + + // Assert + tracker.DisposeCount.Should().Be(1); + } + + [Fact] + public async Task UseMiddlewareFactory_DisposesAsyncDisposableMiddleware() + { + // Arrange + var builder = new LambdaApplicationBuilder(new LambdaApplicationOptions()); + var tracker = new MiddlewareTracker(); + builder.Services.AddSingleton(tracker); + builder.Services.AddTransient(); + var host = builder.Build(); + var app = new LambdaApplication(host); + app.UseMiddleware(); + app.Handle(_ => Task.CompletedTask); + var pipeline = app.Build(); + var context = Substitute.For(); + context.ServiceProvider.Returns(host.Services); + + // Act + await pipeline(context); + + // Assert + tracker.AsyncDisposeCount.Should().Be(1); + } + + private sealed class MiddlewareTracker + { + public int CreateCount { get; private set; } + public int InvokeCount { get; private set; } + public int DisposeCount { get; private set; } + public int AsyncDisposeCount { get; private set; } + + public void RecordCreate() => CreateCount++; + + public void RecordInvoke() => InvokeCount++; + + public void RecordDispose() => DisposeCount++; + + public void RecordAsyncDispose() => AsyncDisposeCount++; + } + + private sealed class TestMiddlewareFactory(MiddlewareTracker tracker) : ILambdaMiddlewareFactory + { + public ILambdaMiddleware Create() + { + tracker.RecordCreate(); + return new TestMiddleware(tracker); + } + } + + private sealed class DisposableMiddlewareFactory(MiddlewareTracker tracker) + : ILambdaMiddlewareFactory + { + public ILambdaMiddleware Create() + { + tracker.RecordCreate(); + return new DisposableMiddleware(tracker); + } + } + + private sealed class AsyncDisposableMiddlewareFactory(MiddlewareTracker tracker) + : ILambdaMiddlewareFactory + { + public ILambdaMiddleware Create() + { + tracker.RecordCreate(); + return new AsyncDisposableMiddleware(tracker); + } + } + + private sealed class TestMiddleware(MiddlewareTracker tracker) : ILambdaMiddleware + { + public Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + tracker.RecordInvoke(); + return next(context); + } + } + + private sealed class DisposableMiddleware(MiddlewareTracker tracker) + : ILambdaMiddleware, + IDisposable + { + public Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + tracker.RecordInvoke(); + return next(context); + } + + public void Dispose() => tracker.RecordDispose(); + } + + private sealed class AsyncDisposableMiddleware(MiddlewareTracker tracker) + : ILambdaMiddleware, + IAsyncDisposable + { + public Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + tracker.RecordInvoke(); + return next(context); + } + + public ValueTask DisposeAsync() + { + tracker.RecordAsyncDispose(); + return ValueTask.CompletedTask; + } + } } From 5801ffde3796b3717d740ab24f3d9600931dbc00 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 21 Dec 2025 13:28:59 -0500 Subject: [PATCH 3/4] docs(middleware): add guide for factory-based middleware usage - Documented `ILambdaMiddlewareFactory` for custom middleware creation. - Included example for middleware registration using `UseMiddleware()`. - Demonstrated integration of DI and proper handling of disposal features. --- docs/guides/middleware.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index 8be74b34..8d03ed6a 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -262,6 +262,35 @@ internal sealed class ValidationMiddleware : ILambdaMiddleware For more on service lifetimes and DI patterns, see [Dependency Injection](dependency-injection.md). +#### Factory-Based Middleware + +When middleware construction needs to be customized or deferred, register a factory that implements +`ILambdaMiddlewareFactory` and use `UseMiddleware()`. The factory is resolved from the +invocation's `ServiceProvider` and executed per invocation. If the created middleware implements +`IDisposable` or `IAsyncDisposable`, it is disposed after the invocation completes. + +```csharp title="CachingMiddlewareFactory.cs" linenums="1" +using MinimalLambda; + +internal sealed class CachingMiddlewareFactory(ICache cache, ILogger logger) + : ILambdaMiddlewareFactory +{ + public ILambdaMiddleware Create() => new CachingMiddleware(cache, logger); +} +``` + +```csharp title="Program.cs" +var builder = LambdaApplication.CreateBuilder(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var lambda = builder.Build(); +lambda.UseMiddleware(); + +lambda.MapHandler(([FromEvent] OrderRequest req) => ProcessOrder(req)); +await lambda.RunAsync(); +``` + #### Parameter Sources Control how constructor parameters are resolved using attributes: From 49da19e1755697aad03aac26f54199fcebd634e3 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 21 Dec 2025 13:32:32 -0500 Subject: [PATCH 4/4] docs(claude): update scope definition reference - Replaced hardcoded scope list with a reference to `./.claude/commands/pr.md` for valid scopes. - Clarified where to find the complete list of allowed scope values. --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5c304134..bb29332c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,8 +154,8 @@ When Claude makes commits, ALWAYS follow conventional commits format: **Types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci` -**Scope (optional but recommended):** Must be one of: `host`, `abstractions`, `opentelemetry`, -`deps`, `build`, `ci`, `github` +**Scope (optional but recommended):** See `./.claude/commands/pr.md` for the complete list of valid +scopes. - Omit scope for general changes