From e0985eb022c0760f4fd3d33f79c5b3a74064e625 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Wed, 17 Dec 2025 09:46:43 -0500 Subject: [PATCH 01/25] chore(build): bump version to 2.0.0-beta.11 - Updated version in Directory.Build.props file. --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From f747006b8f9bc00a5553471bc396237ccabbb8d6 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Wed, 17 Dec 2025 11:24:53 -0500 Subject: [PATCH 02/25] feat(examples): add MinimalLambda.Example.ClassMiddleware project - Introduced a new example project for Class Middleware. - Configured project settings, references, and dependencies. - Added appsettings.json for logging and LambdaHost configuration. - Included launchSettings.json with environment variable presets. --- MinimalLambda.sln | 15 ++++++++++ ...nimalLambda.Example.ClassMiddleware.csproj | 30 +++++++++++++++++++ .../Properties/launchSettings.json | 15 ++++++++++ .../appsettings.json | 20 +++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 examples/MinimalLambda.Example.ClassMiddleware/MinimalLambda.Example.ClassMiddleware.csproj create mode 100644 examples/MinimalLambda.Example.ClassMiddleware/Properties/launchSettings.json create mode 100644 examples/MinimalLambda.Example.ClassMiddleware/appsettings.json 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/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/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 From 7a439f56fee69f3157491721a440aa6b39a2ffa9 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Wed, 17 Dec 2025 11:25:22 -0500 Subject: [PATCH 03/25] feat(examples): add Program.cs to MinimalLambda.Example.ClassMiddleware - Set up a Lambda application builder and configured request/response handling. - Implemented a simple event handler mapping example. - Added `Request` and `Response` record definitions for the example. --- .../Program.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 examples/MinimalLambda.Example.ClassMiddleware/Program.cs diff --git a/examples/MinimalLambda.Example.ClassMiddleware/Program.cs b/examples/MinimalLambda.Example.ClassMiddleware/Program.cs new file mode 100644 index 00000000..59fd1993 --- /dev/null +++ b/examples/MinimalLambda.Example.ClassMiddleware/Program.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.Hosting; +using MinimalLambda.Builder; + +// Create the application builder +var builder = LambdaApplication.CreateBuilder(); + +// Build the Lambda application +var lambda = builder.Build(); + +// Map your handler - the event is automatically injected +lambda.MapHandler( + ([FromEvent] Request request) => new Response($"Hello {request.Name}!", DateTime.UtcNow) +); + +// Run the Lambda +await lambda.RunAsync(); + +internal record Response(string Message, DateTime TimestampUtc); + +internal record Request(string Name); From 86b1b2ef325f318101caa435bae7e6a870d2514d Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Wed, 17 Dec 2025 17:13:58 -0500 Subject: [PATCH 04/25] feat(source-generators): add support for `UseMiddleware()` - Implemented `ILambdaMiddleware` interface to define middleware behavior. - Added `UseMiddleware()` syntax interception using source generators. - Introduced `UseMiddlewareTInfo` struct to store middleware-related metadata. - Extended incremental generator for middleware syntax analysis and metadata collection. - Created `MiddlewareConstructorAttribute` and `FromArgumentsAttribute` for middleware constructors. - Included unit tests to verify `UseMiddleware()` functionality. --- .../Attributes/FromArgumentsAttribute.cs | 4 + .../MiddlewareConstructorAttribute.cs | 4 + .../Core/ILambdaMiddleware.cs | 6 + .../MapHandlerIncrementalGenerator.cs | 17 ++- .../Models/CompilationInfo.cs | 3 +- .../Models/UseMiddlewareTInfo.cs | 13 ++ .../UseMiddlewareTSyntaxProvider.cs | 80 ++++++++++++ ...seMiddlewareLambdaApplicationExtensions.cs | 15 +++ .../VerifyTests/UseMiddlewareTVerifyTests.cs | 116 ++++++++++++++++++ 9 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs create mode 100644 src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs create mode 100644 src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs create mode 100644 src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs create mode 100644 src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs create mode 100644 src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs diff --git a/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs new file mode 100644 index 00000000..63effb2e --- /dev/null +++ b/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs @@ -0,0 +1,4 @@ +namespace MinimalLambda; + +[AttributeUsage(AttributeTargets.Parameter)] +public class FromArgumentsAttribute : Attribute; diff --git a/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs new file mode 100644 index 00000000..ea0c2687 --- /dev/null +++ b/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs @@ -0,0 +1,4 @@ +namespace MinimalLambda; + +[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..03520d1e --- /dev/null +++ b/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs @@ -0,0 +1,6 @@ +namespace MinimalLambda; + +public interface ILambdaMiddleware +{ + Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next); +} 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/CompilationInfo.cs b/src/MinimalLambda.SourceGenerators/Models/CompilationInfo.cs index f168b3e3..544e3aad 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 UseMiddlewareTInfo ); diff --git a/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs b/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs new file mode 100644 index 00000000..66254a1b --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs @@ -0,0 +1,13 @@ +using MinimalLambda.SourceGenerators.Types; + +namespace MinimalLambda.SourceGenerators.Models; + +internal readonly record struct UseMiddlewareTInfo( + InterceptableLocationInfo InterceptableLocationInfo, + ClassInfo ClassInfo, + EquatableArray ConstructorInfos +); + +internal readonly record struct ClassInfo(string GloballyQualifiedName); + +internal readonly record struct ConstructorInfo(int ArgumentCount); diff --git a/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs b/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs new file mode 100644 index 00000000..bcc39db3 --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs @@ -0,0 +1,80 @@ +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.Extensions; +using MinimalLambda.SourceGenerators.Models; +using MinimalLambda.SourceGenerators.Types; + +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 globally qualified name of the class + var globallyQualifiedName = middlewareClassType.GetAsGlobal(); + + // handle each instance constructor on the type + var constructorInfo = ((INamedTypeSymbol)middlewareClassType) + .InstanceConstructors.Select(constructor => + { + return new ConstructorInfo(0); + }) + .ToEquatableArray(); + + var classInfo = new ClassInfo { GloballyQualifiedName = globallyQualifiedName }; + + var interceptableLocation = context.SemanticModel.GetInterceptableLocation( + (InvocationExpressionSyntax)targetOperation.Syntax, + cancellationToken + )!; + + var useMiddlewareTInfo = new UseMiddlewareTInfo( + InterceptableLocationInfo.CreateFrom(interceptableLocation), + classInfo, + constructorInfo + ); + + return useMiddlewareTInfo; + } +} diff --git a/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs b/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs new file mode 100644 index 00000000..fcb3121e --- /dev/null +++ b/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace MinimalLambda.Builder; + +[ExcludeFromCodeCoverage] +public static class UseMiddlewareLambdaApplicationExtensions +{ + public static ILambdaOnInitBuilder UseMiddleware(this ILambdaOnInitBuilder _) + 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/VerifyTests/UseMiddlewareTVerifyTests.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs new file mode 100644 index 00000000..fd99317d --- /dev/null +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs @@ -0,0 +1,116 @@ +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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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_() => + await GeneratorTestHelpers.Verify( + """ + + """ + ); +} From 36f7194b92100cdb925aee53a0f7e4fcbc6ac255 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Wed, 17 Dec 2025 17:46:02 -0500 Subject: [PATCH 05/25] feat(source-generators): enhance middleware metadata support - Added support for capturing multiple constructors and attributes in `ConstructorInfo`. - Introduced `ClassInfo.Create()` and `ConstructorInfo.Create()` for metadata generation. - Updated `UseMiddlewareTInfo` to eliminate redundant `ConstructorInfos` field. - Extended `ParameterInfo` with attribute name tracking via `AttributeNames`. - Adjusted `UseMiddlewareTSyntaxProvider` to use the updated `ClassInfo` structure. - Enhanced unit tests to validate middleware with multiple constructors and attributes. --- .../GeneratorConstants.cs | 5 ++ .../Models/ClassInfo.cs | 30 ++++++++++++ .../Models/ConstructorInfo.cs | 32 +++++++++++++ .../Models/ParameterInfo.cs | 13 ++++- .../Models/UseMiddlewareTInfo.cs | 9 +--- .../UseMiddlewareTSyntaxProvider.cs | 18 +------ .../VerifyTests/UseMiddlewareTVerifyTests.cs | 47 +++++++++++++++++++ 7 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs create mode 100644 src/MinimalLambda.SourceGenerators/Models/ConstructorInfo.cs diff --git a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs index e148405e..883ed044 100644 --- a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs +++ b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs @@ -38,6 +38,11 @@ internal static class AttributeConstants internal const string FromKeyedService = "Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute"; + + internal const string MiddlewareConstructor = + "global::MinimalLambda.MiddlewareConstructorAttribute"; + + internal const string FromArguments = "global::MinimalLambda.FromArgumentsAttribute"; } internal static class GeneratorConstants diff --git a/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs b/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs new file mode 100644 index 00000000..fa97c6f3 --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs @@ -0,0 +1,30 @@ +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, + EquatableArray ConstructorInfos +); + +internal static class ClassInfoExtensions +{ + extension(ClassInfo) + { + internal static ClassInfo Create(ITypeSymbol typeSymbol) + { + // get the globally qualified name of the class + var globallyQualifiedName = typeSymbol.GetAsGlobal(); + + // handle each instance constructor on the type + var constructorInfo = ((INamedTypeSymbol)typeSymbol) + .InstanceConstructors.Select(ConstructorInfo.Create) + .ToEquatableArray(); + + return new ClassInfo(globallyQualifiedName, constructorInfo); + } + } +} diff --git a/src/MinimalLambda.SourceGenerators/Models/ConstructorInfo.cs b/src/MinimalLambda.SourceGenerators/Models/ConstructorInfo.cs new file mode 100644 index 00000000..b622fb0d --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/Models/ConstructorInfo.cs @@ -0,0 +1,32 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using MinimalLambda.SourceGenerators.Types; + +namespace MinimalLambda.SourceGenerators.Models; + +internal readonly record struct ConstructorInfo( + int ArgumentCount, + EquatableArray Attributes, + EquatableArray Parameters +); + +internal static class ConstructorInfoExtensions +{ + extension(ConstructorInfo) + { + internal static ConstructorInfo Create(IMethodSymbol constructor) + { + var attributeNames = constructor + .GetAttributes() + .Where(a => a.AttributeClass is not null) + .Select(a => a.AttributeClass!.ToString()) + .ToEquatableArray(); + + var parameterInfos = constructor + .Parameters.Select(ParameterInfo.Create) + .ToEquatableArray(); + + return new ConstructorInfo(parameterInfos.Count, attributeNames, parameterInfos); + } + } +} diff --git a/src/MinimalLambda.SourceGenerators/Models/ParameterInfo.cs b/src/MinimalLambda.SourceGenerators/Models/ParameterInfo.cs index dba5f091..65809fc3 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; @@ -26,6 +29,11 @@ internal static ParameterInfo Create(IParameterSymbol parameter) ); 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 index 66254a1b..94872c1b 100644 --- a/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs @@ -1,13 +1,6 @@ -using MinimalLambda.SourceGenerators.Types; - namespace MinimalLambda.SourceGenerators.Models; internal readonly record struct UseMiddlewareTInfo( InterceptableLocationInfo InterceptableLocationInfo, - ClassInfo ClassInfo, - EquatableArray ConstructorInfos + ClassInfo ClassInfo ); - -internal readonly record struct ClassInfo(string GloballyQualifiedName); - -internal readonly record struct ConstructorInfo(int ArgumentCount); diff --git a/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs b/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs index bcc39db3..5d0ac0c8 100644 --- a/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs +++ b/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs @@ -4,9 +4,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; -using MinimalLambda.SourceGenerators.Extensions; using MinimalLambda.SourceGenerators.Models; -using MinimalLambda.SourceGenerators.Types; namespace MinimalLambda.SourceGenerators; @@ -51,18 +49,7 @@ is not IInvocationOperation // get class TypeInfo var middlewareClassType = targetOperation.TargetMethod.TypeArguments[0]; - // get globally qualified name of the class - var globallyQualifiedName = middlewareClassType.GetAsGlobal(); - - // handle each instance constructor on the type - var constructorInfo = ((INamedTypeSymbol)middlewareClassType) - .InstanceConstructors.Select(constructor => - { - return new ConstructorInfo(0); - }) - .ToEquatableArray(); - - var classInfo = new ClassInfo { GloballyQualifiedName = globallyQualifiedName }; + var classInfo = ClassInfo.Create(middlewareClassType); var interceptableLocation = context.SemanticModel.GetInterceptableLocation( (InvocationExpressionSyntax)targetOperation.Syntax, @@ -71,8 +58,7 @@ is not IInvocationOperation var useMiddlewareTInfo = new UseMiddlewareTInfo( InterceptableLocationInfo.CreateFrom(interceptableLocation), - classInfo, - constructorInfo + classInfo ); return useMiddlewareTInfo; diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs index fd99317d..33e26672 100644 --- a/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs @@ -106,6 +106,53 @@ internal interface IService """ ); + [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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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_() => await GeneratorTestHelpers.Verify( From 032ae7d14fb7d6e582d9114aceeaad5561016ac8 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Wed, 17 Dec 2025 18:01:32 -0500 Subject: [PATCH 06/25] feat(source-generators): add support for generating `UseMiddleware` calls - Implemented `UseMiddlewareTSource.Generate` for rendering middleware invocation calls. - Updated `LambdaHostOutputGenerator` to include middleware calls in generated output. - Renamed `UseMiddlewareTInfo` to `UseMiddlewareTInfos` in `CompilationInfo` for consistency. --- .../Models/CompilationInfo.cs | 2 +- .../LambdaHostOutputGenerator.cs | 9 ++++++ .../OutputGenerators/UseMiddlewareTSource.cs | 31 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs diff --git a/src/MinimalLambda.SourceGenerators/Models/CompilationInfo.cs b/src/MinimalLambda.SourceGenerators/Models/CompilationInfo.cs index 544e3aad..4391a438 100644 --- a/src/MinimalLambda.SourceGenerators/Models/CompilationInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/CompilationInfo.cs @@ -7,5 +7,5 @@ internal readonly record struct CompilationInfo( EquatableArray OnShutdownInvocationInfos, EquatableArray OnInitInvocationInfos, EquatableArray BuilderInfos, - EquatableArray UseMiddlewareTInfo + EquatableArray UseMiddlewareTInfos ); 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..2efe94fe --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs @@ -0,0 +1,31 @@ +using System.Linq; +using MinimalLambda.SourceGenerators.Models; +using MinimalLambda.SourceGenerators.Types; + +namespace MinimalLambda.SourceGenerators; + +public class UseMiddlewareTSource +{ + internal static string Generate( + EquatableArray useMiddlewareTInfos, + string generatedCodeAttribute + ) + { + var useMiddlewareTCalls = useMiddlewareTInfos.Select(useMiddlewareTInfo => + { + return new { }; + }); + + var template = TemplateHelper.LoadTemplate( + GeneratorConstants.LambdaHostMapHandlerExtensionsTemplateFile + ); + + return template.Render( + new + { + GeneratedCodeAttribute = generatedCodeAttribute, + UseMiddlewareCalls = useMiddlewareTCalls, + } + ); + } +} From 0d32f8aa953fed072b0076ab53e5ab9444f9c0a2 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Wed, 17 Dec 2025 19:10:49 -0500 Subject: [PATCH 07/25] feat(source-generators): enhance `UseMiddleware` generation with constructor caching and templates - Introduced `UseMiddlewareT.scriban` template for generating middleware invocation methods. - Added constructor caching mechanism in `UseMiddlewareExtensions` for optimized resolution. - Replaced `ConstructorInfo` with `MethodInfo` for improved compatibility and flexibility. - Updated `ClassInfo` to include `ShortName` for contextual middleware references. - Adjusted `UseMiddlewareTSource` to prioritize `[MiddlewareConstructor]` or default constructors. - Refactored unit tests to reflect changes in middleware constructor handling. --- .../GeneratorConstants.cs | 2 + .../Models/ClassInfo.cs | 10 ++- .../Models/ConstructorInfo.cs | 32 -------- .../Models/MethodInfo.cs | 51 ++++++++++++ .../OutputGenerators/UseMiddlewareTSource.cs | 35 ++++++--- .../Templates/UseMiddlewareT.scriban | 78 +++++++++++++++++++ .../VerifyTests/UseMiddlewareTVerifyTests.cs | 2 +- 7 files changed, 165 insertions(+), 45 deletions(-) delete mode 100644 src/MinimalLambda.SourceGenerators/Models/ConstructorInfo.cs create mode 100644 src/MinimalLambda.SourceGenerators/Models/MethodInfo.cs create mode 100644 src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban diff --git a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs index 883ed044..c1da2863 100644 --- a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs +++ b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs @@ -61,6 +61,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/Models/ClassInfo.cs b/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs index fa97c6f3..6ac36369 100644 --- a/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs @@ -7,7 +7,8 @@ namespace MinimalLambda.SourceGenerators.Models; internal readonly record struct ClassInfo( string GloballyQualifiedName, - EquatableArray ConstructorInfos + string ShortName, + EquatableArray ConstructorInfos ); internal static class ClassInfoExtensions @@ -19,12 +20,15 @@ internal static ClassInfo Create(ITypeSymbol typeSymbol) // 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(ConstructorInfo.Create) + .InstanceConstructors.Select(MethodInfo.Create) .ToEquatableArray(); - return new ClassInfo(globallyQualifiedName, constructorInfo); + return new ClassInfo(globallyQualifiedName, shortName, constructorInfo); } } } diff --git a/src/MinimalLambda.SourceGenerators/Models/ConstructorInfo.cs b/src/MinimalLambda.SourceGenerators/Models/ConstructorInfo.cs deleted file mode 100644 index b622fb0d..00000000 --- a/src/MinimalLambda.SourceGenerators/Models/ConstructorInfo.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Linq; -using Microsoft.CodeAnalysis; -using MinimalLambda.SourceGenerators.Types; - -namespace MinimalLambda.SourceGenerators.Models; - -internal readonly record struct ConstructorInfo( - int ArgumentCount, - EquatableArray Attributes, - EquatableArray Parameters -); - -internal static class ConstructorInfoExtensions -{ - extension(ConstructorInfo) - { - internal static ConstructorInfo Create(IMethodSymbol constructor) - { - var attributeNames = constructor - .GetAttributes() - .Where(a => a.AttributeClass is not null) - .Select(a => a.AttributeClass!.ToString()) - .ToEquatableArray(); - - var parameterInfos = constructor - .Parameters.Select(ParameterInfo.Create) - .ToEquatableArray(); - - return new ConstructorInfo(parameterInfos.Count, attributeNames, parameterInfos); - } - } -} diff --git a/src/MinimalLambda.SourceGenerators/Models/MethodInfo.cs b/src/MinimalLambda.SourceGenerators/Models/MethodInfo.cs new file mode 100644 index 00000000..c05fcbbd --- /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 = LocationInfo.CreateFrom(location); + + var name = attributeData.AttributeClass?.ToString() ?? "UNKNOWN"; + + return new AttributeInfo(locationData, name); + } + } +} diff --git a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs index 2efe94fe..41074bc5 100644 --- a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs +++ b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs @@ -13,19 +13,36 @@ string generatedCodeAttribute { var useMiddlewareTCalls = useMiddlewareTInfos.Select(useMiddlewareTInfo => { - return new { }; + var classInfo = useMiddlewareTInfo.ClassInfo; + + // choose what constructor to use with the following criteria: + // 1. if it has an `[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(); + + return new + { + Location = useMiddlewareTInfo.InterceptableLocationInfo, + FullMiddlewareClassName = classInfo.GloballyQualifiedName, + ShortMiddlewareClassName = classInfo.ShortName, + constructor.Value.Parameters, + }; }); - var template = TemplateHelper.LoadTemplate( - GeneratorConstants.LambdaHostMapHandlerExtensionsTemplateFile - ); + var template = TemplateHelper.LoadTemplate(GeneratorConstants.UseMiddlewareTTemplateFile); return template.Render( - new - { - GeneratedCodeAttribute = generatedCodeAttribute, - UseMiddlewareCalls = useMiddlewareTCalls, - } + new { GeneratedCodeAttribute = generatedCodeAttribute, Calls = useMiddlewareTCalls } ); } } diff --git a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban new file mode 100644 index 00000000..9490598d --- /dev/null +++ b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban @@ -0,0 +1,78 @@ + {{ 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); + + builder.Use(next => context => resolver.Create(context).InvokeAsync(context, next)); + + return builder; + } + + private class {{ call.short_middleware_class_name }}Resolver{{ for.index }} + { + private const int NotCached = -1; + private const int FromServices = -2; + private readonly object[] _args; + + // Cache fields: -1 = not cached, -2 = resolve from DI, >= 0 = args index + {{~ for parameter in call.parameters ~}} + private int _cache{{ for.index }} = NotCached; // {{ parameter.type_info.fully_qualified_type }} + {{~ end ~}} + + internal {{ call.short_middleware_class_name }}Resolver{{ for.index }}(object[] args) => _args = args; + + internal ILambdaMiddleware Create(ILambdaInvocationContext context) + { + // First call builds the resolution cache + if (_cache0 == NotCached) + BuildResolutionCache(); + + // Fast resolution using cached indices + {{~ for parameter in call.parameters ~}} + var arg0 = + _cache{{ for.index }} >= 0 + ? ({{ parameter.type_info.fully_qualified_type }})_args[_cache{{ for.index }}] + : context.ServiceProvider.GetRequiredService<{{ parameter.type_info.fully_qualified_type }}>(); + {{~ end ~}} + + return new {{ call.full_middleware_class_name }}({{ for arg in call.parameters }}arg{{ for.index }}{{ if !for.last }}, {{ end }}{{ end }}); + } + + private void BuildResolutionCache() + { + // Initialize all to "resolve from services" + {{~ for parameter in call.parameters ~}} + _cache{{ for.index }} = FromServices; + {{~ end ~}} + + // Single pass through args - find where each type lives + for (var i = 0; i < _args.Length; i++) + { + var arg = _args[i]; + if (arg is null) + continue; + + switch (arg) + { + {{~ for parameter in call.parameters ~}} + case {{ parameter.type_info.fully_qualified_type }} when _cache{{ for.index }} == FromServices: + _cache{{ for.index }} = i; + break; + {{~ end ~}} + } + } + } + } + {{~ if !for.last ~}} + + {{~ end ~}} + {{~ end ~}} + } diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs index 33e26672..289e8f96 100644 --- a/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs @@ -134,7 +134,7 @@ internal MyLambdaMiddleware(IService service) _service = service; } - [MiddlewareConstructor] + // [MiddlewareConstructor] internal MyLambdaMiddleware() { _service = null!; From 21ae19e6aa705b06ff99f8740cc84a62411b20ea Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Wed, 17 Dec 2025 19:11:21 -0500 Subject: [PATCH 08/25] refactor(source-generators): simplify `UseMiddleware` template by removing redundant comments - Removed outdated and unnecessary comments from `UseMiddlewareT.scriban`. - Streamlined code for improved readability and maintainability. --- .../Templates/UseMiddlewareT.scriban | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban index 9490598d..8845ada5 100644 --- a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban +++ b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban @@ -22,7 +22,6 @@ private const int FromServices = -2; private readonly object[] _args; - // Cache fields: -1 = not cached, -2 = resolve from DI, >= 0 = args index {{~ for parameter in call.parameters ~}} private int _cache{{ for.index }} = NotCached; // {{ parameter.type_info.fully_qualified_type }} {{~ end ~}} @@ -31,11 +30,9 @@ internal ILambdaMiddleware Create(ILambdaInvocationContext context) { - // First call builds the resolution cache if (_cache0 == NotCached) BuildResolutionCache(); - // Fast resolution using cached indices {{~ for parameter in call.parameters ~}} var arg0 = _cache{{ for.index }} >= 0 @@ -48,12 +45,10 @@ private void BuildResolutionCache() { - // Initialize all to "resolve from services" {{~ for parameter in call.parameters ~}} _cache{{ for.index }} = FromServices; {{~ end ~}} - // Single pass through args - find where each type lives for (var i = 0; i < _args.Length; i++) { var arg = _args[i]; From ba4e8a55cf025324d9e199de8b03da15e1e190ca Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Wed, 17 Dec 2025 19:56:02 -0500 Subject: [PATCH 09/25] feat(source-generators): enhance `UseMiddleware` with parameter validation and improved codegen - Added validation for `[MiddlewareConstructor]` to ensure parameters are provided explicitly in `args`. - Optimized `UseMiddlewareT.scriban` to handle optional and required parameters with proper assignments. - Updated `GeneratorConstants` to simplify attribute constants' format and naming. - Improved `UseMiddlewareLambdaApplicationExtensions` to accept `params object[] args`. - Refactored `UseMiddlewareTSource` to include support for parameter assignment logic. --- .../GeneratorConstants.cs | 5 +- .../OutputGenerators/UseMiddlewareTSource.cs | 51 +++++++++++++++++-- .../Templates/UseMiddlewareT.scriban | 25 ++++++--- ...seMiddlewareLambdaApplicationExtensions.cs | 5 +- .../VerifyTests/UseMiddlewareTVerifyTests.cs | 2 +- 5 files changed, 73 insertions(+), 15 deletions(-) diff --git a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs index c1da2863..a62feeef 100644 --- a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs +++ b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs @@ -39,10 +39,9 @@ internal static class AttributeConstants internal const string FromKeyedService = "Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute"; - internal const string MiddlewareConstructor = - "global::MinimalLambda.MiddlewareConstructorAttribute"; + internal const string MiddlewareConstructor = "MinimalLambda.MiddlewareConstructorAttribute"; - internal const string FromArguments = "global::MinimalLambda.FromArgumentsAttribute"; + internal const string FromArguments = "MinimalLambda.FromArgumentsAttribute"; } internal static class GeneratorConstants diff --git a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs index 41074bc5..44c9a5ea 100644 --- a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs +++ b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs @@ -4,7 +4,7 @@ namespace MinimalLambda.SourceGenerators; -public class UseMiddlewareTSource +internal static class UseMiddlewareTSource { internal static string Generate( EquatableArray useMiddlewareTInfos, @@ -16,7 +16,8 @@ string generatedCodeAttribute var classInfo = useMiddlewareTInfo.ClassInfo; // choose what constructor to use with the following criteria: - // 1. if it has an `[MiddlewareConstructor]` attribute. Multiple of these are not valid. + // 1. if it has an `[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) @@ -30,12 +31,31 @@ string generatedCodeAttribute .ConstructorInfos.OrderByDescending(c => c.ArgumentCount) .First(); + var parameters = constructor + .Value.Parameters.Select(p => + { + var fromArgs = p.AttributeNames.Any(n => n == AttributeConstants.FromArguments); + + var paramAssignment = p.BuildParameterAssignment(); + + return new + { + p.TypeInfo.FullyQualifiedType, + p.Name, + FromArguments = fromArgs, + paramAssignment.Assignment, + paramAssignment.String, + }; + }) + .ToArray(); + return new { Location = useMiddlewareTInfo.InterceptableLocationInfo, FullMiddlewareClassName = classInfo.GloballyQualifiedName, ShortMiddlewareClassName = classInfo.ShortName, - constructor.Value.Parameters, + Parameters = parameters, + AnyParameters = parameters.Length > 0, }; }); @@ -45,4 +65,29 @@ string generatedCodeAttribute 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 readonly record struct ParameterArg(string String, string Assignment); } diff --git a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban index 8845ada5..ed89a30d 100644 --- a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban +++ b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban @@ -23,26 +23,36 @@ private readonly object[] _args; {{~ for parameter in call.parameters ~}} - private int _cache{{ for.index }} = NotCached; // {{ parameter.type_info.fully_qualified_type }} + private int _cache{{ for.index }} = NotCached; // {{ parameter.fully_qualified_type }} + {{~ end ~}} + {{~ if call.any_parameters ~}} + {{~ end ~}} - internal {{ call.short_middleware_class_name }}Resolver{{ for.index }}(object[] args) => _args = args; internal ILambdaMiddleware Create(ILambdaInvocationContext context) { + {{~ if call.any_parameters ~}} if (_cache0 == NotCached) BuildResolutionCache(); {{~ for parameter in call.parameters ~}} var arg0 = _cache{{ for.index }} >= 0 - ? ({{ parameter.type_info.fully_qualified_type }})_args[_cache{{ for.index }}] - : context.ServiceProvider.GetRequiredService<{{ parameter.type_info.fully_qualified_type }}>(); + ? ({{ 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.string }} + : {{ parameter.assignment }}; + {{~ 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 ~}} + private void BuildResolutionCache() { {{~ for parameter in call.parameters ~}} @@ -58,13 +68,14 @@ switch (arg) { {{~ for parameter in call.parameters ~}} - case {{ parameter.type_info.fully_qualified_type }} when _cache{{ for.index }} == FromServices: + case {{ parameter.fully_qualified_type }} when _cache{{ for.index }} == FromServices: _cache{{ for.index }} = i; break; {{~ end ~}} } } } + {{~ end ~}} } {{~ if !for.last ~}} diff --git a/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs b/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs index fcb3121e..bcf5c455 100644 --- a/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs +++ b/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs @@ -6,7 +6,10 @@ namespace MinimalLambda.Builder; [ExcludeFromCodeCoverage] public static class UseMiddlewareLambdaApplicationExtensions { - public static ILambdaOnInitBuilder UseMiddleware(this ILambdaOnInitBuilder _) + public static ILambdaInvocationBuilder UseMiddleware( + this ILambdaInvocationBuilder builder, + params object[] args + ) where T : ILambdaMiddleware { Debug.Fail("This method should have been intercepted at compile time!"); diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs index 289e8f96..33e26672 100644 --- a/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs @@ -134,7 +134,7 @@ internal MyLambdaMiddleware(IService service) _service = service; } - // [MiddlewareConstructor] + [MiddlewareConstructor] internal MyLambdaMiddleware() { _service = null!; From a6ea3a451694e53fb0b8553ba356b52d9a197187 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Wed, 17 Dec 2025 21:49:17 -0500 Subject: [PATCH 10/25] feat(source-generators): add support for `FromServicesAttribute` in `UseMiddleware` - Introduced `FromServicesAttribute` to enable service injection for middleware parameters. - Updated `UseMiddlewareT.scriban` to resolve parameters marked with `FromServicesAttribute`. - Enhanced `UseMiddlewareTSource` to detect `FromServicesAttribute` and assign parameters correctly. - Added `FromServices` constant to `GeneratorConstants` for attribute recognition. --- .../Attributes/FromServicesAttribute.cs | 4 ++++ .../GeneratorConstants.cs | 2 ++ .../OutputGenerators/UseMiddlewareTSource.cs | 5 +++++ .../Templates/UseMiddlewareT.scriban | 12 +++++++++--- 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs diff --git a/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs new file mode 100644 index 00000000..f27116ed --- /dev/null +++ b/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs @@ -0,0 +1,4 @@ +namespace MinimalLambda; + +[AttributeUsage(AttributeTargets.Parameter)] +public class FromServicesAttribute : Attribute; diff --git a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs index a62feeef..0c315f17 100644 --- a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs +++ b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs @@ -42,6 +42,8 @@ internal static class AttributeConstants internal const string MiddlewareConstructor = "MinimalLambda.MiddlewareConstructorAttribute"; internal const string FromArguments = "MinimalLambda.FromArgumentsAttribute"; + + internal const string FromServices = "MinimalLambda.FromServicesAttribute"; } internal static class GeneratorConstants diff --git a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs index 44c9a5ea..6436acc9 100644 --- a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs +++ b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs @@ -36,6 +36,10 @@ string generatedCodeAttribute { var fromArgs = p.AttributeNames.Any(n => n == AttributeConstants.FromArguments); + var fromServices = p.AttributeNames.Any(n => + n == AttributeConstants.FromServices + ); + var paramAssignment = p.BuildParameterAssignment(); return new @@ -43,6 +47,7 @@ string generatedCodeAttribute p.TypeInfo.FullyQualifiedType, p.Name, FromArguments = fromArgs, + FromServices = fromServices, paramAssignment.Assignment, paramAssignment.String, }; diff --git a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban index ed89a30d..c998fd14 100644 --- a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban +++ b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban @@ -30,24 +30,28 @@ {{~ end ~}} internal {{ call.short_middleware_class_name }}Resolver{{ for.index }}(object[] args) => _args = args; - internal ILambdaMiddleware Create(ILambdaInvocationContext context) + internal {{ call.full_middleware_class_name }} Create(ILambdaInvocationContext context) { {{~ if call.any_parameters ~}} if (_cache0 == NotCached) BuildResolutionCache(); {{~ for parameter in call.parameters ~}} - var arg0 = + // {{ 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.string }} : {{ 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 }}); } @@ -68,10 +72,12 @@ switch (arg) { {{~ for parameter in call.parameters ~}} + {{~ if !parameter.from_services ~}} case {{ parameter.fully_qualified_type }} when _cache{{ for.index }} == FromServices: _cache{{ for.index }} = i; break; {{~ end ~}} + {{~ end ~}} } } } From b2eb71431992784287efa13cfc0dd4bc40ad7d22 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 09:41:44 -0500 Subject: [PATCH 11/25] feat(source-generators): update `UseMiddleware` to handle non-nullable parameter types - Replaced `parameter.fully_qualified_type` with `parameter.fully_qualified_type_not_null` in template. - Added `RemoveTrailingChar` helper method to strip nullable indicator from parameter types. - Updated `UseMiddlewareTSource` to include non-nullable type processing for `FromServices`. --- .../OutputGenerators/UseMiddlewareTSource.cs | 6 ++++++ .../Templates/UseMiddlewareT.scriban | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs index 6436acc9..0c400f12 100644 --- a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs +++ b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs @@ -45,6 +45,9 @@ string generatedCodeAttribute return new { p.TypeInfo.FullyQualifiedType, + FullyQualifiedTypeNotNull = p.TypeInfo.FullyQualifiedType.RemoveTrailingChar( + "?" + ), p.Name, FromArguments = fromArgs, FromServices = fromServices, @@ -94,5 +97,8 @@ private static ParameterArg BuildParameterAssignment(this ParameterInfo param) = }, }; + 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/Templates/UseMiddlewareT.scriban b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban index c998fd14..f5299bfd 100644 --- a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban +++ b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban @@ -73,7 +73,7 @@ { {{~ for parameter in call.parameters ~}} {{~ if !parameter.from_services ~}} - case {{ parameter.fully_qualified_type }} when _cache{{ for.index }} == FromServices: + case {{ parameter.fully_qualified_type_not_null }} when _cache{{ for.index }} == FromServices: _cache{{ for.index }} = i; break; {{~ end ~}} From ad7aeb11cc644a449e337f4ad3d10ebace719ee7 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 10:03:17 -0500 Subject: [PATCH 12/25] feat(source-generators): update `UseMiddleware` to skip caching for `FromServices` parameters - Modified `UseMiddlewareT.scriban` to skip cache initialization for `FromServices` parameters. - Adjusted `BuildResolutionCache` logic to exclude `FromServices` parameters from cache assignments. --- .../Templates/UseMiddlewareT.scriban | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban index f5299bfd..0e63d2d8 100644 --- a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban +++ b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban @@ -23,8 +23,10 @@ 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 ~}} {{~ end ~}} @@ -60,8 +62,10 @@ 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++) { From 98110cc3218d0c43d5a48c6679f1ebe34cf6ed0e Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 10:30:44 -0500 Subject: [PATCH 13/25] feat(source-generators): add rule `LH0005` for middleware constructor attribute validation - Introduced diagnostic rule `LH0005` to ensure `[MiddlewareConstructor]` is used by only one constructor. - Updated `DiagnosticGenerator` to detect and report multiple constructors using the attribute. - Added `Diagnostics.MultipleConstructorsWithAttribute` descriptor for the new rule. --- .../AnalyzerReleases.Unshipped.md | 8 ++++++- .../Diagnostics/DiagnosticGenerator.cs | 22 +++++++++++++++++++ .../Diagnostics/Diagnostics.cs | 9 ++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md b/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md index 805230ad..8727d19d 100644 --- a/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -1,5 +1,11 @@ +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +LH0005 | 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..22821145 100644 --- a/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs +++ b/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs @@ -43,6 +43,28 @@ internal static List GenerateDiagnostics(CompilationInfo compilation compilationInfo.OnShutdownInvocationInfos.GenerateKeyedServiceKeyDiagnostics() ); + // validate that middleware class constructors only use `[MiddlewareConstructor]` once + foreach (var useMiddlewareTInfo in compilationInfo.UseMiddlewareTInfos) + 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..7e9c0d93 100644 --- a/src/MinimalLambda.SourceGenerators/Diagnostics/Diagnostics.cs +++ b/src/MinimalLambda.SourceGenerators/Diagnostics/Diagnostics.cs @@ -34,4 +34,13 @@ 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 + ); } From df38b5438a5a3857126c321f0e34aceaf9c650c2 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 10:36:03 -0500 Subject: [PATCH 14/25] test(source-generators): add unit test for middleware constructor attribute validation - Added `Test_MiddlewareClassHasMoreThanOneMiddlewareConstructorAttribute` to validate diagnostic `LH0005`. - Ensures an error is raised if multiple `[MiddlewareConstructor]` attributes are applied. --- .../DiagnosticTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs index a36c90b3..4352380a 100644 --- a/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs @@ -150,6 +150,50 @@ 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); + } + } + private static ImmutableArray GenerateDiagnostics( string source, LanguageVersion languageVersion = LanguageVersion.CSharp11 From a654964049ea3b51c8d0febf06a9a239b6b57e55 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 12:04:07 -0500 Subject: [PATCH 15/25] feat(source-generators): enhance `UseMiddleware` to support `AllFromServices` optimization - Added `AllFromServices` property in `UseMiddlewareTSource` to track parameters fully marked as `FromServices`. - Updated `UseMiddlewareT.scriban` to skip resolution cache initialization if `AllFromServices` is true. - Introduced `_cacheBuilt` flag to simplify cache-building logic for mixed parameter sources. - Optimized `BuildResolutionCache` to handle scenarios where services are exclusively used. --- .../OutputGenerators/UseMiddlewareTSource.cs | 5 +++++ .../Templates/UseMiddlewareT.scriban | 14 ++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs index 0c400f12..6f614d62 100644 --- a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs +++ b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs @@ -57,11 +57,16 @@ string generatedCodeAttribute }) .ToArray(); + // TODO: support IDisposable and 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, }; diff --git a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban index 0e63d2d8..3025923f 100644 --- a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban +++ b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban @@ -21,13 +21,16 @@ private const int NotCached = -1; private const int FromServices = -2; private readonly object[] _args; + {{~ if call.any_parameters && !call.all_from_services ~}} + private bool _cacheBuilt = false; + {{~ end ~}} {{~ 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 ~}} + {{~ if call.any_parameters && !call.all_from_services ~}} {{~ end ~}} internal {{ call.short_middleware_class_name }}Resolver{{ for.index }}(object[] args) => _args = args; @@ -35,9 +38,11 @@ internal {{ call.full_middleware_class_name }} Create(ILambdaInvocationContext context) { {{~ if call.any_parameters ~}} - if (_cache0 == NotCached) + {{~ if !call.all_from_services ~}} + if (!_cacheBuilt) BuildResolutionCache(); + {{~ end ~}} {{~ for parameter in call.parameters ~}} // {{ parameter.string }} {{~ if parameter.from_services ~}} @@ -54,10 +59,10 @@ {{~ 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 ~}} + {{~ if call.any_parameters && !call.all_from_services ~}} private void BuildResolutionCache() { @@ -84,6 +89,7 @@ {{~ end ~}} } } + _cacheBuilt = true; } {{~ end ~}} } From ad0d3b70a260d6e84d066da9d545a999d88c2e45 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 15:06:51 -0500 Subject: [PATCH 16/25] feat(source-generators): add support for disposable middleware in `UseMiddleware` - Enhanced `UseMiddlewareT.scriban` to handle `IDisposable` and `IAsyncDisposable` middleware. - Added interface checks in `UseMiddlewareTSource` to identify disposable middleware types. - Extended `ClassInfo` with `ImplementedInterfaces` and helper method `IsInterfaceImplemented`. - Updated unit tests to verify scenarios with disposable middleware. - Refactored code generation to properly manage lifecycle of disposable middleware instances. --- .../Attributes}/EventAttribute.cs | 0 .../Attributes/FromArgumentsAttribute.cs | 2 +- .../Attributes}/FromEventAttribute.cs | 0 .../Attributes/FromServicesAttribute.cs | 2 +- .../MiddlewareConstructorAttribute.cs | 2 +- .../GeneratorConstants.cs | 4 + .../Models/ClassInfo.cs | 15 +- .../OutputGenerators/UseMiddlewareTSource.cs | 24 +- .../Templates/UseMiddlewareT.scriban | 30 +- .../VerifyTests/UseMiddlewareTVerifyTests.cs | 349 +++++++++++++++++- 10 files changed, 412 insertions(+), 16 deletions(-) rename src/{MinimalLambda/Builder => MinimalLambda.Abstractions/Attributes}/EventAttribute.cs (100%) rename src/{MinimalLambda/Builder => MinimalLambda.Abstractions/Attributes}/FromEventAttribute.cs (100%) 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 index 63effb2e..b744375b 100644 --- a/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs +++ b/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs @@ -1,4 +1,4 @@ -namespace MinimalLambda; +namespace MinimalLambda.Builder; [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 index f27116ed..dc3897c3 100644 --- a/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs +++ b/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs @@ -1,4 +1,4 @@ -namespace MinimalLambda; +namespace MinimalLambda.Builder; [AttributeUsage(AttributeTargets.Parameter)] public class FromServicesAttribute : Attribute; diff --git a/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs index ea0c2687..b954d479 100644 --- a/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs +++ b/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs @@ -1,4 +1,4 @@ -namespace MinimalLambda; +namespace MinimalLambda.Builder; [AttributeUsage(AttributeTargets.Constructor)] public class MiddlewareConstructorAttribute : Attribute; diff --git a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs index 0c315f17..73d9f4b2 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"; diff --git a/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs b/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs index 6ac36369..f0de426d 100644 --- a/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs @@ -8,12 +8,13 @@ namespace MinimalLambda.SourceGenerators.Models; internal readonly record struct ClassInfo( string GloballyQualifiedName, string ShortName, - EquatableArray ConstructorInfos + EquatableArray ConstructorInfos, + EquatableArray ImplementedInterfaces ); internal static class ClassInfoExtensions { - extension(ClassInfo) + extension(ClassInfo classInfo) { internal static ClassInfo Create(ITypeSymbol typeSymbol) { @@ -28,7 +29,15 @@ internal static ClassInfo Create(ITypeSymbol typeSymbol) .InstanceConstructors.Select(MethodInfo.Create) .ToEquatableArray(); - return new ClassInfo(globallyQualifiedName, shortName, constructorInfo); + // get all interfaces + var interfaceNames = typeSymbol + .AllInterfaces.Select(i => i.GetAsGlobal()) + .ToEquatableArray(); + + return new ClassInfo(globallyQualifiedName, shortName, constructorInfo, interfaceNames); } + + internal bool IsInterfaceImplemented(string interfaceName) => + classInfo.ImplementedInterfaces.Any(i => i == interfaceName); } } diff --git a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs index 6f614d62..54feeba3 100644 --- a/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs +++ b/src/MinimalLambda.SourceGenerators/OutputGenerators/UseMiddlewareTSource.cs @@ -16,8 +16,7 @@ string generatedCodeAttribute var classInfo = useMiddlewareTInfo.ClassInfo; // choose what constructor to use with the following criteria: - // 1. if it has an `[MiddlewareConstructor]` attribute. Multiple of these are - // not valid. + // 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) @@ -36,18 +35,21 @@ string generatedCodeAttribute { 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 == AttributeConstants.FromServices + n is AttributeConstants.FromServices or AttributeConstants.FromKeyedService ); var paramAssignment = p.BuildParameterAssignment(); + var fullyQualifiedTypeNotNull = + p.TypeInfo.FullyQualifiedType.RemoveTrailingChar("?"); + return new { p.TypeInfo.FullyQualifiedType, - FullyQualifiedTypeNotNull = p.TypeInfo.FullyQualifiedType.RemoveTrailingChar( - "?" - ), + FullyQualifiedTypeNotNull = fullyQualifiedTypeNotNull, p.Name, FromArguments = fromArgs, FromServices = fromServices, @@ -57,7 +59,13 @@ string generatedCodeAttribute }) .ToArray(); - // TODO: support IDisposable and IAsyncDisposable + var isDisposable = useMiddlewareTInfo.ClassInfo.IsInterfaceImplemented( + TypeConstants.IDisposable + ); + + var isAsyncDisposable = useMiddlewareTInfo.ClassInfo.IsInterfaceImplemented( + TypeConstants.IAsyncDisposable + ); var allFromServices = parameters.All(p => p.FromServices); @@ -69,6 +77,8 @@ string generatedCodeAttribute AllFromServices = allFromServices, Parameters = parameters, AnyParameters = parameters.Length > 0, + IsDisposable = isDisposable, + IsAsyncDisposable = isAsyncDisposable, }; }); diff --git a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban index 3025923f..6db502c7 100644 --- a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban +++ b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban @@ -10,8 +10,34 @@ where T : ILambdaMiddleware { var resolver = new {{ call.short_middleware_class_name }}Resolver{{ for.index }}(args); - - builder.Use(next => context => resolver.Create(context).InvokeAsync(context, next)); + + {{~ 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; } diff --git a/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs index 33e26672..6aa9e96a 100644 --- a/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs @@ -154,10 +154,357 @@ internal interface IService ); [Fact] - public async Task Test_MiddlewareClass_() => + 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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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(); + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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"); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + 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(); + + lambda.MapHandler(() => { }); + + await lambda.RunAsync(); + + internal class MyLambdaMiddleware : ILambdaMiddleware, IAsyncDisposable + { + public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) + { + await next(context); + } + + public async ValueTask DisposeAsync() { } + } """ ); } From 4736b83922f48de8fbe4253332a4e461c1d5e887 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 17:15:29 -0500 Subject: [PATCH 17/25] feat(attributes): add documentation for middleware-related attributes - Added detailed XML documentation for `FromArgumentsAttribute`, `FromServicesAttribute`, and `MiddlewareConstructorAttribute` to explain usage and behavior. - Included examples demonstrating use cases for each attribute. - Enhanced `UseMiddlewareLambdaApplicationExtensions` with inline comments for better clarity and usability. --- .../Attributes/FromArgumentsAttribute.cs | 22 ++++++++++++ .../Attributes/FromServicesAttribute.cs | 20 +++++++++++ .../MiddlewareConstructorAttribute.cs | 28 +++++++++++++++ ...seMiddlewareLambdaApplicationExtensions.cs | 34 +++++++++++++++++++ 4 files changed, 104 insertions(+) diff --git a/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs index b744375b..a0eb7efe 100644 --- a/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs +++ b/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs @@ -1,4 +1,26 @@ 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.Abstractions/Attributes/FromServicesAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs index dc3897c3..d8a936ca 100644 --- a/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs +++ b/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs @@ -1,4 +1,24 @@ 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 index b954d479..a758deba 100644 --- a/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs +++ b/src/MinimalLambda.Abstractions/Attributes/MiddlewareConstructorAttribute.cs @@ -1,4 +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/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs b/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs index bcf5c455..a335ef14 100644 --- a/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs +++ b/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs @@ -3,9 +3,43 @@ 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 From 1c7fb2cd73184654ba5606454fd316a364a55b93 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 17:15:53 -0500 Subject: [PATCH 18/25] feat(tests): improve `UseMiddleware` unit tests and add snapshot tests - Removed redundant `lambda.MapHandler` and `lambda.RunAsync` calls from existing test cases. - Added comprehensive snapshot tests to verify generated code for various middleware scenarios: - Middleware with abstract classes. - Middleware with constructor arguments from services and keyed services. - Middleware with mixed parameter sources. - Middleware implementing `IDisposable` and `IAsyncDisposable`. - Ensured better coverage and clarity for middleware usage scenarios. --- ...ractMiddleware#LambdaHandler.g.verified.cs | 80 +++++++++++ ...lWorldScenario#LambdaHandler.g.verified.cs | 131 ++++++++++++++++++ ...ructorWithArgs#LambdaHandler.g.verified.cs | 114 +++++++++++++++ ...mentsAttribute#LambdaHandler.g.verified.cs | 114 +++++++++++++++ ...vicesAttribute#LambdaHandler.g.verified.cs | 83 +++++++++++ ...vicesAttribute#LambdaHandler.g.verified.cs | 83 +++++++++++ ...syncDisposable#LambdaHandler.g.verified.cs | 81 +++++++++++ ...ss_IDisposable#LambdaHandler.g.verified.cs | 81 +++++++++++ ...rameterSources#LambdaHandler.g.verified.cs | 131 ++++++++++++++++++ ...areConstructor#LambdaHandler.g.verified.cs | 80 +++++++++++ ...lableParameter#LambdaHandler.g.verified.cs | 114 +++++++++++++++ ...thDefaultValue#LambdaHandler.g.verified.cs | 114 +++++++++++++++ ...reClass_Simple#LambdaHandler.g.verified.cs | 80 +++++++++++ ..._WithArgsArray#LambdaHandler.g.verified.cs | 125 +++++++++++++++++ .../VerifyTests/UseMiddlewareTVerifyTests.cs | 56 -------- 15 files changed, 1411 insertions(+), 56 deletions(-) create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_AbstractMiddleware#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_ComplexRealWorldScenario#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_ConstructorWithArgs#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromArgumentsAttribute#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromKeyedServicesAttribute#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_FromServicesAttribute#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_IAsyncDisposable#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_IDisposable#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_MixedParameterSources#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_MultipleConstructorsAndOneWithMiddlewareConstructor#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_NullableParameter#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_OptionalParameterWithDefaultValue#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_Simple#LambdaHandler.g.verified.cs create mode 100644 tests/MinimalLambda.SourceGenerators.UnitTests/Snapshots/UseMiddlewareTVerifyTests.Test_MiddlewareClass_WithArgsArray#LambdaHandler.g.verified.cs 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 index 6aa9e96a..1e5f6ba9 100644 --- a/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/VerifyTests/UseMiddlewareTVerifyTests.cs @@ -17,10 +17,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware : ILambdaMiddleware { public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) @@ -46,10 +42,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal abstract class MyLambdaMiddleware : ILambdaMiddleware { public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) @@ -80,10 +72,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware : ILambdaMiddleware { private readonly IService _service; @@ -121,10 +109,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware : ILambdaMiddleware { private readonly IService _service; @@ -169,10 +153,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware( [FromServices] IService service ) : ILambdaMiddleware @@ -206,10 +186,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware( [FromKeyedServices("myKey")] IService service ) : ILambdaMiddleware @@ -243,10 +219,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware( [FromArguments] string apiKey ) : ILambdaMiddleware @@ -274,10 +246,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware( IService? service ) : ILambdaMiddleware @@ -310,10 +278,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware : ILambdaMiddleware { internal MyLambdaMiddleware(string name = "default") @@ -344,10 +308,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware( [FromServices] ILogger logger, [FromKeyedServices("cache")] ICache cache, @@ -382,10 +342,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware("myApiKey"); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware( string apiKey, IService service @@ -421,10 +377,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware( [FromArguments] string name, [FromKeyedServices("primary")] ILogger logger, @@ -460,10 +412,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware : ILambdaMiddleware, IDisposable { public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) @@ -492,10 +440,6 @@ await GeneratorTestHelpers.Verify( lambda.UseMiddleware(); - lambda.MapHandler(() => { }); - - await lambda.RunAsync(); - internal class MyLambdaMiddleware : ILambdaMiddleware, IAsyncDisposable { public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocationDelegate next) From 57ca8644e258cdc1654033d640854ca2e3805c68 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 17:16:10 -0500 Subject: [PATCH 19/25] feat(source-generators): add rule `LH0006` for middleware type validation - Introduced diagnostic rule `LH0006` to ensure middleware types are concrete classes. - Updated `DiagnosticGenerator` to detect and report non-concrete middleware types like interfaces and abstract classes. - Added `Diagnostics.MustBeConcreteType` descriptor for the new rule. - Included a unit test `Test_MiddlewareClassMustNotBeAbstractOrAnInterface` to verify `LH0006`. - Updated `AnalyzerReleases.Unshipped.md` to document the new rule. --- .../AnalyzerReleases.Unshipped.md | 7 ++-- .../Diagnostics/DiagnosticGenerator.cs | 14 +++++++- .../Diagnostics/Diagnostics.cs | 9 +++++ .../DiagnosticTests.cs | 34 +++++++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md b/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md index 8727d19d..4508b3ff 100644 --- a/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/src/MinimalLambda.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -1,8 +1,9 @@ ### New Rules -Rule ID | Category | Severity | Notes ---------|----------|----------|------- -LH0005 | MinimalLambda.Configuration | Error | Diagnostics + Rule ID | Category | Severity | Notes +---------|-----------------------------|----------|------------- + LH0005 | MinimalLambda.Configuration | Error | Diagnostics + LH0006 | MinimalLambda.Configuration | Error | Diagnostics ### Removed Rules diff --git a/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs b/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs index 22821145..1e9c7db9 100644 --- a/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs +++ b/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs @@ -43,8 +43,19 @@ internal static List GenerateDiagnostics(CompilationInfo compilation compilationInfo.OnShutdownInvocationInfos.GenerateKeyedServiceKeyDiagnostics() ); - // validate that middleware class constructors only use `[MiddlewareConstructor]` once 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 => @@ -64,6 +75,7 @@ internal static List GenerateDiagnostics(CompilationInfo compilation ) ) ); + } return diagnostics; } diff --git a/src/MinimalLambda.SourceGenerators/Diagnostics/Diagnostics.cs b/src/MinimalLambda.SourceGenerators/Diagnostics/Diagnostics.cs index 7e9c0d93..d850d6c8 100644 --- a/src/MinimalLambda.SourceGenerators/Diagnostics/Diagnostics.cs +++ b/src/MinimalLambda.SourceGenerators/Diagnostics/Diagnostics.cs @@ -43,4 +43,13 @@ internal static class Diagnostics 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/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs b/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs index 4352380a..90754699 100644 --- a/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs +++ b/tests/MinimalLambda.SourceGenerators.UnitTests/DiagnosticTests.cs @@ -194,6 +194,40 @@ public async Task InvokeAsync(ILambdaInvocationContext context, LambdaInvocation } } + [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 From e712dc56955ec882dd43756925b83efd9da17223 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 17:16:24 -0500 Subject: [PATCH 20/25] feat(source-generators): improve type handling and extend middleware support - Added support for resolving the location of generic type arguments in `UseMiddleware`. - Enhanced `TypeSymbolExtensions` with a `GetTypeKind` method to determine precise type definitions. - Updated `ClassInfo` to include `TypeKind` for better type classification and diagnostics. - Improved middleware code generation to incorporate generic argument location and accurate type info. - Refactored `GeneratorConstants` to adjust attribute namespaces for consistency. - Enhanced documentation for `ILambdaMiddleware` to provide detailed usage information. --- .../Core/ILambdaMiddleware.cs | 16 ++++++ .../Extensions/TypeSymbolExtensions.cs | 52 +++++++++++++++++++ .../GeneratorConstants.cs | 7 +-- .../Models/ClassInfo.cs | 13 ++++- .../Models/UseMiddlewareTInfo.cs | 3 +- .../UseMiddlewareTSyntaxProvider.cs | 19 ++++++- .../Templates/UseMiddlewareT.scriban | 4 +- 7 files changed, 105 insertions(+), 9 deletions(-) diff --git a/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs b/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs index 03520d1e..c861af2b 100644 --- a/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs +++ b/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs @@ -1,6 +1,22 @@ 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/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 73d9f4b2..6bfb933d 100644 --- a/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs +++ b/src/MinimalLambda.SourceGenerators/GeneratorConstants.cs @@ -43,11 +43,12 @@ internal static class AttributeConstants internal const string FromKeyedService = "Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute"; - internal const string MiddlewareConstructor = "MinimalLambda.MiddlewareConstructorAttribute"; + internal const string MiddlewareConstructor = + "MinimalLambda.Builder.MiddlewareConstructorAttribute"; - internal const string FromArguments = "MinimalLambda.FromArgumentsAttribute"; + internal const string FromArguments = "MinimalLambda.Builder.FromArgumentsAttribute"; - internal const string FromServices = "MinimalLambda.FromServicesAttribute"; + internal const string FromServices = "MinimalLambda.Builder.FromServicesAttribute"; } internal static class GeneratorConstants diff --git a/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs b/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs index f0de426d..76edb059 100644 --- a/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/ClassInfo.cs @@ -9,7 +9,8 @@ internal readonly record struct ClassInfo( string GloballyQualifiedName, string ShortName, EquatableArray ConstructorInfos, - EquatableArray ImplementedInterfaces + EquatableArray ImplementedInterfaces, + string TypeKind ); internal static class ClassInfoExtensions @@ -18,6 +19,8 @@ internal static class ClassInfoExtensions { internal static ClassInfo Create(ITypeSymbol typeSymbol) { + var typeKind = typeSymbol.GetTypeKind(); + // get the globally qualified name of the class var globallyQualifiedName = typeSymbol.GetAsGlobal(); @@ -34,7 +37,13 @@ internal static ClassInfo Create(ITypeSymbol typeSymbol) .AllInterfaces.Select(i => i.GetAsGlobal()) .ToEquatableArray(); - return new ClassInfo(globallyQualifiedName, shortName, constructorInfo, interfaceNames); + return new ClassInfo( + globallyQualifiedName, + shortName, + constructorInfo, + interfaceNames, + typeKind + ); } internal bool IsInterfaceImplemented(string interfaceName) => diff --git a/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs b/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs index 94872c1b..3ff642f3 100644 --- a/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/UseMiddlewareTInfo.cs @@ -2,5 +2,6 @@ namespace MinimalLambda.SourceGenerators.Models; internal readonly record struct UseMiddlewareTInfo( InterceptableLocationInfo InterceptableLocationInfo, - ClassInfo ClassInfo + ClassInfo ClassInfo, + LocationInfo? GenericTypeArgumentLocation ); diff --git a/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs b/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs index 5d0ac0c8..bda19a03 100644 --- a/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs +++ b/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs @@ -49,6 +49,20 @@ is not IInvocationOperation // 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( @@ -58,7 +72,10 @@ is not IInvocationOperation var useMiddlewareTInfo = new UseMiddlewareTInfo( InterceptableLocationInfo.CreateFrom(interceptableLocation), - classInfo + classInfo, + genericArgumentLocation is not null + ? LocationInfo.CreateFrom(genericArgumentLocation) + : null ); return useMiddlewareTInfo; diff --git a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban index 6db502c7..944a7467 100644 --- a/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban +++ b/src/MinimalLambda.SourceGenerators/Templates/UseMiddlewareT.scriban @@ -44,12 +44,12 @@ 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 readonly object[] _args; - {{~ if call.any_parameters && !call.all_from_services ~}} private bool _cacheBuilt = false; {{~ end ~}} + private readonly object[] _args; {{~ for parameter in call.parameters ~}} {{~ if !parameter.from_services ~}} From c6b961a78c5af60df9f9dbcf0950ca81673e0a39 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 17:34:34 -0500 Subject: [PATCH 21/25] feat(diagnostics): improve validation and streamline XML documentation - Added additional validation block in `DiagnosticGenerator` for middleware class type checks. - Refactored XML documentation to consolidate paragraphs and improve readability. - Streamlined multiple attribute and interface summaries by reducing excessive line breaks. --- .../Attributes/FromArgumentsAttribute.cs | 14 +++++--------- .../Attributes/FromServicesAttribute.cs | 11 ++++------- .../Core/ILambdaMiddleware.cs | 13 ++++++------- .../Diagnostics/DiagnosticGenerator.cs | 2 ++ .../UseMiddlewareLambdaApplicationExtensions.cs | 8 ++++---- 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs index a0eb7efe..367d3c59 100644 --- a/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs +++ b/src/MinimalLambda.Abstractions/Attributes/FromArgumentsAttribute.cs @@ -1,16 +1,12 @@ namespace MinimalLambda.Builder; -/// -/// Marks a middleware constructor parameter to receive its value from the arguments passed to -/// UseMiddleware<T>(). -/// +/// 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. +/// 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. /// /// /// diff --git a/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs b/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs index d8a936ca..414b8617 100644 --- a/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs +++ b/src/MinimalLambda.Abstractions/Attributes/FromServicesAttribute.cs @@ -1,14 +1,11 @@ namespace MinimalLambda.Builder; -/// -/// Marks a middleware constructor parameter to receive its value from the dependency -/// injection container. -/// +/// 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. +/// 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. /// /// /// diff --git a/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs b/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs index c861af2b..037c9eaf 100644 --- a/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs +++ b/src/MinimalLambda.Abstractions/Core/ILambdaMiddleware.cs @@ -3,13 +3,12 @@ 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. +/// 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 diff --git a/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs b/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs index 1e9c7db9..85753d13 100644 --- a/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs +++ b/src/MinimalLambda.SourceGenerators/Diagnostics/DiagnosticGenerator.cs @@ -47,6 +47,7 @@ internal static List GenerateDiagnostics(CompilationInfo compilation { // ensure middleware class is concrete if (useMiddlewareTInfo.ClassInfo.TypeKind is "interface" or "abstract class") + { diagnostics.Add( Diagnostic.Create( Diagnostics.MustBeConcreteType, @@ -54,6 +55,7 @@ internal static List GenerateDiagnostics(CompilationInfo compilation useMiddlewareTInfo.ClassInfo.ShortName ) ); + } // validate that middleware class constructors only use `[MiddlewareConstructor]` once diagnostics.AddRange( diff --git a/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs b/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs index a335ef14..260003cf 100644 --- a/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs +++ b/src/MinimalLambda/Builder/InterceptionTargets/UseMiddlewareLambdaApplicationExtensions.cs @@ -18,14 +18,14 @@ public static class UseMiddlewareLambdaApplicationExtensions /// 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. + /// to select a specific constructor when multiple + /// exist. /// /// /// The middleware type implementing . /// - /// The instance to register the - /// middleware with. + /// The instance to register the middleware + /// with. /// /// /// Arguments to pass to the middleware constructor for parameters marked with From e26ab79634a68a399ef36e1e2698f40ab302f711 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 17:40:56 -0500 Subject: [PATCH 22/25] feat(docs): expand middleware guide with inline and class-based approaches - Added detailed documentation on inline and class-based middleware styles. - Included examples for both approaches with best practice recommendations. - Explained parameter resolution using constructor attributes like `[FromServices]`. - Added common patterns and lifecycle management details for middleware. - Enhanced ordering strategy section with mixed inline and class-based examples. --- docs/guides/middleware.md | 841 +++++++++++++++++++++++++++++++++++++- 1 file changed, 825 insertions(+), 16 deletions(-) diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index 5297eeb2..993b4e1e 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -1,8 +1,10 @@ # 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/) +runs code before/after the next component, and can short-circuit the pipeline. You can use inline +delegates for simple cases or class-based middleware for complex, reusable components. 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. @@ -76,11 +78,752 @@ Key members: - `Properties` – cross-invocation storage. - `Features` – ASP.NET-style 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) + { + 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. + +!!! info "ASP.NET Core Similarity" +This mirrors ASP.NET Core's `UseMiddleware()` extension, but uses source generation instead +of reflection for Lambda's cold-start performance requirements. + +#### 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 +992,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 +1017,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. From fec69da01e969bff701af550ba30fa2cd4b9222a Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 18:54:19 -0500 Subject: [PATCH 23/25] feat(docs): refine middleware guide for clarity and conciseness - Simplified explanations for ASP.NET Core middleware similarities and invocation scopes. - Improved readability by restructuring examples and condensing redundant content. - Clarified usage of `Features` and added new example for request start time in `Items`. - Reworked notes on class-based middleware for better alignment with reusable package guidance. - Updated formatting for tips, warnings, and compile-time diagnostics to enhance presentation. --- docs/guides/middleware.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index 993b4e1e..8be74b34 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -1,12 +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. You can use inline -delegates for simple cases or class-based middleware for complex, reusable components. 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 @@ -60,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); @@ -76,7 +76,8 @@ 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. ## Middleware Approaches @@ -124,9 +125,9 @@ lambda.UseMiddleware(async (context, next) => // Access response after handler executes var response = context.GetResponse(); - if (response is not null) + if (response is not null && request is not null) { - await cache.SetAsync(request!.OrderId, response); + await cache.SetAsync(request.OrderId, response); } }); ``` @@ -200,9 +201,10 @@ await lambda.RunAsync(); that instantiates your middleware and resolves constructor parameters automatically. No reflection, no runtime overhead. -!!! info "ASP.NET Core Similarity" -This mirrors ASP.NET Core's `UseMiddleware()` extension, but uses source generation instead -of reflection for Lambda's cold-start performance requirements. +!!! 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 From 5f18cb4f8a9f0b3517f32774edeef20bf1d9fad0 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 20:24:31 -0500 Subject: [PATCH 24/25] feat(source-generators): refactor `LocationInfo` creation for improved maintainability - Replaced static factory methods in `LocationInfo` with extension methods for cleaner syntax. - Simplified `LocationInfo` creation across multiple syntax providers using new extension methods: - Updated creation calls in `ParameterInfo`, `UseMiddlewareTSyntaxProvider`, and others. - Consolidated duplication of logic for `SyntaxNode`, `ISymbol`, and `Location` into reusable extensions. - Enhanced readability and reduced redundant code across the source generator logic. --- .../Models/KeyedServiceKeyInfo.cs | 2 +- .../Models/LocationInfo.cs | 44 ++++++++++++------- .../Models/MethodInfo.cs | 2 +- .../Models/ParameterInfo.cs | 2 +- .../Extractors/HandlerInfoExtractor.cs | 4 +- ...daApplicationBuilderBuildSyntaxProvider.cs | 2 +- .../UseMiddlewareTSyntaxProvider.cs | 4 +- 7 files changed, 36 insertions(+), 24 deletions(-) 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 index c05fcbbd..854e2621 100644 --- a/src/MinimalLambda.SourceGenerators/Models/MethodInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/MethodInfo.cs @@ -41,7 +41,7 @@ internal static AttributeInfo Create(AttributeData attributeData) { var syntax = attributeData.ApplicationSyntaxReference?.GetSyntax(); var location = syntax?.GetLocation(); - var locationData = LocationInfo.CreateFrom(location); + var locationData = location?.CreateLocationInfo(); var name = attributeData.AttributeClass?.ToString() ?? "UNKNOWN"; diff --git a/src/MinimalLambda.SourceGenerators/Models/ParameterInfo.cs b/src/MinimalLambda.SourceGenerators/Models/ParameterInfo.cs index 65809fc3..3f29672a 100644 --- a/src/MinimalLambda.SourceGenerators/Models/ParameterInfo.cs +++ b/src/MinimalLambda.SourceGenerators/Models/ParameterInfo.cs @@ -22,7 +22,7 @@ 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 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 index bda19a03..a9a735ef 100644 --- a/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs +++ b/src/MinimalLambda.SourceGenerators/SyntaxProviders/UseMiddlewareTSyntaxProvider.cs @@ -73,9 +73,7 @@ targetOperation.Syntax is InvocationExpressionSyntax var useMiddlewareTInfo = new UseMiddlewareTInfo( InterceptableLocationInfo.CreateFrom(interceptableLocation), classInfo, - genericArgumentLocation is not null - ? LocationInfo.CreateFrom(genericArgumentLocation) - : null + genericArgumentLocation?.CreateLocationInfo() ); return useMiddlewareTInfo; From a6ab2525a95d49031a32ab0f655caba2b359bc58 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Thu, 18 Dec 2025 20:27:56 -0500 Subject: [PATCH 25/25] feat(examples): add class-based middleware example in `MinimalLambda.Example.ClassMiddleware` - Introduced a `Middleware` class implementing `ILambdaMiddleware` to demonstrate class-based usage. - Enhanced logging in middleware to log both request and response details. - Updated `Program.cs` to include middleware and log key invocation events. - Improved example readability and showcased middleware's role in request/response processing. --- .../Program.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/examples/MinimalLambda.Example.ClassMiddleware/Program.cs b/examples/MinimalLambda.Example.ClassMiddleware/Program.cs index 59fd1993..66036bd8 100644 --- a/examples/MinimalLambda.Example.ClassMiddleware/Program.cs +++ b/examples/MinimalLambda.Example.ClassMiddleware/Program.cs @@ -1,21 +1,34 @@ using System; +using System.Threading.Tasks; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MinimalLambda; using MinimalLambda.Builder; -// Create the application builder var builder = LambdaApplication.CreateBuilder(); -// Build the Lambda application var lambda = builder.Build(); -// Map your handler - the event is automatically injected lambda.MapHandler( ([FromEvent] Request request) => new Response($"Hello {request.Name}!", DateTime.UtcNow) ); -// Run the Lambda 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); + } +}