├── .gitattributes ├── src ├── Immediate.Handlers.Analyzers │ ├── AnalyzerReleases.Unshipped.md │ ├── Properties │ │ └── launchSettings.json │ ├── Immediate.Handlers.Analyzers.csproj │ ├── DiagnosticIds.cs │ ├── AnalyzerReleases.Shipped.md │ ├── InvalidIHandlerAnalyzer.cs │ ├── Immediate.Handlers.Analyzers.md │ └── BehaviorsAnalyzer.cs ├── Immediate.Handlers.CodeFixes │ ├── AnalyzerReleases.Shipped.md │ ├── AnalyzerReleases.Unshipped.md │ ├── Properties │ │ └── launchSettings.json │ ├── Immediate.Handlers.CodeFixes.csproj │ ├── RefactoringExtensions.cs │ ├── HandlerMethodMustExistCodeFixProvider.cs │ └── StaticToSealedHandlerCodeFixProvider.cs ├── Immediate.Handlers.Shared │ ├── HandlerAttribute.cs │ ├── Immediate.Handlers.Shared.csproj │ ├── .editorconfig │ ├── IHandler.cs │ ├── BehaviorsAttribute.cs │ └── Behavior.cs ├── Immediate.Handlers.Generators │ ├── Properties │ │ └── launchSettings.json │ ├── Utility.cs │ ├── DisplayNameFormatters.cs │ ├── Templates │ │ ├── ServiceCollectionExtensions.sbntxt │ │ └── Handler.sbntxt │ ├── Immediate.Handlers.Generators.csproj │ ├── EquatableReadOnlyList.cs │ ├── Models.cs │ └── TransformBehaviors.cs ├── Common │ ├── SyntaxExtensions.cs │ └── ITypeSymbolExtensions.cs └── Immediate.Handlers │ └── Immediate.Handlers.csproj ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── benchmarks ├── Benchmark.Large │ ├── Program.cs │ ├── Benchmark.Large.csproj │ └── Benchmark.cs ├── Benchmark.Simple │ ├── Program.cs │ └── Benchmark.Simple.csproj └── Benchmark.Behaviors │ ├── Program.cs │ └── Benchmark.Behaviors.csproj ├── tests ├── Immediate.Handlers.Tests │ ├── .editorconfig │ ├── ModuleInitializer.cs │ ├── GeneratorTests │ │ ├── HandlerTests.SimpleParameterAttribute#IH.ServiceCollectionExtensions.g.verified.cs │ │ ├── HandlerTests.ComplexParameterAttribute#IH.ServiceCollectionExtensions.g.verified.cs │ │ ├── HandlerTests.MultipleParameterAttributes#IH.ServiceCollectionExtensions.g.verified.cs │ │ ├── BehaviorTests.CrtpBehavior_assemblies=Msdi#IH.ServiceCollectionExtensions.g.verified.cs │ │ ├── BehaviorTests.NestedBehavior_assemblies=Msdi#IH.ServiceCollectionExtensions.g.verified.cs │ │ ├── BehaviorTests.SingleBehavior_assemblies=Msdi#IH.ServiceCollectionExtensions.g.verified.cs │ │ ├── BehaviorTests.MultipleBehaviors_assemblies=Msdi#IH.ServiceCollectionExtensions.g.verified.cs │ │ ├── HandlerTests.MissingCancellationToken_modifier=static#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── HandlerTests.IntReturnType_modifier=static#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── HandlerTests.MissingCancellationToken_modifier=#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── HandlerTests.VoidReturnType_modifier=static#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── HandlerTests.IntReturnType_modifier=#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── HandlerTests.VoidReturnType_modifier=#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── BehaviorTests.CrtpBehavior_assemblies=Normal#IH..ConstraintHandler.g.verified.cs │ │ ├── BehaviorTests.SingleBehavior_assemblies=Normal#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── BehaviorTests.NestedBehavior_assemblies=Normal#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── HandlerTests.ComplexParameterAttribute#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── HandlerTests.SimpleParameterAttribute#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── HandlerTests.MultipleParameterAttributes#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── GeneratorTestHelper.cs │ │ ├── BehaviorTests.CrtpBehavior_assemblies=Msdi#IH..ConstraintHandler.g.verified.cs │ │ ├── BehaviorTests.NestedBehavior_assemblies=Msdi#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── BehaviorTests.SingleBehavior_assemblies=Msdi#IH.Dummy.GetUsersQuery.g.verified.cs │ │ ├── BehaviorTests.MultipleBehaviors_assemblies=Normal#IH.Dummy.GetUsersQuery.g.verified.cs │ │ └── BehaviorTests.MultipleBehaviors_assemblies=Msdi#IH.Dummy.GetUsersQuery.g.verified.cs │ ├── AnalyzerTests │ │ ├── HandlerClassAnalyzerTests │ │ │ ├── Tests.HandlerClassSealed.cs │ │ │ ├── Tests.HandleMethodDoesNotExist.cs │ │ │ ├── Tests.HandlerClassStatic.cs │ │ │ ├── Tests.HandlerClassNested.cs │ │ │ ├── Tests.HandlerClassMembersPrivate.cs │ │ │ ├── Tests.HandleMethodIsCorrectWithVoidReturn.cs │ │ │ ├── Tests.HandleMethodDoesNotReturnTask.cs │ │ │ ├── Tests.HandleMethodShouldUseCancellationToken.cs │ │ │ ├── Tests.HandleMethodIsCorrectWithIntReturn.cs │ │ │ ├── Tests.HandleMethodIsNotPrivate.cs │ │ │ ├── Tests.HandleMethodTooManyParameters.cs │ │ │ ├── Tests.HandleMethodIsNotUnique.cs │ │ │ └── Tests.HandleMethodMissingRequest.cs │ │ ├── AnalyzerTestHelpers.cs │ │ ├── BehaviorAnalyzerTests │ │ │ ├── Tests.BehaviorTypeDoesNotUseUnboundedReference.cs │ │ │ ├── Tests.BehaviorTypeIsUsedMoreThanOnce.cs │ │ │ ├── Tests.BehaviorTypeDoesNotInheritFromGenericBehavior.cs │ │ │ ├── Tests.BehaviorTypeDoesNotHaveTwoGenericParameters.cs │ │ │ └── Tests.BehaviorTypeIsValid.cs │ │ └── InvalidIHandlerAnalyzerTests.cs │ ├── Helpers │ │ └── ReferenceAssemblyHelpers.cs │ ├── CodeFixTests │ │ ├── CodeFixTestHelper.cs │ │ ├── HandlerMethodMustExistCodeFixProviderTests.cs │ │ └── StaticToSealedHandlerRefactoringProviderTests.cs │ └── Immediate.Handlers.Tests.csproj └── Immediate.Handlers.FunctionalTests │ ├── .editorconfig │ ├── HandlerResolver.cs │ ├── Behavior │ ├── BehaviorTests.cs │ └── Constraints │ │ ├── Tests.BehaviorShouldConstrain_A.cs │ │ ├── Tests.BehaviorShouldConstrain_B.cs │ │ ├── Tests.BehaviorShouldConstrain_C.cs │ │ ├── Tests.BehaviorShouldConstrain_D.cs │ │ └── Tests.Base.cs │ ├── Immediate.Handlers.FunctionalTests.csproj │ ├── HandlerAbstraction │ └── HandlerAbstractionTests.cs │ ├── NoBehaviors │ ├── ParameterizedTests.cs │ └── ParameterlessTests.cs │ └── MultipleBehaviors │ └── MultipleBehaviorsTests.cs ├── .gitignore ├── Immediate.Handlers.sln.DotSettings ├── samples └── Normal │ ├── Behaviors.cs │ ├── Program.cs │ ├── Normal.csproj │ ├── GetWeatherForecast.cs │ └── Properties │ └── launchSettings.json ├── Directory.Build.props ├── license.txt ├── Immediate.Handlers.slnx └── Directory.Packages.props /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Analyzers/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.CodeFixes/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [viceroypenguin] 4 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.CodeFixes/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | ; Unshipped analyzer release 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | -------------------------------------------------------------------------------- /benchmarks/Benchmark.Large/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | 3 | using BenchmarkDotNet.Running; 4 | using Immediate.Handlers.Benchmarks; 5 | 6 | BenchmarkRunner.Run(); 7 | -------------------------------------------------------------------------------- /benchmarks/Benchmark.Simple/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | 3 | using BenchmarkDotNet.Running; 4 | using Immediate.Handlers.Benchmarks; 5 | 6 | BenchmarkRunner.Run(); 7 | 8 | -------------------------------------------------------------------------------- /benchmarks/Benchmark.Behaviors/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | 3 | using BenchmarkDotNet.Running; 4 | using Immediate.Handlers.Benchmarks; 5 | 6 | BenchmarkRunner.Run(); 7 | 8 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Analyzers/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Normal": { 4 | "commandName": "DebugRoslynComponent", 5 | "targetProject": "..//..//Samples//Normal//Normal.csproj" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.CodeFixes/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Normal": { 4 | "commandName": "DebugRoslynComponent", 5 | "targetProject": "..//..//Samples//Normal//Normal.csproj" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | dotnet_diagnostic.CA1707.severity = none # CA1707: Identifiers should not contain underscores 4 | dotnet_diagnostic.CA1822.severity = none # CA1822: Mark members as static 5 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | dotnet_diagnostic.CA1707.severity = none # CA1707: Identifiers should not contain underscores 4 | dotnet_diagnostic.CA1822.severity = none # CA1822: Mark members as static 5 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Shared/HandlerAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Immediate.Handlers.Shared; 2 | 3 | /// 4 | /// Applied to a class to indicate that handler code should be generated. 5 | /// 6 | [AttributeUsage(AttributeTargets.Class)] 7 | public sealed class HandlerAttribute : Attribute; 8 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/ModuleInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Immediate.Handlers.Tests; 4 | 5 | public static class ModuleInitializer 6 | { 7 | [ModuleInitializer] 8 | public static void Init() 9 | { 10 | VerifierSettings.AutoVerify(includeBuildServer: false); 11 | VerifySourceGenerators.Initialize(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Shared/Immediate.Handlers.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0;net10.0 5 | $(NoWarn);CA1716 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Generators/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Normal": { 4 | "commandName": "DebugRoslynComponent", 5 | "targetProject": "..\\..\\samples\\Normal\\Normal.csproj" 6 | }, 7 | "Benchmark.Simple": { 8 | "commandName": "DebugRoslynComponent", 9 | "targetProject": "..\\..\\benchmarks\\Benchmark.Simple\\Benchmark.Simple.csproj" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Common/SyntaxExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | 4 | namespace Immediate.Handlers; 5 | 6 | internal static class SyntaxExtensions 7 | { 8 | public static bool IsCancellationToken(this SemanticModel model, TypeSyntax? typeSyntax, CancellationToken token) => 9 | typeSyntax is { } syntax 10 | && model.GetSymbolInfo(syntax, token).Symbol is INamedTypeSymbol 11 | { 12 | IsCancellationToken: true, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### VisualStudio ### 2 | ## Ignore Visual Studio temporary files, build results, and 3 | ## files generated by popular Visual Studio add-ons. 4 | 5 | # Build results 6 | [Bb]in/ 7 | [Oo]bj/ 8 | 9 | # Visual Studio cache/options directory 10 | .vs/ 11 | *.user 12 | 13 | # VS Code cache/options directory 14 | .vscode/ 15 | 16 | # Rider temporary files 17 | .idea/ 18 | 19 | # TestResults 20 | [Tt]est[Rr]esults/ 21 | BenchmarkDotNet.Artifacts/ 22 | 23 | # Verify 24 | *.received.* 25 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Generators/Utility.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace Immediate.Handlers.Generators; 4 | 5 | internal static class Utility 6 | { 7 | public static ITypeSymbol? GetTaskReturnType(this IMethodSymbol method) => 8 | method.ReturnType.IsValueTask1() 9 | ? ((INamedTypeSymbol)method.ReturnType).TypeArguments.FirstOrDefault() 10 | : null; 11 | 12 | public static string? NullIf(this string value, string check) => 13 | value.Equals(check, StringComparison.Ordinal) ? null : value; 14 | } 15 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Generators/DisplayNameFormatters.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace Immediate.Handlers.Generators; 4 | 5 | internal static class DisplayNameFormatters 6 | { 7 | public static readonly SymbolDisplayFormat NonGenericFqdnFormat = new( 8 | globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, 9 | typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, 10 | genericsOptions: SymbolDisplayGenericsOptions.None // This excludes the generic type arguments 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Shared/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # XML Documentation 4 | dotnet_diagnostic.CS0105.severity = error # CS0105: Using directive is unnecessary. 5 | dotnet_diagnostic.CS1573.severity = error # CS1573: Missing XML comment for parameter 6 | dotnet_diagnostic.CS1591.severity = error # CS1591: Missing XML comment for publicly visible type or member 7 | dotnet_diagnostic.CS1712.severity = error # CS1712: Type parameter has no matching typeparam tag in the XML comment (but other type parameters do) 8 | -------------------------------------------------------------------------------- /Immediate.Handlers.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | DO_NOT_SHOW 3 | True -------------------------------------------------------------------------------- /samples/Normal/Behaviors.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Shared; 2 | using Normal; 3 | 4 | [assembly: Behaviors( 5 | typeof(LoggingBehavior<,>) 6 | )] 7 | 8 | namespace Normal; 9 | 10 | public sealed class LoggingBehavior(ILogger> logger) 11 | : Behavior 12 | { 13 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 14 | { 15 | _ = logger.ToString(); 16 | var response = await Next(request, cancellationToken); 17 | 18 | return response; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/HandlerResolver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Immediate.Handlers.FunctionalTests; 4 | 5 | public static class HandlerResolver 6 | { 7 | public static T Resolve(Action? serviceCollectionConfigurator = null) 8 | where T : notnull 9 | { 10 | var serviceCollection = new ServiceCollection() 11 | .AddImmediateHandlersFunctionalTestsHandlers() 12 | .AddImmediateHandlersFunctionalTestsBehaviors(); 13 | 14 | serviceCollectionConfigurator?.Invoke(serviceCollection); 15 | 16 | var serviceProvider = serviceCollection.BuildServiceProvider(); 17 | return serviceProvider.GetRequiredService(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/Normal/Program.cs: -------------------------------------------------------------------------------- 1 | using Normal; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | 5 | builder.Services.AddEndpointsApiExplorer(); 6 | builder.Services.AddSwaggerGen(); 7 | builder.Services.AddNormalBehaviors(); 8 | builder.Services.AddNormalHandlers(); 9 | 10 | var app = builder.Build(); 11 | 12 | if (app.Environment.IsDevelopment()) 13 | { 14 | _ = app.UseSwagger(); 15 | _ = app.UseSwaggerUI(); 16 | } 17 | 18 | app.UseHttpsRedirection(); 19 | 20 | app.MapGet( 21 | "/weatherforecast", 22 | async (GetWeatherForecast.Handler handler, [AsParameters] GetWeatherForecast.Query query) => await handler.HandleAsync(query) 23 | ) 24 | .WithName("GetWeatherForecast") 25 | .WithOpenApi(); 26 | 27 | app.Run(); 28 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.SimpleParameterAttribute#IH.ServiceCollectionExtensions.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.ServiceCollectionExtensions.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | #pragma warning disable CS1591 6 | 7 | public static class HandlerServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddTestsBehaviors( 10 | this IServiceCollection services) 11 | { 12 | 13 | return services; 14 | } 15 | 16 | public static IServiceCollection AddTestsHandlers( 17 | this IServiceCollection services, 18 | ServiceLifetime lifetime = ServiceLifetime.Scoped 19 | ) 20 | { 21 | global::Dummy.GetUsersQuery.AddHandlers(services, lifetime); 22 | 23 | return services; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.ComplexParameterAttribute#IH.ServiceCollectionExtensions.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.ServiceCollectionExtensions.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | #pragma warning disable CS1591 6 | 7 | public static class HandlerServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddTestsBehaviors( 10 | this IServiceCollection services) 11 | { 12 | 13 | return services; 14 | } 15 | 16 | public static IServiceCollection AddTestsHandlers( 17 | this IServiceCollection services, 18 | ServiceLifetime lifetime = ServiceLifetime.Scoped 19 | ) 20 | { 21 | global::Dummy.GetUsersQuery.AddHandlers(services, lifetime); 22 | 23 | return services; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.MultipleParameterAttributes#IH.ServiceCollectionExtensions.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.ServiceCollectionExtensions.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | #pragma warning disable CS1591 6 | 7 | public static class HandlerServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddTestsBehaviors( 10 | this IServiceCollection services) 11 | { 12 | 13 | return services; 14 | } 15 | 16 | public static IServiceCollection AddTestsHandlers( 17 | this IServiceCollection services, 18 | ServiceLifetime lifetime = ServiceLifetime.Scoped 19 | ) 20 | { 21 | global::Dummy.GetUsersQuery.AddHandlers(services, lifetime); 22 | 23 | return services; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.CrtpBehavior_assemblies=Msdi#IH.ServiceCollectionExtensions.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.ServiceCollectionExtensions.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | #pragma warning disable CS1591 6 | 7 | public static class HandlerServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddTestsBehaviors( 10 | this IServiceCollection services) 11 | { 12 | services.TryAddTransient(typeof(global::ConstraintBehavior<,>)); 13 | 14 | return services; 15 | } 16 | 17 | public static IServiceCollection AddTestsHandlers( 18 | this IServiceCollection services, 19 | ServiceLifetime lifetime = ServiceLifetime.Scoped 20 | ) 21 | { 22 | global::ConstraintHandler.AddHandlers(services, lifetime); 23 | 24 | return services; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.NestedBehavior_assemblies=Msdi#IH.ServiceCollectionExtensions.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.ServiceCollectionExtensions.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | #pragma warning disable CS1591 6 | 7 | public static class HandlerServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddTestsBehaviors( 10 | this IServiceCollection services) 11 | { 12 | services.TryAddTransient(typeof(global::Dummy.LoggingBehavior<,>)); 13 | 14 | return services; 15 | } 16 | 17 | public static IServiceCollection AddTestsHandlers( 18 | this IServiceCollection services, 19 | ServiceLifetime lifetime = ServiceLifetime.Scoped 20 | ) 21 | { 22 | global::Dummy.GetUsersQuery.AddHandlers(services, lifetime); 23 | 24 | return services; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.SingleBehavior_assemblies=Msdi#IH.ServiceCollectionExtensions.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.ServiceCollectionExtensions.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | #pragma warning disable CS1591 6 | 7 | public static class HandlerServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddTestsBehaviors( 10 | this IServiceCollection services) 11 | { 12 | services.TryAddTransient(typeof(global::Dummy.LoggingBehavior<,>)); 13 | 14 | return services; 15 | } 16 | 17 | public static IServiceCollection AddTestsHandlers( 18 | this IServiceCollection services, 19 | ServiceLifetime lifetime = ServiceLifetime.Scoped 20 | ) 21 | { 22 | global::Dummy.GetUsersQuery.AddHandlers(services, lifetime); 23 | 24 | return services; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: nuget 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | rebase-strategy: auto 13 | ignore: 14 | - dependency-name: "Microsoft.CodeAnalysis.Common" 15 | - dependency-name: "Microsoft.CodeAnalysis.CSharp" 16 | - dependency-name: "Microsoft.CodeAnalysis.CSharp.Workspaces" 17 | - dependency-name: "Microsoft.CodeAnalysis.Workspaces.Common" 18 | 19 | - package-ecosystem: github-actions 20 | directory: "/" 21 | schedule: 22 | interval: weekly 23 | rebase-strategy: auto 24 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/Behavior/BehaviorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Immediate.Handlers.FunctionalTests.Behavior; 2 | 3 | public sealed class BehaviorTests 4 | { 5 | private sealed class TestBehavior : Shared.Behavior 6 | { 7 | public override async ValueTask HandleAsync(int request, CancellationToken cancellationToken) 8 | { 9 | return await Next(request, cancellationToken); 10 | } 11 | } 12 | 13 | [Fact] 14 | public void CannotSetHandlerTwice() 15 | { 16 | var handler = new TestBehavior(); 17 | handler.SetInnerHandler(handler); 18 | _ = Assert.Throws(() => 19 | handler.SetInnerHandler(handler)); 20 | } 21 | 22 | [Fact] 23 | public async Task MustSetHandlerBeforeCallingNext() 24 | { 25 | var handler = new TestBehavior(); 26 | _ = await Assert.ThrowsAsync(async () => 27 | await handler.HandleAsync(1, CancellationToken.None)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | latest 4 | 5 | enable 6 | $(WarningsAsErrors);nullable; 7 | 8 | enable 9 | 10 | latest-all 11 | true 12 | 13 | true 14 | true 15 | 16 | false 17 | false 18 | 19 | 20 | 21 | true 22 | true 23 | true 24 | opencover 25 | 26 | 27 | -------------------------------------------------------------------------------- /samples/Normal/Normal.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Generators/Templates/ServiceCollectionExtensions.sbntxt: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | 4 | #pragma warning disable CS1591 5 | 6 | {{~ if !string.empty namespace ~}} 7 | namespace {{ namespace }}; 8 | 9 | {{~ end ~}} 10 | public static class HandlerServiceCollectionExtensions 11 | { 12 | public static IServiceCollection Add{{ assembly_name }}Behaviors( 13 | this IServiceCollection services) 14 | { 15 | {{~ for b in behaviors ~}} 16 | services.TryAddTransient(typeof({{ b.registration_type }})); 17 | {{~ end ~}} 18 | 19 | return services; 20 | } 21 | 22 | public static IServiceCollection Add{{ assembly_name }}Handlers( 23 | this IServiceCollection services, 24 | ServiceLifetime lifetime = ServiceLifetime.Scoped 25 | ) 26 | { 27 | {{~ for h in handlers ~}} 28 | {{ h }}.AddHandlers(services, lifetime); 29 | {{~ end ~}} 30 | 31 | return services; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Shared/IHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Immediate.Handlers.Shared; 2 | 3 | /// 4 | /// Represents an abstraction implemented by the source-generated handlers 5 | /// 6 | /// 7 | /// The type of the command request 8 | /// 9 | /// 10 | /// The type of the command response 11 | /// 12 | public interface IHandler 13 | { 14 | /// 15 | /// The handlers entrypoint. Will invoke the logic of your defined handler, including potential behaviors 16 | /// 17 | /// 18 | /// The request payload to be handled 19 | /// 20 | /// 21 | /// Optional cancellation token passed to the handler 22 | /// 23 | /// 24 | /// The command response returned by the inner handler 25 | /// 26 | ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken = default); 27 | } 28 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/Immediate.Handlers.FunctionalTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0;net10.0 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Analyzers/Immediate.Handlers.Analyzers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | minor 22 | preview.0 23 | v 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/HandlerAbstraction/HandlerAbstractionTests.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Shared; 2 | 3 | namespace Immediate.Handlers.FunctionalTests.HandlerAbstraction; 4 | 5 | public sealed record HandlerAbstractionOneAdderQuery(int Input); 6 | 7 | [Handler] 8 | public static partial class HandlerAbstractionOneAdder 9 | { 10 | private static ValueTask HandleAsync( 11 | HandlerAbstractionOneAdderQuery handlerAbstractionOneAdderQuery, 12 | CancellationToken _) 13 | { 14 | return ValueTask.FromResult(handlerAbstractionOneAdderQuery.Input + 1); 15 | } 16 | } 17 | 18 | public sealed class HandlerAbstractionTests 19 | { 20 | [Fact] 21 | public async Task NoBehaviorShouldReturnExpectedResponseForAbstraction() 22 | { 23 | const int Input = 1; 24 | 25 | var handler = HandlerResolver.Resolve>(); 26 | 27 | var query = new HandlerAbstractionOneAdderQuery(Input); 28 | 29 | var result = await handler.HandleAsync(query, TestContext.Current.CancellationToken); 30 | 31 | Assert.Equal(Input + 1, result); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'master' 8 | paths-ignore: 9 | - '**/readme.md' 10 | pull_request: 11 | types: [opened, synchronize, reopened] 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v6 20 | 21 | - name: Setup .NET 22 | uses: actions/setup-dotnet@v5 23 | with: 24 | dotnet-version: | 25 | 8.0.x 26 | 9.0.x 27 | 28 | - name: Setup .NET 29 | uses: actions/setup-dotnet@v5 30 | with: 31 | dotnet-quality: 'preview' 32 | dotnet-version: | 33 | 10.0.x 34 | 35 | - name: Restore dependencies 36 | run: dotnet restore 37 | - name: Build 38 | run: dotnet build -c Release --no-restore 39 | - name: Test 40 | run: dotnet test -c Release --no-build 41 | 42 | - name: Upload coverage reports to Codecov with GitHub Action 43 | uses: codecov/codecov-action@v5 44 | with: 45 | token: ${{ secrets.CODECOV_TOKEN }} 46 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandlerClassSealed.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | public sealed partial class Tests 7 | { 8 | [Fact] 9 | public async Task HandlerClassNotSealed_DoesAlert() => 10 | await AnalyzerTestHelpers.CreateAnalyzerTest( 11 | """ 12 | using System; 13 | using System.Collections.Generic; 14 | using System.IO; 15 | using System.Linq; 16 | using System.Net.Http; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | using Immediate.Handlers.Shared; 20 | 21 | [Handler] 22 | public partial class {|IHR0016:GetUsersQuery|} 23 | { 24 | public record Query; 25 | 26 | private ValueTask HandleAsync( 27 | Query _, 28 | CancellationToken token) 29 | { 30 | return ValueTask.FromResult(0); 31 | } 32 | } 33 | """, 34 | DriverReferenceAssemblies.Normal 35 | ).RunAsync(TestContext.Current.CancellationToken); 36 | } 37 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandleMethodDoesNotExist.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task HandleMethodDoesNotExist_AlertDiagnostic() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | 22 | [Handler] 23 | public static class {|IHR0001:GetUsersQuery|} 24 | { 25 | public record Query; 26 | } 27 | """, 28 | DriverReferenceAssemblies.Normal 29 | ).RunAsync(TestContext.Current.CancellationToken); 30 | } 31 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/Helpers/ReferenceAssemblyHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.CodeAnalysis; 3 | 4 | namespace Immediate.Handlers.Tests.Helpers; 5 | 6 | public static class ReferenceAssemblyHelpers 7 | { 8 | public static IEnumerable GetAdditionalReferences(this DriverReferenceAssemblies assemblies) 9 | { 10 | List references = 11 | [ 12 | MetadataReference.CreateFromFile("./Immediate.Handlers.Shared.dll"), 13 | ]; 14 | 15 | if (assemblies is DriverReferenceAssemblies.Normal) 16 | return references; 17 | 18 | references.AddRange( 19 | [ 20 | MetadataReference.CreateFromFile("./Microsoft.Extensions.DependencyInjection.dll"), 21 | MetadataReference.CreateFromFile("./Microsoft.Extensions.DependencyInjection.Abstractions.dll"), 22 | ] 23 | ); 24 | 25 | if (assemblies is DriverReferenceAssemblies.Msdi) 26 | return references; 27 | 28 | // to be done with other renderers 29 | throw new UnreachableException(); 30 | } 31 | } 32 | 33 | public enum DriverReferenceAssemblies 34 | { 35 | None = 0, 36 | Normal, 37 | Msdi, 38 | } 39 | -------------------------------------------------------------------------------- /samples/Normal/GetWeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Shared; 2 | 3 | namespace Normal; 4 | 5 | [Handler] 6 | public static partial class GetWeatherForecast 7 | { 8 | private static readonly string[] s_summaries = 9 | [ 10 | "Freezing", 11 | "Bracing", 12 | "Chilly", 13 | "Cool", 14 | "Mild", 15 | "Warm", 16 | "Balmy", 17 | "Hot", 18 | "Sweltering", 19 | "Scorching", 20 | ]; 21 | 22 | public sealed record Query; 23 | 24 | public sealed record Response(DateOnly Date, int TemperatureC, string? Summary) 25 | { 26 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 27 | } 28 | 29 | private static ValueTask> HandleAsync( 30 | Query _, 31 | CancellationToken token 32 | ) 33 | { 34 | token.ThrowIfCancellationRequested(); 35 | 36 | var forecast = Enumerable.Range(1, 5) 37 | .Select(index => 38 | new Response 39 | ( 40 | DateOnly.FromDateTime(DateTime.Now.AddDays(index)), 41 | Random.Shared.Next(-20, 55), 42 | s_summaries[Random.Shared.Next(s_summaries.Length)] 43 | ) 44 | ); 45 | 46 | return ValueTask.FromResult(forecast); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandlerClassStatic.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | public sealed partial class Tests 7 | { 8 | [Fact] 9 | public async Task HandlerClassNotStatic_DoesAlert() => 10 | await AnalyzerTestHelpers.CreateAnalyzerTest( 11 | """ 12 | using System; 13 | using System.Collections.Generic; 14 | using System.IO; 15 | using System.Linq; 16 | using System.Net.Http; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | using Immediate.Handlers.Shared; 20 | 21 | [Handler] 22 | public partial class {|IHR0019:{|IHR0018:GetUsersQuery|}|} 23 | { 24 | public record Query; 25 | 26 | private static ValueTask HandleAsync( 27 | Query _, 28 | CancellationToken token) 29 | { 30 | return ValueTask.FromResult(0); 31 | } 32 | } 33 | """, 34 | DriverReferenceAssemblies.Normal 35 | ).RunAsync(TestContext.Current.CancellationToken); 36 | } 37 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Immediate.Handlers developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandlerClassNested.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | public sealed partial class Tests 7 | { 8 | [Fact] 9 | public async Task HandlerClassNested_DoesAlert() => 10 | await AnalyzerTestHelpers.CreateAnalyzerTest( 11 | """ 12 | using System; 13 | using System.Collections.Generic; 14 | using System.IO; 15 | using System.Linq; 16 | using System.Net.Http; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | using Immediate.Handlers.Shared; 20 | 21 | public static partial class Wrapper 22 | { 23 | [Handler] 24 | public static class {|IHR0019:{|IHR0005:GetUsersQuery|}|} 25 | { 26 | public record Query; 27 | 28 | private static ValueTask HandleAsync( 29 | Query _, 30 | CancellationToken token) 31 | { 32 | return ValueTask.FromResult(0); 33 | } 34 | } 35 | } 36 | """, 37 | DriverReferenceAssemblies.Normal 38 | ).RunAsync(TestContext.Current.CancellationToken); 39 | } 40 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/NoBehaviors/ParameterizedTests.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Shared; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Immediate.Handlers.FunctionalTests.NoBehaviors; 5 | 6 | [Handler] 7 | public static partial class NoBehaviorParameterizedOneAdder 8 | { 9 | public sealed record Query(int Input); 10 | 11 | private static ValueTask Handle( 12 | Query query, 13 | AddendProvider addendProvider, 14 | CancellationToken _) 15 | { 16 | return ValueTask.FromResult(query.Input + addendProvider.Addend); 17 | } 18 | } 19 | 20 | public sealed record AddendProvider(int Addend); 21 | 22 | public sealed class ParameterizedTests 23 | { 24 | [Fact] 25 | public async Task NoBehaviorShouldReturnExpectedResponse() 26 | { 27 | const int Input = 1; 28 | var addendProvider = new AddendProvider(1); 29 | 30 | var handler = HandlerResolver.Resolve(x => x.AddScoped(_ => addendProvider)); 31 | 32 | var query = new NoBehaviorParameterizedOneAdder.Query(Input); 33 | 34 | var result = await handler.HandleAsync(query, TestContext.Current.CancellationToken); 35 | 36 | Assert.Equal(Input + addendProvider.Addend, result); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.MultipleBehaviors_assemblies=Msdi#IH.ServiceCollectionExtensions.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.ServiceCollectionExtensions.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | #pragma warning disable CS1591 6 | 7 | public static class HandlerServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddTestsBehaviors( 10 | this IServiceCollection services) 11 | { 12 | services.TryAddTransient(typeof(global::Dummy.LoggingBehavior<,>)); 13 | services.TryAddTransient(typeof(global::YetAnotherDummy.OtherBehavior<,>)); 14 | services.TryAddTransient(typeof(global::Dummy.SecondLoggingBehavior<,>)); 15 | services.TryAddTransient(typeof(global::YetAnotherDummy.LoggingBehavior<,>)); 16 | services.TryAddTransient(typeof(global::YetAnotherDummy.SecondLoggingBehavior<,>)); 17 | 18 | return services; 19 | } 20 | 21 | public static IServiceCollection AddTestsHandlers( 22 | this IServiceCollection services, 23 | ServiceLifetime lifetime = ServiceLifetime.Scoped 24 | ) 25 | { 26 | global::Dummy.GetUsersQuery.AddHandlers(services, lifetime); 27 | 28 | return services; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.CodeFixes/Immediate.Handlers.CodeFixes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | minor 27 | preview.0 28 | v 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandlerClassMembersPrivate.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | public sealed partial class Tests 7 | { 8 | [Fact] 9 | public async Task HandlerClassMembers_DoesAlert() => 10 | await AnalyzerTestHelpers.CreateAnalyzerTest( 11 | """ 12 | using System; 13 | using System.Collections.Generic; 14 | using System.IO; 15 | using System.Linq; 16 | using System.Net.Http; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | using Immediate.Handlers.Shared; 20 | 21 | [Handler] 22 | public sealed partial class GetUsersQuery 23 | { 24 | public record Query; 25 | 26 | private ValueTask HandleAsync( 27 | Query _, 28 | CancellationToken token) 29 | { 30 | return ValueTask.FromResult(0); 31 | } 32 | 33 | public void {|IHR0017:Test1|}() { } 34 | protected int {|IHR0017:Test2|} => 1; 35 | internal int {|IHR0017:_test3|}; 36 | } 37 | """, 38 | DriverReferenceAssemblies.Normal 39 | ).RunAsync(TestContext.Current.CancellationToken); 40 | } 41 | -------------------------------------------------------------------------------- /samples/Normal/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:33555", 8 | "sslPort": 44332 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5172", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7288;http://localhost:5172", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/Behavior/Constraints/Tests.BehaviorShouldConstrain_A.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Shared; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Immediate.Handlers.FunctionalTests.Behavior.Constraints; 5 | 6 | [Handler] 7 | [Behaviors(typeof(BehaviorA<,>), typeof(BehaviorB<,>), typeof(BehaviorC<,>), typeof(BehaviorD<,>))] 8 | public static partial class BehaviorShouldConstrainA 9 | { 10 | public sealed record Query(int Input) : A; 11 | 12 | private static ValueTask Handle( 13 | Query query, 14 | CancellationToken _) 15 | { 16 | return ValueTask.FromResult(query.Input + 1); 17 | } 18 | } 19 | 20 | public sealed partial class Tests 21 | { 22 | [Fact] 23 | public async Task BehaviorShouldConstrain_A() 24 | { 25 | IServiceCollection services = new ServiceCollection(); 26 | services = ConfigureBehaviors(services); 27 | services = BehaviorShouldConstrainA.AddHandlers(services); 28 | var serviceProvider = services.BuildServiceProvider(); 29 | 30 | var handler = serviceProvider.GetRequiredService(); 31 | _ = await handler.HandleAsync(new(1), TestContext.Current.CancellationToken); 32 | 33 | var behaviorWalker = serviceProvider.GetRequiredService(); 34 | 35 | Assert.Equal(["BehaviorA"], behaviorWalker.BehaviorsRan); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/Behavior/Constraints/Tests.BehaviorShouldConstrain_B.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Shared; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable CA1707 5 | namespace Immediate.Handlers.FunctionalTests.Behavior.Constraints; 6 | 7 | [Handler] 8 | [Behaviors(typeof(BehaviorA<,>), typeof(BehaviorB<,>), typeof(BehaviorC<,>), typeof(BehaviorD<,>))] 9 | public static partial class BehaviorShouldConstrainB 10 | { 11 | public sealed record Query(int Input) : B; 12 | 13 | private static ValueTask Handle( 14 | Query query, 15 | CancellationToken _) 16 | { 17 | return ValueTask.FromResult(query.Input + 1); 18 | } 19 | } 20 | 21 | public sealed partial class Tests 22 | { 23 | [Fact] 24 | public async Task BehaviorShouldConstrain_B() 25 | { 26 | IServiceCollection services = new ServiceCollection(); 27 | services = ConfigureBehaviors(services); 28 | services = BehaviorShouldConstrainB.AddHandlers(services); 29 | var serviceProvider = services.BuildServiceProvider(); 30 | 31 | var handler = serviceProvider.GetRequiredService(); 32 | _ = await handler.HandleAsync(new(1), TestContext.Current.CancellationToken); 33 | 34 | var behaviorWalker = serviceProvider.GetRequiredService(); 35 | 36 | Assert.Equal(["BehaviorA", "BehaviorB"], behaviorWalker.BehaviorsRan); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/Behavior/Constraints/Tests.BehaviorShouldConstrain_C.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Shared; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable CA1707 5 | namespace Immediate.Handlers.FunctionalTests.Behavior.Constraints; 6 | 7 | [Handler] 8 | [Behaviors(typeof(BehaviorA<,>), typeof(BehaviorB<,>), typeof(BehaviorC<,>), typeof(BehaviorD<,>))] 9 | public static partial class BehaviorShouldConstrainC 10 | { 11 | public sealed record Query(int Input) : C; 12 | 13 | private static ValueTask Handle( 14 | Query query, 15 | CancellationToken _) 16 | { 17 | return ValueTask.FromResult(query.Input + 1); 18 | } 19 | } 20 | 21 | public sealed partial class Tests 22 | { 23 | [Fact] 24 | public async Task BehaviorShouldConstrain_C() 25 | { 26 | IServiceCollection services = new ServiceCollection(); 27 | services = ConfigureBehaviors(services); 28 | services = BehaviorShouldConstrainC.AddHandlers(services); 29 | var serviceProvider = services.BuildServiceProvider(); 30 | 31 | var handler = serviceProvider.GetRequiredService(); 32 | _ = await handler.HandleAsync(new(1), TestContext.Current.CancellationToken); 33 | 34 | var behaviorWalker = serviceProvider.GetRequiredService(); 35 | 36 | Assert.Equal(["BehaviorA", "BehaviorC"], behaviorWalker.BehaviorsRan); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/Behavior/Constraints/Tests.BehaviorShouldConstrain_D.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Shared; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable CA1707 5 | namespace Immediate.Handlers.FunctionalTests.Behavior.Constraints; 6 | 7 | [Handler] 8 | [Behaviors(typeof(BehaviorA<,>), typeof(BehaviorB<,>), typeof(BehaviorC<,>), typeof(BehaviorD<,>))] 9 | public static partial class BehaviorShouldConstrainD 10 | { 11 | public sealed record Query(int Input) : D; 12 | 13 | private static ValueTask Handle( 14 | Query query, 15 | CancellationToken _) 16 | { 17 | return ValueTask.FromResult(query.Input + 1); 18 | } 19 | } 20 | 21 | public sealed partial class Tests 22 | { 23 | [Fact] 24 | public async Task BehaviorShouldConstrain_D() 25 | { 26 | IServiceCollection services = new ServiceCollection(); 27 | services = ConfigureBehaviors(services); 28 | services = BehaviorShouldConstrainD.AddHandlers(services); 29 | var serviceProvider = services.BuildServiceProvider(); 30 | 31 | var handler = serviceProvider.GetRequiredService(); 32 | _ = await handler.HandleAsync(new(1), TestContext.Current.CancellationToken); 33 | 34 | var behaviorWalker = serviceProvider.GetRequiredService(); 35 | 36 | Assert.Equal(["BehaviorA", "BehaviorB", "BehaviorD"], behaviorWalker.BehaviorsRan); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/AnalyzerTestHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Immediate.Handlers.Generators; 3 | using Immediate.Handlers.Tests.Helpers; 4 | using Microsoft.CodeAnalysis.CSharp.Testing; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using Microsoft.CodeAnalysis.Testing; 7 | 8 | namespace Immediate.Handlers.Tests.AnalyzerTests; 9 | 10 | public static class AnalyzerTestHelpers 11 | { 12 | public static CSharpAnalyzerTest CreateAnalyzerTest( 13 | [StringSyntax("c#-test")] string inputSource, 14 | DriverReferenceAssemblies assemblies) 15 | where TAnalyzer : DiagnosticAnalyzer, new() 16 | { 17 | var csTest = new ImmediateHandlersGeneratorAnalyzerTest 18 | { 19 | TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck, 20 | TestState = 21 | { 22 | Sources = { inputSource }, 23 | ReferenceAssemblies = ReferenceAssemblies.Net.Net80, 24 | }, 25 | }; 26 | 27 | csTest.TestState.AdditionalReferences 28 | .AddRange(assemblies.GetAdditionalReferences()); 29 | 30 | return csTest; 31 | } 32 | 33 | private sealed class ImmediateHandlersGeneratorAnalyzerTest : CSharpAnalyzerTest 34 | where TAnalyzer : DiagnosticAnalyzer, new() 35 | { 36 | protected override IEnumerable GetSourceGenerators() => 37 | [typeof(ImmediateHandlersGenerator)]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Analyzers/DiagnosticIds.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Immediate.Handlers.Analyzers; 4 | 5 | [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Diagnostic IDs start with IHR")] 6 | internal static class DiagnosticIds 7 | { 8 | public const string IHR0001HandlerMethodMustExist = "IHR0001"; 9 | public const string IHR0002HandlerMethodMustReturnTask = "IHR0002"; 10 | public const string IHR0005HandlerClassMustNotBeNested = "IHR0005"; 11 | public const string IHR0006BehaviorsMustInheritFromBehavior = "IHR0006"; 12 | public const string IHR0007BehaviorsMustHaveTwoGenericParameters = "IHR0007"; 13 | public const string IHR0008BehaviorsMustUseUnboundGenerics = "IHR0008"; 14 | public const string IHR0010HandlerMethodMustBeUnique = "IHR0010"; 15 | public const string IHR0011HandlerMethodMustBePrivate = "IHR0011"; 16 | public const string IHR0012HandlerShouldUseCancellationToken = "IHR0012"; 17 | public const string IHR0013IHandlerMissingImplementation = "IHR0013"; 18 | public const string IHR0014HandlerMethodMissingRequest = "IHR0014"; 19 | public const string IHR0015HandlerMethodHasTooManyParameters = "IHR0015"; 20 | public const string IHR0016ContainingClassMustBeSealed = "IHR0016"; 21 | public const string IHR0017ContainingClassInstanceMembersMustBePrivate = "IHR0017"; 22 | public const string IHR0018ContainingClassMustBeStatic = "IHR0018"; 23 | public const string IHR0019StaticHandlerCouldBeSealed = "IHR0019"; 24 | } 25 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Shared/BehaviorsAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Immediate.Handlers.Shared; 2 | 3 | /// 4 | /// Allows the specification of s that should be used as part of the 5 | /// pipeline for handling a request. 6 | /// 7 | /// 8 | /// 9 | /// If applied to the Assembly ([assembly: Behavior()]), then the given 10 | /// s will be part of the pipeline for all requests across the assembly. 11 | /// 12 | /// 13 | /// If applied to a , then the given s will 14 | /// be part of the pipeline for the request. 15 | /// 16 | /// 17 | /// However, any that is invalid for a given type will be excluded from 18 | /// the pipeline for that type. 19 | /// 20 | /// 21 | /// 22 | /// The types for each of the s that should be part of the pipeline. 23 | /// 24 | [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)] 25 | public sealed class BehaviorsAttribute(params Type[] types) : Attribute 26 | { 27 | /// 28 | /// The types for each of the s that should be part of the pipeline. 29 | /// 30 | public Type[] Types { get; } = types; 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - '**' 8 | 9 | jobs: 10 | release: 11 | permissions: 12 | id-token: write # enable GitHub OIDC token issuance for this job 13 | contents: write # enable github releases 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v6 19 | 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v5 22 | with: 23 | dotnet-version: | 24 | 8.0.x 25 | 9.0.x 26 | 27 | - name: Setup .NET 28 | uses: actions/setup-dotnet@v5 29 | with: 30 | dotnet-quality: 'preview' 31 | dotnet-version: | 32 | 10.0.x 33 | 34 | - name: Restore dependencies 35 | run: dotnet restore 36 | - name: Build 37 | run: dotnet build -c Release --no-restore 38 | 39 | - name: Package 40 | run: dotnet pack -c Release --no-build --property:PackageOutputPath=../../nupkgs 41 | 42 | - name: NuGet login (OIDC → temp API key) 43 | uses: NuGet/login@v1 44 | id: login 45 | with: 46 | user: viceroypenguin 47 | 48 | - name: Push to Nuget 49 | run: dotnet nuget push "./nupkgs/*.nupkg" --source "https://api.nuget.org/v3/index.json" --api-key ${{ steps.login.outputs.NUGET_API_KEY }} 50 | 51 | - name: Create Release 52 | uses: ncipollo/release-action@v1 53 | with: 54 | generateReleaseNotes: 'true' 55 | makeLatest: 'true' 56 | -------------------------------------------------------------------------------- /benchmarks/Benchmark.Large/Benchmark.Large.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | Immediate.Handlers.Benchmarks 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /benchmarks/Benchmark.Simple/Benchmark.Simple.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | Immediate.Handlers.Benchmarks 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /benchmarks/Benchmark.Behaviors/Benchmark.Behaviors.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | Immediate.Handlers.Benchmarks 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Immediate.Handlers.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.CodeFixes/RefactoringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeFixes; 5 | using Microsoft.CodeAnalysis.Text; 6 | 7 | namespace Immediate.Handlers.CodeFixes; 8 | 9 | [ExcludeFromCodeCoverage] 10 | internal static class RefactoringExtensions 11 | { 12 | internal static void Deconstruct( 13 | this CodeFixContext context, 14 | out Document document, 15 | out TextSpan span, 16 | out ImmutableArray diagnostics, 17 | out CancellationToken cancellationToken) 18 | { 19 | document = context.Document; 20 | span = context.Span; 21 | diagnostics = context.Diagnostics; 22 | cancellationToken = context.CancellationToken; 23 | } 24 | 25 | public static async ValueTask GetRequiredSyntaxRootAsync(this Document document, CancellationToken cancellationToken) 26 | { 27 | if (document.TryGetSyntaxRoot(out var root)) 28 | return root; 29 | 30 | return await document.GetSyntaxRootAsync(cancellationToken) 31 | ?? throw new InvalidOperationException($"Failed to retrieve the syntax root for document '{document.Name ?? document.FilePath ?? "unknown"}'."); 32 | } 33 | 34 | public static async ValueTask GetRequiredSemanticModelAsync(this Document document, CancellationToken cancellationToken) 35 | { 36 | if (document.TryGetSemanticModel(out var semanticModel)) 37 | return semanticModel; 38 | 39 | return await document.GetSemanticModelAsync(cancellationToken) 40 | ?? throw new InvalidOperationException("Could not retrieve semantic model for the document."); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/NoBehaviors/ParameterlessTests.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Shared; 2 | 3 | namespace Immediate.Handlers.FunctionalTests.NoBehaviors; 4 | 5 | [Handler] 6 | public static partial class NoBehaviorParameterlessOneAdder 7 | { 8 | public sealed record Query(int Input); 9 | 10 | private static ValueTask HandleAsync( 11 | Query query, 12 | CancellationToken _) 13 | { 14 | return ValueTask.FromResult(query.Input + 1); 15 | } 16 | } 17 | 18 | [Handler] 19 | public static partial class NoBehaviorNoTokenOneAdder 20 | { 21 | public sealed record Query(int Input); 22 | 23 | private static ValueTask HandleAsync( 24 | Query query 25 | ) 26 | { 27 | return ValueTask.FromResult(query.Input + 1); 28 | } 29 | } 30 | 31 | public sealed class ParameterlessTests 32 | { 33 | [Fact] 34 | public async Task NoBehaviorShouldReturnExpectedResponse() 35 | { 36 | const int Input = 1; 37 | 38 | var handler = HandlerResolver.Resolve(); 39 | 40 | var query = new NoBehaviorParameterlessOneAdder.Query(Input); 41 | 42 | var result = await handler.HandleAsync(query, TestContext.Current.CancellationToken); 43 | 44 | Assert.Equal(Input + 1, result); 45 | } 46 | 47 | [Fact] 48 | public async Task NoTokenShouldReturnExpectedResponse() 49 | { 50 | const int Input = 1; 51 | 52 | var handler = HandlerResolver.Resolve(); 53 | 54 | var query = new NoBehaviorNoTokenOneAdder.Query(Input); 55 | 56 | var result = await handler.HandleAsync(query, TestContext.Current.CancellationToken); 57 | 58 | Assert.Equal(Input + 1, result); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/CodeFixTests/CodeFixTestHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Immediate.Handlers.Tests.Helpers; 3 | using Microsoft.CodeAnalysis.CodeFixes; 4 | using Microsoft.CodeAnalysis.CSharp.Testing; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using Microsoft.CodeAnalysis.Testing; 7 | 8 | namespace Immediate.Handlers.Tests.CodeFixTests; 9 | 10 | public static class CodeFixTestHelper 11 | { 12 | private const string EditorConfig = """ 13 | root = true 14 | 15 | [*.cs] 16 | charset = utf-8 17 | indent_style = tab 18 | insert_final_newline = true 19 | indent_size = 4 20 | """; 21 | 22 | public static CSharpCodeFixTest CreateCodeFixTest( 23 | [StringSyntax("c#-test")] string inputSource, 24 | [StringSyntax("c#-test")] string fixedSource, 25 | DriverReferenceAssemblies assemblies, 26 | int codeActionIndex = 0 27 | ) 28 | where TAnalyzer : DiagnosticAnalyzer, new() 29 | where TCodeFix : CodeFixProvider, new() 30 | { 31 | var csTest = new CSharpCodeFixTest 32 | { 33 | CodeActionIndex = codeActionIndex, 34 | TestState = 35 | { 36 | Sources = { inputSource }, 37 | AnalyzerConfigFiles = 38 | { 39 | { ("/.editorconfig", EditorConfig) }, 40 | }, 41 | ReferenceAssemblies = ReferenceAssemblies.Net.Net80, 42 | }, 43 | FixedState = 44 | { 45 | MarkupHandling = MarkupMode.IgnoreFixable, 46 | Sources = { fixedSource }, 47 | }, 48 | }; 49 | 50 | csTest.TestState.AdditionalReferences 51 | .AddRange(assemblies.GetAdditionalReferences()); 52 | 53 | return csTest; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.MissingCancellationToken_modifier=static#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | #pragma warning disable CS1591 3 | 4 | namespace Dummy; 5 | 6 | partial class GetUsersQuery 7 | { 8 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 9 | { 10 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 11 | 12 | public Handler( 13 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior 14 | ) 15 | { 16 | var handlerType = typeof(GetUsersQuery); 17 | 18 | _handleBehavior = handleBehavior; 19 | 20 | } 21 | 22 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 23 | global::Dummy.GetUsersQuery.Query request, 24 | global::System.Threading.CancellationToken cancellationToken = default 25 | ) 26 | { 27 | return await _handleBehavior 28 | .HandleAsync(request, cancellationToken) 29 | .ConfigureAwait(false); 30 | } 31 | } 32 | 33 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 34 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 35 | { 36 | 37 | public HandleBehavior( 38 | ) 39 | { 40 | } 41 | 42 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 43 | global::Dummy.GetUsersQuery.Query request, 44 | global::System.Threading.CancellationToken cancellationToken 45 | ) 46 | { 47 | return await global::Dummy.GetUsersQuery 48 | .HandleAsync( 49 | request 50 | ) 51 | .ConfigureAwait(false); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.IntReturnType_modifier=static#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | #pragma warning disable CS1591 3 | 4 | namespace Dummy; 5 | 6 | partial class GetUsersQuery 7 | { 8 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 9 | { 10 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 11 | 12 | public Handler( 13 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior 14 | ) 15 | { 16 | var handlerType = typeof(GetUsersQuery); 17 | 18 | _handleBehavior = handleBehavior; 19 | 20 | } 21 | 22 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 23 | global::Dummy.GetUsersQuery.Query request, 24 | global::System.Threading.CancellationToken cancellationToken = default 25 | ) 26 | { 27 | return await _handleBehavior 28 | .HandleAsync(request, cancellationToken) 29 | .ConfigureAwait(false); 30 | } 31 | } 32 | 33 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 34 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 35 | { 36 | 37 | public HandleBehavior( 38 | ) 39 | { 40 | } 41 | 42 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 43 | global::Dummy.GetUsersQuery.Query request, 44 | global::System.Threading.CancellationToken cancellationToken 45 | ) 46 | { 47 | return await global::Dummy.GetUsersQuery 48 | .HandleAsync( 49 | request 50 | , cancellationToken 51 | ) 52 | .ConfigureAwait(false); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Generators/Immediate.Handlers.Generators.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | true 7 | $(NoWarn);CA1716 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | $(GetTargetPathDependsOn);GetDependencyTargetPaths 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | minor 40 | preview.0 41 | v 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Generators/EquatableReadOnlyList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Immediate.Handlers.Generators; 5 | 6 | [ExcludeFromCodeCoverage] 7 | public static class EquatableReadOnlyList 8 | { 9 | public static EquatableReadOnlyList ToEquatableReadOnlyList(this IEnumerable enumerable) 10 | => new(enumerable is IReadOnlyList l ? l : [.. enumerable]); 11 | } 12 | 13 | /// 14 | /// A wrapper for IReadOnlyList that provides value equality support for the wrapped list. 15 | /// 16 | [ExcludeFromCodeCoverage] 17 | public readonly struct EquatableReadOnlyList( 18 | IReadOnlyList? collection 19 | ) : IEquatable>, IReadOnlyList 20 | { 21 | private IReadOnlyList Collection => collection ?? []; 22 | 23 | public bool Equals(EquatableReadOnlyList other) 24 | => this.SequenceEqual(other); 25 | 26 | public override bool Equals(object? obj) 27 | => obj is EquatableReadOnlyList other && Equals(other); 28 | 29 | public override int GetHashCode() 30 | { 31 | var hashCode = new HashCode(); 32 | 33 | foreach (var item in Collection) 34 | hashCode.Add(item); 35 | 36 | return hashCode.ToHashCode(); 37 | } 38 | 39 | IEnumerator IEnumerable.GetEnumerator() 40 | => Collection.GetEnumerator(); 41 | 42 | IEnumerator IEnumerable.GetEnumerator() 43 | => Collection.GetEnumerator(); 44 | 45 | public int Count => Collection.Count; 46 | public T this[int index] => Collection[index]; 47 | 48 | public static bool operator ==(EquatableReadOnlyList left, EquatableReadOnlyList right) 49 | => left.Equals(right); 50 | 51 | public static bool operator !=(EquatableReadOnlyList left, EquatableReadOnlyList right) 52 | => !left.Equals(right); 53 | } 54 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.MissingCancellationToken_modifier=#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | #pragma warning disable CS1591 3 | 4 | namespace Dummy; 5 | 6 | partial class GetUsersQuery 7 | { 8 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 9 | { 10 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 11 | 12 | public Handler( 13 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior 14 | ) 15 | { 16 | var handlerType = typeof(GetUsersQuery); 17 | 18 | _handleBehavior = handleBehavior; 19 | 20 | } 21 | 22 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 23 | global::Dummy.GetUsersQuery.Query request, 24 | global::System.Threading.CancellationToken cancellationToken = default 25 | ) 26 | { 27 | return await _handleBehavior 28 | .HandleAsync(request, cancellationToken) 29 | .ConfigureAwait(false); 30 | } 31 | } 32 | 33 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 34 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 35 | { 36 | private readonly global::Dummy.GetUsersQuery _container; 37 | 38 | public HandleBehavior( 39 | global::Dummy.GetUsersQuery container 40 | ) 41 | { 42 | _container = container; 43 | } 44 | 45 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 46 | global::Dummy.GetUsersQuery.Query request, 47 | global::System.Threading.CancellationToken cancellationToken 48 | ) 49 | { 50 | return await _container 51 | .HandleAsync( 52 | request 53 | ) 54 | .ConfigureAwait(false); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Analyzers/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | ## Release 1.0 2 | 3 | ### New Rules 4 | 5 | Rule ID | Category | Severity | Notes 6 | --------|----------|----------|-------------------- 7 | IHR0001 | ImmediateHandler | Error | HandlerClassAnalyzer 8 | IHR0002 | ImmediateHandler | Error | HandlerClassAnalyzer 9 | IHR0005 | ImmediateHandler | Error | HandlerClassAnalyzer 10 | IHR0006 | ImmediateHandler | Error | BehaviorsAnalyzer 11 | IHR0007 | ImmediateHandler | Error | BehaviorsAnalyzer 12 | IHR0008 | ImmediateHandler | Error | BehaviorsAnalyzer 13 | IHR0009 | ImmediateHandler | Error | HandlerClassAnalyzer 14 | IHR0010 | ImmediateHandler | Error | HandlerClassAnalyzer 15 | IHR0011 | ImmediateHandler | Error | HandlerClassAnalyzer 16 | 17 | 18 | ## Release 1.5 19 | 20 | ### New Rules 21 | 22 | Rule ID | Category | Severity | Notes 23 | --------|----------|----------|-------------------- 24 | IHR0012 | ImmediateHandler | Warning | HandlerClassAnalyzer 25 | 26 | 27 | ## Release 1.6 28 | 29 | ### New Rules 30 | 31 | Rule ID | Category | Severity | Notes 32 | --------|----------|----------|-------------------- 33 | IHR0013 | ImmediateHandler | Warning | InvalidIHandlerAnalyzer 34 | 35 | ## Release 3.0 36 | 37 | ### New Rules 38 | 39 | Rule ID | Category | Severity | Notes 40 | --------|----------|----------|------- 41 | IHR0014 | ImmediateHandler | Error | HandlerClassAnalyzer 42 | IHR0015 | ImmediateHandler | Error | HandlerClassAnalyzer 43 | IHR0016 | ImmediateHandler | Error | HandlerClassAnalyzer 44 | IHR0017 | ImmediateHandler | Error | HandlerClassAnalyzer 45 | IHR0018 | ImmediateHandler | Error | HandlerClassAnalyzer 46 | IHR0019 | ImmediateHandler | Hidden | HandlerClassAnalyzer 47 | 48 | ### Removed Rules 49 | 50 | Rule ID | Category | Severity | Notes 51 | --------|----------|----------|------- 52 | IHR0009 | ImmediateHandler | Error | HandlerClassAnalyzer 53 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.VoidReturnType_modifier=static#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | #pragma warning disable CS1591 3 | 4 | namespace Dummy; 5 | 6 | partial class GetUsersQuery 7 | { 8 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 9 | { 10 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 11 | 12 | public Handler( 13 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior 14 | ) 15 | { 16 | var handlerType = typeof(GetUsersQuery); 17 | 18 | _handleBehavior = handleBehavior; 19 | 20 | } 21 | 22 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 23 | global::Dummy.GetUsersQuery.Query request, 24 | global::System.Threading.CancellationToken cancellationToken = default 25 | ) 26 | { 27 | return await _handleBehavior 28 | .HandleAsync(request, cancellationToken) 29 | .ConfigureAwait(false); 30 | } 31 | } 32 | 33 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 34 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 35 | { 36 | 37 | public HandleBehavior( 38 | ) 39 | { 40 | } 41 | 42 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 43 | global::Dummy.GetUsersQuery.Query request, 44 | global::System.Threading.CancellationToken cancellationToken 45 | ) 46 | { 47 | await global::Dummy.GetUsersQuery 48 | .HandleAsync( 49 | request 50 | , cancellationToken 51 | ) 52 | .ConfigureAwait(false); 53 | 54 | return default; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Generators/Models.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | // ReSharper disable UnusedAutoPropertyAccessor.Local 4 | 5 | namespace Immediate.Handlers.Generators; 6 | 7 | [ExcludeFromCodeCoverage] 8 | public sealed record Behavior 9 | { 10 | public required string RegistrationType { get; init; } 11 | public required string NonGenericTypeName { get; init; } 12 | public required string Name { get; init; } 13 | public required string? RequestType { get; init; } 14 | public required string? ResponseType { get; init; } 15 | } 16 | 17 | [ExcludeFromCodeCoverage] 18 | public sealed record Parameter 19 | { 20 | public required string? Attributes { get; init; } 21 | public required string Type { get; init; } 22 | public required string Name { get; init; } 23 | } 24 | 25 | [ExcludeFromCodeCoverage] 26 | public sealed record GenericType 27 | { 28 | public required string Name { get; init; } 29 | public required EquatableReadOnlyList Implements { get; init; } 30 | } 31 | 32 | [ExcludeFromCodeCoverage] 33 | public sealed record Handler 34 | { 35 | public required string? Namespace { get; init; } 36 | public required string ClassName { get; init; } 37 | public required string DisplayName { get; init; } 38 | 39 | public required string MethodName { get; init; } 40 | public required EquatableReadOnlyList Parameters { get; init; } 41 | public required bool IsStatic { get; init; } 42 | public required bool UseToken { get; init; } 43 | 44 | public required GenericType RequestType { get; init; } 45 | public required GenericType? ResponseType { get; init; } 46 | 47 | public EquatableReadOnlyList? OverrideBehaviors { get; init; } 48 | } 49 | 50 | [ExcludeFromCodeCoverage] 51 | public sealed record ConstraintInfo 52 | { 53 | public required string? RequestType { get; init; } 54 | public required string? ResponseType { get; init; } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.IntReturnType_modifier=#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | #pragma warning disable CS1591 3 | 4 | namespace Dummy; 5 | 6 | partial class GetUsersQuery 7 | { 8 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 9 | { 10 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 11 | 12 | public Handler( 13 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior 14 | ) 15 | { 16 | var handlerType = typeof(GetUsersQuery); 17 | 18 | _handleBehavior = handleBehavior; 19 | 20 | } 21 | 22 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 23 | global::Dummy.GetUsersQuery.Query request, 24 | global::System.Threading.CancellationToken cancellationToken = default 25 | ) 26 | { 27 | return await _handleBehavior 28 | .HandleAsync(request, cancellationToken) 29 | .ConfigureAwait(false); 30 | } 31 | } 32 | 33 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 34 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 35 | { 36 | private readonly global::Dummy.GetUsersQuery _container; 37 | 38 | public HandleBehavior( 39 | global::Dummy.GetUsersQuery container 40 | ) 41 | { 42 | _container = container; 43 | } 44 | 45 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 46 | global::Dummy.GetUsersQuery.Query request, 47 | global::System.Threading.CancellationToken cancellationToken 48 | ) 49 | { 50 | return await _container 51 | .HandleAsync( 52 | request 53 | , cancellationToken 54 | ) 55 | .ConfigureAwait(false); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.VoidReturnType_modifier=#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | #pragma warning disable CS1591 3 | 4 | namespace Dummy; 5 | 6 | partial class GetUsersQuery 7 | { 8 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 9 | { 10 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 11 | 12 | public Handler( 13 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior 14 | ) 15 | { 16 | var handlerType = typeof(GetUsersQuery); 17 | 18 | _handleBehavior = handleBehavior; 19 | 20 | } 21 | 22 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 23 | global::Dummy.GetUsersQuery.Query request, 24 | global::System.Threading.CancellationToken cancellationToken = default 25 | ) 26 | { 27 | return await _handleBehavior 28 | .HandleAsync(request, cancellationToken) 29 | .ConfigureAwait(false); 30 | } 31 | } 32 | 33 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 34 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 35 | { 36 | private readonly global::Dummy.GetUsersQuery _container; 37 | 38 | public HandleBehavior( 39 | global::Dummy.GetUsersQuery container 40 | ) 41 | { 42 | _container = container; 43 | } 44 | 45 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 46 | global::Dummy.GetUsersQuery.Query request, 47 | global::System.Threading.CancellationToken cancellationToken 48 | ) 49 | { 50 | await _container 51 | .HandleAsync( 52 | request 53 | , cancellationToken 54 | ) 55 | .ConfigureAwait(false); 56 | 57 | return default; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandleMethodIsCorrectWithVoidReturn.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task HandleMethodIsCorrectWithVoidReturn_Static_DoesNotAlert() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | 22 | [Handler] 23 | public static partial class {|IHR0019:GetUsersQuery|} 24 | { 25 | public record Query; 26 | 27 | private static async ValueTask HandleAsync( 28 | Query _, 29 | CancellationToken token) 30 | { 31 | } 32 | } 33 | """, 34 | DriverReferenceAssemblies.Normal 35 | ).RunAsync(TestContext.Current.CancellationToken); 36 | 37 | [Fact] 38 | public async Task HandleMethodIsCorrectWithVoidReturn_Instance_DoesNotAlert() => 39 | await AnalyzerTestHelpers.CreateAnalyzerTest( 40 | """ 41 | using System; 42 | using System.Collections.Generic; 43 | using System.IO; 44 | using System.Linq; 45 | using System.Net.Http; 46 | using System.Threading; 47 | using System.Threading.Tasks; 48 | using Immediate.Handlers.Shared; 49 | 50 | [Handler] 51 | public sealed partial class GetUsersQuery 52 | { 53 | public record Query; 54 | 55 | private async ValueTask Handle( 56 | Query _, 57 | CancellationToken token) 58 | { 59 | } 60 | } 61 | """, 62 | DriverReferenceAssemblies.Normal 63 | ).RunAsync(TestContext.Current.CancellationToken); 64 | } 65 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandleMethodDoesNotReturnTask.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task HandleMethodDoesNotReturnTask_Static_AlertDiagnostic() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | 22 | [Handler] 23 | public static partial class {|IHR0019:GetUsersQuery|} 24 | { 25 | public record Query; 26 | 27 | private static int {|IHR0002:HandleAsync|}( 28 | Query _, 29 | CancellationToken token) 30 | { 31 | return 0; 32 | } 33 | } 34 | """, 35 | DriverReferenceAssemblies.Normal 36 | ).RunAsync(TestContext.Current.CancellationToken); 37 | 38 | [Fact] 39 | public async Task HandleMethodDoesNotReturnTask_Instance_AlertDiagnostic() => 40 | await AnalyzerTestHelpers.CreateAnalyzerTest( 41 | """ 42 | using System; 43 | using System.Collections.Generic; 44 | using System.IO; 45 | using System.Linq; 46 | using System.Net.Http; 47 | using System.Threading; 48 | using System.Threading.Tasks; 49 | using Immediate.Handlers.Shared; 50 | 51 | [Handler] 52 | public sealed partial class GetUsersQuery 53 | { 54 | public record Query; 55 | 56 | private int {|IHR0002:Handle|}( 57 | Query _, 58 | CancellationToken token) 59 | { 60 | return 0; 61 | } 62 | } 63 | """, 64 | DriverReferenceAssemblies.Normal 65 | ).RunAsync(TestContext.Current.CancellationToken); 66 | } 67 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandleMethodShouldUseCancellationToken.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task HandleMethodWithoutCancellationToken_Static_AlertDiagnostic() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | 22 | [Handler] 23 | public static partial class {|IHR0019:GetUsersQuery|} 24 | { 25 | public record Query; 26 | 27 | private static ValueTask {|IHR0012:HandleAsync|}( 28 | Query _) 29 | { 30 | return ValueTask.FromResult(0); 31 | } 32 | } 33 | """, 34 | DriverReferenceAssemblies.Normal 35 | ).RunAsync(TestContext.Current.CancellationToken); 36 | 37 | [Fact] 38 | public async Task HandleMethodWithoutCancellationToken_Instance_AlertDiagnostic() => 39 | await AnalyzerTestHelpers.CreateAnalyzerTest( 40 | """ 41 | using System; 42 | using System.Collections.Generic; 43 | using System.IO; 44 | using System.Linq; 45 | using System.Net.Http; 46 | using System.Threading; 47 | using System.Threading.Tasks; 48 | using Immediate.Handlers.Shared; 49 | 50 | [Handler] 51 | public sealed partial class GetUsersQuery 52 | { 53 | public record Query; 54 | 55 | private ValueTask {|IHR0012:HandleAsync|}( 56 | Query _) 57 | { 58 | return ValueTask.FromResult(0); 59 | } 60 | } 61 | """, 62 | DriverReferenceAssemblies.Normal 63 | ).RunAsync(TestContext.Current.CancellationToken); 64 | } 65 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandleMethodIsCorrectWithIntReturn.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task HandleMethodIsCorrectWithIntReturn_Static_DoesNotAlert() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | 22 | [Handler] 23 | public static partial class {|IHR0019:GetUsersQuery|} 24 | { 25 | public record Query; 26 | 27 | private static ValueTask HandleAsync( 28 | Query _, 29 | CancellationToken token) 30 | { 31 | return ValueTask.FromResult(0); 32 | } 33 | } 34 | """, 35 | DriverReferenceAssemblies.Normal 36 | ).RunAsync(TestContext.Current.CancellationToken); 37 | 38 | [Fact] 39 | public async Task HandleMethodIsCorrectWithIntReturn_Instance_DoesNotAlert() => 40 | await AnalyzerTestHelpers.CreateAnalyzerTest( 41 | """ 42 | using System; 43 | using System.Collections.Generic; 44 | using System.IO; 45 | using System.Linq; 46 | using System.Net.Http; 47 | using System.Threading; 48 | using System.Threading.Tasks; 49 | using Immediate.Handlers.Shared; 50 | 51 | [Handler] 52 | public sealed partial class GetUsersQuery 53 | { 54 | public record Query; 55 | 56 | private ValueTask Handle( 57 | Query _, 58 | CancellationToken token) 59 | { 60 | return ValueTask.FromResult(0); 61 | } 62 | } 63 | """, 64 | DriverReferenceAssemblies.Normal 65 | ).RunAsync(TestContext.Current.CancellationToken); 66 | } 67 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandleMethodIsNotPrivate.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task HandleMethodIsNotPrivate_Static_AlertDiagnostic() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | 22 | [Handler] 23 | public static partial class {|IHR0019:GetUsersQuery|} 24 | { 25 | public record Query; 26 | 27 | public static ValueTask {|IHR0011:HandleAsync|}( 28 | Query _, 29 | CancellationToken token) 30 | { 31 | return ValueTask.FromResult(0); 32 | } 33 | } 34 | """, 35 | DriverReferenceAssemblies.Normal 36 | ).RunAsync(TestContext.Current.CancellationToken); 37 | 38 | [Fact] 39 | public async Task HandleMethodIsNotPrivate_Instance_AlertDiagnostic() => 40 | await AnalyzerTestHelpers.CreateAnalyzerTest( 41 | """ 42 | using System; 43 | using System.Collections.Generic; 44 | using System.IO; 45 | using System.Linq; 46 | using System.Net.Http; 47 | using System.Threading; 48 | using System.Threading.Tasks; 49 | using Immediate.Handlers.Shared; 50 | 51 | [Handler] 52 | public sealed partial class GetUsersQuery 53 | { 54 | public record Query; 55 | 56 | public ValueTask {|IHR0011:HandleAsync|}( 57 | Query _, 58 | CancellationToken token) 59 | { 60 | return ValueTask.FromResult(0); 61 | } 62 | } 63 | """, 64 | DriverReferenceAssemblies.Normal 65 | ).RunAsync(TestContext.Current.CancellationToken); 66 | } 67 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/Immediate.Handlers.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | $(NoWarn);NU1903 6 | Exe 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandleMethodTooManyParameters.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task HandleMethodWithTooManyParameters_Instance_AlertDiagnostic() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | 22 | [Handler] 23 | public sealed partial class GetUsersQuery 24 | { 25 | public record Query; 26 | 27 | private ValueTask {|IHR0012:{|IHR0015:HandleAsync|}|}( 28 | Query query1, 29 | Query query2 30 | ) 31 | { 32 | return ValueTask.FromResult(0); 33 | } 34 | } 35 | """, 36 | DriverReferenceAssemblies.Normal 37 | ).RunAsync(TestContext.Current.CancellationToken); 38 | 39 | [Fact] 40 | public async Task HandleMethodWithCancellationTokenAndTooManyParameters_Instance_AlertDiagnostic() => 41 | await AnalyzerTestHelpers.CreateAnalyzerTest( 42 | """ 43 | using System; 44 | using System.Collections.Generic; 45 | using System.IO; 46 | using System.Linq; 47 | using System.Net.Http; 48 | using System.Threading; 49 | using System.Threading.Tasks; 50 | using Immediate.Handlers.Shared; 51 | 52 | [Handler] 53 | public sealed partial class GetUsersQuery 54 | { 55 | public record Query; 56 | 57 | private ValueTask {|IHR0015:HandleAsync|}( 58 | Query query1, 59 | Query query2, 60 | CancellationToken token 61 | ) 62 | { 63 | return ValueTask.FromResult(0); 64 | } 65 | } 66 | """, 67 | DriverReferenceAssemblies.Normal 68 | ).RunAsync(TestContext.Current.CancellationToken); 69 | } 70 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.CrtpBehavior_assemblies=Normal#IH..ConstraintHandler.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH..ConstraintHandler.g.cs 2 | #pragma warning disable CS1591 3 | 4 | partial class ConstraintHandler 5 | { 6 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 7 | { 8 | private readonly global::ConstraintHandler.HandleBehavior _handleBehavior; 9 | private readonly global::ConstraintBehavior _constraintBehavior; 10 | 11 | public Handler( 12 | global::ConstraintHandler.HandleBehavior handleBehavior, 13 | global::ConstraintBehavior constraintBehavior 14 | ) 15 | { 16 | var handlerType = typeof(ConstraintHandler); 17 | 18 | _handleBehavior = handleBehavior; 19 | 20 | _constraintBehavior = constraintBehavior; 21 | _constraintBehavior.HandlerType = handlerType; 22 | 23 | _constraintBehavior.SetInnerHandler(_handleBehavior); 24 | } 25 | 26 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 27 | global::ConstraintHandler.Command request, 28 | global::System.Threading.CancellationToken cancellationToken = default 29 | ) 30 | { 31 | return await _constraintBehavior 32 | .HandleAsync(request, cancellationToken) 33 | .ConfigureAwait(false); 34 | } 35 | } 36 | 37 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 38 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 39 | { 40 | 41 | public HandleBehavior( 42 | ) 43 | { 44 | } 45 | 46 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 47 | global::ConstraintHandler.Command request, 48 | global::System.Threading.CancellationToken cancellationToken 49 | ) 50 | { 51 | await global::ConstraintHandler 52 | .HandleAsync( 53 | request 54 | , cancellationToken 55 | ) 56 | .ConfigureAwait(false); 57 | 58 | return default; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/MultipleBehaviors/MultipleBehaviorsTests.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Shared; 2 | 3 | namespace Immediate.Handlers.FunctionalTests.MultipleBehaviors; 4 | 5 | public sealed class Behavior1 : Behavior 6 | where TRequest : List 7 | { 8 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 9 | { 10 | ArgumentNullException.ThrowIfNull(request); 11 | 12 | request.Add("Behavior1.Enter"); 13 | var response = await Next(request, cancellationToken); 14 | request.Add("Behavior1.Exit"); 15 | 16 | return response; 17 | } 18 | } 19 | 20 | public sealed class Behavior2 : Behavior 21 | where TRequest : List 22 | { 23 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 24 | { 25 | ArgumentNullException.ThrowIfNull(request); 26 | 27 | request.Add("Behavior2.Enter"); 28 | var response = await Next(request, cancellationToken); 29 | request.Add("Behavior2.Exit"); 30 | 31 | return response; 32 | } 33 | } 34 | 35 | [Handler] 36 | [Behaviors( 37 | typeof(Behavior1<,>), 38 | typeof(Behavior2<,>), 39 | typeof(Behavior1<,>) 40 | )] 41 | public static partial class MultipleBehaviorHandler 42 | { 43 | public sealed class Query : List; 44 | 45 | private static async ValueTask HandleAsync(Query query, CancellationToken cancellationToken) 46 | { 47 | cancellationToken.ThrowIfCancellationRequested(); 48 | query.Add("Query.HandleAsync"); 49 | await Task.Yield(); 50 | return 3; 51 | } 52 | } 53 | 54 | public sealed class MultipleBehaviorsTests 55 | { 56 | [Fact] 57 | public async Task TestBehaviorOrdering() 58 | { 59 | var query = new MultipleBehaviorHandler.Query(); 60 | var handler = new MultipleBehaviorHandler.Handler( 61 | new(), new(), new(), new()); 62 | 63 | _ = await handler.HandleAsync(query, TestContext.Current.CancellationToken); 64 | 65 | Assert.Equal( 66 | [ 67 | "Behavior1.Enter", 68 | "Behavior2.Enter", 69 | "Behavior1.Enter", 70 | "Query.HandleAsync", 71 | "Behavior1.Exit", 72 | "Behavior2.Exit", 73 | "Behavior1.Exit", 74 | ], 75 | query 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/BehaviorAnalyzerTests/Tests.BehaviorTypeDoesNotUseUnboundedReference.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.BehaviorAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public sealed partial class Tests 8 | { 9 | [Fact] 10 | public async Task BehaviorTypeDoesNotUseUnboundedReference_Alerts() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | using Normal; 22 | 23 | [assembly: Behaviors( 24 | typeof({|IHR0008:LoggingBehavior|}) 25 | )] 26 | 27 | namespace Normal; 28 | 29 | public class User { }; 30 | public interface ILogger; 31 | 32 | public class LoggingBehavior(ILogger> logger) 33 | : Immediate.Handlers.Shared.Behavior 34 | { 35 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 36 | { 37 | _ = logger.ToString(); 38 | var response = await Next(request, cancellationToken); 39 | 40 | return response; 41 | } 42 | } 43 | 44 | public class UsersService(ILogger logger) 45 | { 46 | public ValueTask> GetUsers() 47 | { 48 | _ = logger.ToString(); 49 | return ValueTask.FromResult(Enumerable.Empty()); 50 | } 51 | } 52 | 53 | [Handler] 54 | [Behaviors( 55 | typeof({|IHR0008:LoggingBehavior|}) 56 | )] 57 | public static partial class GetUsersQuery 58 | { 59 | public record Query; 60 | 61 | private static ValueTask> HandleAsync( 62 | Query _, 63 | UsersService usersService, 64 | CancellationToken token) 65 | { 66 | token.ThrowIfCancellationRequested(); 67 | return usersService.GetUsers(); 68 | } 69 | } 70 | """, 71 | DriverReferenceAssemblies.Normal 72 | ).RunAsync(TestContext.Current.CancellationToken); 73 | } 74 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/BehaviorAnalyzerTests/Tests.BehaviorTypeIsUsedMoreThanOnce.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.BehaviorAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task BehaviorTypeIsUsedMoreThanOnce_Alerts() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | using Normal; 22 | 23 | [assembly: Behaviors( 24 | typeof(LoggingBehavior<,>), 25 | typeof(LoggingBehavior<,>) 26 | )] 27 | 28 | namespace Normal; 29 | 30 | public class User { }; 31 | public interface ILogger; 32 | 33 | public class LoggingBehavior(ILogger> logger) 34 | : Immediate.Handlers.Shared.Behavior 35 | { 36 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 37 | { 38 | _ = logger.ToString(); 39 | var response = await Next(request, cancellationToken); 40 | 41 | return response; 42 | } 43 | } 44 | 45 | public class UsersService(ILogger logger) 46 | { 47 | public ValueTask> GetUsers() 48 | { 49 | _ = logger.ToString(); 50 | return ValueTask.FromResult(Enumerable.Empty()); 51 | } 52 | } 53 | 54 | [Handler] 55 | [Behaviors( 56 | typeof(LoggingBehavior<,>), 57 | typeof(LoggingBehavior<,>) 58 | )] 59 | public static partial class GetUsersQuery 60 | { 61 | public record Query; 62 | 63 | private static ValueTask> HandleAsync( 64 | Query _, 65 | UsersService usersService, 66 | CancellationToken token) 67 | { 68 | token.ThrowIfCancellationRequested(); 69 | return usersService.GetUsers(); 70 | } 71 | } 72 | """, 73 | DriverReferenceAssemblies.Normal 74 | ).RunAsync(TestContext.Current.CancellationToken); 75 | } 76 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/BehaviorAnalyzerTests/Tests.BehaviorTypeDoesNotInheritFromGenericBehavior.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.BehaviorAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task BehaviorTypeDoesNotInheritFromGenericBehavior_Alerts() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | using Normal; 22 | 23 | [assembly: Behaviors( 24 | typeof({|IHR0006:LoggingBehavior<,>|}), 25 | typeof(TestBehavior<,>) 26 | )] 27 | 28 | namespace Normal; 29 | 30 | public class User { } 31 | public interface ILogger; 32 | 33 | public class LoggingBehavior(ILogger> logger) 34 | { 35 | } 36 | 37 | public class TestBehavior : Immediate.Handlers.Shared.Behavior 38 | where TRequest : User 39 | { 40 | public override ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 41 | { 42 | throw new NotImplementedException(); 43 | } 44 | } 45 | 46 | public class UsersService(ILogger logger) 47 | { 48 | public ValueTask> GetUsers() 49 | { 50 | _ = logger.ToString(); 51 | return ValueTask.FromResult(Enumerable.Empty()); 52 | } 53 | } 54 | 55 | [Handler] 56 | [Behaviors( 57 | typeof({|IHR0006:LoggingBehavior<,>|}), 58 | typeof(TestBehavior<,>) 59 | )] 60 | public static partial class GetUsersQuery 61 | { 62 | public record Query; 63 | 64 | private static ValueTask> HandleAsync( 65 | Query _, 66 | UsersService usersService, 67 | CancellationToken token) 68 | { 69 | token.ThrowIfCancellationRequested(); 70 | return usersService.GetUsers(); 71 | } 72 | } 73 | """, 74 | DriverReferenceAssemblies.Normal 75 | ).RunAsync(TestContext.Current.CancellationToken); 76 | } 77 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandleMethodIsNotUnique.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task HandleMethodIsNotUnique_Static_AlertDiagnostic() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | 22 | [Handler] 23 | public static partial class GetUsersQuery 24 | { 25 | public record Query; 26 | 27 | private static ValueTask {|IHR0010:HandleAsync|}( 28 | Query _, 29 | CancellationToken token) 30 | { 31 | return ValueTask.FromResult(0); 32 | } 33 | 34 | private static ValueTask {|IHR0010:Handle|}( 35 | Query _, 36 | CancellationToken token) 37 | { 38 | return ValueTask.FromResult(0); 39 | } 40 | } 41 | """, 42 | DriverReferenceAssemblies.Normal 43 | ).RunAsync(TestContext.Current.CancellationToken); 44 | 45 | [Fact] 46 | public async Task HandleMethodIsNotUnique_Instance_AlertDiagnostic() => 47 | await AnalyzerTestHelpers.CreateAnalyzerTest( 48 | """ 49 | using System; 50 | using System.Collections.Generic; 51 | using System.IO; 52 | using System.Linq; 53 | using System.Net.Http; 54 | using System.Threading; 55 | using System.Threading.Tasks; 56 | using Immediate.Handlers.Shared; 57 | 58 | [Handler] 59 | public sealed partial class GetUsersQuery 60 | { 61 | public record Query; 62 | 63 | private ValueTask {|IHR0010:HandleAsync|}( 64 | Query _, 65 | CancellationToken token) 66 | { 67 | return ValueTask.FromResult(0); 68 | } 69 | 70 | private ValueTask {|IHR0010:Handle|}( 71 | Query _, 72 | CancellationToken token) 73 | { 74 | return ValueTask.FromResult(0); 75 | } 76 | } 77 | """, 78 | DriverReferenceAssemblies.Normal 79 | ).RunAsync(TestContext.Current.CancellationToken); 80 | } 81 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.FunctionalTests/Behavior/Constraints/Tests.Base.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Immediate.Handlers.Shared; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Immediate.Handlers.FunctionalTests.Behavior.Constraints; 6 | 7 | public record A; 8 | 9 | public record B : A; 10 | 11 | public record C : A; 12 | 13 | public record D : B; 14 | 15 | [SuppressMessage("Naming", "CA1707", Justification = "Test names.")] 16 | public sealed partial class Tests 17 | { 18 | private static IServiceCollection ConfigureBehaviors(IServiceCollection services) 19 | { 20 | _ = services.AddSingleton(); 21 | _ = services.AddScoped(typeof(BehaviorA<,>)); 22 | _ = services.AddScoped(typeof(BehaviorB<,>)); 23 | _ = services.AddScoped(typeof(BehaviorC<,>)); 24 | _ = services.AddScoped(typeof(BehaviorD<,>)); 25 | 26 | return services; 27 | } 28 | } 29 | 30 | public sealed class BehaviorWalker 31 | { 32 | public IList BehaviorsRan { get; init; } = []; 33 | } 34 | 35 | public sealed class BehaviorA(BehaviorWalker walker) : Behavior where TRequest : A 36 | { 37 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 38 | { 39 | walker.BehaviorsRan.Add("BehaviorA"); 40 | return await Next(request, cancellationToken); 41 | } 42 | } 43 | 44 | public sealed class BehaviorB(BehaviorWalker walker) : Behavior where TRequest : B 45 | { 46 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 47 | { 48 | walker.BehaviorsRan.Add("BehaviorB"); 49 | return await Next(request, cancellationToken); 50 | } 51 | } 52 | 53 | public sealed class BehaviorC(BehaviorWalker walker) : Behavior where TRequest : C 54 | { 55 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 56 | { 57 | walker.BehaviorsRan.Add("BehaviorC"); 58 | return await Next(request, cancellationToken); 59 | } 60 | } 61 | 62 | public sealed class BehaviorD(BehaviorWalker walker) : Behavior where TRequest : D 63 | { 64 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 65 | { 66 | walker.BehaviorsRan.Add("BehaviorD"); 67 | return await Next(request, cancellationToken); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Analyzers/InvalidIHandlerAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Diagnostics; 2 | using Microsoft.CodeAnalysis; 3 | using System.Collections.Immutable; 4 | 5 | namespace Immediate.Handlers.Analyzers; 6 | 7 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 8 | public sealed class InvalidIHandlerAnalyzer : DiagnosticAnalyzer 9 | { 10 | public static readonly DiagnosticDescriptor IHandlerMissingImplementation = 11 | new( 12 | id: DiagnosticIds.IHR0013IHandlerMissingImplementation, 13 | title: "`IHandler<,>` is missing a concrete implementation", 14 | messageFormat: "`IHandler<{0},{1}>` is missing a concrete implementation", 15 | category: "ImmediateHandler", 16 | defaultSeverity: DiagnosticSeverity.Warning, 17 | isEnabledByDefault: true, 18 | description: "`IHandler<,>` should only be used in reference to concrete handlers for the parameters." 19 | ); 20 | 21 | public override ImmutableArray SupportedDiagnostics { get; } = 22 | ImmutableArray.Create( 23 | [ 24 | IHandlerMissingImplementation, 25 | ]); 26 | 27 | public override void Initialize(AnalysisContext context) 28 | { 29 | if (context == null) 30 | throw new ArgumentNullException(nameof(context)); 31 | 32 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 33 | context.EnableConcurrentExecution(); 34 | 35 | context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); 36 | } 37 | 38 | private void AnalyzeMethod(SymbolAnalysisContext context) 39 | { 40 | if (context.Symbol is not IMethodSymbol methodSymbol) 41 | return; 42 | 43 | foreach (var parameter in methodSymbol.Parameters) 44 | { 45 | var type = parameter.Type as INamedTypeSymbol; 46 | if (!type.IsIHandler()) 47 | continue; 48 | 49 | if (type.TypeArguments[0].TypeKind is TypeKind.TypeParameter 50 | || type.TypeArguments[1].TypeKind is TypeKind.TypeParameter) 51 | { 52 | continue; 53 | } 54 | 55 | var hasConcrete = context.Compilation 56 | .GetSymbolsWithName("Handler", SymbolFilter.Type, context.CancellationToken) 57 | .Any(h => 58 | h is INamedTypeSymbol handler 59 | && SymbolEqualityComparer.Default.Equals(handler.Interfaces.FirstOrDefault(), type) 60 | ); 61 | 62 | if (hasConcrete) 63 | continue; 64 | 65 | context.ReportDiagnostic( 66 | Diagnostic.Create( 67 | IHandlerMissingImplementation, 68 | parameter.Locations[0], 69 | type.TypeArguments[0].ToDisplayString(), 70 | type.TypeArguments[1].ToDisplayString() 71 | ) 72 | ); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/BehaviorAnalyzerTests/Tests.BehaviorTypeDoesNotHaveTwoGenericParameters.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | // using Verifier = 5 | // Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier< 6 | // Immediate.Handlers.Analyzers.BehaviorsAnalyzer>; 7 | 8 | namespace Immediate.Handlers.Tests.AnalyzerTests.BehaviorAnalyzerTests; 9 | 10 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 11 | public partial class Tests 12 | { 13 | [Fact] 14 | public async Task BehaviorTypeDoesNotHaveTwoGenericParameters_Alerts() => 15 | await AnalyzerTestHelpers.CreateAnalyzerTest( 16 | """ 17 | using System; 18 | using System.Collections.Generic; 19 | using System.IO; 20 | using System.Linq; 21 | using System.Net.Http; 22 | using System.Threading; 23 | using System.Threading.Tasks; 24 | using Immediate.Handlers.Shared; 25 | using Normal; 26 | 27 | [assembly: Behaviors( 28 | typeof({|IHR0007:LoggingBehavior<,,>|}) 29 | )] 30 | 31 | namespace Normal; 32 | 33 | public class User { }; 34 | public interface ILogger; 35 | 36 | public class LoggingBehavior(ILogger> logger) 37 | : Immediate.Handlers.Shared.Behavior 38 | { 39 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 40 | { 41 | _ = logger.ToString(); 42 | var response = await Next(request, cancellationToken); 43 | 44 | return response; 45 | } 46 | } 47 | 48 | public class UsersService(ILogger logger) 49 | { 50 | public ValueTask> GetUsers() 51 | { 52 | _ = logger.ToString(); 53 | return ValueTask.FromResult(Enumerable.Empty()); 54 | } 55 | } 56 | 57 | [Handler] 58 | [Behaviors( 59 | typeof({|IHR0007:LoggingBehavior<,,>|}) 60 | )] 61 | public static partial class GetUsersQuery 62 | { 63 | public record Query; 64 | 65 | private static ValueTask> HandleAsync( 66 | Query _, 67 | UsersService usersService, 68 | CancellationToken token) 69 | { 70 | token.ThrowIfCancellationRequested(); 71 | return usersService.GetUsers(); 72 | } 73 | } 74 | """, 75 | DriverReferenceAssemblies.Normal 76 | ).RunAsync(TestContext.Current.CancellationToken); 77 | } 78 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.SingleBehavior_assemblies=Normal#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | #pragma warning disable CS1591 3 | 4 | namespace Dummy; 5 | 6 | partial class GetUsersQuery 7 | { 8 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler> 9 | { 10 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 11 | private readonly global::Dummy.LoggingBehavior> _loggingBehavior; 12 | 13 | public Handler( 14 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior, 15 | global::Dummy.LoggingBehavior> loggingBehavior 16 | ) 17 | { 18 | var handlerType = typeof(GetUsersQuery); 19 | 20 | _handleBehavior = handleBehavior; 21 | 22 | _loggingBehavior = loggingBehavior; 23 | _loggingBehavior.HandlerType = handlerType; 24 | 25 | _loggingBehavior.SetInnerHandler(_handleBehavior); 26 | } 27 | 28 | public async global::System.Threading.Tasks.ValueTask> HandleAsync( 29 | global::Dummy.GetUsersQuery.Query request, 30 | global::System.Threading.CancellationToken cancellationToken = default 31 | ) 32 | { 33 | return await _loggingBehavior 34 | .HandleAsync(request, cancellationToken) 35 | .ConfigureAwait(false); 36 | } 37 | } 38 | 39 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 40 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior> 41 | { 42 | private readonly global::Dummy.GetUsersQuery _container; 43 | 44 | public HandleBehavior( 45 | global::Dummy.GetUsersQuery container 46 | ) 47 | { 48 | _container = container; 49 | } 50 | 51 | public override async global::System.Threading.Tasks.ValueTask> HandleAsync( 52 | global::Dummy.GetUsersQuery.Query request, 53 | global::System.Threading.CancellationToken cancellationToken 54 | ) 55 | { 56 | return await _container 57 | .HandleAsync( 58 | request 59 | , cancellationToken 60 | ) 61 | .ConfigureAwait(false); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.NestedBehavior_assemblies=Normal#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | #pragma warning disable CS1591 3 | 4 | namespace Dummy; 5 | 6 | partial class GetUsersQuery 7 | { 8 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler> 9 | { 10 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 11 | private readonly global::Dummy.LoggingBehavior> _loggingBehavior; 12 | 13 | public Handler( 14 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior, 15 | global::Dummy.LoggingBehavior> loggingBehavior 16 | ) 17 | { 18 | var handlerType = typeof(GetUsersQuery); 19 | 20 | _handleBehavior = handleBehavior; 21 | 22 | _loggingBehavior = loggingBehavior; 23 | _loggingBehavior.HandlerType = handlerType; 24 | 25 | _loggingBehavior.SetInnerHandler(_handleBehavior); 26 | } 27 | 28 | public async global::System.Threading.Tasks.ValueTask> HandleAsync( 29 | global::Dummy.GetUsersQuery.Query request, 30 | global::System.Threading.CancellationToken cancellationToken = default 31 | ) 32 | { 33 | return await _loggingBehavior 34 | .HandleAsync(request, cancellationToken) 35 | .ConfigureAwait(false); 36 | } 37 | } 38 | 39 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 40 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior> 41 | { 42 | private readonly global::Dummy.UsersService _usersService; 43 | 44 | public HandleBehavior( 45 | global::Dummy.UsersService usersService 46 | ) 47 | { 48 | _usersService = usersService; 49 | } 50 | 51 | public override async global::System.Threading.Tasks.ValueTask> HandleAsync( 52 | global::Dummy.GetUsersQuery.Query request, 53 | global::System.Threading.CancellationToken cancellationToken 54 | ) 55 | { 56 | return await global::Dummy.GetUsersQuery 57 | .HandleAsync( 58 | request 59 | , _usersService 60 | , cancellationToken 61 | ) 62 | .ConfigureAwait(false); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/BehaviorAnalyzerTests/Tests.BehaviorTypeIsValid.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.BehaviorAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public sealed partial class Tests 8 | { 9 | [Fact] 10 | public async Task BehaviorTypeIsValid_DoesNotAlert() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | using Normal; 22 | 23 | [assembly: Behaviors( 24 | typeof(LoggingBehavior<,>), 25 | typeof(TestBehavior<,>) 26 | )] 27 | 28 | namespace Normal; 29 | 30 | public class User { } 31 | public interface ILogger; 32 | 33 | public class LoggingBehavior(ILogger> logger) 34 | : Immediate.Handlers.Shared.Behavior 35 | { 36 | public override async ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 37 | { 38 | _ = logger.ToString(); 39 | var response = await Next(request, cancellationToken); 40 | 41 | return response; 42 | } 43 | } 44 | 45 | public class TestBehavior : Immediate.Handlers.Shared.Behavior 46 | where TRequest : User 47 | { 48 | public override ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken) 49 | { 50 | throw new NotImplementedException(); 51 | } 52 | } 53 | 54 | public class UsersService(ILogger logger) 55 | { 56 | public ValueTask> GetUsers() 57 | { 58 | _ = logger.ToString(); 59 | return ValueTask.FromResult(Enumerable.Empty()); 60 | } 61 | } 62 | 63 | [Handler] 64 | [Behaviors( 65 | typeof(LoggingBehavior<,>), 66 | typeof(TestBehavior<,>) 67 | )] 68 | public static partial class GetUsersQuery 69 | { 70 | public record Query; 71 | 72 | private static ValueTask> HandleAsync( 73 | Query _, 74 | UsersService usersService, 75 | CancellationToken token) 76 | { 77 | token.ThrowIfCancellationRequested(); 78 | return usersService.GetUsers(); 79 | } 80 | } 81 | """, 82 | DriverReferenceAssemblies.Normal 83 | ).RunAsync(TestContext.Current.CancellationToken); 84 | } 85 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.ComplexParameterAttribute#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable CS1591 5 | 6 | namespace Dummy; 7 | 8 | partial class GetUsersQuery 9 | { 10 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 11 | { 12 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 13 | 14 | public Handler( 15 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior 16 | ) 17 | { 18 | var handlerType = typeof(GetUsersQuery); 19 | 20 | _handleBehavior = handleBehavior; 21 | 22 | } 23 | 24 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 25 | global::Dummy.GetUsersQuery.Query request, 26 | global::System.Threading.CancellationToken cancellationToken = default 27 | ) 28 | { 29 | return await _handleBehavior 30 | .HandleAsync(request, cancellationToken) 31 | .ConfigureAwait(false); 32 | } 33 | } 34 | 35 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 36 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 37 | { 38 | private readonly global::Dummy.Service _service; 39 | 40 | public HandleBehavior( 41 | [global::Dummy.TestAttribute(["Dummy1", "Dummy2"], ["Dummy1", "Dummy2"], ["Dummy3", "Dummy4"])] global::Dummy.Service service 42 | ) 43 | { 44 | _service = service; 45 | } 46 | 47 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 48 | global::Dummy.GetUsersQuery.Query request, 49 | global::System.Threading.CancellationToken cancellationToken 50 | ) 51 | { 52 | return await global::Dummy.GetUsersQuery 53 | .HandleAsync( 54 | request 55 | , _service 56 | , cancellationToken 57 | ) 58 | .ConfigureAwait(false); 59 | } 60 | } 61 | 62 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 63 | public static IServiceCollection AddHandlers( 64 | IServiceCollection services, 65 | ServiceLifetime lifetime = ServiceLifetime.Scoped 66 | ) 67 | { 68 | services.Add(new(typeof(global::Dummy.GetUsersQuery.Handler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 69 | services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 70 | services.Add(new(typeof(global::Dummy.GetUsersQuery.HandleBehavior), typeof(global::Dummy.GetUsersQuery.HandleBehavior), lifetime)); 71 | return services; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.SimpleParameterAttribute#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable CS1591 5 | 6 | namespace Dummy; 7 | 8 | partial class GetUsersQuery 9 | { 10 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 11 | { 12 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 13 | 14 | public Handler( 15 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior 16 | ) 17 | { 18 | var handlerType = typeof(GetUsersQuery); 19 | 20 | _handleBehavior = handleBehavior; 21 | 22 | } 23 | 24 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 25 | global::Dummy.GetUsersQuery.Query request, 26 | global::System.Threading.CancellationToken cancellationToken = default 27 | ) 28 | { 29 | return await _handleBehavior 30 | .HandleAsync(request, cancellationToken) 31 | .ConfigureAwait(false); 32 | } 33 | } 34 | 35 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 36 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 37 | { 38 | private readonly global::Dummy.SomeKeyedService _service; 39 | 40 | public HandleBehavior( 41 | [global::Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute("SomeServiceKey")] global::Dummy.SomeKeyedService service 42 | ) 43 | { 44 | _service = service; 45 | } 46 | 47 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 48 | global::Dummy.GetUsersQuery.Query request, 49 | global::System.Threading.CancellationToken cancellationToken 50 | ) 51 | { 52 | return await global::Dummy.GetUsersQuery 53 | .HandleAsync( 54 | request 55 | , _service 56 | , cancellationToken 57 | ) 58 | .ConfigureAwait(false); 59 | } 60 | } 61 | 62 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 63 | public static IServiceCollection AddHandlers( 64 | IServiceCollection services, 65 | ServiceLifetime lifetime = ServiceLifetime.Scoped 66 | ) 67 | { 68 | services.Add(new(typeof(global::Dummy.GetUsersQuery.Handler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 69 | services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 70 | services.Add(new(typeof(global::Dummy.GetUsersQuery.HandleBehavior), typeof(global::Dummy.GetUsersQuery.HandleBehavior), lifetime)); 71 | return services; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/InvalidIHandlerAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests; 5 | 6 | public sealed class InvalidIHandlerAnalyzerTests 7 | { 8 | [Fact] 9 | public async Task AnalyzerTriggersForMissingImplementation() => 10 | await AnalyzerTestHelpers.CreateAnalyzerTest( 11 | """ 12 | using System; 13 | using System.Collections.Generic; 14 | using System.IO; 15 | using System.Linq; 16 | using System.Net.Http; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | using Immediate.Handlers.Shared; 20 | 21 | public sealed record Query; 22 | public sealed record Response; 23 | 24 | public static class Test 25 | { 26 | public static void Method(IHandler {|IHR0013:handler|}) 27 | { 28 | } 29 | } 30 | """, 31 | DriverReferenceAssemblies.Normal 32 | ).RunAsync(TestContext.Current.CancellationToken); 33 | 34 | [Fact] 35 | public async Task AnalyzerDoesNotTriggerForPresentImplementation() => 36 | await AnalyzerTestHelpers.CreateAnalyzerTest( 37 | """ 38 | using System; 39 | using System.Collections.Generic; 40 | using System.IO; 41 | using System.Linq; 42 | using System.Net.Http; 43 | using System.Threading; 44 | using System.Threading.Tasks; 45 | using Immediate.Handlers.Shared; 46 | 47 | public sealed record Query; 48 | public sealed record Response; 49 | 50 | public static class Test 51 | { 52 | public static void Method(IHandler handler) 53 | { 54 | } 55 | } 56 | 57 | [Handler] 58 | public static partial class DummyHandler 59 | { 60 | private static ValueTask HandleAsync( 61 | Query _, 62 | CancellationToken token) 63 | { 64 | return ValueTask.FromResult(new Response()); 65 | } 66 | } 67 | """, 68 | DriverReferenceAssemblies.Normal 69 | ).RunAsync(TestContext.Current.CancellationToken); 70 | 71 | [Fact] 72 | public async Task AnalyzerDoesNotTriggerForGenericParameter() => 73 | await AnalyzerTestHelpers.CreateAnalyzerTest( 74 | """ 75 | using System; 76 | using System.Collections.Generic; 77 | using System.IO; 78 | using System.Linq; 79 | using System.Net.Http; 80 | using System.Threading; 81 | using System.Threading.Tasks; 82 | using Immediate.Handlers.Shared; 83 | 84 | public static class Test 85 | { 86 | public static void Method(IHandler handler) 87 | { 88 | } 89 | 90 | public static void Method(IHandler handler) 91 | { 92 | } 93 | } 94 | """, 95 | DriverReferenceAssemblies.Normal 96 | ).RunAsync(TestContext.Current.CancellationToken); 97 | } 98 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/HandlerTests.MultipleParameterAttributes#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable CS1591 5 | 6 | namespace Dummy; 7 | 8 | partial class GetUsersQuery 9 | { 10 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 11 | { 12 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 13 | 14 | public Handler( 15 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior 16 | ) 17 | { 18 | var handlerType = typeof(GetUsersQuery); 19 | 20 | _handleBehavior = handleBehavior; 21 | 22 | } 23 | 24 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 25 | global::Dummy.GetUsersQuery.Query request, 26 | global::System.Threading.CancellationToken cancellationToken = default 27 | ) 28 | { 29 | return await _handleBehavior 30 | .HandleAsync(request, cancellationToken) 31 | .ConfigureAwait(false); 32 | } 33 | } 34 | 35 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 36 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 37 | { 38 | private readonly global::Dummy.SomeKeyedService _service; 39 | 40 | public HandleBehavior( 41 | [global::Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute("SomeServiceKey"), global::Dummy.TestAttribute(Message = "Test"), global::Dummy.Test2Attribute] global::Dummy.SomeKeyedService service 42 | ) 43 | { 44 | _service = service; 45 | } 46 | 47 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 48 | global::Dummy.GetUsersQuery.Query request, 49 | global::System.Threading.CancellationToken cancellationToken 50 | ) 51 | { 52 | return await global::Dummy.GetUsersQuery 53 | .HandleAsync( 54 | request 55 | , _service 56 | , cancellationToken 57 | ) 58 | .ConfigureAwait(false); 59 | } 60 | } 61 | 62 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 63 | public static IServiceCollection AddHandlers( 64 | IServiceCollection services, 65 | ServiceLifetime lifetime = ServiceLifetime.Scoped 66 | ) 67 | { 68 | services.Add(new(typeof(global::Dummy.GetUsersQuery.Handler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 69 | services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 70 | services.Add(new(typeof(global::Dummy.GetUsersQuery.HandleBehavior), typeof(global::Dummy.GetUsersQuery.HandleBehavior), lifetime)); 71 | return services; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Analyzers/Immediate.Handlers.Analyzers.md: -------------------------------------------------------------------------------- 1 | # Immediate.Handlers.Analyzers 2 | 3 | ## IHR0001: Handler method must exist 4 | 5 | Handler classes must define a method with the signature `private static ValueTask HandleAsync(TRequest command, CancellationToken)`. 6 | 7 | | Item | Value | 8 | |----------|------------------| 9 | | Category | ImmediateHandler | 10 | | Enabled | True | 11 | | Severity | Error | 12 | | CodeFix | True | 13 | --- 14 | 15 | ## IHR0002: Handler method must return ValueTask 16 | 17 | Handler methods must return a `ValueTask` 18 | 19 | | Item | Value | 20 | |----------|------------------| 21 | | Category | ImmediateHandler | 22 | | Enabled | True | 23 | | Severity | Error | 24 | | CodeFix | False | 25 | --- 26 | 27 | ## IHR0005: Handler class must not be nested in another type 28 | 29 | Nesting the handler class within another type is unsupported, since it creates difficulties with scoping on the source 30 | generated side. While it would technically be possible in certain circumstances (containing type being partial e.g.) 31 | it is not supported for now. 32 | 33 | | Item | Value | 34 | |----------|------------------| 35 | | Category | ImmediateHandler | 36 | | Enabled | True | 37 | | Severity | Error | 38 | | CodeFix | False | 39 | --- 40 | 41 | ## IHR0006: Behaviors must inherit from `Behavior<,>` 42 | 43 | In order to be properly called as part of a pipeline, a behavior must inherit from the `Behavior<,>` class. 44 | 45 | |Item|Value| 46 | |-|-| 47 | |Category|ImmediateHandler| 48 | |Enabled|True| 49 | |Severity|Error| 50 | |CodeFix|False| 51 | --- 52 | 53 | ## IHR0007: Behaviors must have two generic types 54 | 55 | All behaviors must have two generic parameters, for `TRequest` and `TResponse`. Without these parameters, it is not 56 | possible to bind the behavior to the target request and response types. 57 | 58 | |Item|Value| 59 | |-|-| 60 | |Category|ImmediateHandler| 61 | |Enabled|True| 62 | |Severity|Error| 63 | |CodeFix|False| 64 | --- 65 | 66 | ## IHR0008: Behavior must be referenced with unbound generic 67 | 68 | Behaviors must be referenced using the unbound generic syntax. Referencing a generic type using a specific type will 69 | introduce inconsistencies in connecting multiple behaviors in a pipeline. 70 | 71 | |Item|Value| 72 | |-|-| 73 | |Category|ImmediateHandler| 74 | |Enabled|True| 75 | |Severity|Error| 76 | |CodeFix|False| 77 | --- 78 | 79 | ## IHR0010: Handler method must be unique 80 | 81 | If both `Handle` and `HandleAsync` are provided, it will not be possible to identify which is the correct handler 82 | method. Only can be provided. 83 | 84 | |Item|Value| 85 | |-|-| 86 | |Category|ImmediateHandler| 87 | |Enabled|True| 88 | |Severity|Error| 89 | |CodeFix|False| 90 | --- 91 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/GeneratorTestHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Immediate.Handlers.Generators; 4 | using Immediate.Handlers.Tests.Helpers; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | 8 | namespace Immediate.Handlers.Tests.GeneratorTests; 9 | 10 | public static class GeneratorTestHelper 11 | { 12 | public static GeneratorDriverRunResult RunGenerator( 13 | [StringSyntax("c#-test")] string source, 14 | DriverReferenceAssemblies assemblies 15 | ) 16 | { 17 | var syntaxTree = CSharpSyntaxTree.ParseText(source); 18 | 19 | var compilation = CSharpCompilation.Create( 20 | assemblyName: "Tests", 21 | syntaxTrees: [syntaxTree], 22 | references: 23 | [ 24 | .. Basic.Reference.Assemblies.Net80.References.All, 25 | .. assemblies.GetAdditionalReferences(), 26 | ], 27 | options: new( 28 | outputKind: OutputKind.DynamicallyLinkedLibrary 29 | ) 30 | ); 31 | 32 | var clone = compilation.Clone().AddSyntaxTrees(CSharpSyntaxTree.ParseText("// dummy")); 33 | 34 | GeneratorDriver driver = CSharpGeneratorDriver.Create( 35 | generators: [new ImmediateHandlersGenerator().AsSourceGenerator()], 36 | driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true) 37 | ); 38 | 39 | var result1 = RunGenerator(ref driver, compilation); 40 | var result2 = RunGenerator(ref driver, clone); 41 | 42 | foreach (var (_, step) in result2.Results[0].TrackedOutputSteps) 43 | AssertSteps(step); 44 | 45 | foreach (var step in TrackedSteps) 46 | { 47 | if (result2.Results[0].TrackedSteps.TryGetValue(step, out var outputs)) 48 | AssertSteps(outputs); 49 | } 50 | 51 | return result1; 52 | } 53 | 54 | private static GeneratorDriverRunResult RunGenerator( 55 | ref GeneratorDriver driver, 56 | Compilation compilation 57 | ) 58 | { 59 | driver = driver 60 | .RunGeneratorsAndUpdateCompilation( 61 | compilation, 62 | out var outputCompilation, 63 | out var diagnostics 64 | ); 65 | 66 | Assert.Empty( 67 | outputCompilation 68 | .GetDiagnostics() 69 | .Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning) 70 | ); 71 | 72 | Assert.Empty(diagnostics); 73 | return driver.GetRunResult(); 74 | } 75 | 76 | private static ReadOnlySpan TrackedSteps => 77 | new string[] 78 | { 79 | "MsDi", 80 | "AssemblyName", 81 | "RootNamespace", 82 | "Behaviors", 83 | "Handlers", 84 | "HandlersWithBehaviors", 85 | "Registrations", 86 | }; 87 | 88 | private static void AssertSteps( 89 | ImmutableArray steps 90 | ) 91 | { 92 | var outputs = steps.SelectMany(o => o.Outputs); 93 | 94 | Assert.All(outputs, o => Assert.True(o.Reason is IncrementalStepRunReason.Unchanged or IncrementalStepRunReason.Cached)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/CodeFixTests/HandlerMethodMustExistCodeFixProviderTests.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.CodeFixes; 3 | using Immediate.Handlers.Tests.Helpers; 4 | 5 | namespace Immediate.Handlers.Tests.CodeFixTests; 6 | 7 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 8 | public sealed partial class HandlerMethodMustExistCodeFixProviderTests 9 | { 10 | [Fact] 11 | public async Task HandleMethodDoesNotExistOnStaticClass() => 12 | await CodeFixTestHelper.CreateCodeFixTest( 13 | """ 14 | using System; 15 | using System.Collections.Generic; 16 | using System.IO; 17 | using System.Linq; 18 | using System.Net.Http; 19 | using System.Threading; 20 | using System.Threading.Tasks; 21 | using Immediate.Handlers.Shared; 22 | 23 | [Handler] 24 | public static class {|IHR0001:GetUsersQuery|} 25 | { 26 | public record Query; 27 | } 28 | """, 29 | """ 30 | using System; 31 | using System.Collections.Generic; 32 | using System.IO; 33 | using System.Linq; 34 | using System.Net.Http; 35 | using System.Threading; 36 | using System.Threading.Tasks; 37 | using Immediate.Handlers.Shared; 38 | 39 | [Handler] 40 | public static class {|IHR0019:GetUsersQuery|} 41 | { 42 | public record Query; 43 | 44 | private static ValueTask HandleAsync(Query query, CancellationToken token) 45 | { 46 | return default; 47 | } 48 | } 49 | """, 50 | DriverReferenceAssemblies.Normal 51 | ).RunAsync(TestContext.Current.CancellationToken); 52 | 53 | [Fact] 54 | public async Task HandleMethodDoesNotExistOnSealedClass() => 55 | await CodeFixTestHelper.CreateCodeFixTest( 56 | """ 57 | using System; 58 | using System.Collections.Generic; 59 | using System.IO; 60 | using System.Linq; 61 | using System.Net.Http; 62 | using System.Threading; 63 | using System.Threading.Tasks; 64 | using Immediate.Handlers.Shared; 65 | 66 | [Handler] 67 | public sealed class {|IHR0001:GetUsersQuery|} 68 | { 69 | public record Response; 70 | } 71 | """, 72 | """ 73 | using System; 74 | using System.Collections.Generic; 75 | using System.IO; 76 | using System.Linq; 77 | using System.Net.Http; 78 | using System.Threading; 79 | using System.Threading.Tasks; 80 | using Immediate.Handlers.Shared; 81 | 82 | [Handler] 83 | public sealed class GetUsersQuery 84 | { 85 | public record Response; 86 | 87 | private ValueTask HandleAsync(object _, CancellationToken token) 88 | { 89 | return default; 90 | } 91 | } 92 | """, 93 | DriverReferenceAssemblies.Normal 94 | ).RunAsync(TestContext.Current.CancellationToken); 95 | } 96 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Shared/Behavior.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Immediate.Handlers.Shared; 5 | 6 | /// 7 | /// Represents a cross-cutting pipeline behavior 8 | /// 9 | /// 10 | /// The type of a command-request 11 | /// 12 | /// 13 | /// The type of a command-response 14 | /// 15 | public abstract class Behavior 16 | { 17 | /// 18 | /// The of the Handler class for the current request. 19 | /// 20 | public Type HandlerType { get; [EditorBrowsable(EditorBrowsableState.Never)] set; } = default!; 21 | 22 | private Behavior? _innerHandler; 23 | 24 | [DoesNotReturn] 25 | private static void ThrowException(string message) => 26 | throw new InvalidOperationException(message); 27 | 28 | /// 29 | /// The next entry in the pipeline for the current request. 30 | /// 31 | /// 32 | /// This property is called by the infrastructure, and should not be called manually. 33 | /// 34 | [EditorBrowsable(EditorBrowsableState.Never)] 35 | public void SetInnerHandler(Behavior handler) 36 | { 37 | if (_innerHandler != null) 38 | ThrowException("Cannot set `_innerHandler` more than once."); 39 | _innerHandler = handler; 40 | } 41 | 42 | /// 43 | /// Calls the next entry in the pipeline for the current request. 44 | /// 45 | /// 46 | /// The currently processing request. 47 | /// 48 | /// 49 | /// The optional cancellation token to be used for cancelling the operation at any time. 50 | /// 51 | /// 52 | /// A representing a promise to return a from the 53 | /// next entry in the pipeline. 54 | /// 55 | protected ValueTask Next(TRequest request, CancellationToken cancellationToken) 56 | { 57 | if (_innerHandler == null) 58 | ThrowException("`_innerHandler` must be set before calling `Next()`"); 59 | return _innerHandler.HandleAsync(request, cancellationToken); 60 | } 61 | 62 | /// 63 | /// Pipeline handler. Perform any additional behavior and 64 | /// Next(request, cancellationToken). 65 | /// 66 | /// 67 | /// Incoming request 68 | /// 69 | /// 70 | /// The optional cancellation token to be used for cancelling the operation at any time. 71 | /// 72 | /// 73 | /// A representing a promise to return a . 74 | /// 75 | public abstract ValueTask HandleAsync(TRequest request, CancellationToken cancellationToken); 76 | } 77 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.CrtpBehavior_assemblies=Msdi#IH..ConstraintHandler.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH..ConstraintHandler.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable CS1591 5 | 6 | partial class ConstraintHandler 7 | { 8 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler 9 | { 10 | private readonly global::ConstraintHandler.HandleBehavior _handleBehavior; 11 | private readonly global::ConstraintBehavior _constraintBehavior; 12 | 13 | public Handler( 14 | global::ConstraintHandler.HandleBehavior handleBehavior, 15 | global::ConstraintBehavior constraintBehavior 16 | ) 17 | { 18 | var handlerType = typeof(ConstraintHandler); 19 | 20 | _handleBehavior = handleBehavior; 21 | 22 | _constraintBehavior = constraintBehavior; 23 | _constraintBehavior.HandlerType = handlerType; 24 | 25 | _constraintBehavior.SetInnerHandler(_handleBehavior); 26 | } 27 | 28 | public async global::System.Threading.Tasks.ValueTask HandleAsync( 29 | global::ConstraintHandler.Command request, 30 | global::System.Threading.CancellationToken cancellationToken = default 31 | ) 32 | { 33 | return await _constraintBehavior 34 | .HandleAsync(request, cancellationToken) 35 | .ConfigureAwait(false); 36 | } 37 | } 38 | 39 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 40 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior 41 | { 42 | 43 | public HandleBehavior( 44 | ) 45 | { 46 | } 47 | 48 | public override async global::System.Threading.Tasks.ValueTask HandleAsync( 49 | global::ConstraintHandler.Command request, 50 | global::System.Threading.CancellationToken cancellationToken 51 | ) 52 | { 53 | await global::ConstraintHandler 54 | .HandleAsync( 55 | request 56 | , cancellationToken 57 | ) 58 | .ConfigureAwait(false); 59 | 60 | return default; 61 | } 62 | } 63 | 64 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 65 | public static IServiceCollection AddHandlers( 66 | IServiceCollection services, 67 | ServiceLifetime lifetime = ServiceLifetime.Scoped 68 | ) 69 | { 70 | services.Add(new(typeof(global::ConstraintHandler.Handler), typeof(global::ConstraintHandler.Handler), lifetime)); 71 | services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler), typeof(global::ConstraintHandler.Handler), lifetime)); 72 | services.Add(new(typeof(global::ConstraintHandler.HandleBehavior), typeof(global::ConstraintHandler.HandleBehavior), lifetime)); 73 | return services; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Common/ITypeSymbolExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Microsoft.CodeAnalysis; 3 | 4 | namespace Immediate.Handlers; 5 | 6 | internal static class ITypeSymbolExtensions 7 | { 8 | public static bool IsHandlerAttribute([NotNullWhen(true)] this ITypeSymbol? typeSymbol) => 9 | typeSymbol is INamedTypeSymbol 10 | { 11 | Arity: 0, 12 | Name: "HandlerAttribute", 13 | ContainingNamespace.IsImmediateHandlersShared: true, 14 | }; 15 | 16 | public static bool IsBehavior2([NotNullWhen(true)] this ITypeSymbol typeSymbol) => 17 | typeSymbol is INamedTypeSymbol 18 | { 19 | Arity: 2, 20 | Name: "Behavior", 21 | ContainingNamespace.IsImmediateHandlersShared: true, 22 | }; 23 | 24 | public static bool IsBehaviorsAttribute([NotNullWhen(true)] this ITypeSymbol? typeSymbol) => 25 | typeSymbol is INamedTypeSymbol 26 | { 27 | Arity: 0, 28 | Name: "BehaviorsAttribute", 29 | ContainingNamespace.IsImmediateHandlersShared: true, 30 | }; 31 | 32 | public static bool IsIHandler([NotNullWhen(true)] this ITypeSymbol? typeSymbol) => 33 | typeSymbol is INamedTypeSymbol 34 | { 35 | Arity: 2, 36 | Name: "IHandler", 37 | ContainingNamespace.IsImmediateHandlersShared: true, 38 | }; 39 | 40 | public static bool ImplementsBehavior([NotNullWhen(true)] this INamedTypeSymbol typeSymbol) => 41 | typeSymbol.IsBehavior2() 42 | || (typeSymbol.BaseType is not null && ImplementsBehavior(typeSymbol.BaseType.OriginalDefinition)); 43 | 44 | public static bool IsValueTask1([NotNullWhen(true)] this ITypeSymbol? typeSymbol) => 45 | typeSymbol is INamedTypeSymbol 46 | { 47 | Arity: 1, 48 | Name: "ValueTask", 49 | ContainingNamespace.IsSystemThreadingTasks: true, 50 | }; 51 | 52 | public static bool IsIEquatable1([NotNullWhen(true)] this ITypeSymbol? typeSymbol) => 53 | typeSymbol is INamedTypeSymbol 54 | { 55 | Arity: 1, 56 | Name: "IEquatable", 57 | ContainingNamespace: 58 | { 59 | Name: "System", 60 | ContainingNamespace.IsGlobalNamespace: true 61 | }, 62 | }; 63 | 64 | extension(ITypeSymbol? typeSymbol) 65 | { 66 | public bool IsCancellationToken => 67 | typeSymbol is INamedTypeSymbol 68 | { 69 | Name: "CancellationToken", 70 | ContainingNamespace: 71 | { 72 | Name: "Threading", 73 | ContainingNamespace: 74 | { 75 | Name: "System", 76 | ContainingNamespace.IsGlobalNamespace: true 77 | } 78 | } 79 | }; 80 | } 81 | 82 | extension(INamespaceSymbol namespaceSymbol) 83 | { 84 | public bool IsSystemThreadingTasks => 85 | namespaceSymbol is 86 | { 87 | Name: "Tasks", 88 | ContainingNamespace: 89 | { 90 | Name: "Threading", 91 | ContainingNamespace: 92 | { 93 | Name: "System", 94 | ContainingNamespace.IsGlobalNamespace: true 95 | } 96 | }, 97 | }; 98 | 99 | public bool IsImmediateHandlersShared => 100 | namespaceSymbol is 101 | { 102 | Name: "Shared", 103 | ContainingNamespace: 104 | { 105 | Name: "Handlers", 106 | ContainingNamespace: 107 | { 108 | Name: "Immediate", 109 | ContainingNamespace.IsGlobalNamespace: true, 110 | }, 111 | }, 112 | }; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.NestedBehavior_assemblies=Msdi#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable CS1591 5 | 6 | namespace Dummy; 7 | 8 | partial class GetUsersQuery 9 | { 10 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler> 11 | { 12 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 13 | private readonly global::Dummy.LoggingBehavior> _loggingBehavior; 14 | 15 | public Handler( 16 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior, 17 | global::Dummy.LoggingBehavior> loggingBehavior 18 | ) 19 | { 20 | var handlerType = typeof(GetUsersQuery); 21 | 22 | _handleBehavior = handleBehavior; 23 | 24 | _loggingBehavior = loggingBehavior; 25 | _loggingBehavior.HandlerType = handlerType; 26 | 27 | _loggingBehavior.SetInnerHandler(_handleBehavior); 28 | } 29 | 30 | public async global::System.Threading.Tasks.ValueTask> HandleAsync( 31 | global::Dummy.GetUsersQuery.Query request, 32 | global::System.Threading.CancellationToken cancellationToken = default 33 | ) 34 | { 35 | return await _loggingBehavior 36 | .HandleAsync(request, cancellationToken) 37 | .ConfigureAwait(false); 38 | } 39 | } 40 | 41 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 42 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior> 43 | { 44 | private readonly global::Dummy.UsersService _usersService; 45 | 46 | public HandleBehavior( 47 | global::Dummy.UsersService usersService 48 | ) 49 | { 50 | _usersService = usersService; 51 | } 52 | 53 | public override async global::System.Threading.Tasks.ValueTask> HandleAsync( 54 | global::Dummy.GetUsersQuery.Query request, 55 | global::System.Threading.CancellationToken cancellationToken 56 | ) 57 | { 58 | return await global::Dummy.GetUsersQuery 59 | .HandleAsync( 60 | request 61 | , _usersService 62 | , cancellationToken 63 | ) 64 | .ConfigureAwait(false); 65 | } 66 | } 67 | 68 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 69 | public static IServiceCollection AddHandlers( 70 | IServiceCollection services, 71 | ServiceLifetime lifetime = ServiceLifetime.Scoped 72 | ) 73 | { 74 | services.Add(new(typeof(global::Dummy.GetUsersQuery.Handler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 75 | services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler>), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 76 | services.Add(new(typeof(global::Dummy.GetUsersQuery.HandleBehavior), typeof(global::Dummy.GetUsersQuery.HandleBehavior), lifetime)); 77 | return services; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Immediate.Handlers/Immediate.Handlers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0;net10.0 5 | true 6 | false 7 | 8 | 9 | 10 | Immediate.Handlers 11 | An implementation of the mediator pattern in .NET using source-generation. 12 | 13 | Immediate.Handlers Developers 14 | © 2024-2025 Immediate.Handlers Developers 15 | 16 | MIT 17 | readme.md 18 | csharp-sourcegenerator;mediator;mediator-pattern 19 | 20 | true 21 | https://github.com/viceroypenguin/Immediate.Handlers 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | minor 65 | preview.0 66 | v 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.SingleBehavior_assemblies=Msdi#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable CS1591 5 | 6 | namespace Dummy; 7 | 8 | partial class GetUsersQuery 9 | { 10 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler> 11 | { 12 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 13 | private readonly global::Dummy.LoggingBehavior> _loggingBehavior; 14 | 15 | public Handler( 16 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior, 17 | global::Dummy.LoggingBehavior> loggingBehavior 18 | ) 19 | { 20 | var handlerType = typeof(GetUsersQuery); 21 | 22 | _handleBehavior = handleBehavior; 23 | 24 | _loggingBehavior = loggingBehavior; 25 | _loggingBehavior.HandlerType = handlerType; 26 | 27 | _loggingBehavior.SetInnerHandler(_handleBehavior); 28 | } 29 | 30 | public async global::System.Threading.Tasks.ValueTask> HandleAsync( 31 | global::Dummy.GetUsersQuery.Query request, 32 | global::System.Threading.CancellationToken cancellationToken = default 33 | ) 34 | { 35 | return await _loggingBehavior 36 | .HandleAsync(request, cancellationToken) 37 | .ConfigureAwait(false); 38 | } 39 | } 40 | 41 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 42 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior> 43 | { 44 | private readonly global::Dummy.GetUsersQuery _container; 45 | 46 | public HandleBehavior( 47 | global::Dummy.GetUsersQuery container 48 | ) 49 | { 50 | _container = container; 51 | } 52 | 53 | public override async global::System.Threading.Tasks.ValueTask> HandleAsync( 54 | global::Dummy.GetUsersQuery.Query request, 55 | global::System.Threading.CancellationToken cancellationToken 56 | ) 57 | { 58 | return await _container 59 | .HandleAsync( 60 | request 61 | , cancellationToken 62 | ) 63 | .ConfigureAwait(false); 64 | } 65 | } 66 | 67 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 68 | public static IServiceCollection AddHandlers( 69 | IServiceCollection services, 70 | ServiceLifetime lifetime = ServiceLifetime.Scoped 71 | ) 72 | { 73 | services.Add(new(typeof(global::Dummy.GetUsersQuery.Handler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 74 | services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler>), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 75 | services.Add(new(typeof(global::Dummy.GetUsersQuery.HandleBehavior), typeof(global::Dummy.GetUsersQuery.HandleBehavior), lifetime)); 76 | services.Add(new(typeof(global::Dummy.GetUsersQuery), typeof(global::Dummy.GetUsersQuery), lifetime)); 77 | return services; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/AnalyzerTests/HandlerClassAnalyzerTests/Tests.HandleMethodMissingRequest.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.Tests.Helpers; 3 | 4 | namespace Immediate.Handlers.Tests.AnalyzerTests.HandlerClassAnalyzerTests; 5 | 6 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Not being consumed by other code")] 7 | public partial class Tests 8 | { 9 | [Fact] 10 | public async Task HandleMethodWithZeroParameters_Static_AlertDiagnostic() => 11 | await AnalyzerTestHelpers.CreateAnalyzerTest( 12 | """ 13 | using System; 14 | using System.Collections.Generic; 15 | using System.IO; 16 | using System.Linq; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Immediate.Handlers.Shared; 21 | 22 | [Handler] 23 | public static partial class {|IHR0019:GetUsersQuery|} 24 | { 25 | public record Query; 26 | 27 | private static ValueTask {|IHR0014:HandleAsync|}() 28 | { 29 | return ValueTask.FromResult(0); 30 | } 31 | } 32 | """, 33 | DriverReferenceAssemblies.Normal 34 | ).RunAsync(TestContext.Current.CancellationToken); 35 | 36 | [Fact] 37 | public async Task HandleMethodWithOnlyCancellationToken_Static_AlertDiagnostic() => 38 | await AnalyzerTestHelpers.CreateAnalyzerTest( 39 | """ 40 | using System; 41 | using System.Collections.Generic; 42 | using System.IO; 43 | using System.Linq; 44 | using System.Net.Http; 45 | using System.Threading; 46 | using System.Threading.Tasks; 47 | using Immediate.Handlers.Shared; 48 | 49 | [Handler] 50 | public static partial class {|IHR0019:GetUsersQuery|} 51 | { 52 | public record Query; 53 | 54 | private static ValueTask {|IHR0014:HandleAsync|}( 55 | CancellationToken token 56 | ) 57 | { 58 | return ValueTask.FromResult(0); 59 | } 60 | } 61 | """, 62 | DriverReferenceAssemblies.Normal 63 | ).RunAsync(TestContext.Current.CancellationToken); 64 | 65 | [Fact] 66 | public async Task HandleMethodWithZeroParameters_Instance_AlertDiagnostic() => 67 | await AnalyzerTestHelpers.CreateAnalyzerTest( 68 | """ 69 | using System; 70 | using System.Collections.Generic; 71 | using System.IO; 72 | using System.Linq; 73 | using System.Net.Http; 74 | using System.Threading; 75 | using System.Threading.Tasks; 76 | using Immediate.Handlers.Shared; 77 | 78 | [Handler] 79 | public sealed partial class GetUsersQuery 80 | { 81 | public record Query; 82 | 83 | private ValueTask {|IHR0014:HandleAsync|}() 84 | { 85 | return ValueTask.FromResult(0); 86 | } 87 | } 88 | """, 89 | DriverReferenceAssemblies.Normal 90 | ).RunAsync(TestContext.Current.CancellationToken); 91 | 92 | [Fact] 93 | public async Task HandleMethodWithOnlyCancellationToken_Instance_AlertDiagnostic() => 94 | await AnalyzerTestHelpers.CreateAnalyzerTest( 95 | """ 96 | using System; 97 | using System.Collections.Generic; 98 | using System.IO; 99 | using System.Linq; 100 | using System.Net.Http; 101 | using System.Threading; 102 | using System.Threading.Tasks; 103 | using Immediate.Handlers.Shared; 104 | 105 | [Handler] 106 | public sealed partial class GetUsersQuery 107 | { 108 | public record Query; 109 | 110 | private ValueTask {|IHR0014:HandleAsync|}( 111 | CancellationToken token 112 | ) 113 | { 114 | return ValueTask.FromResult(0); 115 | } 116 | } 117 | """, 118 | DriverReferenceAssemblies.Normal 119 | ).RunAsync(TestContext.Current.CancellationToken); 120 | } 121 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Generators/TransformBehaviors.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace Immediate.Handlers.Generators; 4 | 5 | internal static class TransformBehaviors 6 | { 7 | public static EquatableReadOnlyList ParseBehaviors( 8 | GeneratorAttributeSyntaxContext context, 9 | CancellationToken cancellationToken 10 | ) => context.Attributes[0].ParseBehaviors(cancellationToken); 11 | 12 | public static EquatableReadOnlyList ParseBehaviors( 13 | this AttributeData attribute, 14 | CancellationToken cancellationToken 15 | ) 16 | { 17 | cancellationToken.ThrowIfCancellationRequested(); 18 | 19 | if (attribute.ConstructorArguments.Length != 1) 20 | return []; 21 | 22 | var ca = attribute.ConstructorArguments[0]; 23 | if (ca.Type is not IArrayTypeSymbol 24 | { 25 | ElementType: 26 | { 27 | Name: "Type", 28 | ContainingNamespace: 29 | { 30 | Name: "System", 31 | ContainingNamespace.IsGlobalNamespace: true, 32 | }, 33 | }, 34 | }) 35 | { 36 | return []; 37 | } 38 | 39 | cancellationToken.ThrowIfCancellationRequested(); 40 | return ca.Values 41 | .Select(v => 42 | { 43 | cancellationToken.ThrowIfCancellationRequested(); 44 | if (v.Value is not INamedTypeSymbol symbol) 45 | return null; 46 | 47 | if (!symbol.IsUnboundGenericType) 48 | return null; 49 | 50 | var originalDefinition = symbol.OriginalDefinition; 51 | if (originalDefinition.TypeParameters.Length != 2) 52 | return null; 53 | 54 | if (originalDefinition.IsAbstract) 55 | return null; 56 | 57 | if (!originalDefinition.ImplementsBehavior()) 58 | return null; 59 | 60 | cancellationToken.ThrowIfCancellationRequested(); 61 | 62 | // global::Dummy.LoggingBehavior<,> 63 | // for: `services.AddScoped(typeof(..));` 64 | var typeName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); 65 | 66 | cancellationToken.ThrowIfCancellationRequested(); 67 | 68 | // global::Dummy.LoggingBehavior 69 | // for: private readonly global::Dummy.LoggingBehavior 70 | var constructorType = symbol.OriginalDefinition.ToDisplayString(DisplayNameFormatters.NonGenericFqdnFormat); 71 | 72 | var constraint = GetConstraintInfo(symbol, cancellationToken); 73 | if (constraint == null) 74 | return null; 75 | 76 | cancellationToken.ThrowIfCancellationRequested(); 77 | return new Behavior 78 | { 79 | RegistrationType = typeName, 80 | NonGenericTypeName = constructorType, 81 | RequestType = constraint.RequestType, 82 | ResponseType = constraint.ResponseType, 83 | Name = originalDefinition.Name, 84 | }; 85 | }) 86 | .ToEquatableReadOnlyList(); 87 | } 88 | 89 | private static ConstraintInfo? GetConstraintInfo(INamedTypeSymbol symbol, CancellationToken cancellationToken) 90 | { 91 | cancellationToken.ThrowIfCancellationRequested(); 92 | 93 | var originalDefinition = symbol.OriginalDefinition; 94 | 95 | if (GetConstraintType(originalDefinition.TypeParameters[0]) is not (true, var requestType)) 96 | return null; 97 | 98 | cancellationToken.ThrowIfCancellationRequested(); 99 | 100 | if (GetConstraintType(originalDefinition.TypeParameters[1]) is not (true, var responseType)) 101 | return null; 102 | 103 | cancellationToken.ThrowIfCancellationRequested(); 104 | 105 | return new() 106 | { 107 | RequestType = requestType, 108 | ResponseType = responseType, 109 | }; 110 | } 111 | 112 | private static (bool Valid, string? Constraint) GetConstraintType(ITypeParameterSymbol parameter) 113 | { 114 | if (parameter.ConstraintTypes is []) 115 | return (true, null); 116 | 117 | if (parameter.ConstraintTypes is not [var constraint]) 118 | return default; 119 | 120 | var displayString = constraint.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); 121 | 122 | if (constraint is INamedTypeSymbol 123 | { 124 | IsGenericType: true, 125 | TypeArguments: 126 | [ 127 | ITypeParameterSymbol s, 128 | ] 129 | } 130 | && s.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase) 131 | ) 132 | { 133 | displayString = displayString.Replace(parameter.Name, "_TRequest_"); 134 | } 135 | 136 | return (true, displayString); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.CodeFixes/HandlerMethodMustExistCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Immediate.Handlers.Analyzers; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeActions; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; 7 | using Microsoft.CodeAnalysis.Formatting; 8 | using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; 9 | using SyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind; 10 | 11 | namespace Immediate.Handlers.CodeFixes; 12 | 13 | [ExportCodeFixProvider(LanguageNames.CSharp)] 14 | public sealed class HandlerMethodMustExistCodeFixProvider : CodeFixProvider 15 | { 16 | public override ImmutableArray FixableDiagnosticIds { get; } = 17 | ImmutableArray.Create([DiagnosticIds.IHR0001HandlerMethodMustExist]); 18 | 19 | public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; 20 | 21 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 22 | { 23 | // We link only one diagnostic and assume there is only one diagnostic in the context. 24 | var diagnostic = context.Diagnostics.Single(); 25 | 26 | // 'SourceSpan' of 'Location' is the highlighted area. We're going to use this area to find the 'SyntaxNode' to rename. 27 | var diagnosticSpan = diagnostic.Location.SourceSpan; 28 | 29 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 30 | 31 | if (root?.FindNode(diagnosticSpan) is ClassDeclarationSyntax classDeclarationSyntax) 32 | { 33 | context.RegisterCodeFix( 34 | CodeAction.Create( 35 | title: "Add HandleAsync method", 36 | createChangedDocument: _ => AddHandleAsyncMethodAsync(context.Document, root, classDeclarationSyntax), 37 | equivalenceKey: nameof(HandlerMethodMustExistCodeFixProvider) 38 | ), 39 | diagnostic); 40 | } 41 | } 42 | 43 | private static Task AddHandleAsyncMethodAsync(Document document, 44 | SyntaxNode root, 45 | ClassDeclarationSyntax classDeclarationSyntax) 46 | { 47 | var requestType = classDeclarationSyntax.Members 48 | .OfType() 49 | .FirstOrDefault(x => 50 | x.Identifier.Text.EndsWith("Query", StringComparison.Ordinal) 51 | || x.Identifier.Text.EndsWith("Command", StringComparison.Ordinal) 52 | ); 53 | 54 | var responseType = classDeclarationSyntax.Members 55 | .OfType() 56 | .FirstOrDefault(x => x.Identifier.Text.EndsWith("Response", StringComparison.Ordinal)); 57 | 58 | var methodDeclaration = 59 | MethodDeclaration( 60 | GenericName( 61 | Identifier("ValueTask") 62 | ) 63 | .WithTypeArgumentList( 64 | TypeArgumentList( 65 | SingletonSeparatedList( 66 | responseType is { } 67 | ? IdentifierName(responseType.Identifier.Text) 68 | : PredefinedType(Token(SyntaxKind.IntKeyword)) 69 | ) 70 | ) 71 | ), 72 | Identifier("HandleAsync") 73 | ) 74 | .WithModifiers( 75 | TokenList( 76 | classDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)) 77 | ? [Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.StaticKeyword)] 78 | : [Token(SyntaxKind.PrivateKeyword)] 79 | ) 80 | ) 81 | .WithParameterList( 82 | ParameterList( 83 | SeparatedList( 84 | new SyntaxNodeOrToken[] 85 | { 86 | Parameter(Identifier(requestType?.Identifier.Text.ToCamelCase() ?? "_")) 87 | .WithType( 88 | requestType is { } 89 | ? IdentifierName(requestType.Identifier.Text) 90 | : PredefinedType(Token(SyntaxKind.ObjectKeyword)) 91 | ), 92 | 93 | Token(SyntaxKind.CommaToken), 94 | 95 | Parameter(Identifier("token")) 96 | .WithType( 97 | IdentifierName("CancellationToken") 98 | ), 99 | } 100 | ) 101 | ) 102 | ) 103 | .WithBody( 104 | Block( 105 | SingletonList( 106 | ReturnStatement( 107 | LiteralExpression(SyntaxKind.DefaultLiteralExpression) 108 | ) 109 | ) 110 | ) 111 | ) 112 | .WithAdditionalAnnotations(Formatter.Annotation); 113 | 114 | var newClassDecl = classDeclarationSyntax.AddMembers(methodDeclaration); 115 | 116 | return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(classDeclarationSyntax, newClassDecl))); 117 | } 118 | } 119 | 120 | file static class Extensions 121 | { 122 | public static string ToCamelCase(this string s) => 123 | char.ToLowerInvariant(s[0]) + s[1..]; 124 | } 125 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.MultipleBehaviors_assemblies=Normal#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | #pragma warning disable CS1591 3 | 4 | namespace Dummy; 5 | 6 | partial class GetUsersQuery 7 | { 8 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler> 9 | { 10 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 11 | private readonly global::YetAnotherDummy.SecondLoggingBehavior> _secondLoggingBehavior1; 12 | private readonly global::YetAnotherDummy.LoggingBehavior> _loggingBehavior1; 13 | private readonly global::Dummy.SecondLoggingBehavior> _secondLoggingBehavior; 14 | private readonly global::YetAnotherDummy.OtherBehavior> _otherBehavior; 15 | private readonly global::Dummy.LoggingBehavior> _loggingBehavior; 16 | 17 | public Handler( 18 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior, 19 | global::YetAnotherDummy.SecondLoggingBehavior> secondLoggingBehavior1, 20 | global::YetAnotherDummy.LoggingBehavior> loggingBehavior1, 21 | global::Dummy.SecondLoggingBehavior> secondLoggingBehavior, 22 | global::YetAnotherDummy.OtherBehavior> otherBehavior, 23 | global::Dummy.LoggingBehavior> loggingBehavior 24 | ) 25 | { 26 | var handlerType = typeof(GetUsersQuery); 27 | 28 | _handleBehavior = handleBehavior; 29 | 30 | _loggingBehavior = loggingBehavior; 31 | _loggingBehavior.HandlerType = handlerType; 32 | 33 | _otherBehavior = otherBehavior; 34 | _otherBehavior.HandlerType = handlerType; 35 | 36 | _secondLoggingBehavior = secondLoggingBehavior; 37 | _secondLoggingBehavior.HandlerType = handlerType; 38 | 39 | _loggingBehavior1 = loggingBehavior1; 40 | _loggingBehavior1.HandlerType = handlerType; 41 | 42 | _secondLoggingBehavior1 = secondLoggingBehavior1; 43 | _secondLoggingBehavior1.HandlerType = handlerType; 44 | 45 | _secondLoggingBehavior1.SetInnerHandler(_handleBehavior); 46 | _loggingBehavior1.SetInnerHandler(_secondLoggingBehavior1); 47 | _secondLoggingBehavior.SetInnerHandler(_loggingBehavior1); 48 | _otherBehavior.SetInnerHandler(_secondLoggingBehavior); 49 | _loggingBehavior.SetInnerHandler(_otherBehavior); 50 | } 51 | 52 | public async global::System.Threading.Tasks.ValueTask> HandleAsync( 53 | global::Dummy.GetUsersQuery.Query request, 54 | global::System.Threading.CancellationToken cancellationToken = default 55 | ) 56 | { 57 | return await _loggingBehavior 58 | .HandleAsync(request, cancellationToken) 59 | .ConfigureAwait(false); 60 | } 61 | } 62 | 63 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 64 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior> 65 | { 66 | private readonly global::Dummy.UsersService _usersService; 67 | 68 | public HandleBehavior( 69 | global::Dummy.UsersService usersService 70 | ) 71 | { 72 | _usersService = usersService; 73 | } 74 | 75 | public override async global::System.Threading.Tasks.ValueTask> HandleAsync( 76 | global::Dummy.GetUsersQuery.Query request, 77 | global::System.Threading.CancellationToken cancellationToken 78 | ) 79 | { 80 | return await global::Dummy.GetUsersQuery 81 | .HandleAsync( 82 | request 83 | , _usersService 84 | , cancellationToken 85 | ) 86 | .ConfigureAwait(false); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.CodeFixes/StaticToSealedHandlerCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Immediate.Handlers.Analyzers; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeActions; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; 9 | 10 | namespace Immediate.Handlers.CodeFixes; 11 | 12 | [ExportCodeFixProvider(LanguageNames.CSharp)] 13 | public sealed class StaticToSealedHandlerCodeFixProvider : CodeFixProvider 14 | { 15 | public override ImmutableArray FixableDiagnosticIds { get; } = 16 | ImmutableArray.Create([DiagnosticIds.IHR0019StaticHandlerCouldBeSealed]); 17 | 18 | public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; 19 | 20 | public override async Task RegisterCodeFixesAsync(CodeFixContext context) 21 | { 22 | var (document, span, diagnostics, token) = context; 23 | token.ThrowIfCancellationRequested(); 24 | 25 | if (await document.GetRequiredSyntaxRootAsync(token) is not CompilationUnitSyntax root) 26 | return; 27 | 28 | var model = await document.GetRequiredSemanticModelAsync(token); 29 | 30 | if (root.FindNode(span) is not ClassDeclarationSyntax cds) 31 | return; 32 | 33 | if (model.GetDeclaredSymbol(cds, token) is not INamedTypeSymbol { IsStatic: true } container) 34 | return; 35 | 36 | if (!container.GetAttributes().Any(a => a.AttributeClass.IsHandlerAttribute())) 37 | return; 38 | 39 | var method = container.GetMembers() 40 | .OfType() 41 | .FirstOrDefault(m => m is { IsStatic: true, Name: "Handle" or "HandleAsync" }); 42 | 43 | if (method is null) 44 | return; 45 | 46 | var mds = (MethodDeclarationSyntax)await method 47 | .DeclaringSyntaxReferences[0] 48 | .GetSyntaxAsync(token); 49 | 50 | var service = new RefactoringService( 51 | document, 52 | model, 53 | root, 54 | cds, 55 | mds 56 | ); 57 | 58 | context.RegisterCodeFix( 59 | CodeAction.Create( 60 | title: "Convert to instance handler", 61 | createChangedDocument: service.ConvertToInstanceHandler, 62 | equivalenceKey: nameof(StaticToSealedHandlerCodeFixProvider) 63 | ), 64 | diagnostics[0] 65 | ); 66 | } 67 | } 68 | 69 | file sealed class RefactoringService( 70 | Document document, 71 | SemanticModel model, 72 | CompilationUnitSyntax documentRoot, 73 | ClassDeclarationSyntax classDeclarationSyntax, 74 | MethodDeclarationSyntax methodDeclarationSyntax 75 | ) 76 | { 77 | public Task ConvertToInstanceHandler( 78 | CancellationToken token 79 | ) 80 | { 81 | var methodParameters = methodDeclarationSyntax.ParameterList.Parameters; 82 | 83 | var isLastParamCancellationToken = model.IsCancellationToken(methodParameters[^1].Type, token); 84 | 85 | var classParameters = methodParameters 86 | .Skip(1) 87 | .Take(methodParameters.Count - (isLastParamCancellationToken ? 2 : 1)) 88 | .Select(p => p.WithTrailingTrivia(ElasticSpace)) 89 | .ToList(); 90 | 91 | var newMethodParameters = methodParameters.RemoveParametersUntilCount(isLastParamCancellationToken ? 2 : 1); 92 | 93 | var newMethodDeclarationSyntax = methodDeclarationSyntax 94 | .WithParameterList( 95 | methodDeclarationSyntax.ParameterList 96 | .WithParameters(newMethodParameters) 97 | ) 98 | .WithModifiers( 99 | methodDeclarationSyntax.Modifiers 100 | .RemoveStaticModifier() 101 | ); 102 | 103 | var newClassDeclarationSyntax = classDeclarationSyntax 104 | .ReplaceNode(methodDeclarationSyntax, newMethodDeclarationSyntax) 105 | .WithModifiers( 106 | classDeclarationSyntax.Modifiers 107 | .RemoveStaticModifier() 108 | .Insert( 109 | // valid case will have `partial` as final element; insert `sealed` before `partial` 110 | classDeclarationSyntax.Modifiers.Count - 2, 111 | Token(SyntaxKind.SealedKeyword).WithTrailingTrivia(ElasticSpace) 112 | ) 113 | ); 114 | 115 | if (classParameters.Count > 0) 116 | { 117 | newClassDeclarationSyntax = newClassDeclarationSyntax 118 | .WithParameterList( 119 | ParameterList(SeparatedList(classParameters)) 120 | ) 121 | .WithIdentifier(classDeclarationSyntax.Identifier.WithoutTrivia()); 122 | } 123 | 124 | return Task.FromResult(document.WithSyntaxRoot(documentRoot.ReplaceNode(classDeclarationSyntax, newClassDeclarationSyntax))); 125 | } 126 | } 127 | 128 | file static class SyntaxExtensions 129 | { 130 | public static SeparatedSyntaxList RemoveParametersUntilCount( 131 | this SeparatedSyntaxList nodes, 132 | int count 133 | ) 134 | { 135 | while (nodes.Count > count) 136 | nodes = nodes.RemoveAt(1); 137 | return nodes; 138 | } 139 | 140 | public static SyntaxTokenList RemoveStaticModifier( 141 | this SyntaxTokenList list 142 | ) => new(list.Where(static token => !token.IsKind(SyntaxKind.StaticKeyword))); 143 | } 144 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/CodeFixTests/StaticToSealedHandlerRefactoringProviderTests.cs: -------------------------------------------------------------------------------- 1 | using Immediate.Handlers.Analyzers; 2 | using Immediate.Handlers.CodeFixes; 3 | 4 | namespace Immediate.Handlers.Tests.CodeFixTests; 5 | 6 | public sealed class StaticToSealedHandlerCodeFixProviderTests 7 | { 8 | [Fact] 9 | public async Task RefactorOnHandlerClass() => 10 | await CodeFixTestHelper.CreateCodeFixTest( 11 | """ 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | 15 | namespace Immediate.Handlers.Shared; 16 | 17 | public sealed class Dependency1; 18 | public sealed class Dependency2; 19 | 20 | [Handler] 21 | public static partial class {|IHR0019:DoSomething|} 22 | { 23 | public sealed record Query; 24 | public sealed record Response; 25 | 26 | private static ValueTask HandleAsync( 27 | Query query, 28 | Dependency1 dependency1, 29 | Dependency2 dependency2, 30 | CancellationToken token 31 | ) 32 | { 33 | Method(dependency2, 2); 34 | return new(); 35 | } 36 | 37 | private static void Method(Dependency2 dependency2, int value) 38 | { 39 | } 40 | } 41 | """, 42 | """ 43 | using System.Threading; 44 | using System.Threading.Tasks; 45 | 46 | namespace Immediate.Handlers.Shared; 47 | 48 | public sealed class Dependency1; 49 | public sealed class Dependency2; 50 | 51 | [Handler] 52 | public sealed partial class DoSomething(Dependency1 dependency1, Dependency2 dependency2) 53 | { 54 | public sealed record Query; 55 | public sealed record Response; 56 | 57 | private ValueTask HandleAsync( 58 | Query query, 59 | CancellationToken token 60 | ) 61 | { 62 | Method(dependency2, 2); 63 | return new(); 64 | } 65 | 66 | private static void Method(Dependency2 dependency2, int value) 67 | { 68 | } 69 | } 70 | """, 71 | Helpers.DriverReferenceAssemblies.Normal 72 | ).RunAsync(TestContext.Current.CancellationToken); 73 | 74 | [Fact] 75 | public async Task RefactorOnHandlerMethod() => 76 | await CodeFixTestHelper.CreateCodeFixTest( 77 | """ 78 | using System.Threading; 79 | using System.Threading.Tasks; 80 | 81 | namespace Immediate.Handlers.Shared; 82 | 83 | public sealed class Dependency1; 84 | public sealed class Dependency2; 85 | 86 | [Handler] 87 | public static partial class {|IHR0019:DoSomething|} 88 | { 89 | public sealed record Query; 90 | public sealed record Response; 91 | 92 | private static ValueTask HandleAsync( 93 | Query query, 94 | Dependency1 dependency1, 95 | Dependency2 dependency2, 96 | CancellationToken token 97 | ) 98 | { 99 | Method(dependency2, 2); 100 | return new(); 101 | } 102 | 103 | private static void Method(Dependency2 dependency2, int value) 104 | { 105 | } 106 | } 107 | """, 108 | """ 109 | using System.Threading; 110 | using System.Threading.Tasks; 111 | 112 | namespace Immediate.Handlers.Shared; 113 | 114 | public sealed class Dependency1; 115 | public sealed class Dependency2; 116 | 117 | [Handler] 118 | public sealed partial class DoSomething(Dependency1 dependency1, Dependency2 dependency2) 119 | { 120 | public sealed record Query; 121 | public sealed record Response; 122 | 123 | private ValueTask HandleAsync( 124 | Query query, 125 | CancellationToken token 126 | ) 127 | { 128 | Method(dependency2, 2); 129 | return new(); 130 | } 131 | 132 | private static void Method(Dependency2 dependency2, int value) 133 | { 134 | } 135 | } 136 | """, 137 | Helpers.DriverReferenceAssemblies.Normal 138 | ).RunAsync(TestContext.Current.CancellationToken); 139 | 140 | [Fact] 141 | public async Task RefactorWithNoDependencyParameters() => 142 | await CodeFixTestHelper.CreateCodeFixTest( 143 | """ 144 | using System.Threading; 145 | using System.Threading.Tasks; 146 | 147 | namespace Immediate.Handlers.Shared; 148 | 149 | [Handler] 150 | public static partial class {|IHR0019:DoSomething|} 151 | { 152 | public sealed record Query; 153 | public sealed record Response; 154 | 155 | private static ValueTask HandleAsync( 156 | Query query, 157 | CancellationToken token 158 | ) 159 | { 160 | return new(); 161 | } 162 | } 163 | """, 164 | """ 165 | using System.Threading; 166 | using System.Threading.Tasks; 167 | 168 | namespace Immediate.Handlers.Shared; 169 | 170 | [Handler] 171 | public sealed partial class DoSomething 172 | { 173 | public sealed record Query; 174 | public sealed record Response; 175 | 176 | private ValueTask HandleAsync( 177 | Query query, 178 | CancellationToken token 179 | ) 180 | { 181 | return new(); 182 | } 183 | } 184 | """, 185 | Helpers.DriverReferenceAssemblies.Normal 186 | ).RunAsync(TestContext.Current.CancellationToken); 187 | } 188 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Analyzers/BehaviorsAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | using Microsoft.CodeAnalysis.Operations; 6 | 7 | namespace Immediate.Handlers.Analyzers; 8 | 9 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 10 | public sealed class BehaviorsAnalyzer : DiagnosticAnalyzer 11 | { 12 | public static readonly DiagnosticDescriptor BehaviorsMustInheritFromBehavior = 13 | new( 14 | id: DiagnosticIds.IHR0006BehaviorsMustInheritFromBehavior, 15 | title: "Behaviors must inherit from Behavior<,>", 16 | messageFormat: "Behavior type '{0}' must inherit from Behavior<,>", 17 | category: "ImmediateHandler", 18 | defaultSeverity: DiagnosticSeverity.Error, 19 | isEnabledByDefault: true, 20 | description: "All Behaviors must inherit from Behavior<,>." 21 | ); 22 | 23 | public static readonly DiagnosticDescriptor BehaviorsMustHaveTwoGenericParameters = 24 | new( 25 | id: DiagnosticIds.IHR0007BehaviorsMustHaveTwoGenericParameters, 26 | title: "Behaviors must have two generic parameters", 27 | messageFormat: "Behavior type '{0}' must have two generic parameters", 28 | category: "ImmediateHandler", 29 | defaultSeverity: DiagnosticSeverity.Error, 30 | isEnabledByDefault: true, 31 | description: "All Behaviors must have two generic parameters, correctly referencing `TRequest` and `TResponse`." 32 | ); 33 | 34 | public static readonly DiagnosticDescriptor BehaviorsMustUseUnboundGenerics = 35 | new( 36 | id: DiagnosticIds.IHR0008BehaviorsMustUseUnboundGenerics, 37 | title: "Behaviors must use unbound generics", 38 | messageFormat: "Behavior type '{0}' must be a generic type without type arguments", 39 | category: "ImmediateHandler", 40 | defaultSeverity: DiagnosticSeverity.Error, 41 | isEnabledByDefault: true, 42 | description: "All behaviors must use a generic type without type arguments." 43 | ); 44 | 45 | public override ImmutableArray SupportedDiagnostics { get; } = 46 | ImmutableArray.Create( 47 | [ 48 | BehaviorsMustInheritFromBehavior, 49 | BehaviorsMustHaveTwoGenericParameters, 50 | BehaviorsMustUseUnboundGenerics, 51 | ]); 52 | 53 | public override void Initialize(AnalysisContext context) 54 | { 55 | if (context == null) 56 | throw new ArgumentNullException(nameof(context)); 57 | 58 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 59 | context.EnableConcurrentExecution(); 60 | 61 | context.RegisterOperationAction(AnalyzeOperation, OperationKind.Attribute); 62 | } 63 | 64 | private void AnalyzeOperation(OperationAnalysisContext context) 65 | { 66 | var token = context.CancellationToken; 67 | token.ThrowIfCancellationRequested(); 68 | 69 | if (context.Operation is not IAttributeOperation { Operation: IObjectCreationOperation attribute }) 70 | return; 71 | 72 | if (!attribute.Type.IsBehaviorsAttribute()) 73 | return; 74 | 75 | if (attribute.Arguments.Length != 1) 76 | { 77 | // note: this will already be a compiler error anyway 78 | return; 79 | } 80 | 81 | token.ThrowIfCancellationRequested(); 82 | var array = attribute.Arguments[0].Value; 83 | 84 | if (array is not 85 | { 86 | Type: IArrayTypeSymbol 87 | { 88 | ElementType: 89 | { 90 | Name: "Type", 91 | ContainingNamespace: 92 | { 93 | Name: "System", 94 | ContainingNamespace.IsGlobalNamespace: true, 95 | }, 96 | }, 97 | }, 98 | ChildOperations.Count: 2 99 | } 100 | || array.ChildOperations.ElementAt(1) is not IArrayInitializerOperation aio) 101 | { 102 | // note: this will already be a compiler error anyway 103 | return; 104 | } 105 | 106 | token.ThrowIfCancellationRequested(); 107 | var baseBehaviorSymbol = context.Compilation.GetTypeByMetadataName("Immediate.Handlers.Shared.Behavior`2"); 108 | if (baseBehaviorSymbol is null) 109 | return; 110 | 111 | foreach (var op in aio.ChildOperations) 112 | { 113 | token.ThrowIfCancellationRequested(); 114 | if (op is not ITypeOfOperation 115 | { 116 | TypeOperand: INamedTypeSymbol behaviorType, 117 | Syntax: TypeOfExpressionSyntax toes, 118 | } 119 | ) 120 | { 121 | continue; 122 | } 123 | 124 | var location = toes.Type.GetLocation(); 125 | var originalDefinition = behaviorType.OriginalDefinition; 126 | 127 | if (!originalDefinition.ImplementsBehavior()) 128 | { 129 | context.ReportDiagnostic( 130 | Diagnostic.Create( 131 | BehaviorsMustInheritFromBehavior, 132 | location, 133 | originalDefinition.Name) 134 | ); 135 | } 136 | 137 | if (!originalDefinition.IsGenericType 138 | || originalDefinition.TypeParameters.Length != 2) 139 | { 140 | context.ReportDiagnostic( 141 | Diagnostic.Create( 142 | BehaviorsMustHaveTwoGenericParameters, 143 | location, 144 | originalDefinition.Name) 145 | ); 146 | } 147 | else if (!behaviorType.IsUnboundGenericType) 148 | { 149 | context.ReportDiagnostic( 150 | Diagnostic.Create( 151 | BehaviorsMustUseUnboundGenerics, 152 | location, 153 | originalDefinition.Name) 154 | ); 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.MultipleBehaviors_assemblies=Msdi#IH.Dummy.GetUsersQuery.g.verified.cs: -------------------------------------------------------------------------------- 1 | //HintName: IH.Dummy.GetUsersQuery.g.cs 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable CS1591 5 | 6 | namespace Dummy; 7 | 8 | partial class GetUsersQuery 9 | { 10 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler> 11 | { 12 | private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior; 13 | private readonly global::YetAnotherDummy.SecondLoggingBehavior> _secondLoggingBehavior1; 14 | private readonly global::YetAnotherDummy.LoggingBehavior> _loggingBehavior1; 15 | private readonly global::Dummy.SecondLoggingBehavior> _secondLoggingBehavior; 16 | private readonly global::YetAnotherDummy.OtherBehavior> _otherBehavior; 17 | private readonly global::Dummy.LoggingBehavior> _loggingBehavior; 18 | 19 | public Handler( 20 | global::Dummy.GetUsersQuery.HandleBehavior handleBehavior, 21 | global::YetAnotherDummy.SecondLoggingBehavior> secondLoggingBehavior1, 22 | global::YetAnotherDummy.LoggingBehavior> loggingBehavior1, 23 | global::Dummy.SecondLoggingBehavior> secondLoggingBehavior, 24 | global::YetAnotherDummy.OtherBehavior> otherBehavior, 25 | global::Dummy.LoggingBehavior> loggingBehavior 26 | ) 27 | { 28 | var handlerType = typeof(GetUsersQuery); 29 | 30 | _handleBehavior = handleBehavior; 31 | 32 | _loggingBehavior = loggingBehavior; 33 | _loggingBehavior.HandlerType = handlerType; 34 | 35 | _otherBehavior = otherBehavior; 36 | _otherBehavior.HandlerType = handlerType; 37 | 38 | _secondLoggingBehavior = secondLoggingBehavior; 39 | _secondLoggingBehavior.HandlerType = handlerType; 40 | 41 | _loggingBehavior1 = loggingBehavior1; 42 | _loggingBehavior1.HandlerType = handlerType; 43 | 44 | _secondLoggingBehavior1 = secondLoggingBehavior1; 45 | _secondLoggingBehavior1.HandlerType = handlerType; 46 | 47 | _secondLoggingBehavior1.SetInnerHandler(_handleBehavior); 48 | _loggingBehavior1.SetInnerHandler(_secondLoggingBehavior1); 49 | _secondLoggingBehavior.SetInnerHandler(_loggingBehavior1); 50 | _otherBehavior.SetInnerHandler(_secondLoggingBehavior); 51 | _loggingBehavior.SetInnerHandler(_otherBehavior); 52 | } 53 | 54 | public async global::System.Threading.Tasks.ValueTask> HandleAsync( 55 | global::Dummy.GetUsersQuery.Query request, 56 | global::System.Threading.CancellationToken cancellationToken = default 57 | ) 58 | { 59 | return await _loggingBehavior 60 | .HandleAsync(request, cancellationToken) 61 | .ConfigureAwait(false); 62 | } 63 | } 64 | 65 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 66 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior> 67 | { 68 | private readonly global::Dummy.UsersService _usersService; 69 | 70 | public HandleBehavior( 71 | global::Dummy.UsersService usersService 72 | ) 73 | { 74 | _usersService = usersService; 75 | } 76 | 77 | public override async global::System.Threading.Tasks.ValueTask> HandleAsync( 78 | global::Dummy.GetUsersQuery.Query request, 79 | global::System.Threading.CancellationToken cancellationToken 80 | ) 81 | { 82 | return await global::Dummy.GetUsersQuery 83 | .HandleAsync( 84 | request 85 | , _usersService 86 | , cancellationToken 87 | ) 88 | .ConfigureAwait(false); 89 | } 90 | } 91 | 92 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 93 | public static IServiceCollection AddHandlers( 94 | IServiceCollection services, 95 | ServiceLifetime lifetime = ServiceLifetime.Scoped 96 | ) 97 | { 98 | services.Add(new(typeof(global::Dummy.GetUsersQuery.Handler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 99 | services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler>), typeof(global::Dummy.GetUsersQuery.Handler), lifetime)); 100 | services.Add(new(typeof(global::Dummy.GetUsersQuery.HandleBehavior), typeof(global::Dummy.GetUsersQuery.HandleBehavior), lifetime)); 101 | return services; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /benchmarks/Benchmark.Large/Benchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using BenchmarkDotNet.Attributes; 3 | using BenchmarkDotNet.Order; 4 | using Immediate.Handlers.Shared; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Immediate.Handlers.Benchmarks; 8 | 9 | public sealed record SomeRequest(Guid Id) 10 | : Mediator.IRequest, 11 | MediatR.IRequest; 12 | 13 | public sealed record SomeResponse(Guid Id); 14 | 15 | public sealed partial class SomeHandlerClass 16 | : Mediator.IRequestHandler, 17 | MediatR.IRequestHandler 18 | { 19 | private static readonly SomeResponse s_response = new(Guid.NewGuid()); 20 | 21 | [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Bench instance method")] 22 | [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Not Being Tested")] 23 | public ValueTask Handle( 24 | SomeRequest request, 25 | CancellationToken cancellationToken 26 | ) => ValueTask.FromResult(s_response); 27 | 28 | ValueTask Mediator.IRequestHandler.Handle( 29 | SomeRequest request, 30 | CancellationToken cancellationToken 31 | ) => ValueTask.FromResult(s_response); 32 | 33 | Task MediatR.IRequestHandler.Handle( 34 | SomeRequest request, 35 | CancellationToken cancellationToken 36 | ) => Task.FromResult(s_response); 37 | } 38 | 39 | [Handler] 40 | public static partial class StaticIhExample 41 | { 42 | private static readonly SomeResponse s_response = new(Guid.NewGuid()); 43 | 44 | [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Not Being Tested")] 45 | private static ValueTask HandleAsync( 46 | SomeRequest request, 47 | CancellationToken token 48 | ) => ValueTask.FromResult(s_response); 49 | } 50 | 51 | [Handler] 52 | public sealed partial class SealedIhExample 53 | { 54 | private static readonly SomeResponse s_response = new(Guid.NewGuid()); 55 | 56 | [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Not Being Tested")] 57 | [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Bench instance method")] 58 | private ValueTask HandleAsync( 59 | SomeRequest request, 60 | CancellationToken token 61 | ) => ValueTask.FromResult(s_response); 62 | } 63 | 64 | [SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net90)] 65 | [SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net10_0)] 66 | [MemoryDiagnoser] 67 | [Orderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)] 68 | [RankColumn] 69 | #pragma warning disable CA1707 // Identifiers should not contain underscores 70 | public class RequestBenchmarks 71 | { 72 | private IServiceProvider? _serviceProvider; 73 | private IServiceProvider? _abstractionServiceProvider; 74 | private IServiceScope? _serviceScope; 75 | private IServiceScope? _abstractionServiceScope; 76 | private Mediator.IMediator? _mediator; 77 | private Mediator.Mediator? _concreteMediator; 78 | private MediatR.IMediator? _mediatr; 79 | private StaticIhExample.Handler? _immediateStaticHandler; 80 | private SealedIhExample.Handler? _immediateSealedHandler; 81 | private IHandler? _immediateHandlerAbstraction; 82 | private SomeHandlerClass? _handler; 83 | private SomeRequest? _request; 84 | 85 | [GlobalSetup] 86 | public void Setup() 87 | { 88 | var services = new ServiceCollection(); 89 | 90 | _ = services.AddBenchmarkLargeHandlers(); 91 | 92 | _ = services.AddMediator(opts => opts.ServiceLifetime = ServiceLifetime.Scoped); 93 | _ = services.AddMediatR( 94 | cfg => cfg.RegisterServicesFromAssemblyContaining() 95 | ); 96 | 97 | _serviceProvider = services.BuildServiceProvider(); 98 | 99 | _serviceScope = _serviceProvider.CreateScope(); 100 | _serviceProvider = _serviceScope.ServiceProvider; 101 | 102 | _mediator = _serviceProvider.GetRequiredService(); 103 | _concreteMediator = _serviceProvider.GetRequiredService(); 104 | _mediatr = _serviceProvider.GetRequiredService(); 105 | _immediateStaticHandler = _serviceProvider.GetRequiredService(); 106 | _immediateSealedHandler = _serviceProvider.GetRequiredService(); 107 | _handler = _serviceProvider.GetRequiredService(); 108 | _request = new(Guid.NewGuid()); 109 | 110 | _abstractionServiceScope = _serviceProvider.CreateScope(); 111 | _abstractionServiceProvider = _abstractionServiceScope.ServiceProvider; 112 | _immediateHandlerAbstraction = _abstractionServiceProvider.GetRequiredService>(); 113 | } 114 | 115 | [GlobalCleanup] 116 | public void Cleanup() 117 | { 118 | if (_serviceScope is not null) 119 | _serviceScope.Dispose(); 120 | else 121 | (_serviceProvider as IDisposable)?.Dispose(); 122 | } 123 | 124 | [Benchmark] 125 | public ValueTask SendRequest_ImmediateStaticHandler() 126 | { 127 | return _immediateStaticHandler!.HandleAsync(_request!, CancellationToken.None); 128 | } 129 | 130 | [Benchmark] 131 | public ValueTask SendRequest_ImmediateSealedHandler() 132 | { 133 | return _immediateSealedHandler!.HandleAsync(_request!, CancellationToken.None); 134 | } 135 | 136 | [Benchmark] 137 | public ValueTask SendRequest_ImmediateHandler_Abstraction() 138 | { 139 | return _immediateHandlerAbstraction!.HandleAsync(_request!, CancellationToken.None); 140 | } 141 | 142 | [Benchmark] 143 | public Task SendRequest_MediatR() 144 | { 145 | return _mediatr!.Send(_request!, CancellationToken.None); 146 | } 147 | 148 | [Benchmark] 149 | public ValueTask SendRequest_IMediator() 150 | { 151 | return _mediator!.Send(_request!, CancellationToken.None); 152 | } 153 | 154 | [Benchmark] 155 | public ValueTask SendRequest_Mediator() 156 | { 157 | return _concreteMediator!.Send(_request!, CancellationToken.None); 158 | } 159 | 160 | [Benchmark(Baseline = true)] 161 | public ValueTask SendRequest_Baseline() 162 | { 163 | return _handler!.Handle(_request!, CancellationToken.None); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Immediate.Handlers.Generators/Templates/Handler.sbntxt: -------------------------------------------------------------------------------- 1 | {{~ if has_ms_di ~}} 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | {{~ end ~}} 5 | #pragma warning disable CS1591 6 | 7 | {{~ if !string.empty namespace ~}} 8 | namespace {{ namespace }}; 9 | 10 | {{~ end ~}} 11 | partial class {{ class_name }} 12 | { 13 | public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler<{{ request_type }}, {{ response_type }}> 14 | { 15 | private readonly {{ class_fully_qualified_name }}.HandleBehavior _handleBehavior; 16 | {{~ for behavior in (behaviors | array.reverse) ~}} 17 | private readonly {{ behavior.non_generic_type_name }}<{{ request_type }}, {{ response_type }}> _{{ behavior.variable_name }}; 18 | {{~ end ~}} 19 | 20 | public Handler( 21 | {{ class_fully_qualified_name }}.HandleBehavior handleBehavior{{ if behaviors.size > 0 }},{{ end }} 22 | {{~ for behavior in (behaviors | array.reverse) ~}} 23 | {{ behavior.non_generic_type_name }}<{{ request_type }}, {{ response_type }}> {{ behavior.variable_name }}{{ if !for.last }},{{end }} 24 | {{~ end ~}} 25 | ) 26 | { 27 | var handlerType = typeof({{ class_name }}); 28 | 29 | _handleBehavior = handleBehavior; 30 | {{~ for behavior in behaviors ~}} 31 | 32 | _{{ behavior.variable_name }} = {{ behavior.variable_name }}; 33 | _{{ behavior.variable_name }}.HandlerType = handlerType; 34 | {{~ end ~}} 35 | 36 | {{~ for behavior in (behaviors | array.reverse) ~}} 37 | {{~ if for.first ~}} 38 | _{{ behavior.variable_name }}.SetInnerHandler(_handleBehavior); 39 | {{~ else ~}} 40 | _{{ behavior.variable_name }}.SetInnerHandler(_{{ behaviors[behaviors.size - for.index].variable_name }}); 41 | {{~ end ~}} 42 | {{~ end ~}} 43 | } 44 | 45 | public async global::System.Threading.Tasks.ValueTask<{{ response_type }}> HandleAsync( 46 | {{ request_type }} request, 47 | global::System.Threading.CancellationToken cancellationToken = default 48 | ) 49 | { 50 | {{~ if behaviors.size > 0 ~}} 51 | return await _{{ behaviors[0].variable_name }} 52 | .HandleAsync(request, cancellationToken) 53 | .ConfigureAwait(false); 54 | {{~ else ~}} 55 | return await _handleBehavior 56 | .HandleAsync(request, cancellationToken) 57 | .ConfigureAwait(false); 58 | {{~ end ~}} 59 | } 60 | } 61 | 62 | {{~ if is_static ~}} 63 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 64 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior<{{ request_type }}, {{ response_type }}> 65 | { 66 | {{~ for parameter in handler_parameters ~}} 67 | private readonly {{ parameter.type }} _{{ parameter.name }}; 68 | {{~ end ~}} 69 | 70 | public HandleBehavior( 71 | {{~ for parameter in handler_parameters ~}} 72 | {{ parameter.attributes }}{{ parameter.type }} {{ parameter.name }}{{ if !for.last }},{{ end }} 73 | {{~ end ~}} 74 | ) 75 | { 76 | {{~ for parameter in handler_parameters ~}} 77 | _{{ parameter.name }} = {{ parameter.name }}; 78 | {{~ end ~}} 79 | } 80 | 81 | public override async global::System.Threading.Tasks.ValueTask<{{ response_type }}> HandleAsync( 82 | {{ request_type }} request, 83 | global::System.Threading.CancellationToken cancellationToken 84 | ) 85 | { 86 | {{~ if !is_implicit_value_tuple ~}} 87 | return await {{ class_fully_qualified_name }} 88 | .{{ method_name }}( 89 | request 90 | {{~ for parameter in handler_parameters ~}} 91 | , _{{ parameter.name }} 92 | {{~ end ~}} 93 | {{~ if use_token ~}} 94 | , cancellationToken 95 | {{~ end ~}} 96 | ) 97 | .ConfigureAwait(false); 98 | {{~ else ~}} 99 | await {{ class_fully_qualified_name }} 100 | .{{ method_name }}( 101 | request 102 | {{~ for parameter in handler_parameters ~}} 103 | , _{{ parameter.name }} 104 | {{~ end ~}} 105 | {{~ if use_token ~}} 106 | , cancellationToken 107 | {{~ end ~}} 108 | ) 109 | .ConfigureAwait(false); 110 | 111 | return default; 112 | {{~ end ~}} 113 | } 114 | } 115 | {{~ else ~}} 116 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 117 | public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior<{{ request_type }}, {{ response_type }}> 118 | { 119 | private readonly {{ class_fully_qualified_name }} _container; 120 | 121 | public HandleBehavior( 122 | {{ class_fully_qualified_name }} container 123 | ) 124 | { 125 | _container = container; 126 | } 127 | 128 | public override async global::System.Threading.Tasks.ValueTask<{{ response_type }}> HandleAsync( 129 | {{ request_type }} request, 130 | global::System.Threading.CancellationToken cancellationToken 131 | ) 132 | { 133 | {{~ if !is_implicit_value_tuple ~}} 134 | return await _container 135 | .{{ method_name }}( 136 | request 137 | {{~ if use_token ~}} 138 | , cancellationToken 139 | {{~ end ~}} 140 | ) 141 | .ConfigureAwait(false); 142 | {{~ else ~}} 143 | await _container 144 | .{{ method_name }}( 145 | request 146 | {{~ if use_token ~}} 147 | , cancellationToken 148 | {{~ end ~}} 149 | ) 150 | .ConfigureAwait(false); 151 | 152 | return default; 153 | {{~ end ~}} 154 | } 155 | } 156 | {{~ end ~}} 157 | {{~ if has_ms_di ~}} 158 | 159 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 160 | public static IServiceCollection AddHandlers( 161 | IServiceCollection services, 162 | ServiceLifetime lifetime = ServiceLifetime.Scoped 163 | ) 164 | { 165 | services.Add(new(typeof({{ class_fully_qualified_name }}.Handler), typeof({{ class_fully_qualified_name }}.Handler), lifetime)); 166 | services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler<{{ request_type }}, {{ response_type }}>), typeof({{ class_fully_qualified_name }}.Handler), lifetime)); 167 | services.Add(new(typeof({{ class_fully_qualified_name }}.HandleBehavior), typeof({{ class_fully_qualified_name }}.HandleBehavior), lifetime)); 168 | {{~ if !is_static ~}} 169 | services.Add(new(typeof({{ class_fully_qualified_name }}), typeof({{ class_fully_qualified_name }}), lifetime)); 170 | {{~ end ~}} 171 | return services; 172 | } 173 | {{~ end ~}} 174 | } 175 | --------------------------------------------------------------------------------