Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 29 additions & 0 deletions docs/guides/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TFactory>()`. 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<CachingMiddleware> logger)
: ILambdaMiddlewareFactory
{
public ILambdaMiddleware Create() => new CachingMiddleware(cache, logger);
}
```

```csharp title="Program.cs"
var builder = LambdaApplication.CreateBuilder();
builder.Services.AddSingleton<ICache, RedisCache>();
builder.Services.AddSingleton<ILambdaMiddlewareFactory, CachingMiddlewareFactory>();

var lambda = builder.Build();
lambda.UseMiddleware<CachingMiddlewareFactory>();

lambda.MapHandler(([FromEvent] OrderRequest req) => ProcessOrder(req));
await lambda.RunAsync();
```

#### Parameter Sources

Control how constructor parameters are resolved using attributes:
Expand Down
16 changes: 16 additions & 0 deletions src/MinimalLambda.Abstractions/Core/ILambdaMiddlewareFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace MinimalLambda;

/// <summary>Defines a factory for creating middleware instances.</summary>
/// <remarks>
/// <para>
/// Use an <see cref="ILambdaMiddlewareFactory" /> when middleware construction needs to be
/// customized or deferred. Register the factory in the DI container and use
/// <c>UseMiddleware&lt;TFactory&gt;()</c> to resolve it per invocation.
/// </para>
/// </remarks>
public interface ILambdaMiddlewareFactory
{
/// <summary>Creates a middleware instance.</summary>
/// <returns>The <see cref="ILambdaMiddleware" /> instance to execute in the pipeline.</returns>
ILambdaMiddleware Create();
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Microsoft.Extensions.DependencyInjection;

namespace MinimalLambda.Builder;

/// <summary>Provides extension methods for adding middleware to the Lambda invocation pipeline.</summary>
Expand Down Expand Up @@ -41,5 +43,60 @@ Func<ILambdaInvocationContext, LambdaInvocationDelegate, Task> middleware

return application;
}

/// <summary>Adds middleware created by a factory resolved from the invocation service provider.</summary>
/// <remarks>
/// <para>
/// The factory is resolved per invocation from <see cref="ILambdaInvocationContext" /> and
/// used to create a middleware instance. If the middleware implements
/// <see cref="IAsyncDisposable" /> or <see cref="IDisposable" />, it is disposed after
/// invocation.
/// </para>
/// </remarks>
/// <typeparam name="TFactory">The factory type implementing <see cref="ILambdaMiddlewareFactory" />.</typeparam>
/// <returns>The current <see cref="ILambdaInvocationBuilder" /> instance for method chaining.</returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <see cref="ILambdaInvocationBuilder" /> is
/// <c>null</c>.
/// </exception>
/// <seealso cref="ILambdaMiddlewareFactory" />
/// <seealso cref="ILambdaInvocationBuilder.Use" />
public ILambdaInvocationBuilder UseMiddleware<TFactory>()
where TFactory : ILambdaMiddlewareFactory
{
ArgumentNullException.ThrowIfNull(application);

application.Use(next =>
{
return async context =>
{
var factory = context.ServiceProvider.GetRequiredService<TFactory>();
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace MinimalLambda.UnitTests.Application.Extensions;
Expand Down Expand Up @@ -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<TestMiddlewareFactory>();

// Assert
act.Should().ThrowExactly<ArgumentNullException>();
}

[Fact]
public void UseMiddlewareFactory_WithValidFactory_AddsMiddlewareToApplication()
{
// Arrange
var builder = new LambdaApplicationBuilder(new LambdaApplicationOptions());
builder.Services.AddSingleton<MiddlewareTracker>();
builder.Services.AddTransient<TestMiddlewareFactory>();
var host = builder.Build();
var app = new LambdaApplication(host);

// Act
app.UseMiddleware<TestMiddlewareFactory>();

// 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<TestMiddlewareFactory>();
var host = builder.Build();
var app = new LambdaApplication(host);
app.UseMiddleware<TestMiddlewareFactory>();
app.Handle(_ => Task.CompletedTask);
var pipeline = app.Build();
var context = Substitute.For<ILambdaInvocationContext>();
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<DisposableMiddlewareFactory>();
var host = builder.Build();
var app = new LambdaApplication(host);
app.UseMiddleware<DisposableMiddlewareFactory>();
app.Handle(_ => Task.CompletedTask);
var pipeline = app.Build();
var context = Substitute.For<ILambdaInvocationContext>();
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<AsyncDisposableMiddlewareFactory>();
var host = builder.Build();
var app = new LambdaApplication(host);
app.UseMiddleware<AsyncDisposableMiddlewareFactory>();
app.Handle(_ => Task.CompletedTask);
var pipeline = app.Build();
var context = Substitute.For<ILambdaInvocationContext>();
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;
}
}
}
Loading