diff --git a/Directory.Build.props b/Directory.Build.props index 67706273..546795da 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.0.0-beta.10 + 2.0.0-beta.11 MIT diff --git a/MinimalLambda.sln b/MinimalLambda.sln index 10dd44a9..d466b293 100644 --- a/MinimalLambda.sln +++ b/MinimalLambda.sln @@ -91,6 +91,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lambdas", "Lambdas", "{D910 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalLambda.Envelopes", "src\Envelopes\MinimalLambda.Envelopes\MinimalLambda.Envelopes.csproj", "{190CC9C7-007F-48C5-867D-03B911D53397}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalLambda.Example.ClassMiddleware", "examples\MinimalLambda.Example.ClassMiddleware\MinimalLambda.Example.ClassMiddleware.csproj", "{69785FC6-B746-4104-8157-1618B80B0A4C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -449,6 +451,18 @@ Global {190CC9C7-007F-48C5-867D-03B911D53397}.Release|x64.Build.0 = Release|Any CPU {190CC9C7-007F-48C5-867D-03B911D53397}.Release|x86.ActiveCfg = Release|Any CPU {190CC9C7-007F-48C5-867D-03B911D53397}.Release|x86.Build.0 = Release|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Debug|x64.ActiveCfg = Debug|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Debug|x64.Build.0 = Debug|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Debug|x86.ActiveCfg = Debug|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Debug|x86.Build.0 = Debug|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Release|Any CPU.Build.0 = Release|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Release|x64.ActiveCfg = Release|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Release|x64.Build.0 = Release|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Release|x86.ActiveCfg = Release|Any CPU + {69785FC6-B746-4104-8157-1618B80B0A4C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -486,5 +500,6 @@ Global {381F49C0-297F-4B61-8A82-E9E502E523AD} = {D9109C8A-AFA8-49C8-A19C-381500902B4D} {7E7B3004-C6C4-4A8C-8610-2A1CB61F834A} = {D9109C8A-AFA8-49C8-A19C-381500902B4D} {190CC9C7-007F-48C5-867D-03B911D53397} = {1C3C52D9-2936-4A0C-A9C8-F330F22B8359} + {69785FC6-B746-4104-8157-1618B80B0A4C} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} EndGlobalSection EndGlobal diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index 5297eeb2..8be74b34 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -1,10 +1,11 @@ # Middleware -`minimal-lambda` uses the same middleware model as ASP.NET Core: each component gets a context object, -runs code before/after the next component, and can short-circuit the pipeline. If you're new to the -pattern, skim the [ASP.NET Core middleware overview](https://learn.microsoft.com/aspnet/core/fundamentals/middleware/) -first. This guide focuses on Lambda-specific behavior: invocation scopes, feature access, and -composition tips that keep middleware and handlers decoupled without extra DI plumbing. +`minimal-lambda` uses a middleware model similar to ASP.NET Core: each component gets a context +object, runs code before/after the next component, and can short-circuit the pipeline. If you're new +to the pattern, the +[ASP.NET Core middleware overview](https://learn.microsoft.com/aspnet/core/fundamentals/middleware/) +is a helpful primer. This guide focuses on Lambda-specific behavior: invocation scopes, feature +access, and composition tips that keep middleware and handlers decoupled without extra DI plumbing. ## Pipeline Basics @@ -58,6 +59,7 @@ lambda.UseMiddleware(async (context, next) => } context.Items["RequestId"] = Guid.NewGuid().ToString(); + context.Items["Start"] = DateTimeOffset.UtcNow; context.Properties["Version"] ??= "1.0.0"; // safe cross-invocation value await next(context); @@ -74,13 +76,756 @@ Key members: `LambdaHostOptions.InvocationCancellationBuffer`). Pass it to downstream async work. - `Items` – per-invocation storage shared by middleware/handler. - `Properties` – cross-invocation storage. -- `Features` – ASP.NET-style typed capabilities such as `IEventFeature` and `IResponseFeature` that let middleware collaborate without injecting each other. +- `Features` – typed capabilities such as `IEventFeature` and `IResponseFeature` that let + middleware collaborate without injecting each other. -## Inline Middleware +## Middleware Approaches -`UseMiddleware` currently accepts inline delegates. Class-based middleware activators are on the roadmap, -so for now keep middleware logic inside the lambda or extract helper services (registered in DI) for reuse. -Treat the delegate as the orchestration glue and push heavy lifting into services so the code stays testable. +`MinimalLambda` supports two middleware styles: + +**Inline delegates** – Best for simple, application-specific middleware that orchestrates services. +Quick to write and keeps logic visible in the pipeline configuration. + +**Class-based middleware** – Best for complex, reusable middleware with dependencies, state +management, +or disposal needs. Easier to test and share across projects. + +Most applications use both: inline delegates for orchestration, class-based for heavy lifting. + +### Inline Middleware + +Inline middleware uses delegates registered directly in `Program.cs`. Despite the inline syntax, +you have full access to the invocation context and all its capabilities: + +```csharp title="Program.cs" +lambda.UseMiddleware(async (context, next) => +{ + // Resolve services from DI + var cache = context.ServiceProvider.GetRequiredService(); + var logger = context.ServiceProvider.GetRequiredService>(); + + // Access event data using type-safe helpers + if (context.TryGetEvent(out var request)) + { + logger.LogInformation("Processing order {OrderId}", request.OrderId); + + // Check cache before continuing + if (cache.TryGet(request.OrderId, out OrderResponse cached)) + { + context.Features.Get>()!.Response = cached; + return; // Short-circuit + } + } + + // Store per-invocation data + context.Items["RequestId"] = Guid.NewGuid().ToString(); + context.Items["StartTime"] = DateTimeOffset.UtcNow; + + await next(context); + + // Access response after handler executes + var response = context.GetResponse(); + if (response is not null && request is not null) + { + await cache.SetAsync(request.OrderId, response); + } +}); +``` + +**What inline middleware can access:** + +- **Dependency Injection** - `context.ServiceProvider.GetRequiredService()` +- **Event/Response Data** - `GetEvent()`, `GetResponse()`, `TryGetEvent()` ( + see [Type-Safe Feature Access](#type-safe-feature-access)) +- **Features** - `context.Features.Get>()` ( + see [Working with Features](#working-with-features)) +- **Per-Invocation State** - `context.Items` for temporary data within the request +- **Cross-Invocation State** - `context.Properties` for data shared across Lambda invocations +- **Cancellation** - `context.CancellationToken` for cooperative cancellation +- **AWS Context** - All standard `ILambdaContext` properties (`AwsRequestId`, `RemainingTime`, etc.) + +**When to use inline middleware:** + +- Application-specific orchestration logic +- Simple logging, metrics, or tracing +- One-off middleware that won't be reused +- Quick prototyping before extracting to a class +- Gluing together services without needing a separate file + +**Best practice:** Keep inline middleware thin. Push complex logic into services registered in DI so +the middleware stays readable and testable. Treat inline middleware as the glue between services. + +### Class-Based Middleware + +Class-based middleware promotes reusability, testability, and clean separation of concerns. Define a +class implementing `ILambdaMiddleware`, then register it with `UseMiddleware()`. + +```csharp title="LoggingMiddleware.cs" linenums="1" +using System.Diagnostics; +using MinimalLambda; + +internal sealed class LoggingMiddleware : ILambdaMiddleware +{ + private readonly ILogger _logger; + + public LoggingMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + _logger.LogInformation("Invocation starting"); + + var stopwatch = Stopwatch.StartNew(); + + await next(context); + + _logger.LogInformation("Invocation completed in {Duration}ms", stopwatch.ElapsedMilliseconds); + } +} +``` + +```csharp title="Program.cs" +var builder = LambdaApplication.CreateBuilder(); +var lambda = builder.Build(); + +lambda.UseMiddleware(); + +lambda.MapHandler(([FromEvent] Request req) => new Response("OK")); + +await lambda.RunAsync(); +``` + +**How it works:** Source generators intercept `UseMiddleware()` at compile-time, generating code +that instantiates your middleware and resolves constructor parameters automatically. No reflection, +no runtime overhead. + +!!! tip "Reusable packages" +Class-based middleware is a good fit for shared packages: ship the middleware type and attributes, +and the consuming app's build generates the wiring code. The generated code lives in the +application's build output, not in your package. + +#### Dependency Injection + +Constructor parameters are automatically resolved from the DI container: + +```csharp title="ValidationMiddleware.cs" linenums="1" +using MinimalLambda; + +internal sealed class ValidationMiddleware : ILambdaMiddleware +{ + private readonly IValidator _validator; + private readonly ILogger _logger; + private readonly IMetrics _metrics; + + public ValidationMiddleware( + IValidator validator, + ILogger logger, + IMetrics metrics) + { + _validator = validator; + _logger = logger; + _metrics = metrics; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + if (!context.TryGetEvent(out var request)) + { + _logger.LogWarning("No event found for validation"); + return; + } + + var result = await _validator.ValidateAsync(request); + + if (!result.IsValid) + { + _metrics.IncrementCounter("validation.failed"); + context.Features.Get>()!.Response + = new ErrorResponse(result.Errors); + return; // short-circuit + } + + await next(context); + } +} +``` + +**Default resolution behavior:** + +- Parameters without attributes first check args passed to `UseMiddleware()`, then fall back to + DI +- Services must be registered in `builder.Services` before calling `builder.Build()` +- Use `[FromServices]` to skip args and resolve directly from DI ( + see [Parameter Sources](#parameter-sources)) + +For more on service lifetimes and DI patterns, see [Dependency Injection](dependency-injection.md). + +#### Parameter Sources + +Control how constructor parameters are resolved using attributes: + +| Attribute | Source | Behavior | +|-----------------------|---------------|---------------------------------------------------------| +| (none) | Args, then DI | Try args first, fall back to DI if no match | +| `[FromServices]` | DI only | Resolve from DI container, skip args | +| `[FromKeyedServices]` | Keyed DI | Resolve keyed service from DI (e.g., `"primary"` cache) | +| `[FromArguments]` | Args only | Require value from args; throw if not found | + +**Example: Mixed Parameter Sources** + +```csharp title="CachingMiddleware.cs" linenums="1" +using Microsoft.Extensions.DependencyInjection; +using MinimalLambda.Builder; + +internal sealed class CachingMiddleware : ILambdaMiddleware +{ + private readonly string _cacheKey; + private readonly ICache _primaryCache; + private readonly ICache _fallbackCache; + private readonly ILogger _logger; + private readonly IMetrics? _metrics; + + public CachingMiddleware( + [FromArguments] string cacheKey, // Required from args + [FromKeyedServices("primary")] ICache primaryCache, // Keyed service + [FromKeyedServices("fallback")] ICache fallbackCache, + [FromServices] ILogger logger, // Explicit DI + IMetrics? metrics) // Optional: args or DI + { + _cacheKey = cacheKey; + _primaryCache = primaryCache; + _fallbackCache = fallbackCache; + _logger = logger; + _metrics = metrics; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + if (await _primaryCache.TryGetAsync(_cacheKey, out var cached)) + { + context.Features.Get>()!.Response = cached; + _metrics?.IncrementCounter("cache.hit"); + return; + } + + await next(context); + + // Cache the response + var response = context.GetResponse(); + if (response is not null) + { + await _primaryCache.SetAsync(_cacheKey, response); + } + } +} +``` + +```csharp title="Program.cs" linenums="1" +var builder = LambdaApplication.CreateBuilder(); + +builder.Services.AddKeyedSingleton("primary"); +builder.Services.AddKeyedSingleton("fallback"); +builder.Services.AddSingleton, Logger>(); +builder.Services.AddSingleton(); // Optional + +var lambda = builder.Build(); + +lambda.UseMiddleware("order-cache"); // Pass cacheKey as arg + +lambda.MapHandler(([FromEvent] OrderRequest req) => ProcessOrder(req)); + +await lambda.RunAsync(); +``` + +!!! tip "When to use [FromArguments]" +Use `[FromArguments]` for configuration values that vary per middleware registration (like +cache keys, API endpoints, or feature flags). This makes the middleware reusable with +different configurations. + +#### Multiple Constructors + +When a middleware class has multiple constructors, the source generator selects the one with the +**most parameters** by default. Override this behavior with `[MiddlewareConstructor]`: + +```csharp title="AuthMiddleware.cs" linenums="1" +using MinimalLambda.Builder; + +internal sealed class AuthMiddleware : ILambdaMiddleware +{ + private readonly IAuthService _authService; + private readonly ILogger _logger; + private readonly bool _allowAnonymous; + + // This constructor has more parameters, so it would be selected by default + public AuthMiddleware( + IAuthService authService, + ILogger logger, + bool allowAnonymous) + { + _authService = authService; + _logger = logger; + _allowAnonymous = allowAnonymous; + } + + // Explicitly select this simpler constructor instead + [MiddlewareConstructor] + public AuthMiddleware(IAuthService authService) + { + _authService = authService; + _logger = null!; + _allowAnonymous = false; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + var token = context.Items["AuthToken"] as string; + + if (token is null && !_allowAnonymous) + { + _logger?.LogWarning("Missing authentication token"); + return; + } + + if (token is not null && !await _authService.ValidateAsync(token)) + { + _logger?.LogWarning("Invalid authentication token"); + return; + } + + await next(context); + } +} +``` + +!!! warning +Only one constructor can have `[MiddlewareConstructor]`. Applying it to multiple constructors +triggers compile-time diagnostic **LH0005**. + +#### Lifecycle and Disposal + +Middleware instances are created **per invocation**. Each Lambda invocation gets a fresh instance, +which is disposed after the invocation completes. + +**IDisposable and IAsyncDisposable:** + +```csharp title="TracingMiddleware.cs" linenums="1" +using System.Diagnostics; + +internal sealed class TracingMiddleware : ILambdaMiddleware, IAsyncDisposable +{ + private readonly ITracer _tracer; + private ISpan? _span; + + public TracingMiddleware(ITracer tracer) + { + _tracer = tracer; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + _span = _tracer.StartSpan("lambda.invocation"); + _span.SetAttribute("requestId", context.Items["RequestId"]?.ToString() ?? "unknown"); + + try + { + await next(context); + _span.SetStatus(SpanStatus.Ok); + } + catch (Exception ex) + { + _span.SetStatus(SpanStatus.Error); + _span.RecordException(ex); + throw; + } + } + + public async ValueTask DisposeAsync() + { + if (_span is not null) + { + await _span.EndAsync(); + await _tracer.FlushAsync(); + } + } +} +``` + +**Disposal timing:** + +- `Dispose()` or `DisposeAsync()` is called after `InvokeAsync()` completes +- Even if an exception occurs, disposal is guaranteed (wrapped in try/finally) +- The generated code prefers `IAsyncDisposable` over `IDisposable` if both are implemented + +!!! info "Singleton vs. Per-Invocation" +Unlike services registered in DI (which can be singleton, scoped, or transient), middleware +instances are **always per-invocation**. For shared state, inject singleton services into the +middleware constructor. + +For more on lifecycle hooks, see [Lifecycle Management](lifecycle-management.md). + +#### Testing Class-Based Middleware + +Class-based middleware is straightforward to test: create instances directly and verify behavior. + +```csharp title="LoggingMiddlewareTests.cs" linenums="1" +[Theory] +[AutoNSubstituteData] +internal async Task InvokeAsync_LogsStartAndCompletion( + [Frozen] ILogger logger, + LoggingMiddleware middleware, + ILambdaInvocationContext context, + LambdaInvocationDelegate next) +{ + // Act + await middleware.InvokeAsync(context, next); + + // Assert + logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("starting")), + null, + Arg.Any>()); + + logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("completed")), + null, + Arg.Any>()); + + await next.Received(1).Invoke(context); +} +``` + +**Testing with parameter resolution:** + +```csharp title="CachingMiddlewareTests.cs" linenums="1" +[Fact] +internal async Task InvokeAsync_ReturnsCachedResult_WhenCacheHit() +{ + // Arrange + var primaryCache = Substitute.For(); + var fallbackCache = Substitute.For(); + var logger = Substitute.For>(); + var cachedResponse = new Response("cached"); + + primaryCache.TryGetAsync("test-key", out Arg.Any()) + .Returns(x => + { + x[1] = cachedResponse; + return true; + }); + + var middleware = new CachingMiddleware( + "test-key", + primaryCache, + fallbackCache, + logger, + null); + + var context = Substitute.For(); + var responseFeature = Substitute.For>(); + context.Features.Get>().Returns(responseFeature); + + var next = Substitute.For(); + + // Act + await middleware.InvokeAsync(context, next); + + // Assert + responseFeature.Response.Should().Be(cachedResponse); + await next.DidNotReceive().Invoke(Arg.Any()); +} +``` + +!!! tip "Testing Strategy" +Test middleware in isolation by mocking `ILambdaInvocationContext` and the `next` delegate. +This keeps tests fast and focused on middleware behavior without spinning up the entire pipeline. + +For more testing patterns, see [Testing](testing.md). + +#### Real-World Examples + +**JWT Authentication:** + +```csharp title="JwtAuthMiddleware.cs" linenums="1" +internal sealed class JwtAuthMiddleware : ILambdaMiddleware +{ + private readonly IJwtValidator _jwtValidator; + private readonly ILogger _logger; + + public JwtAuthMiddleware( + IJwtValidator jwtValidator, + ILogger logger) + { + _jwtValidator = jwtValidator; + _logger = logger; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + // Extract JWT from event (e.g., API Gateway authorizer context) + var request = context.GetEvent(); + if (request?.Headers is null || + !request.Headers.TryGetValue("Authorization", out var authHeader)) + { + _logger.LogWarning("Missing Authorization header"); + SetUnauthorizedResponse(context); + return; + } + + var token = authHeader.Replace("Bearer ", string.Empty); + + var validationResult = await _jwtValidator.ValidateAsync(token, context.CancellationToken); + if (!validationResult.IsValid) + { + _logger.LogWarning("Invalid JWT token: {Reason}", validationResult.FailureReason); + SetUnauthorizedResponse(context); + return; + } + + // Store claims for downstream handlers + context.Items["User"] = validationResult.User; + context.Items["Claims"] = validationResult.Claims; + + await next(context); + } + + private static void SetUnauthorizedResponse(ILambdaInvocationContext context) + { + var responseFeature = context.Features.Get>(); + if (responseFeature is not null) + { + responseFeature.Response = new ApiGatewayProxyResponse + { + StatusCode = 401, + Body = "{\"error\":\"Unauthorized\"}" + }; + } + } +} +``` + +**Request/Response Transformation (Envelopes):** + +```csharp title="EnvelopeMiddleware.cs" linenums="1" +internal sealed class EnvelopeMiddleware : ILambdaMiddleware +{ + private readonly ILogger _logger; + + public EnvelopeMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + // Unwrap envelope before handler + var rawEvent = context.GetEvent(); + if (rawEvent?.Payload is not null) + { + var innerEvent = JsonSerializer.Deserialize(rawEvent.Payload); + context.Features.Get>()!.Event = innerEvent; + } + + await next(context); + + // Wrap response in envelope + var rawResponse = context.GetResponse(); + if (rawResponse is not null) + { + var envelope = new EnvelopedResponse + { + Timestamp = DateTimeOffset.UtcNow, + CorrelationId = rawEvent?.CorrelationId ?? Guid.NewGuid().ToString(), + Payload = JsonSerializer.Serialize(rawResponse) + }; + + context.Features.Get>()!.Response = envelope; + } + } +} +``` + +**Distributed Tracing (OpenTelemetry):** + +```csharp title="OpenTelemetryMiddleware.cs" linenums="1" +using System.Diagnostics; + +internal sealed class OpenTelemetryMiddleware : ILambdaMiddleware, IDisposable +{ + private readonly ActivitySource _activitySource; + private Activity? _activity; + + public OpenTelemetryMiddleware([FromArguments] string serviceName) + { + _activitySource = new ActivitySource(serviceName); + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + _activity = _activitySource.StartActivity("lambda.invocation", ActivityKind.Server); + + if (_activity is not null) + { + _activity.SetTag("aws.request_id", context.AwsRequestId); + _activity.SetTag("service.name", _activitySource.Name); + } + + try + { + await next(context); + _activity?.SetStatus(ActivityStatusCode.Ok); + } + catch (Exception ex) + { + _activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + _activity?.RecordException(ex); + throw; + } + } + + public void Dispose() + { + _activity?.Dispose(); + } +} +``` + +```csharp title="Program.cs" +lambda.UseMiddleware("my-lambda-service"); +``` + +#### Common Patterns + +**Pattern: Conditional Middleware** + +Run middleware only for specific event types: + +```csharp title="ConditionalValidationMiddleware.cs" +internal sealed class ConditionalValidationMiddleware : ILambdaMiddleware +{ + private readonly IValidator _validator; + + public ConditionalValidationMiddleware(IValidator validator) + { + _validator = validator; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + // Only validate if event is OrderRequest + if (context.TryGetEvent(out var order)) + { + var result = await _validator.ValidateAsync(order); + if (!result.IsValid) + { + // Set error response and short-circuit + context.Features.Get>()!.Response + = new ErrorResponse(result.Errors); + return; + } + } + + await next(context); + } +} +``` + +**Pattern: Shared State via DI** + +Share state across invocations using singleton services: + +```csharp title="RateLimitMiddleware.cs" +internal sealed class RateLimitMiddleware : ILambdaMiddleware +{ + private readonly IRateLimiter _rateLimiter; // Singleton service + + public RateLimitMiddleware(IRateLimiter rateLimiter) + { + _rateLimiter = rateLimiter; // Shared across invocations + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + var clientId = context.Items["ClientId"]?.ToString() ?? "unknown"; + + if (!await _rateLimiter.AllowRequestAsync(clientId)) + { + // Rate limit exceeded + context.Features.Get>()!.Response + = new ErrorResponse("Rate limit exceeded"); + return; + } + + await next(context); + } +} +``` + +```csharp title="Program.cs" +builder.Services.AddSingleton(); // Singleton! + +var lambda = builder.Build(); +lambda.UseMiddleware(); +``` + +**Pattern: Early Response** + +Set a response and skip the handler: + +```csharp title="MaintenanceModeMiddleware.cs" +internal sealed class MaintenanceModeMiddleware : ILambdaMiddleware +{ + private readonly IFeatureFlagService _featureFlags; + + public MaintenanceModeMiddleware(IFeatureFlagService featureFlags) + { + _featureFlags = featureFlags; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + if (await _featureFlags.IsEnabledAsync("maintenance-mode")) + { + context.Features.Get>()!.Response + = new MaintenanceResponse("Service unavailable during maintenance"); + return; // Don't call next + } + + await next(context); + } +} +``` + +#### Diagnostics + +The source generator validates middleware at compile-time: + +| Diagnostic | Severity | Description | +|------------|----------|-------------------------------------------------------------------------| +| **LH0005** | Error | Multiple constructors have `[MiddlewareConstructor]` (only one allowed) | +| **LH0006** | Error | Middleware type must be a concrete class (not interface/abstract) | + +**Example: LH0006** + +```csharp +// ❌ This triggers LH0006 +lambda.UseMiddleware(); // Interface, not allowed + +// ❌ This triggers LH0006 +lambda.UseMiddleware(); // Abstract class, not allowed + +// ✅ This is correct +lambda.UseMiddleware(); // Concrete class +``` + +!!! info "Compile-Time Safety" +These diagnostics catch configuration errors during build, not at runtime. This prevents +deployment of misconfigured middleware. ## Working with Features @@ -249,15 +994,23 @@ lambda.UseMiddleware(async (context, next) => ## Ordering Strategy -Register middleware from outermost to innermost: +Register middleware from outermost to innermost. Mix inline delegates and class-based middleware +freely: ```csharp title="Order" -lambda.UseMiddleware(); // catches everything -lambda.UseMiddleware(); // logs every request -lambda.UseMiddleware(); // records durations -lambda.UseMiddleware(); // auth first -lambda.UseMiddleware(); // then authorization -lambda.UseMiddleware(); // validate payloads +lambda.UseMiddleware(); // Class-based: catches everything +lambda.UseMiddleware(); // Class-based: logs every request + +lambda.UseMiddleware(async (context, next) => // Inline: quick metric +{ + var sw = Stopwatch.StartNew(); + await next(context); + Console.WriteLine($"Duration: {sw.ElapsedMilliseconds}ms"); +}); + +lambda.UseMiddleware(); // Class-based: auth first +lambda.UseMiddleware(); // Class-based: then authorization +lambda.UseMiddleware(); // Class-based: validate payloads lambda.MapHandler(/* handler */); ``` @@ -266,22 +1019,80 @@ Guidelines: - Error/diagnostics (logging, metrics) go first so they see every request. - Authentication/authorization should wrap validation and business logic. - Response caching happens late so only valid, authorized responses are stored. +- Inline and class-based middleware execute in registration order - no difference in behavior. ## Configuration and Options -Even though middleware delegates are inline, they still run inside the invocation scope. Resolve options -or services via `context.ServiceProvider` the same way you would inside a handler. +**Inline middleware** resolves services via `context.ServiceProvider`: + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + var options = context.ServiceProvider.GetRequiredService>().Value; + // Use options... + await next(context); +}); +``` + +**Class-based middleware** injects services via constructor: + +```csharp +internal sealed class ConfiguredMiddleware : ILambdaMiddleware +{ + private readonly MyOptions _options; + + public ConfiguredMiddleware(IOptions options) + { + _options = options.Value; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + // Use _options... + await next(context); + } +} +``` + +Both approaches access the same options registered in `builder.Services.Configure(...)`. ## Best Practices +**General:** + - **Keep middleware focused.** One responsibility per component (logging, metrics, caching, etc.). - **Always call `await next(context)`** unless you intentionally short-circuit; forgetting it prevents the handler from running. -- **Never swallow exceptions silently.** If you handle an error, set a response or log it so Lambda doesn’t +- **Never swallow exceptions silently.** If you handle an error, set a response or log it so Lambda + doesn't report success unintentionally. - **Use per-invocation state wisely.** `Items` is cleared after each request; `Properties` live for the life of the container and must be thread-safe. - **Make cancellation cooperative.** Honor `context.CancellationToken` in middleware and pass it to downstream I/O. +**Inline Middleware:** + +- Push complex logic into services so inline middleware stays thin and readable +- Use inline for orchestration, not implementation +- Great for prototyping before extracting to a class + +**Class-Based Middleware:** + +- Implement `IDisposable` or `IAsyncDisposable` if you acquire resources (connections, spans, etc.) +- Use `[FromArguments]` for configuration that varies per registration +- Inject dependencies via constructor for testability +- Share state across invocations via singleton services, not middleware fields +- Write unit tests by mocking `ILambdaInvocationContext` and the `next` delegate + +**Choosing Between Inline and Class-Based:** + +| Use Inline When... | Use Class-Based When... | +|--------------------------------------------|---------------------------------------------------| +| Middleware is application-specific | Middleware will be reused across projects | +| Logic is simple orchestration | Logic is complex or has multiple responsibilities | +| No disposal or lifecycle management needed | Need `IDisposable` or `IAsyncDisposable` support | +| Quickly prototyping or experimenting | Ready to formalize and test thoroughly | +| Tight coupling to app logic is acceptable | Clean separation of concerns is important | + With these patterns, you can build rich, testable pipelines around your Lambda handlers while keeping business logic small and focused. diff --git a/examples/MinimalLambda.Example.ClassMiddleware/MinimalLambda.Example.ClassMiddleware.csproj b/examples/MinimalLambda.Example.ClassMiddleware/MinimalLambda.Example.ClassMiddleware.csproj new file mode 100644 index 00000000..6df39b83 --- /dev/null +++ b/examples/MinimalLambda.Example.ClassMiddleware/MinimalLambda.Example.ClassMiddleware.csproj @@ -0,0 +1,30 @@ + + + Exe + net8.0 + latest + disable + enable + true + Lambda + + true + + true + $(InterceptorsNamespaces);MinimalLambda.Generated + false + + + + + + + + PreserveNewest + + + diff --git a/examples/MinimalLambda.Example.ClassMiddleware/Program.cs b/examples/MinimalLambda.Example.ClassMiddleware/Program.cs new file mode 100644 index 00000000..66036bd8 --- /dev/null +++ b/examples/MinimalLambda.Example.ClassMiddleware/Program.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MinimalLambda; +using MinimalLambda.Builder; + +var builder = LambdaApplication.CreateBuilder(); + +var lambda = builder.Build(); + +lambda.MapHandler( + ([FromEvent] Request request) => new Response($"Hello {request.Name}!", DateTime.UtcNow) +); + +await lambda.RunAsync(); + +internal record Response(string Message, DateTime TimestampUtc); + +internal record Request(string Name); + +internal class Middleware(ILogger logger) : ILambdaMiddleware +{ + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + var request = context.GetRequiredEvent(); + logger.LogInformation("Event received with name: {Name}", request.Name); + + await next(context); + + var response = context.GetRequiredResponse(); + logger.LogInformation("Response sent with message: {message}", response.Message); + } +} diff --git a/examples/MinimalLambda.Example.ClassMiddleware/Properties/launchSettings.json b/examples/MinimalLambda.Example.ClassMiddleware/Properties/launchSettings.json new file mode 100644 index 00000000..2f29c0b6 --- /dev/null +++ b/examples/MinimalLambda.Example.ClassMiddleware/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Local": { + "commandName": "Project", + "environmentVariables": { + "AWS_LAMBDA_RUNTIME_API": "localhost:5050", + "DOTNET_ENVIRONMENT": "Development", + "AWS_LAMBDA_LOG_FORMAT": "JSON", + "AWS_LAMBDA_LOG_LEVEL": "DEBUG", + "AWS_LAMBDA_FUNCTION_NAME": "MinimalLambda-Example-ClassMiddleware" + } + } + } +} diff --git a/examples/MinimalLambda.Example.ClassMiddleware/appsettings.json b/examples/MinimalLambda.Example.ClassMiddleware/appsettings.json new file mode 100644 index 00000000..9942f934 --- /dev/null +++ b/examples/MinimalLambda.Example.ClassMiddleware/appsettings.json @@ -0,0 +1,20 @@ +{ + "LambdaHost": { + "InvocationCancellationBuffer": "00:00:05" + }, + "Logging": { + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "SingleLine": true, + "IncludeScopes": false, + "TimestampFormat": "yyyy-MM-dd HH:mm:ss ", + "UseUtcTimestamp": false + } + }, + "LogLevel": { + "Microsoft": "Warning", + "Default": "Information" + } + } +} \ No newline at end of file diff --git a/src/MinimalLambda/Builder/EventAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/EventAttribute.cs similarity index 100% rename from src/MinimalLambda/Builder/EventAttribute.cs rename to src/MinimalLambda.Abstractions/Attributes/EventAttribute.cs diff --git a/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs new file mode 100644 index 00000000..367d3c59 --- /dev/null +++ b/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs @@ -0,0 +1,22 @@ +namespace MinimalLambda.Builder; + +/// Marks a middleware constructor parameter to receive its value from the arguments passed to UseMiddleware<T>(). +/// +/// +/// Parameters marked with this attribute are resolved exclusively from the args array passed to +/// UseMiddleware<T>(params object[] args). If no matching argument is found, an +/// is thrown. Without this attribute, parameters first attempt +/// resolution from args, then fall back to the DI container if no match is found. +/// +/// +/// +/// +/// internal class MyMiddleware([FromArguments] string config) : ILambdaMiddleware +/// { +/// public Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) +/// => next(context); +/// } +/// +/// +[AttributeUsage(AttributeTargets.Parameter)] +public class FromArgumentsAttribute : Attribute; diff --git a/src/MinimalLambda/Builder/FromEventAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/FromEventAttribute.cs similarity index 100% rename from src/MinimalLambda/Builder/FromEventAttribute.cs rename to src/MinimalLambda.Abstractions/Attributes/FromEventAttribute.cs diff --git a/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs new file mode 100644 index 00000000..414b8617 --- /dev/null +++ b/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs @@ -0,0 +1,21 @@ +namespace MinimalLambda.Builder; + +/// Marks a middleware constructor parameter to receive its value from the dependency injection container. +/// +/// +/// Parameters marked with this attribute are resolved exclusively from the DI container, skipping +/// argument resolution. Without this attribute, parameters first attempt resolution from args, then +/// fall back to the DI container if no match is found. +/// +/// +/// +/// +/// internal class MyMiddleware([FromServices] ILogger logger) : ILambdaMiddleware +/// { +/// public Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) +/// => next(context); +/// } +/// +/// +[AttributeUsage(AttributeTargets.Parameter)] +public class FromServicesAttribute : Attribute; diff --git a/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs new file mode 100644 index 00000000..a758deba --- /dev/null +++ b/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs @@ -0,0 +1,32 @@ +namespace MinimalLambda.Builder; + +/// Marks the constructor to use when a middleware class has multiple constructors. +/// +/// +/// By default, the source generator selects the constructor with the most parameters. Apply +/// this attribute to explicitly select a different constructor. Only one constructor per class +/// can use this attribute, otherwise a compile-time error (diagnostic LH0005) is raised. +/// +/// +/// +/// +/// internal class MyMiddleware : ILambdaMiddleware +/// { +/// public MyMiddleware(ILogger logger, IMetrics metrics) +/// { +/// // Constructor with most parameters (would be selected by default) +/// } +/// +/// [MiddlewareConstructor] +/// public MyMiddleware([FromArguments] string config) +/// { +/// // Explicitly selected constructor +/// } +/// +/// public Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) +/// => next(context); +/// } +/// +/// +[AttributeUsage(AttributeTargets.Constructor)] +public class MiddlewareConstructorAttribute : Attribute; diff --git a/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs b/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs new file mode 100644 index 00000000..037c9eaf --- /dev/null +++ b/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs @@ -0,0 +1,21 @@ +namespace MinimalLambda; + +/// Defines a middleware component for the Lambda invocation pipeline. +/// +/// +/// Middleware components are invoked in sequence during each Lambda invocation. Each middleware +/// can perform operations before and after calling the next delegate to continue the pipeline. +/// Register middleware using UseMiddleware<T>(). Constructor parameters can be +/// annotated with , , +/// or FromKeyedServicesAttribute. Use to +/// select a specific constructor when multiple exist. +/// +/// +public interface ILambdaMiddleware +{ + /// Processes a Lambda invocation. + /// The for the current invocation. + /// The delegate representing the next middleware in the pipeline. + /// A that completes when the middleware processing finishes. + Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next); +} diff --git a/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md b/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md index 805230ad..4508b3ff 100644 --- a/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -1,5 +1,12 @@ +### New Rules + + Rule ID | Category | Severity | Notes +---------|-----------------------------|----------|------------- + LH0005 | MinimalLambda.Configuration | Error | Diagnostics + LH0006 | MinimalLambda.Configuration | Error | Diagnostics + ### Removed Rules Rule ID | Category | Severity | Notes ---------|------------------------------|----------|----------------------------------- - LH0001 | MinimalLambda.Usage | Error | Multiple method calls detected \ No newline at end of file +LH0001 | MinimalLambda.Usage | Error | Multiple method calls detected \ No newline at end of file diff --git a/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs b/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs index e6d95c28..85753d13 100644 --- a/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs +++ b/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs @@ -43,6 +43,42 @@ internal static List GenerateDiagnostics(CompilationInfo compilation compilationInfo.OnShutdownInvocationInfos.GenerateKeyedServiceKeyDiagnostics() ); + foreach (var useMiddlewareTInfo in compilationInfo.UseMiddlewareTInfos) + { + // ensure middleware class is concrete + if (useMiddlewareTInfo.ClassInfo.TypeKind is "interface" or "abstract class") + { + diagnostics.Add( + Diagnostic.Create( + Diagnostics.MustBeConcreteType, + useMiddlewareTInfo.GenericTypeArgumentLocation?.ToLocation(), + useMiddlewareTInfo.ClassInfo.ShortName + ) + ); + } + + // validate that middleware class constructors only use `[MiddlewareConstructor]` once + diagnostics.AddRange( + useMiddlewareTInfo + .ClassInfo.ConstructorInfos.Where(c => + c.AttributeInfos.Any(a => + a.FullName == AttributeConstants.MiddlewareConstructor + ) + ) + .Skip(1) + .Select(c => + Diagnostic.Create( + Diagnostics.MultipleConstructorsWithAttribute, + c.AttributeInfos.First(a => + a.FullName == AttributeConstants.MiddlewareConstructor + ) + .LocationInfo?.ToLocation(), + AttributeConstants.MiddlewareConstructor + ) + ) + ); + } + return diagnostics; } diff --git a/src/MinimalLambda.SourceGenerators/Diagnostics/Diagnostics.cs b/src/MinimalLambda.SourceGenerators/Diagnostics/Diagnostics.cs index 31c9ac07..d850d6c8 100644 --- a/src/MinimalLambda.SourceGenerators/Diagnostics/Diagnostics.cs +++ b/src/MinimalLambda.SourceGenerators/Diagnostics/Diagnostics.cs @@ -34,4 +34,22 @@ internal static class Diagnostics DiagnosticSeverity.Error, true ); + + public static readonly DiagnosticDescriptor MultipleConstructorsWithAttribute = new( + "LH0005", + "Multiple constructors use attribute", + "Type contains multiple constructors that use the '{0}' attribute. Only one constructor can use this attribute.", + ConfigurationCategory, + DiagnosticSeverity.Error, + true + ); + + public static readonly DiagnosticDescriptor MustBeConcreteType = new( + "LH0006", + "Type must be a concrete class", + "The type '{0}' must be a concrete class. Interfaces, abstract classes, and other non-instantiable types cannot be used as middleware.", + ConfigurationCategory, + DiagnosticSeverity.Error, + true + ); } diff --git a/src/MinimalLambda.SourceGenerators/Extensions/TypeSymbolExtensions.cs b/src/MinimalLambda.SourceGenerators/Extensions/TypeSymbolExtensions.cs index 5dec2f61..2231e75f 100644 --- a/src/MinimalLambda.SourceGenerators/Extensions/TypeSymbolExtensions.cs +++ b/src/MinimalLambda.SourceGenerators/Extensions/TypeSymbolExtensions.cs @@ -15,5 +15,57 @@ internal bool IsTask() => internal bool IsValueTask() => typeSymbol.Name == "ValueTask" && typeSymbol.ContainingNamespace?.ToDisplayString() == "System.Threading.Tasks"; + + internal string GetTypeKind() + { + // Check if it's an interface + if (typeSymbol.TypeKind == TypeKind.Interface) + return "interface"; + + // Check if it's a class + if (typeSymbol.TypeKind == TypeKind.Class) + { + // Check if it's abstract + if (typeSymbol.IsAbstract && typeSymbol.IsSealed) + return "static class"; + + if (typeSymbol.IsAbstract) + return "abstract class"; + + if (typeSymbol.IsSealed) + return "sealed class"; + + if (typeSymbol.IsRecord) + return "record class"; + + return "class"; + } + + // Check if it's a struct + if (typeSymbol.TypeKind == TypeKind.Struct) + { + if (typeSymbol.IsRecord) + return "record struct"; + + if (typeSymbol.IsReadOnly) + return "readonly struct"; + + if (typeSymbol.IsRefLikeType) + return "ref struct"; + + return "struct"; + } + + // Check if it's an enum + if (typeSymbol.TypeKind == TypeKind.Enum) + return "enum"; + + // Check if it's a delegate + if (typeSymbol.TypeKind == TypeKind.Delegate) + return "delegate"; + + // Other types + return typeSymbol.TypeKind.ToString().ToLower(); + } } } diff --git a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs index e148405e..6bfb933d 100644 --- a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs +++ b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs @@ -10,6 +10,10 @@ internal static class TypeConstants internal const string ILambdaLifecycleContext = "global::MinimalLambda.ILambdaLifecycleContext"; + internal const string IDisposable = "global::System.IDisposable"; + + internal const string IAsyncDisposable = "global::System.IAsyncDisposable"; + internal const string CancellationToken = "global::System.Threading.CancellationToken"; internal const string Task = "global::System.Threading.Tasks.Task"; @@ -38,6 +42,13 @@ internal static class AttributeConstants internal const string FromKeyedService = "Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute"; + + internal const string MiddlewareConstructor = + "MinimalLambda.Builder.MiddlewareConstructorAttribute"; + + internal const string FromArguments = "MinimalLambda.Builder.FromArgumentsAttribute"; + + internal const string FromServices = "MinimalLambda.Builder.FromServicesAttribute"; } internal static class GeneratorConstants @@ -56,6 +67,8 @@ internal static class GeneratorConstants internal const string LambdaHostMapHandlerExtensionsTemplateFile = "Templates/MapHandler.scriban"; + internal const string UseMiddlewareTTemplateFile = "Templates/UseMiddlewareT.scriban"; + internal const string GenericHandlerTemplateFile = "Templates/GenericHandler.scriban"; internal const string LambdaHostUseOpenTelemetryTracingExtensionsTemplateFile = diff --git a/src/MinimalLambda.SourceGenerators/MapHandlerIncrementalGenerator.cs b/src/MinimalLambda.SourceGenerators/MapHandlerIncrementalGenerator.cs index 09b1f8d9..48b437f7 100644 --- a/src/MinimalLambda.SourceGenerators/MapHandlerIncrementalGenerator.cs +++ b/src/MinimalLambda.SourceGenerators/MapHandlerIncrementalGenerator.cs @@ -86,23 +86,35 @@ is CSharpCompilation .Where(static m => m is not null) .Select(static (m, _) => m!.Value); + // find UseMiddleware() calls + var useMiddlewareTCalls = context + .SyntaxProvider.CreateSyntaxProvider( + UseMiddlewareTSyntaxProvider.Predicate, + UseMiddlewareTSyntaxProvider.Transformer + ) + .Where(static m => m is not null) + .Select(static (m, _) => m!.Value); + // collect call var mapHandlerCallsCollected = mapHandlerCalls.Collect(); var onShutdownCallsCollected = onShutdownCalls.Collect(); var onInitCallsCollected = onInitCalls.Collect(); var lambdaApplicationBuilderBuildCallsCollected = lambdaApplicationBuilderBuildCalls.Collect(); + var useMiddlewareTCallsCollected = useMiddlewareTCalls.Collect(); // combine the compilation and map handler calls var combined = mapHandlerCallsCollected .Combine(onShutdownCallsCollected) .Combine(onInitCallsCollected) .Combine(lambdaApplicationBuilderBuildCallsCollected) + .Combine(useMiddlewareTCallsCollected) .Select( CompilationInfo? (t, _) => { if ( - t.Left.Left.Left.Length == 0 + t.Left.Left.Left.Left.Length == 0 + && t.Left.Left.Left.Right.Length == 0 && t.Left.Left.Right.Length == 0 && t.Left.Right.Length == 0 && t.Right.Length == 0 @@ -110,7 +122,8 @@ is CSharpCompilation return null; return new CompilationInfo( - t.Left.Left.Left.ToEquatableArray(), + t.Left.Left.Left.Left.ToEquatableArray(), + t.Left.Left.Left.Right.ToEquatableArray(), t.Left.Left.Right.ToEquatableArray(), t.Left.Right.ToEquatableArray(), t.Right.ToEquatableArray() diff --git a/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs b/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs new file mode 100644 index 00000000..76edb059 --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs @@ -0,0 +1,52 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using MinimalLambda.SourceGenerators.Extensions; +using MinimalLambda.SourceGenerators.Types; + +namespace MinimalLambda.SourceGenerators.Models; + +internal readonly record struct ClassInfo( + string GloballyQualifiedName, + string ShortName, + EquatableArray ConstructorInfos, + EquatableArray ImplementedInterfaces, + string TypeKind +); + +internal static class ClassInfoExtensions +{ + extension(ClassInfo classInfo) + { + internal static ClassInfo Create(ITypeSymbol typeSymbol) + { + var typeKind = typeSymbol.GetTypeKind(); + + // get the globally qualified name of the class + var globallyQualifiedName = typeSymbol.GetAsGlobal(); + + // get short name + var shortName = typeSymbol.Name; + + // handle each instance constructor on the type + var constructorInfo = ((INamedTypeSymbol)typeSymbol) + .InstanceConstructors.Select(MethodInfo.Create) + .ToEquatableArray(); + + // get all interfaces + var interfaceNames = typeSymbol + .AllInterfaces.Select(i => i.GetAsGlobal()) + .ToEquatableArray(); + + return new ClassInfo( + globallyQualifiedName, + shortName, + constructorInfo, + interfaceNames, + typeKind + ); + } + + internal bool IsInterfaceImplemented(string interfaceName) => + classInfo.ImplementedInterfaces.Any(i => i == interfaceName); + } +} diff --git a/src/MinimalLambda.SourceGenerators/Models/CompilationInfo.cs b/src/MinimalLambda.SourceGenerators/Models/CompilationInfo.cs index f168b3e3..4391a438 100644 --- a/src/MinimalLambda.SourceGenerators/Models/CompilationInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/CompilationInfo.cs @@ -6,5 +6,6 @@ internal readonly record struct CompilationInfo( EquatableArray MapHandlerInvocationInfos, EquatableArray OnShutdownInvocationInfos, EquatableArray OnInitInvocationInfos, - EquatableArray BuilderInfos + EquatableArray BuilderInfos, + EquatableArray UseMiddlewareTInfos ); diff --git a/src/MinimalLambda.SourceGenerators/Models/KeyedServiceKeyInfo.cs b/src/MinimalLambda.SourceGenerators/Models/KeyedServiceKeyInfo.cs index 5a0bde68..ccc5085d 100644 --- a/src/MinimalLambda.SourceGenerators/Models/KeyedServiceKeyInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/KeyedServiceKeyInfo.cs @@ -28,7 +28,7 @@ key is null { var argument = argumentList.Arguments[0]; var location = argument.Expression.GetLocation(); - var locationInfo = Models.LocationInfo.CreateFrom(location); + var locationInfo = location.CreateLocationInfo(); return keyedServiceKeyInfo with { LocationInfo = locationInfo }; } diff --git a/src/MinimalLambda.SourceGenerators/Models/LocationInfo.cs b/src/MinimalLambda.SourceGenerators/Models/LocationInfo.cs index 3365f656..2963b5de 100644 --- a/src/MinimalLambda.SourceGenerators/Models/LocationInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/LocationInfo.cs @@ -8,23 +8,37 @@ internal readonly record struct LocationInfo( string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan -) +); + +internal static class LocationInfoExtensions { - internal Location ToLocation() => Location.Create(FilePath, TextSpan, LineSpan); + extension(LocationInfo locationInfo) + { + internal Location ToLocation() => + Location.Create(locationInfo.FilePath, locationInfo.TextSpan, locationInfo.LineSpan); + } - internal static LocationInfo? CreateFrom(SyntaxNode node) => CreateFrom(node.GetLocation()); + extension(Location location) + { + internal LocationInfo? CreateLocationInfo() => + location.SourceTree is null + ? null + : new LocationInfo( + location.SourceTree.FilePath, + location.SourceSpan, + location.GetLineSpan().Span + ); + } - internal static LocationInfo? CreateFrom(ISymbol symbol) => - symbol.Locations.FirstOrDefault() is not null and var location - ? CreateFrom(location) - : null; + extension(ISymbol symbol) + { + internal LocationInfo? CreateLocationInfo() => + symbol.Locations.FirstOrDefault()?.CreateLocationInfo(); + } - internal static LocationInfo? CreateFrom(Location location) => - location.SourceTree is null - ? null - : new LocationInfo( - location.SourceTree.FilePath, - location.SourceSpan, - location.GetLineSpan().Span - ); + extension(SyntaxNode syntaxNode) + { + internal LocationInfo? CreateLocationInfo() => + syntaxNode.GetLocation().CreateLocationInfo(); + } } diff --git a/src/MinimalLambda.SourceGenerators/Models/MethodInfo.cs b/src/MinimalLambda.SourceGenerators/Models/MethodInfo.cs new file mode 100644 index 00000000..854e2621 --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/Models/MethodInfo.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using MinimalLambda.SourceGenerators.Types; + +namespace MinimalLambda.SourceGenerators.Models; + +internal readonly record struct MethodInfo( + int ArgumentCount, + EquatableArray AttributeInfos, + EquatableArray Parameters +); + +internal static class ConstructorInfoExtensions +{ + extension(MethodInfo) + { + internal static MethodInfo Create(IMethodSymbol constructor) + { + var attributeInfos = constructor + .GetAttributes() + .Where(a => a.AttributeClass is not null) + .Select(AttributeInfo.Create) + .ToEquatableArray(); + + var parameterInfos = constructor + .Parameters.Select(ParameterInfo.Create) + .ToEquatableArray(); + + return new MethodInfo(parameterInfos.Count, attributeInfos, parameterInfos); + } + } +} + +internal readonly record struct AttributeInfo(LocationInfo? LocationInfo, string FullName); + +internal static class AttributeInfoExtensions +{ + extension(AttributeInfo) + { + internal static AttributeInfo Create(AttributeData attributeData) + { + var syntax = attributeData.ApplicationSyntaxReference?.GetSyntax(); + var location = syntax?.GetLocation(); + var locationData = location?.CreateLocationInfo(); + + var name = attributeData.AttributeClass?.ToString() ?? "UNKNOWN"; + + return new AttributeInfo(locationData, name); + } + } +} diff --git a/src/MinimalLambda.SourceGenerators/Models/ParameterInfo.cs b/src/MinimalLambda.SourceGenerators/Models/ParameterInfo.cs index dba5f091..3f29672a 100644 --- a/src/MinimalLambda.SourceGenerators/Models/ParameterInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/ParameterInfo.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis; +using MinimalLambda.SourceGenerators.Types; namespace MinimalLambda.SourceGenerators.Models; @@ -10,7 +12,8 @@ internal readonly record struct ParameterInfo( ParameterSource Source, KeyedServiceKeyInfo? KeyedServiceKey, bool IsNullable, - bool IsOptional + bool IsOptional, + EquatableArray AttributeNames ) { internal bool IsRequired => !IsOptional && !IsNullable; @@ -19,13 +22,18 @@ internal static ParameterInfo Create(IParameterSymbol parameter) { var typeInfo = TypeInfo.Create(parameter.Type); var name = parameter.Name; - var location = Models.LocationInfo.CreateFrom(parameter); + var location = parameter.CreateLocationInfo(); var (source, keyedService) = GetSourceFromAttribute( parameter.GetAttributes(), typeInfo.FullyQualifiedType ); var isNullable = parameter.NullableAnnotation == NullableAnnotation.Annotated; var isOptional = parameter.IsOptional; + var attributeNames = parameter + .GetAttributes() + .Where(a => a.AttributeClass is not null) + .Select(a => a.AttributeClass!.ToString()) + .ToEquatableArray(); return new ParameterInfo( name, @@ -34,7 +42,8 @@ internal static ParameterInfo Create(IParameterSymbol parameter) source, keyedService, isNullable, - isOptional + isOptional, + attributeNames ); } diff --git a/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs b/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs new file mode 100644 index 00000000..3ff642f3 --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs @@ -0,0 +1,7 @@ +namespace MinimalLambda.SourceGenerators.Models; + +internal readonly record struct UseMiddlewareTInfo( + InterceptableLocationInfo InterceptableLocationInfo, + ClassInfo ClassInfo, + LocationInfo? GenericTypeArgumentLocation +); diff --git a/src/MinimalLambda.SourceGenerators/OutputGenerators/LambdaHostOutputGenerator.cs b/src/MinimalLambda.SourceGenerators/OutputGenerators/LambdaHostOutputGenerator.cs index f9a88a0a..391abb14 100644 --- a/src/MinimalLambda.SourceGenerators/OutputGenerators/LambdaHostOutputGenerator.cs +++ b/src/MinimalLambda.SourceGenerators/OutputGenerators/LambdaHostOutputGenerator.cs @@ -57,6 +57,15 @@ namespace MinimalLambda.Generated ) ); + // add UseMiddleware interceptors + if (compilationInfo.UseMiddlewareTInfos.Count >= 1) + outputs.Add( + UseMiddlewareTSource.Generate( + compilationInfo.UseMiddlewareTInfos, + generatedCodeAttribute + ) + ); + // add OnInit interceptors if (compilationInfo.OnInitInvocationInfos.Count >= 1) outputs.Add( diff --git a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs new file mode 100644 index 00000000..54feeba3 --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs @@ -0,0 +1,119 @@ +using System.Linq; +using MinimalLambda.SourceGenerators.Models; +using MinimalLambda.SourceGenerators.Types; + +namespace MinimalLambda.SourceGenerators; + +internal static class UseMiddlewareTSource +{ + internal static string Generate( + EquatableArray useMiddlewareTInfos, + string generatedCodeAttribute + ) + { + var useMiddlewareTCalls = useMiddlewareTInfos.Select(useMiddlewareTInfo => + { + var classInfo = useMiddlewareTInfo.ClassInfo; + + // choose what constructor to use with the following criteria: + // 1. if it has a `[MiddlewareConstructor]` attribute. Multiple of these are not valid. + // 2. default to the constructor with the most arguments + var constructor = classInfo + .ConstructorInfos.Select(c => (MethodInfo?)c) + .FirstOrDefault(c => + c!.Value.AttributeInfos.Any(a => + a.FullName == AttributeConstants.MiddlewareConstructor + ) + ); + + constructor ??= classInfo + .ConstructorInfos.OrderByDescending(c => c.ArgumentCount) + .First(); + + var parameters = constructor + .Value.Parameters.Select(p => + { + var fromArgs = p.AttributeNames.Any(n => n == AttributeConstants.FromArguments); + + // From services is defined as either having a `[FromServices]` attribute or a + // `[FromKeyedServices]` attribute + var fromServices = p.AttributeNames.Any(n => + n is AttributeConstants.FromServices or AttributeConstants.FromKeyedService + ); + + var paramAssignment = p.BuildParameterAssignment(); + + var fullyQualifiedTypeNotNull = + p.TypeInfo.FullyQualifiedType.RemoveTrailingChar("?"); + + return new + { + p.TypeInfo.FullyQualifiedType, + FullyQualifiedTypeNotNull = fullyQualifiedTypeNotNull, + p.Name, + FromArguments = fromArgs, + FromServices = fromServices, + paramAssignment.Assignment, + paramAssignment.String, + }; + }) + .ToArray(); + + var isDisposable = useMiddlewareTInfo.ClassInfo.IsInterfaceImplemented( + TypeConstants.IDisposable + ); + + var isAsyncDisposable = useMiddlewareTInfo.ClassInfo.IsInterfaceImplemented( + TypeConstants.IAsyncDisposable + ); + + var allFromServices = parameters.All(p => p.FromServices); + + return new + { + Location = useMiddlewareTInfo.InterceptableLocationInfo, + FullMiddlewareClassName = classInfo.GloballyQualifiedName, + ShortMiddlewareClassName = classInfo.ShortName, + AllFromServices = allFromServices, + Parameters = parameters, + AnyParameters = parameters.Length > 0, + IsDisposable = isDisposable, + IsAsyncDisposable = isAsyncDisposable, + }; + }); + + var template = TemplateHelper.LoadTemplate(GeneratorConstants.UseMiddlewareTTemplateFile); + + return template.Render( + new { GeneratedCodeAttribute = generatedCodeAttribute, Calls = useMiddlewareTCalls } + ); + } + + private static ParameterArg BuildParameterAssignment(this ParameterInfo param) => + new() + { + String = param.ToPublicString(), + Assignment = param.Source switch + { + // inject keyed service from the DI container - required + ParameterSource.KeyedService when param.IsRequired => + $"context.ServiceProvider.GetRequiredKeyedService<{param.TypeInfo.FullyQualifiedType}>({param.KeyedServiceKey?.DisplayValue})", + + // inject keyed service from the DI container - optional + ParameterSource.KeyedService => + $"context.ServiceProvider.GetKeyedService<{param.TypeInfo.FullyQualifiedType}>({param.KeyedServiceKey?.DisplayValue})", + + // default: inject service from the DI container - required + _ when param.IsRequired => + $"context.ServiceProvider.GetRequiredService<{param.TypeInfo.FullyQualifiedType}>()", + + // default: inject service from the DI container - optional + _ => $"context.ServiceProvider.GetService<{param.TypeInfo.FullyQualifiedType}>()", + }, + }; + + private static string RemoveTrailingChar(this string value, string trailing) => + value.EndsWith(trailing) ? value[..^1] : value; + + private readonly record struct ParameterArg(string String, string Assignment); +} diff --git a/src/MinimalLambda.SourceGenerators/SyntaxProviders/Extractors/HandlerInfoExtractor.cs b/src/MinimalLambda.SourceGenerators/SyntaxProviders/Extractors/HandlerInfoExtractor.cs index 41306e4a..161adbc4 100644 --- a/src/MinimalLambda.SourceGenerators/SyntaxProviders/Extractors/HandlerInfoExtractor.cs +++ b/src/MinimalLambda.SourceGenerators/SyntaxProviders/Extractors/HandlerInfoExtractor.cs @@ -78,7 +78,7 @@ is not IInvocationOperation return new HigherOrderMethodInfo( targetOperation.TargetMethod.Name, - LocationInfo: LocationInfo.CreateFrom(context.Node), + LocationInfo: context.Node.CreateLocationInfo(), DelegateInfo: delegateInfo.Value, InterceptableLocationInfo: InterceptableLocationInfo.CreateFrom(interceptableLocation), ArgumentsInfos: argumentInfos @@ -210,7 +210,7 @@ CastExpressionSyntax castExpression originalParam with { TypeInfo = TypeInfo.Create(castParam.Type), - LocationInfo = LocationInfo.CreateFrom(castParam), + LocationInfo = castParam.CreateLocationInfo(), } ) .ToEquatableArray(); diff --git a/src/MinimalLambda.SourceGenerators/SyntaxProviders/LambdaApplicationBuilderBuildSyntaxProvider.cs b/src/MinimalLambda.SourceGenerators/SyntaxProviders/LambdaApplicationBuilderBuildSyntaxProvider.cs index aec95558..08b16bcd 100644 --- a/src/MinimalLambda.SourceGenerators/SyntaxProviders/LambdaApplicationBuilderBuildSyntaxProvider.cs +++ b/src/MinimalLambda.SourceGenerators/SyntaxProviders/LambdaApplicationBuilderBuildSyntaxProvider.cs @@ -44,7 +44,7 @@ is IInvocationOperation return new SimpleMethodInfo( targetOperation.TargetMethod.Name, - LocationInfo.CreateFrom(context.Node), + context.Node.CreateLocationInfo(), InterceptableLocationInfo.CreateFrom(interceptableLocation) ); } diff --git a/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs b/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs new file mode 100644 index 00000000..a9a735ef --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs @@ -0,0 +1,81 @@ +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using MinimalLambda.SourceGenerators.Models; + +namespace MinimalLambda.SourceGenerators; + +internal static class UseMiddlewareTSyntaxProvider +{ + internal static bool Predicate(SyntaxNode node, CancellationToken _) => + !node.IsGeneratedFile() && node.TryGetMethodName(out var name) && name == "UseMiddleware"; + + internal static UseMiddlewareTInfo? Transformer( + GeneratorSyntaxContext context, + CancellationToken cancellationToken + ) + { + var operation = context.SemanticModel.GetOperation(context.Node, cancellationToken); + + if ( + operation + is not IInvocationOperation + { + TargetMethod: + { + IsGenericMethod: true, + ContainingAssembly.Name: "MinimalLambda", + ContainingNamespace: + { + Name: "Builder", + ContainingNamespace: + { Name: "MinimalLambda", ContainingNamespace.IsGlobalNamespace: true }, + }, + }, + } targetOperation + || !targetOperation + .TargetMethod.ConstructedFrom.TypeParameters[0] + .ConstraintTypes.Any(c => + c.Name == "ILambdaMiddleware" + && c.ContainingNamespace + is { Name: "MinimalLambda", ContainingNamespace.IsGlobalNamespace: true } + ) + ) + return null; + + // get class TypeInfo + var middlewareClassType = targetOperation.TargetMethod.TypeArguments[0]; + + // Get location of the generic argument + Location? genericArgumentLocation = null; + if ( + targetOperation.Syntax is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax { Name: GenericNameSyntax genericName }, + } + ) + { + // Get the first type argument's location + var typeArgument = genericName.TypeArgumentList.Arguments[0]; + genericArgumentLocation = typeArgument.GetLocation(); + } + + var classInfo = ClassInfo.Create(middlewareClassType); + + var interceptableLocation = context.SemanticModel.GetInterceptableLocation( + (InvocationExpressionSyntax)targetOperation.Syntax, + cancellationToken + )!; + + var useMiddlewareTInfo = new UseMiddlewareTInfo( + InterceptableLocationInfo.CreateFrom(interceptableLocation), + classInfo, + genericArgumentLocation?.CreateLocationInfo() + ); + + return useMiddlewareTInfo; + } +} diff --git a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban new file mode 100644 index 00000000..944a7467 --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban @@ -0,0 +1,126 @@ + {{ generated_code_attribute }} + file static class UseMiddlewareExtensions + { + {{~ for call in calls ~}} + [InterceptsLocation({{ call.location.version }}, "{{ call.location.data }}")] + internal static ILambdaInvocationBuilder UseMiddleware{{ for.index }}( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new {{ call.short_middleware_class_name }}Resolver{{ for.index }}(args); + + {{~ if call.is_async_disposable ~}} + builder.Use(next => + { + return async context => + { + await using var middleware = resolver.Create(context); + await middleware.InvokeAsync(context, next); + }; + }); + {{~ else if call.is_disposable ~}} + builder.Use(next => + { + return async context => + { + using var middleware = resolver.Create(context); + await middleware.InvokeAsync(context, next); + }; + }); + {{~ else ~}} + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + {{~ end ~}} + + return builder; + } + + private class {{ call.short_middleware_class_name }}Resolver{{ for.index }} + { + {{~ if call.any_parameters && !call.all_from_services ~}} + private const int NotCached = -1; + private const int FromServices = -2; + private bool _cacheBuilt = false; + {{~ end ~}} + private readonly object[] _args; + + {{~ for parameter in call.parameters ~}} + {{~ if !parameter.from_services ~}} + private int _cache{{ for.index }} = NotCached; // {{ parameter.fully_qualified_type }} + {{~ end ~}} + {{~ end ~}} + {{~ if call.any_parameters && !call.all_from_services ~}} + + {{~ end ~}} + internal {{ call.short_middleware_class_name }}Resolver{{ for.index }}(object[] args) => _args = args; + + internal {{ call.full_middleware_class_name }} Create(ILambdaInvocationContext context) + { + {{~ if call.any_parameters ~}} + {{~ if !call.all_from_services ~}} + if (!_cacheBuilt) + BuildResolutionCache(); + + {{~ end ~}} + {{~ for parameter in call.parameters ~}} + // {{ parameter.string }} + {{~ if parameter.from_services ~}} + var arg{{ for.index }} = {{ parameter.assignment }}; + {{~ else ~}} + var arg{{ for.index }} = + _cache{{ for.index }} >= 0 + ? ({{ parameter.fully_qualified_type }})_args[_cache{{ for.index }}] + {{~ if parameter.from_arguments ~}} + : throw new InvalidOperationException("Parameter '{{ parameter.name }}' of type '{{ parameter.fully_qualified_type }}' must be provided in args"); + {{~ else ~}} + : {{ parameter.assignment }}; + {{~ end ~}} + {{~ end ~}} + + {{~ end ~}} + {{~ end ~}} + return new {{ call.full_middleware_class_name }}({{ for arg in call.parameters }}arg{{ for.index }}{{ if !for.last }}, {{ end }}{{ end }}); + } + {{~ if call.any_parameters && !call.all_from_services ~}} + + private void BuildResolutionCache() + { + {{~ for parameter in call.parameters ~}} + {{~ if !parameter.from_services ~}} + _cache{{ for.index }} = FromServices; + {{~ end ~}} + {{~ end ~}} + + for (var i = 0; i < _args.Length; i++) + { + var arg = _args[i]; + if (arg is null) + continue; + + switch (arg) + { + {{~ for parameter in call.parameters ~}} + {{~ if !parameter.from_services ~}} + case {{ parameter.fully_qualified_type_not_null }} when _cache{{ for.index }} == FromServices: + _cache{{ for.index }} = i; + break; + {{~ end ~}} + {{~ end ~}} + } + } + _cacheBuilt = true; + } + {{~ end ~}} + } + {{~ if !for.last ~}} + + {{~ end ~}} + {{~ end ~}} + } diff --git a/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs b/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs new file mode 100644 index 00000000..260003cf --- /dev/null +++ b/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs @@ -0,0 +1,52 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace MinimalLambda.Builder; + +/// +/// Provides extension methods for registering class-based middleware to the Lambda invocation +/// pipeline. +/// +[ExcludeFromCodeCoverage] +public static class UseMiddlewareLambdaApplicationExtensions +{ + /// Registers a class-based middleware component with automatic dependency injection. + /// + /// + /// Source generation creates the wiring code to instantiate the middleware and resolve its + /// constructor parameters, using compile-time interceptors to replace this call. Constructor + /// parameters can be annotated with to resolve from + /// , to resolve from the DI + /// container, or FromKeyedServicesAttribute for keyed services. Use + /// to select a specific constructor when multiple + /// exist. + /// + /// + /// The middleware type implementing . + /// + /// The instance to register the middleware + /// with. + /// + /// + /// Arguments to pass to the middleware constructor for parameters marked with + /// . + /// + /// The current instance for method chaining. + /// + /// Thrown if called at runtime; this exception is + /// unreachable as this method is intercepted by the source generator code at compile time. + /// + /// + /// + /// + /// + public static ILambdaInvocationBuilder UseMiddleware( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + Debug.Fail("This method should have been intercepted at compile time!"); + throw new InvalidOperationException("This method is replaced at compile time."); + } +} diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs index a36c90b3..90754699 100644 --- a/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs @@ -150,6 +150,84 @@ public void Test_CSharpVersionTooLow() } } + [Fact] + public void Test_MiddlewareClassHasMoreThanOneMiddlewareConstructorAttribute() + { + var diagnostics = GenerateDiagnostics( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + internal class MyLambdaMiddleware : ILambdaMiddleware + { + [MiddlewareConstructor] + public MyLambdaMiddleware() { } + + [MiddlewareConstructor] + public MyLambdaMiddleware(string input) { } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + """ + ); + + diagnostics.Length.Should().Be(1); + foreach (var diagnostic in diagnostics) + { + diagnostic.Id.Should().Be("LH0005"); + diagnostic.Severity.Should().Be(DiagnosticSeverity.Error); + } + } + + [Fact] + public void Test_MiddlewareClassMustNotBeAbstractOrAnInterface() + { + var diagnostics = GenerateDiagnostics( + """ + using System.Threading.Tasks; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + lambda.UseMiddleware(); + + interface IMiddleware : ILambdaMiddleware { } + + abstract class Middleware2 : ILambdaMiddleware + { + public Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) => + next(context); + } + """ + ); + + diagnostics.Length.Should().Be(2); + foreach (var diagnostic in diagnostics) + { + diagnostic.Id.Should().Be("LH0006"); + diagnostic.Severity.Should().Be(DiagnosticSeverity.Error); + } + } + private static ImmutableArray GenerateDiagnostics( string source, LanguageVersion languageVersion = LanguageVersion.CSharp11 diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_AbstractMiddleware#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_AbstractMiddleware#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..f8e99c0b --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_AbstractMiddleware#LambdaHandler.g.verified.cs @@ -0,0 +1,80 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "bfS1EdqfgRl2Xr/8V7JcF9kAAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddleware2Resolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddleware2Resolver0 + { + private readonly object[] _args; + + internal MyLambdaMiddleware2Resolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware2 Create(ILambdaInvocationContext context) + { + return new global::MyLambdaMiddleware2(); + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_ComplexRealWorldScenario#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_ComplexRealWorldScenario#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..ec8e7175 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_ComplexRealWorldScenario#LambdaHandler.g.verified.cs @@ -0,0 +1,131 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "kDxACCQgVpkhkOccrv1uASMBAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private const int NotCached = -1; + private const int FromServices = -2; + private bool _cacheBuilt = false; + private readonly object[] _args; + + private int _cache0 = NotCached; // string + private int _cache3 = NotCached; // global::IDataService? + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + if (!_cacheBuilt) + BuildResolutionCache(); + + // ParameterInfo { Type = string, Name = name, Source = Service, IsNullable = False, IsOptional = False} + var arg0 = + _cache0 >= 0 + ? (string)_args[_cache0] + : throw new InvalidOperationException("Parameter 'name' of type 'string' must be provided in args"); + + // ParameterInfo { Type = global::ILogger, Name = logger, Source = KeyedService, IsNullable = False, IsOptional = False, KeyedServiceKeyInfo { DisplayValue = "primary", Type = string, BaseType = object } } + var arg1 = context.ServiceProvider.GetRequiredKeyedService("primary"); + + // ParameterInfo { Type = global::IMetrics, Name = metrics, Source = Service, IsNullable = False, IsOptional = False} + var arg2 = context.ServiceProvider.GetRequiredService(); + + // ParameterInfo { Type = global::IDataService?, Name = dataService, Source = Service, IsNullable = True, IsOptional = False} + var arg3 = + _cache3 >= 0 + ? (global::IDataService?)_args[_cache3] + : context.ServiceProvider.GetService(); + + return new global::MyLambdaMiddleware(arg0, arg1, arg2, arg3); + } + + private void BuildResolutionCache() + { + _cache0 = FromServices; + _cache3 = FromServices; + + for (var i = 0; i < _args.Length; i++) + { + var arg = _args[i]; + if (arg is null) + continue; + + switch (arg) + { + case string when _cache0 == FromServices: + _cache0 = i; + break; + case global::IDataService when _cache3 == FromServices: + _cache3 = i; + break; + } + } + _cacheBuilt = true; + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_ConstructorWithArgs#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_ConstructorWithArgs#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..d23e0240 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_ConstructorWithArgs#LambdaHandler.g.verified.cs @@ -0,0 +1,114 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "POU0h/pUESMeSE5RxaKDvtkAAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private const int NotCached = -1; + private const int FromServices = -2; + private bool _cacheBuilt = false; + private readonly object[] _args; + + private int _cache0 = NotCached; // global::IService + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + if (!_cacheBuilt) + BuildResolutionCache(); + + // ParameterInfo { Type = global::IService, Name = service, Source = Service, IsNullable = False, IsOptional = False} + var arg0 = + _cache0 >= 0 + ? (global::IService)_args[_cache0] + : context.ServiceProvider.GetRequiredService(); + + return new global::MyLambdaMiddleware(arg0); + } + + private void BuildResolutionCache() + { + _cache0 = FromServices; + + for (var i = 0; i < _args.Length; i++) + { + var arg = _args[i]; + if (arg is null) + continue; + + switch (arg) + { + case global::IService when _cache0 == FromServices: + _cache0 = i; + break; + } + } + _cacheBuilt = true; + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromArgumentsAttribute#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromArgumentsAttribute#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..b2d37c02 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromArgumentsAttribute#LambdaHandler.g.verified.cs @@ -0,0 +1,114 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "2PY/UkDSZ0gJKBmOx8LHA/MAAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private const int NotCached = -1; + private const int FromServices = -2; + private bool _cacheBuilt = false; + private readonly object[] _args; + + private int _cache0 = NotCached; // string + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + if (!_cacheBuilt) + BuildResolutionCache(); + + // ParameterInfo { Type = string, Name = apiKey, Source = Service, IsNullable = False, IsOptional = False} + var arg0 = + _cache0 >= 0 + ? (string)_args[_cache0] + : throw new InvalidOperationException("Parameter 'apiKey' of type 'string' must be provided in args"); + + return new global::MyLambdaMiddleware(arg0); + } + + private void BuildResolutionCache() + { + _cache0 = FromServices; + + for (var i = 0; i < _args.Length; i++) + { + var arg = _args[i]; + if (arg is null) + continue; + + switch (arg) + { + case string when _cache0 == FromServices: + _cache0 = i; + break; + } + } + _cacheBuilt = true; + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromKeyedServicesAttribute#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromKeyedServicesAttribute#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..bef136e7 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromKeyedServicesAttribute#LambdaHandler.g.verified.cs @@ -0,0 +1,83 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "tvudG5XDjp+n6D0V7XtOegkBAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private readonly object[] _args; + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + // ParameterInfo { Type = global::IService, Name = service, Source = KeyedService, IsNullable = False, IsOptional = False, KeyedServiceKeyInfo { DisplayValue = "myKey", Type = string, BaseType = object } } + var arg0 = context.ServiceProvider.GetRequiredKeyedService("myKey"); + + return new global::MyLambdaMiddleware(arg0); + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromServicesAttribute#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromServicesAttribute#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..9abbaeb7 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromServicesAttribute#LambdaHandler.g.verified.cs @@ -0,0 +1,83 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "Sf77NTiY6mQYL0S/Tk+LNwkBAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private readonly object[] _args; + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + // ParameterInfo { Type = global::IService, Name = service, Source = Service, IsNullable = False, IsOptional = False} + var arg0 = context.ServiceProvider.GetRequiredService(); + + return new global::MyLambdaMiddleware(arg0); + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_IAsyncDisposable#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_IAsyncDisposable#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..0a6e7049 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_IAsyncDisposable#LambdaHandler.g.verified.cs @@ -0,0 +1,81 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "p6F9mr5nQrDoT4xxrawd2+cAAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return async context => + { + await using var middleware = resolver.Create(context); + await middleware.InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private readonly object[] _args; + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + return new global::MyLambdaMiddleware(); + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_IDisposable#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_IDisposable#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..ea7693b4 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_IDisposable#LambdaHandler.g.verified.cs @@ -0,0 +1,81 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "lRt7PioesBPod6+JZEeC7+cAAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return async context => + { + using var middleware = resolver.Create(context); + await middleware.InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private readonly object[] _args; + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + return new global::MyLambdaMiddleware(); + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_MixedParameterSources#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_MixedParameterSources#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..eb499bd9 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_MixedParameterSources#LambdaHandler.g.verified.cs @@ -0,0 +1,131 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "pTRjukdFvWAmjtqI7hgUkQkBAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private const int NotCached = -1; + private const int FromServices = -2; + private bool _cacheBuilt = false; + private readonly object[] _args; + + private int _cache2 = NotCached; // string + private int _cache3 = NotCached; // global::IMetrics? + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + if (!_cacheBuilt) + BuildResolutionCache(); + + // ParameterInfo { Type = global::ILogger, Name = logger, Source = Service, IsNullable = False, IsOptional = False} + var arg0 = context.ServiceProvider.GetRequiredService(); + + // ParameterInfo { Type = global::ICache, Name = cache, Source = KeyedService, IsNullable = False, IsOptional = False, KeyedServiceKeyInfo { DisplayValue = "cache", Type = string, BaseType = object } } + var arg1 = context.ServiceProvider.GetRequiredKeyedService("cache"); + + // ParameterInfo { Type = string, Name = apiKey, Source = Service, IsNullable = False, IsOptional = False} + var arg2 = + _cache2 >= 0 + ? (string)_args[_cache2] + : throw new InvalidOperationException("Parameter 'apiKey' of type 'string' must be provided in args"); + + // ParameterInfo { Type = global::IMetrics?, Name = metrics, Source = Service, IsNullable = True, IsOptional = False} + var arg3 = + _cache3 >= 0 + ? (global::IMetrics?)_args[_cache3] + : context.ServiceProvider.GetService(); + + return new global::MyLambdaMiddleware(arg0, arg1, arg2, arg3); + } + + private void BuildResolutionCache() + { + _cache2 = FromServices; + _cache3 = FromServices; + + for (var i = 0; i < _args.Length; i++) + { + var arg = _args[i]; + if (arg is null) + continue; + + switch (arg) + { + case string when _cache2 == FromServices: + _cache2 = i; + break; + case global::IMetrics when _cache3 == FromServices: + _cache3 = i; + break; + } + } + _cacheBuilt = true; + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_MultipleConstructorsAndOneWithMiddlewareConstructor#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_MultipleConstructorsAndOneWithMiddlewareConstructor#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..14513dbc --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_MultipleConstructorsAndOneWithMiddlewareConstructor#LambdaHandler.g.verified.cs @@ -0,0 +1,80 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "Y0YW1Yh6hjdh+sRDAe9fDtkAAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private readonly object[] _args; + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + return new global::MyLambdaMiddleware(); + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_NullableParameter#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_NullableParameter#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..1bb2a0d7 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_NullableParameter#LambdaHandler.g.verified.cs @@ -0,0 +1,114 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "RL5SM4QdQyvDIXuCIsrd3tkAAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private const int NotCached = -1; + private const int FromServices = -2; + private bool _cacheBuilt = false; + private readonly object[] _args; + + private int _cache0 = NotCached; // global::IService? + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + if (!_cacheBuilt) + BuildResolutionCache(); + + // ParameterInfo { Type = global::IService?, Name = service, Source = Service, IsNullable = True, IsOptional = False} + var arg0 = + _cache0 >= 0 + ? (global::IService?)_args[_cache0] + : context.ServiceProvider.GetService(); + + return new global::MyLambdaMiddleware(arg0); + } + + private void BuildResolutionCache() + { + _cache0 = FromServices; + + for (var i = 0; i < _args.Length; i++) + { + var arg = _args[i]; + if (arg is null) + continue; + + switch (arg) + { + case global::IService when _cache0 == FromServices: + _cache0 = i; + break; + } + } + _cacheBuilt = true; + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_OptionalParameterWithDefaultValue#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_OptionalParameterWithDefaultValue#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..86f954ca --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_OptionalParameterWithDefaultValue#LambdaHandler.g.verified.cs @@ -0,0 +1,114 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "OgunNbI2p9A/xk9Pbp7U99kAAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private const int NotCached = -1; + private const int FromServices = -2; + private bool _cacheBuilt = false; + private readonly object[] _args; + + private int _cache0 = NotCached; // string + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + if (!_cacheBuilt) + BuildResolutionCache(); + + // ParameterInfo { Type = string, Name = name, Source = Service, IsNullable = False, IsOptional = True} + var arg0 = + _cache0 >= 0 + ? (string)_args[_cache0] + : context.ServiceProvider.GetService(); + + return new global::MyLambdaMiddleware(arg0); + } + + private void BuildResolutionCache() + { + _cache0 = FromServices; + + for (var i = 0; i < _args.Length; i++) + { + var arg = _args[i]; + if (arg is null) + continue; + + switch (arg) + { + case string when _cache0 == FromServices: + _cache0 = i; + break; + } + } + _cacheBuilt = true; + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_Simple#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_Simple#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..77dfdf5b --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_Simple#LambdaHandler.g.verified.cs @@ -0,0 +1,80 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "Yw0irwbfb7fSIvQDMI+AOdkAAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private readonly object[] _args; + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + return new global::MyLambdaMiddleware(); + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_WithArgsArray#LambdaHandler.g.verified.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_WithArgsArray#LambdaHandler.g.verified.cs new file mode 100644 index 00000000..5b03fe94 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_WithArgsArray#LambdaHandler.g.verified.cs @@ -0,0 +1,125 @@ +//HintName: LambdaHandler.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +namespace System.Runtime.CompilerServices +{ + using System.CodeDom.Compiler; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace MinimalLambda.Generated +{ + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + [GeneratedCode("MinimalLambda.SourceGenerators", "0.0.0")] + file static class UseMiddlewareExtensions + { + [InterceptsLocation(1, "8R28K8qJLzNQRb0o39t1a9kAAABJbnB1dEZpbGUuY3M=")] + internal static ILambdaInvocationBuilder UseMiddleware0( + this ILambdaInvocationBuilder builder, + params object[] args + ) + where T : ILambdaMiddleware + { + var resolver = new MyLambdaMiddlewareResolver0(args); + + builder.Use(next => + { + return context => + { + return resolver.Create(context).InvokeAsync(context, next); + }; + }); + + return builder; + } + + private class MyLambdaMiddlewareResolver0 + { + private const int NotCached = -1; + private const int FromServices = -2; + private bool _cacheBuilt = false; + private readonly object[] _args; + + private int _cache0 = NotCached; // string + private int _cache1 = NotCached; // global::IService + + internal MyLambdaMiddlewareResolver0(object[] args) => _args = args; + + internal global::MyLambdaMiddleware Create(ILambdaInvocationContext context) + { + if (!_cacheBuilt) + BuildResolutionCache(); + + // ParameterInfo { Type = string, Name = apiKey, Source = Service, IsNullable = False, IsOptional = False} + var arg0 = + _cache0 >= 0 + ? (string)_args[_cache0] + : context.ServiceProvider.GetRequiredService(); + + // ParameterInfo { Type = global::IService, Name = service, Source = Service, IsNullable = False, IsOptional = False} + var arg1 = + _cache1 >= 0 + ? (global::IService)_args[_cache1] + : context.ServiceProvider.GetRequiredService(); + + return new global::MyLambdaMiddleware(arg0, arg1); + } + + private void BuildResolutionCache() + { + _cache0 = FromServices; + _cache1 = FromServices; + + for (var i = 0; i < _args.Length; i++) + { + var arg = _args[i]; + if (arg is null) + continue; + + switch (arg) + { + case string when _cache0 == FromServices: + _cache0 = i; + break; + case global::IService when _cache1 == FromServices: + _cache1 = i; + break; + } + } + _cacheBuilt = true; + } + } + } + + file static class Utilities + { + internal static T Cast(Delegate d, T _) where T : Delegate => (T)d; + } +} \ No newline at end of file diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs new file mode 100644 index 00000000..1e5f6ba9 --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs @@ -0,0 +1,454 @@ +namespace MinimalLambda.SourceGenerators.UnitTests; + +public class UseMiddlewareTVerifyTests +{ + [Fact] + public async Task Test_MiddlewareClass_Simple() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware : ILambdaMiddleware + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_AbstractMiddleware() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal abstract class MyLambdaMiddleware : ILambdaMiddleware + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + + internal class MyLambdaMiddleware2 : MyLambdaMiddleware + { + public string Do() => "Hello World!"; + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_ConstructorWithArgs() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware : ILambdaMiddleware + { + private readonly IService _service; + + internal MyLambdaMiddleware(IService service) + { + _service = service; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + + internal interface IService + { + string GetMessage(); + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_MultipleConstructorsAndOneWithMiddlewareConstructor() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware : ILambdaMiddleware + { + private readonly IService _service; + + internal MyLambdaMiddleware(IService service) + { + _service = service; + } + + [MiddlewareConstructor] + internal MyLambdaMiddleware() + { + _service = null!; + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + + internal interface IService + { + string GetMessage(); + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_FromServicesAttribute() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware( + [FromServices] IService service + ) : ILambdaMiddleware + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + + internal interface IService + { + string GetMessage(); + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_FromKeyedServicesAttribute() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware( + [FromKeyedServices("myKey")] IService service + ) : ILambdaMiddleware + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + + internal interface IService + { + string GetMessage(); + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_FromArgumentsAttribute() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Amazon.Lambda.Core; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware( + [FromArguments] string apiKey + ) : ILambdaMiddleware + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_NullableParameter() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware( + IService? service + ) : ILambdaMiddleware + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + + internal interface IService + { + string GetMessage(); + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_OptionalParameterWithDefaultValue() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware : ILambdaMiddleware + { + internal MyLambdaMiddleware(string name = "default") + { + } + + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_MixedParameterSources() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.DependencyInjection; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware( + [FromServices] ILogger logger, + [FromKeyedServices("cache")] ICache cache, + [FromArguments] string apiKey, + IMetrics? metrics + ) : ILambdaMiddleware + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + + internal interface ILogger { } + internal interface ICache { } + internal interface IMetrics { } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_WithArgsArray() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware("myApiKey"); + + internal class MyLambdaMiddleware( + string apiKey, + IService service + ) : ILambdaMiddleware + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + + internal interface IService + { + string GetMessage(); + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_ComplexRealWorldScenario() => + await GeneratorTestHelpers.Verify( + """ + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.DependencyInjection; + using Amazon.Lambda.Core; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware( + [FromArguments] string name, + [FromKeyedServices("primary")] ILogger logger, + [FromServices] IMetrics metrics, + IDataService? dataService + ) : ILambdaMiddleware + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + } + + internal interface ILogger { } + internal interface IMetrics { } + internal interface IDataService { } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_IDisposable() => + await GeneratorTestHelpers.Verify( + """ + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware : ILambdaMiddleware, IDisposable + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + + public void Dispose() { } + } + """ + ); + + [Fact] + public async Task Test_MiddlewareClass_IAsyncDisposable() => + await GeneratorTestHelpers.Verify( + """ + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using MinimalLambda; + using MinimalLambda.Builder; + + var builder = LambdaApplication.CreateBuilder(); + + await using var lambda = builder.Build(); + + lambda.UseMiddleware(); + + internal class MyLambdaMiddleware : ILambdaMiddleware, IAsyncDisposable + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + + public async ValueTask DisposeAsync() { } + } + """ + ); +}