Skip to content

feat(host): add class-based middleware registration via ILambdaMiddleware interface #198

Description

@j-d-ha

Summary

Add support for registering middleware from classes implementing an ILambdaMiddleware interface, providing a cleaner, testable, and more maintainable alternative to inline delegate-based middleware. This feature may include source generation for enhanced performance and compile-time validation.

Problem Statement

Currently, middleware can only be registered using inline delegates via UseMiddleware() or as static extension methods. This approach has several limitations:

  • Testability: Difficult to unit test complex middleware logic in isolation
  • Maintainability: Inline delegates become hard to read and maintain for complex middleware
  • Dependency Injection: No built-in way to inject services directly into middleware
  • Discoverability: Middleware logic is scattered across extension methods and inline code
  • Reusability: Hard to share middleware implementations across different Lambda applications

Proposed Solution

Implement a class-based middleware registration system inspired by ASP.NET Core's IMiddleware pattern, adapted for AWS Lambda invocations:

Core Interface

namespace AwsLambda.Host.Abstractions;

/// <summary>
/// Defines middleware that can be added to the Lambda invocation pipeline.
/// </summary>
public interface ILambdaMiddleware
{
    /// <summary>
    /// Processes the Lambda invocation.
    /// </summary>
    /// <param name="context">The Lambda host context.</param>
    /// <param name="next">The next middleware in the pipeline.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next);
}

Registration API

extension(ILambdaInvocationBuilder application)
{
    /// <summary>
    /// Registers middleware of type <typeparamref name="TMiddleware"/> in the pipeline.
    /// The middleware will be resolved from the service provider on each invocation.
    /// </summary>
    public ILambdaInvocationBuilder UseMiddleware<TMiddleware>()
        where TMiddleware : ILambdaMiddleware
    {
        // Implementation resolves TMiddleware from DI container
    }
    
    /// <summary>
    /// Registers middleware of the specified type in the pipeline.
    /// The middleware will be resolved from the service provider on each invocation.
    /// </summary>
    public ILambdaInvocationBuilder UseMiddleware(Type middlewareType)
    {
        // Implementation resolves middlewareType from DI container
    }
}

Example Implementation

// Example middleware class with DI
public class LoggingMiddleware : ILambdaMiddleware
{
    private readonly ILogger<LoggingMiddleware> _logger;
    
    public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
    {
        _logger = logger;
    }
    
    public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next)
    {
        _logger.LogInformation("Invocation started");
        
        try
        {
            await next(context);
            _logger.LogInformation("Invocation completed successfully");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Invocation failed");
            throw;
        }
    }
}

// Registration
services.AddScoped<LoggingMiddleware>();

var app = new LambdaApplication(host);
app.UseMiddleware<LoggingMiddleware>();

Implementation Options

Option 1: Runtime Reflection (Simple)

  • Resolve middleware from DI container at runtime
  • Use reflection to call InvokeAsync
  • Pros: Simple implementation, no build-time complexity
  • Cons: Performance overhead from reflection

Option 2: Source Generation (Optimal)

  • Generate strongly-typed middleware adapters at compile-time
  • Eliminate reflection overhead
  • Provide compile-time validation
  • Pros: Best performance, AOT-compatible, compile-time safety
  • Cons: More complex implementation, requires incremental generator

Recommendation

Start with Option 1 for initial release, then add Option 2 as an optimization in a future release. This provides immediate value while allowing for future performance improvements.

Source Generation Considerations (Future Enhancement)

If implementing source generation:

// User code
[assembly: LambdaMiddleware(typeof(LoggingMiddleware))]
[assembly: LambdaMiddleware(typeof(ValidationMiddleware))]

// Generated code
partial class LambdaInvocationBuilderExtensions
{
    public static ILambdaInvocationBuilder UseMiddleware<LoggingMiddleware>(
        this ILambdaInvocationBuilder app)
    {
        return app.Use(next => 
        {
            return async context =>
            {
                var middleware = context.Services.GetRequiredService<LoggingMiddleware>();
                await middleware.InvokeAsync(context, next);
            };
        });
    }
}

Design Decisions

Lifecycle

  • Scoped per invocation: Middleware instances are resolved from the DI container for each Lambda invocation
  • This aligns with Lambda's execution model where each invocation gets a fresh scope

Service Resolution

  • Middleware types should be registered in DI container by the user
  • Use GetRequiredService<T>() to fail fast if not registered
  • Support both Scoped and Transient lifetimes

Ordering

Error Handling

  • If middleware type is not registered in DI, throw clear exception at invocation time
  • Consider compile-time warnings with source generation

Benefits

  • Testability: Middleware classes can be unit tested in isolation with mocked dependencies
  • Maintainability: Complex middleware logic is organized in dedicated classes
  • Dependency Injection: First-class support for injecting services into middleware
  • Reusability: Middleware classes can be packaged and shared across projects
  • Discoverability: Middleware implementations are easy to find and understand
  • ASP.NET Core Familiarity: Developers familiar with ASP.NET Core will recognize the pattern

Example Usage Scenarios

Authentication Middleware

public class AuthenticationMiddleware : ILambdaMiddleware
{
    private readonly IAuthService _authService;
    
    public AuthenticationMiddleware(IAuthService authService)
    {
        _authService = authService;
    }
    
    public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next)
    {
        var token = ExtractToken(context);
        var principal = await _authService.ValidateTokenAsync(token);
        
        // Add authenticated user to context
        context.Features.Set<IPrincipal>(principal);
        
        await next(context);
    }
}

Validation Middleware

public class ValidationMiddleware<TEvent> : ILambdaMiddleware
{
    private readonly IValidator<TEvent> _validator;
    
    public ValidationMiddleware(IValidator<TEvent> validator)
    {
        _validator = validator;
    }
    
    public async Task InvokeAsync(ILambdaHostContext context, LambdaInvocationDelegate next)
    {
        var @event = context.Event.Get<TEvent>();
        var result = await _validator.ValidateAsync(@event);
        
        if (!result.IsValid)
        {
            throw new ValidationException(result.Errors);
        }
        
        await next(context);
    }
}

Related Issues

Breaking Changes

None - this is purely additive functionality that extends the existing middleware system.

Testing Considerations

  • Unit tests for ILambdaMiddleware interface implementation
  • Unit tests for middleware registration extension methods
  • Integration tests with DI container resolution
  • Tests for middleware execution order
  • Tests for error handling when middleware not registered in DI
  • Performance benchmarks comparing reflection vs source generation approaches
  • AOT compatibility tests if source generation is implemented

Documentation Requirements

  • XML docs for ILambdaMiddleware interface
  • Usage guide with examples
  • Migration guide for converting delegate-based middleware to class-based
  • Performance considerations documentation
  • Best practices for middleware design

Implementation Phases

Phase 1: Core Interface & Runtime Implementation

  1. Define ILambdaMiddleware interface in AwsLambda.Host.Abstractions
  2. Implement UseMiddleware<T>() extension method with runtime resolution
  3. Add unit tests
  4. Add documentation

Phase 2: Source Generation (Optional Future Enhancement)

  1. Create incremental source generator project
  2. Implement generator for middleware adapters
  3. Add compile-time diagnostics
  4. Performance benchmarks
  5. AOT compatibility validation

Type of Change

  • New feature
  • Breaking change
  • Requires documentation updates
  • Requires migration guide

Acceptance Criteria

  • ILambdaMiddleware interface defined in Abstractions package
  • UseMiddleware<T>() extension method implemented
  • Middleware resolved from DI container per invocation
  • Unit tests achieve >90% coverage
  • Integration tests validate DI resolution
  • XML documentation complete
  • Usage examples in README
  • Performance benchmarks documented

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions