From 359e11166b6f6adc4924fb3502aa097e20a2cf1b Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 22 Mar 2026 20:46:03 +0000 Subject: [PATCH 1/7] Adds mediator for mocha --- src/All.slnx | 1 + src/Mocha/Mocha.sln | 90 ++ ...rks.Messaging.ImmediateCommandHandler.g.cs | 68 + ...g.ImmediateFullPipelineCommandHandler.g.cs | 86 ++ ...aging.ImmediatePipelineCommandHandler.g.cs | 74 + .../IH.ServiceCollectionExtensions.g.cs | 32 + .../AssemblyReference.g.cs | 38 + .../Mediator.g.cs | 1346 +++++++++++++++++ .../MediatorOptions.g.cs | 55 + .../MediatorOptionsAttribute.g.cs | 33 + .../BenchmarkSwitchMediator.g.cs | 243 +++ ...lderExtensions._RXvJz6gQPQLNfJu4g_tRw.g.cs | 49 + .../BenchmarksMediatorBuilderExtensions.g.cs | 35 + .../Internal/DispatchPatternBenchmarks.cs | 348 +++++ .../Internal/LookupBenchmarks.cs | 77 + .../Internal/MediatorSgRegistration.cs | 11 + .../Internal/PipelinePatternBenchmarks.cs | 168 ++ .../Internal/PoolingBenchmarks.cs | 152 ++ .../Messaging/BenchmarkSetup.cs | 159 ++ .../Messaging/CommandBenchmarks.cs | 190 +++ .../Messaging/ConcurrentPipelineBenchmarks.cs | 124 ++ .../Messaging/FullPipelineBenchmarks.cs | 147 ++ .../Messaging/FullPipelineMessages.cs | 244 +++ .../Messaging/ImmediateHandlersMessages.cs | 44 + .../Messaging/MassTransitMessages.cs | 28 + .../Messaging/MediatorSgHelper.cs | 9 + .../Messaging/MediatorSgMessages.cs | 43 + .../Messaging/Messages.cs | 84 + .../Messaging/NotificationBenchmarks.cs | 169 +++ .../Messaging/PipelineBenchmarks.cs | 120 ++ .../Messaging/SwitchMediatorMessages.cs | 46 + .../Messaging/WolverineMessages.cs | 23 + .../Mocha.Mediator.Benchmarks.csproj | 39 + .../Mocha.Mediator.Benchmarks/Program.cs | 12 + .../Commands/ProcessPaymentCommand.cs | 57 + .../src/Demo/Demo.Billing/Demo.Billing.csproj | 2 + src/Mocha/src/Demo/Demo.Billing/Program.cs | 133 +- .../Demo.Billing/Queries/InvoiceQueries.cs | 38 + .../Demo.Billing/Queries/PaymentQueries.cs | 16 + .../Demo.Billing/Queries/RefundQueries.cs | 26 + .../Queries/RevenueSummaryQueries.cs | 27 + .../Commands/InitiateReturnCommand.cs | 81 + .../Commands/PlaceBulkOrderCommand.cs | 54 + .../Commands/PlaceOrderCommand.cs | 73 + .../Commands/RequestQuickRefundCommand.cs | 62 + .../src/Demo/Demo.Catalog/Demo.Catalog.csproj | 2 + src/Mocha/src/Demo/Demo.Catalog/Program.cs | 289 +--- .../Demo.Catalog/Queries/CategoryQueries.cs | 16 + .../Demo/Demo.Catalog/Queries/OrderQueries.cs | 27 + .../Demo.Catalog/Queries/ProductQueries.cs | 27 + .../Commands/ReceiveReturnPackageCommand.cs | 60 + .../Commands/ShipShipmentCommand.cs | 49 + .../Demo/Demo.Shipping/Demo.Shipping.csproj | 2 + src/Mocha/src/Demo/Demo.Shipping/Program.cs | 172 +-- .../Queries/ReturnShipmentQueries.cs | 38 + .../Demo.Shipping/Queries/ShipmentQueries.cs | 38 + .../PublicTopLevelProgram.Generated.g.cs | 5 + ...lderExtensions._5hYhW_IBO7W3L_MSoxCPw.g.cs | 88 ++ .../src/Examples/MediatorShowcase/Handlers.cs | 141 ++ .../MediatorShowcase/MediatorShowcase.cs | 105 ++ .../MediatorShowcase/MediatorShowcase.csproj | 19 + .../MediatorShowcase/PipelineBehaviors.cs | 208 +++ .../Properties/launchSettings.json | 14 + .../AnalyzerReleases.Shipped.md | 1 + .../AnalyzerReleases.Unshipped.md | 8 + src/Mocha/src/Mocha.Analyzers/Errors.cs | 70 + .../DependencyInjectionFileBuilder.cs | 195 +++ .../FileBuilders/FileBuilderBase.cs | 91 ++ .../FileBuilders/IFileBuilder.cs | 38 + .../Filters/AssemblyAttributeListFilter.cs | 21 + .../Filters/ClassWithMochaBaseListFilter.cs | 53 + .../Mocha.Analyzers/Filters/ISyntaxFilter.cs | 17 + .../Filters/SyntaxFilterBuilder.cs | 68 + .../DependencyInjectionGenerator.cs | 99 ++ .../Generators/ISyntaxGenerator.cs | 25 + .../Inspectors/AbstractHandlerInspector.cs | 112 ++ .../Inspectors/HandlerInspector.cs | 90 ++ .../Inspectors/ISyntaxInspector.cs | 44 + .../Inspectors/MediatorModuleInspector.cs | 65 + .../Inspectors/MessageTypeInspector.cs | 153 ++ .../NotificationHandlerInspector.cs | 72 + .../src/Mocha.Analyzers/IsExternalInit.cs | 53 + .../src/Mocha.Analyzers/KnownTypeSymbols.cs | 98 ++ .../src/Mocha.Analyzers/MediatorGenerator.cs | 283 ++++ .../Mocha.Analyzers/Mocha.Analyzers.csproj | 23 + .../Mocha.Analyzers/Models/DiagnosticInfo.cs | 15 + .../src/Mocha.Analyzers/Models/HandlerInfo.cs | 22 + .../src/Mocha.Analyzers/Models/HandlerKind.cs | 22 + .../Mocha.Analyzers/Models/LocationInfo.cs | 12 + .../Models/MediatorModuleInfo.cs | 12 + .../src/Mocha.Analyzers/Models/MessageKind.cs | 22 + .../Mocha.Analyzers/Models/MessageTypeInfo.cs | 22 + .../Models/NotificationHandlerInfo.cs | 16 + .../src/Mocha.Analyzers/Models/SyntaxInfo.cs | 23 + .../src/Mocha.Analyzers/SyntaxConstants.cs | 53 + .../src/Mocha.Analyzers/Utils/CodeWriter.cs | 152 ++ .../Utils/CodeWriterExtensions.cs | 48 + .../Utils/ImmutableEquatableArray.cs | 115 ++ .../Mocha.Analyzers/Utils/ModuleNameHelper.cs | 41 + .../Mocha.Analyzers/Utils/PooledObjects.cs | 163 ++ .../Mocha.Analyzers/Utils/RoslynExtensions.cs | 83 + ...meworkCorePersistenceBuilderExtensions.cs} | 2 +- .../EntityFrameworkTransactionFeature.cs | 22 + .../EntityFrameworkTransactionMiddleware.cs | 68 + .../IEntityFrameworkCoreBuilder.cs | 8 +- ...ediatorBuilderEntityFrameworkExtensions.cs | 50 + .../MediatorEntityFrameworkOptions.cs | 32 + .../MessageBusHostBuilderExtensions.cs | 18 +- .../MessagingDbContextOptions.cs | 15 +- .../Mocha.EntityFrameworkCore.csproj | 1 + ...meworkCorePersistenceBuilderExtensions.cs} | 2 +- .../Sagas/DbContextSagaStore.cs | 4 +- .../Mocha.Mediator.Abstractions/ICommand.cs | 12 + .../ICommandHandler.cs | 32 + .../Mocha.Mediator.Abstractions/IMediator.cs | 10 + .../INotification.cs | 6 + .../INotificationHandler.cs | 17 + .../INotificationStrategy.cs | 24 + .../Mocha.Mediator.Abstractions/IPublisher.cs | 29 + .../src/Mocha.Mediator.Abstractions/IQuery.cs | 7 + .../IQueryHandler.cs | 17 + .../Mocha.Mediator.Abstractions/ISender.cs | 48 + .../MediatorModuleAttribute.cs | 26 + .../Mocha.Mediator.Abstractions.csproj | 8 + .../src/Mocha.Mediator.Abstractions/Unit.cs | 51 + .../DependencyInjection/IMediatorBuilder.cs | 62 + .../IMediatorHostBuilder.cs | 26 + .../DependencyInjection/MediatorBuilder.cs | 213 +++ ...ediatorBuilderInstrumentationExtensions.cs | 55 + .../MediatorHostBuilder.cs | 12 + .../MediatorHostBuilderExtensions.cs | 173 +++ .../MediatorServiceCollectionExtensions.cs | 70 + .../DependencyInjection/MediatorSetup.cs | 6 + .../src/Mocha.Mediator/IMediatorContext.cs | 49 + .../src/Mocha.Mediator/IMediatorPools.cs | 15 + .../src/Mocha.Mediator/IMediatorRuntime.cs | 13 + .../ActivityMediatorDiagnosticListener.cs | 47 + .../AggregateMediatorDiagnosticEvents.cs | 43 + .../IMediatorDiagnosticEventListener.cs | 9 + .../IMediatorDiagnosticEvents.cs | 19 + .../MediatorDiagnosticEventListener.cs | 27 + .../MediatorDiagnosticMiddleware.cs | 35 + .../MochaMediatorActivitySource.cs | 14 + .../NoopMediatorDiagnosticEvents.cs | 11 + .../Instrumentation/SemanticConventions.cs | 27 + src/Mocha/src/Mocha.Mediator/Mediator.cs | 206 +++ .../src/Mocha.Mediator/MediatorContext.cs | 85 ++ .../src/Mocha.Mediator/MediatorContextPool.cs | 26 + .../src/Mocha.Mediator/MediatorDelegate.cs | 6 + .../src/Mocha.Mediator/MediatorMiddleware.cs | 7 + .../MediatorMiddlewareCompiler.cs | 49 + .../MediatorMiddlewareConfiguration.cs | 6 + .../MediatorMiddlewareFactoryContext.cs | 34 + ...iatorMiddlewareFactoryContextExtensions.cs | 73 + .../src/Mocha.Mediator/MediatorOptions.cs | 15 + src/Mocha/src/Mocha.Mediator/MediatorPools.cs | 8 + .../src/Mocha.Mediator/MediatorRuntime.cs | 86 ++ .../src/Mocha.Mediator/Mocha.Mediator.csproj | 24 + .../Pipeline/ForeachAwaitPublisher.cs | 50 + .../Pipeline/MediatorPipelineConfiguration.cs | 31 + .../Pipeline/NotificationStrategyFeature.cs | 9 + .../Pipeline/PipelineBuilder.cs | 164 ++ .../Pipeline/TaskWhenAllPublisher.cs | 54 + .../PoolingMediatorExtensions.cs | 19 + .../Buffers/ArrayMemoryOwner.cs | 0 .../Buffers/BufferPools.cs | 0 .../Buffers/IWritableMemory.cs | 0 .../Buffers/PooledArrayWriter.cs | 0 .../Buffers/ReadOnlyMemorySegment.cs | 0 .../Features/EmptyFeatureCollection.cs | 0 .../Features/FeatureCollection.cs | 0 .../Features/FeatureCollectionExtensions.cs | 0 .../Features/IPooledFeature.cs | 0 .../Features/ISealable.cs | 0 .../Features/PooledFeatureCollection.cs | 0 .../Features/ReadOnlyFeatureCollection.cs | 0 .../Mocha.Utilities/Mocha.Utilities.csproj | 17 + .../IMessageBusHostBuilderExtensions.cs | 10 +- src/Mocha/src/Mocha/Mocha.csproj | 1 + .../CommandHandlerGeneratorTests.cs | 80 + .../Mocha.Analyzers.Tests/DiagnosticTests.cs | 171 +++ .../ExplicitModuleNameTests.cs | 27 + .../GenericHandlerTests.cs | 87 ++ .../InternalHandlerTests.cs | 27 + .../KnownTypeSymbolsTests.cs | 116 ++ .../MediatorModuleTests.cs | 71 + .../MixedHandlerGeneratorTests.cs | 110 ++ .../Mocha.Analyzers.Tests.csproj | 33 + .../test/Mocha.Analyzers.Tests/ModuleInfo.cs | 3 + .../ModuleNameHelperTests.cs | 134 ++ .../NestedHandlerTests.cs | 28 + .../NotificationHandlerGeneratorTests.cs | 52 + .../PartialClassHandlerTests.cs | 102 ++ .../QueryHandlerGeneratorTests.cs | 63 + .../test/Mocha.Analyzers.Tests/TestHelper.cs | 178 +++ .../WarmUpGeneratorTests.cs | 55 + ...mandWithResponseHandler_MatchesSnapshot.md | 39 + ...MultipleCommandHandlers_MatchesSnapshot.md | 45 + ...rate_VoidCommandHandler_MatchesSnapshot.md | 38 + ...001_CommandWithNoHandler_ReportsWarning.md | 17 + ...O0001_QueryWithNoHandler_ReportsWarning.md | 17 + ...002_CommandWithTwoHandlers_ReportsError.md | 66 + ...VoidCommandWithTwoHandlers_ReportsError.md | 64 + ...s.MO0003_AbstractHandler_ReportsWarning.md | 28 + ...s.MO0004_OpenGenericCommand_ReportsInfo.md | 58 + ...sts.MO0004_OpenGenericQuery_ReportsInfo.md | 59 + ...Warning_CommandWithHandler_NoDiagnostic.md | 38 + ...rate_ModuleWithOnlyName_MatchesSnapshot.md | 38 + ...rate_GenericBaseHandler_MatchesSnapshot.md | 28 + ...pace_DeterministicOrder_MatchesSnapshot.md | 50 + ...rate_OpenGenericCommand_MatchesSnapshot.md | 59 + ...enerate_InternalHandler_MatchesSnapshot.md | 38 + ...olution_ICommandGeneric_MatchesSnapshot.md | 39 + ...ution_ICommandInterface_MatchesSnapshot.md | 38 + ...ypes_AllSymbolsResolved_MatchesSnapshot.md | 60 + ...gs_NoHandlersRegistered_MatchesSnapshot.md | 2 + ...ultAssemblyName_PrefixesWithLastSegment.md | 39 + ...rate_DottedAssemblyName_UsesLastSegment.md | 38 + ...ModuleFile_ContainsHandlerRegistrations.md | 39 + ...enerate_AllHandlerTypes_MatchesSnapshot.md | 61 + ...rsInDifferentNamespaces_MatchesSnapshot.md | 46 + ...sts.Generate_NoHandlers_MatchesSnapshot.md | 2 + ...sesLastSegmentSanitized_MatchesSnapshot.md | 38 + ...ame_UsesAssemblyDefault_MatchesSnapshot.md | 38 + ...rate_NestedClassHandler_MatchesSnapshot.md | 38 + ...lersForSameNotification_MatchesSnapshot.md | 39 + ...ngleNotificationHandler_MatchesSnapshot.md | 38 + ...ate_PartialClassHandler_MatchesSnapshot.md | 39 + ...ueryHandler_AcrossFiles_MatchesSnapshot.md | 39 + ...mandHandler_AcrossFiles_MatchesSnapshot.md | 38 + ...e_MultipleQueryHandlers_MatchesSnapshot.md | 46 + ...s.Generate_QueryHandler_MatchesSnapshot.md | 39 + ...hod_WithAllHandlerTypes_MatchesSnapshot.md | 60 + ...tityFrameworkTransactionMiddlewareTests.cs | 359 +++++ .../Mocha.EntityFrameworkCore.Tests.csproj | 13 + .../ContextPoolingTests.cs | 231 +++ .../InstrumentationTests.cs | 202 +++ .../MediatorDispatchTests.cs | 199 +++ .../MiddlewareFactoryContextTests.cs | 555 +++++++ .../MiddlewarePipelineTests.cs | 372 +++++ .../Mocha.Mediator.Tests.csproj | 12 + .../NamedMediatorTests.cs | 148 ++ .../NotificationStrategyTests.cs | 220 +++ .../test/Mocha.Mediator.Tests/TestSetup.cs | 222 +++ .../InMemoryTopologyConventionTests.cs | 26 +- website/src/docs/docs.json | 8 + website/src/docs/mocha/v1/index.md | 57 +- website/src/docs/mocha/v1/mediator/index.md | 535 +++++++ .../v1/mediator/pipeline-and-middleware.md | 573 +++++++ website/src/docs/mocha/v1/messages.md | 2 +- 250 files changed, 17286 insertions(+), 454 deletions(-) create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.ServiceCollectionExtensions.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/AssemblyReference.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/Mediator.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/MediatorOptions.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/MediatorOptionsAttribute.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.Switch.SourceGenerator/Mediator.Switch.SourceGenerator.SwitchMediatorSourceGenerator/BenchmarkSwitchMediator.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/BenchmarksMediatorBuilderExtensions._RXvJz6gQPQLNfJu4g_tRw.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/BenchmarksMediatorBuilderExtensions.g.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/DispatchPatternBenchmarks.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/LookupBenchmarks.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/MediatorSgRegistration.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PipelinePatternBenchmarks.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PoolingBenchmarks.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/BenchmarkSetup.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/CommandBenchmarks.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/ConcurrentPipelineBenchmarks.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/FullPipelineBenchmarks.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/FullPipelineMessages.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/ImmediateHandlersMessages.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MassTransitMessages.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MediatorSgHelper.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MediatorSgMessages.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/Messages.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/NotificationBenchmarks.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/PipelineBenchmarks.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/SwitchMediatorMessages.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/WolverineMessages.cs create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Mocha.Mediator.Benchmarks.csproj create mode 100644 src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Program.cs create mode 100644 src/Mocha/src/Demo/Demo.Billing/Commands/ProcessPaymentCommand.cs create mode 100644 src/Mocha/src/Demo/Demo.Billing/Queries/InvoiceQueries.cs create mode 100644 src/Mocha/src/Demo/Demo.Billing/Queries/PaymentQueries.cs create mode 100644 src/Mocha/src/Demo/Demo.Billing/Queries/RefundQueries.cs create mode 100644 src/Mocha/src/Demo/Demo.Billing/Queries/RevenueSummaryQueries.cs create mode 100644 src/Mocha/src/Demo/Demo.Catalog/Commands/InitiateReturnCommand.cs create mode 100644 src/Mocha/src/Demo/Demo.Catalog/Commands/PlaceBulkOrderCommand.cs create mode 100644 src/Mocha/src/Demo/Demo.Catalog/Commands/PlaceOrderCommand.cs create mode 100644 src/Mocha/src/Demo/Demo.Catalog/Commands/RequestQuickRefundCommand.cs create mode 100644 src/Mocha/src/Demo/Demo.Catalog/Queries/CategoryQueries.cs create mode 100644 src/Mocha/src/Demo/Demo.Catalog/Queries/OrderQueries.cs create mode 100644 src/Mocha/src/Demo/Demo.Catalog/Queries/ProductQueries.cs create mode 100644 src/Mocha/src/Demo/Demo.Shipping/Commands/ReceiveReturnPackageCommand.cs create mode 100644 src/Mocha/src/Demo/Demo.Shipping/Commands/ShipShipmentCommand.cs create mode 100644 src/Mocha/src/Demo/Demo.Shipping/Queries/ReturnShipmentQueries.cs create mode 100644 src/Mocha/src/Demo/Demo.Shipping/Queries/ShipmentQueries.cs create mode 100644 src/Mocha/src/Examples/MediatorShowcase/Generated/Microsoft.AspNetCore.App.SourceGenerators/Microsoft.AspNetCore.SourceGenerators.PublicProgramSourceGenerator/PublicTopLevelProgram.Generated.g.cs create mode 100644 src/Mocha/src/Examples/MediatorShowcase/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/MediatorShowcaseMediatorBuilderExtensions._5hYhW_IBO7W3L_MSoxCPw.g.cs create mode 100644 src/Mocha/src/Examples/MediatorShowcase/Handlers.cs create mode 100644 src/Mocha/src/Examples/MediatorShowcase/MediatorShowcase.cs create mode 100644 src/Mocha/src/Examples/MediatorShowcase/MediatorShowcase.csproj create mode 100644 src/Mocha/src/Examples/MediatorShowcase/PipelineBehaviors.cs create mode 100644 src/Mocha/src/Examples/MediatorShowcase/Properties/launchSettings.json create mode 100644 src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 src/Mocha/src/Mocha.Analyzers/Errors.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/FileBuilders/FileBuilderBase.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/FileBuilders/IFileBuilder.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Filters/AssemblyAttributeListFilter.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Filters/ClassWithMochaBaseListFilter.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Filters/ISyntaxFilter.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Filters/SyntaxFilterBuilder.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Generators/ISyntaxGenerator.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Inspectors/AbstractHandlerInspector.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Inspectors/ISyntaxInspector.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Inspectors/MediatorModuleInspector.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Inspectors/MessageTypeInspector.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Inspectors/NotificationHandlerInspector.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/IsExternalInit.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/KnownTypeSymbols.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj create mode 100644 src/Mocha/src/Mocha.Analyzers/Models/DiagnosticInfo.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Models/HandlerInfo.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Models/HandlerKind.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Models/LocationInfo.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Models/MediatorModuleInfo.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Models/MessageKind.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Models/MessageTypeInfo.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Models/NotificationHandlerInfo.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Models/SyntaxInfo.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/SyntaxConstants.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Utils/CodeWriter.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Utils/CodeWriterExtensions.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Utils/ImmutableEquatableArray.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Utils/ModuleNameHelper.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Utils/PooledObjects.cs create mode 100644 src/Mocha/src/Mocha.Analyzers/Utils/RoslynExtensions.cs rename src/Mocha/src/Mocha.EntityFrameworkCore/{EntityFrameworkCorePersistanceBuilderExtensions.cs => EntityFrameworkCorePersistenceBuilderExtensions.cs} (95%) create mode 100644 src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkTransactionFeature.cs create mode 100644 src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkTransactionMiddleware.cs create mode 100644 src/Mocha/src/Mocha.EntityFrameworkCore/MediatorBuilderEntityFrameworkExtensions.cs create mode 100644 src/Mocha/src/Mocha.EntityFrameworkCore/MediatorEntityFrameworkOptions.cs rename src/Mocha/src/Mocha.EntityFrameworkCore/Outbox/{OutboxEntityFrameworkCorePersistanceBuilderExtensions.cs => OutboxEntityFrameworkCorePersistenceBuilderExtensions.cs} (95%) create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/ICommand.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/ICommandHandler.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/IMediator.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/INotification.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/INotificationHandler.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/INotificationStrategy.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/IPublisher.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/IQuery.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/IQueryHandler.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/ISender.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/MediatorModuleAttribute.cs create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/Mocha.Mediator.Abstractions.csproj create mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/Unit.cs create mode 100644 src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorBuilder.cs create mode 100644 src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorHostBuilder.cs create mode 100644 src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs create mode 100644 src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilderInstrumentationExtensions.cs create mode 100644 src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilder.cs create mode 100644 src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderExtensions.cs create mode 100644 src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorServiceCollectionExtensions.cs create mode 100644 src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorSetup.cs create mode 100644 src/Mocha/src/Mocha.Mediator/IMediatorContext.cs create mode 100644 src/Mocha/src/Mocha.Mediator/IMediatorPools.cs create mode 100644 src/Mocha/src/Mocha.Mediator/IMediatorRuntime.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Instrumentation/ActivityMediatorDiagnosticListener.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Instrumentation/AggregateMediatorDiagnosticEvents.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Instrumentation/IMediatorDiagnosticEventListener.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Instrumentation/IMediatorDiagnosticEvents.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Instrumentation/MediatorDiagnosticEventListener.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Instrumentation/MediatorDiagnosticMiddleware.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Instrumentation/MochaMediatorActivitySource.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Instrumentation/NoopMediatorDiagnosticEvents.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Instrumentation/SemanticConventions.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Mediator.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorContext.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorContextPool.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorDelegate.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorMiddleware.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorMiddlewareCompiler.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorMiddlewareConfiguration.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContext.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorOptions.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorPools.cs create mode 100644 src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Mocha.Mediator.csproj create mode 100644 src/Mocha/src/Mocha.Mediator/Pipeline/ForeachAwaitPublisher.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Pipeline/MediatorPipelineConfiguration.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Pipeline/NotificationStrategyFeature.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Pipeline/PipelineBuilder.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Pipeline/TaskWhenAllPublisher.cs create mode 100644 src/Mocha/src/Mocha.Mediator/PoolingMediatorExtensions.cs rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Buffers/ArrayMemoryOwner.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Buffers/BufferPools.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Buffers/IWritableMemory.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Buffers/PooledArrayWriter.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Buffers/ReadOnlyMemorySegment.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Features/EmptyFeatureCollection.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Features/FeatureCollection.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Features/FeatureCollectionExtensions.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Features/IPooledFeature.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Features/ISealable.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Features/PooledFeatureCollection.cs (100%) rename src/Mocha/src/{Mocha/Utils => Mocha.Utilities}/Features/ReadOnlyFeatureCollection.cs (100%) create mode 100644 src/Mocha/src/Mocha.Utilities/Mocha.Utilities.csproj create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/CommandHandlerGeneratorTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/ExplicitModuleNameTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/GenericHandlerTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/InternalHandlerTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/KnownTypeSymbolsTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/MediatorModuleTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/MixedHandlerGeneratorTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/Mocha.Analyzers.Tests.csproj create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/ModuleInfo.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/ModuleNameHelperTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/NestedHandlerTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/NotificationHandlerGeneratorTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/PartialClassHandlerTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/QueryHandlerGeneratorTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/TestHelper.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/WarmUpGeneratorTests.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0001_CommandWithNoHandler_ReportsWarning.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0001_QueryWithNoHandler_ReportsWarning.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0003_AbstractHandler_ReportsWarning.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_GenericBaseHandler_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithoutMochaUsings_NoHandlersRegistered_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_NoHandlers_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md create mode 100644 src/Mocha/test/Mocha.EntityFrameworkCore.Tests/EntityFrameworkTransactionMiddlewareTests.cs create mode 100644 src/Mocha/test/Mocha.EntityFrameworkCore.Tests/Mocha.EntityFrameworkCore.Tests.csproj create mode 100644 src/Mocha/test/Mocha.Mediator.Tests/ContextPoolingTests.cs create mode 100644 src/Mocha/test/Mocha.Mediator.Tests/InstrumentationTests.cs create mode 100644 src/Mocha/test/Mocha.Mediator.Tests/MediatorDispatchTests.cs create mode 100644 src/Mocha/test/Mocha.Mediator.Tests/MiddlewareFactoryContextTests.cs create mode 100644 src/Mocha/test/Mocha.Mediator.Tests/MiddlewarePipelineTests.cs create mode 100644 src/Mocha/test/Mocha.Mediator.Tests/Mocha.Mediator.Tests.csproj create mode 100644 src/Mocha/test/Mocha.Mediator.Tests/NamedMediatorTests.cs create mode 100644 src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs create mode 100644 src/Mocha/test/Mocha.Mediator.Tests/TestSetup.cs create mode 100644 website/src/docs/mocha/v1/mediator/index.md create mode 100644 website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md diff --git a/src/All.slnx b/src/All.slnx index c38c23b472e..1c80c088655 100644 --- a/src/All.slnx +++ b/src/All.slnx @@ -347,6 +347,7 @@ + diff --git a/src/Mocha/Mocha.sln b/src/Mocha/Mocha.sln index 279aa211269..15e836210f4 100644 --- a/src/Mocha/Mocha.sln +++ b/src/Mocha/Mocha.sln @@ -57,6 +57,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo.ServiceDefaults", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo.Contracts", "src\Demo\Demo.Contracts\Demo.Contracts.csproj", "{58C302B9-4E15-447B-ACE3-867813189848}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.Mediator", "src\Mocha.Mediator\Mocha.Mediator.csproj", "{04AEA85E-50C1-46A6-9346-D3C02046A013}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.Analyzers", "src\Mocha.Analyzers\Mocha.Analyzers.csproj", "{F1866F0C-7638-4DEB-AF64-46189F727FA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.Analyzers.Tests", "test\Mocha.Analyzers.Tests\Mocha.Analyzers.Tests.csproj", "{C779F845-2F89-47C2-B993-1D2A1652C7FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.EntityFrameworkCore.Tests", "test\Mocha.EntityFrameworkCore.Tests\Mocha.EntityFrameworkCore.Tests.csproj", "{EBB56CB0-71DA-48A5-BB8D-26870E500467}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.Mediator.Abstractions", "src\Mocha.Mediator.Abstractions\Mocha.Mediator.Abstractions.csproj", "{074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.Utilities", "src\Mocha.Utilities\Mocha.Utilities.csproj", "{7C34B5D2-CDEC-483B-BDF7-9B414D97835E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -355,6 +367,78 @@ Global {58C302B9-4E15-447B-ACE3-867813189848}.Release|x64.Build.0 = Release|Any CPU {58C302B9-4E15-447B-ACE3-867813189848}.Release|x86.ActiveCfg = Release|Any CPU {58C302B9-4E15-447B-ACE3-867813189848}.Release|x86.Build.0 = Release|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Debug|x64.ActiveCfg = Debug|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Debug|x64.Build.0 = Debug|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Debug|x86.ActiveCfg = Debug|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Debug|x86.Build.0 = Debug|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Release|Any CPU.Build.0 = Release|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Release|x64.ActiveCfg = Release|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Release|x64.Build.0 = Release|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Release|x86.ActiveCfg = Release|Any CPU + {04AEA85E-50C1-46A6-9346-D3C02046A013}.Release|x86.Build.0 = Release|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Debug|x64.Build.0 = Debug|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Debug|x86.ActiveCfg = Debug|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Debug|x86.Build.0 = Debug|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Release|Any CPU.Build.0 = Release|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Release|x64.ActiveCfg = Release|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Release|x64.Build.0 = Release|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Release|x86.ActiveCfg = Release|Any CPU + {F1866F0C-7638-4DEB-AF64-46189F727FA2}.Release|x86.Build.0 = Release|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Debug|x64.ActiveCfg = Debug|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Debug|x64.Build.0 = Debug|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Debug|x86.ActiveCfg = Debug|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Debug|x86.Build.0 = Debug|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Release|Any CPU.Build.0 = Release|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Release|x64.ActiveCfg = Release|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Release|x64.Build.0 = Release|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Release|x86.ActiveCfg = Release|Any CPU + {C779F845-2F89-47C2-B993-1D2A1652C7FF}.Release|x86.Build.0 = Release|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Debug|x64.ActiveCfg = Debug|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Debug|x64.Build.0 = Debug|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Debug|x86.ActiveCfg = Debug|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Debug|x86.Build.0 = Debug|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Release|Any CPU.Build.0 = Release|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Release|x64.ActiveCfg = Release|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Release|x64.Build.0 = Release|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Release|x86.ActiveCfg = Release|Any CPU + {EBB56CB0-71DA-48A5-BB8D-26870E500467}.Release|x86.Build.0 = Release|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Debug|x64.Build.0 = Debug|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Debug|x86.Build.0 = Debug|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Release|Any CPU.Build.0 = Release|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Release|x64.ActiveCfg = Release|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Release|x64.Build.0 = Release|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Release|x86.ActiveCfg = Release|Any CPU + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3}.Release|x86.Build.0 = Release|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Debug|x64.Build.0 = Debug|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Debug|x86.Build.0 = Debug|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Release|Any CPU.Build.0 = Release|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Release|x64.ActiveCfg = Release|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Release|x64.Build.0 = Release|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Release|x86.ActiveCfg = Release|Any CPU + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -382,5 +466,11 @@ Global {105A06D4-58C3-4A37-A590-C766D313AC8F} = {FB64595D-7A02-F2D4-9C1E-6F343453585F} {046A5F8B-AF61-4C9C-A2E7-F965A3DF3AF6} = {FB64595D-7A02-F2D4-9C1E-6F343453585F} {58C302B9-4E15-447B-ACE3-867813189848} = {FB64595D-7A02-F2D4-9C1E-6F343453585F} + {04AEA85E-50C1-46A6-9346-D3C02046A013} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {F1866F0C-7638-4DEB-AF64-46189F727FA2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {C779F845-2F89-47C2-B993-1D2A1652C7FF} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {EBB56CB0-71DA-48A5-BB8D-26870E500467} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + {074FF3A5-43AE-416C-8A12-9AA56D4DC7A3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {7C34B5D2-CDEC-483B-BDF7-9B414D97835E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection EndGlobal diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.g.cs new file mode 100644 index 00000000000..d44e27c4f92 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.g.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.DependencyInjection; + +#pragma warning disable CS1591 + +namespace Mocha.Mediator.Benchmarks.Messaging; + +partial class ImmediateCommandHandler +{ + public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler + { + private readonly global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.HandleBehavior _handleBehavior; + + public Handler( + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.HandleBehavior handleBehavior + ) + { + var handlerType = typeof(ImmediateCommandHandler); + + _handleBehavior = handleBehavior; + + } + + public async global::System.Threading.Tasks.ValueTask HandleAsync( + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.Command request, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + return await _handleBehavior + .HandleAsync(request, cancellationToken) + .ConfigureAwait(false); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior + { + + public HandleBehavior( + ) + { + } + + public override async global::System.Threading.Tasks.ValueTask HandleAsync( + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.Command request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return await global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler + .HandleAsync( + request + , cancellationToken + ) + .ConfigureAwait(false); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static IServiceCollection AddHandlers( + IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped + ) + { + services.Add(new(typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.Handler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.Handler), lifetime)); + services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.Handler), lifetime)); + services.Add(new(typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.HandleBehavior), typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.HandleBehavior), lifetime)); + return services; + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.g.cs new file mode 100644 index 00000000000..ee4801c1fa9 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.g.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.DependencyInjection; + +#pragma warning disable CS1591 + +namespace Mocha.Mediator.Benchmarks.Messaging; + +partial class ImmediateFullPipelineCommandHandler +{ + public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler + { + private readonly global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.HandleBehavior _handleBehavior; + private readonly global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelinePostBehavior _immediateFullPipelinePostBehavior; + private readonly global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineMainBehavior _immediateFullPipelineMainBehavior; + private readonly global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelinePreBehavior _immediateFullPipelinePreBehavior; + + public Handler( + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.HandleBehavior handleBehavior, + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelinePostBehavior immediateFullPipelinePostBehavior, + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineMainBehavior immediateFullPipelineMainBehavior, + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelinePreBehavior immediateFullPipelinePreBehavior + ) + { + var handlerType = typeof(ImmediateFullPipelineCommandHandler); + + _handleBehavior = handleBehavior; + + _immediateFullPipelinePreBehavior = immediateFullPipelinePreBehavior; + _immediateFullPipelinePreBehavior.HandlerType = handlerType; + + _immediateFullPipelineMainBehavior = immediateFullPipelineMainBehavior; + _immediateFullPipelineMainBehavior.HandlerType = handlerType; + + _immediateFullPipelinePostBehavior = immediateFullPipelinePostBehavior; + _immediateFullPipelinePostBehavior.HandlerType = handlerType; + + _immediateFullPipelinePostBehavior.SetInnerHandler(_handleBehavior); + _immediateFullPipelineMainBehavior.SetInnerHandler(_immediateFullPipelinePostBehavior); + _immediateFullPipelinePreBehavior.SetInnerHandler(_immediateFullPipelineMainBehavior); + } + + public async global::System.Threading.Tasks.ValueTask HandleAsync( + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.Command request, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + return await _immediateFullPipelinePreBehavior + .HandleAsync(request, cancellationToken) + .ConfigureAwait(false); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior + { + + public HandleBehavior( + ) + { + } + + public override async global::System.Threading.Tasks.ValueTask HandleAsync( + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.Command request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return await global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler + .HandleAsync( + request + , cancellationToken + ) + .ConfigureAwait(false); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static IServiceCollection AddHandlers( + IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped + ) + { + services.Add(new(typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.Handler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.Handler), lifetime)); + services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.Handler), lifetime)); + services.Add(new(typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.HandleBehavior), typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.HandleBehavior), lifetime)); + return services; + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.g.cs new file mode 100644 index 00000000000..424dacfe6aa --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.g.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.DependencyInjection; + +#pragma warning disable CS1591 + +namespace Mocha.Mediator.Benchmarks.Messaging; + +partial class ImmediatePipelineCommandHandler +{ + public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler + { + private readonly global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.HandleBehavior _handleBehavior; + private readonly global::Mocha.Mediator.Benchmarks.Messaging.ImmediateBenchmarkBehavior _immediateBenchmarkBehavior; + + public Handler( + global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.HandleBehavior handleBehavior, + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateBenchmarkBehavior immediateBenchmarkBehavior + ) + { + var handlerType = typeof(ImmediatePipelineCommandHandler); + + _handleBehavior = handleBehavior; + + _immediateBenchmarkBehavior = immediateBenchmarkBehavior; + _immediateBenchmarkBehavior.HandlerType = handlerType; + + _immediateBenchmarkBehavior.SetInnerHandler(_handleBehavior); + } + + public async global::System.Threading.Tasks.ValueTask HandleAsync( + global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.Command request, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + return await _immediateBenchmarkBehavior + .HandleAsync(request, cancellationToken) + .ConfigureAwait(false); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior + { + + public HandleBehavior( + ) + { + } + + public override async global::System.Threading.Tasks.ValueTask HandleAsync( + global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.Command request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return await global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler + .HandleAsync( + request + , cancellationToken + ) + .ConfigureAwait(false); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static IServiceCollection AddHandlers( + IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped + ) + { + services.Add(new(typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.Handler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.Handler), lifetime)); + services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.Handler), lifetime)); + services.Add(new(typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.HandleBehavior), typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.HandleBehavior), lifetime)); + return services; + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.ServiceCollectionExtensions.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.ServiceCollectionExtensions.g.cs new file mode 100644 index 00000000000..aa172200091 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.ServiceCollectionExtensions.g.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +#pragma warning disable CS1591 + +namespace Mocha.Mediator.Benchmarks; + +public static class HandlerServiceCollectionExtensions +{ + public static IServiceCollection AddMochaMediatorBenchmarksBehaviors( + this IServiceCollection services) + { + services.TryAddTransient(typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelinePreBehavior<,>)); + services.TryAddTransient(typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineMainBehavior<,>)); + services.TryAddTransient(typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelinePostBehavior<,>)); + services.TryAddTransient(typeof(global::Mocha.Mediator.Benchmarks.Messaging.ImmediateBenchmarkBehavior<,>)); + + return services; + } + + public static IServiceCollection AddMochaMediatorBenchmarksHandlers( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped + ) + { + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateFullPipelineCommandHandler.AddHandlers(services, lifetime); + global::Mocha.Mediator.Benchmarks.Messaging.ImmediateCommandHandler.AddHandlers(services, lifetime); + global::Mocha.Mediator.Benchmarks.Messaging.ImmediatePipelineCommandHandler.AddHandlers(services, lifetime); + + return services; + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/AssemblyReference.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/AssemblyReference.g.cs new file mode 100644 index 00000000000..ab2d7cdbc69 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/AssemblyReference.g.cs @@ -0,0 +1,38 @@ +// +// Generated by the Mediator source generator. +// + +namespace Mediator +{ + /// + /// Represents an assembly reference. + /// This is used to specify the types or assemblies to scan for Mediator handlers. + /// + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + public sealed class AssemblyReference + { + /// + /// The assembly reference. + /// + public global::System.Reflection.Assembly Assembly { get; } + + private AssemblyReference(global::System.Reflection.Assembly assembly) + { + Assembly = assembly; + } + + /// + /// Creates a new instance of from the specified type. + /// + /// The type + /// A new instance of + public static implicit operator AssemblyReference(global::System.Type type) => new AssemblyReference(type.Assembly); + + /// + /// Creates a new instance of from the specified assembly. + /// + /// The assembly + /// A new instance of + public static implicit operator AssemblyReference(global::System.Reflection.Assembly assembly) => new AssemblyReference(assembly); + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/Mediator.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/Mediator.g.cs new file mode 100644 index 00000000000..2f4f8ae0958 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/Mediator.g.cs @@ -0,0 +1,1346 @@ +// +// Generated by the Mediator source generator. +// + +#pragma warning disable CS8019 // Unused usings +#pragma warning disable CS8321 // Unused local function +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Linq; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// DI extensions for Mediator. + /// + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public static class MediatorDependencyInjectionExtensions + { + /// + /// Adds the Mediator implementation and handlers of your application. + /// + public static IServiceCollection AddMediator(this IServiceCollection services) + { + return AddMediator(services, null); + } + + /// + /// Adds the Mediator implementation and handlers of your application, with specified options. + /// + public static IServiceCollection AddMediator(this IServiceCollection services, global::System.Action? options) + { + var opts = new global::Mediator.MediatorOptions(); + if (options != null) + options(opts); + + var configuredViaAttribute = false; + if (opts.ServiceLifetime != global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton && !configuredViaAttribute) + { + var errMsg = "Invalid configuration detected for Mediator. "; + errMsg += "Generated code for 'Singleton' lifetime, but got '" + opts.ServiceLifetime + "' lifetime from options. "; + errMsg += "This means that the source generator hasn't seen the 'AddMediator' method call during compilation. "; + errMsg += "Make sure that the 'AddMediator' method is called from the project that references the Mediator.SourceGenerator package."; + throw new global::System.Exception(errMsg); + } + + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.Mediator), typeof(global::Mediator.Mediator), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.TryAdd(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.IMediator), sp => sp.GetRequiredService(), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.TryAdd(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.ISender), sp => sp.GetRequiredService(), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.TryAdd(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.IPublisher), sp => sp.GetRequiredService(), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + + // Register handlers for request messages + services.TryAdd(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommandHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommandHandler), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.IRequestHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommandHandler), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.Internals.RequestHandlerWrapper), typeof(global::Mediator.Internals.RequestHandlerWrapper), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.TryAdd(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgCommandHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgCommandHandler), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.IRequestHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgCommandHandler), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.Internals.RequestHandlerWrapper), typeof(global::Mediator.Internals.RequestHandlerWrapper), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + + // Register handlers and wrappers for notification messages + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.Internals.NotificationHandlerWrapper), typeof(global::Mediator.Internals.NotificationHandlerWrapper), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + + // Register notification handlers + services.TryAdd(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgNotificationHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgNotificationHandler), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.INotificationHandler), GetRequiredService(), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + + // Register the notification publisher that was configured + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.ForeachAwaitPublisher), typeof(global::Mediator.ForeachAwaitPublisher), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.TryAdd(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.INotificationPublisher), sp => sp.GetRequiredService(), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + + // Register internal components + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.Internals.IContainerProbe), typeof(global::Mediator.Internals.ContainerProbe0), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.Internals.IContainerProbe), typeof(global::Mediator.Internals.ContainerProbe1), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mediator.Internals.ContainerMetadata), typeof(global::Mediator.Internals.ContainerMetadata), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + + return services; + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + static global::System.Func GetRequiredService() where T : notnull => sp => sp.GetRequiredService(); + } + } +} + +namespace Mediator.Internals +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + internal interface IMessageHandlerBase + { + global::System.Threading.Tasks.ValueTask Handle( + object request, + global::System.Threading.CancellationToken cancellationToken + ); + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + internal interface INotificationHandlerBase + { + global::System.Threading.Tasks.ValueTask Handle( + object notification, + global::System.Threading.CancellationToken cancellationToken + ); + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + internal interface IStreamMessageHandlerBase + { + global::System.Collections.Generic.IAsyncEnumerable Handle( + object request, + global::System.Threading.CancellationToken cancellationToken + ); + } + + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + internal interface IRequestHandlerBase : IMessageHandlerBase + { + global::System.Threading.Tasks.ValueTask Handle( + global::Mediator.IRequest request, + global::System.Threading.CancellationToken cancellationToken + ); + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + internal sealed class RequestHandlerWrapper : IRequestHandlerBase + where TRequest : global::Mediator.IRequest + { + private global::Mediator.MessageHandlerDelegate _rootHandler = null!; + + public RequestHandlerWrapper Init( + global::Mediator.Internals.ContainerMetadata containerMetadata, + global::System.IServiceProvider sp + ) + { + var concreteHandler = sp.GetRequiredService>(); + var pipelineBehaviours = sp.GetServices>(); + var handler = (global::Mediator.MessageHandlerDelegate)concreteHandler.Handle; + + global::Mediator.IPipelineBehavior[] pipelineBehavioursArray; + if (containerMetadata.ServicesUnderlyingTypeIsArray) + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is global::Mediator.IPipelineBehavior[] + ); + pipelineBehavioursArray = global::System.Runtime.CompilerServices.Unsafe.As[]>( + pipelineBehaviours + ); + } + else + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is not global::Mediator.IPipelineBehavior[] + ); + pipelineBehavioursArray = pipelineBehaviours.ToArray(); + } + + for (int i = pipelineBehavioursArray.Length - 1; i >= 0; i--) + { + var pipeline = pipelineBehavioursArray[i]; + var handlerCopy = handler; + var pipelineCopy = pipeline; + handler = (TRequest message, System.Threading.CancellationToken cancellationToken) => pipelineCopy.Handle(message, handlerCopy, cancellationToken); + } + + _rootHandler = handler; + return this; + } + + public global::System.Threading.Tasks.ValueTask Handle( + TRequest request, + global::System.Threading.CancellationToken cancellationToken + ) + { + var handler = _rootHandler; + return handler(request, cancellationToken); + } + + public global::System.Threading.Tasks.ValueTask Handle( + global::Mediator.IRequest request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return Handle((TRequest)request, cancellationToken); + } + + public async global::System.Threading.Tasks.ValueTask Handle( + object request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return await Handle((TRequest)request, cancellationToken); + } + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + internal interface IStreamRequestHandlerBase : IStreamMessageHandlerBase + { + global::System.Collections.Generic.IAsyncEnumerable Handle( + global::Mediator.IStreamRequest request, + global::System.Threading.CancellationToken cancellationToken + ); + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + internal sealed class StreamRequestHandlerWrapper : IStreamRequestHandlerBase + where TRequest : global::Mediator.IStreamRequest + { + private global::Mediator.StreamHandlerDelegate _rootHandler = null!; + + public StreamRequestHandlerWrapper Init( + global::Mediator.Internals.ContainerMetadata containerMetadata, + global::System.IServiceProvider sp + ) + { + var concreteHandler = sp.GetRequiredService>(); + var pipelineBehaviours = sp.GetServices>(); + var handler = (global::Mediator.StreamHandlerDelegate)concreteHandler.Handle; + + global::Mediator.IStreamPipelineBehavior[] pipelineBehavioursArray; + if (containerMetadata.ServicesUnderlyingTypeIsArray) + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is global::Mediator.IStreamPipelineBehavior[] + ); + pipelineBehavioursArray = global::System.Runtime.CompilerServices.Unsafe.As[]>( + pipelineBehaviours + ); + } + else + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is not global::Mediator.IStreamPipelineBehavior[] + ); + pipelineBehavioursArray = pipelineBehaviours.ToArray(); + } + + for (int i = pipelineBehavioursArray.Length - 1; i >= 0; i--) + { + var pipeline = pipelineBehavioursArray[i]; + var handlerCopy = handler; + var pipelineCopy = pipeline; + handler = (TRequest message, System.Threading.CancellationToken cancellationToken) => pipelineCopy.Handle(message, handlerCopy, cancellationToken); + } + + _rootHandler = handler; + return this; + } + + public global::System.Collections.Generic.IAsyncEnumerable Handle( + TRequest request, + global::System.Threading.CancellationToken cancellationToken + ) + { + var handler = _rootHandler; + return handler(request, cancellationToken); + } + + public global::System.Collections.Generic.IAsyncEnumerable Handle( + global::Mediator.IStreamRequest request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return Handle((TRequest)request, cancellationToken); + } + + public async global::System.Collections.Generic.IAsyncEnumerable Handle( + object request, + [global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken + ) + { + await foreach (var el in Handle((TRequest)request, cancellationToken)) + yield return el; + } + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + internal interface ICommandHandlerBase : IMessageHandlerBase + { + global::System.Threading.Tasks.ValueTask Handle( + global::Mediator.ICommand request, + global::System.Threading.CancellationToken cancellationToken + ); + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + internal sealed class CommandHandlerWrapper : ICommandHandlerBase + where TRequest : global::Mediator.ICommand + { + private global::Mediator.MessageHandlerDelegate _rootHandler = null!; + + public CommandHandlerWrapper Init( + global::Mediator.Internals.ContainerMetadata containerMetadata, + global::System.IServiceProvider sp + ) + { + var concreteHandler = sp.GetRequiredService>(); + var pipelineBehaviours = sp.GetServices>(); + var handler = (global::Mediator.MessageHandlerDelegate)concreteHandler.Handle; + + global::Mediator.IPipelineBehavior[] pipelineBehavioursArray; + if (containerMetadata.ServicesUnderlyingTypeIsArray) + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is global::Mediator.IPipelineBehavior[] + ); + pipelineBehavioursArray = global::System.Runtime.CompilerServices.Unsafe.As[]>( + pipelineBehaviours + ); + } + else + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is not global::Mediator.IPipelineBehavior[] + ); + pipelineBehavioursArray = pipelineBehaviours.ToArray(); + } + + for (int i = pipelineBehavioursArray.Length - 1; i >= 0; i--) + { + var pipeline = pipelineBehavioursArray[i]; + var handlerCopy = handler; + var pipelineCopy = pipeline; + handler = (TRequest message, System.Threading.CancellationToken cancellationToken) => pipelineCopy.Handle(message, handlerCopy, cancellationToken); + } + + _rootHandler = handler; + return this; + } + + public global::System.Threading.Tasks.ValueTask Handle( + TRequest request, + global::System.Threading.CancellationToken cancellationToken + ) + { + var handler = _rootHandler; + return handler(request, cancellationToken); + } + + public global::System.Threading.Tasks.ValueTask Handle( + global::Mediator.ICommand request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return Handle((TRequest)request, cancellationToken); + } + + public async global::System.Threading.Tasks.ValueTask Handle( + object request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return await Handle((TRequest)request, cancellationToken); + } + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + internal interface IStreamCommandHandlerBase : IStreamMessageHandlerBase + { + global::System.Collections.Generic.IAsyncEnumerable Handle( + global::Mediator.IStreamCommand request, + global::System.Threading.CancellationToken cancellationToken + ); + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + internal sealed class StreamCommandHandlerWrapper : IStreamCommandHandlerBase + where TRequest : global::Mediator.IStreamCommand + { + private global::Mediator.StreamHandlerDelegate _rootHandler = null!; + + public StreamCommandHandlerWrapper Init( + global::Mediator.Internals.ContainerMetadata containerMetadata, + global::System.IServiceProvider sp + ) + { + var concreteHandler = sp.GetRequiredService>(); + var pipelineBehaviours = sp.GetServices>(); + var handler = (global::Mediator.StreamHandlerDelegate)concreteHandler.Handle; + + global::Mediator.IStreamPipelineBehavior[] pipelineBehavioursArray; + if (containerMetadata.ServicesUnderlyingTypeIsArray) + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is global::Mediator.IStreamPipelineBehavior[] + ); + pipelineBehavioursArray = global::System.Runtime.CompilerServices.Unsafe.As[]>( + pipelineBehaviours + ); + } + else + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is not global::Mediator.IStreamPipelineBehavior[] + ); + pipelineBehavioursArray = pipelineBehaviours.ToArray(); + } + + for (int i = pipelineBehavioursArray.Length - 1; i >= 0; i--) + { + var pipeline = pipelineBehavioursArray[i]; + var handlerCopy = handler; + var pipelineCopy = pipeline; + handler = (TRequest message, System.Threading.CancellationToken cancellationToken) => pipelineCopy.Handle(message, handlerCopy, cancellationToken); + } + + _rootHandler = handler; + return this; + } + + public global::System.Collections.Generic.IAsyncEnumerable Handle( + TRequest request, + global::System.Threading.CancellationToken cancellationToken + ) + { + var handler = _rootHandler; + return handler(request, cancellationToken); + } + + public global::System.Collections.Generic.IAsyncEnumerable Handle( + global::Mediator.IStreamCommand request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return Handle((TRequest)request, cancellationToken); + } + + public async global::System.Collections.Generic.IAsyncEnumerable Handle( + object request, + [global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken + ) + { + await foreach (var el in Handle((TRequest)request, cancellationToken)) + yield return el; + } + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + internal interface IQueryHandlerBase : IMessageHandlerBase + { + global::System.Threading.Tasks.ValueTask Handle( + global::Mediator.IQuery request, + global::System.Threading.CancellationToken cancellationToken + ); + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + internal sealed class QueryHandlerWrapper : IQueryHandlerBase + where TRequest : global::Mediator.IQuery + { + private global::Mediator.MessageHandlerDelegate _rootHandler = null!; + + public QueryHandlerWrapper Init( + global::Mediator.Internals.ContainerMetadata containerMetadata, + global::System.IServiceProvider sp + ) + { + var concreteHandler = sp.GetRequiredService>(); + var pipelineBehaviours = sp.GetServices>(); + var handler = (global::Mediator.MessageHandlerDelegate)concreteHandler.Handle; + + global::Mediator.IPipelineBehavior[] pipelineBehavioursArray; + if (containerMetadata.ServicesUnderlyingTypeIsArray) + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is global::Mediator.IPipelineBehavior[] + ); + pipelineBehavioursArray = global::System.Runtime.CompilerServices.Unsafe.As[]>( + pipelineBehaviours + ); + } + else + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is not global::Mediator.IPipelineBehavior[] + ); + pipelineBehavioursArray = pipelineBehaviours.ToArray(); + } + + for (int i = pipelineBehavioursArray.Length - 1; i >= 0; i--) + { + var pipeline = pipelineBehavioursArray[i]; + var handlerCopy = handler; + var pipelineCopy = pipeline; + handler = (TRequest message, System.Threading.CancellationToken cancellationToken) => pipelineCopy.Handle(message, handlerCopy, cancellationToken); + } + + _rootHandler = handler; + return this; + } + + public global::System.Threading.Tasks.ValueTask Handle( + TRequest request, + global::System.Threading.CancellationToken cancellationToken + ) + { + var handler = _rootHandler; + return handler(request, cancellationToken); + } + + public global::System.Threading.Tasks.ValueTask Handle( + global::Mediator.IQuery request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return Handle((TRequest)request, cancellationToken); + } + + public async global::System.Threading.Tasks.ValueTask Handle( + object request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return await Handle((TRequest)request, cancellationToken); + } + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + internal interface IStreamQueryHandlerBase : IStreamMessageHandlerBase + { + global::System.Collections.Generic.IAsyncEnumerable Handle( + global::Mediator.IStreamQuery request, + global::System.Threading.CancellationToken cancellationToken + ); + } + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + internal sealed class StreamQueryHandlerWrapper : IStreamQueryHandlerBase + where TRequest : global::Mediator.IStreamQuery + { + private global::Mediator.StreamHandlerDelegate _rootHandler = null!; + + public StreamQueryHandlerWrapper Init( + global::Mediator.Internals.ContainerMetadata containerMetadata, + global::System.IServiceProvider sp + ) + { + var concreteHandler = sp.GetRequiredService>(); + var pipelineBehaviours = sp.GetServices>(); + var handler = (global::Mediator.StreamHandlerDelegate)concreteHandler.Handle; + + global::Mediator.IStreamPipelineBehavior[] pipelineBehavioursArray; + if (containerMetadata.ServicesUnderlyingTypeIsArray) + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is global::Mediator.IStreamPipelineBehavior[] + ); + pipelineBehavioursArray = global::System.Runtime.CompilerServices.Unsafe.As[]>( + pipelineBehaviours + ); + } + else + { + global::System.Diagnostics.Debug.Assert( + pipelineBehaviours is not global::Mediator.IStreamPipelineBehavior[] + ); + pipelineBehavioursArray = pipelineBehaviours.ToArray(); + } + + for (int i = pipelineBehavioursArray.Length - 1; i >= 0; i--) + { + var pipeline = pipelineBehavioursArray[i]; + var handlerCopy = handler; + var pipelineCopy = pipeline; + handler = (TRequest message, System.Threading.CancellationToken cancellationToken) => pipelineCopy.Handle(message, handlerCopy, cancellationToken); + } + + _rootHandler = handler; + return this; + } + + public global::System.Collections.Generic.IAsyncEnumerable Handle( + TRequest request, + global::System.Threading.CancellationToken cancellationToken + ) + { + var handler = _rootHandler; + return handler(request, cancellationToken); + } + + public global::System.Collections.Generic.IAsyncEnumerable Handle( + global::Mediator.IStreamQuery request, + global::System.Threading.CancellationToken cancellationToken + ) + { + return Handle((TRequest)request, cancellationToken); + } + + public async global::System.Collections.Generic.IAsyncEnumerable Handle( + object request, + [global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken + ) + { + await foreach (var el in Handle((TRequest)request, cancellationToken)) + yield return el; + } + } + + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + internal sealed class NotificationHandlerWrapper : INotificationHandlerBase + where TNotification : global::Mediator.INotification + { + private global::Mediator.ForeachAwaitPublisher _publisher = null!; + private global::Mediator.INotificationHandler[] _handlers = null!; + + public NotificationHandlerWrapper Init( + global::Mediator.Internals.ContainerMetadata containerMetadata, + global::System.IServiceProvider sp + ) + { + _publisher = containerMetadata.NotificationPublisher; + var handlers = sp.GetServices>(); + if (containerMetadata.ServicesUnderlyingTypeIsArray) + { + global::System.Diagnostics.Debug.Assert( + handlers is global::Mediator.INotificationHandler[], + $"Unexpected type: {handlers.GetType()}" + ); + _handlers = global::System.Runtime.CompilerServices.Unsafe.As[]>( + handlers + ); + } + else + { + global::System.Diagnostics.Debug.Assert( + handlers is not global::Mediator.INotificationHandler[], + $"Unexpected type: {handlers.GetType()}" + ); + _handlers = handlers.ToArray(); + } + return this; + } + + public global::System.Threading.Tasks.ValueTask Handle( + TNotification notification, + global::System.Threading.CancellationToken cancellationToken + ) + { + var handlers = _handlers; + if (handlers.Length == 0) + { + return default; + } + return _publisher.Publish( + new global::Mediator.NotificationHandlers(handlers, isArray: true), + notification, + cancellationToken + ); + } + + public global::System.Threading.Tasks.ValueTask Handle( + object notification, + global::System.Threading.CancellationToken cancellationToken + ) + { + return Handle((TNotification)notification, cancellationToken); + } + } + + internal interface IContainerProbe { } + internal sealed class ContainerProbe0 : IContainerProbe { } + internal sealed class ContainerProbe1 : IContainerProbe { } + + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + internal sealed class ContainerMetadata + { + public readonly bool ServicesUnderlyingTypeIsArray; + + public readonly global::System.Collections.Frozen.FrozenDictionary RequestHandlerWrappers; + public readonly global::System.Collections.Frozen.FrozenDictionary CommandHandlerWrappers; + public readonly global::System.Collections.Frozen.FrozenDictionary QueryHandlerWrappers; + + public readonly global::System.Collections.Frozen.FrozenDictionary StreamRequestHandlerWrappers; + public readonly global::System.Collections.Frozen.FrozenDictionary StreamCommandHandlerWrappers; + public readonly global::System.Collections.Frozen.FrozenDictionary StreamQueryHandlerWrappers; + + public readonly global::System.Collections.Frozen.FrozenDictionary NotificationHandlerWrappers; + + public readonly global::Mediator.Internals.RequestHandlerWrapper Wrapper_For_Mocha_Mediator_Benchmarks_Messaging_FullPipelineMediatorSgCommand; + public readonly global::Mediator.Internals.RequestHandlerWrapper Wrapper_For_Mocha_Mediator_Benchmarks_Messaging_MediatorSgCommand; + + public readonly global::Mediator.Internals.NotificationHandlerWrapper Wrapper_For_Mocha_Mediator_Benchmarks_Messaging_MediatorSgNotification; + + public readonly global::Mediator.ForeachAwaitPublisher NotificationPublisher; + + public ContainerMetadata(global::System.IServiceProvider sp) + { + ServicesUnderlyingTypeIsArray = sp.GetServices() is global::Mediator.Internals.IContainerProbe[]; + + NotificationPublisher = sp.GetRequiredService(); + + var requestHandlerTypes = new global::System.Collections.Generic.Dictionary(2); + var commandHandlerTypes = new global::System.Collections.Generic.Dictionary(0); + var queryHandlerTypes = new global::System.Collections.Generic.Dictionary(0); + requestHandlerTypes.Add(typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommand), sp.GetRequiredService>().Init(this, sp)); + requestHandlerTypes.Add(typeof(global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgCommand), sp.GetRequiredService>().Init(this, sp)); + RequestHandlerWrappers = global::System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary(requestHandlerTypes); + CommandHandlerWrappers = global::System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary(commandHandlerTypes); + QueryHandlerWrappers = global::System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary(queryHandlerTypes); + + var streamRequestHandlerTypes = new global::System.Collections.Generic.Dictionary(0); + var streamCommandHandlerTypes = new global::System.Collections.Generic.Dictionary(0); + var streamQueryHandlerTypes = new global::System.Collections.Generic.Dictionary(0); + StreamRequestHandlerWrappers = global::System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary(streamRequestHandlerTypes); + StreamCommandHandlerWrappers = global::System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary(streamCommandHandlerTypes); + StreamQueryHandlerWrappers = global::System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary(streamQueryHandlerTypes); + + var notificationHandlerTypes = new global::System.Collections.Generic.Dictionary(1); + notificationHandlerTypes.Add(typeof(global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgNotification), sp.GetRequiredService>().Init(this, sp)); + NotificationHandlerWrappers = global::System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary(notificationHandlerTypes); + + Wrapper_For_Mocha_Mediator_Benchmarks_Messaging_FullPipelineMediatorSgCommand = sp.GetRequiredService>().Init(this, sp); + Wrapper_For_Mocha_Mediator_Benchmarks_Messaging_MediatorSgCommand = sp.GetRequiredService>().Init(this, sp); + + Wrapper_For_Mocha_Mediator_Benchmarks_Messaging_MediatorSgNotification = sp.GetRequiredService>().Init(this, sp); + } + } +} + +namespace Mediator +{ + /// + /// Generated code for Mediator implementation. + /// This type is also registered as a DI service. + /// Can be used directly for high performance scenarios. + /// + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.Diagnostics.DebuggerStepThroughAttribute] + public sealed partial class Mediator : global::Mediator.IMediator, global::Mediator.ISender, global::Mediator.IPublisher + { + internal readonly global::System.IServiceProvider Services; + private FastLazyValue _containerMetadata; + private global::Mediator.ForeachAwaitPublisher? _notificationPublisher; + internal global::Mediator.ForeachAwaitPublisher NotificationPublisher + { + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get + { + if (_notificationPublisher == null) + _notificationPublisher = _containerMetadata.Value.NotificationPublisher; + return _notificationPublisher!; + } + } + private bool? _servicesUnderlyingTypeIsArray; + internal bool ServicesUnderlyingTypeIsArray + { + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get + { + if (_servicesUnderlyingTypeIsArray == null) + _servicesUnderlyingTypeIsArray = _containerMetadata.Value.ServicesUnderlyingTypeIsArray; + return _servicesUnderlyingTypeIsArray!.Value; + } + } + + /// + /// The lifetime of Mediator-related service registrations in DI container. + /// + public const global::Microsoft.Extensions.DependencyInjection.ServiceLifetime ServiceLifetime = global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton; + + /// + /// The name of the notification publisher service that was configured. + /// + public const string NotificationPublisherName = "ForeachAwaitPublisher"; + + /// + /// The total number of Mediator messages that were discovered. + /// + public const int TotalMessages = 3; + + /// + /// Constructor for DI, should not be used by consumer. + /// + public Mediator(global::System.IServiceProvider sp) + { + Services = sp; + _containerMetadata = new FastLazyValue( + self => self.Services.GetRequiredService(), + this + ); + } + + private struct FastLazyValue + { + private const long UNINIT = 0; + private const long INITING = 1; + private const long INITD = 2; + + private global::System.Func _generator; + private long _state; + private T _value; + private TArg _arg; + + public T Value + { + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get + { + if (_state != INITD) + return ValueSlow; + + return _value; + } + } + + private T ValueSlow + { + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + get + { + var prevState = global::System.Threading.Interlocked.CompareExchange(ref _state, INITING, UNINIT); + switch (prevState) + { + case INITD: + // Someone has already completed init + return _value; + case INITING: + // Wait for someone else to complete + var spinWait = default(global::System.Threading.SpinWait); + while (global::System.Threading.Interlocked.Read(ref _state) < INITD) + spinWait.SpinOnce(); + return _value; + case UNINIT: + _value = _generator(_arg); + global::System.Threading.Interlocked.Exchange(ref _state, INITD); + return _value; + } + + return _value; + } + } + + public FastLazyValue(global::System.Func generator, TArg arg) + { + _generator = generator; + _state = UNINIT; + _value = default!; + _arg = arg; + } + } + + + + /// + /// Send a request of type global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommand. + /// Throws if request is null. + /// + /// Incoming request + /// Cancellation token + /// Awaitable task + public global::System.Threading.Tasks.ValueTask Send( + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommand request, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowIfNull(request, nameof(request)); + return _containerMetadata.Value.Wrapper_For_Mocha_Mediator_Benchmarks_Messaging_FullPipelineMediatorSgCommand.Handle(request, cancellationToken); + } + + /// + /// Send a request of type global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgCommand. + /// Throws if request is null. + /// + /// Incoming request + /// Cancellation token + /// Awaitable task + public global::System.Threading.Tasks.ValueTask Send( + global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgCommand request, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowIfNull(request, nameof(request)); + return _containerMetadata.Value.Wrapper_For_Mocha_Mediator_Benchmarks_Messaging_MediatorSgCommand.Handle(request, cancellationToken); + } + + /// + /// Send request. + /// Throws if message is null. + /// Throws if request does not implement . + /// Throws if no handler is registered. + /// + /// Incoming request + /// Cancellation token + /// Awaitable task + public global::System.Threading.Tasks.ValueTask Send( + global::Mediator.IRequest request, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + switch (request) + { + case global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommand r: + { + if (typeof(TResponse) == typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse)) + { + var task = Send(r, cancellationToken); + return global::System.Runtime.CompilerServices.Unsafe.As, global::System.Threading.Tasks.ValueTask>(ref task); + } + return SendAsync(request, cancellationToken); + } + case global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgCommand r: + { + if (typeof(TResponse) == typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse)) + { + var task = Send(r, cancellationToken); + return global::System.Runtime.CompilerServices.Unsafe.As, global::System.Threading.Tasks.ValueTask>(ref task); + } + return SendAsync(request, cancellationToken); + } + default: + { + ThrowInvalidRequest(request, nameof(request)); + return default; + } + } + } + + private async global::System.Threading.Tasks.ValueTask SendAsync( + global::Mediator.IRequest request, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + switch (request) + { + case global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommand r: + { + var response = await Send(r, cancellationToken); + return global::System.Runtime.CompilerServices.Unsafe.As(ref response); + } + case global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgCommand r: + { + var response = await Send(r, cancellationToken); + return global::System.Runtime.CompilerServices.Unsafe.As(ref response); + } + default: + { + ThrowInvalidRequest(request, nameof(request)); + return default!; + } + } + } + + /// + /// Create stream for request. + /// Throws if message is null. + /// Throws if request does not implement . + /// Throws if no handler is registered. + /// + /// Incoming message + /// Cancellation token + /// Async enumerable + public global::System.Collections.Generic.IAsyncEnumerable CreateStream( + global::Mediator.IStreamRequest request, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowInvalidStreamRequest(request, nameof(request)); + return default!; + } + + /// + /// Send command. + /// Throws if message is null. + /// Throws if command does not implement . + /// Throws if no handler is registered. + /// + /// Incoming command + /// Cancellation token + /// Awaitable task + public global::System.Threading.Tasks.ValueTask Send( + global::Mediator.ICommand command, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowInvalidCommand(command, nameof(command)); + return default; + } + + private async global::System.Threading.Tasks.ValueTask SendAsync( + global::Mediator.ICommand command, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowInvalidCommand(command, nameof(command)); + return default!; + } + + /// + /// Create stream for command. + /// Throws if message is null. + /// Throws if command does not implement . + /// Throws if no handler is registered. + /// + /// Incoming message + /// Cancellation token + /// Async enumerable + public global::System.Collections.Generic.IAsyncEnumerable CreateStream( + global::Mediator.IStreamCommand command, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowInvalidStreamCommand(command, nameof(command)); + return default!; + } + + /// + /// Send query. + /// Throws if message is null. + /// Throws if query does not implement . + /// Throws if no handler is registered. + /// + /// Incoming query + /// Cancellation token + /// Awaitable task + public global::System.Threading.Tasks.ValueTask Send( + global::Mediator.IQuery query, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowInvalidQuery(query, nameof(query)); + return default; + } + + private async global::System.Threading.Tasks.ValueTask SendAsync( + global::Mediator.IQuery query, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowInvalidQuery(query, nameof(query)); + return default!; + } + + /// + /// Create stream for query. + /// Throws if message is null. + /// Throws if query does not implement . + /// Throws if no handler is registered. + /// + /// Incoming message + /// Cancellation token + /// Async enumerable + public global::System.Collections.Generic.IAsyncEnumerable CreateStream( + global::Mediator.IStreamQuery query, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowInvalidStreamQuery(query, nameof(query)); + return default!; + } + + /// + /// Send message. + /// Throws if message is null. + /// Throws if message does not implement . + /// Throws if no handler is registered. + /// + /// Incoming message + /// Cancellation token + /// Awaitable task + public async global::System.Threading.Tasks.ValueTask Send( + object message, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + switch (message) + { + case global::Mediator.IBaseRequest request: + switch (request) + { + case global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommand r: return await Send(r, cancellationToken); + case global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgCommand r: return await Send(r, cancellationToken); + default: + { + ThrowInvalidRequest(request, nameof(request)); + return default; + } + } + case global::Mediator.IBaseCommand command: + switch (command) + { + default: + { + ThrowInvalidCommand(command, nameof(command)); + return default; + } + } + case global::Mediator.IBaseQuery query: + switch (query) + { + default: + { + ThrowInvalidQuery(query, nameof(query)); + return default; + } + } + default: + ThrowInvalidMessage(message, nameof(message)); + return default!; + } + } + + /// + /// Create stream. + /// Throws if message is null. + /// Throws if message does not implement . + /// Throws if no handler is registered. + /// + /// Incoming message + /// Cancellation token + /// Async enumerable + public global::System.Collections.Generic.IAsyncEnumerable CreateStream( + object message, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowInvalidStreamMessage(message, nameof(message)); + return default!; + } + + /// + /// Publish notification. + /// Throws if message is null. + /// Throws if notification does not implement . + /// Throws if handlers throw exception(s). + /// Drops messages + /// + /// Incoming notification + /// Cancellation token + /// Awaitable task + public global::System.Threading.Tasks.ValueTask Publish( + object notification, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + switch (notification) + { + case global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgNotification n: return Publish(n, cancellationToken); + default: + { + ThrowInvalidNotification(notification, nameof(notification)); + return default; + } + } + } + + /// + /// Send a notification of type global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgNotification. + /// Throws if message is null. + /// Throws if handlers throw exception(s). + /// + /// Incoming message + /// Cancellation token + /// Awaitable task + public global::System.Threading.Tasks.ValueTask Publish( + global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgNotification notification, + global::System.Threading.CancellationToken cancellationToken = default + ) + { + ThrowIfNull(notification, nameof(notification)); + + return _containerMetadata.Value.Wrapper_For_Mocha_Mediator_Benchmarks_Messaging_MediatorSgNotification.Handle(notification, cancellationToken); + } + + /// + /// Publish notification. + /// Throws if message is null. + /// Throws if notification does not implement . + /// Throws if handlers throw exception(s). + /// + /// Incoming notification + /// Cancellation token + /// Awaitable task + public global::System.Threading.Tasks.ValueTask Publish( + TNotification notification, + global::System.Threading.CancellationToken cancellationToken = default + ) + where TNotification : global::Mediator.INotification + { + switch (notification) + { + case global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgNotification n: return Publish(n, cancellationToken); + default: + { + ThrowInvalidNotification(notification, nameof(notification)); + return default; + } + } + } + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowMissingHandler(object msg) => + throw new global::Mediator.MissingMessageHandlerException(msg); + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowInvalidMessage(T? msg, string? paramName = null) + { + if (msg == null) + ThrowArgumentNull(paramName); + else if (!(msg is global::Mediator.IMessage)) + ThrowInvalidMessage(msg); + else + ThrowMissingHandler(msg); + } + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowInvalidRequest(T? msg, string? paramName = null) + { + if (msg == null) + ThrowArgumentNull(paramName); + else if (!(msg is global::Mediator.IBaseRequest)) + ThrowInvalidMessage(msg); + else + ThrowMissingHandler(msg); + } + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowInvalidCommand(T? msg, string? paramName = null) + { + if (msg == null) + ThrowArgumentNull(paramName); + else if (!(msg is global::Mediator.IBaseCommand)) + ThrowInvalidMessage(msg); + else + ThrowMissingHandler(msg); + } + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowInvalidQuery(T? msg, string? paramName = null) + { + if (msg == null) + ThrowArgumentNull(paramName); + else if (!(msg is global::Mediator.IBaseQuery)) + ThrowInvalidMessage(msg); + else + ThrowMissingHandler(msg); + } + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowInvalidStreamMessage(T? msg, string? paramName = null) + { + if (msg == null) + ThrowArgumentNull(paramName); + else if (!(msg is global::Mediator.IStreamMessage)) + ThrowInvalidMessage(msg); + else + ThrowMissingHandler(msg); + } + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowInvalidStreamRequest(T? msg, string? paramName = null) + { + if (msg == null) + ThrowArgumentNull(paramName); + else if (!(msg is global::Mediator.IBaseStreamRequest)) + ThrowInvalidMessage(msg); + else + ThrowMissingHandler(msg); + } + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowInvalidStreamCommand(T? msg, string? paramName = null) + { + if (msg == null) + ThrowArgumentNull(paramName); + else if (!(msg is global::Mediator.IBaseStreamCommand)) + ThrowInvalidMessage(msg); + else + ThrowMissingHandler(msg); + } + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowInvalidStreamQuery(T? msg, string? paramName = null) + { + if (msg == null) + ThrowArgumentNull(paramName); + else if (!(msg is global::Mediator.IBaseStreamQuery)) + ThrowInvalidMessage(msg); + else + ThrowMissingHandler(msg); + } + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowArgumentNull(string? paramName) => + throw new global::System.ArgumentNullException(paramName); + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowInvalidMessage(T msg) => + throw new global::Mediator.InvalidMessageException(msg); + + private static void ThrowIfNull(T? argument, string paramName) + { + if (argument == null) + ThrowArgumentNull(paramName); + } + + private static void ThrowInvalidNotification(T? argument, string paramName) + { + if (argument == null) + ThrowArgumentNull(paramName); + else if (!(argument is global::Mediator.INotification)) + ThrowInvalidMessage(argument); + } + +#if NETSTANDARD2_1_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DoesNotReturn] +#endif + private static void ThrowAggregateException(global::System.Collections.Generic.List exceptions) => + throw new global::System.AggregateException(exceptions); + + private static void MaybeThrowAggregateException(global::System.Collections.Generic.List? exceptions) + { + if (exceptions != null) + { + ThrowAggregateException(exceptions); + } + } + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/MediatorOptions.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/MediatorOptions.g.cs new file mode 100644 index 00000000000..309c180ee53 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/MediatorOptions.g.cs @@ -0,0 +1,55 @@ +// +// Generated by the Mediator source generator. +// + +namespace Mediator +{ + /// + /// Provide options for the Mediator source generator. + /// + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + public sealed class MediatorOptions + { + /// + /// The namespace in which the Mediator implementation is generated. + /// By default, the namespace is "Mediator". + /// + public string Namespace { get; set; } = "Mediator"; + + /// + /// Wether or not generated types should be internal (they are public by default). + /// + public bool GenerateTypesAsInternal { get; set; } = false; + + /// + /// The type to use when publishing notifications. + /// By default, the type is . + /// + public global::System.Type NotificationPublisherType { get; set; } = typeof(global::Mediator.ForeachAwaitPublisher); + + /// + /// The default lifetime of the services registered in the DI container by the Mediator source generator. + /// By default, the lifetime is . + /// + public global::Microsoft.Extensions.DependencyInjection.ServiceLifetime ServiceLifetime { get; set; } = + global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton; + + /// + /// The collection of assemblies to scan for Mediator handlers. + /// By default, the collection is empty, in which case the source generator will scan all assemblies through references from the source generated project. + /// + public global::System.Collections.Generic.IReadOnlyList Assemblies { get; set; } = new global::Mediator.AssemblyReference[0]; + + /// + /// The collection of types of pipeline behaviors to register in DI. + /// When the type is an unconstructed generic type, the source generator will register all the constructed types of the generic type (open generics that is supported during AoT). + /// + public global::System.Collections.Generic.IReadOnlyList PipelineBehaviors { get; set; } = new global::System.Type[0]; + + /// + /// The collection of types of streaming pipeline behaviors to register in DI. + /// When the type is an unconstructed generic type, the source generator will register all the constructed types of the generic type (open generics that is supported during AoT). + /// + public global::System.Collections.Generic.IReadOnlyList StreamPipelineBehaviors { get; set; } = new global::System.Type[0]; + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/MediatorOptionsAttribute.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/MediatorOptionsAttribute.g.cs new file mode 100644 index 00000000000..8ecee2a6d3c --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.SourceGenerator/Mediator.SourceGenerator.IncrementalMediatorGenerator/MediatorOptionsAttribute.g.cs @@ -0,0 +1,33 @@ +// +// Generated by the Mediator source generator. +// + +namespace Mediator +{ + /// + /// Provide options for the Mediator source generator. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Assembly, AllowMultiple = false)] + [global::System.CodeDom.Compiler.GeneratedCode("Mediator.SourceGenerator", "3.0.0.0")] + public sealed class MediatorOptionsAttribute : global::System.Attribute + { + /// + /// The namespace in which the Mediator implementation is generated. + /// By default, the namespace is "Mediator". + /// + public string Namespace { get; set; } = "Mediator"; + + /// + /// The type to use when publishing notifications. + /// By default, the type is . + /// + public global::System.Type NotificationPublisherType { get; set; } = typeof(global::Mediator.ForeachAwaitPublisher); + + /// + /// The default lifetime of the services registered in the DI container by the Mediator source generator. + /// By default, the lifetime is . + /// + public global::Microsoft.Extensions.DependencyInjection.ServiceLifetime ServiceLifetime { get; set; } = + global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton; + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.Switch.SourceGenerator/Mediator.Switch.SourceGenerator.SwitchMediatorSourceGenerator/BenchmarkSwitchMediator.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.Switch.SourceGenerator/Mediator.Switch.SourceGenerator.SwitchMediatorSourceGenerator/BenchmarkSwitchMediator.g.cs new file mode 100644 index 00000000000..b0d115d06ea --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mediator.Switch.SourceGenerator/Mediator.Switch.SourceGenerator.SwitchMediatorSourceGenerator/BenchmarkSwitchMediator.g.cs @@ -0,0 +1,243 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by SwitchMediator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif + +namespace Mocha.Mediator.Benchmarks.Messaging; + +#pragma warning disable CS1998 + +public partial class BenchmarkSwitchMediator : global::Mediator.Switch.IMediator, global::Mediator.Switch.IValueMediator +{ + #region Fields + + private global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommandHandler? _mocha_Mediator_Benchmarks_Messaging_FullPipelineSwitchMediatorCommandHandler; + private global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorCommandHandler? _mocha_Mediator_Benchmarks_Messaging_SwitchMediatorCommandHandler; + private global::System.Collections.Generic.IEnumerable>? _mocha_Mediator_Benchmarks_Messaging_SwitchMediatorNotification__ValueHandlers; + + private readonly global::Mediator.Switch.ISwitchMediatorServiceProvider _svc; + + #endregion + + #region Constructor + + public BenchmarkSwitchMediator(global::Mediator.Switch.ISwitchMediatorServiceProvider serviceProvider) + { + _svc = serviceProvider; + } + + #endregion + + public static (global::System.Collections.Generic.IReadOnlyList RequestHandlerTypes, global::System.Collections.Generic.IReadOnlyList<(global::System.Type NotificationType, global::System.Collections.Generic.IReadOnlyList HandlerTypes)> NotificationTypes, global::System.Collections.Generic.IReadOnlyList PipelineBehaviorTypes) KnownTypes + { + get { return (SwitchMediatorKnownTypes.RequestHandlerTypes, SwitchMediatorKnownTypes.NotificationTypes, SwitchMediatorKnownTypes.PipelineBehaviorTypes); } + } + + public global::System.Threading.Tasks.Task Send(global::Mediator.Switch.IRequest request, global::System.Threading.CancellationToken cancellationToken = default) + { + if (TaskSendSwitchCase.Cases.TryGetValue(request.GetType(), out var handle)) + { + return global::System.Runtime.CompilerServices.Unsafe.As, global::System.Threading.CancellationToken, global::System.Threading.Tasks.Task>>(handle)(this, request, cancellationToken); + } + + throw new global::System.ArgumentException($"No handler for {request.GetType().Name}"); + } + + private static class TaskSendSwitchCase + { + public static readonly global::System.Collections.Generic.IDictionary Cases = new (global::System.Type, object)[] + { + ( // case Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommand: + typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommand), (global::System.Func, global::System.Threading.CancellationToken, global::System.Threading.Tasks.Task>) ((instance, request, cancellationToken) => + instance.Handle_Mocha_Mediator_Benchmarks_Messaging_FullPipelineSwitchMediatorCommand( + (global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommand) request, cancellationToken).AsTask()) + ), + ( // case Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorCommand: + typeof(global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorCommand), (global::System.Func, global::System.Threading.CancellationToken, global::System.Threading.Tasks.Task>) ((instance, request, cancellationToken) => + instance.Handle_Mocha_Mediator_Benchmarks_Messaging_SwitchMediatorCommand( + (global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorCommand) request, cancellationToken).AsTask()) + ) + } +#if NET8_0_OR_GREATER + .ToFrozenDictionary +#else + .ToDictionary +#endif + (t => t.Item1, t => t.Item2); + } + + global::System.Threading.Tasks.ValueTask global::Mediator.Switch.IValueSender.Send(global::Mediator.Switch.IRequest request, global::System.Threading.CancellationToken cancellationToken) + { + if (ValueSendSwitchCase.Cases.TryGetValue(request.GetType(), out var handle)) + { + return global::System.Runtime.CompilerServices.Unsafe.As, global::System.Threading.CancellationToken, global::System.Threading.Tasks.ValueTask>>(handle)(this, request, cancellationToken); + } + + throw new global::System.ArgumentException($"No handler for {request.GetType().Name}"); + } + + private static class ValueSendSwitchCase + { + public static readonly global::System.Collections.Generic.IDictionary Cases = new (global::System.Type, object)[] + { + ( // case Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommand: + typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommand), (global::System.Func, global::System.Threading.CancellationToken, global::System.Threading.Tasks.ValueTask>) ((instance, request, cancellationToken) => + instance.Handle_Mocha_Mediator_Benchmarks_Messaging_FullPipelineSwitchMediatorCommand( + (global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommand) request, cancellationToken)) + ), + ( // case Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorCommand: + typeof(global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorCommand), (global::System.Func, global::System.Threading.CancellationToken, global::System.Threading.Tasks.ValueTask>) ((instance, request, cancellationToken) => + instance.Handle_Mocha_Mediator_Benchmarks_Messaging_SwitchMediatorCommand( + (global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorCommand) request, cancellationToken)) + ) + } +#if NET8_0_OR_GREATER + .ToFrozenDictionary +#else + .ToDictionary +#endif + (t => t.Item1, t => t.Item2); + } + + public global::System.Threading.Tasks.Task Publish(global::Mediator.Switch.INotification notification, global::System.Threading.CancellationToken cancellationToken = default) + { + if (TaskPublishSwitchCase.Cases.TryGetValue(notification.GetType(), out var handle)) + { + return handle(this, notification, cancellationToken); + } + + if (ValuePublishSwitchCase.Cases.TryGetValue(notification.GetType(), out var valueHandle)) + { + return valueHandle(this, notification, cancellationToken).AsTask(); + } + + return global::System.Threading.Tasks.Task.CompletedTask; + } + + private static class TaskPublishSwitchCase + { + public static readonly global::System.Collections.Generic.IDictionary> Cases = new (global::System.Type, global::System.Func)[] + { + + } +#if NET8_0_OR_GREATER + .ToFrozenDictionary +#else + .ToDictionary +#endif + (t => t.Item1, t => t.Item2); + } + + global::System.Threading.Tasks.ValueTask global::Mediator.Switch.IValuePublisher.Publish(global::Mediator.Switch.INotification notification, global::System.Threading.CancellationToken cancellationToken) + { + if (ValuePublishSwitchCase.Cases.TryGetValue(notification.GetType(), out var handle)) + { + return handle(this, notification, cancellationToken); + } + + if (TaskPublishSwitchCase.Cases.TryGetValue(notification.GetType(), out var taskHandle)) + { + return new global::System.Threading.Tasks.ValueTask(taskHandle(this, notification, cancellationToken)); + } + + return global::System.Threading.Tasks.ValueTask.CompletedTask; + } + + private static class ValuePublishSwitchCase + { + public static readonly global::System.Collections.Generic.IDictionary> Cases = new (global::System.Type, global::System.Func)[] + { + ( // case Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorNotification: + typeof(global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorNotification), async (instance, notification, cancellationToken) => + { + var handlers = instance.Get(ref instance._mocha_Mediator_Benchmarks_Messaging_SwitchMediatorNotification__ValueHandlers); + + var typedNotification = (global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorNotification) notification; + + foreach (var handler in handlers) + { + await + /* Notification Handler */ handler.Handle(typedNotification, cancellationToken); + } + } + ) + } +#if NET8_0_OR_GREATER + .ToFrozenDictionary +#else + .ToDictionary +#endif + (t => t.Item1, t => t.Item2); + } + + private global::System.Threading.Tasks.ValueTask Handle_Mocha_Mediator_Benchmarks_Messaging_FullPipelineSwitchMediatorCommand( + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommand request, + global::System.Threading.CancellationToken cancellationToken) + { + + var mocha_Mediator_Benchmarks_Messaging_FullPipelineSwitchMediatorCommandHandler = Get(ref _mocha_Mediator_Benchmarks_Messaging_FullPipelineSwitchMediatorCommandHandler); + + return + /* Request Handler */ mocha_Mediator_Benchmarks_Messaging_FullPipelineSwitchMediatorCommandHandler.Handle(request, cancellationToken); + } + + private global::System.Threading.Tasks.ValueTask Handle_Mocha_Mediator_Benchmarks_Messaging_SwitchMediatorCommand( + global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorCommand request, + global::System.Threading.CancellationToken cancellationToken) + { + + var mocha_Mediator_Benchmarks_Messaging_SwitchMediatorCommandHandler = Get(ref _mocha_Mediator_Benchmarks_Messaging_SwitchMediatorCommandHandler); + + return + /* Request Handler */ mocha_Mediator_Benchmarks_Messaging_SwitchMediatorCommandHandler.Handle(request, cancellationToken); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [DebuggerStepThrough] + private T Get(ref T? field) where T : notnull + { + return field ?? (field = _svc.Get()); + } + + /// + /// Provides lists of SwitchMediator component implementation types. + /// + public static class SwitchMediatorKnownTypes + { + public static readonly global::System.Collections.Generic.IReadOnlyList RequestHandlerTypes = + new global::System.Type[] { + typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommandHandler), + typeof(global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorCommandHandler) + }.AsReadOnly(); + + public static readonly global::System.Collections.Generic.IReadOnlyList<(global::System.Type NotificationType, global::System.Collections.Generic.IReadOnlyList HandlerTypes)> NotificationTypes = + new (global::System.Type NotificationType, global::System.Collections.Generic.IReadOnlyList HandlerTypes)[] { + (typeof(global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorNotification), new global::System.Type[] { + typeof(global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorNotificationHandler) + }) + }.AsReadOnly(); + + public static readonly global::System.Collections.Generic.IReadOnlyList PipelineBehaviorTypes = + new global::System.Type[] { + + }.AsReadOnly(); + } +} \ No newline at end of file diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/BenchmarksMediatorBuilderExtensions._RXvJz6gQPQLNfJu4g_tRw.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/BenchmarksMediatorBuilderExtensions._RXvJz6gQPQLNfJu4g_tRw.g.cs new file mode 100644 index 00000000000..4c4616bb7d5 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/BenchmarksMediatorBuilderExtensions._RXvJz6gQPQLNfJu4g_tRw.g.cs @@ -0,0 +1,49 @@ +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "0.0.0.0")] + public static class BenchmarksMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddBenchmarks( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkCommandHandler), lifetime)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineCommandHandler), lifetime)); + + // Register notification handlers + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkNotificationHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkNotificationHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkCommand), + ResponseType = typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineCommand), + ResponseType = typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkNotification), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkNotificationHandler) }) + }); + }); + + return builder; + } + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/BenchmarksMediatorBuilderExtensions.g.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/BenchmarksMediatorBuilderExtensions.g.cs new file mode 100644 index 00000000000..7e036874fe4 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/BenchmarksMediatorBuilderExtensions.g.cs @@ -0,0 +1,35 @@ +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "0.0.0.0")] + public static class BenchmarksMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddBenchmarks( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkCommandHandler), lifetime)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineCommandHandler), lifetime)); + + // Register notification handlers + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkNotificationHandler), typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkNotificationHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkCommand), global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal()); + b.RegisterPipeline(typeof(global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineCommand), global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal()); + b.RegisterPipeline(typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkNotification), global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkNotificationHandler) })); + }); + + return builder; + } + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/DispatchPatternBenchmarks.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/DispatchPatternBenchmarks.cs new file mode 100644 index 00000000000..d9732a5ea6b --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/DispatchPatternBenchmarks.cs @@ -0,0 +1,348 @@ +using System.Collections.Concurrent; +using System.Collections.Frozen; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Mocha.Mediator.Benchmarks.Internal; + +/// +/// Compares different dispatch strategies for routing messages to handlers. +/// Tests switch-based pattern matching vs dictionary lookup approaches. +/// +[MemoryDiagnoser] +[RankColumn] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class DispatchPatternBenchmarks +{ + // 20 dummy message types + public sealed record Msg00(Guid Id); + public sealed record Msg01(Guid Id); + public sealed record Msg02(Guid Id); + public sealed record Msg03(Guid Id); + public sealed record Msg04(Guid Id); + public sealed record Msg05(Guid Id); + public sealed record Msg06(Guid Id); + public sealed record Msg07(Guid Id); + public sealed record Msg08(Guid Id); + public sealed record Msg09(Guid Id); + public sealed record Msg10(Guid Id); + public sealed record Msg11(Guid Id); + public sealed record Msg12(Guid Id); + public sealed record Msg13(Guid Id); + public sealed record Msg14(Guid Id); + public sealed record Msg15(Guid Id); + public sealed record Msg16(Guid Id); + public sealed record Msg17(Guid Id); + public sealed record Msg18(Guid Id); + public sealed record Msg19(Guid Id); + + private static readonly Guid _result = Guid.NewGuid(); + private static readonly Func _handler = _ => _result; + + private object[] _messages = null!; + private object _targetMessage = null!; + + private Dictionary> _dictionary = null!; + private FrozenDictionary> _frozenDictionary = null!; + private ConcurrentDictionary> _concurrentDictionary = null!; + + [StructLayout(LayoutKind.Sequential)] + private struct Entry + { + public nint TypeHandle; + public Func Handler; + } + + private Entry[] _table = null!; + private int _mask; + private int _shift; + private ulong _multiplier; + + private FrozenDictionary> _handleFrozenDictionary = null!; + + public enum MessagePosition + { + First, + Middle, + Last + } + + [Params(MessagePosition.First, MessagePosition.Middle, MessagePosition.Last)] + public MessagePosition Position { get; set; } + + [GlobalSetup] + public void Setup() + { + _messages = + [ + new Msg00(Guid.NewGuid()), new Msg01(Guid.NewGuid()), new Msg02(Guid.NewGuid()), + new Msg03(Guid.NewGuid()), new Msg04(Guid.NewGuid()), new Msg05(Guid.NewGuid()), + new Msg06(Guid.NewGuid()), new Msg07(Guid.NewGuid()), new Msg08(Guid.NewGuid()), + new Msg09(Guid.NewGuid()), new Msg10(Guid.NewGuid()), new Msg11(Guid.NewGuid()), + new Msg12(Guid.NewGuid()), new Msg13(Guid.NewGuid()), new Msg14(Guid.NewGuid()), + new Msg15(Guid.NewGuid()), new Msg16(Guid.NewGuid()), new Msg17(Guid.NewGuid()), + new Msg18(Guid.NewGuid()), new Msg19(Guid.NewGuid()) + ]; + + _targetMessage = Position switch + { + MessagePosition.First => _messages[0], + MessagePosition.Middle => _messages[10], + MessagePosition.Last => _messages[19], + _ => throw new ArgumentOutOfRangeException() + }; + + var entries = new (Type, Func)[] + { + (typeof(Msg00), _handler), (typeof(Msg01), _handler), (typeof(Msg02), _handler), + (typeof(Msg03), _handler), (typeof(Msg04), _handler), (typeof(Msg05), _handler), + (typeof(Msg06), _handler), (typeof(Msg07), _handler), (typeof(Msg08), _handler), + (typeof(Msg09), _handler), (typeof(Msg10), _handler), (typeof(Msg11), _handler), + (typeof(Msg12), _handler), (typeof(Msg13), _handler), (typeof(Msg14), _handler), + (typeof(Msg15), _handler), (typeof(Msg16), _handler), (typeof(Msg17), _handler), + (typeof(Msg18), _handler), (typeof(Msg19), _handler) + }; + + _dictionary = new Dictionary>(entries.Length); + foreach (var (type, handler) in entries) + { + _dictionary[type] = handler; + } + + _frozenDictionary = _dictionary.ToFrozenDictionary(); + + _concurrentDictionary = new ConcurrentDictionary>(); + foreach (var (type, handler) in entries) + { + _concurrentDictionary[type] = handler; + } + + // Build hash table for span access benchmark + var types = new Type[20]; + for (int i = 0; i < 20; i++) + types[i] = _messages[i].GetType(); + + BuildTable(types, _handler, out _table, out _mask, out _shift, out _multiplier, out _); + + var handleDict = new Dictionary>(20); + foreach (var t in types) handleDict[t.TypeHandle.Value] = _handler; + _handleFrozenDictionary = handleDict.ToFrozenDictionary(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetSlot(nint handle, int mask, int shift, ulong multiplier) + { + unchecked + { + return (int)(((ulong)handle * multiplier) >> shift) & mask; + } + } + + private static void BuildTable( + Type[] types, + Func handler, + out Entry[] table, + out int mask, + out int shift, + out ulong multiplier, + out int maxProbeLength) + { + int n = types.Length; + int minBits = (int)Math.Ceiling(Math.Log2(n)) + 1; + + Entry[]? bestTable = null; + int bestMask = 0, bestShift = 0; + ulong bestMultiplier = 0; + int bestMaxProbe = int.MaxValue; + + for (int bits = minBits; bits <= minBits + 4; bits++) + { + int tableSize = 1 << bits; + int tableMask = tableSize - 1; + int tableShift = 64 - bits; + + for (ulong m = 0x9E3779B97F4A7C15UL; m < 0x9E3779B97F4A7C15UL + 100_000; m += 2) + { + var test = new Entry[tableSize]; + int localMaxProbe = 0; + bool ok = true; + + foreach (var t in types) + { + nint h = t.TypeHandle.Value; + int slot; + unchecked + { + slot = (int)(((ulong)h * m) >> tableShift) & tableMask; + } + int probe = 0; + + while (test[slot].Handler != null) + { + slot = (slot + 1) & tableMask; + probe++; + if (probe >= tableSize) { ok = false; break; } + } + + if (!ok) break; + test[slot] = new Entry { TypeHandle = h, Handler = handler }; + localMaxProbe = Math.Max(localMaxProbe, probe); + } + + if (!ok) continue; + + if (localMaxProbe == 0) + { + table = test; + mask = tableMask; + shift = tableShift; + multiplier = m; + maxProbeLength = 0; + return; + } + + if (localMaxProbe < bestMaxProbe) + { + bestTable = test; + bestMask = tableMask; + bestShift = tableShift; + bestMultiplier = m; + bestMaxProbe = localMaxProbe; + if (bestMaxProbe <= 1) break; + } + } + + if (bestMaxProbe <= 1) break; + } + + table = bestTable!; + mask = bestMask; + shift = bestShift; + multiplier = bestMultiplier; + maxProbeLength = bestMaxProbe; + } + + [Benchmark(Baseline = true)] + public Guid SwitchType() + { + return _targetMessage switch + { + Msg00 m => _handler(m), + Msg01 m => _handler(m), + Msg02 m => _handler(m), + Msg03 m => _handler(m), + Msg04 m => _handler(m), + Msg05 m => _handler(m), + Msg06 m => _handler(m), + Msg07 m => _handler(m), + Msg08 m => _handler(m), + Msg09 m => _handler(m), + Msg10 m => _handler(m), + Msg11 m => _handler(m), + Msg12 m => _handler(m), + Msg13 m => _handler(m), + Msg14 m => _handler(m), + Msg15 m => _handler(m), + Msg16 m => _handler(m), + Msg17 m => _handler(m), + Msg18 m => _handler(m), + Msg19 m => _handler(m), + _ => throw new InvalidOperationException() + }; + } + + [Benchmark] + public Guid SwitchGetType() + { + var type = _targetMessage.GetType(); + if (type == typeof(Msg00)) return _handler(_targetMessage); + if (type == typeof(Msg01)) return _handler(_targetMessage); + if (type == typeof(Msg02)) return _handler(_targetMessage); + if (type == typeof(Msg03)) return _handler(_targetMessage); + if (type == typeof(Msg04)) return _handler(_targetMessage); + if (type == typeof(Msg05)) return _handler(_targetMessage); + if (type == typeof(Msg06)) return _handler(_targetMessage); + if (type == typeof(Msg07)) return _handler(_targetMessage); + if (type == typeof(Msg08)) return _handler(_targetMessage); + if (type == typeof(Msg09)) return _handler(_targetMessage); + if (type == typeof(Msg10)) return _handler(_targetMessage); + if (type == typeof(Msg11)) return _handler(_targetMessage); + if (type == typeof(Msg12)) return _handler(_targetMessage); + if (type == typeof(Msg13)) return _handler(_targetMessage); + if (type == typeof(Msg14)) return _handler(_targetMessage); + if (type == typeof(Msg15)) return _handler(_targetMessage); + if (type == typeof(Msg16)) return _handler(_targetMessage); + if (type == typeof(Msg17)) return _handler(_targetMessage); + if (type == typeof(Msg18)) return _handler(_targetMessage); + if (type == typeof(Msg19)) return _handler(_targetMessage); + throw new InvalidOperationException(); + } + + [Benchmark] + public Guid DictionaryLookup() + { + return _dictionary[_targetMessage.GetType()](_targetMessage); + } + + [Benchmark] + public Guid FrozenDictionaryLookup() + { + return _frozenDictionary[_targetMessage.GetType()](_targetMessage); + } + + [Benchmark] + public Guid ConcurrentDictionaryLookup() + { + return _concurrentDictionary[_targetMessage.GetType()](_targetMessage); + } + + [Benchmark] + public Guid Span_Access() + { + nint handle = _targetMessage.GetType().TypeHandle.Value; + int slot = GetSlot(handle, _mask, _shift, _multiplier); + + Span span = _table.AsSpan(); + + ref Entry entry = ref span[slot]; + if (entry.TypeHandle == handle) + return entry.Handler(_targetMessage); + + while (true) + { + slot = (slot + 1) & _mask; + entry = ref span[slot]; + if (entry.TypeHandle == handle) + return entry.Handler(_targetMessage); + } + } + + [Benchmark] + public Guid UnsafeAdd_NoBoundsCheck() + { + nint handle = _targetMessage.GetType().TypeHandle.Value; + int slot = GetSlot(handle, _mask, _shift, _multiplier); + + ref Entry origin = ref MemoryMarshal.GetArrayDataReference(_table); + + ref Entry entry = ref Unsafe.Add(ref origin, slot); + if (entry.TypeHandle == handle) + return entry.Handler(_targetMessage); + + while (true) + { + slot = (slot + 1) & _mask; + entry = ref Unsafe.Add(ref origin, slot); + if (entry.TypeHandle == handle) + return entry.Handler(_targetMessage); + } + } + + [Benchmark] + public Guid HandleFrozenDictionary() + { + return _handleFrozenDictionary[_targetMessage.GetType().TypeHandle.Value](_targetMessage); + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/LookupBenchmarks.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/LookupBenchmarks.cs new file mode 100644 index 00000000000..450c27a2cc7 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/LookupBenchmarks.cs @@ -0,0 +1,77 @@ +using System.Collections.Frozen; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Benchmarks.Internal; + +/// +/// Compares DI resolution patterns vs cached delegate/dictionary lookups +/// to understand the cost of service resolution at dispatch time. +/// +[MemoryDiagnoser] +[RankColumn] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class LookupBenchmarks +{ + private sealed record LookupRequest(Guid Id); + + private sealed class LookupHandler + { + private static readonly Guid _result = Guid.NewGuid(); + public Guid Handle(LookupRequest request) => _result; + } + + private IServiceProvider _serviceProvider = null!; + private Func _cachedDelegate = null!; + private FrozenDictionary _frozenLookup = null!; + private Type _handlerType = null!; + private LookupRequest _request = null!; + + [GlobalSetup] + public void Setup() + { + _request = new LookupRequest(Guid.NewGuid()); + _handlerType = typeof(LookupHandler); + + var services = new ServiceCollection(); + services.AddScoped(); + _serviceProvider = services.BuildServiceProvider().CreateScope().ServiceProvider; + + var handler = _serviceProvider.GetRequiredService(); + _cachedDelegate = () => handler.Handle(_request); + + _frozenLookup = new Dictionary + { + [typeof(LookupHandler)] = handler + }.ToFrozenDictionary(); + } + + [Benchmark] + public Guid DI_GetRequiredService() + { + var handler = (LookupHandler)_serviceProvider.GetRequiredService(_handlerType); + return handler.Handle(_request); + } + + [Benchmark] + public Guid DI_Scoped_GetRequiredService() + { + using var scope = ((IServiceProvider)_serviceProvider).CreateScope(); + var handler = scope.ServiceProvider.GetRequiredService(); + return handler.Handle(_request); + } + + [Benchmark(Baseline = true)] + public Guid CachedDelegate() + { + return _cachedDelegate(); + } + + [Benchmark] + public Guid FrozenDictionary_TypeLookup() + { + var handler = (LookupHandler)_frozenLookup[_handlerType]; + return handler.Handle(_request); + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/MediatorSgRegistration.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/MediatorSgRegistration.cs new file mode 100644 index 00000000000..bec13ceead0 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/MediatorSgRegistration.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Benchmarks.Internal; + +// In this namespace, services.AddMediator() unambiguously resolves +// to martinothamar's Mediator source generator extension method. +internal static class MediatorSgRegistration +{ + public static void Register(IServiceCollection services) + => services.AddMediator(); +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PipelinePatternBenchmarks.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PipelinePatternBenchmarks.cs new file mode 100644 index 00000000000..8472f853741 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PipelinePatternBenchmarks.cs @@ -0,0 +1,168 @@ +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using Mocha.Mediator; + +namespace Mocha.Mediator.Benchmarks.Internal; + +/// +/// Compares different pipeline composition strategies to understand +/// the overhead of building delegate chains per-call vs pre-compiled. +/// +[MemoryDiagnoser] +[RankColumn] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class PipelinePatternBenchmarks +{ + public sealed record PipelineRequest(Guid Id); + + private static readonly Guid _responseId = Guid.NewGuid(); + + private PipelineRequest _request = null!; + + private MediatorDelegate _directHandler = null!; + private MediatorMiddleware[] _middlewares = null!; + private MediatorDelegate _preCompiledPipeline = null!; + private MediatorContext _context = null!; + + [Params(0, 1, 3)] + public int BehaviorCount { get; set; } + + [GlobalSetup] + public void Setup() + { + _request = new PipelineRequest(Guid.NewGuid()); + _directHandler = static ctx => + { + ctx.Result = _responseId; + return ValueTask.CompletedTask; + }; + + _middlewares = new MediatorMiddleware[BehaviorCount]; + for (var i = 0; i < BehaviorCount; i++) + { + _middlewares[i] = static (_, next) => ctx => next(ctx); + } + + // Pre-compile the pipeline once + _preCompiledPipeline = BuildDelegateChain(_directHandler, _middlewares); + + // Raw inlined (no delegates, no interfaces) + _inlinedBehavior1 = new InlinedBehavior1(); + _inlinedBehavior3 = new InlinedBehavior3(); + + _context = new MediatorContext(); + } + + private void SetupContext() + { + _context.Message = _request; + _context.MessageType = typeof(PipelineRequest); + _context.ResponseType = typeof(Guid); + _context.CancellationToken = CancellationToken.None; + } + + [Benchmark(Baseline = true)] + public ValueTask DirectHandler() + { + SetupContext(); + return _directHandler(_context); + } + + [Benchmark] + public ValueTask DelegateChain_PerCall() + { + SetupContext(); + var pipeline = BuildDelegateChain(_directHandler, _middlewares); + return pipeline(_context); + } + + [Benchmark] + public ValueTask DelegateChain_PreCompiled() + { + SetupContext(); + return _preCompiledPipeline(_context); + } + + private static MediatorDelegate BuildDelegateChain( + MediatorDelegate handler, + MediatorMiddleware[] middlewares) + { + var pipeline = handler; + for (var i = middlewares.Length - 1; i >= 0; i--) + { + var next = pipeline; + var mw = middlewares[i]; + pipeline = mw(new MediatorMiddlewareFactoryContext { Services = null!, Features = null! }, next); + } + return pipeline; + } + + /// + /// Simulates what a source generator could emit: direct inline calls + /// with no delegate indirection and no interface dispatch. + /// This is the theoretical upper bound for pipeline performance. + /// + [Benchmark] + public ValueTask RawInlined_NoBehaviors() + { + return new ValueTask(_responseId); + } + + [Benchmark] + public ValueTask RawInlined_1Behavior() + { + return _inlinedBehavior1.HandleDirect(_request, CancellationToken.None); + } + + [Benchmark] + public ValueTask RawInlined_3Behaviors() + { + return _inlinedBehavior3.HandleDirect(_request, CancellationToken.None); + } + + public sealed class InlinedBehavior1 + { + private readonly InlinedHandler _handler = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ValueTask HandleDirect(PipelineRequest message, CancellationToken ct) + { + return _handler.HandleDirect(message, ct); + } + } + + public sealed class InlinedBehavior3 + { + private readonly InlinedBehavior2 _next = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ValueTask HandleDirect(PipelineRequest message, CancellationToken ct) + { + return _next.HandleDirect(message, ct); + } + } + + public sealed class InlinedBehavior2 + { + private readonly InlinedBehavior1 _next = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ValueTask HandleDirect(PipelineRequest message, CancellationToken ct) + { + return _next.HandleDirect(message, ct); + } + } + + public sealed class InlinedHandler + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ValueTask HandleDirect(PipelineRequest message, CancellationToken ct) + { + return new ValueTask(_responseId); + } + } + + private InlinedBehavior1 _inlinedBehavior1 = null!; + private InlinedBehavior3 _inlinedBehavior3 = null!; +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PoolingBenchmarks.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PoolingBenchmarks.cs new file mode 100644 index 00000000000..9c8b7c03fed --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PoolingBenchmarks.cs @@ -0,0 +1,152 @@ +using System.Collections.Concurrent; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using Microsoft.Extensions.ObjectPool; + +namespace Mocha.Mediator.Benchmarks.Internal; + +/// +/// Compares pooling strategies for the MediatorContext object. +/// Each benchmark rents an object, writes to it, reads back, and returns it. +/// +[MemoryDiagnoser] +[RankColumn] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class PoolingBenchmarks +{ + private sealed class PooledObject + { + public object? Message; + public Type? MessageType; + public object? Result; + + public void Reset() + { + Message = null; + MessageType = null; + Result = null; + } + } + + private sealed class PooledObjectPolicy : PooledObjectPolicy + { + public override PooledObject Create() => new(); + + public override bool Return(PooledObject obj) + { + obj.Reset(); + return true; + } + } + + private ConcurrentQueue _concurrentQueue = null!; + private ObjectPool _objectPool = null!; + private ThreadLocal _threadLocal = null!; + private ObjectPool _threadStaticFallbackPool = null!; + + [ThreadStatic] + private static PooledObject? t_cached; + + private static readonly object s_message = "hello"; + private static readonly Type s_type = typeof(string); + + [GlobalSetup] + public void Setup() + { + _concurrentQueue = new ConcurrentQueue(); + _objectPool = new DefaultObjectPool(new PooledObjectPolicy(), 64); + _threadLocal = new ThreadLocal(() => new PooledObject(), trackAllValues: false); + _threadStaticFallbackPool = new DefaultObjectPool(new PooledObjectPolicy(), 64); + t_cached = null; + } + + [GlobalCleanup] + public void Cleanup() + { + _threadLocal.Dispose(); + } + + [Benchmark(Baseline = true)] + public object? NewEveryTime() + { + var obj = new PooledObject(); + obj.Message = s_message; + obj.MessageType = s_type; + var result = obj.Message; + // No return — GC collects it + return result; + } + + [Benchmark] + public object? ConcurrentQueue_Pool() + { + if (!_concurrentQueue.TryDequeue(out var obj)) + { + obj = new PooledObject(); + } + + obj.Message = s_message; + obj.MessageType = s_type; + var result = obj.Message; + + obj.Reset(); + _concurrentQueue.Enqueue(obj); + return result; + } + + [Benchmark] + public object? ObjectPool_Default() + { + var obj = _objectPool.Get(); + + obj.Message = s_message; + obj.MessageType = s_type; + var result = obj.Message; + + _objectPool.Return(obj); + return result; + } + + [Benchmark] + public object? ThreadLocal_Reuse() + { + var obj = _threadLocal.Value!; + + obj.Message = s_message; + obj.MessageType = s_type; + var result = obj.Message; + + obj.Reset(); + return result; + } + + [Benchmark] + public object? ThreadStatic_WithObjectPoolFallback() + { + var obj = t_cached; + if (obj is not null) + { + t_cached = null; + } + else + { + obj = _threadStaticFallbackPool.Get(); + } + + obj.Message = s_message; + obj.MessageType = s_type; + var result = obj.Message; + + obj.Reset(); + if (t_cached is null) + { + t_cached = obj; + } + else + { + _threadStaticFallbackPool.Return(obj); + } + + return result; + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/BenchmarkSetup.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/BenchmarkSetup.cs new file mode 100644 index 00000000000..d4b3da0a52a --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/BenchmarkSetup.cs @@ -0,0 +1,159 @@ +using MassTransit; +using MassTransit.Mediator; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Wolverine; + +namespace Mocha.Mediator.Benchmarks.Messaging; + +internal static class MediatorSgFactory +{ + public static IServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + MediatorSgHelper.Register(services); + return services.BuildServiceProvider().CreateScope().ServiceProvider; + } + + public static IServiceProvider CreateServiceProviderWithPipeline() + { + var services = new ServiceCollection(); + MediatorSgHelper.Register(services); + services.AddSingleton< + global::Mediator.IPipelineBehavior< + global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgCommand, + global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse>, + global::Mocha.Mediator.Benchmarks.Messaging.MediatorSgPipelineBehavior>(); + return services.BuildServiceProvider().CreateScope().ServiceProvider; + } + + public static IServiceProvider CreateServiceProviderWithFullPipeline() + { + var services = new ServiceCollection(); + MediatorSgHelper.Register(services); + // Native pre-processor (MessagePreProcessor<,> implements IPipelineBehavior<,>) + services.AddSingleton< + global::Mediator.IPipelineBehavior< + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommand, + global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse>, + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgPreProcessor>(); + // Pipeline behavior + services.AddSingleton< + global::Mediator.IPipelineBehavior< + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommand, + global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse>, + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgBehavior>(); + // Native post-processor (MessagePostProcessor<,> implements IPipelineBehavior<,>) + services.AddSingleton< + global::Mediator.IPipelineBehavior< + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgCommand, + global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse>, + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineMediatorSgPostProcessor>(); + return services.BuildServiceProvider().CreateScope().ServiceProvider; + } +} + +internal static class WolverineFactory +{ + public static (IHost Host, Wolverine.IMessageBus Bus) Create() + { + var builder = Host.CreateApplicationBuilder(); + builder.UseWolverine(opts => + { + opts.Discovery.DisableConventionalDiscovery(); + opts.Discovery.IncludeType(typeof(global::Mocha.Mediator.Benchmarks.Messaging.WolverineCommandHandler)); + opts.Discovery.IncludeType(typeof(global::Mocha.Mediator.Benchmarks.Messaging.WolverineNotificationHandler)); + }); + var host = builder.Build(); + host.StartAsync().GetAwaiter().GetResult(); + var bus = host.Services.GetRequiredService(); + return (host, bus); + } +} + +internal static class SwitchMediatorFactory +{ + private static void AddSwitchMediator(ServiceCollection services) + { + global::Mediator.Switch.Extensions.Microsoft.DependencyInjection.ServiceCollectionExtensions + .AddMediator(services, _ => { }); + // SwitchMediator DI extension may not register concrete handler types; + // ensure they are available for the generated mediator to resolve. + services.AddScoped(); + services.AddScoped(); + } + + public static IServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + AddSwitchMediator(services); + return services.BuildServiceProvider().CreateScope().ServiceProvider; + } + + public static IServiceProvider CreateServiceProviderWithPipeline() + { + var services = new ServiceCollection(); + AddSwitchMediator(services); + services.AddSingleton< + global::Mediator.Switch.IValuePipelineBehavior< + global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorCommand, + global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse>, + global::Mocha.Mediator.Benchmarks.Messaging.SwitchMediatorPipelineBehavior>(); + return services.BuildServiceProvider().CreateScope().ServiceProvider; + } + + public static IServiceProvider CreateServiceProviderWithFullPipeline() + { + var services = new ServiceCollection(); + AddSwitchMediator(services); + services.AddScoped(); + services.AddSingleton< + global::Mediator.Switch.IValuePipelineBehavior< + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommand, + global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse>, + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorPreBehavior>(); + services.AddSingleton< + global::Mediator.Switch.IValuePipelineBehavior< + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommand, + global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse>, + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorMainBehavior>(); + services.AddSingleton< + global::Mediator.Switch.IValuePipelineBehavior< + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorCommand, + global::Mocha.Mediator.Benchmarks.Messaging.BenchmarkResponse>, + global::Mocha.Mediator.Benchmarks.Messaging.FullPipelineSwitchMediatorPostBehavior>(); + return services.BuildServiceProvider().CreateScope().ServiceProvider; + } +} + +internal static class MassTransitFactory +{ + public static IServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + services.AddMediator(cfg => + { + cfg.AddConsumer(); + cfg.AddConsumer(); + }); + return services.BuildServiceProvider().CreateScope().ServiceProvider; + } +} + +internal static class ImmediateHandlersFactory +{ + public static IServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + services.AddMochaMediatorBenchmarksHandlers(); + return services.BuildServiceProvider().CreateScope().ServiceProvider; + } + + public static IServiceProvider CreateServiceProviderWithPipeline() + { + var services = new ServiceCollection(); + services.AddMochaMediatorBenchmarksBehaviors(); + services.AddMochaMediatorBenchmarksHandlers(); + return services.BuildServiceProvider().CreateScope().ServiceProvider; + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/CommandBenchmarks.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/CommandBenchmarks.cs new file mode 100644 index 00000000000..4fccd5030d4 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/CommandBenchmarks.cs @@ -0,0 +1,190 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using Immediate.Handlers.Shared; +using MassTransit.Mediator; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Mocha.Mediator.Benchmarks.Messaging; + +/// +/// Compares command dispatch performance across mediator libraries. +/// Each benchmark resolves mediators from a fresh scope to mirror real request handling. +/// +[MemoryDiagnoser] +[RankColumn] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class CommandBenchmarks +{ + private ServiceProvider _rootProvider = null!; + private IServiceProvider? _mediatorSgProvider; + private Wolverine.IMessageBus? _wolverineBus; + private IHost? _wolverineHost; + private IServiceProvider? _switchMediatorProvider; + private IServiceProvider? _immediateProvider; + private IServiceProvider? _massTransitProvider; + private BenchmarkCommandHandler _handler = null!; + private BenchmarkCommand _command = null!; + private MediatorSgCommand _mediatorSgCommand = null!; + private WolverineCommand _wolverineCommand = null!; + private SwitchMediatorCommand _switchMediatorCommand = null!; + private ImmediateCommandHandler.Command _immediateCommand = null!; + private MassTransitCommand _massTransitCommand = null!; + + [GlobalSetup] + public void Setup() + { + // Mocha + MediatR (shared provider) + var services = new ServiceCollection(); + MediatorServiceCollectionExtensions.AddMediator(services).AddBenchmarks(); + services.AddMediatR(opts => + opts.RegisterServicesFromAssembly(typeof(BenchmarkCommandHandler).Assembly)); + _rootProvider = services.BuildServiceProvider(); + + // martinothamar Mediator (separate provider to avoid AddMediator collision) + try + { + _mediatorSgProvider = MediatorSgFactory.CreateServiceProvider(); + } + catch (InvalidOperationException) + { + } + + // Wolverine + try + { + (_wolverineHost, _wolverineBus) = WolverineFactory.Create(); + } + catch (InvalidOperationException) + { + } + + // SwitchMediator (separate provider) + try + { + _switchMediatorProvider = SwitchMediatorFactory.CreateServiceProvider(); + } + catch (InvalidOperationException) + { + } + + // Immediate.Handlers (separate provider) + try + { + _immediateProvider = ImmediateHandlersFactory.CreateServiceProvider(); + } + catch (InvalidOperationException) + { + } + + // MassTransit Mediator (separate provider) + try + { + _massTransitProvider = MassTransitFactory.CreateServiceProvider(); + } + catch (InvalidOperationException) + { + } + + _handler = new BenchmarkCommandHandler(); + _command = new BenchmarkCommand(Guid.NewGuid()); + _mediatorSgCommand = new MediatorSgCommand(Guid.NewGuid()); + _wolverineCommand = new WolverineCommand(Guid.NewGuid()); + _switchMediatorCommand = new SwitchMediatorCommand(Guid.NewGuid()); + _immediateCommand = new ImmediateCommandHandler.Command(Guid.NewGuid()); + _massTransitCommand = new MassTransitCommand(Guid.NewGuid()); + } + + [GlobalCleanup] + public void Cleanup() + { + _wolverineHost?.StopAsync().GetAwaiter().GetResult(); + _wolverineHost?.Dispose(); + } + + [Benchmark] + public Task SendCommand_MediatR() + { + var scope = _rootProvider.CreateScope(); + var mediatr = scope.ServiceProvider.GetRequiredService(); + return mediatr.Send(_command, CancellationToken.None); + } + + [Benchmark] + public ValueTask SendCommand_Mocha_IMediator() + { + var scope = _rootProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.SendAsync(_command, CancellationToken.None); + } + + [Benchmark] + public ValueTask SendCommand_Mocha_Concrete() + { + var scope = _rootProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.SendAsync(_command, CancellationToken.None); + } + + [Benchmark] + public ValueTask SendCommand_MediatorSg() + { + if (_mediatorSgProvider is null) + return new ValueTask(new BenchmarkResponse(Guid.Empty)); + + var scope = _mediatorSgProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.Send(_mediatorSgCommand, CancellationToken.None); + } + + [Benchmark] + public Task SendCommand_Wolverine() + { + return _wolverineBus?.InvokeAsync(_wolverineCommand, CancellationToken.None) + ?? Task.FromResult(new BenchmarkResponse(Guid.Empty)); + } + + [Benchmark] + public ValueTask SendCommand_SwitchMediator() + { + if (_switchMediatorProvider is null) + return new ValueTask(new BenchmarkResponse(Guid.Empty)); + + var scope = _switchMediatorProvider.CreateScope(); + var sender = scope.ServiceProvider.GetRequiredService(); + return sender.Send(_switchMediatorCommand, CancellationToken.None); + } + + [Benchmark] + public ValueTask SendCommand_ImmediateHandlers() + { + if (_immediateProvider is null) + return new ValueTask(new BenchmarkResponse(Guid.Empty)); + + var scope = _immediateProvider.CreateScope(); + var handler = scope.ServiceProvider.GetRequiredService>(); + return handler.HandleAsync(_immediateCommand, CancellationToken.None); + } + + [Benchmark] + public async Task SendCommand_MassTransit() + { + if (_massTransitProvider is not null) + { + var scope = _massTransitProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + var client = mediator.CreateRequestClient(); + var response = await client.GetResponse( + _massTransitCommand, CancellationToken.None); + return response.Message; + } + + return new MassTransitCommandResponse(Guid.Empty); + } + + [Benchmark(Baseline = true)] + public ValueTask SendCommand_Baseline() + { + return _handler.HandleAsync(_command, CancellationToken.None); + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/ConcurrentPipelineBenchmarks.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/ConcurrentPipelineBenchmarks.cs new file mode 100644 index 00000000000..1dd277ce192 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/ConcurrentPipelineBenchmarks.cs @@ -0,0 +1,124 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using Immediate.Handlers.Shared; +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Benchmarks.Messaging; + +/// +/// Compares pipeline behavior overhead across mediator libraries. +/// Each benchmark uses a singleton mediator resolved once during setup. +/// Wolverine and MassTransit are excluded as they use fundamentally different middleware models. +/// +[MemoryDiagnoser] +[RankColumn] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class ConcurrentPipelineBenchmarks +{ + private MediatR.IMediator _mediatr = null!; + private Mocha.Mediator.IMediator _mocha = null!; + private global::Mediator.IMediator? _mediatorSg; + private global::Mediator.Switch.IValueSender? _switchMediator; + private IHandler? _immediateHandler; + private BenchmarkCommand _command = null!; + private MediatorSgCommand _mediatorSgCommand = null!; + private SwitchMediatorCommand _switchMediatorCommand = null!; + private ImmediatePipelineCommandHandler.Command _immediateCommand = null!; + + [GlobalSetup] + public void Setup() + { + // Mocha + MediatR (shared provider) + var services = new ServiceCollection(); + var builder = MediatorServiceCollectionExtensions.AddMediator(services); + builder.Use(BenchmarkMochaMiddleware.Create()); + builder.AddBenchmarks(); + services.AddMediatR(opts => + { + opts.RegisterServicesFromAssembly(typeof(BenchmarkCommandHandler).Assembly); + opts.AddBehavior, + BenchmarkMediatRPipelineBehavior>(); + }); + var rootProvider = services.BuildServiceProvider(); + _mediatr = rootProvider.GetRequiredService(); + _mocha = rootProvider.GetRequiredService(); + + // martinothamar Mediator with pipeline behavior + try + { + var provider = MediatorSgFactory.CreateServiceProviderWithPipeline(); + _mediatorSg = provider.GetRequiredService(); + } + catch (InvalidOperationException) + { + } + + // SwitchMediator with pipeline behavior + try + { + var provider = SwitchMediatorFactory.CreateServiceProviderWithPipeline(); + _switchMediator = provider.GetRequiredService(); + } + catch (InvalidOperationException) + { + } + + // Immediate.Handlers with pipeline behavior + try + { + var provider = ImmediateHandlersFactory.CreateServiceProviderWithPipeline(); + _immediateHandler = provider.GetRequiredService>(); + } + catch (InvalidOperationException) + { + } + + _command = new BenchmarkCommand(Guid.NewGuid()); + _mediatorSgCommand = new MediatorSgCommand(Guid.NewGuid()); + _switchMediatorCommand = new SwitchMediatorCommand(Guid.NewGuid()); + _immediateCommand = new ImmediatePipelineCommandHandler.Command(Guid.NewGuid()); + } + + [Benchmark(Baseline = true)] + public async Task WithPipeline_MediatR() + { + var tasks = new Task[10]; + for (int i = 0; i < tasks.Length; i++) + { + tasks[i] = _mediatr.Send(_command, CancellationToken.None); + } + + for (int i = 0; i < tasks.Length; i++) + { + _ = await tasks[i]; + } + } + + [Benchmark] + public async ValueTask WithPipeline_Mocha() + { + var tasks = new ValueTask[10]; + for (int i = 0; i < tasks.Length; i++) { + tasks[i] = _mocha.SendAsync(_command, CancellationToken.None); + } + + for (int i = 0; i < tasks.Length; i++) + { + _ = await tasks[i]; + } + } + + [Benchmark] + public async ValueTask WithPipeline_MediatorSg() + { + var tasks = new ValueTask[10]; + for (int i = 0; i < tasks.Length; i++) { + tasks[i] = _mediatorSg!.Send(_mediatorSgCommand, CancellationToken.None); + } + + for (int i = 0; i < tasks.Length; i++) + { + _ = await tasks[i]!; + } + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/FullPipelineBenchmarks.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/FullPipelineBenchmarks.cs new file mode 100644 index 00000000000..d80d0cb7c8f --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/FullPipelineBenchmarks.cs @@ -0,0 +1,147 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using Immediate.Handlers.Shared; +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Benchmarks.Messaging; + +/// +/// Compares full pipeline performance across mediator libraries. +/// Each library processes a command through 3 middleware stages (pre + behavior + post) +/// using native features where available, or equivalent pipeline behaviors. +/// Each benchmark resolves mediators from a fresh scope to mirror real request handling. +/// +/// Mocha: 3 middleware delegates +/// MediatR: pre-processor + pipeline behavior + post-processor (native) +/// MediatorSg/SwitchMediator/Immediate.Handlers: 3 pipeline behaviors (equivalent depth) +/// +/// Wolverine and MassTransit are excluded as they use fundamentally different middleware models. +/// +[MemoryDiagnoser] +[RankColumn] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class FullPipelineBenchmarks +{ + private ServiceProvider _rootProvider = null!; + private IServiceProvider? _mediatorSgProvider; + private IServiceProvider? _switchMediatorProvider; + private IServiceProvider? _immediateProvider; + private FullPipelineCommand _command = null!; + private FullPipelineMediatorSgCommand _mediatorSgCommand = null!; + private FullPipelineSwitchMediatorCommand _switchMediatorCommand = null!; + private ImmediateFullPipelineCommandHandler.Command _immediateCommand = null!; + + [GlobalSetup] + public void Setup() + { + // Mocha + MediatR (shared provider) + var services = new ServiceCollection(); + var builder = MediatorServiceCollectionExtensions.AddMediator(services); + builder.Use(FullPipelineMochaPreMiddleware.Create()); + builder.Use(FullPipelineMochaBehaviorMiddleware.Create()); + builder.Use(FullPipelineMochaPostMiddleware.Create()); + builder.AddBenchmarks(); + services.AddMediatR(opts => + { + opts.RegisterServicesFromAssembly(typeof(FullPipelineCommandHandler).Assembly); + opts.AddBehavior, + FullPipelineMediatRBehavior>(); + }); + _rootProvider = services.BuildServiceProvider(); + + // MediatorSg with 3 behaviors (separate provider) + try + { + _mediatorSgProvider = MediatorSgFactory.CreateServiceProviderWithFullPipeline(); + } + catch (InvalidOperationException) + { + } + + // SwitchMediator with 3 behaviors (separate provider) + try + { + _switchMediatorProvider = SwitchMediatorFactory.CreateServiceProviderWithFullPipeline(); + } + catch (InvalidOperationException) + { + } + + // Immediate.Handlers with 3 behaviors (separate provider) + try + { + _immediateProvider = ImmediateHandlersFactory.CreateServiceProviderWithPipeline(); + } + catch (InvalidOperationException) + { + } + + _command = new FullPipelineCommand(Guid.NewGuid()); + _mediatorSgCommand = new FullPipelineMediatorSgCommand(Guid.NewGuid()); + _switchMediatorCommand = new FullPipelineSwitchMediatorCommand(Guid.NewGuid()); + _immediateCommand = new ImmediateFullPipelineCommandHandler.Command(Guid.NewGuid()); + } + + [Benchmark] + public Task FullPipeline_MediatR() + { + var scope = _rootProvider.CreateScope(); + var mediatr = scope.ServiceProvider.GetRequiredService(); + return mediatr.Send(_command, CancellationToken.None); + } + + [Benchmark] + public ValueTask FullPipeline_Mocha_IMediator() + { + var scope = _rootProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.SendAsync(_command, CancellationToken.None); + } + + [Benchmark] + public ValueTask FullPipeline_Mocha_Concrete() + { + var scope = _rootProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.SendAsync(_command, CancellationToken.None); + } + + [Benchmark] + public ValueTask FullPipeline_MediatorSg() + { + if (_mediatorSgProvider is null) + return new ValueTask(new BenchmarkResponse(Guid.Empty)); + + var scope = _mediatorSgProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.Send(_mediatorSgCommand, CancellationToken.None); + } + + [Benchmark] + public ValueTask FullPipeline_SwitchMediator() + { + if (_switchMediatorProvider is null) + return new ValueTask(new BenchmarkResponse(Guid.Empty)); + + var scope = _switchMediatorProvider.CreateScope(); + var sender = scope.ServiceProvider.GetRequiredService(); + return sender.Send(_switchMediatorCommand, CancellationToken.None); + } + + [Benchmark] + public ValueTask FullPipeline_ImmediateHandlers() + { + if (_immediateProvider is null) + return new ValueTask(new BenchmarkResponse(Guid.Empty)); + + var scope = _immediateProvider.CreateScope(); + var handler = scope.ServiceProvider.GetRequiredService>(); + return handler.HandleAsync(_immediateCommand, CancellationToken.None); + } + + [Benchmark(Baseline = true)] + public ValueTask FullPipeline_Baseline() + { + return new FullPipelineCommandHandler().HandleAsync(_command, CancellationToken.None); + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/FullPipelineMessages.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/FullPipelineMessages.cs new file mode 100644 index 00000000000..5067a9a64fb --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/FullPipelineMessages.cs @@ -0,0 +1,244 @@ +using Immediate.Handlers.Shared; + +namespace Mocha.Mediator.Benchmarks.Messaging; + +// ─── Full Pipeline Command (Mocha + MediatR shared) ─── + +public sealed record FullPipelineCommand(Guid Id) + : Mocha.Mediator.ICommand, + MediatR.IRequest; + +public sealed class FullPipelineCommandHandler + : Mocha.Mediator.ICommandHandler, + MediatR.IRequestHandler +{ + private static readonly BenchmarkResponse _response = new(Guid.NewGuid()); + private static readonly Task _taskResponse = Task.FromResult(_response); + + public ValueTask HandleAsync( + FullPipelineCommand command, + CancellationToken cancellationToken) + => new(_response); + + Task MediatR.IRequestHandler.Handle( + FullPipelineCommand request, + CancellationToken cancellationToken) + => _taskResponse; +} + +// Mocha pre-processing middleware +public sealed class FullPipelineMochaPreMiddleware +{ + public async ValueTask InvokeAsync(Mocha.Mediator.IMediatorContext context, Mocha.Mediator.MediatorDelegate next) + { + // Pre-processing (no-op) + await next(context); + } + + public static Mocha.Mediator.MediatorMiddlewareConfiguration Create() + => new(static (_, next) => + { + var mw = new FullPipelineMochaPreMiddleware(); + return ctx => mw.InvokeAsync(ctx, next); + }, "Pre"); +} + +// Mocha pipeline behavior middleware +public sealed class FullPipelineMochaBehaviorMiddleware +{ + public async ValueTask InvokeAsync(Mocha.Mediator.IMediatorContext context, Mocha.Mediator.MediatorDelegate next) + { + await next(context); + } + + public static Mocha.Mediator.MediatorMiddlewareConfiguration Create() + => new(static (_, next) => + { + var mw = new FullPipelineMochaBehaviorMiddleware(); + return ctx => mw.InvokeAsync(ctx, next); + }, "Behavior"); +} + +// Mocha post-processing middleware +public sealed class FullPipelineMochaPostMiddleware +{ + public async ValueTask InvokeAsync(Mocha.Mediator.IMediatorContext context, Mocha.Mediator.MediatorDelegate next) + { + await next(context); + // Post-processing (no-op) + } + + public static Mocha.Mediator.MediatorMiddlewareConfiguration Create() + => new(static (_, next) => + { + var mw = new FullPipelineMochaPostMiddleware(); + return ctx => mw.InvokeAsync(ctx, next); + }, "Post"); +} + +// MediatR pre-processor +public sealed class FullPipelineMediatRPreProcessor + : MediatR.Pipeline.IRequestPreProcessor +{ + public Task Process(FullPipelineCommand request, CancellationToken cancellationToken) + => Task.CompletedTask; +} + +// MediatR post-processor +public sealed class FullPipelineMediatRPostProcessor + : MediatR.Pipeline.IRequestPostProcessor +{ + public Task Process(FullPipelineCommand request, BenchmarkResponse response, CancellationToken cancellationToken) + => Task.CompletedTask; +} + +// MediatR pipeline behavior +public sealed class FullPipelineMediatRBehavior + : MediatR.IPipelineBehavior +{ + public async Task Handle( + FullPipelineCommand request, + MediatR.RequestHandlerDelegate next, + CancellationToken cancellationToken) + => await next(); +} + +// ─── MediatorSg (martinothamar) - native pre/post processors + 1 behavior ─── + +public sealed record FullPipelineMediatorSgCommand(Guid Id) + : global::Mediator.IRequest; + +public sealed class FullPipelineMediatorSgCommandHandler + : global::Mediator.IRequestHandler +{ + private static readonly BenchmarkResponse _response = new(Guid.NewGuid()); + + public ValueTask Handle( + FullPipelineMediatorSgCommand request, + CancellationToken cancellationToken) + => new(_response); +} + +// Native pre-processor (inherits MessagePreProcessor which implements IPipelineBehavior) +public sealed class FullPipelineMediatorSgPreProcessor + : global::Mediator.MessagePreProcessor +{ + protected override ValueTask Handle( + FullPipelineMediatorSgCommand message, + CancellationToken cancellationToken) + => default; +} + +// Pipeline behavior +public sealed class FullPipelineMediatorSgBehavior + : global::Mediator.IPipelineBehavior +{ + public ValueTask Handle( + FullPipelineMediatorSgCommand message, + global::Mediator.MessageHandlerDelegate next, + CancellationToken cancellationToken) + => next(message, cancellationToken); +} + +// Native post-processor (inherits MessagePostProcessor which implements IPipelineBehavior) +public sealed class FullPipelineMediatorSgPostProcessor + : global::Mediator.MessagePostProcessor +{ + protected override ValueTask Handle( + FullPipelineMediatorSgCommand message, + BenchmarkResponse response, + CancellationToken cancellationToken) + => default; +} + +// ─── SwitchMediator - 3 behaviors to match pipeline depth ─── + +public sealed record FullPipelineSwitchMediatorCommand(Guid Id) + : global::Mediator.Switch.IRequest; + +public sealed class FullPipelineSwitchMediatorCommandHandler + : global::Mediator.Switch.IValueRequestHandler +{ + private static readonly BenchmarkResponse _response = new(Guid.NewGuid()); + + public ValueTask Handle( + FullPipelineSwitchMediatorCommand request, + CancellationToken cancellationToken) + => new(_response); +} + +public sealed class FullPipelineSwitchMediatorPreBehavior + : global::Mediator.Switch.IValuePipelineBehavior +{ + public ValueTask Handle( + FullPipelineSwitchMediatorCommand request, + global::Mediator.Switch.ValueRequestHandlerDelegate next, + CancellationToken cancellationToken) + => next(cancellationToken); +} + +public sealed class FullPipelineSwitchMediatorMainBehavior + : global::Mediator.Switch.IValuePipelineBehavior +{ + public ValueTask Handle( + FullPipelineSwitchMediatorCommand request, + global::Mediator.Switch.ValueRequestHandlerDelegate next, + CancellationToken cancellationToken) + => next(cancellationToken); +} + +public sealed class FullPipelineSwitchMediatorPostBehavior + : global::Mediator.Switch.IValuePipelineBehavior +{ + public ValueTask Handle( + FullPipelineSwitchMediatorCommand request, + global::Mediator.Switch.ValueRequestHandlerDelegate next, + CancellationToken cancellationToken) + => next(cancellationToken); +} + +// ─── Immediate.Handlers - 3 behaviors to match pipeline depth ─── + +public sealed class ImmediateFullPipelinePreBehavior + : Behavior +{ + public override async ValueTask HandleAsync( + TRequest request, + CancellationToken cancellationToken) + => await Next(request, cancellationToken); +} + +public sealed class ImmediateFullPipelineMainBehavior + : Behavior +{ + public override async ValueTask HandleAsync( + TRequest request, + CancellationToken cancellationToken) + => await Next(request, cancellationToken); +} + +public sealed class ImmediateFullPipelinePostBehavior + : Behavior +{ + public override async ValueTask HandleAsync( + TRequest request, + CancellationToken cancellationToken) + => await Next(request, cancellationToken); +} + +[Handler] +[Behaviors( + typeof(ImmediateFullPipelinePreBehavior<,>), + typeof(ImmediateFullPipelineMainBehavior<,>), + typeof(ImmediateFullPipelinePostBehavior<,>))] +public static partial class ImmediateFullPipelineCommandHandler +{ + public sealed record Command(Guid Id); + + private static readonly BenchmarkResponse _response = new(Guid.NewGuid()); + + private static ValueTask HandleAsync( + Command command, + CancellationToken ct) + => new(_response); +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/ImmediateHandlersMessages.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/ImmediateHandlersMessages.cs new file mode 100644 index 00000000000..43dab0268fa --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/ImmediateHandlersMessages.cs @@ -0,0 +1,44 @@ +using Immediate.Handlers.Shared; + +namespace Mocha.Mediator.Benchmarks.Messaging; + +// Command handler for Immediate.Handlers (source-generated) +[Handler] +public static partial class ImmediateCommandHandler +{ + public sealed record Command(Guid Id); + + private static readonly BenchmarkResponse _response = new(Guid.NewGuid()); + + private static ValueTask HandleAsync( + Command command, + CancellationToken ct) + => new(_response); +} + +// Pipeline behavior for Immediate.Handlers +public sealed class ImmediateBenchmarkBehavior + : Behavior +{ + public override async ValueTask HandleAsync( + TRequest request, + CancellationToken cancellationToken) + { + return await Next(request, cancellationToken); + } +} + +// Separate handler with pipeline behavior for pipeline benchmarks +[Handler] +[Behaviors(typeof(ImmediateBenchmarkBehavior<,>))] +public static partial class ImmediatePipelineCommandHandler +{ + public sealed record Command(Guid Id); + + private static readonly BenchmarkResponse _response = new(Guid.NewGuid()); + + private static ValueTask HandleAsync( + Command command, + CancellationToken ct) + => new(_response); +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MassTransitMessages.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MassTransitMessages.cs new file mode 100644 index 00000000000..693684d0105 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MassTransitMessages.cs @@ -0,0 +1,28 @@ +using MassTransit; + +namespace Mocha.Mediator.Benchmarks.Messaging; + +// Command for MassTransit (plain POCO, no marker interfaces) +public sealed record MassTransitCommand(Guid Id); + +// Response contract for MassTransit request/response +public sealed record MassTransitCommandResponse(Guid Id); + +// Notification for MassTransit +public sealed record MassTransitNotification(Guid Id); + +// Consumer handling request/response +public sealed class MassTransitCommandConsumer : IConsumer +{ + private static readonly MassTransitCommandResponse _response = new(Guid.NewGuid()); + + public Task Consume(ConsumeContext context) + => context.RespondAsync(_response); +} + +// Consumer handling notification (event) +public sealed class MassTransitNotificationConsumer : IConsumer +{ + public Task Consume(ConsumeContext context) + => Task.CompletedTask; +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MediatorSgHelper.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MediatorSgHelper.cs new file mode 100644 index 00000000000..d32af6301e7 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MediatorSgHelper.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Benchmarks.Messaging; + +internal static class MediatorSgHelper +{ + public static void Register(IServiceCollection services) + => global::Benchmarks.Internal.MediatorSgRegistration.Register(services); +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MediatorSgMessages.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MediatorSgMessages.cs new file mode 100644 index 00000000000..6093255c19b --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/MediatorSgMessages.cs @@ -0,0 +1,43 @@ +namespace Mocha.Mediator.Benchmarks.Messaging; + +// Command for martinothamar Mediator (source-generated) +public sealed record MediatorSgCommand(Guid Id) : global::Mediator.IRequest; + +// Notification for martinothamar Mediator +public sealed record MediatorSgNotification(Guid Id) : global::Mediator.INotification; + +// Command handler for martinothamar Mediator +public sealed class MediatorSgCommandHandler + : global::Mediator.IRequestHandler +{ + private static readonly BenchmarkResponse _response = new(Guid.NewGuid()); + + public async ValueTask Handle( + MediatorSgCommand request, + CancellationToken cancellationToken) + { + await Task.Yield(); + return _response; + } +} + +// Notification handler for martinothamar Mediator +public sealed class MediatorSgNotificationHandler + : global::Mediator.INotificationHandler +{ + public ValueTask Handle( + MediatorSgNotification notification, + CancellationToken cancellationToken) + => default; +} + +// Pipeline behavior for martinothamar Mediator +public sealed class MediatorSgPipelineBehavior + : global::Mediator.IPipelineBehavior +{ + public ValueTask Handle( + MediatorSgCommand message, + global::Mediator.MessageHandlerDelegate next, + CancellationToken cancellationToken) + => next(message, cancellationToken); +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/Messages.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/Messages.cs new file mode 100644 index 00000000000..5b8c06d1774 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/Messages.cs @@ -0,0 +1,84 @@ +namespace Mocha.Mediator.Benchmarks.Messaging; + +// Command that implements both Mocha and MediatR interfaces +public sealed record BenchmarkCommand(Guid Id) + : Mocha.Mediator.ICommand, + MediatR.IRequest; + +// Response type shared by both +public sealed record BenchmarkResponse(Guid Id); + +// Notification that implements both Mocha and MediatR interfaces +public sealed record BenchmarkNotification(Guid Id) + : Mocha.Mediator.INotification, + MediatR.INotification; + +// Command handler implementing both interfaces +public sealed class BenchmarkCommandHandler + : Mocha.Mediator.ICommandHandler, + MediatR.IRequestHandler +{ + private static readonly BenchmarkResponse _response = new(Guid.NewGuid()); + private static readonly Task _taskResponse = Task.FromResult(_response); + + public async ValueTask HandleAsync( + BenchmarkCommand command, + CancellationToken cancellationToken) + { + await Task.Yield(); + return _response; + } + + async Task MediatR.IRequestHandler.Handle( + BenchmarkCommand request, + CancellationToken cancellationToken) + { + await Task.Yield(); + return _response; + } +} + +// Notification handler implementing both interfaces +public sealed class BenchmarkNotificationHandler + : Mocha.Mediator.INotificationHandler, + MediatR.INotificationHandler +{ + public ValueTask HandleAsync( + BenchmarkNotification notification, + CancellationToken cancellationToken) + => default; + + Task MediatR.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + => Task.CompletedTask; +} + +// Mocha middleware (replaces pipeline behavior) +public sealed class BenchmarkMochaMiddleware +{ + public async ValueTask InvokeAsync(Mocha.Mediator.IMediatorContext context, Mocha.Mediator.MediatorDelegate next) + { + await next(context); + } + + public static Mocha.Mediator.MediatorMiddlewareConfiguration Create() + => new( + static (_, next) => + { + var middleware = new BenchmarkMochaMiddleware(); + return ctx => middleware.InvokeAsync(ctx, next); + }, + "BenchmarkBehavior"); +} + +// MediatR pipeline behavior +public sealed class BenchmarkMediatRPipelineBehavior + : MediatR.IPipelineBehavior +{ + public async Task Handle( + BenchmarkCommand request, + MediatR.RequestHandlerDelegate next, + CancellationToken cancellationToken) + => await next(); +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/NotificationBenchmarks.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/NotificationBenchmarks.cs new file mode 100644 index 00000000000..d4156074115 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/NotificationBenchmarks.cs @@ -0,0 +1,169 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using MassTransit.Mediator; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Mocha.Mediator.Benchmarks.Messaging; + +/// +/// Compares notification publish performance across mediator libraries. +/// Each benchmark resolves mediators from a fresh scope to mirror real request handling. +/// Immediate.Handlers is excluded as it does not support notifications. +/// +[MemoryDiagnoser] +[RankColumn] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class NotificationBenchmarks +{ + private ServiceProvider _rootProvider = null!; + private IServiceProvider? _mediatorSgProvider; + private Wolverine.IMessageBus? _wolverineBus; + private IHost? _wolverineHost; + private IServiceProvider? _switchMediatorProvider; + private IServiceProvider? _massTransitProvider; + private BenchmarkNotificationHandler _handler = null!; + private BenchmarkNotification _notification = null!; + private MediatorSgNotification _mediatorSgNotification = null!; + private WolverineNotification _wolverineNotification = null!; + private SwitchMediatorNotification _switchMediatorNotification = null!; + private MassTransitNotification _massTransitNotification = null!; + + [GlobalSetup] + public void Setup() + { + // Mocha + MediatR (shared provider) + var services = new ServiceCollection(); + MediatorServiceCollectionExtensions.AddMediator(services).AddBenchmarks(); + services.AddMediatR(opts => + opts.RegisterServicesFromAssembly(typeof(BenchmarkNotificationHandler).Assembly)); + _rootProvider = services.BuildServiceProvider(); + + // martinothamar Mediator (separate provider) + try + { + _mediatorSgProvider = MediatorSgFactory.CreateServiceProvider(); + } + catch (InvalidOperationException) + { + } + + // Wolverine + try + { + (_wolverineHost, _wolverineBus) = WolverineFactory.Create(); + } + catch (InvalidOperationException) + { + } + + // SwitchMediator (separate provider) + try + { + _switchMediatorProvider = SwitchMediatorFactory.CreateServiceProvider(); + } + catch (InvalidOperationException) + { + } + + // MassTransit Mediator (separate provider) + try + { + _massTransitProvider = MassTransitFactory.CreateServiceProvider(); + } + catch (InvalidOperationException) + { + } + + _handler = new BenchmarkNotificationHandler(); + _notification = new BenchmarkNotification(Guid.NewGuid()); + _mediatorSgNotification = new MediatorSgNotification(Guid.NewGuid()); + _wolverineNotification = new WolverineNotification(Guid.NewGuid()); + _switchMediatorNotification = new SwitchMediatorNotification(Guid.NewGuid()); + _massTransitNotification = new MassTransitNotification(Guid.NewGuid()); + } + + [GlobalCleanup] + public void Cleanup() + { + _wolverineHost?.StopAsync().GetAwaiter().GetResult(); + _wolverineHost?.Dispose(); + } + + [Benchmark] + public Task PublishNotification_MediatR() + { + var scope = _rootProvider.CreateScope(); + var mediatr = scope.ServiceProvider.GetRequiredService(); + return mediatr.Publish(_notification, CancellationToken.None); + } + + [Benchmark] + public ValueTask PublishNotification_Mocha_IMediator() + { + var scope = _rootProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.PublishAsync(_notification, CancellationToken.None); + } + + [Benchmark] + public ValueTask PublishNotification_Mocha_Concrete() + { + var scope = _rootProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.PublishAsync(_notification, CancellationToken.None); + } + + [Benchmark] + public ValueTask PublishNotification_MediatorSg() + { + if (_mediatorSgProvider is null) + return default; + + var scope = _mediatorSgProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.Publish(_mediatorSgNotification, CancellationToken.None); + } + + [Benchmark] + public Task PublishNotification_Wolverine() + { + if (_wolverineBus is not null) + { + return _wolverineBus.InvokeAsync(_wolverineNotification, CancellationToken.None); + } + + return Task.CompletedTask; + } + + [Benchmark] + public ValueTask PublishNotification_SwitchMediator() + { + if (_switchMediatorProvider is null) + return default; + + var scope = _switchMediatorProvider.CreateScope(); + var publisher = scope.ServiceProvider.GetRequiredService(); + return publisher.Publish(_switchMediatorNotification, CancellationToken.None); + } + + [Benchmark] + public Task PublishNotification_MassTransit() + { + if (_massTransitProvider is not null) + { + var scope = _massTransitProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.Publish( + _massTransitNotification, CancellationToken.None); + } + + return Task.CompletedTask; + } + + [Benchmark(Baseline = true)] + public ValueTask PublishNotification_Baseline() + { + return _handler.HandleAsync(_notification, CancellationToken.None); + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/PipelineBenchmarks.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/PipelineBenchmarks.cs new file mode 100644 index 00000000000..ee97fbb4f67 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/PipelineBenchmarks.cs @@ -0,0 +1,120 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using Immediate.Handlers.Shared; +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Benchmarks.Messaging; + +/// +/// Compares pipeline behavior overhead across mediator libraries. +/// Each benchmark uses a singleton mediator resolved once during setup. +/// Wolverine and MassTransit are excluded as they use fundamentally different middleware models. +/// +[MemoryDiagnoser] +[RankColumn] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class PipelineBenchmarks +{ + private MediatR.IMediator _mediatr = null!; + private Mocha.Mediator.IMediator _mocha = null!; + private global::Mediator.IMediator? _mediatorSg; + private global::Mediator.Switch.IValueSender? _switchMediator; + private IHandler? _immediateHandler; + private BenchmarkCommand _command = null!; + private MediatorSgCommand _mediatorSgCommand = null!; + private SwitchMediatorCommand _switchMediatorCommand = null!; + private ImmediatePipelineCommandHandler.Command _immediateCommand = null!; + + [GlobalSetup] + public void Setup() + { + // Mocha + MediatR (shared provider) + var services = new ServiceCollection(); + var builder = MediatorServiceCollectionExtensions.AddMediator(services); + builder.Use(BenchmarkMochaMiddleware.Create()); + builder.AddBenchmarks(); + services.AddMediatR(opts => + { + opts.RegisterServicesFromAssembly(typeof(BenchmarkCommandHandler).Assembly); + opts.AddBehavior, + BenchmarkMediatRPipelineBehavior>(); + }); + var rootProvider = services.BuildServiceProvider(); + _mediatr = rootProvider.GetRequiredService(); + _mocha = rootProvider.GetRequiredService(); + + // martinothamar Mediator with pipeline behavior + try + { + var provider = MediatorSgFactory.CreateServiceProviderWithPipeline(); + _mediatorSg = provider.GetRequiredService(); + } + catch (InvalidOperationException) + { + } + + // SwitchMediator with pipeline behavior + try + { + var provider = SwitchMediatorFactory.CreateServiceProviderWithPipeline(); + _switchMediator = provider.GetRequiredService(); + } + catch (InvalidOperationException) + { + } + + // Immediate.Handlers with pipeline behavior + try + { + var provider = ImmediateHandlersFactory.CreateServiceProviderWithPipeline(); + _immediateHandler = provider.GetRequiredService>(); + } + catch (InvalidOperationException) + { + } + + _command = new BenchmarkCommand(Guid.NewGuid()); + _mediatorSgCommand = new MediatorSgCommand(Guid.NewGuid()); + _switchMediatorCommand = new SwitchMediatorCommand(Guid.NewGuid()); + _immediateCommand = new ImmediatePipelineCommandHandler.Command(Guid.NewGuid()); + } + + [Benchmark(Baseline = true)] + public Task WithPipeline_MediatR() + { + return _mediatr.Send(_command, CancellationToken.None); + } + + [Benchmark] + public ValueTask WithPipeline_Mocha() + { + return _mocha.SendAsync(_command, CancellationToken.None); + } + + [Benchmark] + public ValueTask WithPipeline_MediatorSg() + { + if (_mediatorSg is null) + return new ValueTask(new BenchmarkResponse(Guid.Empty)); + + return _mediatorSg.Send(_mediatorSgCommand, CancellationToken.None); + } + + // [Benchmark] + public ValueTask WithPipeline_SwitchMediator() + { + if (_switchMediator is null) + return new ValueTask(new BenchmarkResponse(Guid.Empty)); + + return _switchMediator.Send(_switchMediatorCommand, CancellationToken.None); + } + + // [Benchmark] + public ValueTask WithPipeline_ImmediateHandlers() + { + if (_immediateHandler is null) + return new ValueTask(new BenchmarkResponse(Guid.Empty)); + + return _immediateHandler.HandleAsync(_immediateCommand, CancellationToken.None); + } +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/SwitchMediatorMessages.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/SwitchMediatorMessages.cs new file mode 100644 index 00000000000..603a3b1bf27 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/SwitchMediatorMessages.cs @@ -0,0 +1,46 @@ +namespace Mocha.Mediator.Benchmarks.Messaging; + +// SwitchMediator partial classsource generator populates the dispatch logic +[global::Mediator.Switch.SwitchMediator] +public partial class BenchmarkSwitchMediator; + +// Command for SwitchMediator (ValueTask path) +public sealed record SwitchMediatorCommand(Guid Id) + : global::Mediator.Switch.IRequest; + +// Notification for SwitchMediator +public sealed record SwitchMediatorNotification(Guid Id) + : global::Mediator.Switch.INotification; + +// Command handler (ValueTask variant for zero-alloc hot path) +public sealed class SwitchMediatorCommandHandler + : global::Mediator.Switch.IValueRequestHandler +{ + private static readonly BenchmarkResponse _response = new(Guid.NewGuid()); + + public ValueTask Handle( + SwitchMediatorCommand request, + CancellationToken cancellationToken) + => new(_response); +} + +// Notification handler (ValueTask variant) +public sealed class SwitchMediatorNotificationHandler + : global::Mediator.Switch.IValueNotificationHandler +{ + public ValueTask Handle( + SwitchMediatorNotification notification, + CancellationToken cancellationToken) + => default; +} + +// Pipeline behavior (ValueTask variant) +public sealed class SwitchMediatorPipelineBehavior + : global::Mediator.Switch.IValuePipelineBehavior +{ + public ValueTask Handle( + SwitchMediatorCommand request, + global::Mediator.Switch.ValueRequestHandlerDelegate next, + CancellationToken cancellationToken) + => next(cancellationToken); +} diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/WolverineMessages.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/WolverineMessages.cs new file mode 100644 index 00000000000..f8279b28083 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Messaging/WolverineMessages.cs @@ -0,0 +1,23 @@ +namespace Mocha.Mediator.Benchmarks.Messaging; + +// Command for Wolverine (plain record, no interface needed) +public sealed record WolverineCommand(Guid Id); + +// Notification for Wolverine +public sealed record WolverineNotification(Guid Id); + +// Command handler for Wolverine (convention-based) +#pragma warning disable RCS1102 // Wolverine convention requires instance handlers for DI registration +public class WolverineCommandHandler +{ + private static readonly BenchmarkResponse _response = new(Guid.NewGuid()); + + public BenchmarkResponse Handle(WolverineCommand command) => _response; +} + +// Notification handler for Wolverine (convention-based) +public class WolverineNotificationHandler +{ + public void Handle(WolverineNotification notification) { } +} +#pragma warning restore RCS1102 diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Mocha.Mediator.Benchmarks.csproj b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Mocha.Mediator.Benchmarks.csproj new file mode 100644 index 00000000000..dcdeb19350b --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Mocha.Mediator.Benchmarks.csproj @@ -0,0 +1,39 @@ + + + Exe + net8.0;net9.0;net10.0 + latest + enable + enable + $(NoWarn);NU1605;NU1608 + true + Generated + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Program.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Program.cs new file mode 100644 index 00000000000..98103c9af46 --- /dev/null +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Program.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.CsProj; +using BenchmarkDotNet.Toolchains.DotNetCli; + +var config = DefaultConfig.Instance + .AddJob(Job.Default.WithToolchain( + CsProjCoreToolchain.From( + new NetCoreAppSettings("net9.0", null, ".NET 9.0")))); + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); diff --git a/src/Mocha/src/Demo/Demo.Billing/Commands/ProcessPaymentCommand.cs b/src/Mocha/src/Demo/Demo.Billing/Commands/ProcessPaymentCommand.cs new file mode 100644 index 00000000000..183d25d9d83 --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Billing/Commands/ProcessPaymentCommand.cs @@ -0,0 +1,57 @@ +using Demo.Billing.Data; +using Demo.Billing.Entities; +using Demo.Contracts.Events; +using Microsoft.EntityFrameworkCore; +using Mocha; +using Mocha.Mediator; + +namespace Demo.Billing.Commands; + +public record ProcessPaymentCommand(Guid InvoiceId, string PaymentMethod) : ICommand; + +public record ProcessPaymentResult(bool Success, Payment? Payment = null, string? Error = null); + +public class ProcessPaymentCommandHandler(BillingDbContext db, IMessageBus messageBus) + : ICommandHandler +{ + public async ValueTask HandleAsync( + ProcessPaymentCommand command, CancellationToken cancellationToken) + { + var invoice = await db.Invoices.FirstOrDefaultAsync( + i => i.Id == command.InvoiceId, cancellationToken); + if (invoice is null) + return new ProcessPaymentResult(false, Error: "Invoice not found"); + + if (invoice.Status == InvoiceStatus.Paid) + return new ProcessPaymentResult(false, Error: "Invoice already paid"); + + var payment = new Payment + { + Id = Guid.NewGuid(), + InvoiceId = invoice.Id, + Amount = invoice.Amount, + Method = command.PaymentMethod, + Status = PaymentStatus.Completed, + ProcessedAt = DateTimeOffset.UtcNow + }; + + db.Payments.Add(payment); + invoice.Status = InvoiceStatus.Paid; + invoice.UpdatedAt = DateTimeOffset.UtcNow; + await db.SaveChangesAsync(cancellationToken); + + await messageBus.PublishAsync( + new PaymentCompletedEvent + { + PaymentId = payment.Id, + InvoiceId = invoice.Id, + OrderId = invoice.OrderId, + Amount = payment.Amount, + PaymentMethod = payment.Method, + ProcessedAt = payment.ProcessedAt + }, + cancellationToken); + + return new ProcessPaymentResult(true, payment); + } +} diff --git a/src/Mocha/src/Demo/Demo.Billing/Demo.Billing.csproj b/src/Mocha/src/Demo/Demo.Billing/Demo.Billing.csproj index 71a5395c230..a3c3fe59a05 100644 --- a/src/Mocha/src/Demo/Demo.Billing/Demo.Billing.csproj +++ b/src/Mocha/src/Demo/Demo.Billing/Demo.Billing.csproj @@ -7,6 +7,8 @@ + + diff --git a/src/Mocha/src/Demo/Demo.Billing/Program.cs b/src/Mocha/src/Demo/Demo.Billing/Program.cs index c86b2da7d1a..f6016155a49 100644 --- a/src/Mocha/src/Demo/Demo.Billing/Program.cs +++ b/src/Mocha/src/Demo/Demo.Billing/Program.cs @@ -1,9 +1,12 @@ +using Demo.Billing.Commands; using Demo.Billing.Data; -using Demo.Billing.Entities; using Demo.Billing.Handlers; +using Demo.Billing.Queries; using Demo.Contracts.Events; using Microsoft.EntityFrameworkCore; using Mocha; +using Mocha.EntityFrameworkCore; +using Mocha.Mediator; using Mocha.Transport.RabbitMQ; var builder = WebApplication.CreateBuilder(args); @@ -16,6 +19,11 @@ // RabbitMQ builder.AddRabbitMQClient("rabbitmq", x => x.DisableTracing = true); +// Mocha.Mediator +builder.Services.AddMediator() + .AddBilling() + .UseEntityFrameworkTransactions(); + // MessageBus builder .Services.AddMessageBus() @@ -52,89 +60,52 @@ app.MapGet("/", () => "Billing Service"); // Invoices -app.MapGet("/api/invoices", async (BillingDbContext db) => await db.Invoices.Include(i => i.Payments).ToListAsync()); - -app.MapGet( - "/api/invoices/{id:guid}", - async (Guid id, BillingDbContext db) => - await db.Invoices.Include(i => i.Payments).FirstOrDefaultAsync(i => i.Id == id) is { } invoice - ? Results.Ok(invoice) - : Results.NotFound()); - -app.MapGet( - "/api/invoices/order/{orderId:guid}", - async (Guid orderId, BillingDbContext db) => - await db.Invoices.Include(i => i.Payments).FirstOrDefaultAsync(i => i.OrderId == orderId) is { } invoice - ? Results.Ok(invoice) - : Results.NotFound()); - -// Payments - manually trigger payment processing -app.MapPost( - "/api/payments/{invoiceId:guid}", - async (Guid invoiceId, ProcessPaymentRequest request, BillingDbContext db, IMessageBus messageBus) => +app.MapGet("/api/invoices", async (ISender sender) => + await sender.QueryAsync(new GetInvoicesQuery())); + +app.MapGet("/api/invoices/{id:guid}", async (Guid id, ISender sender) => + await sender.QueryAsync(new GetInvoiceByIdQuery(id)) is { } invoice + ? Results.Ok(invoice) + : Results.NotFound()); + +app.MapGet("/api/invoices/order/{orderId:guid}", async (Guid orderId, ISender sender) => + await sender.QueryAsync(new GetInvoiceByOrderIdQuery(orderId)) is { } invoice + ? Results.Ok(invoice) + : Results.NotFound()); + +// Payments +app.MapPost("/api/payments/{invoiceId:guid}", async (Guid invoiceId, ProcessPaymentRequest request, ISender sender) => +{ + var result = await sender.SendAsync(new ProcessPaymentCommand(invoiceId, request.PaymentMethod)); + + if (!result.Success) { - var invoice = await db.Invoices.FirstOrDefaultAsync(i => i.Id == invoiceId); - if (invoice is null) - { - return Results.NotFound("Invoice not found"); - } - - if (invoice.Status == InvoiceStatus.Paid) - { - return Results.BadRequest("Invoice already paid"); - } - - var payment = new Payment - { - Id = Guid.NewGuid(), - InvoiceId = invoice.Id, - Amount = invoice.Amount, - Method = request.PaymentMethod, - Status = PaymentStatus.Completed, - ProcessedAt = DateTimeOffset.UtcNow - }; - - db.Payments.Add(payment); - invoice.Status = InvoiceStatus.Paid; - invoice.UpdatedAt = DateTimeOffset.UtcNow; - await db.SaveChangesAsync(); - - // Publish PaymentCompletedEvent - await messageBus.PublishAsync( - new PaymentCompletedEvent - { - PaymentId = payment.Id, - InvoiceId = invoice.Id, - OrderId = invoice.OrderId, - Amount = payment.Amount, - PaymentMethod = payment.Method, - ProcessedAt = payment.ProcessedAt - }, - CancellationToken.None); - - return Results.Ok(payment); - }); - -app.MapGet("/api/payments", async (BillingDbContext db) => await db.Payments.Include(p => p.Invoice).ToListAsync()); + return result.Error == "Invoice not found" + ? Results.NotFound(result.Error) + : Results.BadRequest(result.Error); + } + + return Results.Ok(result.Payment); +}); + +app.MapGet("/api/payments", async (ISender sender) => + await sender.QueryAsync(new GetPaymentsQuery())); // Refunds -app.MapGet("/api/refunds", async (BillingDbContext db) => await db.Refunds.ToListAsync()); - -app.MapGet( - "/api/refunds/order/{orderId:guid}", - async (Guid orderId, BillingDbContext db) => await db.Refunds.Where(r => r.OrderId == orderId).ToListAsync()); - -// Revenue Summaries (batch analytics) -app.MapGet( - "/api/revenue-summaries", - async (BillingDbContext db) => await db.RevenueSummaries.OrderByDescending(r => r.CreatedAt).ToListAsync()); - -app.MapGet( - "/api/revenue-summaries/latest", - async (BillingDbContext db) => - await db.RevenueSummaries.OrderByDescending(r => r.CreatedAt).FirstOrDefaultAsync() is { } summary - ? Results.Ok(summary) - : Results.NotFound()); +app.MapGet("/api/refunds", async (ISender sender) => + await sender.QueryAsync(new GetRefundsQuery())); + +app.MapGet("/api/refunds/order/{orderId:guid}", async (Guid orderId, ISender sender) => + await sender.QueryAsync(new GetRefundsByOrderIdQuery(orderId))); + +// Revenue Summaries +app.MapGet("/api/revenue-summaries", async (ISender sender) => + await sender.QueryAsync(new GetRevenueSummariesQuery())); + +app.MapGet("/api/revenue-summaries/latest", async (ISender sender) => + await sender.QueryAsync(new GetLatestRevenueSummaryQuery()) is { } summary + ? Results.Ok(summary) + : Results.NotFound()); app.Run(); diff --git a/src/Mocha/src/Demo/Demo.Billing/Queries/InvoiceQueries.cs b/src/Mocha/src/Demo/Demo.Billing/Queries/InvoiceQueries.cs new file mode 100644 index 00000000000..4d4f651e189 --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Billing/Queries/InvoiceQueries.cs @@ -0,0 +1,38 @@ +using Demo.Billing.Data; +using Demo.Billing.Entities; +using Microsoft.EntityFrameworkCore; +using Mocha.Mediator; + +namespace Demo.Billing.Queries; + +public record GetInvoicesQuery : IQuery>; + +public class GetInvoicesQueryHandler(BillingDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetInvoicesQuery query, CancellationToken cancellationToken) + => await db.Invoices.Include(i => i.Payments).ToListAsync(cancellationToken); +} + +public record GetInvoiceByIdQuery(Guid Id) : IQuery; + +public class GetInvoiceByIdQueryHandler(BillingDbContext db) + : IQueryHandler +{ + public async ValueTask HandleAsync( + GetInvoiceByIdQuery query, CancellationToken cancellationToken) + => await db.Invoices.Include(i => i.Payments) + .FirstOrDefaultAsync(i => i.Id == query.Id, cancellationToken); +} + +public record GetInvoiceByOrderIdQuery(Guid OrderId) : IQuery; + +public class GetInvoiceByOrderIdQueryHandler(BillingDbContext db) + : IQueryHandler +{ + public async ValueTask HandleAsync( + GetInvoiceByOrderIdQuery query, CancellationToken cancellationToken) + => await db.Invoices.Include(i => i.Payments) + .FirstOrDefaultAsync(i => i.OrderId == query.OrderId, cancellationToken); +} diff --git a/src/Mocha/src/Demo/Demo.Billing/Queries/PaymentQueries.cs b/src/Mocha/src/Demo/Demo.Billing/Queries/PaymentQueries.cs new file mode 100644 index 00000000000..ceaa59542dd --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Billing/Queries/PaymentQueries.cs @@ -0,0 +1,16 @@ +using Demo.Billing.Data; +using Demo.Billing.Entities; +using Microsoft.EntityFrameworkCore; +using Mocha.Mediator; + +namespace Demo.Billing.Queries; + +public record GetPaymentsQuery : IQuery>; + +public class GetPaymentsQueryHandler(BillingDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetPaymentsQuery query, CancellationToken cancellationToken) + => await db.Payments.Include(p => p.Invoice).ToListAsync(cancellationToken); +} diff --git a/src/Mocha/src/Demo/Demo.Billing/Queries/RefundQueries.cs b/src/Mocha/src/Demo/Demo.Billing/Queries/RefundQueries.cs new file mode 100644 index 00000000000..3865a8a4ce7 --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Billing/Queries/RefundQueries.cs @@ -0,0 +1,26 @@ +using Demo.Billing.Data; +using Demo.Billing.Entities; +using Microsoft.EntityFrameworkCore; +using Mocha.Mediator; + +namespace Demo.Billing.Queries; + +public record GetRefundsQuery : IQuery>; + +public class GetRefundsQueryHandler(BillingDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetRefundsQuery query, CancellationToken cancellationToken) + => await db.Refunds.ToListAsync(cancellationToken); +} + +public record GetRefundsByOrderIdQuery(Guid OrderId) : IQuery>; + +public class GetRefundsByOrderIdQueryHandler(BillingDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetRefundsByOrderIdQuery query, CancellationToken cancellationToken) + => await db.Refunds.Where(r => r.OrderId == query.OrderId).ToListAsync(cancellationToken); +} diff --git a/src/Mocha/src/Demo/Demo.Billing/Queries/RevenueSummaryQueries.cs b/src/Mocha/src/Demo/Demo.Billing/Queries/RevenueSummaryQueries.cs new file mode 100644 index 00000000000..959309288cb --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Billing/Queries/RevenueSummaryQueries.cs @@ -0,0 +1,27 @@ +using Demo.Billing.Data; +using Demo.Billing.Entities; +using Microsoft.EntityFrameworkCore; +using Mocha.Mediator; + +namespace Demo.Billing.Queries; + +public record GetRevenueSummariesQuery : IQuery>; + +public class GetRevenueSummariesQueryHandler(BillingDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetRevenueSummariesQuery query, CancellationToken cancellationToken) + => await db.RevenueSummaries.OrderByDescending(r => r.CreatedAt).ToListAsync(cancellationToken); +} + +public record GetLatestRevenueSummaryQuery : IQuery; + +public class GetLatestRevenueSummaryQueryHandler(BillingDbContext db) + : IQueryHandler +{ + public async ValueTask HandleAsync( + GetLatestRevenueSummaryQuery query, CancellationToken cancellationToken) + => await db.RevenueSummaries.OrderByDescending(r => r.CreatedAt) + .FirstOrDefaultAsync(cancellationToken); +} diff --git a/src/Mocha/src/Demo/Demo.Catalog/Commands/InitiateReturnCommand.cs b/src/Mocha/src/Demo/Demo.Catalog/Commands/InitiateReturnCommand.cs new file mode 100644 index 00000000000..86c5525630b --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Catalog/Commands/InitiateReturnCommand.cs @@ -0,0 +1,81 @@ +using Demo.Catalog.Data; +using Demo.Catalog.Entities; +using Demo.Contracts.Commands; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Mocha; +using Mocha.Mediator; + +namespace Demo.Catalog.Commands; + +public record InitiateReturnCommand( + Guid OrderId, + Guid ShipmentId, + string Reason) : ICommand; + +public record InitiateReturnResult( + bool Success, + Guid? ReturnId = null, + string? ReturnTrackingNumber = null, + string? ReturnLabelUrl = null, + string? Error = null); + +public class InitiateReturnCommandHandler( + CatalogDbContext db, + IMessageBus messageBus, + ILogger logger) + : ICommandHandler +{ + public async ValueTask HandleAsync( + InitiateReturnCommand command, CancellationToken cancellationToken) + { + var order = await db.Orders.Include(o => o.Product) + .FirstOrDefaultAsync(o => o.Id == command.OrderId, cancellationToken); + if (order is null) + return new InitiateReturnResult(false, Error: "Order not found"); + + if (order.Status != OrderStatus.Delivered && order.Status != OrderStatus.Shipping) + return new InitiateReturnResult(false, Error: $"Order cannot be returned in status: {order.Status}"); + + logger.LogInformation("Creating return label for order {OrderId}", command.OrderId); + + try + { + var labelResponse = await messageBus.RequestAsync( + new CreateReturnLabelCommand + { + OrderId = command.OrderId, + OriginalShipmentId = command.ShipmentId, + CustomerAddress = order.ShippingAddress, + CustomerId = order.CustomerId, + ProductId = order.ProductId, + Quantity = order.Quantity, + Amount = order.TotalAmount, + Reason = command.Reason + }, + cancellationToken); + + if (!labelResponse.Success) + return new InitiateReturnResult(false, Error: $"Failed to create return label: {labelResponse.FailureReason}"); + + order.Status = OrderStatus.ReturnInitiated; + order.UpdatedAt = DateTimeOffset.UtcNow; + await db.SaveChangesAsync(cancellationToken); + + logger.LogInformation( + "Return label created for order {OrderId}: {ReturnId}, tracking: {Tracking}", + command.OrderId, labelResponse.ReturnId, labelResponse.ReturnTrackingNumber); + + return new InitiateReturnResult( + true, + labelResponse.ReturnId, + labelResponse.ReturnTrackingNumber, + labelResponse.ReturnLabelUrl); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create return label for order {OrderId}", command.OrderId); + return new InitiateReturnResult(false, Error: "Failed to create return label: " + ex.Message); + } + } +} diff --git a/src/Mocha/src/Demo/Demo.Catalog/Commands/PlaceBulkOrderCommand.cs b/src/Mocha/src/Demo/Demo.Catalog/Commands/PlaceBulkOrderCommand.cs new file mode 100644 index 00000000000..52165b95a60 --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Catalog/Commands/PlaceBulkOrderCommand.cs @@ -0,0 +1,54 @@ +using Demo.Contracts.Events; +using Microsoft.Extensions.Logging; +using Mocha; +using Mocha.Mediator; + +namespace Demo.Catalog.Commands; + +public record PlaceBulkOrderCommand(int Count) : ICommand; + +public record BulkOrderResult(int Dispatched, long ElapsedMs); + +public class PlaceBulkOrderCommandHandler(IMessageBus messageBus, ILogger logger) + : ICommandHandler +{ + public async ValueTask HandleAsync( + PlaceBulkOrderCommand command, CancellationToken cancellationToken) + { + var count = command.Count is > 0 ? command.Count : 2000; + + var products = new[] + { + ("Wireless Headphones", 299.99m), + ("Mechanical Keyboard", 149.99m), + ("Clean Code", 39.99m) + }; + + logger.LogInformation("Dispatching {Count} BulkOrderEvents", count); + var sw = System.Diagnostics.Stopwatch.StartNew(); + + for (var i = 0; i < count; i++) + { + var (name, price) = products[i % products.Length]; + var qty = (i % 5) + 1; + + await messageBus.PublishAsync( + new BulkOrderEvent + { + OrderId = Guid.NewGuid(), + ProductName = name, + Quantity = qty, + UnitPrice = price, + TotalAmount = price * qty, + CustomerId = $"bulk-customer-{i:D5}", + CreatedAt = DateTimeOffset.UtcNow + }, + cancellationToken); + } + + sw.Stop(); + logger.LogInformation("Dispatched {Count} BulkOrderEvents in {Elapsed}ms", count, sw.ElapsedMilliseconds); + + return new BulkOrderResult(count, sw.ElapsedMilliseconds); + } +} diff --git a/src/Mocha/src/Demo/Demo.Catalog/Commands/PlaceOrderCommand.cs b/src/Mocha/src/Demo/Demo.Catalog/Commands/PlaceOrderCommand.cs new file mode 100644 index 00000000000..d4979119dca --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Catalog/Commands/PlaceOrderCommand.cs @@ -0,0 +1,73 @@ +using Demo.Catalog.Data; +using Demo.Catalog.Entities; +using Demo.Contracts.Events; +using Microsoft.EntityFrameworkCore; +using Mocha; +using Mocha.Mediator; + +namespace Demo.Catalog.Commands; + +public record PlaceOrderCommand( + Guid ProductId, + int Quantity, + string CustomerId, + string ShippingAddress) : ICommand; + +public record PlaceOrderResult(bool Success, OrderRecord? Order = null, string? Error = null); + +public class PlaceOrderCommandHandler(CatalogDbContext db, IMessageBus messageBus) + : ICommandHandler +{ + public async ValueTask HandleAsync( + PlaceOrderCommand command, CancellationToken cancellationToken) + { + var executionStrategy = db.Database.CreateExecutionStrategy(); + + return await executionStrategy.ExecuteAsync(async () => + { + await using var transaction = await db.Database.BeginTransactionAsync(); + + var product = await db.Products.FindAsync(command.ProductId); + if (product is null) + return new PlaceOrderResult(false, Error: "Product not found"); + + if (product.StockQuantity < command.Quantity) + return new PlaceOrderResult(false, Error: "Insufficient stock"); + + var order = new OrderRecord + { + Id = Guid.NewGuid(), + ProductId = product.Id, + Quantity = command.Quantity, + CustomerId = command.CustomerId, + ShippingAddress = command.ShippingAddress, + TotalAmount = product.Price * command.Quantity, + Status = OrderStatus.Pending, + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow + }; + + db.Orders.Add(order); + await db.SaveChangesAsync(); + + await messageBus.PublishAsync( + new OrderPlacedEvent + { + OrderId = order.Id, + ProductId = product.Id, + ProductName = product.Name, + Quantity = order.Quantity, + UnitPrice = product.Price, + TotalAmount = order.TotalAmount, + CustomerId = order.CustomerId, + ShippingAddress = order.ShippingAddress, + CreatedAt = order.CreatedAt + }, + CancellationToken.None); + + await transaction.CommitAsync(); + + return new PlaceOrderResult(true, order); + }); + } +} diff --git a/src/Mocha/src/Demo/Demo.Catalog/Commands/RequestQuickRefundCommand.cs b/src/Mocha/src/Demo/Demo.Catalog/Commands/RequestQuickRefundCommand.cs new file mode 100644 index 00000000000..5201f30d588 --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Catalog/Commands/RequestQuickRefundCommand.cs @@ -0,0 +1,62 @@ +using Demo.Catalog.Data; +using Demo.Catalog.Entities; +using Demo.Contracts.Saga; +using Microsoft.Extensions.Logging; +using Mocha; +using Mocha.Mediator; + +namespace Demo.Catalog.Commands; + +public record RequestQuickRefundCommand( + Guid OrderId, + decimal? Amount, + string Reason) : ICommand; + +public record RequestQuickRefundResult( + bool Success, + QuickRefundResponse? Response = null, + string? Error = null); + +public class RequestQuickRefundCommandHandler( + CatalogDbContext db, + IMessageBus messageBus, + ILogger logger) + : ICommandHandler +{ + public async ValueTask HandleAsync( + RequestQuickRefundCommand command, CancellationToken cancellationToken) + { + var order = await db.Orders.FindAsync([command.OrderId], cancellationToken); + if (order is null) + return new RequestQuickRefundResult(false, Error: "Order not found"); + + logger.LogInformation("Initiating quick refund saga for order {OrderId}", command.OrderId); + + try + { + var response = await messageBus.RequestAsync( + new RequestQuickRefundRequest + { + OrderId = command.OrderId, + Amount = command.Amount ?? order.TotalAmount, + CustomerId = order.CustomerId, + Reason = command.Reason + }, + cancellationToken); + + if (response.Success) + { + order.Status = OrderStatus.Cancelled; + order.UpdatedAt = DateTimeOffset.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } + + return new RequestQuickRefundResult(true, response); + } + catch (Exception ex) + { + logger.LogError(ex, "Quick refund saga failed for order {OrderId}", command.OrderId); + return new RequestQuickRefundResult(false, Error: "Refund processing failed: " + ex.Message); + } + } +} diff --git a/src/Mocha/src/Demo/Demo.Catalog/Demo.Catalog.csproj b/src/Mocha/src/Demo/Demo.Catalog/Demo.Catalog.csproj index cfb8287c04a..1605134d4af 100644 --- a/src/Mocha/src/Demo/Demo.Catalog/Demo.Catalog.csproj +++ b/src/Mocha/src/Demo/Demo.Catalog/Demo.Catalog.csproj @@ -8,6 +8,8 @@ + + diff --git a/src/Mocha/src/Demo/Demo.Catalog/Program.cs b/src/Mocha/src/Demo/Demo.Catalog/Program.cs index 9d576d64bd6..29b4a7acab5 100644 --- a/src/Mocha/src/Demo/Demo.Catalog/Program.cs +++ b/src/Mocha/src/Demo/Demo.Catalog/Program.cs @@ -1,6 +1,7 @@ +using Demo.Catalog.Commands; using Demo.Catalog.Data; -using Demo.Catalog.Entities; using Demo.Catalog.Handlers; +using Demo.Catalog.Queries; using Demo.Catalog.Sagas; using Demo.Contracts.Commands; using Demo.Contracts.Events; @@ -9,6 +10,7 @@ using Mocha; using Mocha.EntityFrameworkCore; using Mocha.Hosting; +using Mocha.Mediator; using Mocha.Outbox; using Mocha.Sagas; using Mocha.Transport.RabbitMQ; @@ -23,6 +25,11 @@ // RabbitMQ builder.AddRabbitMQClient("rabbitmq", x => x.DisableTracing = true); +// Mocha.Mediator +builder.Services.AddMediator() + .AddCatalog() + .UseEntityFrameworkTransactions(); + // MessageBus builder .Services.AddMessageBus() @@ -60,246 +67,80 @@ app.MapGet("/", () => "Catalog Service"); // Products -app.MapGet("/api/products", async (CatalogDbContext db) => await db.Products.Include(p => p.Category).ToListAsync()); +app.MapGet("/api/products", async (ISender sender) => + await sender.QueryAsync(new GetProductsQuery())); -app.MapGet( - "/api/products/{id:guid}", - async (Guid id, CatalogDbContext db) => - await db.Products.Include(p => p.Category).FirstOrDefaultAsync(p => p.Id == id) is { } product - ? Results.Ok(product) - : Results.NotFound()); +app.MapGet("/api/products/{id:guid}", async (Guid id, ISender sender) => + await sender.QueryAsync(new GetProductByIdQuery(id)) is { } product + ? Results.Ok(product) + : Results.NotFound()); // Categories -app.MapGet("/api/categories", async (CatalogDbContext db) => await db.Categories.ToListAsync()); +app.MapGet("/api/categories", async (ISender sender) => + await sender.QueryAsync(new GetCategoriesQuery())); // Orders - placing an order triggers OrderPlacedEvent -app.MapPost( - "/api/orders", - async (PlaceOrderRequest request, CatalogDbContext db, IMessageBus messageBus) => - { - var executionStrategy = db.Database.CreateExecutionStrategy(); - - return await executionStrategy.ExecuteAsync(async () => - { - await using var transaction = await db.Database.BeginTransactionAsync(); - - var product = await db.Products.FindAsync(request.ProductId); - if (product is null) - { - return Results.NotFound("Product not found"); - } - - if (product.StockQuantity < request.Quantity) - { - return Results.BadRequest("Insufficient stock"); - } - - var order = new OrderRecord - { - Id = Guid.NewGuid(), - ProductId = product.Id, - Quantity = request.Quantity, - CustomerId = request.CustomerId, - ShippingAddress = request.ShippingAddress, - TotalAmount = product.Price * request.Quantity, - Status = OrderStatus.Pending, - CreatedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow - }; - - db.Orders.Add(order); - await db.SaveChangesAsync(); - - // Publish OrderPlacedEvent - await messageBus.PublishAsync( - new OrderPlacedEvent - { - OrderId = order.Id, - ProductId = product.Id, - ProductName = product.Name, - Quantity = order.Quantity, - UnitPrice = product.Price, - TotalAmount = order.TotalAmount, - CustomerId = order.CustomerId, - ShippingAddress = order.ShippingAddress, - CreatedAt = order.CreatedAt - }, - CancellationToken.None); - - await transaction.CommitAsync(); - - return Results.Created($"/api/orders/{order.Id}", order); - }); - }); - -// Bulk order dispatch — fires thousands of BulkOrderEvents for batch processing demo -app.MapPost( - "/api/orders/bulk", - async (BulkOrderRequest request, IMessageBus messageBus, ILogger logger) => - { - var count = request.Count is > 0 ? request.Count : 2000; - - var products = new[] - { - ("Wireless Headphones", 299.99m), - ("Mechanical Keyboard", 149.99m), - ("Clean Code", 39.99m) - }; +app.MapPost("/api/orders", async (PlaceOrderRequest request, ISender sender) => +{ + var result = await sender.SendAsync( + new PlaceOrderCommand(request.ProductId, request.Quantity, request.CustomerId, request.ShippingAddress)); - logger.LogInformation("Dispatching {Count} BulkOrderEvents", count); - var sw = System.Diagnostics.Stopwatch.StartNew(); + return result.Success + ? Results.Created($"/api/orders/{result.Order!.Id}", result.Order) + : Results.BadRequest(result.Error); +}); - for (var i = 0; i < count; i++) - { - var (name, price) = products[i % products.Length]; - var qty = (i % 5) + 1; +// Bulk order dispatch +app.MapPost("/api/orders/bulk", async (BulkOrderRequest request, ISender sender) => +{ + var result = await sender.SendAsync(new PlaceBulkOrderCommand(request.Count)); + return Results.Ok(new { dispatched = result.Dispatched, elapsedMs = result.ElapsedMs }); +}); - await messageBus.PublishAsync( - new BulkOrderEvent - { - OrderId = Guid.NewGuid(), - ProductName = name, - Quantity = qty, - UnitPrice = price, - TotalAmount = price * qty, - CustomerId = $"bulk-customer-{i:D5}", - CreatedAt = DateTimeOffset.UtcNow - }, - CancellationToken.None); - } +app.MapGet("/api/orders", async (ISender sender) => + await sender.QueryAsync(new GetOrdersQuery())); - sw.Stop(); - logger.LogInformation("Dispatched {Count} BulkOrderEvents in {Elapsed}ms", count, sw.ElapsedMilliseconds); +app.MapGet("/api/orders/{id:guid}", async (Guid id, ISender sender) => + await sender.QueryAsync(new GetOrderByIdQuery(id)) is { } order + ? Results.Ok(order) + : Results.NotFound()); - return Results.Ok(new { dispatched = count, elapsedMs = sw.ElapsedMilliseconds }); - }); +// Quick Refund Saga +app.MapPost("/api/refunds/quick", async (QuickRefundRequest request, ISender sender) => +{ + var result = await sender.SendAsync( + new RequestQuickRefundCommand(request.OrderId, request.Amount, request.Reason)); -app.MapGet("/api/orders", async (CatalogDbContext db) => await db.Orders.Include(o => o.Product).ToListAsync()); + if (!result.Success) + return result.Error == "Order not found" ? Results.NotFound(result.Error) : Results.Problem(result.Error); -app.MapGet( - "/api/orders/{id:guid}", - async (Guid id, CatalogDbContext db) => - await db.Orders.Include(o => o.Product).FirstOrDefaultAsync(o => o.Id == id) is { } order - ? Results.Ok(order) - : Results.NotFound()); + return Results.Ok(result.Response); +}); -// ============================================ -// Saga Endpoints -// ============================================ +// Return Processing +app.MapPost("/api/returns/initiate", async (InitiateReturnRequestDto request, ISender sender) => +{ + var result = await sender.SendAsync( + new InitiateReturnCommand(request.OrderId, request.ShipmentId, request.Reason)); -// Quick Refund Saga - for digital goods or goodwill refunds -app.MapPost( - "/api/refunds/quick", - async (QuickRefundRequest request, CatalogDbContext db, IMessageBus messageBus, ILogger logger) => + if (!result.Success) { - // Verify order exists - var order = await db.Orders.FindAsync(request.OrderId); - if (order is null) - { - return Results.NotFound("Order not found"); - } - - logger.LogInformation("Initiating quick refund saga for order {OrderId}", request.OrderId); - - try - { - var response = await messageBus.RequestAsync( - new RequestQuickRefundRequest - { - OrderId = request.OrderId, - Amount = request.Amount ?? order.TotalAmount, - CustomerId = order.CustomerId, - Reason = request.Reason - }, - CancellationToken.None); - - if (response.Success) - { - // Update order status - order.Status = OrderStatus.Cancelled; - order.UpdatedAt = DateTimeOffset.UtcNow; - await db.SaveChangesAsync(); - } - - return Results.Ok(response); - } - catch (Exception ex) - { - logger.LogError(ex, "Quick refund saga failed for order {OrderId}", request.OrderId); - return Results.Problem("Refund processing failed: " + ex.Message); - } - }); - -// Return Processing - creates return label, saga handles the rest async when package arrives -app.MapPost( - "/api/returns/initiate", - async (InitiateReturnRequestDto request, CatalogDbContext db, IMessageBus messageBus, ILogger logger) => + if (result.Error!.Contains("not found")) + return Results.NotFound(result.Error); + if (result.Error.Contains("cannot be returned")) + return Results.BadRequest(result.Error); + return Results.Problem(result.Error); + } + + return Results.Ok(new { - // Verify order exists - var order = await db.Orders.Include(o => o.Product).FirstOrDefaultAsync(o => o.Id == request.OrderId); - if (order is null) - { - return Results.NotFound("Order not found"); - } - - if (order.Status != OrderStatus.Delivered && order.Status != OrderStatus.Shipping) - { - return Results.BadRequest($"Order cannot be returned in status: {order.Status}"); - } - - logger.LogInformation("Creating return label for order {OrderId}", request.OrderId); - - try - { - // Step 1: Create return label synchronously via Shipping service - var labelResponse = await messageBus.RequestAsync( - new CreateReturnLabelCommand - { - OrderId = request.OrderId, - OriginalShipmentId = request.ShipmentId, - CustomerAddress = order.ShippingAddress, - CustomerId = order.CustomerId, - // Include order details for saga when package arrives - ProductId = order.ProductId, - Quantity = order.Quantity, - Amount = order.TotalAmount, - Reason = request.Reason - }, - CancellationToken.None); - - if (!labelResponse.Success) - { - return Results.Problem($"Failed to create return label: {labelResponse.FailureReason}"); - } - - // Update order status to indicate return in progress - order.Status = OrderStatus.ReturnInitiated; - order.UpdatedAt = DateTimeOffset.UtcNow; - await db.SaveChangesAsync(); - - logger.LogInformation( - "Return label created for order {OrderId}: {ReturnId}, tracking: {Tracking}", - request.OrderId, - labelResponse.ReturnId, - labelResponse.ReturnTrackingNumber); - - // Return immediately - saga will continue when package arrives (ReturnPackageReceivedEvent) - return Results.Ok( - new - { - orderId = request.OrderId, - returnId = labelResponse.ReturnId, - returnTrackingNumber = labelResponse.ReturnTrackingNumber, - returnLabelUrl = labelResponse.ReturnLabelUrl, - message = "Return label created. Ship the package and we'll process the refund when it arrives." - }); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to create return label for order {OrderId}", request.OrderId); - return Results.Problem("Failed to create return label: " + ex.Message); - } + orderId = request.OrderId, + returnId = result.ReturnId, + returnTrackingNumber = result.ReturnTrackingNumber, + returnLabelUrl = result.ReturnLabelUrl, + message = "Return label created. Ship the package and we'll process the refund when it arrives." }); +}); app.MapMessageBusDeveloperTopology(); diff --git a/src/Mocha/src/Demo/Demo.Catalog/Queries/CategoryQueries.cs b/src/Mocha/src/Demo/Demo.Catalog/Queries/CategoryQueries.cs new file mode 100644 index 00000000000..a92442e2e25 --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Catalog/Queries/CategoryQueries.cs @@ -0,0 +1,16 @@ +using Demo.Catalog.Data; +using Demo.Catalog.Entities; +using Microsoft.EntityFrameworkCore; +using Mocha.Mediator; + +namespace Demo.Catalog.Queries; + +public record GetCategoriesQuery : IQuery>; + +public class GetCategoriesQueryHandler(CatalogDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetCategoriesQuery query, CancellationToken cancellationToken) + => await db.Categories.ToListAsync(cancellationToken); +} diff --git a/src/Mocha/src/Demo/Demo.Catalog/Queries/OrderQueries.cs b/src/Mocha/src/Demo/Demo.Catalog/Queries/OrderQueries.cs new file mode 100644 index 00000000000..a99c030077e --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Catalog/Queries/OrderQueries.cs @@ -0,0 +1,27 @@ +using Demo.Catalog.Data; +using Demo.Catalog.Entities; +using Microsoft.EntityFrameworkCore; +using Mocha.Mediator; + +namespace Demo.Catalog.Queries; + +public record GetOrdersQuery : IQuery>; + +public class GetOrdersQueryHandler(CatalogDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetOrdersQuery query, CancellationToken cancellationToken) + => await db.Orders.Include(o => o.Product).ToListAsync(cancellationToken); +} + +public record GetOrderByIdQuery(Guid Id) : IQuery; + +public class GetOrderByIdQueryHandler(CatalogDbContext db) + : IQueryHandler +{ + public async ValueTask HandleAsync( + GetOrderByIdQuery query, CancellationToken cancellationToken) + => await db.Orders.Include(o => o.Product) + .FirstOrDefaultAsync(o => o.Id == query.Id, cancellationToken); +} diff --git a/src/Mocha/src/Demo/Demo.Catalog/Queries/ProductQueries.cs b/src/Mocha/src/Demo/Demo.Catalog/Queries/ProductQueries.cs new file mode 100644 index 00000000000..b328d8d9b19 --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Catalog/Queries/ProductQueries.cs @@ -0,0 +1,27 @@ +using Demo.Catalog.Data; +using Demo.Catalog.Entities; +using Microsoft.EntityFrameworkCore; +using Mocha.Mediator; + +namespace Demo.Catalog.Queries; + +public record GetProductsQuery : IQuery>; + +public class GetProductsQueryHandler(CatalogDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetProductsQuery query, CancellationToken cancellationToken) + => await db.Products.Include(p => p.Category).ToListAsync(cancellationToken); +} + +public record GetProductByIdQuery(Guid Id) : IQuery; + +public class GetProductByIdQueryHandler(CatalogDbContext db) + : IQueryHandler +{ + public async ValueTask HandleAsync( + GetProductByIdQuery query, CancellationToken cancellationToken) + => await db.Products.Include(p => p.Category) + .FirstOrDefaultAsync(p => p.Id == query.Id, cancellationToken); +} diff --git a/src/Mocha/src/Demo/Demo.Shipping/Commands/ReceiveReturnPackageCommand.cs b/src/Mocha/src/Demo/Demo.Shipping/Commands/ReceiveReturnPackageCommand.cs new file mode 100644 index 00000000000..4f7a61d4539 --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Shipping/Commands/ReceiveReturnPackageCommand.cs @@ -0,0 +1,60 @@ +using Demo.Contracts.Events; +using Demo.Shipping.Data; +using Demo.Shipping.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Mocha; +using Mocha.Mediator; + +namespace Demo.Shipping.Commands; + +public record ReceiveReturnPackageCommand(Guid ReturnId) : ICommand; + +public record ReceiveReturnPackageResult( + bool Success, + ReturnShipment? ReturnShipment = null, + string? Error = null); + +public class ReceiveReturnPackageCommandHandler( + ShippingDbContext db, + IMessageBus messageBus, + ILogger logger) + : ICommandHandler +{ + public async ValueTask HandleAsync( + ReceiveReturnPackageCommand command, CancellationToken cancellationToken) + { + var returnShipment = await db.ReturnShipments.FirstOrDefaultAsync( + r => r.Id == command.ReturnId, cancellationToken); + if (returnShipment is null) + return new ReceiveReturnPackageResult(false, Error: "Return shipment not found"); + + if (returnShipment.Status == ReturnShipmentStatus.Received) + return new ReceiveReturnPackageResult(false, Error: "Return package already received"); + + returnShipment.Status = ReturnShipmentStatus.Received; + returnShipment.ReceivedAt = DateTimeOffset.UtcNow; + await db.SaveChangesAsync(cancellationToken); + + logger.LogInformation( + "Return package {ReturnId} received, publishing ReturnPackageReceivedEvent", + returnShipment.Id); + + await messageBus.PublishAsync( + new ReturnPackageReceivedEvent + { + ReturnId = returnShipment.Id, + OrderId = returnShipment.OrderId, + TrackingNumber = returnShipment.TrackingNumber!, + ReceivedAt = returnShipment.ReceivedAt.Value, + ProductId = returnShipment.ProductId, + Quantity = returnShipment.Quantity, + Amount = returnShipment.Amount, + CustomerId = returnShipment.CustomerId, + Reason = returnShipment.Reason + }, + cancellationToken); + + return new ReceiveReturnPackageResult(true, returnShipment); + } +} diff --git a/src/Mocha/src/Demo/Demo.Shipping/Commands/ShipShipmentCommand.cs b/src/Mocha/src/Demo/Demo.Shipping/Commands/ShipShipmentCommand.cs new file mode 100644 index 00000000000..b3125200fe7 --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Shipping/Commands/ShipShipmentCommand.cs @@ -0,0 +1,49 @@ +using Demo.Contracts.Events; +using Demo.Shipping.Data; +using Demo.Shipping.Entities; +using Microsoft.EntityFrameworkCore; +using Mocha; +using Mocha.Mediator; + +namespace Demo.Shipping.Commands; + +public record ShipShipmentCommand(Guid ShipmentId, string Carrier, int EstimatedDays = 5) + : ICommand; + +public record ShipShipmentResult(bool Success, Shipment? Shipment = null, string? Error = null); + +public class ShipShipmentCommandHandler(ShippingDbContext db, IMessageBus messageBus) + : ICommandHandler +{ + public async ValueTask HandleAsync( + ShipShipmentCommand command, CancellationToken cancellationToken) + { + var shipment = await db.Shipments.FirstOrDefaultAsync( + s => s.Id == command.ShipmentId, cancellationToken); + if (shipment is null) + return new ShipShipmentResult(false, Error: "Shipment not found"); + + if (shipment.Status == ShipmentStatus.Shipped) + return new ShipShipmentResult(false, Error: "Shipment already shipped"); + + shipment.Status = ShipmentStatus.Shipped; + shipment.Carrier = command.Carrier; + shipment.ShippedAt = DateTimeOffset.UtcNow; + shipment.EstimatedDelivery = DateTimeOffset.UtcNow.AddDays(command.EstimatedDays); + await db.SaveChangesAsync(cancellationToken); + + await messageBus.PublishAsync( + new ShipmentShippedEvent + { + ShipmentId = shipment.Id, + OrderId = shipment.OrderId, + TrackingNumber = shipment.TrackingNumber!, + Carrier = shipment.Carrier, + ShippedAt = shipment.ShippedAt.Value, + EstimatedDelivery = shipment.EstimatedDelivery.Value + }, + cancellationToken); + + return new ShipShipmentResult(true, shipment); + } +} diff --git a/src/Mocha/src/Demo/Demo.Shipping/Demo.Shipping.csproj b/src/Mocha/src/Demo/Demo.Shipping/Demo.Shipping.csproj index 5d7e1f167f1..fd7274e18f8 100644 --- a/src/Mocha/src/Demo/Demo.Shipping/Demo.Shipping.csproj +++ b/src/Mocha/src/Demo/Demo.Shipping/Demo.Shipping.csproj @@ -7,6 +7,8 @@ + + diff --git a/src/Mocha/src/Demo/Demo.Shipping/Program.cs b/src/Mocha/src/Demo/Demo.Shipping/Program.cs index 5e996b11b06..20f4b6f037e 100644 --- a/src/Mocha/src/Demo/Demo.Shipping/Program.cs +++ b/src/Mocha/src/Demo/Demo.Shipping/Program.cs @@ -1,10 +1,12 @@ using Demo.Contracts.Events; +using Demo.Shipping.Commands; using Demo.Shipping.Data; -using Demo.Shipping.Entities; using Demo.Shipping.Handlers; +using Demo.Shipping.Queries; using Microsoft.EntityFrameworkCore; using Mocha; using Mocha.EntityFrameworkCore; +using Mocha.Mediator; using Mocha.Outbox; using Mocha.Transport.RabbitMQ; @@ -18,6 +20,11 @@ // RabbitMQ builder.AddRabbitMQClient("rabbitmq", x => x.DisableTracing = true); +// Mocha.Mediator +builder.Services.AddMediator() + .AddShipping() + .UseEntityFrameworkTransactions(); + // MessageBus builder .Services.AddMessageBus() @@ -44,120 +51,63 @@ app.MapGet("/", () => "Shipping Service"); // Shipments -app.MapGet("/api/shipments", async (ShippingDbContext db) => await db.Shipments.Include(s => s.Items).ToListAsync()); - -app.MapGet( - "/api/shipments/{id:guid}", - async (Guid id, ShippingDbContext db) => - await db.Shipments.Include(s => s.Items).FirstOrDefaultAsync(s => s.Id == id) is { } shipment - ? Results.Ok(shipment) - : Results.NotFound()); - -app.MapGet( - "/api/shipments/order/{orderId:guid}", - async (Guid orderId, ShippingDbContext db) => - await db.Shipments.Include(s => s.Items).FirstOrDefaultAsync(s => s.OrderId == orderId) is { } shipment - ? Results.Ok(shipment) - : Results.NotFound()); - -// Ship a shipment - triggers ShipmentShippedEvent -app.MapPost( - "/api/shipments/{id:guid}/ship", - async (Guid id, ShipShipmentRequest request, ShippingDbContext db, IMessageBus messageBus) => +app.MapGet("/api/shipments", async (ISender sender) => + await sender.QueryAsync(new GetShipmentsQuery())); + +app.MapGet("/api/shipments/{id:guid}", async (Guid id, ISender sender) => + await sender.QueryAsync(new GetShipmentByIdQuery(id)) is { } shipment + ? Results.Ok(shipment) + : Results.NotFound()); + +app.MapGet("/api/shipments/order/{orderId:guid}", async (Guid orderId, ISender sender) => + await sender.QueryAsync(new GetShipmentByOrderIdQuery(orderId)) is { } shipment + ? Results.Ok(shipment) + : Results.NotFound()); + +// Ship a shipment +app.MapPost("/api/shipments/{id:guid}/ship", async (Guid id, ShipShipmentRequest request, ISender sender) => +{ + var result = await sender.SendAsync( + new ShipShipmentCommand(id, request.Carrier, request.EstimatedDays)); + + if (!result.Success) { - var shipment = await db.Shipments.FirstOrDefaultAsync(s => s.Id == id); - if (shipment is null) - { - return Results.NotFound("Shipment not found"); - } - - if (shipment.Status == ShipmentStatus.Shipped) - { - return Results.BadRequest("Shipment already shipped"); - } - - shipment.Status = ShipmentStatus.Shipped; - shipment.Carrier = request.Carrier; - shipment.ShippedAt = DateTimeOffset.UtcNow; - shipment.EstimatedDelivery = DateTimeOffset.UtcNow.AddDays(request.EstimatedDays); - await db.SaveChangesAsync(); - - // Publish ShipmentShippedEvent - await messageBus.PublishAsync( - new ShipmentShippedEvent - { - ShipmentId = shipment.Id, - OrderId = shipment.OrderId, - TrackingNumber = shipment.TrackingNumber!, - Carrier = shipment.Carrier, - ShippedAt = shipment.ShippedAt.Value, - EstimatedDelivery = shipment.EstimatedDelivery.Value - }, - CancellationToken.None); - - return Results.Ok(shipment); - }); + return result.Error == "Shipment not found" + ? Results.NotFound(result.Error) + : Results.BadRequest(result.Error); + } + + return Results.Ok(result.Shipment); +}); // Return Shipments -app.MapGet("/api/returns", async (ShippingDbContext db) => await db.ReturnShipments.ToListAsync()); - -app.MapGet( - "/api/returns/{id:guid}", - async (Guid id, ShippingDbContext db) => - await db.ReturnShipments.FirstOrDefaultAsync(r => r.Id == id) is { } returnShipment - ? Results.Ok(returnShipment) - : Results.NotFound()); - -app.MapGet( - "/api/returns/order/{orderId:guid}", - async (Guid orderId, ShippingDbContext db) => - await db.ReturnShipments.FirstOrDefaultAsync(r => r.OrderId == orderId) is { } returnShipment - ? Results.Ok(returnShipment) - : Results.NotFound()); - -// Simulate return package received - triggers ReturnPackageReceivedEvent for saga -app.MapPost( - "/api/returns/{id:guid}/receive", - async (Guid id, ShippingDbContext db, IMessageBus messageBus, ILogger logger) => +app.MapGet("/api/returns", async (ISender sender) => + await sender.QueryAsync(new GetReturnShipmentsQuery())); + +app.MapGet("/api/returns/{id:guid}", async (Guid id, ISender sender) => + await sender.QueryAsync(new GetReturnShipmentByIdQuery(id)) is { } returnShipment + ? Results.Ok(returnShipment) + : Results.NotFound()); + +app.MapGet("/api/returns/order/{orderId:guid}", async (Guid orderId, ISender sender) => + await sender.QueryAsync(new GetReturnShipmentByOrderIdQuery(orderId)) is { } returnShipment + ? Results.Ok(returnShipment) + : Results.NotFound()); + +// Receive return package +app.MapPost("/api/returns/{id:guid}/receive", async (Guid id, ISender sender) => +{ + var result = await sender.SendAsync(new ReceiveReturnPackageCommand(id)); + + if (!result.Success) { - var returnShipment = await db.ReturnShipments.FirstOrDefaultAsync(r => r.Id == id); - if (returnShipment is null) - { - return Results.NotFound("Return shipment not found"); - } - - if (returnShipment.Status == ReturnShipmentStatus.Received) - { - return Results.BadRequest("Return package already received"); - } - - returnShipment.Status = ReturnShipmentStatus.Received; - returnShipment.ReceivedAt = DateTimeOffset.UtcNow; - await db.SaveChangesAsync(); - - logger.LogInformation( - "Return package {ReturnId} received, publishing ReturnPackageReceivedEvent", - returnShipment.Id); - - // Publish ReturnPackageReceivedEvent to start saga for inspection/refund - await messageBus.PublishAsync( - new ReturnPackageReceivedEvent - { - ReturnId = returnShipment.Id, - OrderId = returnShipment.OrderId, - TrackingNumber = returnShipment.TrackingNumber!, - ReceivedAt = returnShipment.ReceivedAt.Value, - // Include order details for saga processing - ProductId = returnShipment.ProductId, - Quantity = returnShipment.Quantity, - Amount = returnShipment.Amount, - CustomerId = returnShipment.CustomerId, - Reason = returnShipment.Reason - }, - CancellationToken.None); - - return Results.Ok(returnShipment); - }); + return result.Error == "Return shipment not found" + ? Results.NotFound(result.Error) + : Results.BadRequest(result.Error); + } + + return Results.Ok(result.ReturnShipment); +}); app.Run(); diff --git a/src/Mocha/src/Demo/Demo.Shipping/Queries/ReturnShipmentQueries.cs b/src/Mocha/src/Demo/Demo.Shipping/Queries/ReturnShipmentQueries.cs new file mode 100644 index 00000000000..07f45dc7e83 --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Shipping/Queries/ReturnShipmentQueries.cs @@ -0,0 +1,38 @@ +using Demo.Shipping.Data; +using Demo.Shipping.Entities; +using Microsoft.EntityFrameworkCore; +using Mocha.Mediator; + +namespace Demo.Shipping.Queries; + +public record GetReturnShipmentsQuery : IQuery>; + +public class GetReturnShipmentsQueryHandler(ShippingDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetReturnShipmentsQuery query, CancellationToken cancellationToken) + => await db.ReturnShipments.ToListAsync(cancellationToken); +} + +public record GetReturnShipmentByIdQuery(Guid Id) : IQuery; + +public class GetReturnShipmentByIdQueryHandler(ShippingDbContext db) + : IQueryHandler +{ + public async ValueTask HandleAsync( + GetReturnShipmentByIdQuery query, CancellationToken cancellationToken) + => await db.ReturnShipments.FirstOrDefaultAsync( + r => r.Id == query.Id, cancellationToken); +} + +public record GetReturnShipmentByOrderIdQuery(Guid OrderId) : IQuery; + +public class GetReturnShipmentByOrderIdQueryHandler(ShippingDbContext db) + : IQueryHandler +{ + public async ValueTask HandleAsync( + GetReturnShipmentByOrderIdQuery query, CancellationToken cancellationToken) + => await db.ReturnShipments.FirstOrDefaultAsync( + r => r.OrderId == query.OrderId, cancellationToken); +} diff --git a/src/Mocha/src/Demo/Demo.Shipping/Queries/ShipmentQueries.cs b/src/Mocha/src/Demo/Demo.Shipping/Queries/ShipmentQueries.cs new file mode 100644 index 00000000000..f34981703ab --- /dev/null +++ b/src/Mocha/src/Demo/Demo.Shipping/Queries/ShipmentQueries.cs @@ -0,0 +1,38 @@ +using Demo.Shipping.Data; +using Demo.Shipping.Entities; +using Microsoft.EntityFrameworkCore; +using Mocha.Mediator; + +namespace Demo.Shipping.Queries; + +public record GetShipmentsQuery : IQuery>; + +public class GetShipmentsQueryHandler(ShippingDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetShipmentsQuery query, CancellationToken cancellationToken) + => await db.Shipments.Include(s => s.Items).ToListAsync(cancellationToken); +} + +public record GetShipmentByIdQuery(Guid Id) : IQuery; + +public class GetShipmentByIdQueryHandler(ShippingDbContext db) + : IQueryHandler +{ + public async ValueTask HandleAsync( + GetShipmentByIdQuery query, CancellationToken cancellationToken) + => await db.Shipments.Include(s => s.Items) + .FirstOrDefaultAsync(s => s.Id == query.Id, cancellationToken); +} + +public record GetShipmentByOrderIdQuery(Guid OrderId) : IQuery; + +public class GetShipmentByOrderIdQueryHandler(ShippingDbContext db) + : IQueryHandler +{ + public async ValueTask HandleAsync( + GetShipmentByOrderIdQuery query, CancellationToken cancellationToken) + => await db.Shipments.Include(s => s.Items) + .FirstOrDefaultAsync(s => s.OrderId == query.OrderId, cancellationToken); +} diff --git a/src/Mocha/src/Examples/MediatorShowcase/Generated/Microsoft.AspNetCore.App.SourceGenerators/Microsoft.AspNetCore.SourceGenerators.PublicProgramSourceGenerator/PublicTopLevelProgram.Generated.g.cs b/src/Mocha/src/Examples/MediatorShowcase/Generated/Microsoft.AspNetCore.App.SourceGenerators/Microsoft.AspNetCore.SourceGenerators.PublicProgramSourceGenerator/PublicTopLevelProgram.Generated.g.cs new file mode 100644 index 00000000000..bf0e4fa5579 --- /dev/null +++ b/src/Mocha/src/Examples/MediatorShowcase/Generated/Microsoft.AspNetCore.App.SourceGenerators/Microsoft.AspNetCore.SourceGenerators.PublicProgramSourceGenerator/PublicTopLevelProgram.Generated.g.cs @@ -0,0 +1,5 @@ +// +/// +/// Auto-generated public partial Program class for top-level statement apps. +/// +public partial class Program { } \ No newline at end of file diff --git a/src/Mocha/src/Examples/MediatorShowcase/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/MediatorShowcaseMediatorBuilderExtensions._5hYhW_IBO7W3L_MSoxCPw.g.cs b/src/Mocha/src/Examples/MediatorShowcase/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/MediatorShowcaseMediatorBuilderExtensions._5hYhW_IBO7W3L_MSoxCPw.g.cs new file mode 100644 index 00000000000..8831201bb13 --- /dev/null +++ b/src/Mocha/src/Examples/MediatorShowcase/Generated/Mocha.Analyzers/Mocha.Analyzers.MediatorGenerator/MediatorShowcaseMediatorBuilderExtensions._5hYhW_IBO7W3L_MSoxCPw.g.cs @@ -0,0 +1,88 @@ +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class MediatorShowcaseMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddMediatorShowcase( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::MediatorShowcase.PlaceOrderCommandHandler), lifetime)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::MediatorShowcase.RiskyCommandHandler), lifetime)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::MediatorShowcase.CreateProductCommandHandler), lifetime)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::MediatorShowcase.CreateProductCommandHandler2), lifetime)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::MediatorShowcase.CreateProductCommandHandler3), lifetime)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::MediatorShowcase.CreateProductCommandHandler4), lifetime)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::MediatorShowcase.GetProductByIdQueryHandler), lifetime)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler>), typeof(global::MediatorShowcase.GetProductsQueryHandler), lifetime)); + + // Register notification handlers + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::MediatorShowcase.OrderShippedAnalyticsHandler), typeof(global::MediatorShowcase.OrderShippedAnalyticsHandler), lifetime)); + services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::MediatorShowcase.OrderShippedEmailHandler), typeof(global::MediatorShowcase.OrderShippedEmailHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::MediatorShowcase.PlaceOrderCommand), + ResponseType = typeof(global::MediatorShowcase.OrderResult), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::MediatorShowcase.RiskyCommand), + ResponseType = typeof(string), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::MediatorShowcase.CreateProductCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::MediatorShowcase.CreateProductCommand2), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::MediatorShowcase.CreateProductCommand3), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::MediatorShowcase.CreateProductCommand4), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::MediatorShowcase.GetProductByIdQuery), + ResponseType = typeof(global::MediatorShowcase.ProductDto), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::MediatorShowcase.GetProductsQuery), + ResponseType = typeof(global::System.Collections.Generic.IReadOnlyList), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal>() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::MediatorShowcase.OrderShippedNotification), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::MediatorShowcase.OrderShippedAnalyticsHandler), typeof(global::MediatorShowcase.OrderShippedEmailHandler) }) + }); + }); + + return builder; + } + } +} diff --git a/src/Mocha/src/Examples/MediatorShowcase/Handlers.cs b/src/Mocha/src/Examples/MediatorShowcase/Handlers.cs new file mode 100644 index 00000000000..e6767120a1e --- /dev/null +++ b/src/Mocha/src/Examples/MediatorShowcase/Handlers.cs @@ -0,0 +1,141 @@ +using Mocha.Mediator; + +namespace MediatorShowcase; + +// ────────────────────────────────────────────────── +// Command Handlers +// ────────────────────────────────────────────────── + +/// +/// Handles a void command (no return value). +/// +public sealed class CreateProductCommandHandler(ILogger logger) + : ICommandHandler +{ + public ValueTask HandleAsync(CreateProductCommand command, CancellationToken cancellationToken) + { + logger.LogInformation("Product created: {Name} at {Price:C}", command.Name, command.Price); + return ValueTask.CompletedTask; + } +} +public record CreateProductCommand2(string Name, decimal Price) : ICommand; +public sealed class CreateProductCommandHandler2(ILogger logger) + : ICommandHandler +{ + public ValueTask HandleAsync(CreateProductCommand2 command, CancellationToken cancellationToken) + { + logger.LogInformation("Product created: {Name} at {Price:C}", command.Name, command.Price); + return ValueTask.CompletedTask; + } +} + +public record CreateProductCommand3(string Name, decimal Price) : ICommand; +public sealed class CreateProductCommandHandler3(ILogger logger) + : ICommandHandler +{ + public ValueTask HandleAsync(CreateProductCommand3 command, CancellationToken cancellationToken) + { + logger.LogInformation("Product created: {Name} at {Price:C}", command.Name, command.Price); + return ValueTask.CompletedTask; + } +} + +public record CreateProductCommand4(string Name, decimal Price) : ICommand; +public sealed class CreateProductCommandHandler4(ILogger logger) + : ICommandHandler +{ + public ValueTask HandleAsync(CreateProductCommand4 command, CancellationToken cancellationToken) + { + logger.LogInformation("Product created: {Name} at {Price:C}", command.Name, command.Price); + return ValueTask.CompletedTask; + } +} + +/// +/// Handles a command that returns a response. +/// +public sealed class PlaceOrderCommandHandler(ILogger logger) + : ICommandHandler +{ + public ValueTask HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken) + { + var orderId = Guid.NewGuid(); + var total = command.Quantity * 29.99m; + + logger.LogInformation( + "Order {OrderId} placed: {Quantity}x {ProductName} = {Total:C}", + orderId, command.Quantity, command.ProductName, total); + + return new ValueTask(new OrderResult(orderId, "Confirmed", total)); + } +} + +/// +/// A command handler that always throws, demonstrating exception handling. +/// +public sealed class RiskyCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(RiskyCommand command, CancellationToken cancellationToken) + { + throw new InvalidOperationException("Something went wrong processing the risky command!"); + } +} + +// ────────────────────────────────────────────────── +// Query Handlers +// ────────────────────────────────────────────────── + +public sealed class GetProductsQueryHandler : IQueryHandler> +{ + private static readonly IReadOnlyList Products = + [ + new(Guid.Parse("11111111-1111-1111-1111-111111111111"), "Mechanical Keyboard", 149.99m), + new(Guid.Parse("22222222-2222-2222-2222-222222222222"), "Ergonomic Mouse", 79.99m), + new(Guid.Parse("33333333-3333-3333-3333-333333333333"), "4K Monitor", 499.99m) + ]; + + public ValueTask> HandleAsync( + GetProductsQuery query, CancellationToken cancellationToken) + => new(Products); +} + +public sealed class GetProductByIdQueryHandler : IQueryHandler +{ + public async ValueTask HandleAsync( + GetProductByIdQuery query, CancellationToken cancellationToken) + { + var products = await new GetProductsQueryHandler() + .HandleAsync(new GetProductsQuery(), cancellationToken); + return products.FirstOrDefault(p => p.Id == query.Id); + } +} + +// ────────────────────────────────────────────────── +// Notification Handlers (multiple handlers per notification) +// ────────────────────────────────────────────────── + +/// +/// First handler: sends an email when an order is shipped. +/// +public sealed class OrderShippedEmailHandler(ILogger logger) + : INotificationHandler +{ + public ValueTask HandleAsync(OrderShippedNotification notification, CancellationToken cancellationToken) + { + logger.LogInformation("[Email] Order {OrderId} shipped — email sent to customer", notification.OrderId); + return ValueTask.CompletedTask; + } +} + +/// +/// Second handler: updates analytics when an order is shipped. +/// +public sealed class OrderShippedAnalyticsHandler(ILogger logger) + : INotificationHandler +{ + public ValueTask HandleAsync(OrderShippedNotification notification, CancellationToken cancellationToken) + { + logger.LogInformation("[Analytics] Order {OrderId} shipped — metrics recorded", notification.OrderId); + return ValueTask.CompletedTask; + } +} diff --git a/src/Mocha/src/Examples/MediatorShowcase/MediatorShowcase.cs b/src/Mocha/src/Examples/MediatorShowcase/MediatorShowcase.cs new file mode 100644 index 00000000000..4a03bcddf73 --- /dev/null +++ b/src/Mocha/src/Examples/MediatorShowcase/MediatorShowcase.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; +using MediatorShowcase; +using Mocha.Mediator; + +var builder = WebApplication.CreateBuilder(args); + +// Register the Mocha Mediator with source-generated handlers. +builder.Services.AddMediator() + .AddMediatorShowcase() + .Use(LoggingMiddleware.Create()) + .Use(PlaceOrderValidationMiddleware.Create()) + .Use(PlaceOrderAuditMiddleware.Create()) + .Use(ExceptionHandlingMiddleware.Create()) + .AddInstrumentation(); + +var app = builder.Build(); + +// ────────────────────────────────────────────────── +// Commands +// ────────────────────────────────────────────────── + +// Void command — no return value +app.MapPost("/api/products", async (CreateProductRequest req, ISender sender) => +{ + await sender.SendAsync(new CreateProductCommand(req.Name, req.Price)); + return Results.Created(); +}); + +// Command with response +app.MapPost("/api/orders", async (PlaceOrderRequest req, ISender sender) => +{ + var result = await sender.SendAsync( + new PlaceOrderCommand(req.ProductName, req.Quantity)); + return Results.Ok(result); +}); + +// ────────────────────────────────────────────────── +// Queries +// ────────────────────────────────────────────────── + +app.MapGet("/api/products", async (ISender sender) => + await sender.QueryAsync(new GetProductsQuery())); + +app.MapGet("/api/products/{id:guid}", async (Guid id, ISender sender) => + await sender.QueryAsync(new GetProductByIdQuery(id)) is { } product + ? Results.Ok(product) + : Results.NotFound()); + +// ────────────────────────────────────────────────── +// Notifications +// ────────────────────────────────────────────────── + +app.MapPost("/api/notifications/order-shipped", async (Guid orderId, IPublisher publisher) => +{ + await publisher.PublishAsync(new OrderShippedNotification(orderId)); + return Results.Ok("Notification published"); +}); + +// ────────────────────────────────────────────────── +// Exception handling demo +// ────────────────────────────────────────────────── + +app.MapGet("/api/demo/exception", async (ISender sender) => +{ + var result = await sender.SendAsync(new RiskyCommand()); + return Results.Ok(result); +}); + +app.Run(); + +// ────────────────────────────────────────────────── +// Request/Response DTOs +// ────────────────────────────────────────────────── + +public record CreateProductRequest(string Name, decimal Price); +public record PlaceOrderRequest(string ProductName, int Quantity); + +// ────────────────────────────────────────────────── +// Messages +// ────────────────────────────────────────────────── + +namespace MediatorShowcase +{ + // -- Commands -- + + public sealed record CreateProductCommand(string Name, decimal Price) : ICommand; + + public sealed record PlaceOrderCommand(string ProductName, int Quantity) : ICommand; + + public sealed record OrderResult(Guid OrderId, string Status, decimal Total); + + public sealed record RiskyCommand : ICommand; + + // -- Queries -- + + public sealed record GetProductsQuery : IQuery>; + + public sealed record GetProductByIdQuery(Guid Id) : IQuery; + + public sealed record ProductDto(Guid Id, string Name, decimal Price); + + // -- Notifications -- + + public sealed record OrderShippedNotification(Guid OrderId) : INotification; +} diff --git a/src/Mocha/src/Examples/MediatorShowcase/MediatorShowcase.csproj b/src/Mocha/src/Examples/MediatorShowcase/MediatorShowcase.csproj new file mode 100644 index 00000000000..dff903661b2 --- /dev/null +++ b/src/Mocha/src/Examples/MediatorShowcase/MediatorShowcase.csproj @@ -0,0 +1,19 @@ + + + true + Generated + + + + + + + + + + + diff --git a/src/Mocha/src/Examples/MediatorShowcase/PipelineBehaviors.cs b/src/Mocha/src/Examples/MediatorShowcase/PipelineBehaviors.cs new file mode 100644 index 00000000000..439e7adaaa6 --- /dev/null +++ b/src/Mocha/src/Examples/MediatorShowcase/PipelineBehaviors.cs @@ -0,0 +1,208 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Mocha.Mediator; + +namespace MediatorShowcase; + +// ────────────────────────────────────────────────── +// Logging Middleware (cross-cutting, all messages) +// ────────────────────────────────────────────────── + +/// +/// Middleware that logs and times every message passing through the pipeline. +/// Applies to all commands, queries, and notifications automatically. +/// +public static class LoggingMiddleware +{ + public static MediatorMiddlewareConfiguration Create() + => new( + static (factoryCtx, next) => + { + var logger = factoryCtx.Services.GetRequiredService() + .CreateLogger("Pipeline.Logging"); + + return ctx => + { + var messageTypeName = ctx.MessageType.Name; + logger.LogInformation("[Pipeline] Handling {MessageType}...", messageTypeName); + + var sw = Stopwatch.StartNew(); + var task = next(ctx); + + if (task.IsCompletedSuccessfully) + { + sw.Stop(); + logger.LogInformation( + "[Pipeline] Handled {MessageType} in {ElapsedMs}ms", + messageTypeName, sw.ElapsedMilliseconds); + return default; + } + + return Awaited(task, sw, logger, messageTypeName); + + [MethodImpl(MethodImplOptions.NoInlining)] + static async ValueTask Awaited( + ValueTask t, Stopwatch sw, ILogger log, string msgType) + { + await t.ConfigureAwait(false); + sw.Stop(); + log.LogInformation( + "[Pipeline] Handled {MessageType} in {ElapsedMs}ms", + msgType, sw.ElapsedMilliseconds); + } + }; + }, + "Logging"); +} + +// ────────────────────────────────────────────────── +// Validation Middleware (message-specific pre-check) +// ────────────────────────────────────────────────── + +/// +/// Middleware that validates PlaceOrderCommand before the handler runs. +/// Demonstrates message-type-specific pre-processing. +/// +public static class PlaceOrderValidationMiddleware +{ + public static MediatorMiddlewareConfiguration Create() + => new( + static (factoryCtx, next) => + { + var logger = factoryCtx.Services.GetRequiredService() + .CreateLogger("Pipeline.Validation"); + + return ctx => + { + if (ctx.Message is PlaceOrderCommand order) + { + logger.LogInformation( + "[PreProcessor] Validating order: {Quantity}x {Product}", + order.Quantity, order.ProductName); + + if (order.Quantity <= 0) + { + throw new ArgumentException("Quantity must be greater than zero."); + } + } + + return next(ctx); + }; + }, + "Validation"); +} + +// ────────────────────────────────────────────────── +// Auditing Middleware (post-processing) +// ────────────────────────────────────────────────── + +/// +/// Middleware that audits PlaceOrderCommand results after the handler runs. +/// Demonstrates message-type-specific post-processing. +/// +public static class PlaceOrderAuditMiddleware +{ + public static MediatorMiddlewareConfiguration Create() + => new( + static (factoryCtx, next) => + { + var logger = factoryCtx.Services.GetRequiredService() + .CreateLogger("Pipeline.Audit"); + + return ctx => + { + var task = next(ctx); + + if (task.IsCompletedSuccessfully) + { + LogResult(ctx, logger); + return default; + } + + return Awaited(task, ctx, logger); + + [MethodImpl(MethodImplOptions.NoInlining)] + static async ValueTask Awaited( + ValueTask t, IMediatorContext ctx, ILogger log) + { + await t.ConfigureAwait(false); + LogResult(ctx, log); + } + + static void LogResult(IMediatorContext ctx, ILogger log) + { + if (ctx.Result is OrderResult result) + { + log.LogInformation( + "[PostProcessor] Order {OrderId} confirmed with total {Total:C}", + result.OrderId, result.Total); + } + } + }; + }, + "Audit"); +} + +// ────────────────────────────────────────────────── +// Exception Handling Middleware +// ────────────────────────────────────────────────── + +/// +/// Middleware that catches InvalidOperationException from RiskyCommand +/// and returns a fallback response instead of propagating the exception. +/// +public static class ExceptionHandlingMiddleware +{ + public static MediatorMiddlewareConfiguration Create() + => new( + static (factoryCtx, next) => + { + var logger = factoryCtx.Services.GetRequiredService() + .CreateLogger("Pipeline.ExceptionHandler"); + + return ctx => + { + try + { + var task = next(ctx); + + if (task.IsCompletedSuccessfully) + { + return default; + } + + return Awaited(task, ctx, logger); + } + catch (InvalidOperationException ex) when (ctx.Message is RiskyCommand) + { + HandleException(ctx, ex, logger); + return default; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static async ValueTask Awaited( + ValueTask t, IMediatorContext ctx, ILogger log) + { + try + { + await t.ConfigureAwait(false); + } + catch (InvalidOperationException ex) when (ctx.Message is RiskyCommand) + { + HandleException(ctx, ex, log); + } + } + + static void HandleException( + IMediatorContext ctx, InvalidOperationException ex, ILogger log) + { + log.LogWarning( + "[ExceptionHandler] Caught {ExceptionType}: {Message}", + ex.GetType().Name, ex.Message); + + ctx.Result = "Recovered gracefully from error."; + } + }; + }, + "ExceptionHandler"); +} diff --git a/src/Mocha/src/Examples/MediatorShowcase/Properties/launchSettings.json b/src/Mocha/src/Examples/MediatorShowcase/Properties/launchSettings.json new file mode 100644 index 00000000000..6d6953b6d7c --- /dev/null +++ b/src/Mocha/src/Examples/MediatorShowcase/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5050", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Shipped.md b/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000000..21176bd3b97 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1 @@ +; No shipped releases yet diff --git a/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md b/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000000..8c98066f5de --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MO0001 | Mediator | Warning | Message type has no registered handler +MO0002 | Mediator | Error | Message type has multiple handlers +MO0003 | Mediator | Warning | Handler is abstract and will not be registered +MO0004 | Mediator | Info | Open generic message type cannot be dispatched diff --git a/src/Mocha/src/Mocha.Analyzers/Errors.cs b/src/Mocha/src/Mocha.Analyzers/Errors.cs new file mode 100644 index 00000000000..c0231e3f398 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Errors.cs @@ -0,0 +1,70 @@ +using Microsoft.CodeAnalysis; + +namespace Mocha.Analyzers; + +/// +/// Provides definitions for all diagnostics reported +/// by the Mocha mediator source generator. +/// +public static class Errors +{ + /// + /// Gets the descriptor for MO0001: a message type has no registered handler. + /// + /// + /// Reported as a warning when a command or query type is declared but no corresponding + /// handler implementation is found in the compilation. + /// + public static readonly DiagnosticDescriptor MissingHandler = new( + id: "MO0001", + title: "Missing handler for message type", + messageFormat: "Message type '{0}' has no registered handler", + category: "Mediator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + /// Gets the descriptor for MO0002: a message type has more than one handler. + /// + /// + /// Reported as an error when a command or query type has multiple handler implementations. + /// Commands and queries must have exactly one handler; notifications are excluded from this rule. + /// + public static readonly DiagnosticDescriptor DuplicateHandler = new( + id: "MO0002", + title: "Duplicate handler for message type", + messageFormat: "Message type '{0}' has multiple handlers: {1}", + category: "Mediator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// Gets the descriptor for MO0003: a handler class is abstract and cannot be registered. + /// + /// + /// Reported as a warning when a class implements a handler interface but is declared + /// , preventing it from being instantiated at runtime. + /// + public static readonly DiagnosticDescriptor AbstractHandler = new( + id: "MO0003", + title: "Handler is abstract", + messageFormat: "Handler '{0}' is abstract and will not be registered", + category: "Mediator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + /// Gets the descriptor for MO0004: a message type is an open generic and cannot be dispatched. + /// + /// + /// Reported as an info when a command or query type has unbound type parameters, + /// making it impossible to dispatch at runtime. + /// + public static readonly DiagnosticDescriptor OpenGenericMessageType = new( + id: "MO0004", + title: "Open generic message type cannot be dispatched", + messageFormat: "Message type '{0}' is an open generic and cannot be dispatched at runtime", + category: "Mediator", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); +} diff --git a/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs b/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs new file mode 100644 index 00000000000..2cbe36e7c98 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs @@ -0,0 +1,195 @@ +using System.Security.Cryptography; +using System.Text; +using Mocha.Analyzers.Utils; + +namespace Mocha.Analyzers.FileBuilders; + +/// +/// Builds the module-prefixed mediator builder extensions source file that registers +/// handlers and pipelines into the dependency injection container. +/// +public sealed class DependencyInjectionFileBuilder : FileBuilderBase +{ + private readonly string _extensionsClassName; + private readonly string _methodName; + + public DependencyInjectionFileBuilder(string moduleName, string assemblyName) : base(moduleName) + { + _extensionsClassName = moduleName + "MediatorBuilderExtensions"; + _methodName = "Add" + moduleName; + HintName = _extensionsClassName + "." + ComputeSalt(assemblyName); + } + + public string HintName { get; } + + /// + protected override string Namespace => "Microsoft.Extensions.DependencyInjection"; + + /// + public override void WriteBeginClass() + { + Writer.WriteGeneratedAttribute(); + Writer.WriteIndentedLine("public static class {0}", _extensionsClassName); + Writer.WriteIndentedLine("{"); + Writer.IncreaseIndent(); + } + + public void WriteBeginRegistrationMethod() + { + Writer.WriteIndentedLine("public static global::Mocha.Mediator.IMediatorHostBuilder {0}(", _methodName); + Writer.IncreaseIndent(); + Writer.WriteIndentedLine("this global::Mocha.Mediator.IMediatorHostBuilder builder)"); + Writer.DecreaseIndent(); + Writer.WriteIndentedLine("{"); + Writer.IncreaseIndent(); + + Writer.WriteIndentedLine("var services = builder.Services;"); + Writer.WriteIndentedLine("var lifetime = builder.Options.ServiceLifetime;"); + } + + public void WriteHandlerRegistration(HandlerInfo handler) + { + switch (handler.Kind) + { + case HandlerKind.CommandVoid: + WriteServiceDescriptor( + "global::Mocha.Mediator.ICommandHandler<{0}>", + handler.HandlerTypeName, + handler.MessageTypeName); + break; + case HandlerKind.CommandResponse: + WriteServiceDescriptor( + "global::Mocha.Mediator.ICommandHandler<{0}, {1}>", + handler.HandlerTypeName, + handler.MessageTypeName, + handler.ResponseTypeName!); + break; + case HandlerKind.Query: + WriteServiceDescriptor( + "global::Mocha.Mediator.IQueryHandler<{0}, {1}>", + handler.HandlerTypeName, + handler.MessageTypeName, + handler.ResponseTypeName!); + break; + } + } + + public void WriteNotificationHandlerRegistration(NotificationHandlerInfo handler) + { + // Use TryAddEnumerable to prevent duplicate handler registrations + // when the generated Add{Module} extension is called more than once + // (e.g. in tests or modular startup). + Writer.WriteIndentedLine( + "global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler<{0}>), typeof({1}), lifetime));", + handler.NotificationTypeName, + handler.HandlerTypeName); + } + + /// + /// Writes the opening of a ConfigureMediator lambda for deferred pipeline registrations. + /// + public void WriteBeginConfigureMediator() + { + Writer.WriteIndentedLine("global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b =>"); + Writer.WriteIndentedLine("{"); + Writer.IncreaseIndent(); + } + + /// + /// Writes the closing of a ConfigureMediator lambda. + /// + public void WriteEndConfigureMediator() + { + Writer.DecreaseIndent(); + Writer.WriteIndentedLine("});"); + } + + /// + /// Writes a pipeline registration for a handler (inside ConfigureMediator lambda, using 'b'). + /// + public void WritePipelineRegistration(HandlerInfo handler) + { + var (terminalMethod, responseType) = handler.Kind switch + { + HandlerKind.CommandVoid => ($"BuildVoidCommandTerminal<{handler.MessageTypeName}>()", null), + HandlerKind.CommandResponse => ($"BuildCommandTerminal<{handler.MessageTypeName}, {handler.ResponseTypeName}>()", handler.ResponseTypeName), + HandlerKind.Query => ($"BuildQueryTerminal<{handler.MessageTypeName}, {handler.ResponseTypeName}>()", handler.ResponseTypeName), + _ => throw new ArgumentOutOfRangeException() + }; + + WritePipelineConfiguration(handler.MessageTypeName, responseType, terminalMethod); + } + + /// + /// Writes a pipeline registration for a notification group (inside ConfigureMediator lambda, using 'b'). + /// + public void WriteNotificationPipelineRegistration(string notificationType, + List groupHandlers) + { + var handlerTypeArgs = string.Join(", ", + groupHandlers.Select(h => $"typeof({h.HandlerTypeName})")); + + var terminalMethod = $"BuildNotificationTerminal<{notificationType}>(new global::System.Type[] {{ {handlerTypeArgs} }})"; + WritePipelineConfiguration(notificationType, null, terminalMethod); + } + + private void WritePipelineConfiguration(string messageTypeName, string? responseTypeName, string terminalMethod) + { + Writer.WriteIndentedLine("b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration"); + Writer.WriteIndentedLine("{"); + Writer.IncreaseIndent(); + Writer.WriteIndentedLine("MessageType = typeof({0}),", messageTypeName); + + if (responseTypeName is not null) + { + Writer.WriteIndentedLine("ResponseType = typeof({0}),", responseTypeName); + } + + Writer.WriteIndentedLine("Terminal = global::Mocha.Mediator.PipelineBuilder.{0}", terminalMethod); + Writer.DecreaseIndent(); + Writer.WriteIndentedLine("});"); + } + + public void WriteSectionComment(string comment) + { + Writer.WriteLine(); + Writer.WriteIndentedLine("// {0}", comment); + } + + public void WriteEndRegistrationMethod() + { + Writer.WriteLine(); + Writer.WriteIndentedLine("return builder;"); + Writer.DecreaseIndent(); + Writer.WriteIndentedLine("}"); + } + + private void WriteServiceDescriptor(string serviceTypeFormat, string implementationType, params object[] typeArgs) + { + // Use TryAdd to prevent duplicate handler registrations + // when the generated Add{Module} extension is called more than once. + var serviceType = string.Format(serviceTypeFormat, typeArgs); + Writer.WriteIndentedLine( + "global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof({0}), typeof({1}), lifetime));", + serviceType, + implementationType); + } + +#pragma warning disable CA5351 // MD5 is used for non-security hashing (file name salting) + private static readonly MD5 s_md5 = MD5.Create(); +#pragma warning restore CA5351 + + private static string ComputeSalt(string assemblyName) + { + byte[] hashBytes; + + lock (s_md5) + { + hashBytes = s_md5.ComputeHash(Encoding.UTF8.GetBytes(assemblyName)); + } + + var base64 = Convert.ToBase64String(hashBytes, Base64FormattingOptions.None); + + return base64.Replace("+", "-").Replace("/", "_").TrimEnd('='); + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/FileBuilders/FileBuilderBase.cs b/src/Mocha/src/Mocha.Analyzers/FileBuilders/FileBuilderBase.cs new file mode 100644 index 00000000000..70cfabfb442 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/FileBuilders/FileBuilderBase.cs @@ -0,0 +1,91 @@ +using System.Text; +using Mocha.Analyzers.Utils; + +namespace Mocha.Analyzers.FileBuilders; + +/// +/// Provides base functionality for file builders that generate C# source code, +/// including pooled management, file header writing, +/// and namespace/class scaffolding. +/// +public abstract class FileBuilderBase : IFileBuilder +{ + private StringBuilder _sb; + private CodeWriter _writer; + private bool _disposed; + + protected FileBuilderBase(string moduleName) + { + ModuleName = moduleName; + _sb = PooledObjects.GetStringBuilder(); + _writer = new CodeWriter(_sb); + } + + /// + /// Gets the code writer used to emit generated source code. + /// + protected CodeWriter Writer => _writer; + + /// + /// Gets the module name used to prefix generated type names. + /// + protected string ModuleName { get; } + + /// + /// Gets the namespace to use in the generated source file. + /// + protected abstract string Namespace { get; } + + /// + public void WriteHeader() + { + _writer.WriteFileHeader(); + } + + /// + public void WriteBeginNamespace() + { + _writer.WriteIndentedLine("namespace {0}", Namespace); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + /// + public void WriteEndNamespace() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + } + + /// + public abstract void WriteBeginClass(); + + /// + public void WriteEndClass() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + } + + /// + public string ToSourceText() + { + _writer.Flush(); + return _sb.ToString(); + } + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _writer.Dispose(); + PooledObjects.Return(_sb); + _sb = null!; + _writer = null!; + _disposed = true; + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/FileBuilders/IFileBuilder.cs b/src/Mocha/src/Mocha.Analyzers/FileBuilders/IFileBuilder.cs new file mode 100644 index 00000000000..796dc1197de --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/FileBuilders/IFileBuilder.cs @@ -0,0 +1,38 @@ +namespace Mocha.Analyzers.FileBuilders; + +/// +/// Defines the contract for a file builder that generates C# source code +/// using a structured sequence of write operations. +/// +public interface IFileBuilder : IDisposable +{ + /// + /// Writes the standard auto-generated file header. + /// + void WriteHeader(); + + /// + /// Writes the opening namespace declaration. + /// + void WriteBeginNamespace(); + + /// + /// Writes the closing namespace brace. + /// + void WriteEndNamespace(); + + /// + /// Writes the opening class declaration. + /// + void WriteBeginClass(); + + /// + /// Writes the closing class brace. + /// + void WriteEndClass(); + + /// + /// Returns the generated source code as a string. + /// + string ToSourceText(); +} diff --git a/src/Mocha/src/Mocha.Analyzers/Filters/AssemblyAttributeListFilter.cs b/src/Mocha/src/Mocha.Analyzers/Filters/AssemblyAttributeListFilter.cs new file mode 100644 index 00000000000..bde10a752d5 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Filters/AssemblyAttributeListFilter.cs @@ -0,0 +1,21 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Mocha.Analyzers.Filters; + +/// +/// Provides a singleton that matches attribute list syntax nodes +/// (used to discover assembly-level attributes such as [assembly: MediatorModule(...)]). +/// +public sealed class AssemblyAttributeListFilter : ISyntaxFilter +{ + private AssemblyAttributeListFilter() { } + + /// + public bool IsMatch(SyntaxNode node) => node is AttributeListSyntax { Target.Identifier.Text: "assembly" }; + + /// + /// Gets the singleton instance of . + /// + public static AssemblyAttributeListFilter Instance { get; } = new(); +} diff --git a/src/Mocha/src/Mocha.Analyzers/Filters/ClassWithMochaBaseListFilter.cs b/src/Mocha/src/Mocha.Analyzers/Filters/ClassWithMochaBaseListFilter.cs new file mode 100644 index 00000000000..675e6159328 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Filters/ClassWithMochaBaseListFilter.cs @@ -0,0 +1,53 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Mocha.Analyzers.Filters; + +/// +/// Provides a singleton that matches type declarations with a base list +/// containing candidate Mocha interface names. This narrows the syntactic predicate to avoid +/// flooding the transform phase with irrelevant types. +/// +public sealed class ClassWithMochaBaseListFilter : ISyntaxFilter +{ + private ClassWithMochaBaseListFilter() { } + + /// + public bool IsMatch(SyntaxNode node) + => node is TypeDeclarationSyntax { BaseList.Types.Count: > 0 } typeDecl + && HasCandidateBaseType(typeDecl.BaseList); + + /// + /// Gets the singleton instance of . + /// + public static ClassWithMochaBaseListFilter Instance { get; } = new(); + + private static bool HasCandidateBaseType(BaseListSyntax baseList) + { + foreach (var baseType in baseList.Types) + { + var name = GetBaseTypeName(baseType); + if (name is not null && IsMochaCandidateName(name)) + { + return true; + } + } + + return false; + } + + private static string? GetBaseTypeName(BaseTypeSyntax baseType) + => baseType.Type switch + { + SimpleNameSyntax simple => simple.Identifier.Text, + QualifiedNameSyntax qualified => qualified.Right.Identifier.Text, + AliasQualifiedNameSyntax alias => alias.Name.Identifier.Text, + _ => null + }; + + private static bool IsMochaCandidateName(string name) + => name.StartsWith("ICommand", StringComparison.Ordinal) + || name.StartsWith("IQuery", StringComparison.Ordinal) + || name.StartsWith("INotification", StringComparison.Ordinal) + || name.StartsWith("IStream", StringComparison.Ordinal); +} diff --git a/src/Mocha/src/Mocha.Analyzers/Filters/ISyntaxFilter.cs b/src/Mocha/src/Mocha.Analyzers/Filters/ISyntaxFilter.cs new file mode 100644 index 00000000000..153decb353a --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Filters/ISyntaxFilter.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; + +namespace Mocha.Analyzers; + +/// +/// Defines a filter that determines whether a is a candidate for further inspection +/// during source generation. +/// +public interface ISyntaxFilter +{ + /// + /// Gets a value indicating whether the specified matches this filter's criteria. + /// + /// The syntax node to evaluate. + /// if the node matches; otherwise, . + bool IsMatch(SyntaxNode node); +} diff --git a/src/Mocha/src/Mocha.Analyzers/Filters/SyntaxFilterBuilder.cs b/src/Mocha/src/Mocha.Analyzers/Filters/SyntaxFilterBuilder.cs new file mode 100644 index 00000000000..dc85fca89f8 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Filters/SyntaxFilterBuilder.cs @@ -0,0 +1,68 @@ +using Microsoft.CodeAnalysis; + +namespace Mocha.Analyzers.Filters; + +/// +/// Provides a builder for composing multiple instances into a single +/// predicate that matches when any filter matches. +/// +public sealed class SyntaxFilterBuilder +{ + private readonly List _filters = []; + + /// + /// Adds a filter to the builder if it has not already been added. + /// + /// The filter to add. + /// This builder instance for chaining. + public SyntaxFilterBuilder Add(ISyntaxFilter filter) + { + if (!_filters.Contains(filter)) + { + _filters.Add(filter); + } + + return this; + } + + /// + /// Adds multiple filters to the builder, skipping any that have already been added. + /// + /// The filters to add. + /// This builder instance for chaining. + public SyntaxFilterBuilder AddRange(IEnumerable filters) + { + foreach (var filter in filters) + { + if (!_filters.Contains(filter)) + { + _filters.Add(filter); + } + } + + return this; + } + + /// + /// Builds a composite predicate that returns if any registered filter matches + /// the given . + /// + /// A predicate combining all registered filters with logical OR semantics. + public Func Build() + { + var filters = _filters.ToArray(); + + return node => + { + for (var i = 0; i < filters.Length; i++) + { + if (filters[i].IsMatch(node)) + { + return true; + } + } + + return false; + }; + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs b/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs new file mode 100644 index 00000000000..53e2193b727 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs @@ -0,0 +1,99 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Mocha.Analyzers.FileBuilders; + +namespace Mocha.Analyzers.Generators; + +/// +/// Provides a source generator that emits the AddHandlers extension method on +/// IMediatorHostBuilder, registering handlers and pipelines into the dependency injection container. +/// +public sealed class DependencyInjectionGenerator : ISyntaxGenerator +{ + /// + public void Generate( + SourceProductionContext context, + string assemblyName, + string moduleName, + ImmutableArray syntaxInfos, + Action addSource) + { + var handlers = syntaxInfos + .OfType() + .Where(h => h.Diagnostics.Count == 0) + .OrderBy(h => h.OrderByKey) + .ToList(); + + var notificationHandlers = syntaxInfos + .OfType() + .Where(h => h.Diagnostics.Count == 0) + .OrderBy(h => h.OrderByKey) + .ToList(); + + if (handlers.Count == 0 && notificationHandlers.Count == 0) + { + return; + } + + using var builder = new DependencyInjectionFileBuilder(moduleName, assemblyName); + + builder.WriteHeader(); + builder.WriteBeginNamespace(); + builder.WriteBeginClass(); + builder.WriteBeginRegistrationMethod(); + + // Register handlers + if (handlers.Count > 0) + { + builder.WriteSectionComment("Register handlers"); + + foreach (var handler in handlers) + { + builder.WriteHandlerRegistration(handler); + } + } + + // Register notification handlers + if (notificationHandlers.Count > 0) + { + builder.WriteSectionComment("Register notification handlers"); + + foreach (var handler in notificationHandlers) + { + builder.WriteNotificationHandlerRegistration(handler); + } + } + + // Register pipelines (all handlers + notifications) via deferred ConfigureMediator + var notificationGroups = notificationHandlers + .GroupBy(h => h.NotificationTypeName) + .OrderBy(g => g.Key) + .ToList(); + + if (handlers.Count > 0 || notificationGroups.Count > 0) + { + builder.WriteSectionComment("Register pipelines"); + builder.WriteBeginConfigureMediator(); + + foreach (var handler in handlers) + { + builder.WritePipelineRegistration(handler); + } + + foreach (var group in notificationGroups) + { + builder.WriteNotificationPipelineRegistration( + group.Key, + group.OrderBy(h => h.HandlerTypeName).ToList()); + } + + builder.WriteEndConfigureMediator(); + } + + builder.WriteEndRegistrationMethod(); + builder.WriteEndClass(); + builder.WriteEndNamespace(); + + addSource(builder.HintName + ".g.cs", builder.ToSourceText()); + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/Generators/ISyntaxGenerator.cs b/src/Mocha/src/Mocha.Analyzers/Generators/ISyntaxGenerator.cs new file mode 100644 index 00000000000..c262116827d --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Generators/ISyntaxGenerator.cs @@ -0,0 +1,25 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Mocha.Analyzers; + +/// +/// Defines a contract for generating C# source code from collected entries. +/// +public interface ISyntaxGenerator +{ + /// + /// Generates source code from the provided syntax information and adds it to the compilation output. + /// + /// The source production context for reporting diagnostics. + /// The name of the assembly being compiled. + /// The module name derived from the assembly name, used to prefix generated type names. + /// The collected syntax information entries to generate code from. + /// A delegate that adds a generated source file with the specified hint name and content. + void Generate( + SourceProductionContext context, + string assemblyName, + string moduleName, + ImmutableArray syntaxInfos, + Action addSource); +} diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/AbstractHandlerInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/AbstractHandlerInspector.cs new file mode 100644 index 00000000000..2f26a630ad0 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/AbstractHandlerInspector.cs @@ -0,0 +1,112 @@ +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Mocha.Analyzers.Filters; +using Mocha.Analyzers.Utils; + +namespace Mocha.Analyzers.Inspectors; + +/// +/// Represents an inspector that detects abstract class or record declarations implementing any handler interface +/// and reports the MO0003 diagnostic to warn that abstract handlers cannot be registered. +/// +public sealed class AbstractHandlerInspector : ISyntaxInspector +{ + /// + public ImmutableArray Filters { get; } = [ClassWithMochaBaseListFilter.Instance]; + + /// + public IImmutableSet SupportedKinds { get; } = + ImmutableHashSet.Create(SyntaxKind.ClassDeclaration, SyntaxKind.RecordDeclaration); + + /// + public bool TryHandle( + KnownTypeSymbols knownSymbols, + SyntaxNode node, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out SyntaxInfo? syntaxInfo) + { + if (node is not TypeDeclarationSyntax typeDeclaration) + { + syntaxInfo = null; + return false; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var typeSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration); + + if (typeSymbol is not { } namedTypeSymbol) + { + syntaxInfo = null; + return false; + } + + // Only handle abstract types (non-interface) + if (!namedTypeSymbol.IsAbstract || namedTypeSymbol.TypeKind == TypeKind.Interface) + { + syntaxInfo = null; + return false; + } + + // Check if it implements any handler interface + if (!ImplementsAnyHandlerInterface(knownSymbols, namedTypeSymbol)) + { + syntaxInfo = null; + return false; + } + + var handlerName = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var locationInfo = typeDeclaration.Identifier.GetLocation().ToLocationInfo(); + + // Create a placeholder SyntaxInfo carrying the diagnostic + syntaxInfo = new AbstractHandlerDiagnosticInfo(handlerName) + { + Diagnostics = new([ + new DiagnosticInfo(Errors.AbstractHandler.Id, locationInfo, new([handlerName])) + ]) + }; + return true; + } + + private static bool ImplementsAnyHandlerInterface(KnownTypeSymbols knownSymbols, INamedTypeSymbol namedTypeSymbol) + { + if (knownSymbols.ICommandHandlerVoid is not null + && namedTypeSymbol.FindImplementedInterface(knownSymbols.ICommandHandlerVoid) is not null) + { + return true; + } + + if (knownSymbols.ICommandHandlerResponse is not null + && namedTypeSymbol.FindImplementedInterface(knownSymbols.ICommandHandlerResponse) is not null) + { + return true; + } + + if (knownSymbols.IQueryHandler is not null + && namedTypeSymbol.FindImplementedInterface(knownSymbols.IQueryHandler) is not null) + { + return true; + } + + if (knownSymbols.INotificationHandler is not null + && namedTypeSymbol.FindImplementedInterface(knownSymbols.INotificationHandler) is not null) + { + return true; + } + + return false; + } +} + +/// +/// A diagnostic-only SyntaxInfo used to carry MO0003 diagnostics. +/// This is not used by code generators. +/// +internal sealed record AbstractHandlerDiagnosticInfo(string HandlerTypeName) : SyntaxInfo +{ + public override string OrderByKey => $"AbstractDiag:{HandlerTypeName}"; +} diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs new file mode 100644 index 00000000000..d3989d8397b --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs @@ -0,0 +1,90 @@ +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Mocha.Analyzers.Filters; +using Mocha.Analyzers.Utils; + +namespace Mocha.Analyzers.Inspectors; + +/// +/// Represents an inspector that detects concrete class or record declarations implementing +/// command or query handler interfaces such as ICommandHandler or IQueryHandler. +/// +public sealed class HandlerInspector : ISyntaxInspector +{ + /// + public ImmutableArray Filters { get; } = [ClassWithMochaBaseListFilter.Instance]; + + /// + public IImmutableSet SupportedKinds { get; } = + ImmutableHashSet.Create(SyntaxKind.ClassDeclaration, SyntaxKind.RecordDeclaration); + + private static readonly HandlerKindDescriptor[] s_handlerKinds = + [ + new(static s => s.ICommandHandlerVoid, HandlerKind.CommandVoid, HasResponse: false), + new(static s => s.ICommandHandlerResponse, HandlerKind.CommandResponse, HasResponse: true), + new(static s => s.IQueryHandler, HandlerKind.Query, HasResponse: true) + ]; + + /// + public bool TryHandle( + KnownTypeSymbols knownSymbols, + SyntaxNode node, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out SyntaxInfo? syntaxInfo) + { + syntaxInfo = null; + + if (node is not TypeDeclarationSyntax typeDeclaration) + { + return false; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var namedTypeSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration); + + if (namedTypeSymbol is null + || namedTypeSymbol.IsAbstract + || namedTypeSymbol.TypeKind == TypeKind.Interface) + { + return false; + } + + foreach (var descriptor in s_handlerKinds) + { + var target = descriptor.GetTarget(knownSymbols); + var implemented = target is not null + ? namedTypeSymbol.FindImplementedInterface(target) + : null; + + if (implemented is null) + { + continue; + } + + var handlerFullName = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var handlerNamespace = namedTypeSymbol.ContainingNamespace?.ToDisplayString() ?? string.Empty; + + syntaxInfo = new HandlerInfo( + handlerFullName, + handlerNamespace, + implemented.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + descriptor.HasResponse + ? implemented.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + : null, + descriptor.Kind); + return true; + } + + return false; + } + + private sealed record HandlerKindDescriptor( + Func GetTarget, + HandlerKind Kind, + bool HasResponse); +} diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/ISyntaxInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/ISyntaxInspector.cs new file mode 100644 index 00000000000..575c481b6dc --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/ISyntaxInspector.cs @@ -0,0 +1,44 @@ +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Mocha.Analyzers; + +/// +/// Defines a contract for inspecting syntax nodes and extracting mediator-related semantic information. +/// +public interface ISyntaxInspector +{ + /// + /// Gets the syntax filters used to pre-screen candidate syntax nodes before inspection. + /// + ImmutableArray Filters { get; } + + /// + /// Gets the set of values that this inspector can process. + /// + IImmutableSet SupportedKinds { get; } + + /// + /// Attempts to inspect the specified syntax node and produce a describing its mediator semantics. + /// + /// The well-known Mocha type symbols resolved from the current compilation. + /// The syntax node to inspect. + /// The semantic model for the syntax tree containing . + /// A token to observe for cancellation requests. + /// + /// When this method returns , contains the extracted syntax information; + /// otherwise, . + /// + /// + /// if the inspector recognized the node and produced a result; otherwise, + /// . + /// + bool TryHandle( + KnownTypeSymbols knownSymbols, + SyntaxNode node, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out SyntaxInfo? syntaxInfo); +} diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/MediatorModuleInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/MediatorModuleInspector.cs new file mode 100644 index 00000000000..d53e90f22d3 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/MediatorModuleInspector.cs @@ -0,0 +1,65 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Mocha.Analyzers.Filters; + +namespace Mocha.Analyzers.Inspectors; + +/// +/// Inspects assembly-level attribute lists to discover [assembly: MediatorModule("...")] +/// declarations and extract from them. +/// +public sealed class MediatorModuleInspector : ISyntaxInspector +{ + /// + public ImmutableArray Filters { get; } = [AssemblyAttributeListFilter.Instance]; + + /// + public IImmutableSet SupportedKinds { get; } = [SyntaxKind.AttributeList]; + + /// + public bool TryHandle( + KnownTypeSymbols knownSymbols, + SyntaxNode node, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out SyntaxInfo? syntaxInfo) + { + if (node is AttributeListSyntax attributeList) + { + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var attributeSyntax in attributeList.Attributes) + { + var symbol = ModelExtensions.GetSymbolInfo(semanticModel, attributeSyntax).Symbol; + + if (symbol is not IMethodSymbol attributeSymbol) + { + continue; + } + + var attributeContainingTypeSymbol = attributeSymbol.ContainingType; + var fullName = attributeContainingTypeSymbol.ToDisplayString(); + + if (string.Equals(fullName, SyntaxConstants.MediatorModuleAttribute, StringComparison.Ordinal) + && attributeSyntax.ArgumentList is { Arguments.Count: > 0 }) + { + var nameExpr = attributeSyntax.ArgumentList.Arguments[0].Expression; + var constantValue = semanticModel.GetConstantValue(nameExpr); + + if (!constantValue.HasValue || constantValue.Value is not string name) + { + continue; + } + + syntaxInfo = new MediatorModuleInfo(name); + return true; + } + } + } + + syntaxInfo = null; + return false; + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/MessageTypeInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/MessageTypeInspector.cs new file mode 100644 index 00000000000..19e5ad0bb35 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/MessageTypeInspector.cs @@ -0,0 +1,153 @@ +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Mocha.Analyzers.Filters; +using Mocha.Analyzers.Utils; + +namespace Mocha.Analyzers.Inspectors; + +/// +/// Represents an inspector that detects concrete type declarations (classes, records, and structs) +/// implementing message interfaces such as ICommand, ICommand<T>, +/// or IQuery<T>. +/// +public sealed class MessageTypeInspector : ISyntaxInspector +{ + /// + public ImmutableArray Filters { get; } = [ClassWithMochaBaseListFilter.Instance]; + + /// + public IImmutableSet SupportedKinds { get; } = + ImmutableHashSet.Create( + SyntaxKind.ClassDeclaration, + SyntaxKind.RecordDeclaration, + SyntaxKind.StructDeclaration, + SyntaxKind.RecordStructDeclaration); + + /// + public bool TryHandle( + KnownTypeSymbols knownSymbols, + SyntaxNode node, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out SyntaxInfo? syntaxInfo) + { + if (node is not TypeDeclarationSyntax typeDeclaration) + { + syntaxInfo = null; + return false; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var typeSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration); + + if (typeSymbol is not { } namedTypeSymbol) + { + syntaxInfo = null; + return false; + } + + // Skip abstract types and interfaces - they are not concrete message types + if (namedTypeSymbol.IsAbstract || namedTypeSymbol.TypeKind == TypeKind.Interface) + { + syntaxInfo = null; + return false; + } + + // Open generic types cannot be dispatched at runtime - report MO0004 + if (namedTypeSymbol.IsGenericType && namedTypeSymbol.TypeParameters.Length > 0 + && SymbolEqualityComparer.Default.Equals(namedTypeSymbol, namedTypeSymbol.OriginalDefinition)) + { + if (ImplementsAnyMessageInterface(knownSymbols, namedTypeSymbol)) + { + var openTypeName = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var locationInfo = typeDeclaration.Identifier.GetLocation().ToLocationInfo(); + + syntaxInfo = new OpenGenericMessageDiagnosticInfo(openTypeName) + { + Diagnostics = new([ + new DiagnosticInfo( + Errors.OpenGenericMessageType.Id, + locationInfo, + new([openTypeName])) + ]) + }; + return true; + } + + syntaxInfo = null; + return false; + } + + var typeName = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var typeNamespace = namedTypeSymbol.ContainingNamespace?.ToDisplayString() ?? string.Empty; + var location = typeDeclaration.Identifier.GetLocation().ToLocationInfo(); + + // Try ICommand (void) + if (knownSymbols.ICommandVoid is not null + && namedTypeSymbol.ImplementsInterface(knownSymbols.ICommandVoid)) + { + // Check if it also implements ICommand - if so, treat as CommandResponse + if (knownSymbols.ICommandOfT is not null + && namedTypeSymbol.FindImplementedInterface(knownSymbols.ICommandOfT) is not null) + { + syntaxInfo = new MessageTypeInfo(typeName, typeNamespace, MessageKind.CommandResponse, location); + return true; + } + + syntaxInfo = new MessageTypeInfo(typeName, typeNamespace, MessageKind.CommandVoid, location); + return true; + } + + // Try ICommand + if (knownSymbols.ICommandOfT is not null + && namedTypeSymbol.FindImplementedInterface(knownSymbols.ICommandOfT) is not null) + { + syntaxInfo = new MessageTypeInfo(typeName, typeNamespace, MessageKind.CommandResponse, location); + return true; + } + + // Try IQuery + if (knownSymbols.IQueryOfT is not null + && namedTypeSymbol.FindImplementedInterface(knownSymbols.IQueryOfT) is not null) + { + syntaxInfo = new MessageTypeInfo(typeName, typeNamespace, MessageKind.Query, location); + return true; + } + + syntaxInfo = null; + return false; + } + + private static bool ImplementsAnyMessageInterface(KnownTypeSymbols knownSymbols, INamedTypeSymbol type) + { + if (knownSymbols.ICommandVoid is not null && type.ImplementsInterface(knownSymbols.ICommandVoid)) + { + return true; + } + + if (knownSymbols.ICommandOfT is not null && type.FindImplementedInterface(knownSymbols.ICommandOfT) is not null) + { + return true; + } + + if (knownSymbols.IQueryOfT is not null && type.FindImplementedInterface(knownSymbols.IQueryOfT) is not null) + { + return true; + } + + return false; + } +} + +/// +/// A diagnostic-only SyntaxInfo used to carry MO0004 diagnostics for open generic message types. +/// This is not used by code generators. +/// +internal sealed record OpenGenericMessageDiagnosticInfo(string MessageTypeName) : SyntaxInfo +{ + public override string OrderByKey => $"OpenGenericDiag:{MessageTypeName}"; +} diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/NotificationHandlerInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/NotificationHandlerInspector.cs new file mode 100644 index 00000000000..c6ae8e1060d --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/NotificationHandlerInspector.cs @@ -0,0 +1,72 @@ +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Mocha.Analyzers.Filters; +using Mocha.Analyzers.Utils; + +namespace Mocha.Analyzers.Inspectors; + +/// +/// Represents an inspector that detects concrete class or record declarations implementing +/// INotificationHandler<T>. +/// +public sealed class NotificationHandlerInspector : ISyntaxInspector +{ + /// + public ImmutableArray Filters { get; } = [ClassWithMochaBaseListFilter.Instance]; + + /// + public IImmutableSet SupportedKinds { get; } = + ImmutableHashSet.Create(SyntaxKind.ClassDeclaration, SyntaxKind.RecordDeclaration); + + /// + public bool TryHandle( + KnownTypeSymbols knownSymbols, + SyntaxNode node, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out SyntaxInfo? syntaxInfo) + { + if (node is not TypeDeclarationSyntax typeDeclaration + || knownSymbols.INotificationHandler is null) + { + syntaxInfo = null; + return false; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var typeSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration); + + if (typeSymbol is not { } namedTypeSymbol) + { + syntaxInfo = null; + return false; + } + + // Skip abstract types and interfaces + if (namedTypeSymbol.IsAbstract || namedTypeSymbol.TypeKind == TypeKind.Interface) + { + syntaxInfo = null; + return false; + } + + var implemented = namedTypeSymbol.FindImplementedInterface(knownSymbols.INotificationHandler); + if (implemented is null) + { + syntaxInfo = null; + return false; + } + + var notificationType = implemented.TypeArguments[0]; + + syntaxInfo = new NotificationHandlerInfo( + namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + namedTypeSymbol.ContainingNamespace?.ToDisplayString() ?? string.Empty, + notificationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + + return true; + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/IsExternalInit.cs b/src/Mocha/src/Mocha.Analyzers/IsExternalInit.cs new file mode 100644 index 00000000000..93406e1e014 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/IsExternalInit.cs @@ -0,0 +1,53 @@ +// Polyfills for netstandard2.0 to enable modern C# features that the compiler +// expects specific types to exist for (init-only setters, required members, etc.). + +namespace System.Runtime.CompilerServices +{ + /// + /// Represents a polyfill that enables init-only property setters on netstandard2.0. + /// + internal sealed class IsExternalInit; + + /// + /// Represents a polyfill that enables the modifier on netstandard2.0. + /// + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, + AllowMultiple = false, + Inherited = false)] + internal sealed class RequiredMemberAttribute : Attribute; + + /// + /// Represents a polyfill that enables compiler feature gating on netstandard2.0. + /// + [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class CompilerFeatureRequiredAttribute : Attribute + { + /// + /// Initializes a new instance of the class + /// with the specified feature name. + /// + /// The name of the compiler feature that is required. + public CompilerFeatureRequiredAttribute(string featureName) => FeatureName = featureName; + + /// + /// Gets the name of the compiler feature that is required. + /// + public string FeatureName { get; } + + /// + /// Gets or sets a value indicating whether the feature requirement is optional. + /// + public bool IsOptional { get; init; } + } +} + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Represents a polyfill that enables the modifier on constructor + /// parameters for netstandard2.0. + /// + [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] + internal sealed class SetsRequiredMembersAttribute : Attribute; +} diff --git a/src/Mocha/src/Mocha.Analyzers/KnownTypeSymbols.cs b/src/Mocha/src/Mocha.Analyzers/KnownTypeSymbols.cs new file mode 100644 index 00000000000..0621a4d9b95 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/KnownTypeSymbols.cs @@ -0,0 +1,98 @@ +using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.CodeAnalysis; + +namespace Mocha.Analyzers; + +/// +/// Represents a cache of commonly used Mocha mediator type symbols resolved from a +/// . Symbols are resolved lazily on first access. +/// A single instance is shared across all syntax nodes for a given compilation via +/// . +/// +public sealed class KnownTypeSymbols +{ + private static readonly ConditionalWeakTable s_cache = new(); + + private readonly Compilation _compilation; + + private Resolved? _commandHandlerVoid; + private Resolved? _commandHandlerResponse; + private Resolved? _queryHandler; + private Resolved? _notificationHandler; + private Resolved? _commandVoid; + private Resolved? _commandOfT; + private Resolved? _queryOfT; + + private KnownTypeSymbols(Compilation compilation) + { + _compilation = compilation; + } + + /// + /// Gets or creates a instance for the specified compilation. + /// The instance is cached and reused for the lifetime of the object. + /// + public static KnownTypeSymbols GetOrCreate(Compilation compilation) + => s_cache.GetValue(compilation, static c => new KnownTypeSymbols(c)); + + /// + /// Gets the symbol for the ICommandHandler<TCommand> interface (void return). + /// + public INamedTypeSymbol? ICommandHandlerVoid + => Resolve(SyntaxConstants.ICommandHandlerVoid, ref _commandHandlerVoid); + + /// + /// Gets the symbol for the ICommandHandler<TCommand, TResponse> interface. + /// + public INamedTypeSymbol? ICommandHandlerResponse + => Resolve(SyntaxConstants.ICommandHandlerResponse, ref _commandHandlerResponse); + + /// + /// Gets the symbol for the IQueryHandler<TQuery, TResponse> interface. + /// + public INamedTypeSymbol? IQueryHandler + => Resolve(SyntaxConstants.IQueryHandler, ref _queryHandler); + + /// + /// Gets the symbol for the INotificationHandler<TNotification> interface. + /// + public INamedTypeSymbol? INotificationHandler + => Resolve(SyntaxConstants.INotificationHandler, ref _notificationHandler); + + /// + /// Gets the symbol for the ICommand marker interface (void return). + /// + public INamedTypeSymbol? ICommandVoid + => Resolve(SyntaxConstants.ICommand, ref _commandVoid); + + /// + /// Gets the symbol for the ICommand<TResponse> interface. + /// + public INamedTypeSymbol? ICommandOfT + => Resolve(SyntaxConstants.ICommandOfT, ref _commandOfT); + + /// + /// Gets the symbol for the IQuery<TResponse> interface. + /// + public INamedTypeSymbol? IQueryOfT + => Resolve(SyntaxConstants.IQueryOfT, ref _queryOfT); + + private INamedTypeSymbol? Resolve(string metadataName, ref Resolved? field) + { + var snapshot = Interlocked.CompareExchange(ref field, null, null); + if (snapshot is not null) + { + return snapshot.Value; + } + + var resolved = new Resolved(_compilation.GetTypeByMetadataName(metadataName)); + var existing = Interlocked.CompareExchange(ref field, resolved, null); + return (existing ?? resolved).Value; + } + + private sealed class Resolved(T value) + { + public readonly T Value = value; + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs b/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs new file mode 100644 index 00000000000..c43a4576e4b --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs @@ -0,0 +1,283 @@ +using System.Collections.Immutable; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Mocha.Analyzers.Filters; +using Mocha.Analyzers.Generators; +using Mocha.Analyzers.Inspectors; +using Mocha.Analyzers.Utils; + +namespace Mocha.Analyzers; + +/// +/// Provides an incremental source generator that discovers mediator message types +/// and handlers from the compilation and emits the mediator dispatch infrastructure +/// and dependency injection registrations. +/// +[Generator] +public sealed class MediatorGenerator : IIncrementalGenerator +{ + private static readonly ISyntaxInspector[] s_allInspectors = + [ + new HandlerInspector(), + new NotificationHandlerInspector(), + new MessageTypeInspector(), + new AbstractHandlerInspector(), + new MediatorModuleInspector() + ]; + + private static readonly ISyntaxGenerator[] s_generators = + [ + new DependencyInjectionGenerator() + ]; + + private static readonly Dictionary> s_inspectorLookup; + + private static readonly Func s_predicate; + + static MediatorGenerator() + { + var filterBuilder = new SyntaxFilterBuilder(); + var inspectorLookup = new Dictionary>(); + + foreach (var inspector in s_allInspectors) + { + filterBuilder.AddRange(inspector.Filters); + + foreach (var supportedKind in inspector.SupportedKinds) + { + if (!inspectorLookup.TryGetValue(supportedKind, out var inspectors)) + { + inspectors = []; + inspectorLookup[supportedKind] = inspectors; + } + + inspectors.Add(inspector); + } + } + + s_predicate = filterBuilder.Build(); + s_inspectorLookup = new Dictionary>(); + + foreach (var kvp in inspectorLookup) + { + s_inspectorLookup[kvp.Key] = kvp.Value.ToImmutableArray(); + } + } + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var syntaxInfos = context + .SyntaxProvider.CreateSyntaxProvider( + predicate: static (s, _) => s_predicate(s), + transform: static (ctx, ct) => Transform(ctx.Node, ctx.SemanticModel, ct)) + .WhereNotNull() + .WithComparer(EqualityComparer.Default) + .WithTrackingName("MochaSyntaxInfos") + .Collect() + .WithTrackingName("MochaCollectedInfos"); + + var assemblyName = context.CompilationProvider.Select(static (c, _) => c.AssemblyName ?? "Unknown"); + + context.RegisterSourceOutput( + assemblyName.Combine(syntaxInfos), + static (context, source) => Execute(context, source.Left, source.Right)); + } + + private static SyntaxInfo? Transform( + SyntaxNode node, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + if (!s_inspectorLookup.TryGetValue(node.Kind(), out var inspectors)) + { + return null; + } + + var knownTypeSymbols = KnownTypeSymbols.GetOrCreate(semanticModel.Compilation); + + foreach (var inspector in inspectors) + { + if (inspector.TryHandle(knownTypeSymbols, node, semanticModel, cancellationToken, out var syntaxInfo)) + { + return syntaxInfo; + } + } + + return null; + } + + private static void Execute( + SourceProductionContext context, + string assemblyName, + ImmutableArray syntaxInfos) + { + var sourceFiles = PooledObjects.GetStringDictionary(); + var moduleInfo = GetModuleInfo(syntaxInfos, ModuleNameHelper.CreateModuleName(assemblyName)); + + try + { + // Report diagnostics attached to individual SyntaxInfo entries (e.g. MO0003). + // Reconstruct Roslyn Diagnostic objects from equatable DiagnosticInfo models. + foreach (var syntaxInfo in syntaxInfos) + { + if (syntaxInfo.Diagnostics.Count > 0) + { + foreach (var diagInfo in syntaxInfo.Diagnostics) + { + context.ReportDiagnostic(ReconstructDiagnostic(diagInfo)); + } + } + } + + // Validate message types vs handlers (MO0001, MO0002) + ValidateMessageHandlerPairing(context, syntaxInfos); + + foreach (var generator in s_generators) + { + generator.Generate( + context, + assemblyName, + moduleInfo.ModuleName, + syntaxInfos, + AddSource); + } + + foreach (var sourceFile in sourceFiles) + { + context.AddSource(sourceFile.Key, SourceText.From(sourceFile.Value, Encoding.UTF8)); + } + } + finally + { + PooledObjects.Return(sourceFiles); + } + + void AddSource(string fileName, string sourceText) + { + sourceFiles[fileName] = sourceText; + } + } + + private static MediatorModuleInfo GetModuleInfo(ImmutableArray syntaxInfos, string defaultModuleName) + { + foreach (var syntaxInfo in syntaxInfos) + { + if (syntaxInfo is MediatorModuleInfo module) + { + return new MediatorModuleInfo(ModuleNameHelper.SanitizeIdentifier(module.ModuleName)); + } + } + + return new MediatorModuleInfo(defaultModuleName); + } + + private static void ValidateMessageHandlerPairing( + SourceProductionContext context, + ImmutableArray syntaxInfos) + { + var messageTypes = new List(); + var handlers = new List(); + + foreach (var info in syntaxInfos) + { + if (info is MessageTypeInfo messageTypeInfo) + { + messageTypes.Add(messageTypeInfo); + } + else if (info is HandlerInfo handlerInfo && handlerInfo.Diagnostics.Count == 0) + { + handlers.Add(handlerInfo); + } + } + + if (messageTypes.Count == 0) + { + return; + } + + // Build a lookup of handlers by message type name + var handlersByMessageType = new Dictionary>(); + foreach (var handler in handlers) + { + if (!handlersByMessageType.TryGetValue(handler.MessageTypeName, out var list)) + { + list = new List(); + handlersByMessageType[handler.MessageTypeName] = list; + } + + list.Add(handler); + } + + foreach (var messageType in messageTypes) + { + var location = ReconstructLocation(messageType.Location); + + if (!handlersByMessageType.TryGetValue(messageType.MessageTypeName, out var matchingHandlers) + || matchingHandlers.Count == 0) + { + // MO0001: Missing handler + context.ReportDiagnostic( + Diagnostic.Create(Errors.MissingHandler, location, messageType.MessageTypeName)); + } + else if (matchingHandlers.Count > 1) + { + // MO0002: Duplicate handler (commands and queries must have exactly one) + // Notifications are allowed to have multiple handlers, but message types + // tracked by MessageTypeInspector are commands/queries only (not notifications) + var handlerNames = string.Join(", ", matchingHandlers.Select(h => h.HandlerTypeName).OrderBy(n => n)); + context.ReportDiagnostic( + Diagnostic.Create( + Errors.DuplicateHandler, + location, + messageType.MessageTypeName, + handlerNames)); + } + } + } + + private static readonly Dictionary s_descriptorLookup = new() + { + [Errors.MissingHandler.Id] = Errors.MissingHandler, + [Errors.DuplicateHandler.Id] = Errors.DuplicateHandler, + [Errors.AbstractHandler.Id] = Errors.AbstractHandler, + [Errors.OpenGenericMessageType.Id] = Errors.OpenGenericMessageType + }; + + private static Diagnostic ReconstructDiagnostic(DiagnosticInfo info) + { + var descriptor = s_descriptorLookup[info.DescriptorId]; + var location = ReconstructLocation(info.Location); + var args = new object[info.MessageArgs.Count]; + for (var i = 0; i < info.MessageArgs.Count; i++) + { + args[i] = info.MessageArgs[i]; + } + + return Diagnostic.Create(descriptor, location, args); + } + + private static Location ReconstructLocation(LocationInfo? locationInfo) + { + if (locationInfo is null) + { + return Location.None; + } + + return Location.Create( + locationInfo.FilePath, + default, + new LinePositionSpan( + new LinePosition(locationInfo.StartLine, locationInfo.StartColumn), + new LinePosition(locationInfo.EndLine, locationInfo.EndColumn))); + } +} + +file static class Extensions +{ + public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider source) + => source.Where(static t => t is not null)!; +} diff --git a/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj b/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj new file mode 100644 index 00000000000..9815174c66a --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj @@ -0,0 +1,23 @@ + + + netstandard2.0 + latest + enable + enable + false + false + true + true + true + + + + + + + + + + + + diff --git a/src/Mocha/src/Mocha.Analyzers/Models/DiagnosticInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/DiagnosticInfo.cs new file mode 100644 index 00000000000..82b20f8e7e8 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Models/DiagnosticInfo.cs @@ -0,0 +1,15 @@ +using Mocha.Analyzers.Utils; + +namespace Mocha.Analyzers; + +/// +/// An equatable representation of diagnostic information that can safely +/// flow through the incremental pipeline without rooting Roslyn objects. +/// +/// The diagnostic descriptor ID (e.g., "MO0003"). +/// The source location, or if unavailable. +/// The format arguments for the diagnostic message. +public sealed record DiagnosticInfo( + string DescriptorId, + LocationInfo? Location, + ImmutableEquatableArray MessageArgs); diff --git a/src/Mocha/src/Mocha.Analyzers/Models/HandlerInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/HandlerInfo.cs new file mode 100644 index 00000000000..1dfbb649edd --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Models/HandlerInfo.cs @@ -0,0 +1,22 @@ +namespace Mocha.Analyzers; + +/// +/// Represents the extracted metadata for a command or query handler discovered during source generation. +/// +/// The simple type name of the handler class. +/// The namespace containing the handler class. +/// The simple type name of the message the handler processes. +/// +/// The simple type name of the response, or if the handler returns no response. +/// +/// The kind of handler. +public sealed record HandlerInfo( + string HandlerTypeName, + string HandlerNamespace, + string MessageTypeName, + string? ResponseTypeName, + HandlerKind Kind) : SyntaxInfo +{ + /// + public override string OrderByKey => $"{Kind}:{MessageTypeName}"; +} diff --git a/src/Mocha/src/Mocha.Analyzers/Models/HandlerKind.cs b/src/Mocha/src/Mocha.Analyzers/Models/HandlerKind.cs new file mode 100644 index 00000000000..8093c8e1b56 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Models/HandlerKind.cs @@ -0,0 +1,22 @@ +namespace Mocha.Analyzers; + +/// +/// Defines the kinds of message handlers supported by the mediator. +/// +public enum HandlerKind +{ + /// + /// A command handler that returns no response. + /// + CommandVoid, + + /// + /// A command handler that returns a response. + /// + CommandResponse, + + /// + /// A query handler that returns a single response. + /// + Query +} diff --git a/src/Mocha/src/Mocha.Analyzers/Models/LocationInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/LocationInfo.cs new file mode 100644 index 00000000000..38baa448979 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Models/LocationInfo.cs @@ -0,0 +1,12 @@ +namespace Mocha.Analyzers; + +/// +/// An equatable representation of a source location that can safely +/// flow through the incremental pipeline without rooting Roslyn objects. +/// +/// The file path of the source location. +/// The zero-based start line number. +/// The zero-based start column number. +/// The zero-based end line number. +/// The zero-based end column number. +public sealed record LocationInfo(string FilePath, int StartLine, int StartColumn, int EndLine, int EndColumn); diff --git a/src/Mocha/src/Mocha.Analyzers/Models/MediatorModuleInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/MediatorModuleInfo.cs new file mode 100644 index 00000000000..73d318103ad --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Models/MediatorModuleInfo.cs @@ -0,0 +1,12 @@ +namespace Mocha.Analyzers; + +/// +/// Represents the metadata for a [assembly: MediatorModule("...")] attribute +/// discovered during source generation. +/// +/// The module name specified in the attribute. +public sealed record MediatorModuleInfo(string ModuleName) : SyntaxInfo +{ + /// + public override string OrderByKey => ModuleName; +} diff --git a/src/Mocha/src/Mocha.Analyzers/Models/MessageKind.cs b/src/Mocha/src/Mocha.Analyzers/Models/MessageKind.cs new file mode 100644 index 00000000000..ed34c42613e --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Models/MessageKind.cs @@ -0,0 +1,22 @@ +namespace Mocha.Analyzers; + +/// +/// Defines the kinds of message types discovered during source generation. +/// +public enum MessageKind +{ + /// + /// A void command (no response). + /// + CommandVoid, + + /// + /// A command that returns a response. + /// + CommandResponse, + + /// + /// A query that returns a response. + /// + Query +} diff --git a/src/Mocha/src/Mocha.Analyzers/Models/MessageTypeInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/MessageTypeInfo.cs new file mode 100644 index 00000000000..f2c6c4dd5bb --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Models/MessageTypeInfo.cs @@ -0,0 +1,22 @@ +namespace Mocha.Analyzers; + +/// +/// Represents the extracted metadata for a message type (command, query, or stream) discovered +/// during source generation. +/// +/// The simple type name of the message. +/// The namespace containing the message type. +/// The kind of message. +/// +/// The equatable source location of the message type declaration, or if +/// unavailable. +/// +public sealed record MessageTypeInfo( + string MessageTypeName, + string MessageNamespace, + MessageKind Kind, + LocationInfo? Location) : SyntaxInfo +{ + /// + public override string OrderByKey => $"Message:{MessageTypeName}"; +} diff --git a/src/Mocha/src/Mocha.Analyzers/Models/NotificationHandlerInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/NotificationHandlerInfo.cs new file mode 100644 index 00000000000..b3b84311736 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Models/NotificationHandlerInfo.cs @@ -0,0 +1,16 @@ +namespace Mocha.Analyzers; + +/// +/// Represents the extracted metadata for a notification handler discovered during source generation. +/// +/// The simple type name of the notification handler class. +/// The namespace containing the notification handler class. +/// The simple type name of the notification the handler processes. +public sealed record NotificationHandlerInfo( + string HandlerTypeName, + string HandlerNamespace, + string NotificationTypeName) : SyntaxInfo +{ + /// + public override string OrderByKey => $"Notification:{NotificationTypeName}:{HandlerTypeName}"; +} diff --git a/src/Mocha/src/Mocha.Analyzers/Models/SyntaxInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/SyntaxInfo.cs new file mode 100644 index 00000000000..7e52e0e647e --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Models/SyntaxInfo.cs @@ -0,0 +1,23 @@ +using Mocha.Analyzers.Utils; + +namespace Mocha.Analyzers; + +/// +/// Represents the base record for all syntax-derived information models +/// collected during source generation. +/// +public abstract record SyntaxInfo +{ + /// + /// Gets the key used to establish a deterministic ordering of syntax information entries. + /// + public abstract string OrderByKey { get; } + + /// + /// Gets the collection of diagnostics associated with this syntax information entry. + /// Uses instead of Roslyn's Diagnostic to maintain + /// value equality and avoid rooting old compilations in memory. + /// + public ImmutableEquatableArray Diagnostics { get; init; } = + ImmutableEquatableArray.Empty; +} diff --git a/src/Mocha/src/Mocha.Analyzers/SyntaxConstants.cs b/src/Mocha/src/Mocha.Analyzers/SyntaxConstants.cs new file mode 100644 index 00000000000..58d6f41d371 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/SyntaxConstants.cs @@ -0,0 +1,53 @@ +namespace Mocha.Analyzers; + +/// +/// Provides CLR metadata names for Mocha mediator types used to resolve +/// instances from a compilation. +/// +public static class SyntaxConstants +{ + /// + /// Gets the metadata name for the ICommandHandler<TCommand> interface (void return). + /// + public const string ICommandHandlerVoid = "Mocha.Mediator.ICommandHandler`1"; + + /// + /// Gets the metadata name for the ICommandHandler<TCommand, TResponse> interface. + /// + public const string ICommandHandlerResponse = "Mocha.Mediator.ICommandHandler`2"; + + /// + /// Gets the metadata name for the IQueryHandler<TQuery, TResponse> interface. + /// + public const string IQueryHandler = "Mocha.Mediator.IQueryHandler`2"; + + /// + /// Gets the metadata name for the INotificationHandler<TNotification> interface. + /// + public const string INotificationHandler = "Mocha.Mediator.INotificationHandler`1"; + + /// + /// Gets the metadata name for the ICommand marker interface (void return). + /// + public const string ICommand = "Mocha.Mediator.ICommand"; + + /// + /// Gets the metadata name for the ICommand<TResponse> interface. + /// + public const string ICommandOfT = "Mocha.Mediator.ICommand`1"; + + /// + /// Gets the metadata name for the IQuery<TResponse> interface. + /// + public const string IQueryOfT = "Mocha.Mediator.IQuery`1"; + + /// + /// Gets the metadata name for the INotification marker interface. + /// + public const string INotificationMarker = "Mocha.Mediator.INotification"; + + /// + /// Gets the metadata name for the MediatorModuleAttribute class. + /// + public const string MediatorModuleAttribute = "Mocha.Mediator.MediatorModuleAttribute"; +} diff --git a/src/Mocha/src/Mocha.Analyzers/Utils/CodeWriter.cs b/src/Mocha/src/Mocha.Analyzers/Utils/CodeWriter.cs new file mode 100644 index 00000000000..9fa3ecd9f1a --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Utils/CodeWriter.cs @@ -0,0 +1,152 @@ +using System.Text; + +namespace Mocha.Analyzers.Utils; + +/// +/// Provides a fluent for generating indented C# source code. +/// +/// +/// This writer tracks indentation level and provides convenience methods for writing +/// indented lines, method signatures, control flow blocks, and brace-delimited scopes. +/// Disposing the writer only disposes the underlying writer when it was internally created +/// (i.e., when constructed from a ). +/// +public class CodeWriter : TextWriter +{ + private readonly TextWriter _writer; + private readonly bool _disposeWriter; + + private bool _disposed; + private int _indent; + + /// + /// Initializes a new instance of the class that writes to the + /// specified . + /// + /// The underlying text writer to write generated code to. + public CodeWriter(TextWriter writer) + { + _writer = writer; + _disposeWriter = false; + } + + /// + /// Initializes a new instance of the class that writes to the + /// specified . + /// + /// The string builder to write generated code to. + public CodeWriter(StringBuilder text) + { + _writer = new StringWriter(text); + _disposeWriter = true; + } + + /// + public override Encoding Encoding { get; } = Encoding.UTF8; + + /// + /// Gets a string representing a single indentation level (four spaces). + /// + public static string Indent { get; } = new(' ', 4); + + /// + public override void Write(char value) => _writer.Write(value); + + /// + /// Writes the current indentation whitespace to the output. + /// + public void WriteIndent() + { + if (_indent > 0) + { + Write(GetIndentString()); + } + } + + /// + /// Gets a string representing the current indentation whitespace. + /// + /// A string of spaces matching the current indentation level, or if at the root level. + public string GetIndentString() + { + if (_indent > 0) + { + return new string(' ', _indent * 4); + } + + return string.Empty; + } + + /// + /// Writes an indented line with optional format arguments, followed by a newline. + /// + /// The format string or literal text to write. + /// Optional format arguments. + public void WriteIndentedLine(string format, params object?[] args) + { + WriteIndent(); + + if (args.Length == 0) + { + Write(format); + } + else + { + Write(format, args); + } + + WriteLine(); + } + + /// + /// Writes a single space character. + /// + public void WriteSpace() => Write(' '); + + /// + /// Increases the indentation level by one and returns a disposable that restores it when disposed. + /// + /// An that decreases the indentation level when disposed. + public IDisposable IncreaseIndent() + { + _indent++; + return new Block(DecreaseIndent); + } + + /// + /// Decreases the indentation level by one, unless already at the root level. + /// + public void DecreaseIndent() + { + if (_indent > 0) + { + _indent--; + } + } + + /// + public override void Flush() + { + base.Flush(); + _writer.Flush(); + } + + /// + protected override void Dispose(bool disposing) + { + if (!_disposed && _disposeWriter) + { + if (disposing) + { + _writer.Dispose(); + } + + _disposed = true; + } + } + + private sealed class Block(Action close) : IDisposable + { + public void Dispose() => close(); + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/Utils/CodeWriterExtensions.cs b/src/Mocha/src/Mocha.Analyzers/Utils/CodeWriterExtensions.cs new file mode 100644 index 00000000000..fa29900bff0 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Utils/CodeWriterExtensions.cs @@ -0,0 +1,48 @@ +namespace Mocha.Analyzers.Utils; + +/// +/// Provides extension methods for to emit common generated code patterns. +/// +public static class CodeWriterExtensions +{ + /// + /// Writes a annotation to the output. + /// + /// The code writer to write to. + /// is . + public static void WriteGeneratedAttribute(this CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + +#if DEBUG + writer.WriteIndentedLine( + "[global::System.CodeDom.Compiler.GeneratedCode(\"Mocha.Analyzers\", \"1.0.0\")]"); +#else + var version = typeof(CodeWriter).Assembly.GetName().Version!.ToString(); + writer.WriteIndentedLine( + $"[global::System.CodeDom.Compiler.GeneratedCode(\"Mocha.Analyzers\", \"{version}\")]"); +#endif + } + + /// + /// Writes the standard auto-generated file header including nullable enable and pragma directives. + /// + /// The code writer to write to. + /// is . + public static void WriteFileHeader(this CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteIndentedLine("// "); + writer.WriteLine(); + writer.WriteIndentedLine("#nullable enable"); + writer.WriteIndentedLine("#pragma warning disable"); + writer.WriteLine(); + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/Utils/ImmutableEquatableArray.cs b/src/Mocha/src/Mocha.Analyzers/Utils/ImmutableEquatableArray.cs new file mode 100644 index 00000000000..bf9f239c632 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Utils/ImmutableEquatableArray.cs @@ -0,0 +1,115 @@ +using System.Collections; +using System.Collections.Immutable; + +namespace Mocha.Analyzers.Utils; + +/// +/// Represents an immutable list that implements sequence-based equality comparison. +/// +/// The element type, which must implement . +/// +/// Two instances are considered equal if they contain the same elements in the same order. +/// This is suitable for use in incremental generator pipelines where value equality +/// is required for caching. +/// +public sealed class ImmutableEquatableArray : IEquatable>, IReadOnlyList + where T : IEquatable +{ + /// + /// Gets an empty . + /// + public static ImmutableEquatableArray Empty { get; } = new(ImmutableArray.Create()); + + private readonly ImmutableArray _values; + + /// + /// Gets the element at the specified index. + /// + /// The zero-based index of the element to get. + /// The element at the specified index. + public T this[int index] => _values[index]; + + /// + /// Gets the number of elements in the array. + /// + public int Count => _values.Length; + + /// + /// Initializes a new instance of the class from the specified sequence. + /// + /// The values to include in the array. + public ImmutableEquatableArray(IEnumerable values) => _values = values.ToImmutableArray(); + + /// + /// Initializes a new instance of the class from the specified immutable array. + /// + /// The immutable array to wrap. + public ImmutableEquatableArray(ImmutableArray values) => _values = values; + + /// + /// Creates a new array with the specified value appended. + /// + /// The value to add. + /// A new with the value appended. + public ImmutableEquatableArray Add(T value) => new(_values.Add(value)); + + /// + /// Gets a value indicating whether this array contains no elements. + /// + public bool IsEmpty => _values.IsEmpty; + + /// + /// Creates a new array with the specified values appended. + /// + /// The values to add. + /// A new with the values appended. + public ImmutableEquatableArray AddRange(IEnumerable values) => new(_values.AddRange(values)); + + /// + /// Determines whether this array is equal to another by comparing elements in sequence. + /// + /// The other array to compare with. + /// if both arrays contain the same elements in the same order; otherwise, . + public bool Equals(ImmutableEquatableArray? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return _values.SequenceEqual(other._values); + } + + /// + public override bool Equals(object? obj) + { + if (obj is not ImmutableEquatableArray other) + { + return false; + } + + return Equals(other); + } + + /// + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var item in _values) + { + hashCode.Add(item); + } + + return hashCode.ToHashCode(); + } + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator(); +} diff --git a/src/Mocha/src/Mocha.Analyzers/Utils/ModuleNameHelper.cs b/src/Mocha/src/Mocha.Analyzers/Utils/ModuleNameHelper.cs new file mode 100644 index 00000000000..a18ddd71613 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Utils/ModuleNameHelper.cs @@ -0,0 +1,41 @@ +using System.Text.RegularExpressions; + +namespace Mocha.Analyzers.Utils; + +/// +/// Provides helper methods for deriving a module name from the assembly name. +/// This ensures generated types are unique per assembly, avoiding collisions +/// when multiple assemblies use the Mocha mediator source generator. +/// +internal static class ModuleNameHelper +{ + private static readonly Regex s_invalidCharsRegex = new("[^a-zA-Z0-9]", RegexOptions.Compiled); + + /// + /// Creates a module name from the assembly name by extracting the last segment + /// (after the last dot), sanitizing invalid characters, and appending a suffix. + /// For example, "Demo.Billing" becomes "Billing". + /// + public static string CreateModuleName(string? assemblyName) + { + if (assemblyName is null) + { + return "Assembly"; + } + + var lastSegment = assemblyName.Split('.').Last(); + return SanitizeIdentifier(lastSegment); + } + + internal static string SanitizeIdentifier(string input) + { + var sanitized = s_invalidCharsRegex.Replace(input, "_"); + + if (sanitized.Length == 0 || !char.IsLetter(sanitized[0])) + { + sanitized = "_" + sanitized; + } + + return sanitized; + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/Utils/PooledObjects.cs b/src/Mocha/src/Mocha.Analyzers/Utils/PooledObjects.cs new file mode 100644 index 00000000000..6be0315fa04 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Utils/PooledObjects.cs @@ -0,0 +1,163 @@ +using System.Text; + +namespace Mocha.Analyzers.Utils; + +/// +/// Provides a thread-safe object pool for reusable , +/// , and instances +/// to reduce allocations during source generation. +/// +public static class PooledObjects +{ + private static readonly HashSet?[] s_stringSets = new HashSet[8]; + private static int s_nextStringSetIndex = -1; + + private static readonly StringBuilder?[] s_stringBuilders = new StringBuilder[8]; + private static int s_nextStringBuilderIndex = -1; + + private static readonly Dictionary?[] s_stringDictionaries = new Dictionary[8]; + private static int s_nextStringDictionaryIndex = -1; + + private static readonly object s_lock = new(); + + /// + /// Gets a from the pool, or creates a new one if the pool is empty. + /// + /// A cleared ready for use. + public static StringBuilder GetStringBuilder() + { + lock (s_lock) + { + if (s_nextStringBuilderIndex >= 0) + { + var sb = s_stringBuilders[s_nextStringBuilderIndex]; + s_stringBuilders[s_nextStringBuilderIndex] = null; + s_nextStringBuilderIndex--; + return sb ?? new StringBuilder(); + } + } + + return new StringBuilder(); + } + + /// + /// Gets a of strings from the pool, or creates a new one if the pool is empty. + /// + /// A cleared ready for use. + public static HashSet GetStringSet() + { + lock (s_lock) + { + if (s_nextStringSetIndex >= 0) + { + var set = s_stringSets[s_nextStringSetIndex]; + s_stringSets[s_nextStringSetIndex] = null; + s_nextStringSetIndex--; + return set ?? []; + } + } + + return []; + } + + /// + /// Gets a of strings from the pool, or creates a new one if the pool is empty. + /// + /// A cleared ready for use. + public static Dictionary GetStringDictionary() + { + lock (s_lock) + { + if (s_nextStringDictionaryIndex >= 0) + { + var dict = s_stringDictionaries[s_nextStringDictionaryIndex]; + s_stringDictionaries[s_nextStringDictionaryIndex] = null; + s_nextStringDictionaryIndex--; + return dict ?? new Dictionary(); + } + } + + return new Dictionary(); + } + + /// + /// Returns a to the pool for reuse. + /// + /// The string builder to return. + /// + /// Instances whose capacity exceeds 128 KB are discarded to avoid retaining excessive memory. + /// + public static void Return(StringBuilder stringBuilder) + { + const int maxCapacity = 128 * 1024; + if (stringBuilder.Capacity > maxCapacity) + { + return; + } + + lock (s_lock) + { + stringBuilder.Clear(); + + if (s_nextStringBuilderIndex + 1 < s_stringBuilders.Length) + { + s_nextStringBuilderIndex++; + s_stringBuilders[s_nextStringBuilderIndex] = stringBuilder; + } + } + } + + /// + /// Returns a of strings to the pool for reuse. + /// + /// The hash set to return. + /// + /// Instances containing more than 256 entries are discarded to avoid retaining excessive memory. + /// + public static void Return(HashSet stringSet) + { + const int maxCount = 256; + if (stringSet.Count > maxCount) + { + return; + } + + lock (s_lock) + { + stringSet.Clear(); + + if (s_nextStringSetIndex + 1 < s_stringSets.Length) + { + s_nextStringSetIndex++; + s_stringSets[s_nextStringSetIndex] = stringSet; + } + } + } + + /// + /// Returns a of strings to the pool for reuse. + /// + /// The dictionary to return. + /// + /// Instances containing more than 256 entries are discarded to avoid retaining excessive memory. + /// + public static void Return(Dictionary stringDictionary) + { + const int maxCount = 256; + if (stringDictionary.Count > maxCount) + { + return; + } + + lock (s_lock) + { + stringDictionary.Clear(); + + if (s_nextStringDictionaryIndex + 1 < s_stringDictionaries.Length) + { + s_nextStringDictionaryIndex++; + s_stringDictionaries[s_nextStringDictionaryIndex] = stringDictionary; + } + } + } +} diff --git a/src/Mocha/src/Mocha.Analyzers/Utils/RoslynExtensions.cs b/src/Mocha/src/Mocha.Analyzers/Utils/RoslynExtensions.cs new file mode 100644 index 00000000000..c14f1298319 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Utils/RoslynExtensions.cs @@ -0,0 +1,83 @@ +using Microsoft.CodeAnalysis; + +namespace Mocha.Analyzers.Utils; + +/// +/// Provides extension methods for Roslyn to simplify +/// type hierarchy and interface implementation queries. +/// +public static class RoslynExtensions +{ + /// + /// Determines whether the specified type symbol directly implements the given interface, + /// comparing by original definition to match open generic interfaces. + /// Only checks directly declared interfaces to avoid the expensive AllInterfaces walk. + /// + /// The type symbol to check. + /// The interface type to search for. + /// + /// if directly implements ; + /// otherwise, . + /// + public static bool ImplementsInterface(this INamedTypeSymbol type, INamedTypeSymbol interfaceType) + { + foreach (var @interface in type.Interfaces) + { + if (SymbolEqualityComparer.Default.Equals(@interface.OriginalDefinition, interfaceType.OriginalDefinition)) + { + return true; + } + } + + return false; + } + + /// + /// Finds the closed constructed interface matching the specified open generic interface + /// in the type's directly declared interfaces. + /// Only checks directly declared interfaces to avoid the expensive AllInterfaces walk. + /// + /// The type symbol to search. + /// The open generic interface definition to match against. + /// + /// The closed constructed if found; otherwise, . + /// + public static INamedTypeSymbol? FindImplementedInterface( + this INamedTypeSymbol type, + INamedTypeSymbol openGenericInterface) + { + foreach (var @interface in type.Interfaces) + { + if (SymbolEqualityComparer.Default.Equals(@interface.OriginalDefinition, openGenericInterface)) + { + return @interface; + } + } + + return null; + } + + /// + /// Extracts an equatable from a Roslyn . + /// + /// The Roslyn location to convert. + /// + /// A if the location is in source; otherwise, . + /// + public static LocationInfo? ToLocationInfo(this Location location) + { + if (!location.IsInSource) + { + return null; + } + + var span = location.GetLineSpan(); + + return new LocationInfo( + span.Path, + span.StartLinePosition.Line, + span.StartLinePosition.Character, + span.EndLinePosition.Line, + span.EndLinePosition.Character); + } +} diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkCorePersistanceBuilderExtensions.cs b/src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkCorePersistenceBuilderExtensions.cs similarity index 95% rename from src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkCorePersistanceBuilderExtensions.cs rename to src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkCorePersistenceBuilderExtensions.cs index bde3c280622..ca9f98476ee 100644 --- a/src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkCorePersistanceBuilderExtensions.cs +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkCorePersistenceBuilderExtensions.cs @@ -6,7 +6,7 @@ namespace Mocha.EntityFrameworkCore; /// Provides extension methods on for registering /// additional services into the DbContext internal service provider. /// -public static class EntityFrameworkCorePersistanceBuilderExtensions +public static class EntityFrameworkCorePersistenceBuilderExtensions { /// /// Registers a callback that configures services within the DbContext internal service provider, diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkTransactionFeature.cs b/src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkTransactionFeature.cs new file mode 100644 index 00000000000..6c71be8618c --- /dev/null +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkTransactionFeature.cs @@ -0,0 +1,22 @@ +using Mocha.Mediator; + +namespace Mocha.EntityFrameworkCore; + +/// +/// Feature that carries the resolved Entity Framework transaction configuration +/// through the mediator's feature collection at pipeline compilation time. +/// +internal sealed class EntityFrameworkTransactionFeature +{ + /// + /// Gets the of the + /// to use for transaction management. + /// + public required Type ContextType { get; init; } + + /// + /// Gets an optional delegate that determines whether a transaction should be created + /// for the given mediator context. When , the default policy is used. + /// + public Func? ShouldCreateTransaction { get; init; } +} diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkTransactionMiddleware.cs b/src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkTransactionMiddleware.cs new file mode 100644 index 00000000000..97f86c34ca5 --- /dev/null +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/EntityFrameworkTransactionMiddleware.cs @@ -0,0 +1,68 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Mocha.Features; +using Mocha.Mediator; + +namespace Mocha.EntityFrameworkCore; + +/// +/// Mediator middleware that wraps command handling in a database transaction. +/// Commits on success and rolls back on failure. +/// Queries and notifications are excluded by default but can be opted in via +/// . +/// +internal sealed class EntityFrameworkTransactionMiddleware( + Type dbContextType, + Func? shouldCreateTransaction) +{ + public async ValueTask InvokeAsync(IMediatorContext context, MediatorDelegate next) + { + if (shouldCreateTransaction is not null && !shouldCreateTransaction(context)) + { + await next(context); + return; + } + + var dbContext = (DbContext)context.Services.GetRequiredService(dbContextType); + + await using var transaction = + await dbContext.Database.BeginTransactionAsync(context.CancellationToken); + + try + { + await next(context); + + await dbContext.SaveChangesAsync(context.CancellationToken); + await transaction.CommitAsync(context.CancellationToken); + } + catch + { + await transaction.RollbackAsync(context.CancellationToken); + + throw; + } + } + + public static MediatorMiddlewareConfiguration Create() + => new( + static (factoryCtx, next) => + { + var feature = factoryCtx.Features.GetRequired(); + + // When no custom predicate is set, apply the default policy at compile + // time: commands get transactions, queries and notifications do not. + // This eliminates the middleware entirely from those pipelines (zero cost). + if (feature.ShouldCreateTransaction is null + && (factoryCtx.IsQuery() || factoryCtx.IsNotification())) + { + return next; + } + + var middleware = new EntityFrameworkTransactionMiddleware( + feature.ContextType, + feature.ShouldCreateTransaction); + + return ctx => middleware.InvokeAsync(ctx, next); + }, + "EntityFrameworkTransaction"); +} diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/IEntityFrameworkCoreBuilder.cs b/src/Mocha/src/Mocha.EntityFrameworkCore/IEntityFrameworkCoreBuilder.cs index 7c259b59725..f334f7e7db2 100644 --- a/src/Mocha/src/Mocha.EntityFrameworkCore/IEntityFrameworkCoreBuilder.cs +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/IEntityFrameworkCoreBuilder.cs @@ -2,11 +2,15 @@ namespace Mocha.EntityFrameworkCore; -// TODO this interface is probably too generic (naming) /// -/// Defines the contract for configuring Entity Framework Core persistence features +/// Defines the contract for configuring Entity Framework Core messaging persistence features /// (outbox, sagas, resilience) for a specific DbContext within the message bus host. /// +/// +/// This builder is scoped to messaging persistence concerns and is not a general-purpose +/// EF Core configuration interface. It is obtained via +/// AddEntityFramework<TContext>() on the message bus host builder. +/// public interface IEntityFrameworkCoreBuilder { /// diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/MediatorBuilderEntityFrameworkExtensions.cs b/src/Mocha/src/Mocha.EntityFrameworkCore/MediatorBuilderEntityFrameworkExtensions.cs new file mode 100644 index 00000000000..4d88666ea22 --- /dev/null +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/MediatorBuilderEntityFrameworkExtensions.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore; +using Mocha.Mediator; + +namespace Mocha.EntityFrameworkCore; + +/// +/// Extension methods for configuring Entity Framework Core transaction behavior +/// on the mediator pipeline. +/// +public static class MediatorBuilderEntityFrameworkExtensions +{ + /// + /// Wraps command handler invocations in a database transaction using the specified + /// . Commits on success, rolls back on failure. + /// Streams, queries, and notifications are excluded at compile time. + /// + public static IMediatorHostBuilder UseEntityFrameworkTransactions( + this IMediatorHostBuilder builder, + Action? configure = null) + where TContext : DbContext + { + builder.ConfigureMediator(b => b.UseEntityFrameworkTransactions(configure)); + return builder; + } + + /// + /// Wraps command handler invocations in a database transaction using the specified + /// . Commits on success, rolls back on failure. + /// Streams, queries, and notifications are excluded at compile time. + /// + public static IMediatorBuilder UseEntityFrameworkTransactions( + this IMediatorBuilder builder, + Action? configure = null) + where TContext : DbContext + { + var options = new MediatorEntityFrameworkOptions { ContextType = typeof(TContext) }; + configure?.Invoke(options); + + var feature = new EntityFrameworkTransactionFeature + { + ContextType = options.ContextType, + ShouldCreateTransaction = options.ShouldCreateTransaction + }; + + builder.ConfigureFeature(features => features.Set(feature)); + builder.Use(EntityFrameworkTransactionMiddleware.Create()); + + return builder; + } +} diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/MediatorEntityFrameworkOptions.cs b/src/Mocha/src/Mocha.EntityFrameworkCore/MediatorEntityFrameworkOptions.cs new file mode 100644 index 00000000000..89f161a181e --- /dev/null +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/MediatorEntityFrameworkOptions.cs @@ -0,0 +1,32 @@ +using Mocha.Mediator; + +namespace Mocha.EntityFrameworkCore; + +/// +/// Options for configuring the Entity Framework Core mediator integration. +/// +public sealed class MediatorEntityFrameworkOptions +{ + /// + /// Gets or sets the of the + /// to use for transaction management. + /// + /// + /// Thrown when accessed before calling + /// . + /// + public Type ContextType + { + get => field ?? + throw new InvalidOperationException( + "ContextType has not been configured. Call UseEntityFrameworkTransactions() on the mediator builder."); + set; + } + + /// + /// Gets or sets a delegate that determines whether a transaction should be created + /// for the given mediator context. When , the default policy is used + /// which creates transactions for commands but not for queries. + /// + public Func? ShouldCreateTransaction { get; set; } +} diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/MessageBusHostBuilderExtensions.cs b/src/Mocha/src/Mocha.EntityFrameworkCore/MessageBusHostBuilderExtensions.cs index 967adc15884..dcc18e0a637 100644 --- a/src/Mocha/src/Mocha.EntityFrameworkCore/MessageBusHostBuilderExtensions.cs +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/MessageBusHostBuilderExtensions.cs @@ -23,7 +23,7 @@ public static IMessageBusHostBuilder AddEntityFramework( Action configure) where TContext : DbContext { - var persistanceBuilder = new EntityFrameworkCoreBuilder + var persistenceBuilder = new EntityFrameworkCoreBuilder { Services = builder.Services, HostBuilder = builder, @@ -31,10 +31,10 @@ public static IMessageBusHostBuilder AddEntityFramework( Name = typeof(TContext).FullName ?? typeof(TContext).Name }; - configure(persistanceBuilder); + configure(persistenceBuilder); builder - .Services.AddOptions(persistanceBuilder.Name) + .Services.AddOptions(persistenceBuilder.Name) .Configure((options, serviceProvider) => options.ServiceProvider = serviceProvider); builder.Services.AddSingleton< @@ -110,8 +110,10 @@ public static ConsumerMiddlewareConfiguration Create() .Services.GetRequiredService() .Get() ?.ContextType - // TODO better exception message - ?? throw new InvalidOperationException("No EntityFramework context type found"); + ?? throw new InvalidOperationException( + "No Entity Framework Core DbContext type has been configured. " + + "Call AddEntityFramework() on the message bus host builder " + + "before using UseResilience()."); var middleware = new EntityFrameworkResilienceConsumeMiddleware(contextType); return ctx => middleware.InvokeAsync(ctx, next); @@ -149,8 +151,10 @@ public static ConsumerMiddlewareConfiguration Create() .Services.GetRequiredService() .Get() ?.ContextType - // TODO better exception message - ?? throw new InvalidOperationException("No EntityFramework context type found"); + ?? throw new InvalidOperationException( + "No Entity Framework Core DbContext type has been configured. " + + "Call AddEntityFramework() on the message bus host builder " + + "before using UseTransaction()."); var middleware = new EntityFrameworkTransactionConsumeMiddleware(contextType); return ctx => middleware.InvokeAsync(ctx, next); diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/MessagingDbContextOptions.cs b/src/Mocha/src/Mocha.EntityFrameworkCore/MessagingDbContextOptions.cs index 02d8b49e269..1c90aa97599 100644 --- a/src/Mocha/src/Mocha.EntityFrameworkCore/MessagingDbContextOptions.cs +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/MessagingDbContextOptions.cs @@ -9,12 +9,21 @@ namespace Mocha.EntityFrameworkCore; public class MessagingDbContextOptions { /// - /// Gets or sets the list of delegates that register services into the DbContext internal service provider. + /// Gets the list of delegates that register services into the DbContext internal service provider. /// - public List> ConfigureServices { get; set; } = []; + public List> ConfigureServices { get; init; } = []; /// /// Gets or sets the application-level service provider used to resolve dependencies during DbContext service configuration. /// - public IServiceProvider ServiceProvider { get; set; } = null!; + /// + /// Thrown when accessed before the service provider has been configured. + /// + public IServiceProvider ServiceProvider + { + get => field ?? + throw new InvalidOperationException( + "ServiceProvider has not been configured. Ensure AddEntityFramework() has been called on the message bus host builder."); + set; + } } diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/Mocha.EntityFrameworkCore.csproj b/src/Mocha/src/Mocha.EntityFrameworkCore/Mocha.EntityFrameworkCore.csproj index 2b33fc29a14..6e1118ae5b5 100644 --- a/src/Mocha/src/Mocha.EntityFrameworkCore/Mocha.EntityFrameworkCore.csproj +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/Mocha.EntityFrameworkCore.csproj @@ -5,6 +5,7 @@ + diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/Outbox/OutboxEntityFrameworkCorePersistanceBuilderExtensions.cs b/src/Mocha/src/Mocha.EntityFrameworkCore/Outbox/OutboxEntityFrameworkCorePersistenceBuilderExtensions.cs similarity index 95% rename from src/Mocha/src/Mocha.EntityFrameworkCore/Outbox/OutboxEntityFrameworkCorePersistanceBuilderExtensions.cs rename to src/Mocha/src/Mocha.EntityFrameworkCore/Outbox/OutboxEntityFrameworkCorePersistenceBuilderExtensions.cs index 82e45e8927c..e850a6583b8 100644 --- a/src/Mocha/src/Mocha.EntityFrameworkCore/Outbox/OutboxEntityFrameworkCorePersistanceBuilderExtensions.cs +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/Outbox/OutboxEntityFrameworkCorePersistenceBuilderExtensions.cs @@ -8,7 +8,7 @@ namespace Mocha.EntityFrameworkCore; /// Provides extension methods on for registering /// core outbox interceptors that signal the outbox processor after save and transaction commit. /// -public static class OutboxEntityFrameworkCorePersistanceBuilderExtensions +public static class OutboxEntityFrameworkCorePersistenceBuilderExtensions { /// /// Registers the core outbox infrastructure, including EF Core interceptors that signal the diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore/Sagas/DbContextSagaStore.cs b/src/Mocha/src/Mocha.EntityFrameworkCore/Sagas/DbContextSagaStore.cs index 2617570d7c6..3755cc1f694 100644 --- a/src/Mocha/src/Mocha.EntityFrameworkCore/Sagas/DbContextSagaStore.cs +++ b/src/Mocha/src/Mocha.EntityFrameworkCore/Sagas/DbContextSagaStore.cs @@ -79,7 +79,7 @@ public async Task SaveAsync(Saga saga, T state, CancellationToken cancellatio saga.Name, document, // TODO timeprovider - DateTime.UtcNow, + DateTimeOffset.UtcNow, DateTimeOffset.UtcNow); sagaState.Version = NewVersion(); set.Add(sagaState); @@ -87,7 +87,7 @@ public async Task SaveAsync(Saga saga, T state, CancellationToken cancellatio else { sagaState.State = document; - sagaState.UpdatedAt = DateTime.UtcNow; + sagaState.UpdatedAt = DateTimeOffset.UtcNow; sagaState.Version = NewVersion(); set.Entry(sagaState).Property(x => x.State).IsModified = true; } diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/ICommand.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/ICommand.cs new file mode 100644 index 00000000000..6a60fe04de0 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/ICommand.cs @@ -0,0 +1,12 @@ +namespace Mocha.Mediator; + +/// +/// Defines a marker for a command that does not return a response. +/// +public interface ICommand; + +/// +/// Defines a marker for a command that returns a response of type . +/// +/// The type of the response. +public interface ICommand; diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/ICommandHandler.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/ICommandHandler.cs new file mode 100644 index 00000000000..5498cf25bdb --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/ICommandHandler.cs @@ -0,0 +1,32 @@ +namespace Mocha.Mediator; + +/// +/// Defines a handler for a command that does not return a response. +/// +/// The type of command to handle. +public interface ICommandHandler where TCommand : ICommand +{ + /// + /// Handles the specified command. + /// + /// The command to handle. + /// A token that may be used to cancel the asynchronous operation. + /// A representing the asynchronous operation. + ValueTask HandleAsync(TCommand command, CancellationToken cancellationToken); +} + +/// +/// Defines a handler for a command that returns a response. +/// +/// The type of command to handle. +/// The type of the response. +public interface ICommandHandler where TCommand : ICommand +{ + /// + /// Handles the specified command and returns a response. + /// + /// The command to handle. + /// A token that may be used to cancel the asynchronous operation. + /// A containing the response. + ValueTask HandleAsync(TCommand command, CancellationToken cancellationToken); +} diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/IMediator.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/IMediator.cs new file mode 100644 index 00000000000..c55d4b29b6f --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/IMediator.cs @@ -0,0 +1,10 @@ +namespace Mocha.Mediator; + +/// +/// Defines a combined mediator that can send commands, queries, and publish notifications. +/// +/// +/// This interface inherits from both and , +/// providing a single entry point for all mediator operations. +/// +public interface IMediator : ISender, IPublisher; diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/INotification.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/INotification.cs new file mode 100644 index 00000000000..d581c2e9485 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/INotification.cs @@ -0,0 +1,6 @@ +namespace Mocha.Mediator; + +/// +/// Defines a marker for a notification that can be dispatched to multiple handlers. +/// +public interface INotification; diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/INotificationHandler.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/INotificationHandler.cs new file mode 100644 index 00000000000..3aea3682f7b --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/INotificationHandler.cs @@ -0,0 +1,17 @@ +namespace Mocha.Mediator; + +/// +/// Defines a handler for a notification. Multiple handlers may be registered +/// for the same notification type. +/// +/// The type of notification to handle. +public interface INotificationHandler where TNotification : INotification +{ + /// + /// Handles the specified notification. + /// + /// The notification to handle. + /// A token that may be used to cancel the asynchronous operation. + /// A representing the asynchronous operation. + ValueTask HandleAsync(TNotification notification, CancellationToken cancellationToken); +} diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/INotificationStrategy.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/INotificationStrategy.cs new file mode 100644 index 00000000000..c14decf82ea --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/INotificationStrategy.cs @@ -0,0 +1,24 @@ +namespace Mocha.Mediator; + +/// +/// Defines the strategy for dispatching notifications to multiple handlers. +/// +/// +/// Implementations control how handlers are invoked (e.g., sequentially, in parallel, or with specific error handling). +/// +public interface INotificationStrategy +{ + /// + /// Publishes a notification to all of the provided handlers using this strategy. + /// + /// The type of notification to publish. + /// The collection of handlers to dispatch the notification to. + /// The notification to publish. + /// A token that may be used to cancel the asynchronous operation. + /// A representing the asynchronous operation. + ValueTask PublishAsync( + IReadOnlyList> handlers, + TNotification notification, + CancellationToken cancellationToken) + where TNotification : INotification; +} diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/IPublisher.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/IPublisher.cs new file mode 100644 index 00000000000..42b56923f1c --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/IPublisher.cs @@ -0,0 +1,29 @@ +namespace Mocha.Mediator; + +/// +/// Defines the contract for publishing notifications to all registered handlers. +/// +public interface IPublisher +{ + /// + /// Publishes a notification to all registered handlers. + /// + /// The type of notification. + /// The notification to publish. + /// A token that may be used to cancel the asynchronous operation. + /// A representing the asynchronous operation. + ValueTask PublishAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : INotification; + + /// + /// Publishes a notification by its runtime type to all registered handlers. + /// + /// The notification to publish. + /// A token that may be used to cancel the asynchronous operation. + /// A representing the asynchronous operation. + /// + /// The runtime type of must implement . + /// An exception is thrown if the object does not implement . + /// + ValueTask PublishAsync(object notification, CancellationToken cancellationToken = default); +} diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/IQuery.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/IQuery.cs new file mode 100644 index 00000000000..f942735312c --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/IQuery.cs @@ -0,0 +1,7 @@ +namespace Mocha.Mediator; + +/// +/// Defines a marker for a query that returns a response of type . +/// +/// The type of the query result. +public interface IQuery; diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/IQueryHandler.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/IQueryHandler.cs new file mode 100644 index 00000000000..8ab4c8a80ff --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/IQueryHandler.cs @@ -0,0 +1,17 @@ +namespace Mocha.Mediator; + +/// +/// Defines a handler for a query that returns a response. +/// +/// The type of query to handle. +/// The type of the query result. +public interface IQueryHandler where TQuery : IQuery +{ + /// + /// Handles the specified query and returns a result. + /// + /// The query to handle. + /// A token that may be used to cancel the asynchronous operation. + /// A containing the result. + ValueTask HandleAsync(TQuery query, CancellationToken cancellationToken); +} diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/ISender.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/ISender.cs new file mode 100644 index 00000000000..47ef76aaba0 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/ISender.cs @@ -0,0 +1,48 @@ +namespace Mocha.Mediator; + +/// +/// Defines the contract for sending commands and queries to their respective single handlers. +/// +public interface ISender +{ + /// + /// Sends a command that does not return a response. + /// + /// The command to send. + /// A token that may be used to cancel the asynchronous operation. + /// A representing the asynchronous operation. + ValueTask SendAsync(ICommand command, CancellationToken cancellationToken = default); + + /// + /// Sends a command that returns a response. + /// + /// The type of the response. + /// The command to send. + /// A token that may be used to cancel the asynchronous operation. + /// A containing the response. + ValueTask SendAsync( + ICommand command, + CancellationToken cancellationToken = default); + + /// + /// Sends a query and returns the result. + /// + /// The type of the query result. + /// The query to send. + /// A token that may be used to cancel the asynchronous operation. + /// A containing the result. + ValueTask QueryAsync(IQuery query, CancellationToken cancellationToken = default); + + /// + /// Sends a message by its runtime type, returning the response as an . + /// + /// The message to send. + /// A token that may be used to cancel the asynchronous operation. + /// A containing the response, or for void commands. + /// + /// The runtime type of must implement one of the following marker interfaces: + /// , , or . + /// An exception is thrown if the message does not implement a supported interface. + /// + ValueTask SendAsync(object message, CancellationToken cancellationToken = default); +} diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/MediatorModuleAttribute.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/MediatorModuleAttribute.cs new file mode 100644 index 00000000000..50bc3a2c085 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/MediatorModuleAttribute.cs @@ -0,0 +1,26 @@ +namespace Mocha.Mediator; + +/// +/// Specifies the assembly module name that is being used in combination +/// with the Mocha.Analyzers source generators. +/// +[AttributeUsage(AttributeTargets.Assembly)] +public sealed class MediatorModuleAttribute : Attribute +{ + /// + /// Initializes a new instance of . + /// + /// + /// The module name. + /// + public MediatorModuleAttribute(string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + Name = name; + } + + /// + /// Gets the module name. + /// + public string Name { get; } +} diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/Mocha.Mediator.Abstractions.csproj b/src/Mocha/src/Mocha.Mediator.Abstractions/Mocha.Mediator.Abstractions.csproj new file mode 100644 index 00000000000..351ef6467a7 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/Mocha.Mediator.Abstractions.csproj @@ -0,0 +1,8 @@ + + + Mocha.Mediator.Abstractions + Mocha.Mediator + enable + enable + + diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/Unit.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/Unit.cs new file mode 100644 index 00000000000..4d2b9be78d3 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator.Abstractions/Unit.cs @@ -0,0 +1,51 @@ +namespace Mocha.Mediator; + +/// +/// Represents a void type since is not a valid generic type argument. +/// +/// +/// Use as the response type for commands that do not return a meaningful value. +/// +public readonly struct Unit : IEquatable, IComparable +{ + /// + /// Gets a completed containing the default value. + /// + public static readonly ValueTask ValueTask = new(Value); + + /// + public bool Equals(Unit other) => true; + + /// + public int CompareTo(Unit other) => 0; + + /// + public override int GetHashCode() => 0; + + /// + public override bool Equals(object? obj) => obj is Unit; + + /// + public override string ToString() => "()"; + + /// + /// Determines whether two values are equal. Always returns . + /// + /// The left operand. + /// The right operand. + /// Always . + public static bool operator ==(Unit left, Unit right) => true; + + /// + /// Determines whether two values are not equal. Always returns . + /// + /// The left operand. + /// The right operand. + /// Always . + public static bool operator !=(Unit left, Unit right) => false; + + /// + /// Gets the default and only value of . + /// + public static Unit Value { get; } +} diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorBuilder.cs new file mode 100644 index 00000000000..4579c1a864e --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorBuilder.cs @@ -0,0 +1,62 @@ +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator; + +/// +/// Defines the contract for configuring a Mocha Mediator pipeline. +/// This is the internal builder interface used by deferred configuration actions. +/// +public interface IMediatorBuilder +{ + /// + /// Configures the for this mediator instance. + /// + IMediatorBuilder ConfigureOptions(Action configure); + + /// + /// Appends a middleware to the end of the pipeline. + /// + IMediatorBuilder Use(MediatorMiddlewareConfiguration middleware); + + /// + /// Inserts a middleware after the middleware identified by . + /// If no middleware with that key is found, the middleware is appended to the end. + /// + IMediatorBuilder Append(string after, MediatorMiddlewareConfiguration middleware); + + /// + /// Inserts a middleware at the beginning of the pipeline. + /// + IMediatorBuilder Prepend(MediatorMiddlewareConfiguration middleware); + + /// + /// Inserts a middleware before the middleware identified by . + /// If no middleware with that key is found, the middleware is prepended to the beginning. + /// + IMediatorBuilder Prepend(string before, MediatorMiddlewareConfiguration middleware); + + /// + /// Configures the mediator's feature collection. + /// Features are available to middleware factories via . + /// + IMediatorBuilder ConfigureFeature(Action configure); + + /// + /// Registers additional services into the mediator's internal service collection. + /// + IMediatorBuilder ConfigureServices(Action configure); + + /// + /// Registers additional services into the mediator's internal service collection, + /// with access to the application-level service provider. + /// + IMediatorBuilder ConfigureServices(Action configure); + + /// + /// Registers a pipeline configuration for the specified message type. + /// This method is intended for use by source-generated code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + void RegisterPipeline(MediatorPipelineConfiguration configuration); +} diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorHostBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorHostBuilder.cs new file mode 100644 index 00000000000..def0b2ab98a --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorHostBuilder.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator; + +/// +/// Provides access to the host-level service collection and mediator name for registering services +/// during host configuration. +/// +public interface IMediatorHostBuilder +{ + /// + /// Gets the logical name of the mediator instance being configured. + /// An empty string represents the default (unnamed) mediator. + /// + string Name { get; } + + /// + /// Gets the host-level service collection for registering dependencies required by the mediator. + /// + IServiceCollection Services { get; } + + /// + /// Gets the mediator options being configured. + /// + MediatorOptions Options { get; } +} diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs new file mode 100644 index 00000000000..538d019d91e --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs @@ -0,0 +1,213 @@ +using System.Collections.Frozen; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Mocha.Features; + +namespace Mocha.Mediator; + +/// +/// Represents the default implementation of . +/// Accumulates configuration and builds the +/// with its own internal service provider. +/// +public sealed class MediatorBuilder : IMediatorBuilder +{ + private readonly List _middlewares = + [ + MediatorDiagnosticMiddleware.Create() + ]; + + private readonly List>> _pipelineModifiers = []; + private readonly List _pipelines = []; + private readonly List> _configureServices = []; + private readonly List> _configureFeatures = []; + private readonly MediatorOptions _options = new(); + + /// + public IMediatorBuilder ConfigureOptions(Action configure) + { + ArgumentNullException.ThrowIfNull(configure); + + configure(_options); + return this; + } + + /// + public IMediatorBuilder Use(MediatorMiddlewareConfiguration middleware) + { + ArgumentNullException.ThrowIfNull(middleware); + + _middlewares.Add(middleware); + return this; + } + + /// + public IMediatorBuilder Append(string after, MediatorMiddlewareConfiguration middleware) + { + ArgumentNullException.ThrowIfNull(after); + ArgumentNullException.ThrowIfNull(middleware); + + _pipelineModifiers.Add(pipeline => + { + var index = pipeline.FindIndex(m => m.Key == after); + + if (index >= 0) + { + pipeline.Insert(index + 1, middleware); + } + else + { + pipeline.Add(middleware); + } + }); + return this; + } + + /// + public IMediatorBuilder Prepend(MediatorMiddlewareConfiguration middleware) + { + ArgumentNullException.ThrowIfNull(middleware); + + _pipelineModifiers.Add(pipeline => pipeline.Insert(0, middleware)); + return this; + } + + /// + public IMediatorBuilder Prepend(string before, MediatorMiddlewareConfiguration middleware) + { + ArgumentNullException.ThrowIfNull(before); + ArgumentNullException.ThrowIfNull(middleware); + + _pipelineModifiers.Add(pipeline => + { + var index = pipeline.FindIndex(m => m.Key == before); + if (index >= 0) + { + pipeline.Insert(index, middleware); + } + else + { + pipeline.Insert(0, middleware); + } + }); + return this; + } + + /// + public IMediatorBuilder ConfigureFeature(Action configure) + { + _configureFeatures.Add(configure); + return this; + } + + /// + public IMediatorBuilder ConfigureServices(Action configure) + { + ArgumentNullException.ThrowIfNull(configure); + + _configureServices.Add((_, services) => configure(services)); + return this; + } + + /// + public IMediatorBuilder ConfigureServices(Action configure) + { + ArgumentNullException.ThrowIfNull(configure); + + _configureServices.Add(configure); + return this; + } + + /// + public void RegisterPipeline(MediatorPipelineConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + + _pipelines.Add(configuration); + } + + /// + /// Builds the by creating an internal service provider, + /// applying deferred service configurations, and compiling all registered pipelines. + /// + /// + /// The application-level service provider used to resolve shared services. + /// + public MediatorRuntime Build(IServiceProvider applicationServices) + { + // Create the mediator's own internal service collection. + var internalServices = new ServiceCollection(); + + // Apply deferred service configurations (e.g. diagnostic listeners). + foreach (var configure in _configureServices) + { + configure(applicationServices, internalServices); + } + + // Resolve the aggregate diagnostic events from registered listeners, + // falling back to a no-op when no listeners are registered. + AddDiagnosticEvents(internalServices); + + var internalProvider = internalServices.BuildServiceProvider(); + + // Build the feature collection. + var features = new FeatureCollection(); + foreach (var configure in _configureFeatures) + { + configure(features); + } + + if (!features.TryGet(out _)) + { + var strategy = applicationServices.GetService() + ?? new ForeachAwaitPublisher(); + features.Set(new NotificationStrategyFeature(strategy)); + } + + // Compile pipelines using the internal provider for middleware factory context. + var factoryCtx = new MediatorMiddlewareFactoryContext { Services = internalProvider, Features = features }; + + var middlewareConfigs = _middlewares.Count > 0 + ? new IReadOnlyList[] { _middlewares } + : []; + + var modifiers = _pipelineModifiers.Count > 0 + ? new IReadOnlyList>>[] { _pipelineModifiers } + : []; + + var pipelines = new Dictionary(_pipelines.Count); + + foreach (var config in _pipelines) + { + factoryCtx.MessageType = config.MessageType; + factoryCtx.ResponseType = config.ResponseType; + + pipelines[config.MessageType] = + MediatorMiddlewareCompiler.Compile(factoryCtx, config.Terminal, middlewareConfigs, modifiers); + } + + var pools = applicationServices.GetRequiredService(); + + return new MediatorRuntime(pipelines.ToFrozenDictionary(), pools, features); + } + + /// + /// Resolves diagnostic event listeners from internal services and registers + /// the aggregate implementation. + /// Falls back to a no-op when no listeners are registered. + /// + private static void AddDiagnosticEvents(IServiceCollection services) + { + services.TryAddSingleton(sp => + { + var listeners = sp.GetServices().ToArray(); + + return listeners.Length switch + { + 0 => NoopMediatorDiagnosticEvents.Instance, + 1 => listeners[0], + _ => new AggregateMediatorDiagnosticEvents(listeners) + }; + }); + } +} diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilderInstrumentationExtensions.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilderInstrumentationExtensions.cs new file mode 100644 index 00000000000..153e63820c0 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilderInstrumentationExtensions.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Mocha.Mediator; + +/// +/// Provides extension methods for adding diagnostics and instrumentation +/// to the mediator pipeline via . +/// +public static class MediatorBuilderInstrumentationExtensions +{ + /// + /// Adds the default OpenTelemetry-compatible diagnostic event listener to the mediator pipeline. + /// + public static IMediatorBuilder AddInstrumentation(this IMediatorBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.AddDiagnosticEventListener(); + + return builder; + } + + /// + /// Registers a custom implementation. + /// + public static IMediatorBuilder AddDiagnosticEventListener(this IMediatorBuilder builder) + where T : class, IMediatorDiagnosticEventListener + { + ArgumentNullException.ThrowIfNull(builder); + + builder.ConfigureServices(static services => + { + services.TryAddSingleton(); + services.AddSingleton(static sp => sp.GetRequiredService()); + }); + + return builder; + } + + /// + /// Registers a diagnostic event listener instance. + /// + public static IMediatorBuilder AddDiagnosticEventListener( + this IMediatorBuilder builder, + IMediatorDiagnosticEventListener listener) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(listener); + + builder.ConfigureServices(services => services.AddSingleton(listener)); + + return builder; + } +} diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilder.cs new file mode 100644 index 00000000000..05b487d6917 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilder.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator; + +internal sealed class MediatorHostBuilder(IServiceCollection services, string name) : IMediatorHostBuilder +{ + public string Name { get; } = name; + + public IServiceCollection Services { get; } = services; + + public MediatorOptions Options { get; } = new(); +} diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderExtensions.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderExtensions.cs new file mode 100644 index 00000000000..10ec0e77e13 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderExtensions.cs @@ -0,0 +1,173 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Mocha.Mediator; + +/// +/// Provides extension methods for configuring the mediator through the host builder. +/// +public static class MediatorHostBuilderExtensions +{ + /// + /// Appends a middleware to the end of the pipeline. + /// + public static IMediatorHostBuilder Use( + this IMediatorHostBuilder builder, + MediatorMiddlewareConfiguration middleware) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(middleware); + + builder.ConfigureMediator(b => b.Use(middleware)); + return builder; + } + + /// + /// Inserts a middleware after the middleware identified by . + /// + public static IMediatorHostBuilder Append( + this IMediatorHostBuilder builder, + string after, + MediatorMiddlewareConfiguration middleware) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(after); + ArgumentNullException.ThrowIfNull(middleware); + + builder.ConfigureMediator(b => b.Append(after, middleware)); + return builder; + } + + /// + /// Inserts a middleware at the beginning of the pipeline. + /// + public static IMediatorHostBuilder Prepend( + this IMediatorHostBuilder builder, + MediatorMiddlewareConfiguration middleware) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(middleware); + + builder.ConfigureMediator(b => b.Prepend(middleware)); + return builder; + } + + /// + /// Inserts a middleware before the middleware identified by . + /// + public static IMediatorHostBuilder Prepend( + this IMediatorHostBuilder builder, + string before, + MediatorMiddlewareConfiguration middleware) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(before); + ArgumentNullException.ThrowIfNull(middleware); + + builder.ConfigureMediator(b => b.Prepend(before, middleware)); + return builder; + } + + /// + /// Configures the for this mediator instance. + /// + public static IMediatorHostBuilder ConfigureOptions( + this IMediatorHostBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + builder.ConfigureMediator(b => b.ConfigureOptions(configure)); + return builder; + } + + /// + /// Adds the default OpenTelemetry-compatible diagnostic event listener to the mediator pipeline. + /// + public static IMediatorHostBuilder AddInstrumentation(this IMediatorHostBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.ConfigureMediator(static b => b.AddInstrumentation()); + return builder; + } + + /// + /// Registers a custom implementation + /// into the mediator's internal services. + /// + public static IMediatorHostBuilder AddDiagnosticEventListener(this IMediatorHostBuilder builder) + where T : class, IMediatorDiagnosticEventListener + { + ArgumentNullException.ThrowIfNull(builder); + + builder.ConfigureMediator(static b => b.AddDiagnosticEventListener()); + return builder; + } + + /// + /// Registers a diagnostic event listener instance into the mediator's internal services. + /// + public static IMediatorHostBuilder AddDiagnosticEventListener( + this IMediatorHostBuilder builder, + IMediatorDiagnosticEventListener listener) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(listener); + + builder.ConfigureMediator(b => b.AddDiagnosticEventListener(listener)); + + return builder; + } + + /// + /// Registers additional services into the mediator's internal service collection. + /// + public static IMediatorHostBuilder ConfigureMediatorServices( + this IMediatorHostBuilder builder, + Action configureServices) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureServices); + + builder.ConfigureMediator(b => b.ConfigureServices(configureServices)); + return builder; + } + + /// + /// Registers additional services into the mediator's internal service collection, + /// with access to the application-level service provider. + /// + public static IMediatorHostBuilder ConfigureMediatorServices( + this IMediatorHostBuilder builder, + Action configureServices) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureServices); + + builder.ConfigureMediator(b => b.ConfigureServices(configureServices)); + return builder; + } + + /// + /// Applies a configuration action directly to the underlying mediator builder. + /// + public static void ConfigureMediator( + this IMediatorHostBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + builder.Configure(options => options.ConfigureMediator.Add(configure)); + } + + private static void Configure( + this IMediatorHostBuilder builder, + Action configure) + where TOptions : class + { + builder.Services.Configure(builder.Name, configure); + } +} diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorServiceCollectionExtensions.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorServiceCollectionExtensions.cs new file mode 100644 index 00000000000..cb68b9d58a2 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorServiceCollectionExtensions.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Mocha.Mediator; + +/// +/// Provides extension methods for registering the Mocha Mediator on . +/// +public static class MediatorServiceCollectionExtensions +{ + /// + /// Adds the default (unnamed) Mocha Mediator infrastructure to the service collection. + /// + public static IMediatorHostBuilder AddMediator(this IServiceCollection services) + => AddMediator(services, string.Empty); + + /// + /// Adds a named Mocha Mediator infrastructure to the service collection. + /// + public static IMediatorHostBuilder AddMediator(this IServiceCollection services, string name) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(name); + + services.AddOptions(); + services.AddMediatorPoolingCore(); + + if (name.Length == 0) + { + services.TryAddSingleton(sp => BuildRuntime(sp, name)); + + services.TryAddScoped(); + services.TryAddScoped(sp => sp.GetRequiredService()); + services.TryAddScoped(sp => sp.GetRequiredService()); + services.TryAddScoped(sp => sp.GetRequiredService()); + } + else + { + services.TryAddKeyedSingleton(name, (sp, _) => BuildRuntime(sp, name)); + + services.TryAddKeyedScoped(name, + static (sp, key) => new Mediator( + sp.GetRequiredKeyedService(key), + sp)); + services.TryAddKeyedScoped(name, + static (sp, key) => sp.GetRequiredKeyedService(key)); + services.TryAddKeyedScoped(name, + static (sp, key) => sp.GetRequiredKeyedService(key)); + services.TryAddKeyedScoped(name, + static (sp, key) => sp.GetRequiredKeyedService(key)); + } + + return new MediatorHostBuilder(services, name); + } + + private static MediatorRuntime BuildRuntime(IServiceProvider sp, string name) + { + var setup = sp.GetRequiredService>().Get(name); + + var builder = new MediatorBuilder(); + + foreach (var configure in setup.ConfigureMediator) + { + configure(builder); + } + + return builder.Build(sp); + } +} diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorSetup.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorSetup.cs new file mode 100644 index 00000000000..918127966fb --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorSetup.cs @@ -0,0 +1,6 @@ +namespace Mocha.Mediator; + +internal sealed class MediatorSetup +{ + public List> ConfigureMediator { get; } = []; +} diff --git a/src/Mocha/src/Mocha.Mediator/IMediatorContext.cs b/src/Mocha/src/Mocha.Mediator/IMediatorContext.cs new file mode 100644 index 00000000000..1dce919ffb7 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/IMediatorContext.cs @@ -0,0 +1,49 @@ +namespace Mocha.Mediator; + +/// +/// Represents the context that flows through the mediator middleware pipeline. +/// +public interface IMediatorContext +{ + /// + /// Gets the scoped service provider for resolving handlers and services. + /// + IServiceProvider Services { get; } + + /// + /// Gets the message being dispatched (command, query, or notification). + /// + object Message { get; } + + /// + /// Gets the runtime type of the message. + /// + Type MessageType { get; } + + /// + /// Gets the expected response type ( for void commands and notifications). + /// + Type ResponseType { get; } + + /// + /// Gets the cancellation token for the operation. + /// + CancellationToken CancellationToken { get; } + + /// + /// Gets the mediator runtime that owns this context. + /// + IMediatorRuntime Runtime { get; } + + /// + /// Gets the per-request feature collection. + /// Middleware can use this to share state within a single pipeline invocation. + /// + IFeatureCollection Features { get; } + + /// + /// Gets or sets the result of the pipeline execution. + /// Set by the terminal handler delegate. + /// + object? Result { get; set; } +} diff --git a/src/Mocha/src/Mocha.Mediator/IMediatorPools.cs b/src/Mocha/src/Mocha.Mediator/IMediatorPools.cs new file mode 100644 index 00000000000..eb8f0d01c0a --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/IMediatorPools.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.ObjectPool; + +namespace Mocha.Mediator; + +/// +/// Provides access to object pools used by the mediator infrastructure, +/// enabling reuse of context objects to reduce allocations. +/// +public interface IMediatorPools +{ + /// + /// Gets the object pool for instances. + /// + ObjectPool MediatorContext { get; } +} diff --git a/src/Mocha/src/Mocha.Mediator/IMediatorRuntime.cs b/src/Mocha/src/Mocha.Mediator/IMediatorRuntime.cs new file mode 100644 index 00000000000..dca90aa63c8 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/IMediatorRuntime.cs @@ -0,0 +1,13 @@ +namespace Mocha.Mediator; + +/// +/// Represents the fully initialized mediator runtime, providing access to +/// the feature collection and compiled pipelines. +/// +public interface IMediatorRuntime +{ + /// + /// Gets the read-only feature collection for this mediator runtime. + /// + IFeatureCollection Features { get; } +} diff --git a/src/Mocha/src/Mocha.Mediator/Instrumentation/ActivityMediatorDiagnosticListener.cs b/src/Mocha/src/Mocha.Mediator/Instrumentation/ActivityMediatorDiagnosticListener.cs new file mode 100644 index 00000000000..f5b661868b6 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Instrumentation/ActivityMediatorDiagnosticListener.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; +using static Mocha.Mediator.MochaMediatorActivitySource; +using static Mocha.Mediator.SemanticConventions; + +namespace Mocha.Mediator; + +internal sealed class ActivityMediatorDiagnosticListener : MediatorDiagnosticEventListener +{ + public override IDisposable Execute(Type messageType, Type responseType, object message) + { + var messageTypeName = messageType.Name; + var operationType = typeof(INotification).IsAssignableFrom(messageType) + ? OperationTypePublish + : OperationTypeSend; + + var activity = Source.StartActivity($"{messageTypeName} {operationType}"); + + if (activity is null) + { + return EmptyScope; + } + + activity.SetTag(MessagingSystem, MessagingSystemValue); + activity.SetTag(MessagingOperationType, operationType); + activity.SetTag(MessagingMessageType, messageTypeName); + activity.SetStatus(ActivityStatusCode.Ok); + + return activity; + } + + public override void ExecutionError(Type messageType, Type responseType, object message, Exception exception) + { + if (Activity.Current is not { } activity) + { + return; + } + + var tags = new ActivityTagsCollection + { + { ExceptionType, exception.GetType().FullName }, + { ExceptionMessage, exception.Message } + }; + + activity.AddEvent(new ActivityEvent(ExceptionEventName, default, tags)); + activity.SetStatus(ActivityStatusCode.Error, exception.Message); + } +} diff --git a/src/Mocha/src/Mocha.Mediator/Instrumentation/AggregateMediatorDiagnosticEvents.cs b/src/Mocha/src/Mocha.Mediator/Instrumentation/AggregateMediatorDiagnosticEvents.cs new file mode 100644 index 00000000000..de5b87d532c --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Instrumentation/AggregateMediatorDiagnosticEvents.cs @@ -0,0 +1,43 @@ +namespace Mocha.Mediator; + +internal sealed class AggregateMediatorDiagnosticEvents(IMediatorDiagnosticEventListener[] listeners) + : IMediatorDiagnosticEvents +{ + public IDisposable Execute(Type messageType, Type responseType, object message) + { + var scopes = new IDisposable[listeners.Length]; + + for (var i = 0; i < listeners.Length; i++) + { + scopes[i] = listeners[i].Execute(messageType, responseType, message); + } + + return new AggregateActivityScope(scopes); + } + + public void ExecutionError(Type messageType, Type responseType, object message, Exception exception) + { + for (var i = 0; i < listeners.Length; i++) + { + listeners[i].ExecutionError(messageType, responseType, message, exception); + } + } + + private sealed class AggregateActivityScope(IDisposable[] scopes) : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (!_disposed) + { + for (var i = 0; i < scopes.Length; i++) + { + scopes[i].Dispose(); + } + + _disposed = true; + } + } + } +} diff --git a/src/Mocha/src/Mocha.Mediator/Instrumentation/IMediatorDiagnosticEventListener.cs b/src/Mocha/src/Mocha.Mediator/Instrumentation/IMediatorDiagnosticEventListener.cs new file mode 100644 index 00000000000..92758a167c0 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Instrumentation/IMediatorDiagnosticEventListener.cs @@ -0,0 +1,9 @@ +namespace Mocha.Mediator; + +/// +/// Register an implementation of this interface in the DI container to +/// listen to diagnostic events. Multiple implementations can be registered +/// and they will all be called in registration order. +/// +/// +public interface IMediatorDiagnosticEventListener : IMediatorDiagnosticEvents; diff --git a/src/Mocha/src/Mocha.Mediator/Instrumentation/IMediatorDiagnosticEvents.cs b/src/Mocha/src/Mocha.Mediator/Instrumentation/IMediatorDiagnosticEvents.cs new file mode 100644 index 00000000000..ad80ec000e1 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Instrumentation/IMediatorDiagnosticEvents.cs @@ -0,0 +1,19 @@ +namespace Mocha.Mediator; + +/// +/// Provides diagnostic events that can be triggered by the mediator pipeline. +/// These events allow monitoring and instrumentation of message handling. +/// +/// +public interface IMediatorDiagnosticEvents +{ + /// + /// Called when a message begins executing. Returns a disposable scope. + /// + IDisposable Execute(Type messageType, Type responseType, object message); + + /// + /// Called when an exception occurs during execution. + /// + void ExecutionError(Type messageType, Type responseType, object message, Exception exception); +} diff --git a/src/Mocha/src/Mocha.Mediator/Instrumentation/MediatorDiagnosticEventListener.cs b/src/Mocha/src/Mocha.Mediator/Instrumentation/MediatorDiagnosticEventListener.cs new file mode 100644 index 00000000000..0ddd2a44eab --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Instrumentation/MediatorDiagnosticEventListener.cs @@ -0,0 +1,27 @@ +namespace Mocha.Mediator; + +/// +/// A base class for diagnostic event listeners with default no-op implementations. +/// Extend this class and override the methods you need. +/// +/// +public class MediatorDiagnosticEventListener : IMediatorDiagnosticEventListener +{ + protected MediatorDiagnosticEventListener() { } + + /// + /// Gets a shared no-op scope. + /// Calling on this instance is safe and performs no operation. + /// Use this as a default return value from when no diagnostic activity is needed. + /// + protected internal static IDisposable EmptyScope { get; } = new EmptyActivityScope(); + + public virtual IDisposable Execute(Type messageType, Type responseType, object message) => EmptyScope; + + public virtual void ExecutionError(Type messageType, Type responseType, object message, Exception exception) { } + + private sealed class EmptyActivityScope : IDisposable + { + public void Dispose() { } + } +} diff --git a/src/Mocha/src/Mocha.Mediator/Instrumentation/MediatorDiagnosticMiddleware.cs b/src/Mocha/src/Mocha.Mediator/Instrumentation/MediatorDiagnosticMiddleware.cs new file mode 100644 index 00000000000..0d8c9cb667a --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Instrumentation/MediatorDiagnosticMiddleware.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator; + +/// +/// Mediator middleware that delegates to +/// to instrument message handling with diagnostic events. +/// +internal sealed class MediatorDiagnosticMiddleware(IMediatorDiagnosticEvents events) +{ + public async ValueTask InvokeAsync(IMediatorContext context, MediatorDelegate next) + { + using var scope = events.Execute(context.MessageType, context.ResponseType, context.Message); + + try + { + await next(context).ConfigureAwait(false); + } + catch (Exception ex) + { + events.ExecutionError(context.MessageType, context.ResponseType, context.Message, ex); + throw; + } + } + + public static MediatorMiddlewareConfiguration Create() + => new( + static (context, next) => + { + var diagnosticEvents = context.Services.GetRequiredService(); + var middleware = new MediatorDiagnosticMiddleware(diagnosticEvents); + return ctx => middleware.InvokeAsync(ctx, next); + }, + "Instrumentation"); +} diff --git a/src/Mocha/src/Mocha.Mediator/Instrumentation/MochaMediatorActivitySource.cs b/src/Mocha/src/Mocha.Mediator/Instrumentation/MochaMediatorActivitySource.cs new file mode 100644 index 00000000000..164c37c9f04 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Instrumentation/MochaMediatorActivitySource.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; + +namespace Mocha.Mediator; + +internal static class MochaMediatorActivitySource +{ + public static ActivitySource Source { get; } = new(GetName(), GetVersion()); + + public static string GetName() + => typeof(ActivityMediatorDiagnosticListener).Assembly.GetName().Name!; + + private static string GetVersion() + => typeof(ActivityMediatorDiagnosticListener).Assembly.GetName().Version!.ToString(); +} diff --git a/src/Mocha/src/Mocha.Mediator/Instrumentation/NoopMediatorDiagnosticEvents.cs b/src/Mocha/src/Mocha.Mediator/Instrumentation/NoopMediatorDiagnosticEvents.cs new file mode 100644 index 00000000000..29892fbb1ff --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Instrumentation/NoopMediatorDiagnosticEvents.cs @@ -0,0 +1,11 @@ +namespace Mocha.Mediator; + +internal sealed class NoopMediatorDiagnosticEvents + : MediatorDiagnosticEventListener +{ + private NoopMediatorDiagnosticEvents() + { + } + + public static NoopMediatorDiagnosticEvents Instance { get; } = new(); +} diff --git a/src/Mocha/src/Mocha.Mediator/Instrumentation/SemanticConventions.cs b/src/Mocha/src/Mocha.Mediator/Instrumentation/SemanticConventions.cs new file mode 100644 index 00000000000..d0a71f2cad1 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Instrumentation/SemanticConventions.cs @@ -0,0 +1,27 @@ +namespace Mocha.Mediator; + +/// +/// OpenTelemetry semantic convention attribute names for messaging systems. +/// +/// +/// See https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/ +/// +internal static class SemanticConventions +{ + // Messaging attributes + public const string MessagingSystem = "messaging.system"; + public const string MessagingOperationType = "messaging.operation.type"; + public const string MessagingMessageType = "messaging.message.type"; + + // Exception attributes + public const string ExceptionEventName = "exception"; + public const string ExceptionType = "exception.type"; + public const string ExceptionMessage = "exception.message"; + + // Messaging system identifier + public const string MessagingSystemValue = "mocha.mediator"; + + // Operation type values + public const string OperationTypeSend = "send"; + public const string OperationTypePublish = "publish"; +} diff --git a/src/Mocha/src/Mocha.Mediator/Mediator.cs b/src/Mocha/src/Mocha.Mediator/Mediator.cs new file mode 100644 index 00000000000..4296ac61539 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Mediator.cs @@ -0,0 +1,206 @@ +using System.Runtime.CompilerServices; + +namespace Mocha.Mediator; + +/// +/// Framework-provided mediator implementation that dispatches commands, queries, +/// and notifications through pre-compiled middleware pipelines using O(1) type lookups. +/// +public sealed class Mediator(MediatorRuntime runtime, IServiceProvider serviceProvider) : IMediator +{ + /// + public ValueTask SendAsync(ICommand command, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(command); + + var messageType = command.GetType(); + var pipeline = runtime.GetPipeline(messageType); + var context = runtime.RentContext(); + + context.Initialize(runtime, serviceProvider, command, messageType, cancellationToken); + + var task = pipeline(context); + + if (task.IsCompletedSuccessfully) + { + runtime.ReturnContext(context); + return default; + } + + return AwaitAndReturn(task, context); + } + + /// + public ValueTask SendAsync( + ICommand command, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(command); + + var messageType = command.GetType(); + var pipeline = runtime.GetPipeline(messageType); + var context = runtime.RentContext(); + + context.Initialize(runtime, serviceProvider, command, messageType, cancellationToken, typeof(TResponse)); + + var task = pipeline(context); + + if (task.IsCompletedSuccessfully) + { + var result = (TResponse)context.Result!; + + runtime.ReturnContext(context); + + return new ValueTask(result); + } + + return AwaitAndReturn(task, context); + } + + /// + public ValueTask QueryAsync( + IQuery query, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + var messageType = query.GetType(); + var pipeline = runtime.GetPipeline(messageType); + var context = runtime.RentContext(); + + context.Initialize(runtime, serviceProvider, query, messageType, cancellationToken, typeof(TResponse)); + + var task = pipeline(context); + + if (task.IsCompletedSuccessfully) + { + var result = (TResponse)context.Result!; + + runtime.ReturnContext(context); + + return new ValueTask(result); + } + + return AwaitAndReturn(task, context); + } + + /// + ValueTask ISender.SendAsync(object message, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(message); + + var messageType = message.GetType(); + + var pipeline = runtime.GetPipeline(messageType); + + var context = runtime.RentContext(); + + context.Initialize(runtime, serviceProvider, message, messageType, cancellationToken, typeof(object)); + + var task = pipeline(context); + if (task.IsCompletedSuccessfully) + { + var result = context.Result; + + runtime.ReturnContext(context); + + return new ValueTask(result); + } + + return AwaitAndReturnObject(task, context); + } + + /// + public ValueTask PublishAsync( + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(notification); + + var messageType = notification.GetType(); + var pipeline = runtime.GetPipeline(messageType); + var context = runtime.RentContext(); + + context.Initialize(runtime, serviceProvider, notification, messageType, cancellationToken); + + var task = pipeline(context); + + if (task.IsCompletedSuccessfully) + { + runtime.ReturnContext(context); + + return default; + } + + return AwaitAndReturn(task, context); + } + + /// + ValueTask IPublisher.PublishAsync(object notification, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(notification); + + var messageType = notification.GetType(); + var pipeline = runtime.GetPipeline(messageType); + var context = runtime.RentContext(); + + context.Initialize(runtime, serviceProvider, notification, messageType, cancellationToken); + + var task = pipeline(context); + + if (task.IsCompletedSuccessfully) + { + runtime.ReturnContext(context); + return default; + } + + return AwaitAndReturn(task, context); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private async ValueTask AwaitAndReturn(ValueTask task, MediatorContext context) + { + try + { + await task.ConfigureAwait(false); + } + finally + { + runtime.ReturnContext(context); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + private async ValueTask AwaitAndReturn(ValueTask task, MediatorContext context) + { + try + { + await task.ConfigureAwait(false); + + return (TResponse)context.Result!; + } + finally + { + runtime.ReturnContext(context); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + private async ValueTask AwaitAndReturnObject(ValueTask task, MediatorContext context) + { + try + { + await task.ConfigureAwait(false); + + return context.Result; + } + finally + { + runtime.ReturnContext(context); + } + } +} diff --git a/src/Mocha/src/Mocha.Mediator/MediatorContext.cs b/src/Mocha/src/Mocha.Mediator/MediatorContext.cs new file mode 100644 index 00000000000..10fd2ccf1e6 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorContext.cs @@ -0,0 +1,85 @@ +using Mocha.Features; + +namespace Mocha.Mediator; + +/// +/// A poolable, mutable context that flows through the mediator middleware pipeline. +/// +public sealed class MediatorContext : IMediatorContext +{ + private readonly PooledFeatureCollection _features; + + public MediatorContext() + { + _features = new PooledFeatureCollection(this); + } + + /// + public IServiceProvider Services { get; set; } = null!; + + /// + public object Message { get; set; } = null!; + + /// + public Type MessageType { get; set; } = null!; + + /// + public Type ResponseType { get; set; } = null!; + + /// + public CancellationToken CancellationToken { get; set; } + + /// + public IFeatureCollection Features => _features; + + /// + public object? Result { get; set; } + + /// + IMediatorRuntime IMediatorContext.Runtime => Runtime; + + /// + /// Gets or sets the concrete mediator runtime that owns this context. + /// + public MediatorRuntime Runtime { get; set; } = null!; + + /// + /// Initializes the context for a new dispatch, setting up runtime feature defaults + /// and the common per-dispatch properties. + /// + internal void Initialize( + MediatorRuntime runtime, + IServiceProvider serviceProvider, + object message, + Type messageType, + CancellationToken cancellationToken, + Type? responseType = null) + { + Runtime = runtime; + Services = serviceProvider; + Message = message; + MessageType = messageType; + CancellationToken = cancellationToken; + if (responseType is not null) + { + ResponseType = responseType; + } + + _features.Initialize(runtime.Features); + } + + /// + /// Resets all fields for return to the pool. + /// + internal void Reset() + { + Services = null!; + Message = null!; + MessageType = null!; + ResponseType = null!; + CancellationToken = CancellationToken.None; + Result = null; + Runtime = null!; + _features.Reset(); + } +} diff --git a/src/Mocha/src/Mocha.Mediator/MediatorContextPool.cs b/src/Mocha/src/Mocha.Mediator/MediatorContextPool.cs new file mode 100644 index 00000000000..2bd978c515c --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorContextPool.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.ObjectPool; + +namespace Mocha.Mediator; + +/// +/// An object pool for instances, reducing allocation +/// overhead in high-throughput dispatch pipelines. +/// +/// The maximum number of contexts to retain in the pool. +public sealed class MediatorContextPool(int maximumRetained = 256) + : DefaultObjectPool(new MediatorContextPoolPolicy(), maximumRetained) +{ + private sealed class MediatorContextPoolPolicy : IPooledObjectPolicy + { + public MediatorContext Create() + { + return new MediatorContext(); + } + + public bool Return(MediatorContext obj) + { + obj.Reset(); + return true; + } + } +} diff --git a/src/Mocha/src/Mocha.Mediator/MediatorDelegate.cs b/src/Mocha/src/Mocha.Mediator/MediatorDelegate.cs new file mode 100644 index 00000000000..643cc50b484 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorDelegate.cs @@ -0,0 +1,6 @@ +namespace Mocha.Mediator; + +/// +/// Represents an asynchronous operation in the mediator pipeline. +/// +public delegate ValueTask MediatorDelegate(IMediatorContext context); diff --git a/src/Mocha/src/Mocha.Mediator/MediatorMiddleware.cs b/src/Mocha/src/Mocha.Mediator/MediatorMiddleware.cs new file mode 100644 index 00000000000..eaf5b17eb73 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorMiddleware.cs @@ -0,0 +1,7 @@ +namespace Mocha.Mediator; + +/// +/// A factory delegate that wraps a with middleware logic. +/// The factory receives context for resolving services and the next delegate in the pipeline. +/// +public delegate MediatorDelegate MediatorMiddleware(MediatorMiddlewareFactoryContext context, MediatorDelegate next); diff --git a/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareCompiler.cs b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareCompiler.cs new file mode 100644 index 00000000000..4865689975e --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareCompiler.cs @@ -0,0 +1,49 @@ +namespace Mocha.Mediator; + +/// +/// Compiles ordered middleware configurations into a single executable . +/// Mirrors the message bus MiddlewareCompiler pattern. +/// +internal static class MediatorMiddlewareCompiler +{ + private static List? s_middlewares; + + public static MediatorDelegate Compile( + MediatorMiddlewareFactoryContext context, + MediatorDelegate terminal, + ReadOnlySpan> middlewareConfigurations, + ReadOnlySpan>>> pipelineModifiers) + { + var middlewares = Interlocked.Exchange(ref s_middlewares, null); + middlewares ??= []; + + foreach (var middleware in middlewareConfigurations) + { + middlewares.AddRange(middleware); + } + + foreach (var modifiers in pipelineModifiers) + { + foreach (var modifier in modifiers) + { + modifier(middlewares); + } + } + + middlewares.Reverse(); + + var pipeline = terminal; + + foreach (var middleware in middlewares) + { + var next = pipeline; + pipeline = middleware.Middleware(context, next); + } + + middlewares.Clear(); + + Interlocked.CompareExchange(ref s_middlewares, middlewares, null); + + return pipeline; + } +} diff --git a/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareConfiguration.cs b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareConfiguration.cs new file mode 100644 index 00000000000..c6d5b79f846 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareConfiguration.cs @@ -0,0 +1,6 @@ +namespace Mocha.Mediator; + +/// +/// Holds a middleware factory and an optional key for ordering or identification. +/// +public sealed record MediatorMiddlewareConfiguration(MediatorMiddleware Middleware, string? Key = null); diff --git a/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContext.cs b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContext.cs new file mode 100644 index 00000000000..53b47c6439d --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContext.cs @@ -0,0 +1,34 @@ +namespace Mocha.Mediator; + +/// +/// Provides context to factories during pipeline compilation. +/// Contains the message type, response type, and feature collection of the pipeline being compiled, +/// allowing middleware to opt out at compile time by returning next directly. +/// +public sealed class MediatorMiddlewareFactoryContext +{ + /// + /// Gets the root service provider, used to resolve singleton or scoped services + /// that the middleware instance needs at construction time. + /// + public required IServiceProvider Services { get; init; } + + /// + /// Gets the feature collection for the mediator being built. + /// Use this to read configuration that was registered via + /// . + /// + public required IFeatureCollection Features { get; init; } + + /// + /// Gets the message type of the pipeline being compiled. + /// + public Type MessageType { get; internal set; } = null!; + + /// + /// Gets the response type of the pipeline being compiled, + /// or for void commands and notifications. + /// For stream pipelines this is IAsyncEnumerable<T>. + /// + public Type? ResponseType { get; internal set; } +} diff --git a/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs new file mode 100644 index 00000000000..8d5bfa72fbb --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs @@ -0,0 +1,73 @@ +namespace Mocha.Mediator; + +/// +/// Extension methods for that allow middleware +/// factories to inspect the pipeline being compiled and decide whether to participate. +/// Returning next from a middleware factory when these checks fail eliminates the +/// middleware from that pipeline entirely — zero runtime cost. +/// +public static class MediatorMiddlewareFactoryContextExtensions +{ + /// + /// Returns if the pipeline is for a void command (). + /// + public static bool IsCommand(this MediatorMiddlewareFactoryContext context) + => typeof(ICommand).IsAssignableFrom(context.MessageType); + + /// + /// Returns if the pipeline is for a command with a response (). + /// + public static bool IsCommandWithResponse(this MediatorMiddlewareFactoryContext context) + => HasGenericInterface(context.MessageType, typeof(ICommand<>)); + + /// + /// Returns if the pipeline is for a query (). + /// + public static bool IsQuery(this MediatorMiddlewareFactoryContext context) + => HasGenericInterface(context.MessageType, typeof(IQuery<>)); + + /// + /// Returns if the pipeline is for a notification (). + /// + public static bool IsNotification(this MediatorMiddlewareFactoryContext context) + => typeof(INotification).IsAssignableFrom(context.MessageType); + + /// + /// Returns if the message type is assignable to . + /// + public static bool IsMessageAssignableTo(this MediatorMiddlewareFactoryContext context) + => typeof(T).IsAssignableFrom(context.MessageType); + + /// + /// Returns if the message type is assignable to . + /// + public static bool IsMessageAssignableTo(this MediatorMiddlewareFactoryContext context, Type type) + => type.IsAssignableFrom(context.MessageType); + + /// + /// Returns if the response type is assignable to . + /// Returns for void commands and notifications (no response type). + /// + public static bool IsResponseAssignableTo(this MediatorMiddlewareFactoryContext context) + => context.ResponseType is not null && typeof(T).IsAssignableFrom(context.ResponseType); + + /// + /// Returns if the response type is assignable to . + /// Returns for void commands and notifications (no response type). + /// + public static bool IsResponseAssignableTo(this MediatorMiddlewareFactoryContext context, Type type) + => context.ResponseType is not null && type.IsAssignableFrom(context.ResponseType); + + private static bool HasGenericInterface(Type type, Type openGeneric) + { + foreach (var @interface in type.GetInterfaces()) + { + if (@interface.IsGenericType && @interface.GetGenericTypeDefinition() == openGeneric) + { + return true; + } + } + + return false; + } +} diff --git a/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs b/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs new file mode 100644 index 00000000000..77017811dbe --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator; + +/// +/// Represents the configuration options for the Mocha Mediator. +/// +public sealed class MediatorOptions +{ + /// + /// Gets or sets the default service lifetime for handlers and behaviors. + /// Default is . + /// + public ServiceLifetime ServiceLifetime { get; set; } = ServiceLifetime.Scoped; +} diff --git a/src/Mocha/src/Mocha.Mediator/MediatorPools.cs b/src/Mocha/src/Mocha.Mediator/MediatorPools.cs new file mode 100644 index 00000000000..a398ab33853 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorPools.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.ObjectPool; + +namespace Mocha.Mediator; + +internal sealed class MediatorPools(ObjectPool mediatorContextPool) : IMediatorPools +{ + public ObjectPool MediatorContext => mediatorContextPool; +} diff --git a/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs b/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs new file mode 100644 index 00000000000..bfad5b99bf6 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs @@ -0,0 +1,86 @@ +using System.Collections.Frozen; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.ObjectPool; + +namespace Mocha.Mediator; + +/// +/// Holds pre-compiled middleware pipelines for all registered message types. +/// Registered as a singleton and shared across all mediator instances. +/// +public sealed class MediatorRuntime : IMediatorRuntime +{ + private readonly FrozenDictionary _pipelines; + private readonly ObjectPool _contextPool; + + [ThreadStatic] + private static MediatorContext? s_cached; + + internal MediatorRuntime( + FrozenDictionary pipelines, + IMediatorPools pools, + IFeatureCollection features) + { + _pipelines = pipelines; + _contextPool = pools.MediatorContext; + Features = features; + } + + /// + /// Gets the read-only feature collection for this mediator runtime. + /// + public IFeatureCollection Features { get; } + + /// + /// Gets the compiled pipeline delegate for the specified message type. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MediatorDelegate GetPipeline(Type messageType) + { + if (_pipelines.TryGetValue(messageType, out var pipeline)) + { + return pipeline; + } + + return ThrowMissingPipeline(messageType); + } + + /// + /// Rents a from the pool. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MediatorContext RentContext() + { + var context = s_cached; + if (context is not null) + { + s_cached = null; + return context; + } + + return _contextPool.Get(); + } + + /// + /// Returns a to the pool after resetting it. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReturnContext(MediatorContext context) + { + context.Reset(); + + if (s_cached is null) + { + s_cached = context; + } + else + { + _contextPool.Return(context); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static MediatorDelegate ThrowMissingPipeline(Type messageType) + => throw new InvalidOperationException( + $"No pipeline registered for message type {messageType}"); +} diff --git a/src/Mocha/src/Mocha.Mediator/Mocha.Mediator.csproj b/src/Mocha/src/Mocha.Mediator/Mocha.Mediator.csproj new file mode 100644 index 00000000000..85557408cce --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Mocha.Mediator.csproj @@ -0,0 +1,24 @@ + + + Mocha.Mediator + Mocha.Mediator + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/src/Mocha/src/Mocha.Mediator/Pipeline/ForeachAwaitPublisher.cs b/src/Mocha/src/Mocha.Mediator/Pipeline/ForeachAwaitPublisher.cs new file mode 100644 index 00000000000..5e90357a51a --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Pipeline/ForeachAwaitPublisher.cs @@ -0,0 +1,50 @@ +namespace Mocha.Mediator; + +/// +/// Represents a notification publishing strategy that dispatches to each handler sequentially, +/// awaiting completion before proceeding to the next. +/// +public sealed class ForeachAwaitPublisher : INotificationStrategy +{ + /// + /// Publishes a notification to each handler in sequence, awaiting each one before proceeding. + /// + /// The type of notification to publish. + /// The collection of handlers to notify. + /// The notification instance to publish. + /// A token to observe for cancellation requests. + /// A representing the asynchronous operation. + public ValueTask PublishAsync( + IReadOnlyList> handlers, + TNotification notification, + CancellationToken cancellationToken) + where TNotification : INotification + { + var count = handlers.Count; + + if (count == 0) + { + return default; + } + + if (count == 1) + { + return handlers[0].HandleAsync(notification, cancellationToken); + } + + return PublishSequentially(handlers, notification, cancellationToken, count); + } + + private static async ValueTask PublishSequentially( + IReadOnlyList> handlers, + TNotification notification, + CancellationToken cancellationToken, + int count) + where TNotification : INotification + { + for (var i = 0; i < count; i++) + { + await handlers[i].HandleAsync(notification, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/Mocha/src/Mocha.Mediator/Pipeline/MediatorPipelineConfiguration.cs b/src/Mocha/src/Mocha.Mediator/Pipeline/MediatorPipelineConfiguration.cs new file mode 100644 index 00000000000..351504660f7 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Pipeline/MediatorPipelineConfiguration.cs @@ -0,0 +1,31 @@ +using System.ComponentModel; + +namespace Mocha.Mediator; + +/// +/// Describes a pipeline to be compiled for a specific message type. +/// Carries all metadata needed by the middleware compiler so it does not +/// have to derive information from the message type at runtime. +/// This type is intended for use by source-generated code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class MediatorPipelineConfiguration +{ + /// + /// Gets the message type this pipeline handles. + /// + public required Type MessageType { get; init; } + + /// + /// Gets the response type produced by the handler, + /// or for void commands and notifications. + /// For stream handlers this is IAsyncEnumerable<TResponse>. + /// + public Type? ResponseType { get; init; } + + /// + /// Gets the terminal delegate that invokes the handler. + /// This is the innermost layer of the middleware pipeline. + /// + public required MediatorDelegate Terminal { get; init; } +} diff --git a/src/Mocha/src/Mocha.Mediator/Pipeline/NotificationStrategyFeature.cs b/src/Mocha/src/Mocha.Mediator/Pipeline/NotificationStrategyFeature.cs new file mode 100644 index 00000000000..3eebc024c58 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Pipeline/NotificationStrategyFeature.cs @@ -0,0 +1,9 @@ +namespace Mocha.Mediator; + +/// +/// Feature that carries the resolved on the mediator runtime. +/// +internal sealed class NotificationStrategyFeature(INotificationStrategy strategy) +{ + public INotificationStrategy Strategy { get; } = strategy; +} diff --git a/src/Mocha/src/Mocha.Mediator/Pipeline/PipelineBuilder.cs b/src/Mocha/src/Mocha.Mediator/Pipeline/PipelineBuilder.cs new file mode 100644 index 00000000000..243e2568ee2 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Pipeline/PipelineBuilder.cs @@ -0,0 +1,164 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using Mocha.Features; + +namespace Mocha.Mediator; + +/// +/// Provides terminal delegate factories for each handler kind. +/// These delegates form the innermost layer of the middleware pipeline, +/// resolving handlers from the scoped service provider and invoking them. +/// This class is intended for use by source-generated code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class PipelineBuilder +{ + /// + /// Builds a terminal delegate for a void command handler. + /// + public static MediatorDelegate BuildVoidCommandTerminal() + where TCommand : ICommand + { + var serviceType = typeof(ICommandHandler); + + return ctx => + { + var handler = (ICommandHandler)ctx.Services.GetRequiredService(serviceType); + var task = handler.HandleAsync((TCommand)ctx.Message, ctx.CancellationToken); + + if (task.IsCompletedSuccessfully) + { + return default; + } + + return Awaited(task); + + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + static async ValueTask Awaited(ValueTask t) + { + await t.ConfigureAwait(false); + } + }; + } + + /// + /// Builds a terminal delegate for a command handler that returns a response. + /// + public static MediatorDelegate BuildCommandTerminal() + where TCommand : ICommand + { + var serviceType = typeof(ICommandHandler); + + return ctx => + { + var handler = (ICommandHandler)ctx.Services.GetRequiredService(serviceType); + var task = handler.HandleAsync((TCommand)ctx.Message, ctx.CancellationToken); + + if (task.IsCompletedSuccessfully) + { + ctx.Result = task.Result; + return default; + } + + return Awaited(task, ctx); + + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + static async ValueTask Awaited(ValueTask t, IMediatorContext c) + { + c.Result = await t.ConfigureAwait(false); + } + }; + } + + /// + /// Builds a terminal delegate for a query handler. + /// + public static MediatorDelegate BuildQueryTerminal() + where TQuery : IQuery + { + var serviceType = typeof(IQueryHandler); + + return ctx => + { + var handler = (IQueryHandler)ctx.Services.GetRequiredService(serviceType); + var task = handler.HandleAsync((TQuery)ctx.Message, ctx.CancellationToken); + + if (task.IsCompletedSuccessfully) + { + ctx.Result = task.Result; + return default; + } + + return Awaited(task, ctx); + + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + static async ValueTask Awaited(ValueTask t, IMediatorContext c) + { + c.Result = await t.ConfigureAwait(false); + } + }; + } + + /// + /// Builds a terminal delegate for a notification with handler types known at compile time. + /// + public static MediatorDelegate BuildNotificationTerminal(Type[] handlerTypes) + where TNotification : INotification + { + if (handlerTypes.Length == 1) + { + var handlerType = handlerTypes[0]; + return ctx => + { + var handler = (INotificationHandler)ctx.Services.GetRequiredService(handlerType); + var task = handler.HandleAsync((TNotification)ctx.Message, ctx.CancellationToken); + + if (task.IsCompletedSuccessfully) + { + return default; + } + + return Awaited(task); + + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + static async ValueTask Awaited(ValueTask t) + { + await t.ConfigureAwait(false); + } + }; + } + + return ctx => + { + var strategy = ctx.Runtime.Features + .GetRequired().Strategy; + + var handlers = new INotificationHandler[handlerTypes.Length]; + for (var i = 0; i < handlerTypes.Length; i++) + { + handlers[i] = (INotificationHandler)ctx.Services.GetRequiredService(handlerTypes[i]); + } + + var task = strategy.PublishAsync(handlers, (TNotification)ctx.Message, ctx.CancellationToken); + + if (task.IsCompletedSuccessfully) + { + return default; + } + + return Awaited(task); + + [MethodImpl(MethodImplOptions.NoInlining)] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + static async ValueTask Awaited(ValueTask t) + { + await t.ConfigureAwait(false); + } + }; + } +} diff --git a/src/Mocha/src/Mocha.Mediator/Pipeline/TaskWhenAllPublisher.cs b/src/Mocha/src/Mocha.Mediator/Pipeline/TaskWhenAllPublisher.cs new file mode 100644 index 00000000000..df3dd75001c --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Pipeline/TaskWhenAllPublisher.cs @@ -0,0 +1,54 @@ +namespace Mocha.Mediator; + +/// +/// Represents a notification publishing strategy that dispatches to all handlers concurrently +/// using . +/// +public sealed class TaskWhenAllPublisher : INotificationStrategy +{ + /// + /// Publishes a notification to all handlers concurrently, awaiting all completions. + /// + /// The type of notification to publish. + /// The collection of handlers to notify. + /// The notification instance to publish. + /// A token to observe for cancellation requests. + /// A representing the asynchronous operation. + public ValueTask PublishAsync( + IReadOnlyList> handlers, + TNotification notification, + CancellationToken cancellationToken) + where TNotification : INotification + { + var count = handlers.Count; + + if (count == 0) + { + return default; + } + + if (count == 1) + { + return handlers[0].HandleAsync(notification, cancellationToken); + } + + return PublishConcurrently(handlers, notification, cancellationToken, count); + } + + private static async ValueTask PublishConcurrently( + IReadOnlyList> handlers, + TNotification notification, + CancellationToken cancellationToken, + int count) + where TNotification : INotification + { + var tasks = new Task[count]; + + for (var i = 0; i < count; i++) + { + tasks[i] = handlers[i].HandleAsync(notification, cancellationToken).AsTask(); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + } +} diff --git a/src/Mocha/src/Mocha.Mediator/PoolingMediatorExtensions.cs b/src/Mocha/src/Mocha.Mediator/PoolingMediatorExtensions.cs new file mode 100644 index 00000000000..44251670cc3 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/PoolingMediatorExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.ObjectPool; + +namespace Mocha.Mediator; + +/// +/// Extension methods for registering object pooling services used by the mediator infrastructure. +/// +public static class PoolingMediatorExtensions +{ + internal static IServiceCollection AddMediatorPoolingCore(this IServiceCollection services) + { + services.TryAddSingleton>(_ => new MediatorContextPool()); + services.TryAddSingleton(); + + return services; + } +} diff --git a/src/Mocha/src/Mocha/Utils/Buffers/ArrayMemoryOwner.cs b/src/Mocha/src/Mocha.Utilities/Buffers/ArrayMemoryOwner.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Buffers/ArrayMemoryOwner.cs rename to src/Mocha/src/Mocha.Utilities/Buffers/ArrayMemoryOwner.cs diff --git a/src/Mocha/src/Mocha/Utils/Buffers/BufferPools.cs b/src/Mocha/src/Mocha.Utilities/Buffers/BufferPools.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Buffers/BufferPools.cs rename to src/Mocha/src/Mocha.Utilities/Buffers/BufferPools.cs diff --git a/src/Mocha/src/Mocha/Utils/Buffers/IWritableMemory.cs b/src/Mocha/src/Mocha.Utilities/Buffers/IWritableMemory.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Buffers/IWritableMemory.cs rename to src/Mocha/src/Mocha.Utilities/Buffers/IWritableMemory.cs diff --git a/src/Mocha/src/Mocha/Utils/Buffers/PooledArrayWriter.cs b/src/Mocha/src/Mocha.Utilities/Buffers/PooledArrayWriter.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Buffers/PooledArrayWriter.cs rename to src/Mocha/src/Mocha.Utilities/Buffers/PooledArrayWriter.cs diff --git a/src/Mocha/src/Mocha/Utils/Buffers/ReadOnlyMemorySegment.cs b/src/Mocha/src/Mocha.Utilities/Buffers/ReadOnlyMemorySegment.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Buffers/ReadOnlyMemorySegment.cs rename to src/Mocha/src/Mocha.Utilities/Buffers/ReadOnlyMemorySegment.cs diff --git a/src/Mocha/src/Mocha/Utils/Features/EmptyFeatureCollection.cs b/src/Mocha/src/Mocha.Utilities/Features/EmptyFeatureCollection.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Features/EmptyFeatureCollection.cs rename to src/Mocha/src/Mocha.Utilities/Features/EmptyFeatureCollection.cs diff --git a/src/Mocha/src/Mocha/Utils/Features/FeatureCollection.cs b/src/Mocha/src/Mocha.Utilities/Features/FeatureCollection.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Features/FeatureCollection.cs rename to src/Mocha/src/Mocha.Utilities/Features/FeatureCollection.cs diff --git a/src/Mocha/src/Mocha/Utils/Features/FeatureCollectionExtensions.cs b/src/Mocha/src/Mocha.Utilities/Features/FeatureCollectionExtensions.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Features/FeatureCollectionExtensions.cs rename to src/Mocha/src/Mocha.Utilities/Features/FeatureCollectionExtensions.cs diff --git a/src/Mocha/src/Mocha/Utils/Features/IPooledFeature.cs b/src/Mocha/src/Mocha.Utilities/Features/IPooledFeature.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Features/IPooledFeature.cs rename to src/Mocha/src/Mocha.Utilities/Features/IPooledFeature.cs diff --git a/src/Mocha/src/Mocha/Utils/Features/ISealable.cs b/src/Mocha/src/Mocha.Utilities/Features/ISealable.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Features/ISealable.cs rename to src/Mocha/src/Mocha.Utilities/Features/ISealable.cs diff --git a/src/Mocha/src/Mocha/Utils/Features/PooledFeatureCollection.cs b/src/Mocha/src/Mocha.Utilities/Features/PooledFeatureCollection.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Features/PooledFeatureCollection.cs rename to src/Mocha/src/Mocha.Utilities/Features/PooledFeatureCollection.cs diff --git a/src/Mocha/src/Mocha/Utils/Features/ReadOnlyFeatureCollection.cs b/src/Mocha/src/Mocha.Utilities/Features/ReadOnlyFeatureCollection.cs similarity index 100% rename from src/Mocha/src/Mocha/Utils/Features/ReadOnlyFeatureCollection.cs rename to src/Mocha/src/Mocha.Utilities/Features/ReadOnlyFeatureCollection.cs diff --git a/src/Mocha/src/Mocha.Utilities/Mocha.Utilities.csproj b/src/Mocha/src/Mocha.Utilities/Mocha.Utilities.csproj new file mode 100644 index 00000000000..4b6bd676d78 --- /dev/null +++ b/src/Mocha/src/Mocha.Utilities/Mocha.Utilities.csproj @@ -0,0 +1,17 @@ + + + Mocha.Utilities + Mocha + enable + enable + + + + + + + + + + + diff --git a/src/Mocha/src/Mocha/Extensions/IMessageBusHostBuilderExtensions.cs b/src/Mocha/src/Mocha/Extensions/IMessageBusHostBuilderExtensions.cs index 3b39a63a857..1aa90a0956c 100644 --- a/src/Mocha/src/Mocha/Extensions/IMessageBusHostBuilderExtensions.cs +++ b/src/Mocha/src/Mocha/Extensions/IMessageBusHostBuilderExtensions.cs @@ -92,12 +92,13 @@ public static IMessageBusHostBuilder AddConsumer< } /// - /// Configures additional services for the message bus through the host builder. + /// Registers additional services into the internal service collection used by the message bus. + /// This is the message bus equivalent of Hot Chocolate's ConfigureSchemaServices. /// /// The host builder. /// The action to configure services. /// The builder for method chaining. - public static IMessageBusHostBuilder ConfigureServices( + public static IMessageBusHostBuilder ConfigureBusServices( this IMessageBusHostBuilder builder, Action configure) { @@ -106,12 +107,13 @@ public static IMessageBusHostBuilder ConfigureServices( } /// - /// Configures additional services for the message bus through the host builder, with access to the existing service provider. + /// Registers additional services into the internal service collection used by the message bus, + /// with access to the application-level service provider for conditional registration. /// /// The host builder. /// The action to configure services with access to the service provider. /// The builder for method chaining. - public static IMessageBusHostBuilder ConfigureServices( + public static IMessageBusHostBuilder ConfigureBusServices( this IMessageBusHostBuilder builder, Action configure) { diff --git a/src/Mocha/src/Mocha/Mocha.csproj b/src/Mocha/src/Mocha/Mocha.csproj index 7006de8a774..32283bbe436 100644 --- a/src/Mocha/src/Mocha/Mocha.csproj +++ b/src/Mocha/src/Mocha/Mocha.csproj @@ -17,5 +17,6 @@ + diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/CommandHandlerGeneratorTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/CommandHandlerGeneratorTests.cs new file mode 100644 index 00000000000..736e95ae586 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/CommandHandlerGeneratorTests.cs @@ -0,0 +1,80 @@ +namespace Mocha.Analyzers.Tests; + +public class CommandHandlerGeneratorTests +{ + [Fact] + public async Task Generate_VoidCommandHandler_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record DeleteOrderCommand(int OrderId) : ICommand; + + public class DeleteOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(DeleteOrderCommand command, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_CommandWithResponseHandler_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record CreateOrderCommand(string Name) : ICommand; + + public class CreateOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken) + => new(42); + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_MultipleCommandHandlers_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record DeleteOrderCommand(int OrderId) : ICommand; + + public class DeleteOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(DeleteOrderCommand command, CancellationToken cancellationToken) + => default; + } + """, + """ + using Mocha.Mediator; + + namespace TestApp; + + public record CreateOrderCommand(string Name) : ICommand; + + public class CreateOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken) + => new(42); + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs new file mode 100644 index 00000000000..822745a5d4f --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs @@ -0,0 +1,171 @@ +namespace Mocha.Analyzers.Tests; + +public class DiagnosticTests +{ + [Fact] + public async Task MO0001_CommandWithNoHandler_ReportsWarning() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record DeleteOrderCommand(int OrderId) : ICommand; + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task MO0001_QueryWithNoHandler_ReportsWarning() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record GetOrderQuery(int OrderId) : IQuery; + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task MO0002_CommandWithTwoHandlers_ReportsError() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record CreateOrderCommand(string Name) : ICommand; + + public class CreateOrderHandlerA : ICommandHandler + { + public ValueTask HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken) + => new(1); + } + + public class CreateOrderHandlerB : ICommandHandler + { + public ValueTask HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken) + => new(2); + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task NoWarning_CommandWithHandler_NoDiagnostic() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record DeleteOrderCommand(int OrderId) : ICommand; + + public class DeleteOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(DeleteOrderCommand command, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task MO0003_AbstractHandler_ReportsWarning() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record DeleteOrderCommand(int OrderId) : ICommand; + + public abstract class BaseDeleteOrderHandler : ICommandHandler + { + public abstract ValueTask HandleAsync(DeleteOrderCommand command, CancellationToken cancellationToken); + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task MO0002_VoidCommandWithTwoHandlers_ReportsError() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record DeleteOrderCommand(int OrderId) : ICommand; + + public class DeleteOrderHandlerA : ICommandHandler + { + public ValueTask HandleAsync(DeleteOrderCommand command, CancellationToken cancellationToken) + => default; + } + + public class DeleteOrderHandlerB : ICommandHandler + { + public ValueTask HandleAsync(DeleteOrderCommand command, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task MO0004_OpenGenericCommand_ReportsInfo() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record GenericCommand(T Value) : ICommand; + + public class GenericCommandHandler : ICommandHandler> + { + public ValueTask HandleAsync(GenericCommand command, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task MO0004_OpenGenericQuery_ReportsInfo() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record GenericQuery : IQuery; + + public class GenericQueryHandler : IQueryHandler, T> + { + public ValueTask HandleAsync(GenericQuery query, CancellationToken cancellationToken) + => default!; + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/ExplicitModuleNameTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/ExplicitModuleNameTests.cs new file mode 100644 index 00000000000..9421367297c --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/ExplicitModuleNameTests.cs @@ -0,0 +1,27 @@ +namespace Mocha.Analyzers.Tests; + +public class ExplicitModuleNameTests +{ + [Fact] + public async Task Generate_ModuleWithOnlyName_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + [assembly: MediatorModule("Test2")] + + namespace TestApp; + + public record DeleteOrderCommand(int OrderId) : ICommand; + + public class DeleteOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(DeleteOrderCommand command, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/GenericHandlerTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/GenericHandlerTests.cs new file mode 100644 index 00000000000..eb8d7c592dc --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/GenericHandlerTests.cs @@ -0,0 +1,87 @@ +namespace Mocha.Analyzers.Tests; + +public class GenericHandlerTests +{ + [Fact] + public async Task Generate_GenericBaseHandler_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public abstract class BaseHandler : ICommandHandler + where TCommand : ICommand + { + public abstract ValueTask HandleAsync(TCommand command, CancellationToken ct); + } + + public record MyCommand(int Id) : ICommand; + + public class MyHandler : BaseHandler + { + public override ValueTask HandleAsync(MyCommand command, CancellationToken ct) + => new("ok"); + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_OpenGenericCommand_MatchesSnapshot() + { + // An open generic command type cannot be dispatched by the mediator at + // runtime, so the generator should either skip it or handle it gracefully. + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record ProcessCommand(T Data) : ICommand; + + public class StringProcessor : ICommandHandler, string> + { + public ValueTask HandleAsync(ProcessCommand command, CancellationToken ct) + => new("processed"); + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot() + { + // Multiple handlers in the same namespace should produce deterministic output ordering. + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record ZetaCommand() : ICommand; + public record AlphaCommand() : ICommand; + public record MidCommand() : ICommand; + + public class ZetaHandler : ICommandHandler + { + public ValueTask HandleAsync(ZetaCommand command, CancellationToken ct) => default; + } + + public class AlphaHandler : ICommandHandler + { + public ValueTask HandleAsync(AlphaCommand command, CancellationToken ct) => default; + } + + public class MidHandler : ICommandHandler + { + public ValueTask HandleAsync(MidCommand command, CancellationToken ct) => default; + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/InternalHandlerTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/InternalHandlerTests.cs new file mode 100644 index 00000000000..94d3683f6c7 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/InternalHandlerTests.cs @@ -0,0 +1,27 @@ +namespace Mocha.Analyzers.Tests; + +public class InternalHandlerTests +{ + [Fact] + public async Task Generate_InternalHandler_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + [assembly: MediatorModule("Test")] + + namespace TestApp; + + public record DeleteOrderCommand(int OrderId) : ICommand; + + internal class DeleteOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(DeleteOrderCommand command, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/KnownTypeSymbolsTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/KnownTypeSymbolsTests.cs new file mode 100644 index 00000000000..32300179953 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/KnownTypeSymbolsTests.cs @@ -0,0 +1,116 @@ +namespace Mocha.Analyzers.Tests; + +/// +/// Tests that KnownTypeSymbols correctly resolves (or fails to resolve) Mocha mediator +/// type symbols from a compilation. These tests exercise symbol resolution indirectly through +/// the source generator via . +/// +public class KnownTypeSymbolsTests +{ + [Fact] + public async Task Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot() + { + // This exercises resolution of KnownTypeSymbols properties by including + // every handler type. If any symbol fails to resolve, the generator would not + // produce the expected registrations. + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + // ICommand (void) -> ICommandHandlerVoid + public record VoidCommand() : ICommand; + public class VoidCommandHandler : ICommandHandler + { + public ValueTask HandleAsync(VoidCommand cmd, CancellationToken ct) => default; + } + + // ICommand -> ICommandHandlerResponse + public record ResponseCommand() : ICommand; + public class ResponseCommandHandler : ICommandHandler + { + public ValueTask HandleAsync(ResponseCommand cmd, CancellationToken ct) => new(42); + } + + // IQuery -> IQueryHandler + public record MyQuery() : IQuery; + public class MyQueryHandler : IQueryHandler + { + public ValueTask HandleAsync(MyQuery q, CancellationToken ct) => new("result"); + } + + // INotification -> INotificationHandler + public record MyEvent() : INotification; + public class MyEventHandler : INotificationHandler + { + public ValueTask HandleAsync(MyEvent n, CancellationToken ct) => default; + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_WithoutMochaUsings_NoHandlersRegistered_MatchesSnapshot() + { + // When code does not reference Mocha types at all, no handler registrations + // should be generated (KnownTypeSymbols properties return null and the generator + // skips the compilation). + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + namespace TestApp; + + public class PlainService + { + public void DoWork() { } + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot() + { + // Tests that the ICommandVoid symbol resolution correctly identifies ICommand + // (the marker interface without TResponse). + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record FireAndForgetCommand(string Data) : ICommand; + + public class FireAndForgetHandler : ICommandHandler + { + public ValueTask HandleAsync(FireAndForgetCommand cmd, CancellationToken ct) => default; + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot() + { + // Tests that the ICommandOfT symbol resolution correctly identifies ICommand. + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record ComputeCommand(int X, int Y) : ICommand; + + public class ComputeHandler : ICommandHandler + { + public ValueTask HandleAsync(ComputeCommand cmd, CancellationToken ct) + => new((long)cmd.X + cmd.Y); + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/MediatorModuleTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/MediatorModuleTests.cs new file mode 100644 index 00000000000..867b58cc851 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/MediatorModuleTests.cs @@ -0,0 +1,71 @@ +namespace Mocha.Analyzers.Tests; + +public class MediatorModuleTests +{ + [Fact] + public async Task Generate_DefaultAssemblyName_PrefixesWithLastSegment() + { + // assemblyName defaults to "Tests" in TestHelper, so module name = "Tests" + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record GetItemQuery(int Id) : IQuery; + + public class GetItemHandler : IQueryHandler + { + public ValueTask HandleAsync(GetItemQuery query, CancellationToken cancellationToken) + => new("item"); + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_DottedAssemblyName_UsesLastSegment() + { + // "MyCompany.Services.Ordering" -> module name = "Ordering" + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record PingCommand() : ICommand; + + public class PingHandler : ICommandHandler + { + public ValueTask HandleAsync(PingCommand command, CancellationToken cancellationToken) + => default; + } + """ + ], assemblyName: "MyCompany.Services.Ordering").MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_ModuleFile_ContainsHandlerRegistrations() + { + // Verifies the module file ({ModuleName}MediatorModule.g.cs) is generated + // with handler registrations for composing multiple modules + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record CreateInvoiceCommand(string Name) : ICommand; + + public class CreateInvoiceHandler : ICommandHandler + { + public ValueTask HandleAsync(CreateInvoiceCommand command, CancellationToken cancellationToken) + => new(1); + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/MixedHandlerGeneratorTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/MixedHandlerGeneratorTests.cs new file mode 100644 index 00000000000..d1591d01e41 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/MixedHandlerGeneratorTests.cs @@ -0,0 +1,110 @@ +namespace Mocha.Analyzers.Tests; + +public class MixedHandlerGeneratorTests +{ + [Fact] + public async Task Generate_AllHandlerTypes_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + // Commands + public record CreateOrderCommand(string Name) : ICommand; + public record DeleteOrderCommand(int OrderId) : ICommand; + + // Queries + public record GetUserQuery(int Id) : IQuery; + public record UserDto(int Id, string Name); + + // Notifications + public record OrderCreated(int OrderId) : INotification; + + // Handlers + public class CreateOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken) + => new(1); + } + + public class DeleteOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(DeleteOrderCommand command, CancellationToken cancellationToken) + => default; + } + + public class GetUserHandler : IQueryHandler + { + public ValueTask HandleAsync(GetUserQuery query, CancellationToken cancellationToken) + => new(new UserDto(1, "Test")); + } + + public class OrderCreatedEmailHandler : INotificationHandler + { + public ValueTask HandleAsync(OrderCreated notification, CancellationToken cancellationToken) + => default; + } + + public class OrderCreatedStatsHandler : INotificationHandler + { + public ValueTask HandleAsync(OrderCreated notification, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_NoHandlers_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + namespace TestApp; + + public class SomeClass + { + public void DoStuff() { } + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_HandlersInDifferentNamespaces_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp.Orders; + + public record CreateOrderCommand(string Name) : ICommand; + + public class CreateOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken) + => new(1); + } + """, + """ + using Mocha.Mediator; + + namespace TestApp.Users; + + public record GetUserQuery(int Id) : IQuery; + public record UserDto(int Id, string Name); + + public class GetUserHandler : IQueryHandler + { + public ValueTask HandleAsync(GetUserQuery query, CancellationToken cancellationToken) + => new(new UserDto(1, "Test")); + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/Mocha.Analyzers.Tests.csproj b/src/Mocha/test/Mocha.Analyzers.Tests/Mocha.Analyzers.Tests.csproj new file mode 100644 index 00000000000..d454ff3e5a5 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/Mocha.Analyzers.Tests.csproj @@ -0,0 +1,33 @@ + + + + false + Mocha.Analyzers.Tests + Mocha.Analyzers.Tests + true + .generated + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/ModuleInfo.cs b/src/Mocha/test/Mocha.Analyzers.Tests/ModuleInfo.cs new file mode 100644 index 00000000000..dbd3efbb23b --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/ModuleInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Mocha.Analyzers")] diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/ModuleNameHelperTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/ModuleNameHelperTests.cs new file mode 100644 index 00000000000..4871829e284 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/ModuleNameHelperTests.cs @@ -0,0 +1,134 @@ +using System.Reflection; + +namespace Mocha.Analyzers.Tests; + +/// +/// Tests ModuleNameHelper edge cases. Because ModuleNameHelper is internal +/// to Mocha.Analyzers and the test project references it only as an analyzer, +/// we use two strategies: +/// 1. Reflection for direct unit tests of CreateModuleName / SanitizeIdentifier. +/// 2. Snapshot tests through the generator for end-to-end verification. +/// +public class ModuleNameHelperTests +{ + private static readonly Type s_helperType = + typeof(MediatorGenerator).Assembly + .GetType("Mocha.Analyzers.Utils.ModuleNameHelper", throwOnError: true)!; + + private static readonly MethodInfo s_createModuleName = + s_helperType.GetMethod("CreateModuleName", BindingFlags.Public | BindingFlags.Static)!; + + private static readonly MethodInfo s_sanitizeIdentifier = + s_helperType.GetMethod("SanitizeIdentifier", BindingFlags.NonPublic | BindingFlags.Static)!; + + private static string CreateModuleName(string? assemblyName) + => (string)s_createModuleName.Invoke(null, [assemblyName])!; + + private static string SanitizeIdentifier(string input) + => (string)s_sanitizeIdentifier.Invoke(null, [input])!; + + [Fact] + public void CreateModuleName_NullAssemblyName_ReturnsAssembly() + { + Assert.Equal("Assembly", CreateModuleName(null)); + } + + [Fact] + public void CreateModuleName_SimpleName_ReturnsSameName() + { + Assert.Equal("MyApp", CreateModuleName("MyApp")); + } + + [Fact] + public void CreateModuleName_DottedName_ReturnsLastSegment() + { + Assert.Equal("Billing", CreateModuleName("MyCompany.Services.Billing")); + } + + [Fact] + public void CreateModuleName_TrailingDot_HandlesGracefully() + { + // "App." splits into ["App", ""], last segment is "" + // SanitizeIdentifier("") should produce "_" + var result = CreateModuleName("App."); + Assert.False(string.IsNullOrEmpty(result)); + Assert.Equal("_", result); + } + + [Fact] + public void CreateModuleName_LeadingNumberSingleSegment_Sanitized() + { + // "3rdParty" -> SanitizeIdentifier("3rdParty") -> "_3rdParty" + var result = CreateModuleName("3rdParty"); + Assert.StartsWith("_", result); + } + + [Fact] + public void SanitizeIdentifier_EmptyString_ReturnsUnderscore() + { + Assert.Equal("_", SanitizeIdentifier("")); + } + + [Fact] + public void SanitizeIdentifier_OnlySpecialChars_ReturnsUnderscores() + { + // "---" -> each '-' replaced with '_' -> "___" + // first char '_' is not a letter -> prepend '_' -> "____" + var result = SanitizeIdentifier("---"); + Assert.False(string.IsNullOrEmpty(result)); + Assert.StartsWith("_", result); + Assert.Equal("____", result); + } + + [Fact] + public void SanitizeIdentifier_StartsWithDigit_PrependsUnderscore() + { + var result = SanitizeIdentifier("123"); + Assert.StartsWith("_", result); + Assert.Equal("_123", result); + } + + // End-to-end snapshot test: verify the generator uses the module name correctly + // for an assembly name with special characters. + [Fact] + public async Task Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record PingCommand() : ICommand; + + public class PingHandler : ICommandHandler + { + public ValueTask HandleAsync(PingCommand command, CancellationToken cancellationToken) + => default; + } + """ + ], assemblyName: "My-Company.Services.Order-Processing").MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record PingCommand() : ICommand; + + public class PingHandler : ICommandHandler + { + public ValueTask HandleAsync(PingCommand command, CancellationToken cancellationToken) + => default; + } + """ + ], assemblyName: null).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/NestedHandlerTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/NestedHandlerTests.cs new file mode 100644 index 00000000000..36b81319f45 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/NestedHandlerTests.cs @@ -0,0 +1,28 @@ +namespace Mocha.Analyzers.Tests; + +public class NestedHandlerTests +{ + [Fact] + public async Task Generate_NestedClassHandler_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record DeleteOrderCommand(int OrderId) : ICommand; + + public class Outer + { + public class DeleteOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(DeleteOrderCommand command, CancellationToken cancellationToken) + => default; + } + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/NotificationHandlerGeneratorTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/NotificationHandlerGeneratorTests.cs new file mode 100644 index 00000000000..b306bb76d2a --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/NotificationHandlerGeneratorTests.cs @@ -0,0 +1,52 @@ +namespace Mocha.Analyzers.Tests; + +public class NotificationHandlerGeneratorTests +{ + [Fact] + public async Task Generate_SingleNotificationHandler_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record OrderCreated(int OrderId) : INotification; + + public class OrderCreatedEmailHandler : INotificationHandler + { + public ValueTask HandleAsync(OrderCreated notification, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_MultipleHandlersForSameNotification_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record OrderCreated(int OrderId) : INotification; + + public class SendEmailHandler : INotificationHandler + { + public ValueTask HandleAsync(OrderCreated notification, CancellationToken cancellationToken) + => default; + } + + public class UpdateStatsHandler : INotificationHandler + { + public ValueTask HandleAsync(OrderCreated notification, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/PartialClassHandlerTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/PartialClassHandlerTests.cs new file mode 100644 index 00000000000..e2d17df4c83 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/PartialClassHandlerTests.cs @@ -0,0 +1,102 @@ +namespace Mocha.Analyzers.Tests; + +public class PartialClassHandlerTests +{ + [Fact] + public async Task Generate_PartialClassHandler_MatchesSnapshot() + { + // The handler interface is declared on one partial declaration, + // while the method body is in a separate partial declaration. + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record CreateOrderCommand(string Name) : ICommand; + + public partial class CreateOrderHandler : ICommandHandler + { + } + """, + """ + using Mocha.Mediator; + + namespace TestApp; + + public partial class CreateOrderHandler + { + public ValueTask HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken) + => new(42); + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot() + { + // Item 9: Partial void command handler split across two syntax trees + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record ProcessOrderCommand(int OrderId) : ICommand; + + public partial class ProcessOrderHandler : ICommandHandler + { + public ValueTask HandleAsync(ProcessOrderCommand command, CancellationToken cancellationToken) + { + Process(command.OrderId); + return default; + } + } + """, + """ + namespace TestApp; + + public partial class ProcessOrderHandler + { + private void Process(int orderId) + { + // Implementation + } + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot() + { + // Item 9: Partial query handler split across two syntax trees + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record GetOrderQuery(int Id) : IQuery; + + public partial class GetOrderQueryHandler : IQueryHandler + { + public ValueTask HandleAsync(GetOrderQuery query, CancellationToken cancellationToken) + => new(FormatOrder(query.Id)); + } + """, + """ + namespace TestApp; + + public partial class GetOrderQueryHandler + { + private static string FormatOrder(int id) => $"Order-{id}"; + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/QueryHandlerGeneratorTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/QueryHandlerGeneratorTests.cs new file mode 100644 index 00000000000..add816b9e0d --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/QueryHandlerGeneratorTests.cs @@ -0,0 +1,63 @@ +namespace Mocha.Analyzers.Tests; + +public class QueryHandlerGeneratorTests +{ + [Fact] + public async Task Generate_QueryHandler_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record GetUserQuery(int Id) : IQuery; + + public record UserDto(int Id, string Name); + + public class GetUserHandler : IQueryHandler + { + public ValueTask HandleAsync(GetUserQuery query, CancellationToken cancellationToken) + => new(new UserDto(query.Id, "Test")); + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task Generate_MultipleQueryHandlers_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record GetUserQuery(int Id) : IQuery; + public record UserDto(int Id, string Name); + + public class GetUserHandler : IQueryHandler + { + public ValueTask HandleAsync(GetUserQuery query, CancellationToken cancellationToken) + => new(new UserDto(query.Id, "Test")); + } + """, + """ + using Mocha.Mediator; + + namespace TestApp; + + public record GetOrderQuery(int Id) : IQuery; + public record OrderDto(int Id, string Status); + + public class GetOrderHandler : IQueryHandler + { + public ValueTask HandleAsync(GetOrderQuery query, CancellationToken cancellationToken) + => new(new OrderDto(query.Id, "Pending")); + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/TestHelper.cs b/src/Mocha/test/Mocha.Analyzers.Tests/TestHelper.cs new file mode 100644 index 00000000000..999872d802d --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/TestHelper.cs @@ -0,0 +1,178 @@ +using System.Collections.Immutable; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using Basic.Reference.Assemblies; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using CookieCrumble; +using Mocha.Analyzers; + +namespace Mocha.Analyzers.Tests; + +internal static class TestHelper +{ + private static readonly HashSet s_ignoreCodes = + ["CS8652", "CS8632", "CS5001", "CS8019", "CS0518", "CS0012"]; + + public static Snapshot GetGeneratedSourceSnapshot( + string[] sourceTexts, + string? assemblyName = "Tests") + { + IEnumerable references = + [ +#if NET8_0 + .. Net80.References.All, +#elif NET9_0 + .. Net90.References.All, +#elif NET10_0 + .. Net100.References.All, +#endif + // Mocha.Mediator + MetadataReference.CreateFromFile(typeof(Mocha.Mediator.IMediator).Assembly.Location), + + // Microsoft.Extensions.DependencyInjection.Abstractions + MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.DependencyInjection.IServiceCollection).Assembly.Location), + + // System.Runtime.CompilerServices.Unsafe + MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.Unsafe).Assembly.Location), + + // System.Runtime from the actual runtime (needed for predefined type resolution + // so that assembly-level attribute constructor arguments can be bound) + MetadataReference.CreateFromFile( + Path.Combine( + Path.GetDirectoryName(typeof(object).Assembly.Location)!, + "System.Runtime.dll")) + ]; + + var compilation = CSharpCompilation.Create( + assemblyName: assemblyName, + syntaxTrees: sourceTexts.Select(s => CSharpSyntaxTree.ParseText(s)), + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var generator = new MediatorGenerator(); + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); + driver = driver.RunGenerators(compilation); + + var snapshot = new Snapshot(); + + foreach (var result in driver.GetRunResult().Results) + { + var sources = result.GeneratedSources.OrderBy(s => s.HintName); + foreach (var source in sources) + { + snapshot.Add(source.SourceText.ToString(), source.HintName, MarkdownLanguages.CSharp); + } + + if (result.Diagnostics.Any()) + { + AddDiagnosticsToSnapshot(snapshot, result.Diagnostics, "Generator Diagnostics"); + } + } + + // Verify generated code can be added to compilation (syntax check). + // We skip emit verification because test compilations using Basic.Reference.Assemblies + // produce TFM-specific emit diagnostics that are not related to the generated code. + + return snapshot; + } + + private static void AddDiagnosticsToSnapshot( + Snapshot snapshot, + ImmutableArray diagnostics, + string title) + { + var hasDiagnostics = false; + using var stream = new MemoryStream(); + using var jsonWriter = new Utf8JsonWriter( + stream, + new JsonWriterOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Indented = true + }); + + jsonWriter.WriteStartArray(); + + foreach (var diagnostic in diagnostics + .OrderBy(d => d.Location.SourceTree?.FilePath) + .ThenBy(d => d.Location.GetLineSpan().StartLinePosition.Line) + .ThenBy(d => d.Location.GetLineSpan().StartLinePosition.Character)) + { + if (s_ignoreCodes.Contains(diagnostic.Id)) + { + continue; + } + + hasDiagnostics = true; + + jsonWriter.WriteStartObject(); + jsonWriter.WriteString(nameof(diagnostic.Id), diagnostic.Id); + + var descriptor = diagnostic.Descriptor; + + jsonWriter.WriteString(nameof(descriptor.Title), descriptor.Title.ToString()); + jsonWriter.WriteString(nameof(diagnostic.Severity), diagnostic.Severity.ToString()); + jsonWriter.WriteNumber(nameof(diagnostic.WarningLevel), diagnostic.WarningLevel); + + jsonWriter.WriteString( + nameof(diagnostic.Location), + diagnostic.Location.GetMappedLineSpan().ToString()); + + var description = descriptor.Description.ToString(); + if (!string.IsNullOrWhiteSpace(description)) + { + jsonWriter.WriteString(nameof(descriptor.Description), description); + } + + var help = descriptor.HelpLinkUri; + if (!string.IsNullOrWhiteSpace(help)) + { + jsonWriter.WriteString(nameof(descriptor.HelpLinkUri), help); + } + + jsonWriter.WriteString( + nameof(descriptor.MessageFormat), + descriptor.MessageFormat.ToString()); + + jsonWriter.WriteString("Message", diagnostic.GetMessage()); + jsonWriter.WriteString(nameof(descriptor.Category), descriptor.Category); + + jsonWriter.WritePropertyName(nameof(descriptor.CustomTags)); + + jsonWriter.WriteStartArray(); + + foreach (var tag in descriptor.CustomTags) + { + jsonWriter.WriteStringValue(tag); + } + + jsonWriter.WriteEndArray(); + + jsonWriter.WriteEndObject(); + } + + jsonWriter.WriteEndArray(); + jsonWriter.Flush(); + + if (hasDiagnostics) + { + snapshot.Add(Encoding.UTF8.GetString(stream.ToArray()), title, MarkdownLanguages.Json); + } + } + + internal static class ForceInvariantDefaultCultureModuleInitializer + { + [ModuleInitializer] + internal static void Initialize() + { + // Compile errors are localized, so enforce a common default culture, + // since otherwise the snapshot comparison may fail + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; + } + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/WarmUpGeneratorTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/WarmUpGeneratorTests.cs new file mode 100644 index 00000000000..db002d20aaf --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/WarmUpGeneratorTests.cs @@ -0,0 +1,55 @@ +namespace Mocha.Analyzers.Tests; + +public class WarmUpGeneratorTests +{ + [Fact] + public async Task Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + // Void command + public record DeleteItemCommand(int Id) : ICommand; + + // Command with response + public record CreateItemCommand(string Name) : ICommand; + + // Query + public record GetItemQuery(int Id) : IQuery; + public record ItemDto(int Id, string Name); + + // Notification with single handler + public record ItemCreated(int Id) : INotification; + + // Handlers + public class DeleteItemHandler : ICommandHandler + { + public ValueTask HandleAsync(DeleteItemCommand command, CancellationToken cancellationToken) + => default; + } + + public class CreateItemHandler : ICommandHandler + { + public ValueTask HandleAsync(CreateItemCommand command, CancellationToken cancellationToken) + => new(1); + } + + public class GetItemHandler : IQueryHandler + { + public ValueTask HandleAsync(GetItemQuery query, CancellationToken cancellationToken) + => new(new ItemDto(1, "Test")); + } + + public class ItemCreatedHandler : INotificationHandler + { + public ValueTask HandleAsync(ItemCreated notification, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } +} diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md new file mode 100644 index 00000000000..fe5c2eaffdb --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md @@ -0,0 +1,39 @@ +# Generate_CommandWithResponseHandler_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.CreateOrderCommand), + ResponseType = typeof(int), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md new file mode 100644 index 00000000000..410dbeb877e --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md @@ -0,0 +1,45 @@ +# Generate_MultipleCommandHandlers_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.CreateOrderCommand), + ResponseType = typeof(int), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md new file mode 100644 index 00000000000..70db30bd4dc --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md @@ -0,0 +1,38 @@ +# Generate_VoidCommandHandler_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0001_CommandWithNoHandler_ReportsWarning.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0001_CommandWithNoHandler_ReportsWarning.md new file mode 100644 index 00000000000..4047a5ee284 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0001_CommandWithNoHandler_ReportsWarning.md @@ -0,0 +1,17 @@ +# MO0001_CommandWithNoHandler_ReportsWarning + +```json +[ + { + "Id": "MO0001", + "Title": "Missing handler for message type", + "Severity": "Warning", + "WarningLevel": 1, + "Location": ": (4,14)-(4,32)", + "MessageFormat": "Message type '{0}' has no registered handler", + "Message": "Message type 'global::TestApp.DeleteOrderCommand' has no registered handler", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0001_QueryWithNoHandler_ReportsWarning.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0001_QueryWithNoHandler_ReportsWarning.md new file mode 100644 index 00000000000..e42e399becc --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0001_QueryWithNoHandler_ReportsWarning.md @@ -0,0 +1,17 @@ +# MO0001_QueryWithNoHandler_ReportsWarning + +```json +[ + { + "Id": "MO0001", + "Title": "Missing handler for message type", + "Severity": "Warning", + "WarningLevel": 1, + "Location": ": (4,14)-(4,27)", + "MessageFormat": "Message type '{0}' has no registered handler", + "Message": "Message type 'global::TestApp.GetOrderQuery' has no registered handler", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md new file mode 100644 index 00000000000..9141fd0c4e9 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md @@ -0,0 +1,66 @@ +# MO0002_CommandWithTwoHandlers_ReportsError + +## TestsMediatorBuilderExtensions.kHkt5Slhw0EY-Xbr5K86dQ.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandlerA), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandlerB), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.CreateOrderCommand), + ResponseType = typeof(int), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.CreateOrderCommand), + ResponseType = typeof(int), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` + +## Generator Diagnostics + +```json +[ + { + "Id": "MO0002", + "Title": "Duplicate handler for message type", + "Severity": "Error", + "WarningLevel": 0, + "Location": ": (4,14)-(4,32)", + "MessageFormat": "Message type '{0}' has multiple handlers: {1}", + "Message": "Message type 'global::TestApp.CreateOrderCommand' has multiple handlers: global::TestApp.CreateOrderHandlerA, global::TestApp.CreateOrderHandlerB", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md new file mode 100644 index 00000000000..f6e58be366c --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md @@ -0,0 +1,64 @@ +# MO0002_VoidCommandWithTwoHandlers_ReportsError + +## TestsMediatorBuilderExtensions.kHkt5Slhw0EY-Xbr5K86dQ.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandlerA), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandlerB), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` + +## Generator Diagnostics + +```json +[ + { + "Id": "MO0002", + "Title": "Duplicate handler for message type", + "Severity": "Error", + "WarningLevel": 0, + "Location": ": (4,14)-(4,32)", + "MessageFormat": "Message type '{0}' has multiple handlers: {1}", + "Message": "Message type 'global::TestApp.DeleteOrderCommand' has multiple handlers: global::TestApp.DeleteOrderHandlerA, global::TestApp.DeleteOrderHandlerB", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0003_AbstractHandler_ReportsWarning.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0003_AbstractHandler_ReportsWarning.md new file mode 100644 index 00000000000..d7cfea648b5 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0003_AbstractHandler_ReportsWarning.md @@ -0,0 +1,28 @@ +# MO0003_AbstractHandler_ReportsWarning + +```json +[ + { + "Id": "MO0001", + "Title": "Missing handler for message type", + "Severity": "Warning", + "WarningLevel": 1, + "Location": ": (4,14)-(4,32)", + "MessageFormat": "Message type '{0}' has no registered handler", + "Message": "Message type 'global::TestApp.DeleteOrderCommand' has no registered handler", + "Category": "Mediator", + "CustomTags": [] + }, + { + "Id": "MO0003", + "Title": "Handler is abstract", + "Severity": "Warning", + "WarningLevel": 1, + "Location": ": (6,22)-(6,44)", + "MessageFormat": "Handler '{0}' is abstract and will not be registered", + "Message": "Handler 'global::TestApp.BaseDeleteOrderHandler' is abstract and will not be registered", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md new file mode 100644 index 00000000000..c46621cff0d --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md @@ -0,0 +1,58 @@ +# MO0004_OpenGenericCommand_ReportsInfo + +## TestsMediatorBuilderExtensions.kHkt5Slhw0EY-Xbr5K86dQ.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler>), typeof(global::TestApp.GenericCommandHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.GenericCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal>() + }); + }); + + return builder; + } + } +} + +``` + +## Generator Diagnostics + +```json +[ + { + "Id": "MO0004", + "Title": "Open generic message type cannot be dispatched", + "Severity": "Info", + "WarningLevel": 1, + "Location": ": (4,14)-(4,28)", + "MessageFormat": "Message type '{0}' is an open generic and cannot be dispatched at runtime", + "Message": "Message type 'global::TestApp.GenericCommand' is an open generic and cannot be dispatched at runtime", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md new file mode 100644 index 00000000000..0e179f97bda --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md @@ -0,0 +1,59 @@ +# MO0004_OpenGenericQuery_ReportsInfo + +## TestsMediatorBuilderExtensions.kHkt5Slhw0EY-Xbr5K86dQ.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler, T>), typeof(global::TestApp.GenericQueryHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.GenericQuery), + ResponseType = typeof(T), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal, T>() + }); + }); + + return builder; + } + } +} + +``` + +## Generator Diagnostics + +```json +[ + { + "Id": "MO0004", + "Title": "Open generic message type cannot be dispatched", + "Severity": "Info", + "WarningLevel": 1, + "Location": ": (4,14)-(4,26)", + "MessageFormat": "Message type '{0}' is an open generic and cannot be dispatched at runtime", + "Message": "Message type 'global::TestApp.GenericQuery' is an open generic and cannot be dispatched at runtime", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md new file mode 100644 index 00000000000..68a1e39dfd0 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md @@ -0,0 +1,38 @@ +# NoWarning_CommandWithHandler_NoDiagnostic + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md new file mode 100644 index 00000000000..430222bdaf2 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md @@ -0,0 +1,38 @@ +# Generate_ModuleWithOnlyName_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class Test2MediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTest2( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_GenericBaseHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_GenericBaseHandler_MatchesSnapshot.md new file mode 100644 index 00000000000..a8258e44089 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_GenericBaseHandler_MatchesSnapshot.md @@ -0,0 +1,28 @@ +# Generate_GenericBaseHandler_MatchesSnapshot + +```json +[ + { + "Id": "MO0003", + "Title": "Handler is abstract", + "Severity": "Warning", + "WarningLevel": 1, + "Location": ": (4,22)-(4,33)", + "MessageFormat": "Handler '{0}' is abstract and will not be registered", + "Message": "Handler 'global::TestApp.BaseHandler' is abstract and will not be registered", + "Category": "Mediator", + "CustomTags": [] + }, + { + "Id": "MO0001", + "Title": "Missing handler for message type", + "Severity": "Warning", + "WarningLevel": 1, + "Location": ": (10,14)-(10,23)", + "MessageFormat": "Message type '{0}' has no registered handler", + "Message": "Message type 'global::TestApp.MyCommand' has no registered handler", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md new file mode 100644 index 00000000000..3621633e3e4 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md @@ -0,0 +1,50 @@ +# Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.AlphaHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.MidHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.ZetaHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.AlphaCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.MidCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.ZetaCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md new file mode 100644 index 00000000000..3102c237869 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md @@ -0,0 +1,59 @@ +# Generate_OpenGenericCommand_MatchesSnapshot + +## TestsMediatorBuilderExtensions.kHkt5Slhw0EY-Xbr5K86dQ.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler, string>), typeof(global::TestApp.StringProcessor), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.ProcessCommand), + ResponseType = typeof(string), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal, string>() + }); + }); + + return builder; + } + } +} + +``` + +## Generator Diagnostics + +```json +[ + { + "Id": "MO0004", + "Title": "Open generic message type cannot be dispatched", + "Severity": "Info", + "WarningLevel": 1, + "Location": ": (4,14)-(4,28)", + "MessageFormat": "Message type '{0}' is an open generic and cannot be dispatched at runtime", + "Message": "Message type 'global::TestApp.ProcessCommand' is an open generic and cannot be dispatched at runtime", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md new file mode 100644 index 00000000000..7ba89aea38e --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md @@ -0,0 +1,38 @@ +# Generate_InternalHandler_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTest( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md new file mode 100644 index 00000000000..6821e52fcd6 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md @@ -0,0 +1,39 @@ +# Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.ComputeHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.ComputeCommand), + ResponseType = typeof(long), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md new file mode 100644 index 00000000000..3b4736118fb --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md @@ -0,0 +1,38 @@ +# Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.FireAndForgetHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.FireAndForgetCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md new file mode 100644 index 00000000000..f9e0b887e29 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md @@ -0,0 +1,60 @@ +# Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.ResponseCommandHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.VoidCommandHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.MyQueryHandler), lifetime)); + + // Register notification handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.MyEventHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.ResponseCommand), + ResponseType = typeof(int), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.VoidCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.MyQuery), + ResponseType = typeof(string), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.MyEvent), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::TestApp.MyEventHandler) }) + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithoutMochaUsings_NoHandlersRegistered_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithoutMochaUsings_NoHandlersRegistered_MatchesSnapshot.md new file mode 100644 index 00000000000..95a722a6e6e --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithoutMochaUsings_NoHandlersRegistered_MatchesSnapshot.md @@ -0,0 +1,2 @@ +# Generate_WithoutMochaUsings_NoHandlersRegistered_MatchesSnapshot + diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md new file mode 100644 index 00000000000..fe8de69d484 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md @@ -0,0 +1,39 @@ +# Generate_DefaultAssemblyName_PrefixesWithLastSegment + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetItemHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.GetItemQuery), + ResponseType = typeof(string), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md new file mode 100644 index 00000000000..43b370701c5 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md @@ -0,0 +1,38 @@ +# Generate_DottedAssemblyName_UsesLastSegment + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class OrderingMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddOrdering( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.PingHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.PingCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md new file mode 100644 index 00000000000..d59668ec973 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md @@ -0,0 +1,39 @@ +# Generate_ModuleFile_ContainsHandlerRegistrations + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateInvoiceHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.CreateInvoiceCommand), + ResponseType = typeof(int), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md new file mode 100644 index 00000000000..f21d20f3a66 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md @@ -0,0 +1,61 @@ +# Generate_AllHandlerTypes_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetUserHandler), lifetime)); + + // Register notification handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.OrderCreatedEmailHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.OrderCreatedStatsHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.CreateOrderCommand), + ResponseType = typeof(int), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.GetUserQuery), + ResponseType = typeof(global::TestApp.UserDto), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.OrderCreated), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::TestApp.OrderCreatedEmailHandler), typeof(global::TestApp.OrderCreatedStatsHandler) }) + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md new file mode 100644 index 00000000000..e650b0f3d7f --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md @@ -0,0 +1,46 @@ +# Generate_HandlersInDifferentNamespaces_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.Orders.CreateOrderHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.Users.GetUserHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.Orders.CreateOrderCommand), + ResponseType = typeof(int), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.Users.GetUserQuery), + ResponseType = typeof(global::TestApp.Users.UserDto), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_NoHandlers_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_NoHandlers_MatchesSnapshot.md new file mode 100644 index 00000000000..bfe3364345d --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_NoHandlers_MatchesSnapshot.md @@ -0,0 +1,2 @@ +# Generate_NoHandlers_MatchesSnapshot + diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md new file mode 100644 index 00000000000..1c863cb9cbb --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md @@ -0,0 +1,38 @@ +# Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class Order_ProcessingMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddOrder_Processing( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.PingHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.PingCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md new file mode 100644 index 00000000000..9a83b4cf1f1 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md @@ -0,0 +1,38 @@ +# Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class UnknownMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddUnknown( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.PingHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.PingCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md new file mode 100644 index 00000000000..053cf099324 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md @@ -0,0 +1,38 @@ +# Generate_NestedClassHandler_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.Outer.DeleteOrderHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md new file mode 100644 index 00000000000..6be1a697c41 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md @@ -0,0 +1,39 @@ +# Generate_MultipleHandlersForSameNotification_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register notification handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.SendEmailHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.UpdateStatsHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.OrderCreated), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::TestApp.SendEmailHandler), typeof(global::TestApp.UpdateStatsHandler) }) + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md new file mode 100644 index 00000000000..c835f34e560 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md @@ -0,0 +1,38 @@ +# Generate_SingleNotificationHandler_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register notification handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.OrderCreatedEmailHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.OrderCreated), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::TestApp.OrderCreatedEmailHandler) }) + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md new file mode 100644 index 00000000000..bbd99eb3a4b --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md @@ -0,0 +1,39 @@ +# Generate_PartialClassHandler_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.CreateOrderCommand), + ResponseType = typeof(int), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md new file mode 100644 index 00000000000..e63a3ad4784 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md @@ -0,0 +1,39 @@ +# Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetOrderQueryHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.GetOrderQuery), + ResponseType = typeof(string), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md new file mode 100644 index 00000000000..ae310054729 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md @@ -0,0 +1,38 @@ +# Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.ProcessOrderHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.ProcessOrderCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md new file mode 100644 index 00000000000..2b6727c1596 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md @@ -0,0 +1,46 @@ +# Generate_MultipleQueryHandlers_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetOrderHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetUserHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.GetOrderQuery), + ResponseType = typeof(global::TestApp.OrderDto), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.GetUserQuery), + ResponseType = typeof(global::TestApp.UserDto), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md new file mode 100644 index 00000000000..4da6331ab91 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md @@ -0,0 +1,39 @@ +# Generate_QueryHandler_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetUserHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.GetUserQuery), + ResponseType = typeof(global::TestApp.UserDto), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md new file mode 100644 index 00000000000..f9bf510dad3 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md @@ -0,0 +1,60 @@ +# Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateItemHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteItemHandler), lifetime)); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetItemHandler), lifetime)); + + // Register notification handlers + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.ItemCreatedHandler), lifetime)); + + // Register pipelines + global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.CreateItemCommand), + ResponseType = typeof(int), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.DeleteItemCommand), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.GetItemQuery), + ResponseType = typeof(global::TestApp.ItemDto), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + }); + b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + { + MessageType = typeof(global::TestApp.ItemCreated), + Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::TestApp.ItemCreatedHandler) }) + }); + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.EntityFrameworkCore.Tests/EntityFrameworkTransactionMiddlewareTests.cs b/src/Mocha/test/Mocha.EntityFrameworkCore.Tests/EntityFrameworkTransactionMiddlewareTests.cs new file mode 100644 index 00000000000..0ea3ffd8fa2 --- /dev/null +++ b/src/Mocha/test/Mocha.EntityFrameworkCore.Tests/EntityFrameworkTransactionMiddlewareTests.cs @@ -0,0 +1,359 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Mocha.Mediator; + +namespace Mocha.EntityFrameworkCore.Tests; + +public sealed class EntityFrameworkTransactionMiddlewareTests : IDisposable +{ + private readonly SqliteConnection _connection; + private readonly ServiceProvider _provider; + private readonly TestDbContext _dbContext; + + public EntityFrameworkTransactionMiddlewareTests() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + + var services = new ServiceCollection(); + + services.AddDbContext(o => o.UseSqlite(_connection)); + + var builder = services.AddMediator() + .UseEntityFrameworkTransactions(); + + services.AddTransient, CreateItemHandler>(); + services.AddTransient, CreateItemWithResponseHandler>(); + + // Register pipelines (normally done by source-generated code) + builder.ConfigureMediator(b => + { + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(CreateItemCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(CreateItemWithResponseCommand), + ResponseType = typeof(int), + Terminal = PipelineBuilder.BuildCommandTerminal() + }); + }); + + _provider = services.BuildServiceProvider(); + + _dbContext = _provider.GetRequiredService(); + _dbContext.Database.EnsureCreated(); + } + + [Fact] + public void UseEntityFrameworkTransactions_Registers_Runtime() + { + // Assert that the MediatorRuntime is available and has compiled pipelines + var runtime = _provider.GetRequiredService(); + + Assert.NotNull(runtime); + Assert.NotNull(runtime.GetPipeline(typeof(CreateItemCommand))); + } + + [Fact] + public async Task Commits_Transaction_On_Success() + { + var runtime = _provider.GetRequiredService(); + using var scope = _provider.CreateScope(); + + var context = runtime.RentContext(); + try + { + context.Services = scope.ServiceProvider; + context.Message = new CreateItemCommand("Test Item"); + context.MessageType = typeof(CreateItemCommand); + context.ResponseType = typeof(void); + context.CancellationToken = CancellationToken.None; + await runtime.GetPipeline(typeof(CreateItemCommand))(context); + } + finally + { + runtime.ReturnContext(context); + } + + // Verify the item was persisted (transaction committed) + var items = await _dbContext.Items.ToListAsync(); + Assert.Single(items); + Assert.Equal("Test Item", items[0].Name); + } + + [Fact] + public async Task Rolls_Back_Transaction_On_Failure() + { + // Register a handler that saves to DB then throws + var services = new ServiceCollection(); + services.AddDbContext(o => o.UseSqlite(_connection)); + var builder = services.AddMediator().UseEntityFrameworkTransactions(); + services.AddTransient, FailingCreateItemHandler>(); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(CreateItemCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + })); + + await using var provider = services.BuildServiceProvider(); + var runtime = provider.GetRequiredService(); + using var scope = provider.CreateScope(); + + var context = runtime.RentContext(); + try + { + context.Services = scope.ServiceProvider; + context.Message = new CreateItemCommand("Should Not Persist"); + context.MessageType = typeof(CreateItemCommand); + context.ResponseType = typeof(void); + context.CancellationToken = CancellationToken.None; + + await Assert.ThrowsAsync(() => + runtime.GetPipeline(typeof(CreateItemCommand))(context).AsTask()); + } + finally + { + runtime.ReturnContext(context); + } + + // Verify the item was NOT persisted (transaction rolled back) + var items = await _dbContext.Items.ToListAsync(); + Assert.Empty(items); + } + + [Fact] + public async Task Works_With_Response_Command() + { + var runtime = _provider.GetRequiredService(); + using var scope = _provider.CreateScope(); + + var context = runtime.RentContext(); + int id; + try + { + context.Services = scope.ServiceProvider; + context.Message = new CreateItemWithResponseCommand("Response Item"); + context.MessageType = typeof(CreateItemWithResponseCommand); + context.ResponseType = typeof(int); + context.CancellationToken = CancellationToken.None; + await runtime.GetPipeline(typeof(CreateItemWithResponseCommand))(context); + id = (int)context.Result!; + } + finally + { + runtime.ReturnContext(context); + } + + Assert.True(id > 0); + var item = await _dbContext.Items.FindAsync(id); + Assert.NotNull(item); + Assert.Equal("Response Item", item.Name); + } + + [Fact] + public async Task Mediator_Dispatches_VoidCommand_Through_Pipeline() + { + using var scope = _provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + await mediator.SendAsync(new CreateItemCommand("Via Mediator")); + + var items = await _dbContext.Items.ToListAsync(); + Assert.Single(items); + Assert.Equal("Via Mediator", items[0].Name); + } + + [Fact] + public async Task Mediator_Dispatches_ResponseCommand_Through_Pipeline() + { + using var scope = _provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + var id = await mediator.SendAsync(new CreateItemWithResponseCommand("Via Mediator Response")); + + Assert.True(id > 0); + var item = await _dbContext.Items.FindAsync(id); + Assert.NotNull(item); + Assert.Equal("Via Mediator Response", item.Name); + } + + [Fact] + public async Task Query_Skips_Transaction_By_Default() + { + var services = new ServiceCollection(); + services.AddDbContext(o => o.UseSqlite(_connection)); + var builder = services.AddMediator().UseEntityFrameworkTransactions(); + services.AddTransient>, GetItemsHandler>(); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(GetItemsQuery), + ResponseType = typeof(List), + Terminal = PipelineBuilder.BuildQueryTerminal>() + })); + + await using var provider = services.BuildServiceProvider(); + + using var scope = provider.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureCreated(); + db.Items.Add(new TestItem { Name = "Existing" }); + await db.SaveChangesAsync(); + + var runtime = provider.GetRequiredService(); + var context = runtime.RentContext(); + try + { + context.Services = scope.ServiceProvider; + context.Message = new GetItemsQuery(); + context.MessageType = typeof(GetItemsQuery); + context.ResponseType = typeof(List); + context.CancellationToken = CancellationToken.None; + + await runtime.GetPipeline(typeof(GetItemsQuery))(context); + + var result = (List)context.Result!; + Assert.Single(result); + + // No transaction was opened - verify the database has no active transaction + Assert.Null(db.Database.CurrentTransaction); + } + finally + { + runtime.ReturnContext(context); + } + } + + [Fact] + public async Task ShouldCreateTransaction_Override_Enables_Query_Transactions() + { + var services = new ServiceCollection(); + services.AddDbContext(o => o.UseSqlite(_connection)); + var builder = services.AddMediator() + .UseEntityFrameworkTransactions(options => + { + // Force transactions for all message types including queries + options.ShouldCreateTransaction = _ => true; + }); + + services.AddTransient>, GetItemsHandler>(); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(GetItemsQuery), + ResponseType = typeof(List), + Terminal = PipelineBuilder.BuildQueryTerminal>() + })); + + await using var provider = services.BuildServiceProvider(); + + using var scope = provider.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureCreated(); + + var runtime = provider.GetRequiredService(); + var context = runtime.RentContext(); + try + { + context.Services = scope.ServiceProvider; + context.Message = new GetItemsQuery(); + context.MessageType = typeof(GetItemsQuery); + context.ResponseType = typeof(List); + context.CancellationToken = CancellationToken.None; + + // Should not throw - transaction wrapping is enabled via delegate + await runtime.GetPipeline(typeof(GetItemsQuery))(context); + + var result = (List)context.Result!; + Assert.NotNull(result); + } + finally + { + runtime.ReturnContext(context); + } + } + + [Fact] + public async Task CancelledToken_Should_ThrowOperationCanceled_When_TransactionBegins() + { + using var scope = _provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act & Assert: the cancelled token should propagate to BeginTransactionAsync + // and result in an OperationCanceledException. + await Assert.ThrowsAnyAsync( + () => mediator.SendAsync(new CreateItemCommand("Should Not Persist"), cts.Token).AsTask()); + + // Verify nothing was persisted + var items = await _dbContext.Items.ToListAsync(); + Assert.Empty(items); + } + + public void Dispose() + { + _dbContext.Dispose(); + _provider.Dispose(); + _connection.Dispose(); + } +} + +public record CreateItemCommand(string Name) : ICommand; + +public record CreateItemWithResponseCommand(string Name) : ICommand; + +public class TestItem +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} + +public class TestDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Items => Set(); +} + +public class CreateItemHandler(TestDbContext db) : ICommandHandler +{ + public async ValueTask HandleAsync(CreateItemCommand command, CancellationToken cancellationToken) + { + db.Items.Add(new TestItem { Name = command.Name }); + await db.SaveChangesAsync(cancellationToken); + } +} + +public class CreateItemWithResponseHandler(TestDbContext db) : ICommandHandler +{ + public async ValueTask HandleAsync(CreateItemWithResponseCommand command, CancellationToken cancellationToken) + { + var item = new TestItem { Name = command.Name }; + db.Items.Add(item); + await db.SaveChangesAsync(cancellationToken); + return item.Id; + } +} + +public class FailingCreateItemHandler(TestDbContext db) : ICommandHandler +{ + public async ValueTask HandleAsync(CreateItemCommand command, CancellationToken cancellationToken) + { + db.Items.Add(new TestItem { Name = command.Name }); + await db.SaveChangesAsync(cancellationToken); + throw new InvalidOperationException("Simulated failure after save"); + } +} + +public record GetItemsQuery : IQuery>; + +public class GetItemsHandler(TestDbContext db) : IQueryHandler> +{ + public async ValueTask> HandleAsync(GetItemsQuery query, CancellationToken cancellationToken) + => await db.Items.ToListAsync(cancellationToken); +} diff --git a/src/Mocha/test/Mocha.EntityFrameworkCore.Tests/Mocha.EntityFrameworkCore.Tests.csproj b/src/Mocha/test/Mocha.EntityFrameworkCore.Tests/Mocha.EntityFrameworkCore.Tests.csproj new file mode 100644 index 00000000000..d2de026d6bb --- /dev/null +++ b/src/Mocha/test/Mocha.EntityFrameworkCore.Tests/Mocha.EntityFrameworkCore.Tests.csproj @@ -0,0 +1,13 @@ + + + Mocha.EntityFrameworkCore.Tests + Mocha.EntityFrameworkCore.Tests + + + + + + + + + diff --git a/src/Mocha/test/Mocha.Mediator.Tests/ContextPoolingTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/ContextPoolingTests.cs new file mode 100644 index 00000000000..530581303e8 --- /dev/null +++ b/src/Mocha/test/Mocha.Mediator.Tests/ContextPoolingTests.cs @@ -0,0 +1,231 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Tests; + +public sealed class ContextPoolingTests : IDisposable +{ + private readonly ServiceProvider _provider; + + public ContextPoolingTests() + { + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + // Void command + services.AddScoped, PoolTestCommandHandler>(); + + // Command with response + services.AddScoped, PoolTestCommandWithResponseHandler>(); + + // Nested-dispatch command: the handler dispatches another command internally + services.AddScoped, NestedOuterCommandHandler>(); + + // Context-capturing command + services.AddScoped, ContextCaptureCommandHandler>(); + + // Register middleware that captures context references for nested-dispatch test + // and context-field verification test. Since Use() applies to all pipelines, + // both NestedOuterCommand and PoolTestCommand pipelines get this middleware. + builder.Use(new MediatorMiddlewareConfiguration( + (_, next) => + { + return ctx => + { + // Capture for nested dispatch test + if (ctx.Message is NestedOuterCommand) + { + NestedOuterCommandHandler.OuterContextRef = ctx; + } + else if (ctx.Message is PoolTestCommand + && NestedOuterCommandHandler.OuterContextRef is not null + && NestedOuterCommandHandler.InnerContextRef is null) + { + NestedOuterCommandHandler.InnerContextRef = ctx; + } + + // Capture for context-field verification test + if (ctx.Message is ContextCaptureCommand) + { + ContextCapture.CapturedServices = ctx.Services; + ContextCapture.CapturedMessage = ctx.Message; + ContextCapture.CapturedMessageType = ctx.MessageType; + ContextCapture.CapturedCancellationToken = ctx.CancellationToken; + } + + return next(ctx); + }; + }, + "TestCapture")); + + builder.ConfigureMediator(b => + { + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PoolTestCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PoolTestCommandWithResponse), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(NestedOuterCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(ContextCaptureCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + }); + }); + + _provider = services.BuildServiceProvider(); + } + + [Fact] + public async Task SendAsync_Should_IsolateContextPerThread_When_DispatchedConcurrently() + { + // Arrange: dispatch from multiple threads simultaneously and capture the + // response to verify no cross-contamination of Message/Result. + + const int threadCount = 8; + var barrier = new Barrier(threadCount); + var capturedMessages = new string[threadCount]; + var tasks = new Task[threadCount]; + + for (var i = 0; i < threadCount; i++) + { + var index = i; + tasks[i] = Task.Factory.StartNew(async () => + { + using var scope = _provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Synchronize all threads to maximize contention + barrier.SignalAndWait(); + + var result = await mediator.SendAsync( + new PoolTestCommandWithResponse($"thread-{index}")); + capturedMessages[index] = result; + }, TaskCreationOptions.LongRunning).Unwrap(); + } + + await Task.WhenAll(tasks); + + // Assert: each thread got its own message value without cross-contamination. + for (var i = 0; i < threadCount; i++) + { + Assert.Equal($"thread-{i}", capturedMessages[i]); + } + } + + [Fact] + public async Task SendAsync_Should_UseDifferentContext_When_DispatchedFromInsideHandler() + { + // Reset the captured references from any prior test runs. + NestedOuterCommandHandler.OuterContextRef = null; + NestedOuterCommandHandler.InnerContextRef = null; + + // Arrange: the NestedOuterCommand handler dispatches PoolTestCommand internally. + // The outer context is still rented, so the inner dispatch must get a different context. + using var scope = _provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + await mediator.SendAsync(new NestedOuterCommand(mediator)); + + // Assert: two distinct context references were captured. + Assert.NotNull(NestedOuterCommandHandler.OuterContextRef); + Assert.NotNull(NestedOuterCommandHandler.InnerContextRef); + Assert.NotSame( + NestedOuterCommandHandler.OuterContextRef, + NestedOuterCommandHandler.InnerContextRef); + } + + [Fact] + public async Task SendAsync_Should_PopulateContextFields_When_PipelineExecutes() + { + // Arrange + ContextCapture.Reset(); + using var cts = new CancellationTokenSource(); + using var scope = _provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + var command = new ContextCaptureCommand("payload"); + + // Act + await mediator.SendAsync(command, cts.Token); + + // Assert: the capturing middleware recorded the correct context fields. + Assert.Same(scope.ServiceProvider, ContextCapture.CapturedServices); + Assert.Same(command, ContextCapture.CapturedMessage); + Assert.Equal(typeof(ContextCaptureCommand), ContextCapture.CapturedMessageType); + Assert.Equal(cts.Token, ContextCapture.CapturedCancellationToken); + } + + public void Dispose() + { + _provider.Dispose(); + } +} + +public sealed record PoolTestCommand(string Value) : ICommand; + +public sealed record PoolTestCommandWithResponse(string Value) : ICommand; + +public sealed record NestedOuterCommand(IMediator Mediator) : ICommand; + +public sealed record ContextCaptureCommand(string Value) : ICommand; + +public sealed class PoolTestCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(PoolTestCommand command, CancellationToken cancellationToken) + => default; +} + +public sealed class PoolTestCommandWithResponseHandler : ICommandHandler +{ + public ValueTask HandleAsync(PoolTestCommandWithResponse command, CancellationToken cancellationToken) + => new(command.Value); +} + +public sealed class NestedOuterCommandHandler : ICommandHandler +{ + public static object? OuterContextRef; + public static object? InnerContextRef; + + public async ValueTask HandleAsync(NestedOuterCommand command, CancellationToken cancellationToken) + { + // Outer context is still rented. Inner dispatch must get a different context. + await command.Mediator.SendAsync(new PoolTestCommand("inner")); + } +} + +public sealed class ContextCaptureCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(ContextCaptureCommand command, CancellationToken cancellationToken) + => new("captured"); +} + +/// +/// Static holder for context fields captured by middleware during pipeline execution. +/// +internal static class ContextCapture +{ + public static IServiceProvider? CapturedServices; + public static object? CapturedMessage; + public static Type? CapturedMessageType; + public static CancellationToken CapturedCancellationToken; + + public static void Reset() + { + CapturedServices = null; + CapturedMessage = null; + CapturedMessageType = null; + CapturedCancellationToken = default; + } +} diff --git a/src/Mocha/test/Mocha.Mediator.Tests/InstrumentationTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/InstrumentationTests.cs new file mode 100644 index 00000000000..c87f58a8808 --- /dev/null +++ b/src/Mocha/test/Mocha.Mediator.Tests/InstrumentationTests.cs @@ -0,0 +1,202 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Tests; + +public sealed class InstrumentationTests : IDisposable +{ + private readonly ServiceProvider _provider; + private readonly TestDiagnosticListener _listener = new(); + + public InstrumentationTests() + { + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + services.AddScoped, InstrumentedCommandHandler>(); + services.AddScoped, InstrumentedThrowingCommandHandler>(); + + var listener = _listener; + builder.ConfigureMediator(b => + { + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(InstrumentedCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + }); + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(InstrumentedThrowingCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + // Register test listener via the builder's internal services. + builder.AddDiagnosticEventListener(listener); + + _provider = services.BuildServiceProvider(); + } + + [Fact] + public async Task SendAsync_Should_InvokeDiagnosticListener_When_HandlerSucceeds() + { + // Arrange + using var scope = _provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + _listener.Reset(); + + // Act + await mediator.SendAsync(new InstrumentedCommand("test-value")); + + // Assert + Assert.True(_listener.ExecuteCalled); + Assert.Equal(typeof(InstrumentedCommand), _listener.CapturedMessageType); + Assert.IsType(_listener.CapturedMessage); + Assert.True(_listener.ScopeDisposed); + } + + [Fact] + public async Task SendAsync_Should_InvokeDiagnosticListenerWithError_When_HandlerThrows() + { + // Arrange + using var scope = _provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + _listener.Reset(); + + // Act + var ex = await Assert.ThrowsAsync( + () => mediator.SendAsync(new InstrumentedThrowingCommand("boom")).AsTask()); + + // Assert + Assert.True(_listener.ExecuteCalled); + Assert.True(_listener.ExecutionErrorCalled); + Assert.Same(ex, _listener.CapturedException); + Assert.Equal(typeof(InstrumentedThrowingCommand), _listener.ErrorMessageType); + Assert.True(_listener.ScopeDisposed); + } + + [Fact] + public async Task SendAsync_Should_InvokeAllListeners_When_MultipleListenersRegistered() + { + // Arrange + var first = new TestDiagnosticListener(); + var second = new SecondTestDiagnosticListener(); + + var services = new ServiceCollection(); + var builder = services.AddMediator(); + services.AddScoped, InstrumentedCommandHandler>(); + + builder.ConfigureMediator(b => + { + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(InstrumentedCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + builder.AddDiagnosticEventListener(first); + builder.AddDiagnosticEventListener(second); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + first.Reset(); + second.Reset(); + + // Act + await mediator.SendAsync(new InstrumentedCommand("multi")); + + // Assert + Assert.True(first.ExecuteCalled); + Assert.True(first.ScopeDisposed); + Assert.True(second.ExecuteCalled); + Assert.True(second.ScopeDisposed); + } + + public void Dispose() + { + _provider.Dispose(); + } +} + +public sealed record InstrumentedCommand(string Value) : ICommand; + +public sealed record InstrumentedThrowingCommand(string Value) : ICommand; + +public sealed class InstrumentedCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(InstrumentedCommand command, CancellationToken cancellationToken) + => default; +} + +public sealed class InstrumentedThrowingCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(InstrumentedThrowingCommand command, CancellationToken cancellationToken) + => throw new InvalidOperationException("instrumented-error"); +} + +public sealed class TestDiagnosticListener : MediatorDiagnosticEventListener +{ + public bool ExecuteCalled { get; private set; } + public bool ExecutionErrorCalled { get; private set; } + public bool ScopeDisposed { get; private set; } + public Type? CapturedMessageType { get; private set; } + public object? CapturedMessage { get; private set; } + public Type? ErrorMessageType { get; private set; } + public Exception? CapturedException { get; private set; } + + public void Reset() + { + ExecuteCalled = false; + ExecutionErrorCalled = false; + ScopeDisposed = false; + CapturedMessageType = null; + CapturedMessage = null; + ErrorMessageType = null; + CapturedException = null; + } + + public override IDisposable Execute(Type messageType, Type? responseType, object message) + { + ExecuteCalled = true; + CapturedMessageType = messageType; + CapturedMessage = message; + return new CallbackDisposable(() => ScopeDisposed = true); + } + + public override void ExecutionError(Type messageType, Type? responseType, object message, Exception exception) + { + ExecutionErrorCalled = true; + ErrorMessageType = messageType; + CapturedException = exception; + } + + private sealed class CallbackDisposable(Action onDispose) : IDisposable + { + public void Dispose() => onDispose(); + } +} + +public sealed class SecondTestDiagnosticListener : MediatorDiagnosticEventListener +{ + public bool ExecuteCalled { get; private set; } + public bool ScopeDisposed { get; private set; } + + public void Reset() + { + ExecuteCalled = false; + ScopeDisposed = false; + } + + public override IDisposable Execute(Type messageType, Type? responseType, object message) + { + ExecuteCalled = true; + return new CallbackDisposable(() => ScopeDisposed = true); + } + + private sealed class CallbackDisposable(Action onDispose) : IDisposable + { + public void Dispose() => onDispose(); + } +} diff --git a/src/Mocha/test/Mocha.Mediator.Tests/MediatorDispatchTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/MediatorDispatchTests.cs new file mode 100644 index 00000000000..237f1d20bcb --- /dev/null +++ b/src/Mocha/test/Mocha.Mediator.Tests/MediatorDispatchTests.cs @@ -0,0 +1,199 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Tests; + +public class MediatorDispatchTests +{ + [Fact] + public async Task SendAsync_Should_DispatchToHandler_When_VoidCommandSent() + { + // Arrange + DispatchVoidCommandHandler.WasInvoked = false; + var sp = DispatchTestHelper.BuildDefaultProvider(); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + await mediator.SendAsync(new DispatchVoidCommand("hello")); + + // Assert + Assert.True(DispatchVoidCommandHandler.WasInvoked); + } + + [Fact] + public async Task SendAsync_Should_ThrowInvalidOperationException_When_PipelineNotRegistered() + { + // Arrange - register mediator but no pipelines + var sp = DispatchTestHelper.BuildProvider((_, _) => { }); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act & Assert + await Assert.ThrowsAsync( + () => mediator.SendAsync(new DispatchVoidCommand("missing")).AsTask()); + } + + [Fact] + public async Task SendAsync_Should_PropagateException_When_HandlerThrows() + { + // Arrange + var sp = DispatchTestHelper.BuildDefaultProvider(); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => mediator.SendAsync(new DispatchThrowingCommand("boom")).AsTask()); + Assert.Equal("handler-exploded", ex.Message); + } + + [Fact] + public async Task SendAsync_Should_PassCancellationTokenToHandler_When_TokenProvided() + { + // Arrange + DispatchTokenCapturingHandler.CapturedToken = default; + var sp = DispatchTestHelper.BuildDefaultProvider(); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + using var cts = new CancellationTokenSource(); + var token = cts.Token; + + // Act + await mediator.SendAsync(new DispatchTokenCapturingCommand(), token); + + // Assert + Assert.Equal(token, DispatchTokenCapturingHandler.CapturedToken); + } + + [Fact] + public async Task SendAsync_Should_ReturnCorrectResponse_When_CommandWithResponseSent() + { + // Arrange + var sp = DispatchTestHelper.BuildDefaultProvider(); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + var result = await mediator.SendAsync(new DispatchCommand("test")); + + // Assert + Assert.NotNull(result); + Assert.Equal("handled", result.Data); + } + + [Fact] + public async Task SendAsync_Should_AwaitAsyncHandler_When_HandlerYields() + { + // Arrange - DispatchAsyncCommandHandler uses Task.Yield() so it's truly async + var sp = DispatchTestHelper.BuildDefaultProvider(); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + var result = await mediator.SendAsync(new DispatchAsyncCommand("async")); + + // Assert + Assert.NotNull(result); + Assert.Equal("async-result", result.Data); + } + + [Fact] + public async Task QueryAsync_Should_ReturnCorrectResponse_When_QueryDispatched() + { + // Arrange + var sp = DispatchTestHelper.BuildDefaultProvider(); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + var result = await mediator.QueryAsync(new DispatchQuery(42)); + + // Assert + Assert.NotNull(result); + Assert.Equal("query-result", result.Data); + } + + [Fact] + public async Task PublishAsync_Should_InvokeHandler_When_NotificationPublished() + { + // Arrange + DispatchNotificationHandler.WasInvoked = false; + var sp = DispatchTestHelper.BuildDefaultProvider(); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + await mediator.PublishAsync(new DispatchNotification("ping")); + + // Assert + Assert.True(DispatchNotificationHandler.WasInvoked); + } + + [Fact] + public async Task PublishAsync_Should_InvokeAllHandlers_When_MultipleHandlersRegistered() + { + // Arrange + DispatchNotificationHandler.WasInvoked = false; + DispatchSecondNotificationHandler.WasInvoked = false; + var sp = DispatchTestHelper.BuildMultiNotificationProvider(); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + await mediator.PublishAsync(new DispatchNotification("fan-out")); + + // Assert + Assert.True(DispatchNotificationHandler.WasInvoked); + Assert.True(DispatchSecondNotificationHandler.WasInvoked); + } + + [Fact] + public async Task SendAsync_Should_ThrowInvalidOperationException_When_NoPipelinesRegistered() + { + // Arrange - no pipelines registered + var sp = DispatchTestHelper.BuildProvider((_, _) => { }); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act & Assert + await Assert.ThrowsAsync( + () => mediator.SendAsync(new DispatchCommand("missing")).AsTask()); + await Assert.ThrowsAsync( + () => mediator.QueryAsync(new DispatchQuery(1)).AsTask()); + await Assert.ThrowsAsync( + () => mediator.PublishAsync(new DispatchNotification("missing")).AsTask()); + } + + [Fact] + public async Task SendAsync_Should_DispatchAndReturnResult_When_CalledViaUntypedISender() + { + // Arrange + var sp = DispatchTestHelper.BuildDefaultProvider(); + using var scope = sp.CreateScope(); + var sender = scope.ServiceProvider.GetRequiredService(); + + // Act - dispatch a command-with-response via the untyped overload + var result = await sender.SendAsync((object)new DispatchCommand("untyped")); + + // Assert + Assert.NotNull(result); + var response = Assert.IsType(result); + Assert.Equal("handled", response.Data); + } + + [Fact] + public async Task PublishAsync_Should_DispatchCorrectly_When_CalledViaUntypedIPublisher() + { + // Arrange + DispatchNotificationHandler.WasInvoked = false; + var sp = DispatchTestHelper.BuildDefaultProvider(); + using var scope = sp.CreateScope(); + var publisher = scope.ServiceProvider.GetRequiredService(); + + // Act + await publisher.PublishAsync((object)new DispatchNotification("untyped")); + + // Assert + Assert.True(DispatchNotificationHandler.WasInvoked); + } +} diff --git a/src/Mocha/test/Mocha.Mediator.Tests/MiddlewareFactoryContextTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/MiddlewareFactoryContextTests.cs new file mode 100644 index 00000000000..d54ac19634d --- /dev/null +++ b/src/Mocha/test/Mocha.Mediator.Tests/MiddlewareFactoryContextTests.cs @@ -0,0 +1,555 @@ +using Microsoft.Extensions.DependencyInjection; +using Mocha.Mediator; + +namespace Mocha.Mediator.Tests; + +public sealed class MiddlewareFactoryContextTests +{ + [Fact] + public async Task FactoryContext_Should_ExposeCorrectTypes_When_CommandWithResponse() + { + // Arrange + Type? capturedMessageType = null; + Type? capturedResponseType = null; + + var mediator = BuildMediator( + CaptureTypesMiddleware(t => capturedMessageType = t, t => capturedResponseType = t), + registerCommand: true); + + // Act + await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.Equal(typeof(PipelineTestCommand), capturedMessageType); + Assert.Equal(typeof(string), capturedResponseType); + } + + [Fact] + public async Task FactoryContext_Should_HaveNullResponseType_When_VoidCommand() + { + // Arrange + Type? capturedMessageType = null; + Type? capturedResponseType = typeof(object); // sentinel to verify it's set to null + + var mediator = BuildMediator( + CaptureTypesMiddleware(t => capturedMessageType = t, t => capturedResponseType = t), + registerVoidCommand: true); + + // Act + await mediator.SendAsync(new CtxTestVoidCommand()); + + // Assert + Assert.Equal(typeof(CtxTestVoidCommand), capturedMessageType); + Assert.Null(capturedResponseType); + } + + [Fact] + public async Task FactoryContext_Should_ExposeCorrectTypes_When_Query() + { + // Arrange + Type? capturedMessageType = null; + Type? capturedResponseType = null; + + var mediator = BuildMediator( + CaptureTypesMiddleware(t => capturedMessageType = t, t => capturedResponseType = t), + registerQuery: true); + + // Act + await mediator.QueryAsync(new CtxTestQuery()); + + // Assert + Assert.Equal(typeof(CtxTestQuery), capturedMessageType); + Assert.Equal(typeof(int), capturedResponseType); + } + + [Fact] + public async Task FactoryContext_Should_HaveNullResponseType_When_Notification() + { + // Arrange + Type? capturedMessageType = null; + Type? capturedResponseType = typeof(object); + + var mediator = BuildMediator( + CaptureTypesMiddleware(t => capturedMessageType = t, t => capturedResponseType = t), + registerNotification: true); + + // Act + await mediator.PublishAsync(new CtxTestNotification()); + + // Assert + Assert.Equal(typeof(CtxTestNotification), capturedMessageType); + Assert.Null(capturedResponseType); + } + + [Fact] + public async Task IsCommand_Should_ReturnTrue_When_VoidCommand() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsCommand()), + registerVoidCommand: true); + + // Act + await mediator.SendAsync(new CtxTestVoidCommand()); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task IsCommandWithResponse_Should_ReturnTrue_When_CommandWithResponse() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsCommandWithResponse()), + registerCommand: true); + + // Act + await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task IsQuery_Should_ReturnTrue_When_Query_And_ReturnFalse_When_Command() + { + // Arrange + bool? queryResult = null; + bool? commandResult = null; + + var queryMiddleware = CheckMiddleware(ctx => queryResult = ctx.IsQuery()); + var commandMiddleware = CheckMiddleware(ctx => commandResult = ctx.IsQuery()); + + var mediator1 = BuildMediator(queryMiddleware, registerQuery: true); + var mediator2 = BuildMediator(commandMiddleware, registerCommand: true); + + // Act + await mediator1.QueryAsync(new CtxTestQuery()); + await mediator2.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.True(queryResult); + Assert.False(commandResult); + } + + [Fact] + public async Task IsNotification_Should_ReturnTrue_When_Notification() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsNotification()), + registerNotification: true); + + // Act + await mediator.PublishAsync(new CtxTestNotification()); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task IsCommand_Should_ReturnFalse_When_Query() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsCommand()), + registerQuery: true); + + // Act + await mediator.QueryAsync(new CtxTestQuery()); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task IsCommandWithResponse_Should_ReturnFalse_When_VoidCommand() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsCommandWithResponse()), + registerVoidCommand: true); + + // Act + await mediator.SendAsync(new CtxTestVoidCommand()); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task IsNotification_Should_ReturnFalse_When_Command() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsNotification()), + registerCommand: true); + + // Act + await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task IsMessageAssignableTo_Should_ReturnTrue_When_MatchingGenericType() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsMessageAssignableTo()), + registerCommand: true); + + // Act + await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task IsMessageAssignableTo_Should_ReturnFalse_When_NonMatchingGenericType() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsMessageAssignableTo()), + registerCommand: true); + + // Act + await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task IsMessageAssignableTo_Should_ReturnTrue_When_MatchingType() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsMessageAssignableTo(typeof(PipelineTestCommand))), + registerCommand: true); + + // Act + await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task IsResponseAssignableTo_Should_ReturnTrue_When_MatchingType() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsResponseAssignableTo(typeof(string))), + registerCommand: true); + + // Act + await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task IsResponseAssignableTo_Should_ReturnFalse_When_NonMatchingType() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsResponseAssignableTo(typeof(int))), + registerCommand: true); + + // Act + await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task IsResponseAssignableTo_Should_ReturnFalse_When_VoidCommand() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsResponseAssignableTo()), + registerVoidCommand: true); + + // Act + await mediator.SendAsync(new CtxTestVoidCommand()); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task IsResponseAssignableTo_Should_ReturnFalse_When_Notification() + { + // Arrange + bool? result = null; + + var mediator = BuildMediator( + CheckMiddleware(ctx => result = ctx.IsResponseAssignableTo()), + registerNotification: true); + + // Act + await mediator.PublishAsync(new CtxTestNotification()); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task Use_Should_SkipMiddleware_When_MessageTypeDoesNotMatch() + { + // Arrange + var middlewareExecuted = false; + + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + services.AddScoped, PipelineTestCommandHandler>(); + services.AddScoped, CtxTestVoidCommandHandler>(); + + // Middleware that only applies to void commands + builder.Use(new MediatorMiddlewareConfiguration( + (factoryCtx, next) => + { + if (!factoryCtx.IsCommand() || factoryCtx.IsCommandWithResponse()) + return next; // Opt out at compile time + + return ctx => + { + middlewareExecuted = true; + return next(ctx); + }; + }, + "VoidCommandOnly")); + + builder.ConfigureMediator(b => + { + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PipelineTestCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(CtxTestVoidCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + }); + }); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act - send command with response (should NOT trigger middleware) + await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.False(middlewareExecuted); + + // Act - send void command (should trigger middleware) + await mediator.SendAsync(new CtxTestVoidCommand()); + + // Assert + Assert.True(middlewareExecuted); + } + + [Fact] + public async Task Use_Should_SkipMiddleware_When_ResponseTypeDoesNotMatch() + { + // Arrange + var middlewareExecuted = false; + + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + services.AddScoped, PipelineTestCommandHandler>(); + services.AddScoped, CtxTestQueryHandler>(); + + // Middleware that only applies when response is int + builder.Use(new MediatorMiddlewareConfiguration( + (factoryCtx, next) => + { + if (!factoryCtx.IsResponseAssignableTo()) + return next; + + return ctx => + { + middlewareExecuted = true; + return next(ctx); + }; + }, + "IntResponseOnly")); + + builder.ConfigureMediator(b => + { + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PipelineTestCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + }); + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(CtxTestQuery), + ResponseType = typeof(int), + Terminal = PipelineBuilder.BuildQueryTerminal() + }); + }); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act - send command with string response (should NOT trigger) + await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.False(middlewareExecuted); + + // Act - send query with int response (should trigger) + await mediator.QueryAsync(new CtxTestQuery()); + + // Assert + Assert.True(middlewareExecuted); + } + + /// + /// Creates a middleware that captures MessageType and ResponseType from the factory context. + /// + private static MediatorMiddlewareConfiguration CaptureTypesMiddleware( + Action onMessageType, + Action onResponseType) + => new( + (factoryCtx, next) => + { + onMessageType(factoryCtx.MessageType); + onResponseType(factoryCtx.ResponseType); + return next; + }); + + /// + /// Creates a middleware that runs a check on the factory context during compilation. + /// + private static MediatorMiddlewareConfiguration CheckMiddleware( + Action check) + => new( + (factoryCtx, next) => + { + check(factoryCtx); + return next; + }); + + private static IMediator BuildMediator( + MediatorMiddlewareConfiguration middleware, + bool registerCommand = false, + bool registerVoidCommand = false, + bool registerQuery = false, + bool registerNotification = false) + { + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + builder.Use(middleware); + + if (registerCommand) + { + services.AddScoped, PipelineTestCommandHandler>(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PipelineTestCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + } + + if (registerVoidCommand) + { + services.AddScoped, CtxTestVoidCommandHandler>(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(CtxTestVoidCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + })); + } + + if (registerQuery) + { + services.AddScoped, CtxTestQueryHandler>(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(CtxTestQuery), + ResponseType = typeof(int), + Terminal = PipelineBuilder.BuildQueryTerminal() + })); + } + + if (registerNotification) + { + services.AddScoped(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(CtxTestNotification), + Terminal = PipelineBuilder.BuildNotificationTerminal( + [typeof(CtxTestNotificationHandler)]) + })); + } + + var provider = services.BuildServiceProvider(); + var scope = provider.CreateScope(); + return scope.ServiceProvider.GetRequiredService(); + } +} + +public sealed record CtxTestVoidCommand : ICommand; + +public sealed class CtxTestVoidCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(CtxTestVoidCommand command, CancellationToken cancellationToken) + => default; +} + +public sealed record CtxTestQuery : IQuery; + +public sealed class CtxTestQueryHandler : IQueryHandler +{ + public ValueTask HandleAsync(CtxTestQuery query, CancellationToken cancellationToken) + => new(42); +} + +public sealed record CtxTestNotification : INotification; + +public sealed class CtxTestNotificationHandler : INotificationHandler +{ + public ValueTask HandleAsync(CtxTestNotification notification, CancellationToken cancellationToken) + => default; +} diff --git a/src/Mocha/test/Mocha.Mediator.Tests/MiddlewarePipelineTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/MiddlewarePipelineTests.cs new file mode 100644 index 00000000000..a357147c4be --- /dev/null +++ b/src/Mocha/test/Mocha.Mediator.Tests/MiddlewarePipelineTests.cs @@ -0,0 +1,372 @@ +using Microsoft.Extensions.DependencyInjection; +using Mocha.Mediator; + +namespace Mocha.Mediator.Tests; + +public sealed class MiddlewarePipelineTests : IDisposable +{ + private readonly ServiceProvider _provider; + + public MiddlewarePipelineTests() + { + var services = new ServiceCollection(); + + var builder = services.AddMediator(); + + services.AddScoped, PipelineTestCommandHandler>(); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PipelineTestCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + _provider = services.BuildServiceProvider(); + } + + [Fact] + public async Task SendAsync_Should_ExecuteMiddlewareInRegistrationOrder_When_MultipleMiddlewareRegistered() + { + // Arrange + var log = new List(); + var services = new ServiceCollection(); + + var builder = services.AddMediator(); + + services.AddScoped, PipelineTestCommandHandler>(); + + builder + .Use(new MediatorMiddlewareConfiguration( + (factoryCtx, next) => ctx => + { + log.Add("MW1-pre"); + var task = next(ctx); + if (task.IsCompletedSuccessfully) + { + log.Add("MW1-post"); + return default; + } + + return Awaited(task, log); + + static async ValueTask Awaited(ValueTask t, List l) + { + await t.ConfigureAwait(false); + l.Add("MW1-post"); + } + }, + "MW1")) + .Use(new MediatorMiddlewareConfiguration( + (factoryCtx, next) => ctx => + { + log.Add("MW2-pre"); + var task = next(ctx); + if (task.IsCompletedSuccessfully) + { + log.Add("MW2-post"); + return default; + } + + return Awaited(task, log); + + static async ValueTask Awaited(ValueTask t, List l) + { + await t.ConfigureAwait(false); + l.Add("MW2-post"); + } + }, + "MW2")) + .Use(new MediatorMiddlewareConfiguration( + (factoryCtx, next) => ctx => + { + log.Add("MW3-pre"); + var task = next(ctx); + if (task.IsCompletedSuccessfully) + { + log.Add("MW3-post"); + return default; + } + + return Awaited(task, log); + + static async ValueTask Awaited(ValueTask t, List l) + { + await t.ConfigureAwait(false); + l.Add("MW3-post"); + } + }, + "MW3")); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PipelineTestCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + var result = await mediator.SendAsync(new PipelineTestCommand("order-test")); + + // Assert + Assert.Equal("handled:order-test", result); + Assert.Equal( + new[] { "MW1-pre", "MW2-pre", "MW3-pre", "MW3-post", "MW2-post", "MW1-post" }, + log); + } + + [Fact] + public async Task SendAsync_Should_ExposeContextProperties_When_MiddlewareReadsContext() + { + // Arrange + object? capturedMessage = null; + Type? capturedMessageType = null; + CancellationToken capturedToken = default; + + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + services.AddScoped, PipelineTestCommandHandler>(); + + builder.Use(new MediatorMiddlewareConfiguration( + (factoryCtx, next) => ctx => + { + capturedMessage = ctx.Message; + capturedMessageType = ctx.MessageType; + capturedToken = ctx.CancellationToken; + return next(ctx); + }, + "ContextReader")); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PipelineTestCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + using var cts = new CancellationTokenSource(); + var command = new PipelineTestCommand("context-check"); + + // Act + await mediator.SendAsync(command, cts.Token); + + // Assert + Assert.Same(command, capturedMessage); + Assert.Equal(typeof(PipelineTestCommand), capturedMessageType); + Assert.Equal(cts.Token, capturedToken); + } + + [Fact] + public async Task SendAsync_Should_ReturnModifiedResult_When_MiddlewareModifiesResultAfterNext() + { + // Arrange + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + services.AddScoped, PipelineTestCommandHandler>(); + + builder.Use(new MediatorMiddlewareConfiguration( + (factoryCtx, next) => ctx => + { + var task = next(ctx); + if (task.IsCompletedSuccessfully) + { + ctx.Result = (string)ctx.Result! + "-modified"; + return default; + } + + return Awaited(task, ctx); + + static async ValueTask Awaited(ValueTask t, IMediatorContext c) + { + await t.ConfigureAwait(false); + c.Result = (string)c.Result! + "-modified"; + } + }, + "ResultModifier")); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PipelineTestCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + var result = await mediator.SendAsync(new PipelineTestCommand("test")); + + // Assert + Assert.Equal("handled:test-modified", result); + } + + [Fact] + public async Task SendAsync_Should_PropagateExceptionThroughMiddleware_When_HandlerThrows() + { + // Arrange + var middlewareSawException = false; + + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + services.AddScoped, PipelineThrowingHandler>(); + + builder.Use(new MediatorMiddlewareConfiguration( + (factoryCtx, next) => async ctx => + { + try + { + await next(ctx).ConfigureAwait(false); + } + catch (InvalidOperationException) + { + middlewareSawException = true; + throw; + } + }, + "ExceptionObserver")); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PipelineTestCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act & Assert + await Assert.ThrowsAsync( + () => mediator.SendAsync(new PipelineTestCommand("boom")).AsTask()); + + Assert.True(middlewareSawException); + } + + [Fact] + public async Task SendAsync_Should_PropagateExceptionAndSkipHandler_When_MiddlewareThrows() + { + // Arrange + var handlerInvoked = false; + + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + services.AddScoped>( + _ => new PipelineTrackingHandler(() => handlerInvoked = true)); + + builder.Use(new MediatorMiddlewareConfiguration( + (factoryCtx, next) => ctx => + throw new ApplicationException("middleware failure"), + "FailingMiddleware")); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PipelineTestCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => mediator.SendAsync(new PipelineTestCommand("never-handled")).AsTask()); + + Assert.Equal("middleware failure", ex.Message); + Assert.False(handlerInvoked); + } + + [Fact] + public async Task SendAsync_Should_ReturnShortCircuitResult_When_MiddlewareSkipsNext() + { + // Arrange + var handlerInvoked = false; + + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + services.AddScoped>( + _ => new PipelineTrackingHandler(() => handlerInvoked = true)); + + builder.Use(new MediatorMiddlewareConfiguration( + (factoryCtx, next) => ctx => + { + ctx.Result = "short-circuited"; + return default; + }, + "ShortCircuit")); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PipelineTestCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + var result = await mediator.SendAsync(new PipelineTestCommand("ignored")); + + // Assert + Assert.Equal("short-circuited", result); + Assert.False(handlerInvoked); + } + + public void Dispose() + { + _provider.Dispose(); + } +} + +public sealed record PipelineTestCommand(string Value) : ICommand; + +public sealed class PipelineTestCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(PipelineTestCommand command, CancellationToken cancellationToken) + { + return new ValueTask($"handled:{command.Value}"); + } +} + +public sealed class PipelineThrowingHandler : ICommandHandler +{ + public ValueTask HandleAsync(PipelineTestCommand command, CancellationToken cancellationToken) + { + throw new InvalidOperationException("handler error"); + } +} + +public sealed class PipelineTrackingHandler : ICommandHandler +{ + private readonly Action _onInvoked; + + public PipelineTrackingHandler(Action onInvoked) + { + _onInvoked = onInvoked; + } + + public ValueTask HandleAsync(PipelineTestCommand command, CancellationToken cancellationToken) + { + _onInvoked(); + return new ValueTask($"handled:{command.Value}"); + } +} diff --git a/src/Mocha/test/Mocha.Mediator.Tests/Mocha.Mediator.Tests.csproj b/src/Mocha/test/Mocha.Mediator.Tests/Mocha.Mediator.Tests.csproj new file mode 100644 index 00000000000..f367a364009 --- /dev/null +++ b/src/Mocha/test/Mocha.Mediator.Tests/Mocha.Mediator.Tests.csproj @@ -0,0 +1,12 @@ + + + Mocha.Mediator.Tests + Mocha.Mediator.Tests + $(NoWarn);RCS1021 + + + + + + + diff --git a/src/Mocha/test/Mocha.Mediator.Tests/NamedMediatorTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/NamedMediatorTests.cs new file mode 100644 index 00000000000..3b9d51e609f --- /dev/null +++ b/src/Mocha/test/Mocha.Mediator.Tests/NamedMediatorTests.cs @@ -0,0 +1,148 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Tests; + +public sealed class NamedMediatorTests : IDisposable +{ + private readonly ServiceProvider _provider; + + public NamedMediatorTests() + { + var services = new ServiceCollection(); + + // Default (unnamed) mediator with its own pipeline + var defaultBuilder = services.AddMediator(); + services.AddScoped, DefaultCommandHandler>(); + defaultBuilder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(DefaultCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + // Named mediator "billing" with its own pipeline + var billingBuilder = services.AddMediator("billing"); + services.AddScoped, BillingCommandHandler>(); + billingBuilder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(BillingCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + // Named mediator "shipping" with its own pipeline + var shippingBuilder = services.AddMediator("shipping"); + services.AddScoped, ShippingCommandHandler>(); + shippingBuilder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(ShippingCommand), + ResponseType = typeof(string), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + _provider = services.BuildServiceProvider(); + } + + [Fact] + public async Task GetKeyedService_Should_ResolveNamedMediator() + { + // Arrange + using var scope = _provider.CreateScope(); + var billingMediator = scope.ServiceProvider.GetRequiredKeyedService("billing"); + + // Act + var result = await billingMediator.SendAsync(new BillingCommand("invoice-123")); + + // Assert + Assert.Equal("billed:invoice-123", result); + } + + [Fact] + public async Task GetService_Should_ResolveCorrectMediator_When_DefaultAndNamedCoexist() + { + // Arrange + using var scope = _provider.CreateScope(); + var defaultMediator = scope.ServiceProvider.GetRequiredService(); + var billingMediator = scope.ServiceProvider.GetRequiredKeyedService("billing"); + + // Act + var defaultResult = await defaultMediator.SendAsync(new DefaultCommand("hello")); + var billingResult = await billingMediator.SendAsync(new BillingCommand("pay-456")); + + // Assert + Assert.Equal("default:hello", defaultResult); + Assert.Equal("billed:pay-456", billingResult); + } + + [Fact] + public async Task SendAsync_Should_DispatchIndependently_When_MultipleNamedMediatorsExist() + { + // Arrange + using var scope = _provider.CreateScope(); + var billingMediator = scope.ServiceProvider.GetRequiredKeyedService("billing"); + var shippingMediator = scope.ServiceProvider.GetRequiredKeyedService("shipping"); + + // Act + var billingResult = await billingMediator.SendAsync(new BillingCommand("b-1")); + var shippingResult = await shippingMediator.SendAsync(new ShippingCommand("s-1")); + + // Assert + Assert.Equal("billed:b-1", billingResult); + Assert.Equal("shipped:s-1", shippingResult); + } + + [Fact] + public void AddMediator_Should_CreateIsolatedRuntime_When_NamedDifferently() + { + // Arrange + var defaultRuntime = _provider.GetRequiredService(); + var billingRuntime = _provider.GetRequiredKeyedService("billing"); + var shippingRuntime = _provider.GetRequiredKeyedService("shipping"); + + // Assert + Assert.NotSame(defaultRuntime, billingRuntime); + Assert.NotSame(defaultRuntime, shippingRuntime); + Assert.NotSame(billingRuntime, shippingRuntime); + } + + [Fact] + public void SendAsync_Should_ThrowInvalidOperationException_When_MessageNotRegisteredOnNamedMediator() + { + // Arrange + using var scope = _provider.CreateScope(); + var billingMediator = scope.ServiceProvider.GetRequiredKeyedService("billing"); + + // Act & Assert + Assert.Throws( + () => billingMediator.SendAsync(new DefaultCommand("wrong")).AsTask().GetAwaiter().GetResult()); + } + + public void Dispose() + { + _provider.Dispose(); + } +} + +public sealed record DefaultCommand(string Value) : ICommand; + +public sealed record BillingCommand(string InvoiceId) : ICommand; + +public sealed record ShippingCommand(string ShipmentId) : ICommand; + +public sealed class DefaultCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(DefaultCommand command, CancellationToken cancellationToken) + => new($"default:{command.Value}"); +} + +public sealed class BillingCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(BillingCommand command, CancellationToken cancellationToken) + => new($"billed:{command.InvoiceId}"); +} + +public sealed class ShippingCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(ShippingCommand command, CancellationToken cancellationToken) + => new($"shipped:{command.ShipmentId}"); +} diff --git a/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs new file mode 100644 index 00000000000..7cedd96efbc --- /dev/null +++ b/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs @@ -0,0 +1,220 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Mocha.Mediator; + +namespace Mocha.Mediator.Tests; + +public sealed class NotificationStrategyTests +{ + [Fact] + public async Task PublishAsync_Should_InvokeHandlersSequentially_When_UsingForeachAwait() + { + // Arrange + var log = new List(); + + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + services.AddScoped( + _ => new SequentialHandler1(log)); + services.AddScoped( + _ => new SequentialHandler2(log)); + services.AddScoped( + _ => new SequentialHandler3(log)); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(StrategyTestNotification), + Terminal = PipelineBuilder.BuildNotificationTerminal( + new[] { typeof(SequentialHandler1), typeof(SequentialHandler2), typeof(SequentialHandler3) }) + })); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + await mediator.PublishAsync(new StrategyTestNotification("sequential")); + + // Assert + Assert.Equal( + new[] { "Handler1:sequential", "Handler2:sequential", "Handler3:sequential" }, + log); + } + + [Fact] + public async Task PublishAsync_Should_StopExecution_When_ForeachAwaitHandlerThrows() + { + // Arrange + var log = new List(); + + var services = new ServiceCollection(); + var builder = services.AddMediator(); + + services.AddScoped( + _ => new SequentialHandler1(log)); + services.AddScoped( + _ => new StrategyThrowingHandler()); + services.AddScoped( + _ => new SequentialHandler3(log)); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(StrategyTestNotification), + Terminal = PipelineBuilder.BuildNotificationTerminal( + new[] { typeof(SequentialHandler1), typeof(StrategyThrowingHandler), typeof(SequentialHandler3) }) + })); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act & Assert + await Assert.ThrowsAsync( + () => mediator.PublishAsync(new StrategyTestNotification("fail")).AsTask()); + + // Handler1 ran, then the throw occurred; Handler3 was never reached + Assert.Single(log); + Assert.Equal("Handler1:fail", log[0]); + } + + [Fact] + public async Task PublishAsync_Should_InvokeAllHandlers_When_UsingTaskWhenAll() + { + // Arrange + var bag = new ConcurrentBag(); + + var services = new ServiceCollection(); + + // Override default strategy with TaskWhenAllPublisher before AddMediator + services.AddSingleton(); + + var builder = services.AddMediator(); + + services.AddScoped( + _ => new ConcurrentHandler1(bag)); + services.AddScoped( + _ => new ConcurrentHandler2(bag)); + services.AddScoped( + _ => new ConcurrentHandler3(bag)); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(StrategyTestNotification), + Terminal = PipelineBuilder.BuildNotificationTerminal( + new[] { typeof(ConcurrentHandler1), typeof(ConcurrentHandler2), typeof(ConcurrentHandler3) }) + })); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + await mediator.PublishAsync(new StrategyTestNotification("concurrent")); + + // Assert - all handlers invoked (order is not guaranteed with WhenAll) + var results = bag.OrderBy(x => x).ToList(); + Assert.Equal(3, results.Count); + Assert.Contains("Concurrent1:concurrent", results); + Assert.Contains("Concurrent2:concurrent", results); + Assert.Contains("Concurrent3:concurrent", results); + } + + [Fact] + public async Task PublishAsync_Should_PropagateException_When_TaskWhenAllHandlerThrows() + { + // Arrange + var bag = new ConcurrentBag(); + + var services = new ServiceCollection(); + services.AddSingleton(); + + var builder = services.AddMediator(); + + services.AddScoped( + _ => new ConcurrentHandler1(bag)); + services.AddScoped( + _ => new StrategyThrowingHandler()); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(StrategyTestNotification), + Terminal = PipelineBuilder.BuildNotificationTerminal( + new[] { typeof(ConcurrentHandler1), typeof(StrategyThrowingHandler) }) + })); + + await using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => mediator.PublishAsync(new StrategyTestNotification("throw")).AsTask()); + + Assert.Equal("notification handler error", ex.Message); + } +} + +public sealed record StrategyTestNotification(string Value) : INotification; + +public sealed class SequentialHandler1(List log) : INotificationHandler +{ + public ValueTask HandleAsync(StrategyTestNotification notification, CancellationToken cancellationToken) + { + log.Add($"Handler1:{notification.Value}"); + return default; + } +} + +public sealed class SequentialHandler2(List log) : INotificationHandler +{ + public ValueTask HandleAsync(StrategyTestNotification notification, CancellationToken cancellationToken) + { + log.Add($"Handler2:{notification.Value}"); + return default; + } +} + +public sealed class SequentialHandler3(List log) : INotificationHandler +{ + public ValueTask HandleAsync(StrategyTestNotification notification, CancellationToken cancellationToken) + { + log.Add($"Handler3:{notification.Value}"); + return default; + } +} + +public sealed class ConcurrentHandler1(ConcurrentBag bag) : INotificationHandler +{ + public ValueTask HandleAsync(StrategyTestNotification notification, CancellationToken cancellationToken) + { + bag.Add($"Concurrent1:{notification.Value}"); + return default; + } +} + +public sealed class ConcurrentHandler2(ConcurrentBag bag) : INotificationHandler +{ + public ValueTask HandleAsync(StrategyTestNotification notification, CancellationToken cancellationToken) + { + bag.Add($"Concurrent2:{notification.Value}"); + return default; + } +} + +public sealed class ConcurrentHandler3(ConcurrentBag bag) : INotificationHandler +{ + public ValueTask HandleAsync(StrategyTestNotification notification, CancellationToken cancellationToken) + { + bag.Add($"Concurrent3:{notification.Value}"); + return default; + } +} + +public sealed class StrategyThrowingHandler : INotificationHandler +{ + public ValueTask HandleAsync(StrategyTestNotification notification, CancellationToken cancellationToken) + { + throw new InvalidOperationException("notification handler error"); + } +} diff --git a/src/Mocha/test/Mocha.Mediator.Tests/TestSetup.cs b/src/Mocha/test/Mocha.Mediator.Tests/TestSetup.cs new file mode 100644 index 00000000000..efe270f1b10 --- /dev/null +++ b/src/Mocha/test/Mocha.Mediator.Tests/TestSetup.cs @@ -0,0 +1,222 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Tests; + +// --------------------------------------------------------------------------- +// Message types (prefixed with "Dispatch" to avoid collisions with +// MiddlewarePipelineTests types) +// --------------------------------------------------------------------------- + +public sealed record DispatchVoidCommand(string Value) : ICommand; + +public sealed record DispatchCommand(string Value) : ICommand; + +public sealed record DispatchQuery(int Id) : IQuery; + +public sealed record DispatchNotification(string Payload) : INotification; + +public sealed record DispatchResponse(string Data); + +// --------------------------------------------------------------------------- +// Command that always throws +// --------------------------------------------------------------------------- + +public sealed record DispatchThrowingCommand(string Value) : ICommand; + +// --------------------------------------------------------------------------- +// Command that captures the CancellationToken +// --------------------------------------------------------------------------- + +public sealed record DispatchTokenCapturingCommand : ICommand; + +// --------------------------------------------------------------------------- +// Async (non-synchronously completing) command +// --------------------------------------------------------------------------- + +public sealed record DispatchAsyncCommand(string Value) : ICommand; + +// --------------------------------------------------------------------------- +// Handlers +// --------------------------------------------------------------------------- + +public sealed class DispatchVoidCommandHandler : ICommandHandler +{ + public static bool WasInvoked { get; set; } + + public ValueTask HandleAsync(DispatchVoidCommand command, CancellationToken cancellationToken) + { + WasInvoked = true; + return default; + } +} + +public sealed class DispatchCommandHandler : ICommandHandler +{ + private static readonly DispatchResponse s_cached = new("handled"); + + public ValueTask HandleAsync(DispatchCommand command, CancellationToken cancellationToken) + => new(s_cached); +} + +public sealed class DispatchQueryHandler : IQueryHandler +{ + private static readonly DispatchResponse s_cached = new("query-result"); + + public ValueTask HandleAsync(DispatchQuery query, CancellationToken cancellationToken) + => new(s_cached); +} + +public sealed class DispatchNotificationHandler : INotificationHandler +{ + public static bool WasInvoked { get; set; } + + public ValueTask HandleAsync(DispatchNotification notification, CancellationToken cancellationToken) + { + WasInvoked = true; + return default; + } +} + +public sealed class DispatchSecondNotificationHandler : INotificationHandler +{ + public static bool WasInvoked { get; set; } + + public ValueTask HandleAsync(DispatchNotification notification, CancellationToken cancellationToken) + { + WasInvoked = true; + return default; + } +} + +public sealed class DispatchThrowingCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(DispatchThrowingCommand command, CancellationToken cancellationToken) + => throw new InvalidOperationException("handler-exploded"); +} + +public sealed class DispatchTokenCapturingHandler : ICommandHandler +{ + public static CancellationToken CapturedToken { get; set; } + + public ValueTask HandleAsync(DispatchTokenCapturingCommand command, CancellationToken cancellationToken) + { + CapturedToken = cancellationToken; + return default; + } +} + +public sealed class DispatchAsyncCommandHandler : ICommandHandler +{ + public async ValueTask HandleAsync(DispatchAsyncCommand command, CancellationToken cancellationToken) + { + await Task.Yield(); + return new DispatchResponse("async-result"); + } +} + +// --------------------------------------------------------------------------- +// Helper to build a fully-wired IServiceProvider +// --------------------------------------------------------------------------- + +public static class DispatchTestHelper +{ + /// + /// Creates a service provider with the mediator infrastructure, registering + /// only the handlers and pipelines specified by the caller. + /// + public static IServiceProvider BuildProvider(Action configure) + { + var services = new ServiceCollection(); + var builder = MediatorServiceCollectionExtensions.AddMediator(services); + configure(builder, services); + return services.BuildServiceProvider(); + } + + /// + /// Creates a service provider with the standard set of test handlers and pipelines. + /// + public static IServiceProvider BuildDefaultProvider() + { + return BuildProvider((builder, services) => + { + // Void command + services.AddScoped, DispatchVoidCommandHandler>(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(DispatchVoidCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + })); + + // Command with response + services.AddScoped, DispatchCommandHandler>(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(DispatchCommand), + ResponseType = typeof(DispatchResponse), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + + // Query + services.AddScoped, DispatchQueryHandler>(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(DispatchQuery), + ResponseType = typeof(DispatchResponse), + Terminal = PipelineBuilder.BuildQueryTerminal() + })); + + // Notification - single handler + services.AddScoped(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(DispatchNotification), + Terminal = PipelineBuilder.BuildNotificationTerminal( + new[] { typeof(DispatchNotificationHandler) }) + })); + + // Throwing command + services.AddScoped, DispatchThrowingCommandHandler>(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(DispatchThrowingCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + })); + + // Token capturing command + services.AddScoped, DispatchTokenCapturingHandler>(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(DispatchTokenCapturingCommand), + Terminal = PipelineBuilder.BuildVoidCommandTerminal() + })); + + // Async command + services.AddScoped, DispatchAsyncCommandHandler>(); + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(DispatchAsyncCommand), + ResponseType = typeof(DispatchResponse), + Terminal = PipelineBuilder.BuildCommandTerminal() + })); + }); + } + + /// + /// Creates a service provider with multiple notification handlers to test fan-out dispatch. + /// + public static IServiceProvider BuildMultiNotificationProvider() + { + return BuildProvider((builder, services) => + { + services.AddScoped(); + services.AddScoped(); + + builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(DispatchNotification), + Terminal = PipelineBuilder.BuildNotificationTerminal( + new[] { typeof(DispatchNotificationHandler), typeof(DispatchSecondNotificationHandler) }) + })); + }); + } +} diff --git a/src/Mocha/test/Mocha.Transport.InMemory.Tests/Topology/InMemoryTopologyConventionTests.cs b/src/Mocha/test/Mocha.Transport.InMemory.Tests/Topology/InMemoryTopologyConventionTests.cs index 0c36efdafcd..cd87f97c0a5 100644 --- a/src/Mocha/test/Mocha.Transport.InMemory.Tests/Topology/InMemoryTopologyConventionTests.cs +++ b/src/Mocha/test/Mocha.Transport.InMemory.Tests/Topology/InMemoryTopologyConventionTests.cs @@ -109,10 +109,10 @@ public void AddEventHandler_Should_CreateTopicQueueAndBinding_When_Registered() var route = runtime.Router.GetInboundByConsumer(consumer).First(); var queueName = route.Endpoint!.Name; - // assert -- a queue must exist for the handler's receive endpoint + // assert - a queue must exist for the handler's receive endpoint Assert.Contains(topology.Queues, q => q.Name == queueName); - // assert -- a binding exists connecting a topic to the queue + // assert - a binding exists connecting a topic to the queue Assert.Contains(topology.Bindings.OfType(), b => b.Destination.Name == queueName); } @@ -134,10 +134,10 @@ public void AddRequestHandler_Should_CreateQueue_When_Registered() const string expectedQueueName = "process-payment"; - // assert -- queue exists + // assert - queue exists Assert.Contains(topology.Queues, q => q.Name == expectedQueueName); - // assert -- receive endpoint exists + // assert - receive endpoint exists Assert.Contains(transport.ReceiveEndpoints, e => e.Name == expectedQueueName); } @@ -149,13 +149,13 @@ public void AddRequestHandler_Should_CreateQueueAndReplyEndpoint_When_ResponseTy const string expectedQueueName = "get-order-status"; - // assert -- queue for the request type exists + // assert - queue for the request type exists Assert.Contains(topology.Queues, q => q.Name == expectedQueueName); - // assert -- a reply receive endpoint is created (needed for request-response) + // assert - a reply receive endpoint is created (needed for request-response) Assert.Contains(transport.ReceiveEndpoints, e => e.Kind == ReceiveEndpointKind.Reply); - // assert -- a reply dispatch endpoint is created + // assert - a reply dispatch endpoint is created Assert.Contains(transport.DispatchEndpoints, e => e.Kind == DispatchEndpointKind.Reply); } @@ -175,18 +175,18 @@ public void AddEventHandler_Should_CreateSeparateQueues_When_MultipleHandlersFor var queue1Name = runtime.Router.GetInboundByConsumer(consumer1).First().Endpoint!.Name; var queue2Name = runtime.Router.GetInboundByConsumer(consumer2).First().Endpoint!.Name; - // assert -- the two handler queues are distinct + // assertthe two handler queues are distinct Assert.NotEqual(queue1Name, queue2Name); - // assert -- both queues exist in topology + // assertboth queues exist in topology Assert.Contains(topology.Queues, q => q.Name == queue1Name); Assert.Contains(topology.Queues, q => q.Name == queue2Name); - // assert -- both queues have bindings from a topic + // assertboth queues have bindings from a topic Assert.Contains(topology.Bindings.OfType(), b => b.Destination.Name == queue1Name); Assert.Contains(topology.Bindings.OfType(), b => b.Destination.Name == queue2Name); - // assert -- they share a common publish topic for OrderCreated + // assertthey share a common publish topic for OrderCreated var publishTopicName = topology .Topics.Select(t => t.Name) .FirstOrDefault(n => n.Contains('.') && n.EndsWith("order-created")); @@ -210,12 +210,12 @@ public void AddHandlers_Should_CreateIndependentTopology_When_EventAndRequestReg var eventQueueName = runtime.Router.GetInboundByConsumer(eventConsumer).First().Endpoint!.Name; var requestQueueName = runtime.Router.GetInboundByConsumer(requestConsumer).First().Endpoint!.Name; - // assert -- both queues exist and are different + // assertboth queues exist and are different Assert.NotEqual(eventQueueName, requestQueueName); Assert.Contains(topology.Queues, q => q.Name == eventQueueName); Assert.Contains(topology.Queues, q => q.Name == requestQueueName); - // assert -- each has its own receive endpoint + // asserteach has its own receive endpoint Assert.Contains(transport.ReceiveEndpoints, e => e.Name == eventQueueName); Assert.Contains(transport.ReceiveEndpoints, e => e.Name == requestQueueName); } diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index 73dba37008c..85e2c116d03 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -3035,6 +3035,14 @@ { "path": "hosting", "title": "Hosting" + }, + { + "path": "mediator", + "title": "Mediator", + "items": [ + { "path": "index", "title": "Overview" }, + { "path": "pipeline-and-middleware", "title": "Pipeline & Middleware" } + ] } ] } diff --git a/website/src/docs/mocha/v1/index.md b/website/src/docs/mocha/v1/index.md index 9111f5fde3f..ab9f22d52d3 100644 --- a/website/src/docs/mocha/v1/index.md +++ b/website/src/docs/mocha/v1/index.md @@ -1,22 +1,33 @@ --- title: "Introduction" -description: "Mocha is a messaging framework for .NET that provides a message bus with handler-based consumers, multiple transport support, middleware pipelines, saga orchestration, and deep observability through Nitro integration." +description: "Mocha is a messaging framework for .NET that provides a message bus for inter-service communication, a source-generated mediator for in-process CQRS, middleware pipelines, saga orchestration, and deep observability through Nitro integration." --- ```csharp +// Inter-service messaging via the message bus builder.Services .AddMessageBus() .AddEventHandler() .AddRabbitMQ(); + +// In-process CQRS via the mediator +builder.Services + .AddMediator() + .AddHandlers(); ``` -That is a complete bus configuration. Register the bus, add your handlers, pick a transport. Mocha handles routing, serialization, endpoint topology, and handler lifecycle. +Mocha gives you two dispatch mechanisms. The **message bus** sends messages across service boundaries through a transport like RabbitMQ. The **mediator** dispatches commands, queries, and notifications within a single process using source-generated code - no reflection, no dictionary lookups. Use them independently or together. # What Mocha is -Mocha is a messaging framework for .NET. It gives your services a structured way to communicate through messages - publishing events, sending requests, and coordinating request/reply flows - following the patterns described in [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/patterns/messaging/Introduction.html). It integrates directly into ASP.NET Core's dependency injection and is designed for [event-driven architectures](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven) where services communicate asynchronously rather than through direct method calls. +Mocha is a messaging framework for .NET with two complementary dispatch systems: + +- **Message bus** - sends messages across service boundaries through transports like RabbitMQ. Supports pub/sub events, request/reply, saga orchestration, inbox/outbox reliability, and pluggable transports. Follows the patterns described in [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/patterns/messaging/Introduction.html). +- **Mediator** - dispatches commands, queries, and notifications within a single process. A Roslyn source generator produces a concrete mediator class at compile time with monomorphized dispatch and pre-compiled pipeline delegates. No reflection, no runtime code generation. -You implement handler interfaces. Mocha wraps your handlers into consumers, compiles the middleware pipeline, binds endpoints to the transport, and starts receiving messages. The framework is handler-first: you declare what you handle, and Mocha builds the infrastructure around that declaration. +Both integrate directly into ASP.NET Core's dependency injection and are designed for [event-driven architectures](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven). Use the message bus when messages cross process boundaries. Use the mediator when you want in-process CQRS with pipeline behaviors for cross-cutting concerns like validation, logging, and transactions. Most real-world services use both: the mediator handles internal command/query dispatch, and the message bus handles inter-service events. + +The framework is handler-first in both cases. You implement handler interfaces, and Mocha builds the infrastructure around those declarations - whether that means wiring up transport endpoints and middleware pipelines for the bus, or generating a type-safe mediator class with pre-compiled dispatch for in-process handlers. # Terminology @@ -33,6 +44,9 @@ These terms appear throughout the documentation. They are defined once here and | **Transport** | The infrastructure layer connecting Mocha to a message broker, such as RabbitMQ or an in-process channel. | | **Pipeline** | The chain of middleware that processes a message from the transport through to the handler. | | **Saga** | A long-running stateful workflow that coordinates multiple messages and transitions across services. | +| **Mediator** | An in-process dispatcher that routes commands, queries, and notifications to their handlers without a transport layer. Source-generated at compile time. | +| **Command** | A mediator message representing an action. Implements `ICommand` (void) or `ICommand` (with response). Dispatched via `SendAsync`. | +| **Query** | A mediator message representing a read operation. Implements `IQuery`. Dispatched via `QueryAsync`. | # Architecture @@ -144,6 +158,40 @@ Sagas coordinate multi-step workflows that span multiple services and messages. Mocha persists saga state, manages transitions, and supports compensation when steps fail. See [Sagas](/docs/mocha/v1/sagas) for a full walkthrough. +## In-process mediator + +For commands and queries that stay within a single service, the mediator provides CQRS dispatch with pipeline behaviors - without a transport layer. Define your messages with marker interfaces, implement handlers, and the source generator wires everything at compile time: + +```csharp +// Define a command and its handler +public record PlaceOrderCommand(Guid ProductId, int Quantity) + : ICommand; + +public class PlaceOrderCommandHandler(AppDbContext db) + : ICommandHandler +{ + public async ValueTask HandleAsync( + PlaceOrderCommand command, CancellationToken cancellationToken) + { + // business logic + return new PlaceOrderResult(true, Guid.NewGuid()); + } +} +``` + +```csharp +// Register and use +builder.Services + .AddMediator() + .AddHandlers() + .UseEntityFrameworkTransactions(); + +app.MapPost("/orders", async (ISender sender) => + await sender.SendAsync(new PlaceOrderCommand(productId, 2))); +``` + +The mediator supports commands (with and without responses), queries, notifications, pipeline behaviors, pre/post processors, and EF Core transaction wrapping (commands only by default, configurable via delegate). `AddHandlers()` is source-generated - it discovers all handlers in your assembly automatically. See [Mediator](/docs/mocha/v1/mediator) for the full guide. + # Learning paths Choose an entry point based on how you learn best: @@ -151,6 +199,7 @@ Choose an entry point based on how you learn best: - **Get something running first:** [Quick Start](/docs/mocha/v1/quick-start) -zero to a working message bus in under five minutes with the InMemory transport. - **Understand the concepts first:** [Messages](/docs/mocha/v1/messages) then [Messaging Patterns](/docs/mocha/v1/messaging-patterns) - learn what flows through the system and what patterns govern how it flows. - **Evaluating Mocha for a specific broker:** [Transports](/docs/mocha/v1/transports) - understand the transport abstraction and what is available. +- **In-process CQRS:** [Mediator](/docs/mocha/v1/mediator) - dispatch commands, queries, and notifications within a single service using the source-generated mediator. - **See a real-world system:** The [Demo application](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Demo) is a complete e-commerce system with three services (Catalog, Billing, Shipping) that demonstrates event-driven communication, sagas, batch processing, the transactional outbox, and .NET Aspire orchestration. diff --git a/website/src/docs/mocha/v1/mediator/index.md b/website/src/docs/mocha/v1/mediator/index.md new file mode 100644 index 00000000000..a5f6d0c1424 --- /dev/null +++ b/website/src/docs/mocha/v1/mediator/index.md @@ -0,0 +1,535 @@ +--- +title: "Overview" +description: "Use the Mocha Mediator to dispatch commands, queries, and notifications within a single process. Source-generated at compile time for zero-reflection dispatch with pre-compiled middleware pipelines." +--- + +```csharp +builder.Services + .AddMediator() + .AddCatalog(); // source-generated from your assembly name +``` + +That registers the mediator infrastructure, discovers your handlers at compile time, and wires up the dispatch pipeline. `.AddCatalog()` is a source-generated extension method - it knows your handlers and message types at compile time and produces direct dispatch code with no reflection. + +# What the mediator is + +The mediator sits between your application code and your handlers. Instead of injecting handler interfaces directly, you inject `IMediator` (or `ISender` / `IPublisher`) and dispatch messages through it. The mediator routes each message to the correct handler based on its type. + +```csharp +// Without mediator — tight coupling +app.MapPost("/orders", async (PlaceOrderCommandHandler handler) => + await handler.HandleAsync(new PlaceOrderCommand(...))); + +// With mediator — decoupled dispatch +app.MapPost("/orders", async (ISender sender) => + await sender.SendAsync(new PlaceOrderCommand(...))); +``` + +The mediator provides three things your handlers cannot do alone: a middleware pipeline that wraps every handler invocation with cross-cutting concerns (logging, transactions, validation), polymorphic dispatch that routes messages by type at runtime, and a seam between your application layer and your domain logic. This is the [Mediator pattern](https://refactoring.guru/design-patterns/mediator) -- objects communicate through a central hub instead of referencing each other directly. + +If you have used [MediatR](https://github.com/jbogard/MediatR), the concepts are familiar. Mocha Mediator takes a different approach to performance: a [Roslyn source generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) analyzes your handler registrations at compile time and produces pre-compiled pipeline delegates. No `MakeGenericType`, no service provider lookups to resolve the pipeline, no reflection at runtime. + +# When to use the mediator vs. the message bus + +Mocha has two dispatch mechanisms. Use the right one for the situation: + +| Use the **mediator** when... | Use the **message bus** when... | +| --- | --- | +| Dispatch stays in-process | Messages cross process or service boundaries | +| You want [CQRS](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) separation of commands and queries | You want pub/sub events across services | +| You need a request/response pipeline with middleware | You need transport-level features (retries, outbox) | +| Handlers live in the same assembly or solution | Handlers live in different services | + +The mediator and the message bus complement each other. A common pattern is to use the mediator for in-process CQRS dispatch within a service, and the message bus for inter-service event-driven communication. + +# Messages + +Messages are plain C# types that implement a marker interface. The marker interface tells the mediator how to route the message and what return type to expect. + +## Commands + +Commands represent actions that change state. Use imperative verb-noun naming: `PlaceOrder`, `ProcessPayment`. + +```csharp +// A command that returns no response +public record DeleteOrderCommand(Guid OrderId) : ICommand; + +// A command that returns a response +public record PlaceOrderCommand( + Guid ProductId, + int Quantity, + string CustomerId) : ICommand; + +public record PlaceOrderResult(bool Success, Guid? OrderId = null, string? Error = null); +``` + +## Queries + +Queries represent read operations that return data without side effects. Use noun-based naming: `GetProducts`, `GetOrderById`. + +```csharp +public record GetProductsQuery : IQuery>; + +public record GetProductByIdQuery(Guid Id) : IQuery; +``` + +## Notifications + +Notifications represent events that multiple handlers can observe. Use past-tense naming: `OrderPlaced`, `PaymentCompleted`. + +```csharp +public record OrderPlacedNotification( + Guid OrderId, + decimal Amount) : INotification; +``` + +## Message type reference + +| Interface | Purpose | Dispatch method | Return type | +| --- | --- | --- | --- | +| `ICommand` | Action, no response | `SendAsync` | `ValueTask` | +| `ICommand` | Action with response | `SendAsync` | `ValueTask` | +| `IQuery` | Read operation | `QueryAsync` | `ValueTask` | +| `INotification` | Multi-handler event | `PublishAsync` | `ValueTask` | + +# Handlers + +Each message type has a corresponding handler interface. The mediator routes each message to exactly one handler - except notifications, which fan out to all registered handlers. + +## Command handlers + +```csharp +// Handles a void command +public sealed class DeleteOrderCommandHandler(AppDbContext db) + : ICommandHandler +{ + public async ValueTask HandleAsync( + DeleteOrderCommand command, CancellationToken cancellationToken) + { + var order = await db.Orders.FindAsync(command.OrderId); + if (order is not null) + { + db.Orders.Remove(order); + await db.SaveChangesAsync(cancellationToken); + } + } +} + +// Handles a command with a response +public sealed class PlaceOrderCommandHandler(AppDbContext db) + : ICommandHandler +{ + public async ValueTask HandleAsync( + PlaceOrderCommand command, CancellationToken cancellationToken) + { + var order = new Order + { + Id = Guid.NewGuid(), + ProductId = command.ProductId, + Quantity = command.Quantity, + CustomerId = command.CustomerId + }; + + db.Orders.Add(order); + await db.SaveChangesAsync(cancellationToken); + + return new PlaceOrderResult(true, order.Id); + } +} +``` + +## Query handlers + +```csharp +public sealed class GetProductsQueryHandler(AppDbContext db) + : IQueryHandler> +{ + public async ValueTask> HandleAsync( + GetProductsQuery query, CancellationToken cancellationToken) + => await db.Products.ToListAsync(cancellationToken); +} +``` + +## Notification handlers + +Multiple handlers can subscribe to the same notification type. The mediator invokes all of them. + +```csharp +public sealed class SendOrderConfirmationEmail(IEmailService email) + : INotificationHandler +{ + public async ValueTask HandleAsync( + OrderPlacedNotification notification, CancellationToken cancellationToken) + { + await email.SendAsync( + $"Order {notification.OrderId} confirmed", cancellationToken); + } +} + +public sealed class UpdateAnalyticsDashboard(IAnalytics analytics) + : INotificationHandler +{ + public async ValueTask HandleAsync( + OrderPlacedNotification notification, CancellationToken cancellationToken) + { + await analytics.RecordOrderAsync( + notification.OrderId, notification.Amount); + } +} +``` + +## Handler interface reference + +| Interface | Message type | Response | +| --- | --- | --- | +| `ICommandHandler` | `ICommand` | void | +| `ICommandHandler` | `ICommand` | `TResponse` | +| `IQueryHandler` | `IQuery` | `TResponse` | +| `INotificationHandler` | `INotification` | void | + +# Dispatching messages + +Inject `IMediator`, `ISender`, or `IPublisher` from DI and call the appropriate method. + +```csharp +// Send a command with a response +app.MapPost("/orders", async (PlaceOrderRequest request, ISender sender) => +{ + var result = await sender.SendAsync( + new PlaceOrderCommand(request.ProductId, request.Quantity, request.CustomerId)); + + return result.Success + ? Results.Created($"/api/orders/{result.OrderId}", result) + : Results.BadRequest(result.Error); +}); + +// Send a query +app.MapGet("/products", async (ISender sender) => + await sender.QueryAsync(new GetProductsQuery())); + +// Publish a notification +app.MapPost("/orders/{id}/ship", async (Guid id, IPublisher publisher) => +{ + await publisher.PublishAsync(new OrderShippedNotification(id)); + return Results.Ok(); +}); +``` + +`ISender` handles commands and queries. `IPublisher` handles notifications. `IMediator` combines both interfaces - inject it when you need both in the same class. + +## Untyped dispatch + +When the message type is not known at compile time, use the `object`-based overloads: + +```csharp +// Dispatch a command or query by runtime type +object message = GetMessageFromSomewhere(); +object? result = await sender.SendAsync(message); + +// Dispatch a notification by runtime type +object notification = GetNotificationFromSomewhere(); +await publisher.PublishAsync(notification); +``` + +The runtime type of the message must implement one of the marker interfaces (`ICommand`, `ICommand`, `IQuery`, or `INotification`). An exception is thrown if it does not. + +# Registration and source generation + +## Register the mediator + +```csharp +builder.Services + .AddMediator() + .AddCatalog(); // source-generated from assembly name "Demo.Catalog" +``` + +`AddMediator()` registers the core mediator infrastructure: the `Mediator` class, context pooling, and the default notification strategy. The source-generated `Add{ModuleName}()` method registers: + +- All command, query, and notification handlers found in your assembly +- Pipeline configurations with pre-compiled terminal delegates for each message type + +You do not register handlers manually. The source generator discovers them by scanning for classes that implement handler interfaces. + +## Module naming + +The source generator names the registration method based on your assembly: + +1. If you apply `[assembly: MediatorModule("Billing")]`, the method is `AddBilling()` +2. Otherwise, it uses the last segment of the assembly name: `Demo.Catalog` produces `AddCatalog()` + +To set an explicit module name, add the attribute to any file in your project: + +```csharp +using Mocha.Mediator; + +[assembly: MediatorModule("Billing")] +``` + +## What the source generator produces + +At compile time, the Mocha source generator: + +1. **Scans** your assembly for all handler implementations +2. **Generates** an `Add{ModuleName}()` extension method on `IMediatorHostBuilder` +3. **Registers** all handlers with the configured `ServiceLifetime` +4. **Creates** `MediatorPipelineConfiguration` entries with terminal delegates built via `PipelineBuilder` + +For example, given assembly name `Demo.Catalog` and this handler: + +```csharp +public sealed class PlaceOrderCommandHandler + : ICommandHandler { ... } +``` + +The generator produces: + +```csharp +public static class CatalogMediatorBuilderExtensions +{ + public static IMediatorHostBuilder AddCatalog( + this IMediatorHostBuilder builder) + { + var services = builder.Services; + var lifetime = builder.Options.ServiceLifetime; + + // Register handlers + services.Add(new ServiceDescriptor( + typeof(ICommandHandler), + typeof(PlaceOrderCommandHandler), + lifetime)); + + // Register pipelines + MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => + { + b.RegisterPipeline(new MediatorPipelineConfiguration + { + MessageType = typeof(PlaceOrderCommand), + ResponseType = typeof(PlaceOrderResult), + Terminal = PipelineBuilder + .BuildCommandTerminal() + }); + }); + + return builder; + } +} +``` + +## Configure service lifetime + +By default, handlers are registered as `Scoped`. To change the default: + +```csharp +builder.Services + .AddMediator() + .ConfigureOptions(options => + { + options.ServiceLifetime = ServiceLifetime.Transient; + }) + .AddCatalog(); +``` + +Call `ConfigureOptions` before `Add{ModuleName}()` so the source-generated method reads the updated lifetime. + +## How the mediator dispatches + +When you call `SendAsync`, `QueryAsync`, or `PublishAsync`, the `Mediator` class: + +1. **Rents** a `MediatorContext` from an object pool (thread-static fast path, then `ObjectPool` fallback) +2. **Initializes** the context with the message, message type, service provider, and cancellation token +3. **Looks up** the pre-compiled pipeline delegate for the message type (O(1) frozen dictionary lookup) +4. **Invokes** the pipeline delegate +5. **Returns** the context to the pool + +## Performance characteristics + +- **Context pooling:** `MediatorContext` objects are pooled with a thread-static fast path for zero-contention reuse +- **Zero-allocation async:** Uses `PoolingAsyncValueTaskMethodBuilder` to avoid allocating `Task` objects on the hot path +- **O(1) pipeline lookup:** Pre-compiled pipelines stored in a frozen dictionary, built once at startup +- **No reflection at runtime:** All handler resolution and pipeline compilation happens at startup or compile time + +# Named mediators + +To run multiple independent mediator instances (each with its own handlers and middleware), use named mediators. Named mediators use .NET's keyed dependency injection. + +```csharp +// Register a named mediator +builder.Services + .AddMediator("billing") + .AddBilling(); + +// Register the default (unnamed) mediator +builder.Services + .AddMediator() + .AddCatalog(); +``` + +Resolve a named mediator from DI: + +```csharp +app.MapPost("/payments", async ( + [FromKeyedServices("billing")] ISender sender, + ProcessPaymentRequest request) => +{ + var result = await sender.SendAsync( + new ProcessPaymentCommand(request.Amount)); + return Results.Ok(result); +}); + +// Or resolve from IServiceProvider directly +var billingMediator = serviceProvider + .GetRequiredKeyedService("billing"); +``` + +Each named mediator has its own handler registrations, middleware pipeline, and `MediatorRuntime`. The default mediator (registered with `AddMediator()` without a name) is resolved normally without keyed services. + +# The Unit type + +`Unit` is a readonly struct that represents void in generic contexts. You do not need it for normal command/query dispatch - the framework handles void commands through `ICommand` and `ICommandHandler`. `Unit` is available if you need a void-equivalent type in your own generic code. + +```csharp +// Unit.Value — the singleton instance +// Unit.ValueTask — a pre-completed ValueTask +// Unit.Task — a pre-completed Task +``` + +# Putting it together + +Here is a complete minimal API application with commands, queries, and notifications: + +```csharp +using Mocha.Mediator; + +var builder = WebApplication.CreateBuilder(args); + +// Register mediator with source-generated handlers +builder.Services + .AddMediator() + .AddMyApp(); + +var app = builder.Build(); + +// Command — place an order +app.MapPost("/orders", async (PlaceOrderRequest request, ISender sender) => +{ + var result = await sender.SendAsync( + new PlaceOrderCommand(request.ProductId, request.Quantity)); + return result.Success + ? Results.Created($"/orders/{result.OrderId}", result) + : Results.BadRequest(result.Error); +}); + +// Query — list products +app.MapGet("/products", async (ISender sender) => + await sender.QueryAsync(new GetProductsQuery())); + +// Notification — broadcast that an order shipped +app.MapPost("/orders/{id}/ship", async (Guid id, IPublisher publisher) => +{ + await publisher.PublishAsync(new OrderShippedNotification(id)); + return Results.Ok(); +}); + +app.Run(); + +// ── Messages ──────────────────────────────────────── + +public record PlaceOrderCommand(Guid ProductId, int Quantity) + : ICommand; + +public record PlaceOrderResult( + bool Success, Guid? OrderId = null, string? Error = null); + +public record GetProductsQuery : IQuery>; + +public record ProductDto(Guid Id, string Name, decimal Price); + +public record OrderShippedNotification(Guid OrderId) : INotification; + +// ── Handlers ──────────────────────────────────────── + +public sealed class PlaceOrderCommandHandler(ILogger logger) + : ICommandHandler +{ + public ValueTask HandleAsync( + PlaceOrderCommand command, CancellationToken cancellationToken) + { + var orderId = Guid.NewGuid(); + logger.LogInformation("Order {OrderId} placed", orderId); + return new ValueTask( + new PlaceOrderResult(true, orderId)); + } +} + +public sealed class GetProductsQueryHandler + : IQueryHandler> +{ + private static readonly IReadOnlyList Products = + [ + new(Guid.NewGuid(), "Keyboard", 149.99m), + new(Guid.NewGuid(), "Mouse", 79.99m), + ]; + + public ValueTask> HandleAsync( + GetProductsQuery query, CancellationToken cancellationToken) + => new(Products); +} + +public sealed class OrderShippedEmailHandler(ILogger logger) + : INotificationHandler +{ + public ValueTask HandleAsync( + OrderShippedNotification notification, + CancellationToken cancellationToken) + { + logger.LogInformation( + "Order {OrderId} shipped — email sent", notification.OrderId); + return ValueTask.CompletedTask; + } +} + +public record PlaceOrderRequest(Guid ProductId, int Quantity); +``` + +If everything worked, `dotnet run` starts the server and you can: + +- `POST /orders` with a JSON body to place an order +- `GET /products` to list products +- `POST /orders/{id}/ship` to publish a shipped notification + +# Troubleshooting + +## `InvalidOperationException: No pipeline registered for message type` + +The source generator did not find a handler for your message type. Verify: + +- Your handler class implements the correct interface (e.g., `ICommandHandler`) +- Your message type implements the correct marker interface (e.g., `ICommand`) +- You called the source-generated `.Add{ModuleName}()` method on the mediator builder +- The handler is in the same project that the source generator can see +- The project references `Mocha.Analyzers` as an analyzer (not a regular project reference) + +## Handlers are not being called + +If dispatch succeeds but your handler code does not execute, check that: + +- Your middleware calls the `next` delegate - a middleware that forgets to call `next` silently short-circuits the pipeline +- You are not accidentally registering handlers manually in addition to the source-generated method, which could result in duplicate registrations + +## The source-generated method does not appear + +If IntelliSense does not show `Add{ModuleName}()`: + +- Confirm the `Mocha.Analyzers` package is referenced with `OutputItemType="Analyzer"` in your `.csproj` +- Rebuild the project - source generators run during compilation +- Check the build output for analyzer warnings prefixed with `MO` + +## Named mediator returns wrong handlers + +Each named mediator resolves handlers from the same DI container. Make sure you register each module's handlers on the correct `IMediatorHostBuilder` instance - the one returned by the `AddMediator("name")` call for that name. + +# Next steps + +You have a working mediator with CQRS dispatch. Here is where to go next: + +- **Customize the pipeline:** [Pipeline & Middleware](/docs/mocha/v1/mediator/pipeline-and-middleware) - add validation, logging, transactions, and other cross-cutting concerns using `MediatorMiddleware` and `MediatorMiddlewareConfiguration`. diff --git a/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md new file mode 100644 index 00000000000..969c824cf7f --- /dev/null +++ b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md @@ -0,0 +1,573 @@ +--- +title: "Pipeline & Middleware" +description: "Add cross-cutting concerns to the Mocha Mediator dispatch pipeline. Write middleware for logging, validation, transactions, and exception handling. Use compile-time filtering to eliminate middleware from pipelines where it does not apply. Configure notification strategies, Entity Framework Core transactions, and OpenTelemetry instrumentation." +--- + +```csharp +public static class LoggingMiddleware +{ + public static MediatorMiddlewareConfiguration Create() + => new( + static (factoryCtx, next) => + { + var logger = factoryCtx.Services.GetRequiredService() + .CreateLogger("Pipeline.Logging"); + + return ctx => + { + logger.LogInformation("Handling {MessageType}...", ctx.MessageType.Name); + + var sw = Stopwatch.StartNew(); + var task = next(ctx); + + if (task.IsCompletedSuccessfully) + { + sw.Stop(); + logger.LogInformation("Handled {MessageType} in {ElapsedMs}ms", + ctx.MessageType.Name, sw.ElapsedMilliseconds); + return default; + } + + return Awaited(task, sw, logger, ctx.MessageType.Name); + + static async ValueTask Awaited( + ValueTask t, Stopwatch sw, ILogger log, string msgType) + { + await t.ConfigureAwait(false); + sw.Stop(); + log.LogInformation("Handled {MessageType} in {ElapsedMs}ms", + msgType, sw.ElapsedMilliseconds); + } + }; + }, + "Logging"); +} +``` + +That is a middleware. It wraps every command, query, and notification with timing and logging. Register it with `.Use()` and it runs for every message that passes through the pipeline. + +# How the pipeline works + +The mediator compiles a middleware pipeline for each registered message type at application startup. Each middleware wraps the next one, forming a [chain of responsibility](https://refactoring.guru/design-patterns/chain-of-responsibility) that terminates at the handler. If you have used [middleware in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/), the mental model is the samethis is the [Pipes and Filters](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PipesAndFilters.html) pattern applied to in-process message dispatch. + +```text +SendAsync(PlaceOrderCommand) + -> LoggingMiddleware + -> ValidationMiddleware + -> TransactionMiddleware + -> PlaceOrderCommandHandler + <- commit / rollback + <- throw on invalid + <- log elapsed time +``` + +The pipeline is built from two delegate types: + +```csharp +// The terminal pipeline delegate — each step in the chain has this shape +public delegate ValueTask MediatorDelegate(IMediatorContext context); + +// The factory that creates a middleware — runs once per message type at startup +public delegate MediatorDelegate MediatorMiddleware( + MediatorMiddlewareFactoryContext context, + MediatorDelegate next); +``` + +At startup, the mediator iterates every registered middleware in reverse order. Each factory receives the `next` delegate and returns a new delegate that wraps it. The result is a single compiled `MediatorDelegate` per message type, stored in a frozen dictionary. At runtime, dispatch is a dictionary lookup followed by a single delegate invocation - no reflection, no generic resolution. + +# Write a middleware + +A middleware is a static class with a `Create()` method that returns a `MediatorMiddlewareConfiguration`. The configuration holds two things: the factory delegate and an optional string key used for [positioning](#middleware-positioning). + +The factory delegate receives two arguments: + +| Argument | Available at | Purpose | +| --- | --- | --- | +| `MediatorMiddlewareFactoryContext` | Startup (compile time) | Resolve singleton services, inspect message/response types, opt out of the pipeline | +| `MediatorDelegate next` | Startup (compile time) | The next middleware or handler in the chain | + +The factory returns a `MediatorDelegate` - the runtime function that receives `IMediatorContext` for each dispatch. + +Here is a minimal timing middleware, step by step: + +```csharp +public static class TimingMiddleware +{ + public static MediatorMiddlewareConfiguration Create() + => new( + static (factoryCtx, next) => + { + // 1. Resolve services once at startup (not per request) + var logger = factoryCtx.Services.GetRequiredService() + .CreateLogger("Pipeline.Timing"); + + // 2. Return the runtime delegate + return async ctx => + { + var sw = Stopwatch.StartNew(); + + await next(ctx); // 3. Call the next middleware or handler + + sw.Stop(); + logger.LogInformation( + "{MessageType} handled in {ElapsedMs}ms", + ctx.MessageType.Name, + sw.ElapsedMilliseconds); + }; + }, + "Timing"); // 4. Key for positioning +} +``` + +Register it on the mediator builder: + +```csharp +builder.Services + .AddMediator() + .AddCatalog() // source-generated handler registration + .Use(TimingMiddleware.Create()); +``` + +The `IMediatorContext` available at runtime provides everything you need during dispatch: + +| Property | Type | Description | +| --- | --- | --- | +| `Message` | `object` | The message instance being dispatched | +| `MessageType` | `Type` | Runtime type of the message | +| `ResponseType` | `Type` | Expected response type (`Unit` for void commands and notifications) | +| `Result` | `object?` | The handler's return value - set by the terminal delegate, readable by middleware after calling `next` | +| `Services` | `IServiceProvider` | Scoped service provider for the current request | +| `CancellationToken` | `CancellationToken` | Cancellation token for the operation | +| `Features` | `IFeatureCollection` | Per-request feature collection for sharing state between middleware | +| `Runtime` | `IMediatorRuntime` | The mediator runtime that owns this context | + +## Short-circuiting + +To prevent the handler from executing, return without calling `next`: + +```csharp +return ctx => +{ + if (ctx.Message is PlaceOrderCommand { Quantity: <= 0 }) + throw new ArgumentException("Quantity must be greater than zero."); + + return next(ctx); // only reached if validation passes +}; +``` + +## Exception handling + +Wrap `next` in a try/catch to handle exceptions: + +```csharp +public static class ExceptionHandlingMiddleware +{ + public static MediatorMiddlewareConfiguration Create() + => new( + static (factoryCtx, next) => + { + var logger = factoryCtx.Services.GetRequiredService() + .CreateLogger("Pipeline.ExceptionHandler"); + + return async ctx => + { + try + { + await next(ctx); + } + catch (Exception ex) + { + logger.LogError(ex, "Error handling {MessageType}", + ctx.MessageType.Name); + throw; // re-throw or set ctx.Result to recover + } + }; + }, + "ExceptionHandling"); +} +``` + +To recover from an exception instead of re-throwing, set `ctx.Result` to a fallback value and return normally. + +## Synchronous fast-path optimization + +When `next` completes synchronously (common for in-memory handlers), you can avoid the `async` state machine overhead by checking `IsCompletedSuccessfully`: + +```csharp +return ctx => +{ + logger.LogInformation("Before"); + + var task = next(ctx); + + if (task.IsCompletedSuccessfully) + { + logger.LogInformation("After (sync)"); + return default; + } + + return Awaited(task, logger); + + static async ValueTask Awaited(ValueTask t, ILogger log) + { + await t.ConfigureAwait(false); + log.LogInformation("After (async)"); + } +}; +``` + +This pattern avoids allocating an async state machine when the pipeline completes synchronously. Use it in performance-sensitive middleware; use plain `async`/`await` everywhere else. + +# Compile-time filtering + +The `MediatorMiddlewareFactoryContext` is available during pipeline compilation - before your application handles its first request. Use it to exclude your middleware from pipelines where it does not apply. + +To opt out, return `next` directly from the factory. The middleware is not included in that pipeline at all - zero runtime cost, no delegate wrapper, no type check on every dispatch. + +```csharp +public static class TransactionMiddleware +{ + public static MediatorMiddlewareConfiguration Create() + => new( + static (factoryCtx, next) => + { + // Skip notifications and queries at compile time + if (factoryCtx.IsNotification() || factoryCtx.IsQuery()) + return next; // not included in this pipeline + + return async ctx => + { + // Resolve DbContext from the scoped service provider + var db = ctx.Services.GetRequiredService(); + + await using var tx = await db.Database + .BeginTransactionAsync(ctx.CancellationToken); + try + { + await next(ctx); + await db.SaveChangesAsync(ctx.CancellationToken); + await tx.CommitAsync(ctx.CancellationToken); + } + catch + { + await tx.RollbackAsync(ctx.CancellationToken); + throw; + } + }; + }, + "Transaction"); +} +``` + +## Message kind checks + +| Method | Returns true when | +| --- | --- | +| `IsCommand()` | Void command (`ICommand`) | +| `IsCommandWithResponse()` | Command with response (`ICommand`) | +| `IsQuery()` | Query (`IQuery`) | +| `IsNotification()` | Notification (`INotification`) | + +## Type assignability checks + +| Method | Returns true when | +| --- | --- | +| `IsMessageAssignableTo()` | Message type is assignable to `T` | +| `IsMessageAssignableTo(Type)` | Message type is assignable to the given type | +| `IsResponseAssignableTo()` | Response type is assignable to `T` (false for void commands and notifications) | +| `IsResponseAssignableTo(Type)` | Response type is assignable to the given type | + +Use `IsMessageAssignableTo` to scope a middleware to a specific message or base type: + +```csharp +public static class PlaceOrderValidationMiddleware +{ + public static MediatorMiddlewareConfiguration Create() + => new( + static (factoryCtx, next) => + { + // Only compile into the PlaceOrderCommand pipeline + if (!factoryCtx.IsMessageAssignableTo()) + return next; + + return ctx => + { + if (ctx.Message is PlaceOrderCommand order && order.Quantity <= 0) + throw new ArgumentException("Quantity must be greater than zero."); + return next(ctx); + }; + }, + "Validation"); +} +``` + +Use `IsResponseAssignableTo` to filter by response type: + +```csharp +// Only audit pipelines that return OrderResult +if (!factoryCtx.IsResponseAssignableTo()) + return next; +``` + +## When to use compile-time vs. runtime checks + +Use **compile-time filtering** (`MediatorMiddlewareFactoryContext`) when: + +- You know at registration time which message kinds the middleware applies to +- You want zero overhead for pipelines that do not need the middleware +- You are filtering by message kind, response type, or base class + +Use **runtime checks** (`IMediatorContext`) when: + +- You need to inspect the actual message instance (check a property value) +- The decision depends on runtime state (feature flags, configuration) + +Both approaches combine well - filter out entire message kinds at compile time, then do finer-grained checks at runtime for the pipelines that remain. + +# Middleware positioning + +Register middleware with `Use`, `Prepend`, or `Append` to control where it sits in the pipeline. + +| Method | Behavior | +| --- | --- | +| `Use(config)` | Appends to the end of the middleware list | +| `Prepend(config)` | Inserts at the beginning | +| `Prepend("Logging", config)` | Inserts before the middleware with key `"Logging"` | +| `Append("Instrumentation", config)` | Inserts after the middleware with key `"Instrumentation"` | + +If the referenced key is not found, `Prepend(key, ...)` falls back to inserting at the beginning and `Append(key, ...)` falls back to appending at the end. + +```csharp +builder.Services + .AddMediator() + .AddCatalog() + .Use(LoggingMiddleware.Create()) // position 1 + .Use(ValidationMiddleware.Create()) // position 2 + .Use(ExceptionHandlingMiddleware.Create()) // position 3 + .Prepend("Logging", SecurityMiddleware.Create()) // before "Logging" + .Append("Logging", CorrelationIdMiddleware.Create()); // after "Logging" +``` + +Resulting order: Security -> Logging -> CorrelationId -> Validation -> ExceptionHandling -> Handler. + +The `Key` property on `MediatorMiddlewareConfiguration` is optional. Middleware without a key can still be registered with `Use` and `Prepend(config)`, but cannot be referenced by other middleware for relative positioning. + +## Built-in middleware keys + +| Key | Middleware | Added by | +| --- | --- | --- | +| `"Instrumentation"` | `MediatorDiagnosticMiddleware` | Always present (added by `MediatorBuilder` constructor) | +| `"EntityFrameworkTransaction"` | `EntityFrameworkTransactionMiddleware` | `UseEntityFrameworkTransactions()` | + +# Pipeline execution order + +Middleware executes in registration order. The first registered middleware becomes the outermost layer - it runs first on the way in and last on the way out. + +```text +Registered: [Instrumentation, Logging, Validation, Transaction] + +Instrumentation <- outermost (runs first) + Logging + Validation + Transaction + Handler <- innermost + Transaction returns + Validation returns + Logging returns +Instrumentation returns <- runs last on the way out +``` + +The `Instrumentation` middleware is always present as the first entry because `MediatorBuilder` adds it in its constructor. Your middleware registered via `Use()` follows in the order you call it. + +# Notification strategies + +When a notification has multiple handlers, the **notification strategy** controls how they are invoked. Mocha ships two strategies: + +| Strategy | Behavior | Default | +| --- | --- | --- | +| `ForeachAwaitPublisher` | Invokes handlers one at a time, sequentially | Yes | +| `TaskWhenAllPublisher` | Invokes all handlers concurrently with `Task.WhenAll` | No | + +The default `ForeachAwaitPublisher` guarantees ordering - handlers execute in registration order. If a handler throws, subsequent handlers do not execute. + +To switch to concurrent execution: + +```csharp +builder.Services.AddSingleton(); +``` + +With `TaskWhenAllPublisher`, all handlers run concurrently. If any handler throws, the aggregate exception propagates after all handlers complete. + +## Custom notification strategies + +Implement `INotificationStrategy` to control dispatch behavior: + +```csharp +public class FireAndForgetPublisher : INotificationStrategy +{ + public ValueTask PublishAsync( + IReadOnlyList> handlers, + TNotification notification, + CancellationToken cancellationToken) + where TNotification : INotification + { + for (var i = 0; i < handlers.Count; i++) + { + _ = handlers[i].HandleAsync(notification, cancellationToken); + } + + return ValueTask.CompletedTask; + } +} +``` + +Register it as a singleton: + +```csharp +builder.Services.AddSingleton(); +``` + +# Entity Framework Core transactions + +The `Mocha.EntityFrameworkCore` package provides middleware that wraps command handlers in a database transaction. Install the package and call `UseEntityFrameworkTransactions`: + +```csharp +builder.Services + .AddMediator() + .AddCatalog() + .UseEntityFrameworkTransactions(); +``` + +The middleware (key: `"EntityFrameworkTransaction"`): + +1. Checks at compile time whether the pipeline is for a command. Queries and notifications are excluded by default - the middleware is not present in their pipelines at all. +2. Begins a database transaction +3. Calls the next middleware or handler +4. Calls `SaveChangesAsync` on success +5. Commits the transaction +6. Rolls back on any exception + +Your command handlers do not need to call `SaveChangesAsync` or manage transactions - the middleware handles both. + +## Customizing transaction scope + +To override which messages get a transaction, provide a `ShouldCreateTransaction` predicate: + +```csharp +builder.Services + .AddMediator() + .AddCatalog() + .UseEntityFrameworkTransactions(options => + { + options.ShouldCreateTransaction = context => + { + // Wrap everything except this specific query + return context.MessageType != typeof(GetCachedReportQuery); + }; + }); +``` + +When `ShouldCreateTransaction` is set, the compile-time elimination for queries and notifications is disabled - the middleware is included in every pipeline and the predicate runs at dispatch time instead. + +The `ShouldCreateTransaction` delegate receives the `IMediatorContext`, giving you access to the message type and instance for fine-grained control. + +# Instrumentation and observability + +The `MediatorDiagnosticMiddleware` (key: `"Instrumentation"`) is always present in the pipeline. By default it uses a no-op listener. To activate OpenTelemetry-compatible tracing, call `AddInstrumentation`: + +```csharp +builder.Services + .AddMediator() + .AddCatalog() + .AddInstrumentation(); +``` + +This registers the `ActivityMediatorDiagnosticListener`, which follows the [OpenTelemetry messaging semantic conventions](https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/): + +- Creates an [`Activity`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts) (OpenTelemetry span) named `"{MessageTypeName} send"` for commands/queries or `"{MessageTypeName} publish"` for notifications +- Tags the span with `messaging.system` = `"mocha.mediator"`, `messaging.operation.type` = `"send"` or `"publish"`, and `messaging.message.type` = the message type name +- Sets `ActivityStatusCode.Ok` on success +- On error: adds an `exception` event with `exception.type` and `exception.message` tags, and sets `ActivityStatusCode.Error` + +Configure your OpenTelemetry exporter to collect from the `Mocha.Mediator` source: + +```csharp +builder.Services.AddOpenTelemetry() + .WithTracing(t => t.AddSource("Mocha.Mediator")) + .WithMetrics(m => m.AddMeter("Mocha.Mediator")); +``` + +## Custom diagnostic event listeners + +To add your own instrumentation alongside or instead of the built-in listener, extend `MediatorDiagnosticEventListener`: + +```csharp +public class SlowMessageListener : MediatorDiagnosticEventListener +{ + public override IDisposable Execute( + Type messageType, Type responseType, object message) + { + return new TimingScope(messageType); + } + + public override void ExecutionError( + Type messageType, Type responseType, object message, Exception exception) + { + // log or alert on errors + } + + private sealed class TimingScope(Type messageType) : IDisposable + { + private readonly long _start = Stopwatch.GetTimestamp(); + + public void Dispose() + { + var elapsed = Stopwatch.GetElapsedTime(_start); + if (elapsed > TimeSpan.FromSeconds(1)) + Console.WriteLine($"Slow message: {messageType.Name} took {elapsed}"); + } + } +} +``` + +Register it: + +```csharp +builder.Services + .AddMediator() + .AddCatalog() + .AddDiagnosticEventListener(); +``` + +Multiple listeners can be registered. They all receive every diagnostic event in registration order. + +# Troubleshooting + +## Middleware does not run for a specific message type + +If your middleware factory returns `next` for that message type (via compile-time filtering), the middleware is excluded from the pipeline entirely. Check your `IsCommand()`, `IsQuery()`, `IsNotification()`, or `IsMessageAssignableTo()` conditions. The filtering runs once at startup, so you will not see any runtime indication that the middleware was skipped. + +## Middleware runs in the wrong order + +Middleware executes in registration order (first registered = outermost). Use `Prepend` or `Append` with a key to control placement relative to other middleware. Check that the middleware you are referencing has a `Key` set in its `MediatorMiddlewareConfiguration`. + +## Entity Framework transactions do not wrap queries + +This is the default behavior. The `EntityFrameworkTransactionMiddleware` excludes queries and notifications at compile time. To include specific queries, set `ShouldCreateTransaction` in the options. Note that setting this predicate disables compile-time elimination - the middleware will be included in all pipelines and the predicate runs at dispatch time. + +## No OpenTelemetry traces appear + +The `MediatorDiagnosticMiddleware` is always present, but it uses a no-op listener by default. You must call `.AddInstrumentation()` to register the `ActivityMediatorDiagnosticListener`. You also need to configure your OpenTelemetry SDK to collect from the `Mocha.Mediator` source via `.AddSource("Mocha.Mediator")`. + +## Services resolved in the factory vs. at runtime + +Services resolved from `factoryCtx.Services` in the middleware factory are resolved once at startup from the mediator's internal service provider. Use this for singletons like `ILoggerFactory`. To resolve scoped services (like `DbContext`), use `ctx.Services` inside the runtime delegate instead. + +> **Full demo:** The [Demo application](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Demo) uses `UseEntityFrameworkTransactions` and `AddInstrumentation` alongside the mediator and message bus in a complete e-commerce system. + +# Next steps + +- **Mediator overview:** [Overview](/docs/mocha/v1/mediator)messages, handlers, dispatching, and registration. +- **Message bus middleware:** [Middleware & Pipelines](/docs/mocha/v1/middleware-and-pipelines)the message bus has its own three-layer pipeline (dispatch, receive, consume) using the same middleware model. +- **Cross service boundaries:** [Messaging Patterns](/docs/mocha/v1/messaging-patterns) -when your commands need to reach another service, switch to the message bus. +- **Coordinate workflows:** [Sagas](/docs/mocha/v1/sagas)orchestrate multi-step processes across services. diff --git a/website/src/docs/mocha/v1/messages.md b/website/src/docs/mocha/v1/messages.md index 4fd4fae66e8..d3f658ed973 100644 --- a/website/src/docs/mocha/v1/messages.md +++ b/website/src/docs/mocha/v1/messages.md @@ -204,4 +204,4 @@ Now that you understand message structure, learn the three messaging patterns. - [**Messaging Patterns**](/docs/mocha/v1/messaging-patterns) - Pub/sub events, point-to-point commands, and request/reply. -> **Full demo:** [Demo.Contracts](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Demo/Demo.Contracts) contains a complete set of message contracts for an e-commerce system -- events (`OrderPlacedEvent`, `PaymentCompletedEvent`), send messages (`ProcessRefundCommand`, `ReserveInventoryCommand`), and request/reply pairs used by sagas. +> **Full demo:** [Demo.Contracts](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Demo/Demo.Contracts) contains a complete set of message contracts for an e-commerce system - events (`OrderPlacedEvent`, `PaymentCompletedEvent`), send messages (`ProcessRefundCommand`, `ReserveInventoryCommand`), and request/reply pairs used by sagas. From b535b6d2cff20266878053fa9c7b9a8b54b0fa06 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 22 Mar 2026 20:51:30 +0000 Subject: [PATCH 2/7] cleanup --- website/src/docs/mocha/v1/mediator/index.md | 79 +------------------ .../v1/mediator/pipeline-and-middleware.md | 8 +- 2 files changed, 8 insertions(+), 79 deletions(-) diff --git a/website/src/docs/mocha/v1/mediator/index.md b/website/src/docs/mocha/v1/mediator/index.md index a5f6d0c1424..776b506c69e 100644 --- a/website/src/docs/mocha/v1/mediator/index.md +++ b/website/src/docs/mocha/v1/mediator/index.md @@ -265,56 +265,6 @@ using Mocha.Mediator; [assembly: MediatorModule("Billing")] ``` -## What the source generator produces - -At compile time, the Mocha source generator: - -1. **Scans** your assembly for all handler implementations -2. **Generates** an `Add{ModuleName}()` extension method on `IMediatorHostBuilder` -3. **Registers** all handlers with the configured `ServiceLifetime` -4. **Creates** `MediatorPipelineConfiguration` entries with terminal delegates built via `PipelineBuilder` - -For example, given assembly name `Demo.Catalog` and this handler: - -```csharp -public sealed class PlaceOrderCommandHandler - : ICommandHandler { ... } -``` - -The generator produces: - -```csharp -public static class CatalogMediatorBuilderExtensions -{ - public static IMediatorHostBuilder AddCatalog( - this IMediatorHostBuilder builder) - { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - - // Register handlers - services.Add(new ServiceDescriptor( - typeof(ICommandHandler), - typeof(PlaceOrderCommandHandler), - lifetime)); - - // Register pipelines - MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PlaceOrderCommand), - ResponseType = typeof(PlaceOrderResult), - Terminal = PipelineBuilder - .BuildCommandTerminal() - }); - }); - - return builder; - } -} -``` - ## Configure service lifetime By default, handlers are registered as `Scoped`. To change the default: @@ -331,23 +281,6 @@ builder.Services Call `ConfigureOptions` before `Add{ModuleName}()` so the source-generated method reads the updated lifetime. -## How the mediator dispatches - -When you call `SendAsync`, `QueryAsync`, or `PublishAsync`, the `Mediator` class: - -1. **Rents** a `MediatorContext` from an object pool (thread-static fast path, then `ObjectPool` fallback) -2. **Initializes** the context with the message, message type, service provider, and cancellation token -3. **Looks up** the pre-compiled pipeline delegate for the message type (O(1) frozen dictionary lookup) -4. **Invokes** the pipeline delegate -5. **Returns** the context to the pool - -## Performance characteristics - -- **Context pooling:** `MediatorContext` objects are pooled with a thread-static fast path for zero-contention reuse -- **Zero-allocation async:** Uses `PoolingAsyncValueTaskMethodBuilder` to avoid allocating `Task` objects on the hot path -- **O(1) pipeline lookup:** Pre-compiled pipelines stored in a frozen dictionary, built once at startup -- **No reflection at runtime:** All handler resolution and pipeline compilation happens at startup or compile time - # Named mediators To run multiple independent mediator instances (each with its own handlers and middleware), use named mediators. Named mediators use .NET's keyed dependency injection. @@ -381,17 +314,7 @@ var billingMediator = serviceProvider .GetRequiredKeyedService("billing"); ``` -Each named mediator has its own handler registrations, middleware pipeline, and `MediatorRuntime`. The default mediator (registered with `AddMediator()` without a name) is resolved normally without keyed services. - -# The Unit type - -`Unit` is a readonly struct that represents void in generic contexts. You do not need it for normal command/query dispatch - the framework handles void commands through `ICommand` and `ICommandHandler`. `Unit` is available if you need a void-equivalent type in your own generic code. - -```csharp -// Unit.Value — the singleton instance -// Unit.ValueTask — a pre-completed ValueTask -// Unit.Task — a pre-completed Task -``` +Each named mediator has its own handler registrations, middleware pipeline, and runtime. The default mediator (registered with `AddMediator()` without a name) is resolved normally without keyed services. # Putting it together diff --git a/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md index 969c824cf7f..242719b948d 100644 --- a/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md +++ b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md @@ -135,7 +135,7 @@ The `IMediatorContext` available at runtime provides everything you need during | `Message` | `object` | The message instance being dispatched | | `MessageType` | `Type` | Runtime type of the message | | `ResponseType` | `Type` | Expected response type (`Unit` for void commands and notifications) | -| `Result` | `object?` | The handler's return value - set by the terminal delegate, readable by middleware after calling `next` | +| `Result` | `object?` | The handler's return value, readable by middleware after calling `next` | | `Services` | `IServiceProvider` | Scoped service provider for the current request | | `CancellationToken` | `CancellationToken` | Cancellation token for the operation | | `Features` | `IFeatureCollection` | Per-request feature collection for sharing state between middleware | @@ -233,7 +233,9 @@ public static class TransactionMiddleware { // Skip notifications and queries at compile time if (factoryCtx.IsNotification() || factoryCtx.IsQuery()) + { return next; // not included in this pipeline + } return async ctx => { @@ -288,7 +290,9 @@ public static class PlaceOrderValidationMiddleware { // Only compile into the PlaceOrderCommand pipeline if (!factoryCtx.IsMessageAssignableTo()) + { return next; + } return ctx => { @@ -306,7 +310,9 @@ Use `IsResponseAssignableTo` to filter by response type: ```csharp // Only audit pipelines that return OrderResult if (!factoryCtx.IsResponseAssignableTo()) +{ return next; +} ``` ## When to use compile-time vs. runtime checks From 833636f6809b660c368cc703133e0ae691e6700a Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 22 Mar 2026 22:14:06 +0100 Subject: [PATCH 3/7] cleanup --- .../Commands/Stages/DeleteStageCommand.cs | 75 ++ .../Stages/DeleteStageCommand.graphql | 15 + .../Commands/Stages/StageCommand.cs | 1 + .../src/CommandLine/schema.graphql | 643 +++++++++--------- 4 files changed, 422 insertions(+), 312 deletions(-) create mode 100644 src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.cs create mode 100644 src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.graphql diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.cs b/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.cs new file mode 100644 index 00000000000..0f7b99759b2 --- /dev/null +++ b/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.cs @@ -0,0 +1,75 @@ +using System.CommandLine.Invocation; +using ChilliCream.Nitro.CommandLine.Client; +using ChilliCream.Nitro.CommandLine.Commands.Stages.Components; +using ChilliCream.Nitro.CommandLine.Configuration; +using ChilliCream.Nitro.CommandLine.Helpers; +using ChilliCream.Nitro.CommandLine.Options; +using ChilliCream.Nitro.CommandLine.Results; +using ChilliCream.Nitro.CommandLine.Services.Sessions; +using static ChilliCream.Nitro.CommandLine.ThrowHelper; + +namespace ChilliCream.Nitro.CommandLine.Commands.Stages; + +internal sealed class DeleteStageCommand : Command +{ + public DeleteStageCommand() : base("delete") + { + Description = "Deletes a stage by name"; + + AddOption(Opt.Instance); + AddOption(Opt.Instance); + AddOption(Opt.Instance); + + this.SetHandler( + ExecuteAsync, + Bind.FromServiceProvider(), + Bind.FromServiceProvider(), + Bind.FromServiceProvider(), + Bind.FromServiceProvider()); + } + + private static async Task ExecuteAsync( + InvocationContext context, + IAnsiConsole console, + IApiClient client, + CancellationToken cancellationToken) + { + const string apiMessage = "For which API do you want to force delete a stage?"; + var apiId = await context.GetOrSelectApiId(apiMessage); + + var stageName = context.ParseResult.GetValueForOption(Opt.Instance)!; + + var shouldDelete = await context.ConfirmWhenNotForced( + $"Do you really want to force delete stage {stageName.AsHighlight()}", + cancellationToken); + + if (!shouldDelete) + { + throw Exit("Stage was not deleted"); + } + + var input = new ForceDeleteStageByIdInput { ApiId = apiId, StageName = stageName }; + var result = await client.ForceDeleteStageByIdCommandMutation + .ExecuteAsync(input, cancellationToken); + + console.EnsureNoErrors(result); + var data = console.EnsureData(result); + console.PrintErrorsAndExit(data.ForceDeleteStageById.Errors); + + var stages = data.ForceDeleteStageById.Api?.Stages; + if (stages is null) + { + throw Exit("Could not delete the stage"); + } + + var items = stages + .Select(x => StageDetailPrompt.From(x).ToObject()) + .ToArray(); + + context.SetResult(new PaginatedListResult(items, null)); + + console.OkLine($"Stage {stageName.AsHighlight()} was force deleted"); + + return ExitCodes.Success; + } +} diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.graphql b/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.graphql new file mode 100644 index 00000000000..af08bb44b5f --- /dev/null +++ b/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.graphql @@ -0,0 +1,15 @@ +mutation ForceDeleteStageByIdCommandMutation($input: ForceDeleteStageByIdInput!) { + forceDeleteStageById(input: $input) { + api { + stages { + ...StageDetailPrompt_Stage + } + } + errors { + ...Error + ...ApiNotFoundError + ...StageNotFoundError + ...UnauthorizedOperation + } + } +} diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/StageCommand.cs b/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/StageCommand.cs index 08b15b73307..2e9e4c84038 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/StageCommand.cs +++ b/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/StageCommand.cs @@ -17,6 +17,7 @@ public StageCommand() : base("stage") this.AddNitroCloudDefaultOptions(); AddCommand(new EditStagesCommand()); + AddCommand(new DeleteStageCommand()); AddCommand(new ListStagesCommand()); } } diff --git a/src/Nitro/CommandLine/src/CommandLine/schema.graphql b/src/Nitro/CommandLine/src/CommandLine/schema.graphql index e58f6008971..d8d69c85ba5 100644 --- a/src/Nitro/CommandLine/src/CommandLine/schema.graphql +++ b/src/Nitro/CommandLine/src/CommandLine/schema.graphql @@ -280,35 +280,34 @@ interface WorkspaceChangeLog { } type AfterStageCondition { - afterStage: Stage + afterStage: Stage @cost(weight: "10") } type Api implements Node { - clients("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ClientsConnection + clients("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ClientsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") createdAt: DateTime! createdBy: UserInfo! - documents("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApiDocumentConnection + documents("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApiDocumentConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") httpConnection: ApiHttpConnection id: ID! kind: ApiKind - mcpFeatureCollections("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiMcpFeatureCollectionsConnection - mockSchemas("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): MockSchemasConnection + mcpFeatureCollections("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiMcpFeatureCollectionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + mockSchemas("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): MockSchemasConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") modifiedAt: DateTime! modifiedBy: UserInfo! name: String! - openApiCollections("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiOpenApiCollectionsConnection - operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OperationsConnection + openApiCollections("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiOpenApiCollectionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OperationsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") path: [String!]! - referenceName: String! - schemaVersions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): SchemaVersionsConnection + schemaVersions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): SchemaVersionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") settings: ApiSettings! - stages: [Stage!]! + stages: [Stage!]! @cost(weight: "10") version: Version! - workspace: Workspace + workspace: Workspace @cost(weight: "10") } type ApiChanged implements ApiChangeLog & WorkspaceChangeLog { - api(onlyIfLatest: Boolean): Api + api(onlyIfLatest: Boolean): Api @cost(weight: "10") apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -320,7 +319,7 @@ type ApiChanged implements ApiChangeLog & WorkspaceChangeLog { } type ApiCreated implements ApiChangeLog & WorkspaceChangeLog { - api(onlyIfLatest: Boolean): Api + api(onlyIfLatest: Boolean): Api @cost(weight: "10") apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -332,7 +331,7 @@ type ApiCreated implements ApiChangeLog & WorkspaceChangeLog { } type ApiDeleted implements ApiChangeLog & WorkspaceChangeLog { - api(onlyIfLatest: Boolean): Api + api(onlyIfLatest: Boolean): Api @cost(weight: "10") apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -349,7 +348,7 @@ type ApiDeletionFailedError implements Error { } type ApiDocument implements Node { - api: Api + api: Api @cost(weight: "10") body: String! createdAt: DateTime! createdBy: UserInfo! @@ -363,7 +362,7 @@ type ApiDocument implements Node { } type ApiDocumentChanged implements ApiDocumentChangeLog & WorkspaceChangeLog { - apiDocument(onlyIfLatest: Boolean): ApiDocument + apiDocument(onlyIfLatest: Boolean): ApiDocument @cost(weight: "10") apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -386,7 +385,7 @@ type ApiDocumentConnection { } type ApiDocumentCreated implements ApiDocumentChangeLog & WorkspaceChangeLog { - apiDocument(onlyIfLatest: Boolean): ApiDocument + apiDocument(onlyIfLatest: Boolean): ApiDocument @cost(weight: "10") apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -399,7 +398,7 @@ type ApiDocumentCreated implements ApiDocumentChangeLog & WorkspaceChangeLog { } type ApiDocumentDeleted implements ApiDocumentChangeLog & WorkspaceChangeLog { - apiDocument(onlyIfLatest: Boolean): ApiDocument + apiDocument(onlyIfLatest: Boolean): ApiDocument @cost(weight: "10") apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -467,9 +466,9 @@ type ApiKey implements Node { createdBy: UserInfo! id: ID! name: String! - roleAssignments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiKeyRoleAssignmentsConnection + roleAssignments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiKeyRoleAssignmentsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") scopes: [ApiKeyScope!]! - workspace: Workspace + workspace: Workspace @cost(weight: "10") } type ApiKeyNotFoundError implements Error { @@ -497,7 +496,7 @@ type ApiKeyRoleAssignmentsEdge { type ApiKeyScope { kind: String! - reference: ApiKeyReference + reference: ApiKeyReference @cost(weight: "10") referenceId: String! } @@ -566,7 +565,7 @@ type ApiOpenApiCollectionsEdge { } type ApiPermissionScope implements PermissionScope { - api: Api + api: Api @cost(weight: "10") id: ID! type: String! } @@ -629,15 +628,15 @@ type AuthorizationEventLog { eventId: UUID! eventType: AuthorizationEventType! isConditional: Boolean! - organization: Organization + organization: Organization @cost(weight: "10") permission: String! - principal: AuthorizationEventLogPrincipal - realm: AuthorizationEventLogRealm - resource: AuthorizationEventLogResource - subject: AuthorizationEventLogSubject + principal: AuthorizationEventLogPrincipal @cost(weight: "10") + realm: AuthorizationEventLogRealm @cost(weight: "10") + resource: AuthorizationEventLogResource @cost(weight: "10") + subject: AuthorizationEventLogSubject @cost(weight: "10") timestamp: DateTime! traceId: String! - workspace: Workspace + workspace: Workspace @cost(weight: "10") } type BasicAuthenticationFlowOptions { @@ -694,13 +693,13 @@ type ChangesEdge { } type Client implements Node { - api: Api + api: Api @cost(weight: "10") createdAt: DateTime! createdBy: UserInfo! id: ID! name: String! - operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OperationsConnection - versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ClientVersionConnection + operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OperationsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ClientVersionConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } type ClientChangeLog implements StageChangeLog & Node { @@ -717,7 +716,7 @@ type ClientDeletedStageChangeEvent implements StageChangedEvent { type ClientDeployment implements Node & Deployment { approval: DeploymentApproval - client: Client + client: Client @cost(weight: "10") createdAt: DateTime! errors: [ClientDeploymentError!]! id: ID! @@ -729,9 +728,9 @@ type ClientDeployment implements Node & Deployment { type ClientInsight { averageLatency: Float - client: Client + client: Client @cost(weight: "10") errorRate: Float - id: ID! + id: ID! @cost(weight: "10") impact: Float name: String opm: Float @@ -764,10 +763,10 @@ type ClientNotFoundError implements Error { } type ClientVersion implements Node { - client: Client + client: Client @cost(weight: "10") createdAt: DateTime! id: ID! - persistedQueries("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): PersistedQueriesConnection + persistedQueries("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): PersistedQueriesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") publishedTo: [PublishedClientVersion!]! tag: String! tags: [String!]! @deprecated(reason: "Use `tag` instead.") @@ -803,12 +802,12 @@ type ClientVersionPublishFailed implements ClientVersionPublishResult { } type ClientVersionPublishSuccess implements ClientVersionPublishResult { - clientVersion: ClientVersion + clientVersion: ClientVersion @cost(weight: "10") state: ProcessingState! } type ClientVersionPublishedStageChangeEvent implements StageChangedEvent { - clientVersion: ClientVersion + clientVersion: ClientVersion @cost(weight: "10") kind: StageChangeKind! } @@ -818,7 +817,7 @@ type ClientVersionRequestNotFoundError implements Error { } type ClientVersionUnpublishedStageChangeEvent implements StageChangedEvent { - clientVersion: ClientVersion + clientVersion: ClientVersion @cost(weight: "10") kind: StageChangeKind! } @@ -859,7 +858,7 @@ type ConcurrentOperationError implements Error & SchemaVersionPublishError & Cli } type CoordinateClientUsage { - client: Client + client: Client @cost(weight: "10") metrics: CoordinateClientUsageMetrics! name: String totalOperations: Long! @@ -869,7 +868,7 @@ type CoordinateClientUsage { type CoordinateClientUsageMetrics implements Node { id: ID! - operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): CoordinateClientUsageOperationInsightsConnection + operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): CoordinateClientUsageOperationInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } type CoordinateClientUsageOperationInsight { @@ -916,16 +915,16 @@ type CoordinateRequestGraphData { type CoordinateUsage { clientCount: Long! - errorRate: Float - firstSeen: DateTime - lastSeen: DateTime - meanDuration: Float + errorRate: Float @cost(weight: "10") + firstSeen: DateTime @cost(weight: "10") + lastSeen: DateTime @cost(weight: "10") + meanDuration: Float @cost(weight: "10") operationCount: Long! - opm: Float + opm: Float @cost(weight: "10") totalReference: Long! @deprecated(reason: "Use totalReferences instead") totalReferences: Long! - totalRequests: Long - totalUsages: Long + totalRequests: Long @cost(weight: "10") + totalUsages: Long @cost(weight: "10") } type CoordinateUsageGraph { @@ -1052,7 +1051,7 @@ type DeploymentCannotBeCancelledError implements Error { } type DeploymentCreatedEvent { - deployment: Deployment! + deployment: Deployment! @cost(weight: "10") } type DeploymentCreatedLog implements DeploymentLog { @@ -1109,7 +1108,7 @@ type DeploymentSuccessLog implements DeploymentLog { } type DeploymentUpdatedEvent { - deployment: Deployment! + deployment: Deployment! @cost(weight: "10") } type DeploymentWaitingForApprovalLog implements DeploymentLog { @@ -1172,7 +1171,7 @@ type DocumentChangeValidationFailed implements Error { type DocumentChanged implements DocumentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - document(onlyIfLatest: Boolean): WorkspaceDocument + document(onlyIfLatest: Boolean): WorkspaceDocument @cost(weight: "10") documentId: ID! id: ID! name: String! @@ -1207,7 +1206,7 @@ type DocumentChangesEdge { type DocumentCreated implements DocumentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - document(onlyIfLatest: Boolean): WorkspaceDocument + document(onlyIfLatest: Boolean): WorkspaceDocument @cost(weight: "10") documentId: ID! id: ID! name: String! @@ -1219,7 +1218,7 @@ type DocumentCreated implements DocumentChangeLog & WorkspaceChangeLog { type DocumentDeleted implements DocumentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - document(onlyIfLatest: Boolean): WorkspaceDocument + document(onlyIfLatest: Boolean): WorkspaceDocument @cost(weight: "10") documentId: ID! id: ID! name: String! @@ -1301,13 +1300,13 @@ type Environment implements Node { name: String! variables: [EnvironmentVariable!]! version: Version! - workspace: Workspace + workspace: Workspace @cost(weight: "10") } type EnvironmentChanged implements EnvironmentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - environment(onlyIfLatest: Boolean): Environment + environment(onlyIfLatest: Boolean): Environment @cost(weight: "10") environmentId: ID! id: ID! name: String! @@ -1318,7 +1317,7 @@ type EnvironmentChanged implements EnvironmentChangeLog & WorkspaceChangeLog { type EnvironmentCreated implements EnvironmentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - environment(onlyIfLatest: Boolean): Environment + environment(onlyIfLatest: Boolean): Environment @cost(weight: "10") environmentId: ID! id: ID! name: String! @@ -1329,7 +1328,7 @@ type EnvironmentCreated implements EnvironmentChangeLog & WorkspaceChangeLog { type EnvironmentDeleted implements EnvironmentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - environment(onlyIfLatest: Boolean): Environment + environment(onlyIfLatest: Boolean): Environment @cost(weight: "10") environmentId: ID! id: ID! name: String! @@ -1364,7 +1363,7 @@ type EnvironmentsEdge { type ErrorInsight { epm: Float! - id: ID! + id: ID! @cost(weight: "10") lastSeen: Float! message: String! totalCount: Long! @@ -1397,11 +1396,11 @@ type FieldAddedChange implements SchemaChange { } type FieldCoordinateMetrics implements CoordinateMetrics { - clientUsage(clientId: ID from: DateTime! to: DateTime!): CoordinateClientUsage - clientUsages(from: DateTime! to: DateTime!): [CoordinateClientUsage!]! - duration(from: DateTime! resolution: Int! = 300 to: DateTime!): FieldDurationGraph - requests(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateRequestGraph - usages(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateUsageGraph + clientUsage(clientId: ID from: DateTime! to: DateTime!): CoordinateClientUsage @cost(weight: "10") + clientUsages(from: DateTime! to: DateTime!): [CoordinateClientUsage!]! @cost(weight: "10") + duration(from: DateTime! resolution: Int! = 300 to: DateTime!): FieldDurationGraph @cost(weight: "10") + requests(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateRequestGraph @cost(weight: "10") + usages(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateUsageGraph @cost(weight: "10") } type FieldDurationGraph { @@ -1425,8 +1424,13 @@ type FieldRemovedChange implements SchemaChange { typeName: String! } +type ForceDeleteStageByIdPayload { + api: Api + errors: [ForceDeleteStageByIdError!] +} + type FusionConfiguration { - downloadUrl: String! + downloadUrl: String! @cost(weight: "10") format: FusionConfigurationFormat! id: ID! publishedAt: DateTime! @@ -1450,18 +1454,18 @@ type FusionConfigurationDeployment implements Node & Deployment { schemaChanges: FusionConfigurationDeploymentSchemaChanges source: SourceMetadata status: DeploymentStatus! - subgraph: Subgraph @deprecated(reason: "Use `subgraphs` instead.") - subgraphs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): DeploymentSubgraphsConnection + subgraph: Subgraph @cost(weight: "10") @deprecated(reason: "Use `subgraphs` instead.") + subgraphs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): DeploymentSubgraphsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") tag: String! } type FusionConfigurationDeploymentSchemaChanges { - changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection + changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) statistic: SchemaChangeLogStatistic! } type FusionConfigurationPublishedStageChangeEvent implements StageChangedEvent { - fusionConfiguration: FusionConfiguration + fusionConfiguration: FusionConfiguration @cost(weight: "10") kind: StageChangeKind! } @@ -1490,14 +1494,14 @@ type FusionConfigurationValidationSuccess implements FusionConfigurationPublishi } type FusionSubgraph implements Subgraph { - api: Api + api: Api @cost(weight: "10") id: ID! name: String! } type FusionSubgraphVersion { createdAt: DateTime! - fusionSubgraph: FusionSubgraph! + fusionSubgraph: FusionSubgraph! @cost(weight: "10") id: ID! tag: String! } @@ -1515,18 +1519,18 @@ type GraphQLDirectiveArgumentDefinition implements Node & GraphQLTypeSystemMembe isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLDirectiveDefinition implements Node & GraphQLTypeSystemMember { - arguments: GraphQLDirectiveDefinitionArgumentsConnection! + arguments: GraphQLDirectiveDefinitionArgumentsConnection! @cost(weight: "10") coordinate: String! id: ID! isDeprecated: Boolean! kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLDirectiveDefinitionArgumentsConnection { @@ -1540,8 +1544,8 @@ type GraphQLEnumTypeDefinition implements Node & GraphQLTypeSystemMember { kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! - values: GraphQLEnumTypeDefinitionValuesConnection! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + values: GraphQLEnumTypeDefinitionValuesConnection! @cost(weight: "10") } type GraphQLEnumTypeDefinitionValuesConnection { @@ -1554,7 +1558,7 @@ type GraphQLEnumValueDefinition implements Node & GraphQLTypeSystemMember { isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLInputObjectFieldDefinition implements Node & GraphQLTypeSystemMember & GraphQLInputValueDefinition { @@ -1563,18 +1567,18 @@ type GraphQLInputObjectFieldDefinition implements Node & GraphQLTypeSystemMember isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLInputObjectTypeDefinition implements Node & GraphQLTypeSystemMember { coordinate: String! - fields: GraphQLInputObjectTypeDefinitionFieldsConnection! + fields: GraphQLInputObjectTypeDefinitionFieldsConnection! @cost(weight: "10") id: ID! isDeprecated: Boolean! kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLInputObjectTypeDefinitionFieldsConnection { @@ -1589,7 +1593,7 @@ type GraphQLInterfaceFieldArgumentDefinition implements Node & GraphQLTypeSystem isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLInterfaceFieldArgumentDefinitionArgumentsConnection implements GraphQLOutputFieldDefinitionArgumentsConnection { @@ -1597,24 +1601,24 @@ type GraphQLInterfaceFieldArgumentDefinitionArgumentsConnection implements Graph } type GraphQLInterfaceFieldDefinition implements Node & GraphQLTypeSystemMember & GraphQLOutputFieldDefinition { - arguments: GraphQLInterfaceFieldArgumentDefinitionArgumentsConnection! + arguments: GraphQLInterfaceFieldArgumentDefinitionArgumentsConnection! @cost(weight: "10") coordinate: String! id: ID! isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLInterfaceTypeDefinition implements Node & GraphQLTypeSystemMember { coordinate: String! - fields: GraphQLInterfaceTypeDefinitionFieldsConnection! + fields: GraphQLInterfaceTypeDefinitionFieldsConnection! @cost(weight: "10") id: ID! isDeprecated: Boolean! kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLInterfaceTypeDefinitionFieldsConnection { @@ -1629,7 +1633,7 @@ type GraphQLObjectFieldArgumentDefinition implements Node & GraphQLTypeSystemMem isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLObjectFieldArgumentDefinitionArgumentsConnection implements GraphQLOutputFieldDefinitionArgumentsConnection { @@ -1637,23 +1641,23 @@ type GraphQLObjectFieldArgumentDefinitionArgumentsConnection implements GraphQLO } type GraphQLObjectFieldDefinition implements Node & GraphQLTypeSystemMember & GraphQLOutputFieldDefinition { - arguments: GraphQLObjectFieldArgumentDefinitionArgumentsConnection! + arguments: GraphQLObjectFieldArgumentDefinitionArgumentsConnection! @cost(weight: "10") coordinate: String! id: ID! isDeprecated: Boolean! metrics: FieldCoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLObjectTypeDefinition implements GraphQLTypeSystemMember { coordinate: String! - fields: GraphQLObjectTypeDefinitionFieldsConnection! + fields: GraphQLObjectTypeDefinitionFieldsConnection! @cost(weight: "10") isDeprecated: Boolean! kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLObjectTypeDefinitionFieldsConnection { @@ -1667,7 +1671,7 @@ type GraphQLScalarTypeDefinition implements Node & GraphQLTypeSystemMember { kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type GraphQLSchemaError { @@ -1682,25 +1686,25 @@ type GraphQLUnionTypeDefinition implements Node & GraphQLTypeSystemMember { kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! + usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") } type Group implements Node { description: String! - groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupGroupsConnection + groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupGroupsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") id: ID! isDefault: Boolean! - members("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupMembersConnection + members("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupMembersConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") name: String! - organization: Organization - roleAssignments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupRoleAssignmentsConnection + organization: Organization @cost(weight: "10") + roleAssignments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupRoleAssignmentsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } type GroupGroupMember implements GroupMember { assignedAt: DateTime! - group: Group + group: Group @cost(weight: "10") id: ID! - nestedGroup: Group + nestedGroup: Group @cost(weight: "10") type: String! } @@ -1830,12 +1834,12 @@ type InvalidSourceMetadataInputError implements Error { } type McpFeatureCollection implements Node { - api: Api + api: Api @cost(weight: "10") createdAt: DateTime! createdBy: UserInfo! id: ID! name: String! - versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): McpFeatureCollectionVersionsConnection + versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): McpFeatureCollectionVersionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } type McpFeatureCollectionChangeLog implements StageChangeLog & Node { @@ -1856,7 +1860,7 @@ type McpFeatureCollectionDeployment implements Node & Deployment { errors: [McpFeatureCollectionDeploymentError!]! id: ID! logs: [DeploymentLog!]! - mcpFeatureCollection: McpFeatureCollection + mcpFeatureCollection: McpFeatureCollection @cost(weight: "10") source: SourceMetadata status: DeploymentStatus! tag: String! @@ -1873,7 +1877,7 @@ type McpFeatureCollectionValidationArchiveError implements McpFeatureCollectionV type McpFeatureCollectionValidationCollection { entities: [McpFeatureCollectionValidationEntity!]! - mcpFeatureCollection: McpFeatureCollection + mcpFeatureCollection: McpFeatureCollection @cost(weight: "10") } type McpFeatureCollectionValidationDocumentError implements McpFeatureCollectionValidationEntityError { @@ -1911,7 +1915,7 @@ type McpFeatureCollectionVersion implements Node { createdAt: DateTime! hash: String! id: ID! - mcpFeatureCollection: McpFeatureCollection + mcpFeatureCollection: McpFeatureCollection @cost(weight: "10") tag: String! } @@ -1927,13 +1931,13 @@ type McpFeatureCollectionVersionPublishFailed implements McpFeatureCollectionVer } type McpFeatureCollectionVersionPublishSuccess implements McpFeatureCollectionVersionPublishResult { - mcpFeatureCollectionVersion: McpFeatureCollectionVersion + mcpFeatureCollectionVersion: McpFeatureCollectionVersion @cost(weight: "10") state: ProcessingState! } type McpFeatureCollectionVersionPublishedStageChangeEvent implements StageChangedEvent { kind: StageChangeKind! - mcpFeatureCollectionVersion: McpFeatureCollectionVersion + mcpFeatureCollectionVersion: McpFeatureCollectionVersion @cost(weight: "10") } type McpFeatureCollectionVersionValidationFailed implements McpFeatureCollectionVersionValidationResult { @@ -2005,59 +2009,60 @@ type MockSchemasEdge { } type Mutation { - approveDeployment(input: ApproveDeploymentInput!): ApproveDeploymentPayload! - beginFusionConfigurationPublish(input: BeginFusionConfigurationPublishInput!): BeginFusionConfigurationPublishPayload! - cancelDeployment(input: CancelDeploymentInput!): CancelDeploymentPayload! - cancelFusionConfigurationComposition(input: CancelFusionConfigurationCompositionInput!): CancelFusionConfigurationCompositionPayload! - commitFusionConfigurationPublish(input: CommitFusionConfigurationPublishInput!): CommitFusionConfigurationPublishPayload! - createAccount: CreateAccountPayload! - createApiKey(input: CreateApiKeyInput!): CreateApiKeyPayload! - createApiKeyForApi(input: CreateApiKeyForApiInput!): CreateApiKeyForApiPayload! - createClient(input: CreateClientInput!): CreateClientPayload! - createMcpFeatureCollection(input: CreateMcpFeatureCollectionInput!): CreateMcpFeatureCollectionPayload! - createMockSchema(input: CreateMockSchemaInput!): CreateMockSchemaPayload! - createOpenApiCollection(input: CreateOpenApiCollectionInput!): CreateOpenApiCollectionPayload! - createPersonalAccessToken(input: CreatePersonalAccessTokenInput!): CreatePersonalAccessTokenPayload! - createWorkspace(input: CreateWorkspaceInput!): CreateWorkspacePayload! - deleteApiById(input: DeleteApiByIdInput!): DeleteApiByIdPayload! - deleteApiKey(input: DeleteApiKeyInput!): DeleteApiKeyPayload! - deleteClientById(input: DeleteClientByIdInput!): DeleteClientByIdPayload! - deleteMcpFeatureCollectionById(input: DeleteMcpFeatureCollectionByIdInput!): DeleteMcpFeatureCollectionByIdPayload! - deleteMockSchemaById(input: DeleteMockSchemaByIdInput!): DeleteMockSchemaByIdPayload! - deleteOpenApiCollectionById(input: DeleteOpenApiCollectionByIdInput!): DeleteOpenApiCollectionByIdPayload! - ensureTunnelSession: EnsureTunnelSessionPayload! - pollClientVersionPublishRequest(input: PollClientVersionPublishRequestInput!): PollClientVersionPublishRequestPayload! - pollClientVersionValidationRequest(input: PollClientVersionValidationRequestInput!): PollClientVersionValidationRequestPayload! - pollSchemaVersionPublishRequest(input: PollSchemaVersionPublishRequestInput!): PollSchemaVersionPublishRequestPayload! - pollSchemaVersionValidationRequest(input: PollSchemaVersionValidationRequestInput!): PollSchemaVersionValidationRequestPayload! - publishClient(input: PublishClientInput!): PublishClientPayload! - publishMcpFeatureCollection(input: PublishMcpFeatureCollectionInput!): PublishMcpFeatureCollectionPayload! - publishOpenApiCollection(input: PublishOpenApiCollectionInput!): PublishOpenApiCollectionPayload! - publishSchema(input: PublishSchemaInput!): PublishSchemaPayload! - pushDocumentChanges(input: PushDocumentChangeInput!): PushDocumentChangesPayload! @deprecated(reason: "Use pushWorkspaceChanges") - pushWorkspaceChanges(input: PushWorkspaceChangesInput!): PushWorkspaceChangesPayload! - removeWorkspace(input: RemoveWorkspaceInput!): RemoveWorkspacePayload! - renameWorkspace(input: RenameWorkspaceInput!): RenameWorkspacePayload! - revokePersonalAccessToken(input: RevokePersonalAccessTokenInput!): RevokePersonalAccessTokenPayload! - setActiveWorkspace(input: SetActiveWorkspaceInput!): SetActiveWorkspacePayload! - startFusionConfigurationComposition(input: StartFusionConfigurationCompositionInput!): StartFusionConfigurationCompositionPayload! - unpublishClient(input: UnpublishClientInput!): UnpublishClientPayload! - updateApiSettings(input: UpdateApiSettingsInput!): UpdateApiSettingsPayload! - updateFeatureFlags(input: UpdateFeatureFlagsInput!): UpdateFeatureFlagsPayload! - updateMockSchema(input: UpdateMockSchemaInput!): UpdateMockSchemaPayload! - updatePreferences(input: UpdatePreferencesInput!): UpdatePreferencesPayload! - updateStages(input: UpdateStagesInput!): UpdateStagesPayload! - updateThemeSettings(input: UpdateThemeSettingsInput!): UpdateThemeSettingsPayload! - uploadClient(input: UploadClientInput!): UploadClientPayload! - uploadFusionSubgraph(input: UploadFusionSubgraphInput!): UploadFusionSubgraphPayload! - uploadMcpFeatureCollection(input: UploadMcpFeatureCollectionInput!): UploadMcpFeatureCollectionPayload! - uploadOpenApiCollection(input: UploadOpenApiCollectionInput!): UploadOpenApiCollectionPayload! - uploadSchema(input: UploadSchemaInput!): UploadSchemaPayload! - validateClient(input: ValidateClientInput!): ValidateClientPayload! - validateFusionConfigurationComposition(input: ValidateFusionConfigurationCompositionInput!): ValidateFusionConfigurationCompositionPayload! - validateMcpFeatureCollection(input: ValidateMcpFeatureCollectionInput!): ValidateMcpFeatureCollectionPayload! - validateOpenApiCollection(input: ValidateOpenApiCollectionInput!): ValidateOpenApiCollectionPayload! - validateSchema(input: ValidateSchemaInput!): ValidateSchemaPayload! + approveDeployment(input: ApproveDeploymentInput!): ApproveDeploymentPayload! @cost(weight: "10") + beginFusionConfigurationPublish(input: BeginFusionConfigurationPublishInput!): BeginFusionConfigurationPublishPayload! @cost(weight: "10") + cancelDeployment(input: CancelDeploymentInput!): CancelDeploymentPayload! @cost(weight: "10") + cancelFusionConfigurationComposition(input: CancelFusionConfigurationCompositionInput!): CancelFusionConfigurationCompositionPayload! @cost(weight: "10") + commitFusionConfigurationPublish(input: CommitFusionConfigurationPublishInput!): CommitFusionConfigurationPublishPayload! @cost(weight: "10") + createAccount: CreateAccountPayload! @cost(weight: "10") + createApiKey(input: CreateApiKeyInput!): CreateApiKeyPayload! @authorize @cost(weight: "10") + createApiKeyForApi(input: CreateApiKeyForApiInput!): CreateApiKeyForApiPayload! @authorize @cost(weight: "10") + createClient(input: CreateClientInput!): CreateClientPayload! @authorize @cost(weight: "10") + createMcpFeatureCollection(input: CreateMcpFeatureCollectionInput!): CreateMcpFeatureCollectionPayload! @authorize @cost(weight: "10") + createMockSchema(input: CreateMockSchemaInput!): CreateMockSchemaPayload! @authorize @cost(weight: "10") + createOpenApiCollection(input: CreateOpenApiCollectionInput!): CreateOpenApiCollectionPayload! @authorize @cost(weight: "10") + createPersonalAccessToken(input: CreatePersonalAccessTokenInput!): CreatePersonalAccessTokenPayload! @authorize @cost(weight: "10") + createWorkspace(input: CreateWorkspaceInput!): CreateWorkspacePayload! @authorize @cost(weight: "10") + deleteApiById(input: DeleteApiByIdInput!): DeleteApiByIdPayload! @authorize @cost(weight: "10") + deleteApiKey(input: DeleteApiKeyInput!): DeleteApiKeyPayload! @authorize @cost(weight: "10") + deleteClientById(input: DeleteClientByIdInput!): DeleteClientByIdPayload! @authorize @cost(weight: "10") + deleteMcpFeatureCollectionById(input: DeleteMcpFeatureCollectionByIdInput!): DeleteMcpFeatureCollectionByIdPayload! @authorize @cost(weight: "10") + deleteMockSchemaById(input: DeleteMockSchemaByIdInput!): DeleteMockSchemaByIdPayload! @authorize @cost(weight: "10") + deleteOpenApiCollectionById(input: DeleteOpenApiCollectionByIdInput!): DeleteOpenApiCollectionByIdPayload! @authorize @cost(weight: "10") + ensureTunnelSession: EnsureTunnelSessionPayload! @authorize @cost(weight: "10") + forceDeleteStageById(input: ForceDeleteStageByIdInput!): ForceDeleteStageByIdPayload! @authorize @cost(weight: "10") + pollClientVersionPublishRequest(input: PollClientVersionPublishRequestInput!): PollClientVersionPublishRequestPayload! @authorize @cost(weight: "10") + pollClientVersionValidationRequest(input: PollClientVersionValidationRequestInput!): PollClientVersionValidationRequestPayload! @authorize @cost(weight: "10") + pollSchemaVersionPublishRequest(input: PollSchemaVersionPublishRequestInput!): PollSchemaVersionPublishRequestPayload! @authorize @cost(weight: "10") + pollSchemaVersionValidationRequest(input: PollSchemaVersionValidationRequestInput!): PollSchemaVersionValidationRequestPayload! @authorize @cost(weight: "10") + publishClient(input: PublishClientInput!): PublishClientPayload! @authorize @cost(weight: "10") + publishMcpFeatureCollection(input: PublishMcpFeatureCollectionInput!): PublishMcpFeatureCollectionPayload! @authorize @cost(weight: "10") + publishOpenApiCollection(input: PublishOpenApiCollectionInput!): PublishOpenApiCollectionPayload! @authorize @cost(weight: "10") + publishSchema(input: PublishSchemaInput!): PublishSchemaPayload! @authorize @cost(weight: "10") + pushDocumentChanges(input: PushDocumentChangeInput!): PushDocumentChangesPayload! @authorize(policy: "DocumentsWrite") @cost(weight: "10") @deprecated(reason: "Use pushWorkspaceChanges") + pushWorkspaceChanges(input: PushWorkspaceChangesInput!): PushWorkspaceChangesPayload! @authorize(policy: "DocumentsWrite") @cost(weight: "10") + removeWorkspace(input: RemoveWorkspaceInput!): RemoveWorkspacePayload! @authorize @cost(weight: "10") + renameWorkspace(input: RenameWorkspaceInput!): RenameWorkspacePayload! @authorize @cost(weight: "10") + revokePersonalAccessToken(input: RevokePersonalAccessTokenInput!): RevokePersonalAccessTokenPayload! @authorize @cost(weight: "10") + setActiveWorkspace(input: SetActiveWorkspaceInput!): SetActiveWorkspacePayload! @authorize(policy: "WorkspaceManage") @cost(weight: "10") + startFusionConfigurationComposition(input: StartFusionConfigurationCompositionInput!): StartFusionConfigurationCompositionPayload! @cost(weight: "10") + unpublishClient(input: UnpublishClientInput!): UnpublishClientPayload! @authorize @cost(weight: "10") + updateApiSettings(input: UpdateApiSettingsInput!): UpdateApiSettingsPayload! @authorize @cost(weight: "10") + updateFeatureFlags(input: UpdateFeatureFlagsInput!): UpdateFeatureFlagsPayload! @authorize @cost(weight: "10") + updateMockSchema(input: UpdateMockSchemaInput!): UpdateMockSchemaPayload! @authorize @cost(weight: "10") + updatePreferences(input: UpdatePreferencesInput!): UpdatePreferencesPayload! @authorize @cost(weight: "10") + updateStages(input: UpdateStagesInput!): UpdateStagesPayload! @cost(weight: "10") + updateThemeSettings(input: UpdateThemeSettingsInput!): UpdateThemeSettingsPayload! @authorize @cost(weight: "10") + uploadClient(input: UploadClientInput!): UploadClientPayload! @authorize @cost(weight: "10") + uploadFusionSubgraph(input: UploadFusionSubgraphInput!): UploadFusionSubgraphPayload! @authorize @cost(weight: "10") + uploadMcpFeatureCollection(input: UploadMcpFeatureCollectionInput!): UploadMcpFeatureCollectionPayload! @authorize @cost(weight: "10") + uploadOpenApiCollection(input: UploadOpenApiCollectionInput!): UploadOpenApiCollectionPayload! @authorize @cost(weight: "10") + uploadSchema(input: UploadSchemaInput!): UploadSchemaPayload! @authorize @cost(weight: "10") + validateClient(input: ValidateClientInput!): ValidateClientPayload! @authorize @cost(weight: "10") + validateFusionConfigurationComposition(input: ValidateFusionConfigurationCompositionInput!): ValidateFusionConfigurationCompositionPayload! @cost(weight: "10") + validateMcpFeatureCollection(input: ValidateMcpFeatureCollectionInput!): ValidateMcpFeatureCollectionPayload! @authorize @cost(weight: "10") + validateOpenApiCollection(input: ValidateOpenApiCollectionInput!): ValidateOpenApiCollectionPayload! @authorize @cost(weight: "10") + validateSchema(input: ValidateSchemaInput!): ValidateSchemaPayload! @authorize @cost(weight: "10") } type OAuth2AuthenticationFlowOptions { @@ -2090,12 +2095,12 @@ type ObjectModifiedChange implements SchemaChange { } type OpenApiCollection implements Node { - api: Api + api: Api @cost(weight: "10") createdAt: DateTime! createdBy: UserInfo! id: ID! name: String! - versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OpenApiCollectionVersionsConnection + versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OpenApiCollectionVersionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } type OpenApiCollectionChangeLog implements StageChangeLog & Node { @@ -2116,7 +2121,7 @@ type OpenApiCollectionDeployment implements Node & Deployment { errors: [OpenApiCollectionDeploymentError!]! id: ID! logs: [DeploymentLog!]! - openApiCollection: OpenApiCollection + openApiCollection: OpenApiCollection @cost(weight: "10") source: SourceMetadata status: DeploymentStatus! tag: String! @@ -2133,7 +2138,7 @@ type OpenApiCollectionValidationArchiveError implements OpenApiCollectionVersion type OpenApiCollectionValidationCollection { entities: [OpenApiCollectionValidationEntity!]! - openApiCollection: OpenApiCollection + openApiCollection: OpenApiCollection @cost(weight: "10") } type OpenApiCollectionValidationDocumentError implements OpenApiCollectionValidationEntityError { @@ -2172,7 +2177,7 @@ type OpenApiCollectionVersion implements Node { createdAt: DateTime! hash: String! id: ID! - openApiCollection: OpenApiCollection + openApiCollection: OpenApiCollection @cost(weight: "10") tag: String! } @@ -2188,13 +2193,13 @@ type OpenApiCollectionVersionPublishFailed implements OpenApiCollectionVersionPu } type OpenApiCollectionVersionPublishSuccess implements OpenApiCollectionVersionPublishResult { - openApiCollectionVersion: OpenApiCollectionVersion + openApiCollectionVersion: OpenApiCollectionVersion @cost(weight: "10") state: ProcessingState! } type OpenApiCollectionVersionPublishedStageChangeEvent implements StageChangedEvent { kind: StageChangeKind! - openApiCollectionVersion: OpenApiCollectionVersion + openApiCollectionVersion: OpenApiCollectionVersion @cost(weight: "10") } type OpenApiCollectionVersionValidationFailed implements OpenApiCollectionVersionValidationResult { @@ -2230,9 +2235,9 @@ type OpenTelemetryBoolAttribute implements OpenTelemetryAttribute { } type OpenTelemetryDbSpan implements OpenTelemetrySpan { - api: Api + api: Api @cost(weight: "10") clockSkew: Float - db: OpenTelemetryDbSpanAttributes + db: OpenTelemetryDbSpanAttributes @cost(weight: "10") duration: Float! epoch: Float! events: [OpenTelemetryTraceEvent!]! @@ -2243,7 +2248,7 @@ type OpenTelemetryDbSpan implements OpenTelemetrySpan { spanId: String! spanKind: String! spanName: String! - stage: Stage + stage: Stage @cost(weight: "10") statusCode: String! statusMessage: String! traceId: String! @@ -2262,7 +2267,7 @@ type OpenTelemetryDbSpanAttributes { } type OpenTelemetryDefaultSpan implements OpenTelemetrySpan { - api: Api + api: Api @cost(weight: "10") clockSkew: Float duration: Float! epoch: Float! @@ -2274,7 +2279,7 @@ type OpenTelemetryDefaultSpan implements OpenTelemetrySpan { spanId: String! spanKind: String! spanName: String! - stage: Stage + stage: Stage @cost(weight: "10") statusCode: String! statusMessage: String! traceId: String! @@ -2282,14 +2287,14 @@ type OpenTelemetryDefaultSpan implements OpenTelemetrySpan { } type OpenTelemetryError { - api: Api + api: Api @cost(weight: "10") epoch: Float! escaped: Boolean! message: String! parentSpanId: String! spanId: String! stackTrace: String! - stage: Stage + stage: Stage @cost(weight: "10") traceId: String! type: String! } @@ -2300,21 +2305,21 @@ type OpenTelemetryFloatAttribute implements OpenTelemetryAttribute { } type OpenTelemetryGraphQLOperationSpan implements OpenTelemetrySpan { - api: Api + api: Api @cost(weight: "10") clockSkew: Float - document: OpenTelemetryGraphQLOperationSpanDocumentAttributes + document: OpenTelemetryGraphQLOperationSpanDocumentAttributes @cost(weight: "10") duration: Float! epoch: Float! events: [OpenTelemetryTraceEvent!]! links: [OpenTelemetryTraceLink!]! - operation: OpenTelemetryGraphQLOperationSpanOperationAttributes + operation: OpenTelemetryGraphQLOperationSpanOperationAttributes @cost(weight: "10") parentSpanId: String! resourceAttributes: [Attribute!]! spanAttributes: [Attribute!]! spanId: String! spanKind: String! spanName: String! - stage: Stage + stage: Stage @cost(weight: "10") statusCode: String! statusMessage: String! traceId: String! @@ -2322,7 +2327,7 @@ type OpenTelemetryGraphQLOperationSpan implements OpenTelemetrySpan { } type OpenTelemetryGraphQLOperationSpanDocumentAttributes { - body: String + body: String @cost(weight: "10") id: String } @@ -2332,7 +2337,7 @@ type OpenTelemetryGraphQLOperationSpanOperationAttributes { } type OpenTelemetryGraphQLResolverSpan implements OpenTelemetrySpan { - api: Api + api: Api @cost(weight: "10") clockSkew: Float duration: Float! epoch: Float! @@ -2340,12 +2345,12 @@ type OpenTelemetryGraphQLResolverSpan implements OpenTelemetrySpan { links: [OpenTelemetryTraceLink!]! parentSpanId: String! resourceAttributes: [Attribute!]! - selection: OpenTelemetryGraphQLResolverSpanSelectionAttributes + selection: OpenTelemetryGraphQLResolverSpanSelectionAttributes @cost(weight: "10") spanAttributes: [Attribute!]! spanId: String! spanKind: String! spanName: String! - stage: Stage + stage: Stage @cost(weight: "10") statusCode: String! statusMessage: String! traceId: String! @@ -2366,12 +2371,12 @@ type OpenTelemetryGraphQLResolverSpanSelectionAttributes { } type OpenTelemetryHttpClientSpan implements OpenTelemetrySpan { - api: Api + api: Api @cost(weight: "10") clockSkew: Float duration: Float! epoch: Float! events: [OpenTelemetryTraceEvent!]! - http: OpenTelemetryHttpClientSpanAttribute + http: OpenTelemetryHttpClientSpanAttribute @cost(weight: "10") links: [OpenTelemetryTraceLink!]! parentSpanId: String! resourceAttributes: [Attribute!]! @@ -2379,7 +2384,7 @@ type OpenTelemetryHttpClientSpan implements OpenTelemetrySpan { spanId: String! spanKind: String! spanName: String! - stage: Stage + stage: Stage @cost(weight: "10") statusCode: String! statusMessage: String! traceId: String! @@ -2396,12 +2401,12 @@ type OpenTelemetryHttpClientSpanAttribute { } type OpenTelemetryHttpServerSpan implements OpenTelemetrySpan { - api: Api + api: Api @cost(weight: "10") clockSkew: Float duration: Float! epoch: Float! events: [OpenTelemetryTraceEvent!]! - http: OpenTelemetryHttpServerSpanAttributes + http: OpenTelemetryHttpServerSpanAttributes @cost(weight: "10") links: [OpenTelemetryTraceLink!]! parentSpanId: String! resourceAttributes: [Attribute!]! @@ -2409,7 +2414,7 @@ type OpenTelemetryHttpServerSpan implements OpenTelemetrySpan { spanId: String! spanKind: String! spanName: String! - stage: Stage + stage: Stage @cost(weight: "10") statusCode: String! statusMessage: String! traceId: String! @@ -2426,7 +2431,7 @@ type OpenTelemetryHttpServerSpanAttributes { } type OpenTelemetryLog { - api: Api + api: Api @cost(weight: "10") body: String! epoch: Float! logAttributes: [OpenTelemetryAttribute!]! @@ -2434,7 +2439,7 @@ type OpenTelemetryLog { severityNumber: Int! severityText: String! spanId: String! - stage: Stage + stage: Stage @cost(weight: "10") traceId: String! } @@ -2481,11 +2486,11 @@ type OpenTelemetryStringAttribute implements OpenTelemetryAttribute { } type OpenTelemetryTrace { - epoch: Float! - errors: [OpenTelemetryError!]! - logs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OpenTelemetryLogsConnection - spans: [OpenTelemetrySpan!]! - totalDuration: Float! + epoch: Float! @cost(weight: "10") + errors: [OpenTelemetryError!]! @cost(weight: "10") + logs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OpenTelemetryLogsConnection @listSize(assumedSize: 200, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + spans: [OpenTelemetrySpan!]! @cost(weight: "10") + totalDuration: Float! @cost(weight: "10") } type OpenTelemetryTraceEvent { @@ -2504,14 +2509,14 @@ type OpenTelemetryTraceLink { type OpenTelemetryTransactionInsight { averageLatency: Float! errorRate: Float! - id: ID! + id: ID! @cost(weight: "10") impact: Float! - latency: OpenTelemetryTransactionLatencyGraph + latency: OpenTelemetryTransactionLatencyGraph @cost(weight: "10") name: String! opm: Float! spanKind: OpenTelemetrySpanKind! successRate: Float! - throughput: OpenTelemetryTransactionThroughputGraph + throughput: OpenTelemetryTransactionThroughputGraph @cost(weight: "10") totalCount: Long! totalCountWithErrors: Long! } @@ -2608,7 +2613,7 @@ type OpenTelemetryTransactionsThroughputGraphData { } type Operation { - document: RequestDocument + document: RequestDocument @cost(weight: "10") kind: OperationKind! name: String } @@ -2622,14 +2627,14 @@ type OperationInsight { documentId: String! errorRate: Float! hash: String! - id: ID! + id: ID! @cost(weight: "10") impact: Float! kind: OperationKind - latency: OperationLatencyGraph + latency: OperationLatencyGraph @cost(weight: "10") operationName: String! opm: Float! successRate: Float! - throughput: OperationThroughputGraph + throughput: OperationThroughputGraph @cost(weight: "10") totalCount: Long! totalCountWithErrors: Long! } @@ -2757,15 +2762,15 @@ type OperationsThroughputGraphData { } type Organization implements Node { - authorizationEventLogs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int from: DateTime "Returns the last _n_ elements from the list." last: Int to: DateTime): OrganizationAuthorizationEventLogsConnection - billingInfo: OrganizationBillingInfo + authorizationEventLogs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int from: DateTime "Returns the last _n_ elements from the list." last: Int to: DateTime): OrganizationAuthorizationEventLogsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + billingInfo: OrganizationBillingInfo @cost(weight: "10") displayName: String! - groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationGroupsConnection + groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationGroupsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") id: ID! - members("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationMembersConnection + members("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationMembersConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") name: String! - plan: OrganizationPlan - usage(from: DateTime to: DateTime): OrganizationUsage + plan: OrganizationPlan @cost(weight: "10") + usage(from: DateTime to: DateTime): OrganizationUsage @cost(weight: "10") } "A connection to a list of items." @@ -2817,17 +2822,17 @@ type OrganizationInfo { } type OrganizationMember implements Node { - groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationMemberGroupsConnection + groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationMemberGroupsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") id: ID! isDisabled: Boolean! - userName: String + userName: String @cost(weight: "10") } type OrganizationMemberGroupMember implements GroupMember { assignedAt: DateTime! - group: Group + group: Group @cost(weight: "10") id: ID! - member: OrganizationMember + member: OrganizationMember @cost(weight: "10") type: String! } @@ -2867,7 +2872,7 @@ type OrganizationPaymentIssue { type OrganizationPermissionScope implements PermissionScope { id: ID! - organization: Organization + organization: Organization @cost(weight: "10") type: String! } @@ -2876,8 +2881,8 @@ type OrganizationPlan { } type OrganizationUsage { - cumulativeGigabyteHours: OrganizationUsageCumulativeGigabyteHoursGraph - gigabyteHours: OrganizationUsageGigabyteHoursGraph + cumulativeGigabyteHours: OrganizationUsageCumulativeGigabyteHoursGraph @cost(weight: "10") + gigabyteHours: OrganizationUsageGigabyteHoursGraph @cost(weight: "10") period: OrganizationUsagePeriod! } @@ -2969,7 +2974,7 @@ type PersistedQueryErrorLocation { } type PersistedQueryValidationError implements ClientVersionPublishError & ClientVersionValidationError & SchemaVersionPublishError & SchemaVersionValidationError & FusionConfigurationValidationError & ProcessingError { - client: Client + client: Client @cost(weight: "10") clientId: ID! @deprecated(reason: "Use `client` instead.") hasMoreErrors: Boolean! message: String! @@ -2977,7 +2982,7 @@ type PersistedQueryValidationError implements ClientVersionPublishError & Client } type PersistedQueryValidationFailed { - deployedTags: [String!]! + deployedTags: [String!]! @cost(weight: "10") errors: [PersistedQueryError!]! hash: String! message: String! @@ -3056,7 +3061,7 @@ type ProcessingTaskApproved implements SchemaVersionPublishResult & ClientVersio } type ProcessingTaskIsQueued implements FusionConfigurationPublishingResult & ClientVersionPublishResult & SchemaVersionPublishResult & OpenApiCollectionVersionPublishResult & McpFeatureCollectionVersionPublishResult { - queuePosition: Int! + queuePosition: Int! @cost(weight: "10") state: ProcessingState! } @@ -3095,16 +3100,16 @@ type PublishedClient { type PublishedClientVersion { publishedAt: DateTime! - stage: Stage - tags: [String!]! @deprecated(reason: "Use `version.tag` instead.") - version: ClientVersion + stage: Stage @cost(weight: "10") + tags: [String!]! @cost(weight: "10") @deprecated(reason: "Use `version.tag` instead.") + version: ClientVersion @cost(weight: "10") } type PublishedSchemaVersion { publishedAt: DateTime! - stage: Stage - tag: String! @deprecated(reason: "Use `version.tag` instead.") - version: SchemaVersion + stage: Stage @cost(weight: "10") + tag: String! @cost(weight: "10") @deprecated(reason: "Use `version.tag` instead.") + version: SchemaVersion @cost(weight: "10") } type PushDocumentChangesPayload { @@ -3118,16 +3123,16 @@ type PushWorkspaceChangesPayload { } type Query { - apiById(id: ID!): Api - fusionConfigurationByApiId(id: ID! stage: String!): FusionConfiguration + apiById(id: ID!): Api @cost(weight: "10") + fusionConfigurationByApiId(id: ID! stage: String!): FusionConfiguration @cost(weight: "10") me: Viewer "Fetches an object given its ID." - node("ID of the object." id: ID!): Node + node("ID of the object." id: ID!): Node @cost(weight: "10") "Lookup nodes by a list of IDs." - nodes("The list of node IDs." ids: [ID!]!): [Node]! - organizationById(id: ID!): Organization - stageById(id: ID!): Stage - workspaceById(workspaceId: ID!): Workspace + nodes("The list of node IDs." ids: [ID!]!): [Node]! @cost(weight: "10") + organizationById(id: ID!): Organization @cost(weight: "10") + stageById(id: ID!): Stage @cost(weight: "10") + workspaceById(workspaceId: ID!): Workspace @cost(weight: "10") } type ReadyTimeoutError implements ClientVersionPublishError & ClientVersionValidationError & SchemaVersionPublishError & SchemaVersionValidationError & FusionConfigurationPublishingError & OpenApiCollectionVersionPublishError & OpenApiCollectionVersionValidationError & McpFeatureCollectionVersionPublishError & McpFeatureCollectionVersionValidationError & ProcessingError { @@ -3154,12 +3159,12 @@ type ResolverInsight { averageLatency: Float! coordinate: String! errorRate: Float! - id: ID! + id: ID! @cost(weight: "10") impact: Float! - latency: ResolverLatencyGraph + latency: ResolverLatencyGraph @cost(weight: "10") opm: Float! successRate: Float! - throughput: ResolverThroughputGraph + throughput: ResolverThroughputGraph @cost(weight: "10") totalCount: Long! totalCountWithErrors: Long! } @@ -3224,8 +3229,8 @@ type RoleAssignment { condition: RoleAssignmentCondition effect: RoleEffect! id: ID! - role: Role - scope: PermissionScope + role: Role @cost(weight: "10") + scope: PermissionScope @cost(weight: "10") } type RoleAssignmentStageAuthorizationCondition { @@ -3246,11 +3251,11 @@ type ScalarModifiedChange implements SchemaChange { type SchemaChangeLog implements StageChangeLog & Node { changedAt: DateTime! - changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection + changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") id: ID! kind: StageChangeLogKind! - previousSchema: SchemaVersion - schema: SchemaVersion + previousSchema: SchemaVersion @cost(weight: "10") + schema: SchemaVersion @cost(weight: "10") statistic: SchemaChangeLogStatistic! tag: String! } @@ -3285,10 +3290,10 @@ type SchemaChangesEdge { } type SchemaCoordinateMetrics implements CoordinateMetrics { - clientUsage(clientId: ID from: DateTime! to: DateTime!): CoordinateClientUsage - clientUsages(from: DateTime! to: DateTime!): [CoordinateClientUsage!]! - requests(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateRequestGraph - usages(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateUsageGraph + clientUsage(clientId: ID from: DateTime! to: DateTime!): CoordinateClientUsage @cost(weight: "10") + clientUsages(from: DateTime! to: DateTime!): [CoordinateClientUsage!]! @cost(weight: "10") + requests(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateRequestGraph @cost(weight: "10") + usages(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateUsageGraph @cost(weight: "10") } type SchemaDeployment implements Node & Deployment { @@ -3304,9 +3309,9 @@ type SchemaDeployment implements Node & Deployment { } type SchemaDeploymentSchemaChanges { - changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection - previousSchema: SchemaVersion - schema: SchemaVersion + changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + previousSchema: SchemaVersion @cost(weight: "10") + schema: SchemaVersion @cost(weight: "10") statistic: SchemaChangeLogStatistic! } @@ -3323,7 +3328,7 @@ type SchemaRegistrySettings { type SchemaVersion { createdAt: DateTime! - downloadUrl: String! + downloadUrl: String! @cost(weight: "1") id: ID! publishedTo: [PublishedSchemaVersion!]! tag: String! @@ -3341,7 +3346,7 @@ type SchemaVersionPublishFailed implements SchemaVersionPublishResult { } type SchemaVersionPublishSuccess implements SchemaVersionPublishResult { - changeLog: SchemaChangeLog + changeLog: SchemaChangeLog @cost(weight: "10") state: ProcessingState! } @@ -3391,23 +3396,23 @@ type SetActiveWorkspacePayload { } type Stage implements Node { - api: Api - changeLog("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int kind: [StageChangeLogKind!] "Returns the last _n_ elements from the list." last: Int): StageChangeLogConnection + api: Api @cost(weight: "10") + changeLog("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int kind: [StageChangeLogKind!] "Returns the last _n_ elements from the list." last: Int): StageChangeLogConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") conditions: [StageCondition!]! - coordinate(coordinate: String!): GraphQLTypeSystemMember - coordinates("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime isDeprecated: Boolean kinds: [CoordinateKind!] orderBy: [GraphQLCoordinateOrderByInput!] search: String to: DateTime): CoordinatesConnection - deployments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): DeploymentsConnection + coordinate(coordinate: String!): GraphQLTypeSystemMember @cost(weight: "10") + coordinates("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime isDeprecated: Boolean kinds: [CoordinateKind!] orderBy: [GraphQLCoordinateOrderByInput!] search: String to: DateTime): CoordinatesConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + deployments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): DeploymentsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") displayName: String! - essentials: StageEssentials + essentials: StageEssentials @cost(weight: "10") id: ID! - logDistribution(from: DateTime! to: DateTime!): OpenTelemetryLogsSeverityGraph - logs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int from: DateTime "Returns the last _n_ elements from the list." last: Int to: DateTime): OpenTelemetryLogsConnection + logDistribution(from: DateTime! to: DateTime!): OpenTelemetryLogsSeverityGraph @cost(weight: "10") + logs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int from: DateTime "Returns the last _n_ elements from the list." last: Int to: DateTime): OpenTelemetryLogsConnection @listSize(assumedSize: 200, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) metrics: StageMetrics! name: String! - publishedClients: [PublishedClient!]! - publishedFusionConfiguration: FusionConfiguration - publishedSchema: PublishedSchemaVersion - traceById(seeker: String spanId: String traceId: String!): OpenTelemetryTrace + publishedClients: [PublishedClient!]! @cost(weight: "10") + publishedFusionConfiguration: FusionConfiguration @cost(weight: "10") + publishedSchema: PublishedSchemaVersion @cost(weight: "10") + traceById(seeker: String spanId: String traceId: String!): OpenTelemetryTrace @cost(weight: "10") } "A connection to a list of items." @@ -3429,11 +3434,11 @@ type StageChangeLogEdge { } type StageClientsMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [ClientInsightsOrderByInput!] to: DateTime!): ClientInsightsConnection + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [ClientInsightsOrderByInput!] to: DateTime!): ClientInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } type StageErrorsMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! search: String to: DateTime!): ErrorInsightsConnection + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! search: String to: DateTime!): ErrorInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } type StageEssentials { @@ -3471,36 +3476,36 @@ type StageNotFoundError implements Error { } type StageOperationMetrics { - latency(from: DateTime! to: DateTime!): OperationLatencyGraph - latencyDistribution(from: DateTime! to: DateTime!): OperationLatencyDistributionGraph - requestDocument: RequestDocument - samples(from: DateTime! maxLatency: Float minLatency: Float to: DateTime!): [OperationTraceSample!]! - throughput(from: DateTime! operationKinds: [OperationKind!] @deprecated(reason: "Not longer in use") to: DateTime!): OperationThroughputGraph + latency(from: DateTime! to: DateTime!): OperationLatencyGraph @cost(weight: "10") + latencyDistribution(from: DateTime! to: DateTime!): OperationLatencyDistributionGraph @cost(weight: "10") + requestDocument: RequestDocument @cost(weight: "10") + samples(from: DateTime! maxLatency: Float minLatency: Float to: DateTime!): [OperationTraceSample!]! @cost(weight: "10") + throughput(from: DateTime! operationKinds: [OperationKind!] @deprecated(reason: "Not longer in use") to: DateTime!): OperationThroughputGraph @cost(weight: "10") } type StageOperationMetricsSummary { - latency: StageLatencySummary - throughput: StageThroughputSummary + latency: StageLatencySummary @cost(weight: "10") + throughput: StageThroughputSummary @cost(weight: "10") } type StageOperationsMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! operationKinds: [OperationKind!] orderBy: [OperationInsightsOrderByInput!] search: String to: DateTime!): OperationInsightsConnection - latency(from: DateTime! operationKinds: [OperationKind!] to: DateTime!): OperationsLatencyGraph - summary(from: DateTime! to: DateTime!): StageOperationMetricsSummary! - throughput(from: DateTime! operationKinds: [OperationKind!] to: DateTime!): OperationsThroughputGraph + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! operationKinds: [OperationKind!] orderBy: [OperationInsightsOrderByInput!] search: String to: DateTime!): OperationInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + latency(from: DateTime! operationKinds: [OperationKind!] to: DateTime!): OperationsLatencyGraph @cost(weight: "10") + summary(from: DateTime! to: DateTime!): StageOperationMetricsSummary! @cost(weight: "10") + throughput(from: DateTime! operationKinds: [OperationKind!] to: DateTime!): OperationsThroughputGraph @cost(weight: "10") } type StageResolverMetrics { - latency(from: DateTime! to: DateTime!): ResolverLatencyGraph - throughput(from: DateTime! to: DateTime!): ResolverThroughputGraph + latency(from: DateTime! to: DateTime!): ResolverLatencyGraph @cost(weight: "10") + throughput(from: DateTime! to: DateTime!): ResolverThroughputGraph @cost(weight: "10") } type StageResolversMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [ResolverInsightsOrderByInput!] search: String to: DateTime!): ResolverInsightsConnection + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [ResolverInsightsOrderByInput!] search: String to: DateTime!): ResolverInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } type StageSubgraphsMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [SubgraphInsightsOrderByInput!] to: DateTime!): SubgraphInsightsConnection + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [SubgraphInsightsOrderByInput!] to: DateTime!): SubgraphInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } type StageThroughputSummary { @@ -3512,22 +3517,22 @@ type StageThroughputSummary { } type StageTransactionMetrics { - latency(from: DateTime! to: DateTime!): OpenTelemetryTransactionLatencyGraph - latencyDistribution(from: DateTime! to: DateTime!): OpenTelemetryTransactionLatencyDistributionGraph - samples(from: DateTime! maxLatency: Float minLatency: Float to: DateTime!): [OpenTelemetryTransactionTraceSample!]! - throughput(from: DateTime! to: DateTime!): OpenTelemetryTransactionThroughputGraph + latency(from: DateTime! to: DateTime!): OpenTelemetryTransactionLatencyGraph @cost(weight: "10") + latencyDistribution(from: DateTime! to: DateTime!): OpenTelemetryTransactionLatencyDistributionGraph @cost(weight: "10") + samples(from: DateTime! maxLatency: Float minLatency: Float to: DateTime!): [OpenTelemetryTransactionTraceSample!]! @cost(weight: "10") + throughput(from: DateTime! to: DateTime!): OpenTelemetryTransactionThroughputGraph @cost(weight: "10") } type StageTransactionMetricsSummary { - latency: StageLatencySummary - throughput: StageThroughputSummary + latency: StageLatencySummary @cost(weight: "10") + throughput: StageThroughputSummary @cost(weight: "10") } type StageTransactionsMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [OpenTelemetryTransactionInsightsOrderByInput!] search: String spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionInsightsConnection - latency(from: DateTime! spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionsLatencyGraph - summary(from: DateTime! to: DateTime!): StageTransactionMetricsSummary! - throughput(from: DateTime! spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionsThroughputGraph + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [OpenTelemetryTransactionInsightsOrderByInput!] search: String spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + latency(from: DateTime! spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionsLatencyGraph @cost(weight: "10") + summary(from: DateTime! to: DateTime!): StageTransactionMetricsSummary! @cost(weight: "10") + throughput(from: DateTime! spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionsThroughputGraph @cost(weight: "10") } type StageValidationError implements Error { @@ -3536,7 +3541,7 @@ type StageValidationError implements Error { type StagesHavePublishedDependenciesError implements Error { message: String! - stages: [Stage!]! + stages: [Stage!]! @cost(weight: "10") } type StartFusionConfigurationCompositionPayload { @@ -3549,13 +3554,13 @@ type SubgraphInsight { errorRate: Float id: ID! impact: Float - latency: SubgraphLatencyGraph + latency: SubgraphLatencyGraph @cost(weight: "10") name: String! opm: Float - stage: Stage - subgraph: Subgraph + stage: Stage @cost(weight: "10") + subgraph: Subgraph @cost(weight: "10") successRate: Float - throughput: SubgraphThroughputGraph + throughput: SubgraphThroughputGraph @cost(weight: "10") totalCount: Long totalCountWithErrors: Long } @@ -3622,7 +3627,7 @@ type Subscription { onSchemaVersionValidationUpdate(requestId: ID!): SchemaVersionValidationResult! onStageChanged(apiId: ID! kind: [StageChangeKind!] stageName: String!): StageChangedEvent! onStageChangeLogAdded(apiId: ID! kind: [StageChangeLogKind!] stageName: String!): StageChangeLog! - onStageDeploymentsChanged(stageId: ID!): DeploymentEvent! + onStageDeploymentsChanged(stageId: ID!): DeploymentEvent! @cost(weight: "10") } type ThemeSettings { @@ -3812,34 +3817,34 @@ type ValidationInProgress implements FusionConfigurationPublishingResult & Clien } type Viewer { - activeOrganization: Organization - activeWorkspace: Workspace + activeOrganization: Organization @cost(weight: "10") + activeWorkspace: Workspace @cost(weight: "10") billingUrl: String manageTenantUrl: String organization: OrganizationInfo - personalAccessTokens("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): PersonalAccessTokensConnection - preferences: Any! + personalAccessTokens("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): PersonalAccessTokensConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + preferences: Any! @cost(weight: "10") sessionId: String! - settings: UserSettings! - user: User - workspaces("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): WorkspacesConnection + settings: UserSettings! @cost(weight: "10") + user: User @cost(weight: "10") + workspaces("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): WorkspacesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } type WaitForApproval implements SchemaVersionPublishResult & ClientVersionPublishResult & FusionConfigurationPublishingResult & OpenApiCollectionVersionPublishResult & McpFeatureCollectionVersionPublishResult { - deployment: Deployment + deployment: Deployment @cost(weight: "10") state: ProcessingState! } type Workspace implements Node { - apiDocuments("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApiDocumentsConnection - apiKeys("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiKeysConnection - apis("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApisConnection - changed(version: Version!): Boolean! - changes("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ChangesConnection - documentChanges("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): DocumentChangesConnection @deprecated(reason: "Use changes") - documents("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): DocumentsConnection - documentsChanged(version: Version): Boolean! @deprecated(reason: "Use changed") - environments("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): EnvironmentsConnection + apiDocuments("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApiDocumentsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + apiKeys("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiKeysConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + apis("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApisConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + changed(version: Version!): Boolean! @cost(weight: "10") + changes("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ChangesConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + documentChanges("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): DocumentChangesConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") @deprecated(reason: "Use changes") + documents("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): DocumentsConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + documentsChanged(version: Version): Boolean! @cost(weight: "10") @deprecated(reason: "Use changed") + environments("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): EnvironmentsConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") id: ID! members: [WorkspaceMember!]! name: String! @@ -3866,7 +3871,7 @@ type WorkspaceDocument implements Node { path: [String!]! variables: String version: Version! - workspace: Workspace + workspace: Workspace @cost(weight: "10") } type WorkspaceDocumentAuthentication { @@ -3903,7 +3908,7 @@ type WorkspaceDocumentHttpConnection { type WorkspaceMember { role: WorkspaceUserRole! - user: UserInfo! + user: UserInfo! @cost(weight: "10") } type WorkspaceNotFound implements Error { @@ -3924,7 +3929,7 @@ type WorkspaceNotFoundForDocument implements Error { type WorkspacePermissionScope implements PermissionScope { id: ID! type: String! - workspace: Workspace + workspace: Workspace @cost(weight: "10") } "A connection to a list of items." @@ -4011,6 +4016,8 @@ union EnumValueChange = DeprecatedChange | DescriptionChanged union FieldChange = ArgumentAdded | ArgumentChanged | ArgumentRemoved | DeprecatedChange | DescriptionChanged | TypeChanged +union ForceDeleteStageByIdError = ApiNotFoundError | StageNotFoundError | UnauthorizedOperation + union FusionConfigurationDeploymentError = PersistedQueryValidationError | McpFeatureCollectionValidationError | OpenApiCollectionValidationError | SchemaChangeViolationError | InvalidGraphQLSchemaError union InputFieldChange = DeprecatedChange | DescriptionChanged | TypeChanged @@ -4119,7 +4126,6 @@ input ApiCreateChangeInput { name: String! path: [String!]! referenceId: String! - referenceName: String workspaceId: ID! } @@ -4198,7 +4204,6 @@ input ApiUpdateChangeInput { name: String! path: [String!]! referenceId: String! - referenceName: String version: Version! workspaceId: ID! } @@ -4348,6 +4353,11 @@ input EnvironmentVariableInput { value: String! } +input ForceDeleteStageByIdInput { + apiId: ID! + stageName: String! +} + input FusionSubgraphVersionInput { name: String! tag: String! @@ -4942,9 +4952,18 @@ enum WorkspaceUserRole { MEMBER } +"The authorize directive." +directive @authorize("Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER "The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!]) on OBJECT | FIELD_DEFINITION + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION + "The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." directive @defer("Deferred when true." if: Boolean "If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT +"The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error." requireOneSlicingArgument: Boolean = true "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!]) on FIELD_DEFINITION + "The `@oneOf` directive is used within the type system definition language to indicate that an Input Object is a OneOf Input Object." directive @oneOf on INPUT_OBJECT From c8bff8c90c2af0f75d933dea27ebc263303fbdb3 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 22 Mar 2026 21:27:26 +0000 Subject: [PATCH 4/7] cleanup --- src/Mocha/src/Mocha.Mediator/MediatorContext.cs | 6 +----- src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs | 5 +++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Mocha/src/Mocha.Mediator/MediatorContext.cs b/src/Mocha/src/Mocha.Mediator/MediatorContext.cs index 10fd2ccf1e6..cbe6f887107 100644 --- a/src/Mocha/src/Mocha.Mediator/MediatorContext.cs +++ b/src/Mocha/src/Mocha.Mediator/MediatorContext.cs @@ -59,12 +59,8 @@ internal void Initialize( Services = serviceProvider; Message = message; MessageType = messageType; + ResponseType = responseType ?? typeof(void); CancellationToken = cancellationToken; - if (responseType is not null) - { - ResponseType = responseType; - } - _features.Initialize(runtime.Features); } diff --git a/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs b/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs index bfad5b99bf6..d63da72c930 100644 --- a/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs +++ b/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs @@ -67,14 +67,15 @@ public MediatorContext RentContext() [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ReturnContext(MediatorContext context) { - context.Reset(); - if (s_cached is null) { + // Reset here since the thread-static path bypasses the pool policy. + context.Reset(); s_cached = context; } else { + // The pool policy's Return calls Reset(), so no need to reset here. _contextPool.Return(context); } } From 955ba30b5f69b5bc37750e8a102e3d9efb4ae6f3 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 22 Mar 2026 22:35:07 +0000 Subject: [PATCH 5/7] Revert "cleanup" This reverts commit 833636f6809b660c368cc703133e0ae691e6700a. --- .../Commands/Stages/DeleteStageCommand.cs | 75 -- .../Stages/DeleteStageCommand.graphql | 15 - .../Commands/Stages/StageCommand.cs | 1 - .../src/CommandLine/schema.graphql | 643 +++++++++--------- 4 files changed, 312 insertions(+), 422 deletions(-) delete mode 100644 src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.cs delete mode 100644 src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.graphql diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.cs b/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.cs deleted file mode 100644 index 0f7b99759b2..00000000000 --- a/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.CommandLine.Invocation; -using ChilliCream.Nitro.CommandLine.Client; -using ChilliCream.Nitro.CommandLine.Commands.Stages.Components; -using ChilliCream.Nitro.CommandLine.Configuration; -using ChilliCream.Nitro.CommandLine.Helpers; -using ChilliCream.Nitro.CommandLine.Options; -using ChilliCream.Nitro.CommandLine.Results; -using ChilliCream.Nitro.CommandLine.Services.Sessions; -using static ChilliCream.Nitro.CommandLine.ThrowHelper; - -namespace ChilliCream.Nitro.CommandLine.Commands.Stages; - -internal sealed class DeleteStageCommand : Command -{ - public DeleteStageCommand() : base("delete") - { - Description = "Deletes a stage by name"; - - AddOption(Opt.Instance); - AddOption(Opt.Instance); - AddOption(Opt.Instance); - - this.SetHandler( - ExecuteAsync, - Bind.FromServiceProvider(), - Bind.FromServiceProvider(), - Bind.FromServiceProvider(), - Bind.FromServiceProvider()); - } - - private static async Task ExecuteAsync( - InvocationContext context, - IAnsiConsole console, - IApiClient client, - CancellationToken cancellationToken) - { - const string apiMessage = "For which API do you want to force delete a stage?"; - var apiId = await context.GetOrSelectApiId(apiMessage); - - var stageName = context.ParseResult.GetValueForOption(Opt.Instance)!; - - var shouldDelete = await context.ConfirmWhenNotForced( - $"Do you really want to force delete stage {stageName.AsHighlight()}", - cancellationToken); - - if (!shouldDelete) - { - throw Exit("Stage was not deleted"); - } - - var input = new ForceDeleteStageByIdInput { ApiId = apiId, StageName = stageName }; - var result = await client.ForceDeleteStageByIdCommandMutation - .ExecuteAsync(input, cancellationToken); - - console.EnsureNoErrors(result); - var data = console.EnsureData(result); - console.PrintErrorsAndExit(data.ForceDeleteStageById.Errors); - - var stages = data.ForceDeleteStageById.Api?.Stages; - if (stages is null) - { - throw Exit("Could not delete the stage"); - } - - var items = stages - .Select(x => StageDetailPrompt.From(x).ToObject()) - .ToArray(); - - context.SetResult(new PaginatedListResult(items, null)); - - console.OkLine($"Stage {stageName.AsHighlight()} was force deleted"); - - return ExitCodes.Success; - } -} diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.graphql b/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.graphql deleted file mode 100644 index af08bb44b5f..00000000000 --- a/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/DeleteStageCommand.graphql +++ /dev/null @@ -1,15 +0,0 @@ -mutation ForceDeleteStageByIdCommandMutation($input: ForceDeleteStageByIdInput!) { - forceDeleteStageById(input: $input) { - api { - stages { - ...StageDetailPrompt_Stage - } - } - errors { - ...Error - ...ApiNotFoundError - ...StageNotFoundError - ...UnauthorizedOperation - } - } -} diff --git a/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/StageCommand.cs b/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/StageCommand.cs index 2e9e4c84038..08b15b73307 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/StageCommand.cs +++ b/src/Nitro/CommandLine/src/CommandLine/Commands/Stages/StageCommand.cs @@ -17,7 +17,6 @@ public StageCommand() : base("stage") this.AddNitroCloudDefaultOptions(); AddCommand(new EditStagesCommand()); - AddCommand(new DeleteStageCommand()); AddCommand(new ListStagesCommand()); } } diff --git a/src/Nitro/CommandLine/src/CommandLine/schema.graphql b/src/Nitro/CommandLine/src/CommandLine/schema.graphql index d8d69c85ba5..e58f6008971 100644 --- a/src/Nitro/CommandLine/src/CommandLine/schema.graphql +++ b/src/Nitro/CommandLine/src/CommandLine/schema.graphql @@ -280,34 +280,35 @@ interface WorkspaceChangeLog { } type AfterStageCondition { - afterStage: Stage @cost(weight: "10") + afterStage: Stage } type Api implements Node { - clients("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ClientsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + clients("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ClientsConnection createdAt: DateTime! createdBy: UserInfo! - documents("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApiDocumentConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + documents("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApiDocumentConnection httpConnection: ApiHttpConnection id: ID! kind: ApiKind - mcpFeatureCollections("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiMcpFeatureCollectionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - mockSchemas("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): MockSchemasConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + mcpFeatureCollections("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiMcpFeatureCollectionsConnection + mockSchemas("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): MockSchemasConnection modifiedAt: DateTime! modifiedBy: UserInfo! name: String! - openApiCollections("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiOpenApiCollectionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OperationsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + openApiCollections("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiOpenApiCollectionsConnection + operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OperationsConnection path: [String!]! - schemaVersions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): SchemaVersionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + referenceName: String! + schemaVersions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): SchemaVersionsConnection settings: ApiSettings! - stages: [Stage!]! @cost(weight: "10") + stages: [Stage!]! version: Version! - workspace: Workspace @cost(weight: "10") + workspace: Workspace } type ApiChanged implements ApiChangeLog & WorkspaceChangeLog { - api(onlyIfLatest: Boolean): Api @cost(weight: "10") + api(onlyIfLatest: Boolean): Api apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -319,7 +320,7 @@ type ApiChanged implements ApiChangeLog & WorkspaceChangeLog { } type ApiCreated implements ApiChangeLog & WorkspaceChangeLog { - api(onlyIfLatest: Boolean): Api @cost(weight: "10") + api(onlyIfLatest: Boolean): Api apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -331,7 +332,7 @@ type ApiCreated implements ApiChangeLog & WorkspaceChangeLog { } type ApiDeleted implements ApiChangeLog & WorkspaceChangeLog { - api(onlyIfLatest: Boolean): Api @cost(weight: "10") + api(onlyIfLatest: Boolean): Api apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -348,7 +349,7 @@ type ApiDeletionFailedError implements Error { } type ApiDocument implements Node { - api: Api @cost(weight: "10") + api: Api body: String! createdAt: DateTime! createdBy: UserInfo! @@ -362,7 +363,7 @@ type ApiDocument implements Node { } type ApiDocumentChanged implements ApiDocumentChangeLog & WorkspaceChangeLog { - apiDocument(onlyIfLatest: Boolean): ApiDocument @cost(weight: "10") + apiDocument(onlyIfLatest: Boolean): ApiDocument apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -385,7 +386,7 @@ type ApiDocumentConnection { } type ApiDocumentCreated implements ApiDocumentChangeLog & WorkspaceChangeLog { - apiDocument(onlyIfLatest: Boolean): ApiDocument @cost(weight: "10") + apiDocument(onlyIfLatest: Boolean): ApiDocument apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -398,7 +399,7 @@ type ApiDocumentCreated implements ApiDocumentChangeLog & WorkspaceChangeLog { } type ApiDocumentDeleted implements ApiDocumentChangeLog & WorkspaceChangeLog { - apiDocument(onlyIfLatest: Boolean): ApiDocument @cost(weight: "10") + apiDocument(onlyIfLatest: Boolean): ApiDocument apiId: ID! changedAt: DateTime! changedBy: UserInfo! @@ -466,9 +467,9 @@ type ApiKey implements Node { createdBy: UserInfo! id: ID! name: String! - roleAssignments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiKeyRoleAssignmentsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + roleAssignments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiKeyRoleAssignmentsConnection scopes: [ApiKeyScope!]! - workspace: Workspace @cost(weight: "10") + workspace: Workspace } type ApiKeyNotFoundError implements Error { @@ -496,7 +497,7 @@ type ApiKeyRoleAssignmentsEdge { type ApiKeyScope { kind: String! - reference: ApiKeyReference @cost(weight: "10") + reference: ApiKeyReference referenceId: String! } @@ -565,7 +566,7 @@ type ApiOpenApiCollectionsEdge { } type ApiPermissionScope implements PermissionScope { - api: Api @cost(weight: "10") + api: Api id: ID! type: String! } @@ -628,15 +629,15 @@ type AuthorizationEventLog { eventId: UUID! eventType: AuthorizationEventType! isConditional: Boolean! - organization: Organization @cost(weight: "10") + organization: Organization permission: String! - principal: AuthorizationEventLogPrincipal @cost(weight: "10") - realm: AuthorizationEventLogRealm @cost(weight: "10") - resource: AuthorizationEventLogResource @cost(weight: "10") - subject: AuthorizationEventLogSubject @cost(weight: "10") + principal: AuthorizationEventLogPrincipal + realm: AuthorizationEventLogRealm + resource: AuthorizationEventLogResource + subject: AuthorizationEventLogSubject timestamp: DateTime! traceId: String! - workspace: Workspace @cost(weight: "10") + workspace: Workspace } type BasicAuthenticationFlowOptions { @@ -693,13 +694,13 @@ type ChangesEdge { } type Client implements Node { - api: Api @cost(weight: "10") + api: Api createdAt: DateTime! createdBy: UserInfo! id: ID! name: String! - operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OperationsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ClientVersionConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OperationsConnection + versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ClientVersionConnection } type ClientChangeLog implements StageChangeLog & Node { @@ -716,7 +717,7 @@ type ClientDeletedStageChangeEvent implements StageChangedEvent { type ClientDeployment implements Node & Deployment { approval: DeploymentApproval - client: Client @cost(weight: "10") + client: Client createdAt: DateTime! errors: [ClientDeploymentError!]! id: ID! @@ -728,9 +729,9 @@ type ClientDeployment implements Node & Deployment { type ClientInsight { averageLatency: Float - client: Client @cost(weight: "10") + client: Client errorRate: Float - id: ID! @cost(weight: "10") + id: ID! impact: Float name: String opm: Float @@ -763,10 +764,10 @@ type ClientNotFoundError implements Error { } type ClientVersion implements Node { - client: Client @cost(weight: "10") + client: Client createdAt: DateTime! id: ID! - persistedQueries("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): PersistedQueriesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + persistedQueries("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): PersistedQueriesConnection publishedTo: [PublishedClientVersion!]! tag: String! tags: [String!]! @deprecated(reason: "Use `tag` instead.") @@ -802,12 +803,12 @@ type ClientVersionPublishFailed implements ClientVersionPublishResult { } type ClientVersionPublishSuccess implements ClientVersionPublishResult { - clientVersion: ClientVersion @cost(weight: "10") + clientVersion: ClientVersion state: ProcessingState! } type ClientVersionPublishedStageChangeEvent implements StageChangedEvent { - clientVersion: ClientVersion @cost(weight: "10") + clientVersion: ClientVersion kind: StageChangeKind! } @@ -817,7 +818,7 @@ type ClientVersionRequestNotFoundError implements Error { } type ClientVersionUnpublishedStageChangeEvent implements StageChangedEvent { - clientVersion: ClientVersion @cost(weight: "10") + clientVersion: ClientVersion kind: StageChangeKind! } @@ -858,7 +859,7 @@ type ConcurrentOperationError implements Error & SchemaVersionPublishError & Cli } type CoordinateClientUsage { - client: Client @cost(weight: "10") + client: Client metrics: CoordinateClientUsageMetrics! name: String totalOperations: Long! @@ -868,7 +869,7 @@ type CoordinateClientUsage { type CoordinateClientUsageMetrics implements Node { id: ID! - operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): CoordinateClientUsageOperationInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + operations("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): CoordinateClientUsageOperationInsightsConnection } type CoordinateClientUsageOperationInsight { @@ -915,16 +916,16 @@ type CoordinateRequestGraphData { type CoordinateUsage { clientCount: Long! - errorRate: Float @cost(weight: "10") - firstSeen: DateTime @cost(weight: "10") - lastSeen: DateTime @cost(weight: "10") - meanDuration: Float @cost(weight: "10") + errorRate: Float + firstSeen: DateTime + lastSeen: DateTime + meanDuration: Float operationCount: Long! - opm: Float @cost(weight: "10") + opm: Float totalReference: Long! @deprecated(reason: "Use totalReferences instead") totalReferences: Long! - totalRequests: Long @cost(weight: "10") - totalUsages: Long @cost(weight: "10") + totalRequests: Long + totalUsages: Long } type CoordinateUsageGraph { @@ -1051,7 +1052,7 @@ type DeploymentCannotBeCancelledError implements Error { } type DeploymentCreatedEvent { - deployment: Deployment! @cost(weight: "10") + deployment: Deployment! } type DeploymentCreatedLog implements DeploymentLog { @@ -1108,7 +1109,7 @@ type DeploymentSuccessLog implements DeploymentLog { } type DeploymentUpdatedEvent { - deployment: Deployment! @cost(weight: "10") + deployment: Deployment! } type DeploymentWaitingForApprovalLog implements DeploymentLog { @@ -1171,7 +1172,7 @@ type DocumentChangeValidationFailed implements Error { type DocumentChanged implements DocumentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - document(onlyIfLatest: Boolean): WorkspaceDocument @cost(weight: "10") + document(onlyIfLatest: Boolean): WorkspaceDocument documentId: ID! id: ID! name: String! @@ -1206,7 +1207,7 @@ type DocumentChangesEdge { type DocumentCreated implements DocumentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - document(onlyIfLatest: Boolean): WorkspaceDocument @cost(weight: "10") + document(onlyIfLatest: Boolean): WorkspaceDocument documentId: ID! id: ID! name: String! @@ -1218,7 +1219,7 @@ type DocumentCreated implements DocumentChangeLog & WorkspaceChangeLog { type DocumentDeleted implements DocumentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - document(onlyIfLatest: Boolean): WorkspaceDocument @cost(weight: "10") + document(onlyIfLatest: Boolean): WorkspaceDocument documentId: ID! id: ID! name: String! @@ -1300,13 +1301,13 @@ type Environment implements Node { name: String! variables: [EnvironmentVariable!]! version: Version! - workspace: Workspace @cost(weight: "10") + workspace: Workspace } type EnvironmentChanged implements EnvironmentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - environment(onlyIfLatest: Boolean): Environment @cost(weight: "10") + environment(onlyIfLatest: Boolean): Environment environmentId: ID! id: ID! name: String! @@ -1317,7 +1318,7 @@ type EnvironmentChanged implements EnvironmentChangeLog & WorkspaceChangeLog { type EnvironmentCreated implements EnvironmentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - environment(onlyIfLatest: Boolean): Environment @cost(weight: "10") + environment(onlyIfLatest: Boolean): Environment environmentId: ID! id: ID! name: String! @@ -1328,7 +1329,7 @@ type EnvironmentCreated implements EnvironmentChangeLog & WorkspaceChangeLog { type EnvironmentDeleted implements EnvironmentChangeLog & WorkspaceChangeLog { changedAt: DateTime! changedBy: UserInfo! - environment(onlyIfLatest: Boolean): Environment @cost(weight: "10") + environment(onlyIfLatest: Boolean): Environment environmentId: ID! id: ID! name: String! @@ -1363,7 +1364,7 @@ type EnvironmentsEdge { type ErrorInsight { epm: Float! - id: ID! @cost(weight: "10") + id: ID! lastSeen: Float! message: String! totalCount: Long! @@ -1396,11 +1397,11 @@ type FieldAddedChange implements SchemaChange { } type FieldCoordinateMetrics implements CoordinateMetrics { - clientUsage(clientId: ID from: DateTime! to: DateTime!): CoordinateClientUsage @cost(weight: "10") - clientUsages(from: DateTime! to: DateTime!): [CoordinateClientUsage!]! @cost(weight: "10") - duration(from: DateTime! resolution: Int! = 300 to: DateTime!): FieldDurationGraph @cost(weight: "10") - requests(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateRequestGraph @cost(weight: "10") - usages(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateUsageGraph @cost(weight: "10") + clientUsage(clientId: ID from: DateTime! to: DateTime!): CoordinateClientUsage + clientUsages(from: DateTime! to: DateTime!): [CoordinateClientUsage!]! + duration(from: DateTime! resolution: Int! = 300 to: DateTime!): FieldDurationGraph + requests(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateRequestGraph + usages(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateUsageGraph } type FieldDurationGraph { @@ -1424,13 +1425,8 @@ type FieldRemovedChange implements SchemaChange { typeName: String! } -type ForceDeleteStageByIdPayload { - api: Api - errors: [ForceDeleteStageByIdError!] -} - type FusionConfiguration { - downloadUrl: String! @cost(weight: "10") + downloadUrl: String! format: FusionConfigurationFormat! id: ID! publishedAt: DateTime! @@ -1454,18 +1450,18 @@ type FusionConfigurationDeployment implements Node & Deployment { schemaChanges: FusionConfigurationDeploymentSchemaChanges source: SourceMetadata status: DeploymentStatus! - subgraph: Subgraph @cost(weight: "10") @deprecated(reason: "Use `subgraphs` instead.") - subgraphs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): DeploymentSubgraphsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + subgraph: Subgraph @deprecated(reason: "Use `subgraphs` instead.") + subgraphs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): DeploymentSubgraphsConnection tag: String! } type FusionConfigurationDeploymentSchemaChanges { - changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection statistic: SchemaChangeLogStatistic! } type FusionConfigurationPublishedStageChangeEvent implements StageChangedEvent { - fusionConfiguration: FusionConfiguration @cost(weight: "10") + fusionConfiguration: FusionConfiguration kind: StageChangeKind! } @@ -1494,14 +1490,14 @@ type FusionConfigurationValidationSuccess implements FusionConfigurationPublishi } type FusionSubgraph implements Subgraph { - api: Api @cost(weight: "10") + api: Api id: ID! name: String! } type FusionSubgraphVersion { createdAt: DateTime! - fusionSubgraph: FusionSubgraph! @cost(weight: "10") + fusionSubgraph: FusionSubgraph! id: ID! tag: String! } @@ -1519,18 +1515,18 @@ type GraphQLDirectiveArgumentDefinition implements Node & GraphQLTypeSystemMembe isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLDirectiveDefinition implements Node & GraphQLTypeSystemMember { - arguments: GraphQLDirectiveDefinitionArgumentsConnection! @cost(weight: "10") + arguments: GraphQLDirectiveDefinitionArgumentsConnection! coordinate: String! id: ID! isDeprecated: Boolean! kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLDirectiveDefinitionArgumentsConnection { @@ -1544,8 +1540,8 @@ type GraphQLEnumTypeDefinition implements Node & GraphQLTypeSystemMember { kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") - values: GraphQLEnumTypeDefinitionValuesConnection! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! + values: GraphQLEnumTypeDefinitionValuesConnection! } type GraphQLEnumTypeDefinitionValuesConnection { @@ -1558,7 +1554,7 @@ type GraphQLEnumValueDefinition implements Node & GraphQLTypeSystemMember { isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLInputObjectFieldDefinition implements Node & GraphQLTypeSystemMember & GraphQLInputValueDefinition { @@ -1567,18 +1563,18 @@ type GraphQLInputObjectFieldDefinition implements Node & GraphQLTypeSystemMember isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLInputObjectTypeDefinition implements Node & GraphQLTypeSystemMember { coordinate: String! - fields: GraphQLInputObjectTypeDefinitionFieldsConnection! @cost(weight: "10") + fields: GraphQLInputObjectTypeDefinitionFieldsConnection! id: ID! isDeprecated: Boolean! kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLInputObjectTypeDefinitionFieldsConnection { @@ -1593,7 +1589,7 @@ type GraphQLInterfaceFieldArgumentDefinition implements Node & GraphQLTypeSystem isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLInterfaceFieldArgumentDefinitionArgumentsConnection implements GraphQLOutputFieldDefinitionArgumentsConnection { @@ -1601,24 +1597,24 @@ type GraphQLInterfaceFieldArgumentDefinitionArgumentsConnection implements Graph } type GraphQLInterfaceFieldDefinition implements Node & GraphQLTypeSystemMember & GraphQLOutputFieldDefinition { - arguments: GraphQLInterfaceFieldArgumentDefinitionArgumentsConnection! @cost(weight: "10") + arguments: GraphQLInterfaceFieldArgumentDefinitionArgumentsConnection! coordinate: String! id: ID! isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLInterfaceTypeDefinition implements Node & GraphQLTypeSystemMember { coordinate: String! - fields: GraphQLInterfaceTypeDefinitionFieldsConnection! @cost(weight: "10") + fields: GraphQLInterfaceTypeDefinitionFieldsConnection! id: ID! isDeprecated: Boolean! kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLInterfaceTypeDefinitionFieldsConnection { @@ -1633,7 +1629,7 @@ type GraphQLObjectFieldArgumentDefinition implements Node & GraphQLTypeSystemMem isDeprecated: Boolean! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLObjectFieldArgumentDefinitionArgumentsConnection implements GraphQLOutputFieldDefinitionArgumentsConnection { @@ -1641,23 +1637,23 @@ type GraphQLObjectFieldArgumentDefinitionArgumentsConnection implements GraphQLO } type GraphQLObjectFieldDefinition implements Node & GraphQLTypeSystemMember & GraphQLOutputFieldDefinition { - arguments: GraphQLObjectFieldArgumentDefinitionArgumentsConnection! @cost(weight: "10") + arguments: GraphQLObjectFieldArgumentDefinitionArgumentsConnection! coordinate: String! id: ID! isDeprecated: Boolean! metrics: FieldCoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLObjectTypeDefinition implements GraphQLTypeSystemMember { coordinate: String! - fields: GraphQLObjectTypeDefinitionFieldsConnection! @cost(weight: "10") + fields: GraphQLObjectTypeDefinitionFieldsConnection! isDeprecated: Boolean! kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLObjectTypeDefinitionFieldsConnection { @@ -1671,7 +1667,7 @@ type GraphQLScalarTypeDefinition implements Node & GraphQLTypeSystemMember { kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type GraphQLSchemaError { @@ -1686,25 +1682,25 @@ type GraphQLUnionTypeDefinition implements Node & GraphQLTypeSystemMember { kind: TypeSystemMemberKind! metrics: CoordinateMetrics! name: String! - usage(from: DateTime to: DateTime): CoordinateUsage! @cost(weight: "10") + usage(from: DateTime to: DateTime): CoordinateUsage! } type Group implements Node { description: String! - groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupGroupsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupGroupsConnection id: ID! isDefault: Boolean! - members("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupMembersConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + members("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupMembersConnection name: String! - organization: Organization @cost(weight: "10") - roleAssignments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupRoleAssignmentsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + organization: Organization + roleAssignments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): GroupRoleAssignmentsConnection } type GroupGroupMember implements GroupMember { assignedAt: DateTime! - group: Group @cost(weight: "10") + group: Group id: ID! - nestedGroup: Group @cost(weight: "10") + nestedGroup: Group type: String! } @@ -1834,12 +1830,12 @@ type InvalidSourceMetadataInputError implements Error { } type McpFeatureCollection implements Node { - api: Api @cost(weight: "10") + api: Api createdAt: DateTime! createdBy: UserInfo! id: ID! name: String! - versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): McpFeatureCollectionVersionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): McpFeatureCollectionVersionsConnection } type McpFeatureCollectionChangeLog implements StageChangeLog & Node { @@ -1860,7 +1856,7 @@ type McpFeatureCollectionDeployment implements Node & Deployment { errors: [McpFeatureCollectionDeploymentError!]! id: ID! logs: [DeploymentLog!]! - mcpFeatureCollection: McpFeatureCollection @cost(weight: "10") + mcpFeatureCollection: McpFeatureCollection source: SourceMetadata status: DeploymentStatus! tag: String! @@ -1877,7 +1873,7 @@ type McpFeatureCollectionValidationArchiveError implements McpFeatureCollectionV type McpFeatureCollectionValidationCollection { entities: [McpFeatureCollectionValidationEntity!]! - mcpFeatureCollection: McpFeatureCollection @cost(weight: "10") + mcpFeatureCollection: McpFeatureCollection } type McpFeatureCollectionValidationDocumentError implements McpFeatureCollectionValidationEntityError { @@ -1915,7 +1911,7 @@ type McpFeatureCollectionVersion implements Node { createdAt: DateTime! hash: String! id: ID! - mcpFeatureCollection: McpFeatureCollection @cost(weight: "10") + mcpFeatureCollection: McpFeatureCollection tag: String! } @@ -1931,13 +1927,13 @@ type McpFeatureCollectionVersionPublishFailed implements McpFeatureCollectionVer } type McpFeatureCollectionVersionPublishSuccess implements McpFeatureCollectionVersionPublishResult { - mcpFeatureCollectionVersion: McpFeatureCollectionVersion @cost(weight: "10") + mcpFeatureCollectionVersion: McpFeatureCollectionVersion state: ProcessingState! } type McpFeatureCollectionVersionPublishedStageChangeEvent implements StageChangedEvent { kind: StageChangeKind! - mcpFeatureCollectionVersion: McpFeatureCollectionVersion @cost(weight: "10") + mcpFeatureCollectionVersion: McpFeatureCollectionVersion } type McpFeatureCollectionVersionValidationFailed implements McpFeatureCollectionVersionValidationResult { @@ -2009,60 +2005,59 @@ type MockSchemasEdge { } type Mutation { - approveDeployment(input: ApproveDeploymentInput!): ApproveDeploymentPayload! @cost(weight: "10") - beginFusionConfigurationPublish(input: BeginFusionConfigurationPublishInput!): BeginFusionConfigurationPublishPayload! @cost(weight: "10") - cancelDeployment(input: CancelDeploymentInput!): CancelDeploymentPayload! @cost(weight: "10") - cancelFusionConfigurationComposition(input: CancelFusionConfigurationCompositionInput!): CancelFusionConfigurationCompositionPayload! @cost(weight: "10") - commitFusionConfigurationPublish(input: CommitFusionConfigurationPublishInput!): CommitFusionConfigurationPublishPayload! @cost(weight: "10") - createAccount: CreateAccountPayload! @cost(weight: "10") - createApiKey(input: CreateApiKeyInput!): CreateApiKeyPayload! @authorize @cost(weight: "10") - createApiKeyForApi(input: CreateApiKeyForApiInput!): CreateApiKeyForApiPayload! @authorize @cost(weight: "10") - createClient(input: CreateClientInput!): CreateClientPayload! @authorize @cost(weight: "10") - createMcpFeatureCollection(input: CreateMcpFeatureCollectionInput!): CreateMcpFeatureCollectionPayload! @authorize @cost(weight: "10") - createMockSchema(input: CreateMockSchemaInput!): CreateMockSchemaPayload! @authorize @cost(weight: "10") - createOpenApiCollection(input: CreateOpenApiCollectionInput!): CreateOpenApiCollectionPayload! @authorize @cost(weight: "10") - createPersonalAccessToken(input: CreatePersonalAccessTokenInput!): CreatePersonalAccessTokenPayload! @authorize @cost(weight: "10") - createWorkspace(input: CreateWorkspaceInput!): CreateWorkspacePayload! @authorize @cost(weight: "10") - deleteApiById(input: DeleteApiByIdInput!): DeleteApiByIdPayload! @authorize @cost(weight: "10") - deleteApiKey(input: DeleteApiKeyInput!): DeleteApiKeyPayload! @authorize @cost(weight: "10") - deleteClientById(input: DeleteClientByIdInput!): DeleteClientByIdPayload! @authorize @cost(weight: "10") - deleteMcpFeatureCollectionById(input: DeleteMcpFeatureCollectionByIdInput!): DeleteMcpFeatureCollectionByIdPayload! @authorize @cost(weight: "10") - deleteMockSchemaById(input: DeleteMockSchemaByIdInput!): DeleteMockSchemaByIdPayload! @authorize @cost(weight: "10") - deleteOpenApiCollectionById(input: DeleteOpenApiCollectionByIdInput!): DeleteOpenApiCollectionByIdPayload! @authorize @cost(weight: "10") - ensureTunnelSession: EnsureTunnelSessionPayload! @authorize @cost(weight: "10") - forceDeleteStageById(input: ForceDeleteStageByIdInput!): ForceDeleteStageByIdPayload! @authorize @cost(weight: "10") - pollClientVersionPublishRequest(input: PollClientVersionPublishRequestInput!): PollClientVersionPublishRequestPayload! @authorize @cost(weight: "10") - pollClientVersionValidationRequest(input: PollClientVersionValidationRequestInput!): PollClientVersionValidationRequestPayload! @authorize @cost(weight: "10") - pollSchemaVersionPublishRequest(input: PollSchemaVersionPublishRequestInput!): PollSchemaVersionPublishRequestPayload! @authorize @cost(weight: "10") - pollSchemaVersionValidationRequest(input: PollSchemaVersionValidationRequestInput!): PollSchemaVersionValidationRequestPayload! @authorize @cost(weight: "10") - publishClient(input: PublishClientInput!): PublishClientPayload! @authorize @cost(weight: "10") - publishMcpFeatureCollection(input: PublishMcpFeatureCollectionInput!): PublishMcpFeatureCollectionPayload! @authorize @cost(weight: "10") - publishOpenApiCollection(input: PublishOpenApiCollectionInput!): PublishOpenApiCollectionPayload! @authorize @cost(weight: "10") - publishSchema(input: PublishSchemaInput!): PublishSchemaPayload! @authorize @cost(weight: "10") - pushDocumentChanges(input: PushDocumentChangeInput!): PushDocumentChangesPayload! @authorize(policy: "DocumentsWrite") @cost(weight: "10") @deprecated(reason: "Use pushWorkspaceChanges") - pushWorkspaceChanges(input: PushWorkspaceChangesInput!): PushWorkspaceChangesPayload! @authorize(policy: "DocumentsWrite") @cost(weight: "10") - removeWorkspace(input: RemoveWorkspaceInput!): RemoveWorkspacePayload! @authorize @cost(weight: "10") - renameWorkspace(input: RenameWorkspaceInput!): RenameWorkspacePayload! @authorize @cost(weight: "10") - revokePersonalAccessToken(input: RevokePersonalAccessTokenInput!): RevokePersonalAccessTokenPayload! @authorize @cost(weight: "10") - setActiveWorkspace(input: SetActiveWorkspaceInput!): SetActiveWorkspacePayload! @authorize(policy: "WorkspaceManage") @cost(weight: "10") - startFusionConfigurationComposition(input: StartFusionConfigurationCompositionInput!): StartFusionConfigurationCompositionPayload! @cost(weight: "10") - unpublishClient(input: UnpublishClientInput!): UnpublishClientPayload! @authorize @cost(weight: "10") - updateApiSettings(input: UpdateApiSettingsInput!): UpdateApiSettingsPayload! @authorize @cost(weight: "10") - updateFeatureFlags(input: UpdateFeatureFlagsInput!): UpdateFeatureFlagsPayload! @authorize @cost(weight: "10") - updateMockSchema(input: UpdateMockSchemaInput!): UpdateMockSchemaPayload! @authorize @cost(weight: "10") - updatePreferences(input: UpdatePreferencesInput!): UpdatePreferencesPayload! @authorize @cost(weight: "10") - updateStages(input: UpdateStagesInput!): UpdateStagesPayload! @cost(weight: "10") - updateThemeSettings(input: UpdateThemeSettingsInput!): UpdateThemeSettingsPayload! @authorize @cost(weight: "10") - uploadClient(input: UploadClientInput!): UploadClientPayload! @authorize @cost(weight: "10") - uploadFusionSubgraph(input: UploadFusionSubgraphInput!): UploadFusionSubgraphPayload! @authorize @cost(weight: "10") - uploadMcpFeatureCollection(input: UploadMcpFeatureCollectionInput!): UploadMcpFeatureCollectionPayload! @authorize @cost(weight: "10") - uploadOpenApiCollection(input: UploadOpenApiCollectionInput!): UploadOpenApiCollectionPayload! @authorize @cost(weight: "10") - uploadSchema(input: UploadSchemaInput!): UploadSchemaPayload! @authorize @cost(weight: "10") - validateClient(input: ValidateClientInput!): ValidateClientPayload! @authorize @cost(weight: "10") - validateFusionConfigurationComposition(input: ValidateFusionConfigurationCompositionInput!): ValidateFusionConfigurationCompositionPayload! @cost(weight: "10") - validateMcpFeatureCollection(input: ValidateMcpFeatureCollectionInput!): ValidateMcpFeatureCollectionPayload! @authorize @cost(weight: "10") - validateOpenApiCollection(input: ValidateOpenApiCollectionInput!): ValidateOpenApiCollectionPayload! @authorize @cost(weight: "10") - validateSchema(input: ValidateSchemaInput!): ValidateSchemaPayload! @authorize @cost(weight: "10") + approveDeployment(input: ApproveDeploymentInput!): ApproveDeploymentPayload! + beginFusionConfigurationPublish(input: BeginFusionConfigurationPublishInput!): BeginFusionConfigurationPublishPayload! + cancelDeployment(input: CancelDeploymentInput!): CancelDeploymentPayload! + cancelFusionConfigurationComposition(input: CancelFusionConfigurationCompositionInput!): CancelFusionConfigurationCompositionPayload! + commitFusionConfigurationPublish(input: CommitFusionConfigurationPublishInput!): CommitFusionConfigurationPublishPayload! + createAccount: CreateAccountPayload! + createApiKey(input: CreateApiKeyInput!): CreateApiKeyPayload! + createApiKeyForApi(input: CreateApiKeyForApiInput!): CreateApiKeyForApiPayload! + createClient(input: CreateClientInput!): CreateClientPayload! + createMcpFeatureCollection(input: CreateMcpFeatureCollectionInput!): CreateMcpFeatureCollectionPayload! + createMockSchema(input: CreateMockSchemaInput!): CreateMockSchemaPayload! + createOpenApiCollection(input: CreateOpenApiCollectionInput!): CreateOpenApiCollectionPayload! + createPersonalAccessToken(input: CreatePersonalAccessTokenInput!): CreatePersonalAccessTokenPayload! + createWorkspace(input: CreateWorkspaceInput!): CreateWorkspacePayload! + deleteApiById(input: DeleteApiByIdInput!): DeleteApiByIdPayload! + deleteApiKey(input: DeleteApiKeyInput!): DeleteApiKeyPayload! + deleteClientById(input: DeleteClientByIdInput!): DeleteClientByIdPayload! + deleteMcpFeatureCollectionById(input: DeleteMcpFeatureCollectionByIdInput!): DeleteMcpFeatureCollectionByIdPayload! + deleteMockSchemaById(input: DeleteMockSchemaByIdInput!): DeleteMockSchemaByIdPayload! + deleteOpenApiCollectionById(input: DeleteOpenApiCollectionByIdInput!): DeleteOpenApiCollectionByIdPayload! + ensureTunnelSession: EnsureTunnelSessionPayload! + pollClientVersionPublishRequest(input: PollClientVersionPublishRequestInput!): PollClientVersionPublishRequestPayload! + pollClientVersionValidationRequest(input: PollClientVersionValidationRequestInput!): PollClientVersionValidationRequestPayload! + pollSchemaVersionPublishRequest(input: PollSchemaVersionPublishRequestInput!): PollSchemaVersionPublishRequestPayload! + pollSchemaVersionValidationRequest(input: PollSchemaVersionValidationRequestInput!): PollSchemaVersionValidationRequestPayload! + publishClient(input: PublishClientInput!): PublishClientPayload! + publishMcpFeatureCollection(input: PublishMcpFeatureCollectionInput!): PublishMcpFeatureCollectionPayload! + publishOpenApiCollection(input: PublishOpenApiCollectionInput!): PublishOpenApiCollectionPayload! + publishSchema(input: PublishSchemaInput!): PublishSchemaPayload! + pushDocumentChanges(input: PushDocumentChangeInput!): PushDocumentChangesPayload! @deprecated(reason: "Use pushWorkspaceChanges") + pushWorkspaceChanges(input: PushWorkspaceChangesInput!): PushWorkspaceChangesPayload! + removeWorkspace(input: RemoveWorkspaceInput!): RemoveWorkspacePayload! + renameWorkspace(input: RenameWorkspaceInput!): RenameWorkspacePayload! + revokePersonalAccessToken(input: RevokePersonalAccessTokenInput!): RevokePersonalAccessTokenPayload! + setActiveWorkspace(input: SetActiveWorkspaceInput!): SetActiveWorkspacePayload! + startFusionConfigurationComposition(input: StartFusionConfigurationCompositionInput!): StartFusionConfigurationCompositionPayload! + unpublishClient(input: UnpublishClientInput!): UnpublishClientPayload! + updateApiSettings(input: UpdateApiSettingsInput!): UpdateApiSettingsPayload! + updateFeatureFlags(input: UpdateFeatureFlagsInput!): UpdateFeatureFlagsPayload! + updateMockSchema(input: UpdateMockSchemaInput!): UpdateMockSchemaPayload! + updatePreferences(input: UpdatePreferencesInput!): UpdatePreferencesPayload! + updateStages(input: UpdateStagesInput!): UpdateStagesPayload! + updateThemeSettings(input: UpdateThemeSettingsInput!): UpdateThemeSettingsPayload! + uploadClient(input: UploadClientInput!): UploadClientPayload! + uploadFusionSubgraph(input: UploadFusionSubgraphInput!): UploadFusionSubgraphPayload! + uploadMcpFeatureCollection(input: UploadMcpFeatureCollectionInput!): UploadMcpFeatureCollectionPayload! + uploadOpenApiCollection(input: UploadOpenApiCollectionInput!): UploadOpenApiCollectionPayload! + uploadSchema(input: UploadSchemaInput!): UploadSchemaPayload! + validateClient(input: ValidateClientInput!): ValidateClientPayload! + validateFusionConfigurationComposition(input: ValidateFusionConfigurationCompositionInput!): ValidateFusionConfigurationCompositionPayload! + validateMcpFeatureCollection(input: ValidateMcpFeatureCollectionInput!): ValidateMcpFeatureCollectionPayload! + validateOpenApiCollection(input: ValidateOpenApiCollectionInput!): ValidateOpenApiCollectionPayload! + validateSchema(input: ValidateSchemaInput!): ValidateSchemaPayload! } type OAuth2AuthenticationFlowOptions { @@ -2095,12 +2090,12 @@ type ObjectModifiedChange implements SchemaChange { } type OpenApiCollection implements Node { - api: Api @cost(weight: "10") + api: Api createdAt: DateTime! createdBy: UserInfo! id: ID! name: String! - versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OpenApiCollectionVersionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + versions("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OpenApiCollectionVersionsConnection } type OpenApiCollectionChangeLog implements StageChangeLog & Node { @@ -2121,7 +2116,7 @@ type OpenApiCollectionDeployment implements Node & Deployment { errors: [OpenApiCollectionDeploymentError!]! id: ID! logs: [DeploymentLog!]! - openApiCollection: OpenApiCollection @cost(weight: "10") + openApiCollection: OpenApiCollection source: SourceMetadata status: DeploymentStatus! tag: String! @@ -2138,7 +2133,7 @@ type OpenApiCollectionValidationArchiveError implements OpenApiCollectionVersion type OpenApiCollectionValidationCollection { entities: [OpenApiCollectionValidationEntity!]! - openApiCollection: OpenApiCollection @cost(weight: "10") + openApiCollection: OpenApiCollection } type OpenApiCollectionValidationDocumentError implements OpenApiCollectionValidationEntityError { @@ -2177,7 +2172,7 @@ type OpenApiCollectionVersion implements Node { createdAt: DateTime! hash: String! id: ID! - openApiCollection: OpenApiCollection @cost(weight: "10") + openApiCollection: OpenApiCollection tag: String! } @@ -2193,13 +2188,13 @@ type OpenApiCollectionVersionPublishFailed implements OpenApiCollectionVersionPu } type OpenApiCollectionVersionPublishSuccess implements OpenApiCollectionVersionPublishResult { - openApiCollectionVersion: OpenApiCollectionVersion @cost(weight: "10") + openApiCollectionVersion: OpenApiCollectionVersion state: ProcessingState! } type OpenApiCollectionVersionPublishedStageChangeEvent implements StageChangedEvent { kind: StageChangeKind! - openApiCollectionVersion: OpenApiCollectionVersion @cost(weight: "10") + openApiCollectionVersion: OpenApiCollectionVersion } type OpenApiCollectionVersionValidationFailed implements OpenApiCollectionVersionValidationResult { @@ -2235,9 +2230,9 @@ type OpenTelemetryBoolAttribute implements OpenTelemetryAttribute { } type OpenTelemetryDbSpan implements OpenTelemetrySpan { - api: Api @cost(weight: "10") + api: Api clockSkew: Float - db: OpenTelemetryDbSpanAttributes @cost(weight: "10") + db: OpenTelemetryDbSpanAttributes duration: Float! epoch: Float! events: [OpenTelemetryTraceEvent!]! @@ -2248,7 +2243,7 @@ type OpenTelemetryDbSpan implements OpenTelemetrySpan { spanId: String! spanKind: String! spanName: String! - stage: Stage @cost(weight: "10") + stage: Stage statusCode: String! statusMessage: String! traceId: String! @@ -2267,7 +2262,7 @@ type OpenTelemetryDbSpanAttributes { } type OpenTelemetryDefaultSpan implements OpenTelemetrySpan { - api: Api @cost(weight: "10") + api: Api clockSkew: Float duration: Float! epoch: Float! @@ -2279,7 +2274,7 @@ type OpenTelemetryDefaultSpan implements OpenTelemetrySpan { spanId: String! spanKind: String! spanName: String! - stage: Stage @cost(weight: "10") + stage: Stage statusCode: String! statusMessage: String! traceId: String! @@ -2287,14 +2282,14 @@ type OpenTelemetryDefaultSpan implements OpenTelemetrySpan { } type OpenTelemetryError { - api: Api @cost(weight: "10") + api: Api epoch: Float! escaped: Boolean! message: String! parentSpanId: String! spanId: String! stackTrace: String! - stage: Stage @cost(weight: "10") + stage: Stage traceId: String! type: String! } @@ -2305,21 +2300,21 @@ type OpenTelemetryFloatAttribute implements OpenTelemetryAttribute { } type OpenTelemetryGraphQLOperationSpan implements OpenTelemetrySpan { - api: Api @cost(weight: "10") + api: Api clockSkew: Float - document: OpenTelemetryGraphQLOperationSpanDocumentAttributes @cost(weight: "10") + document: OpenTelemetryGraphQLOperationSpanDocumentAttributes duration: Float! epoch: Float! events: [OpenTelemetryTraceEvent!]! links: [OpenTelemetryTraceLink!]! - operation: OpenTelemetryGraphQLOperationSpanOperationAttributes @cost(weight: "10") + operation: OpenTelemetryGraphQLOperationSpanOperationAttributes parentSpanId: String! resourceAttributes: [Attribute!]! spanAttributes: [Attribute!]! spanId: String! spanKind: String! spanName: String! - stage: Stage @cost(weight: "10") + stage: Stage statusCode: String! statusMessage: String! traceId: String! @@ -2327,7 +2322,7 @@ type OpenTelemetryGraphQLOperationSpan implements OpenTelemetrySpan { } type OpenTelemetryGraphQLOperationSpanDocumentAttributes { - body: String @cost(weight: "10") + body: String id: String } @@ -2337,7 +2332,7 @@ type OpenTelemetryGraphQLOperationSpanOperationAttributes { } type OpenTelemetryGraphQLResolverSpan implements OpenTelemetrySpan { - api: Api @cost(weight: "10") + api: Api clockSkew: Float duration: Float! epoch: Float! @@ -2345,12 +2340,12 @@ type OpenTelemetryGraphQLResolverSpan implements OpenTelemetrySpan { links: [OpenTelemetryTraceLink!]! parentSpanId: String! resourceAttributes: [Attribute!]! - selection: OpenTelemetryGraphQLResolverSpanSelectionAttributes @cost(weight: "10") + selection: OpenTelemetryGraphQLResolverSpanSelectionAttributes spanAttributes: [Attribute!]! spanId: String! spanKind: String! spanName: String! - stage: Stage @cost(weight: "10") + stage: Stage statusCode: String! statusMessage: String! traceId: String! @@ -2371,12 +2366,12 @@ type OpenTelemetryGraphQLResolverSpanSelectionAttributes { } type OpenTelemetryHttpClientSpan implements OpenTelemetrySpan { - api: Api @cost(weight: "10") + api: Api clockSkew: Float duration: Float! epoch: Float! events: [OpenTelemetryTraceEvent!]! - http: OpenTelemetryHttpClientSpanAttribute @cost(weight: "10") + http: OpenTelemetryHttpClientSpanAttribute links: [OpenTelemetryTraceLink!]! parentSpanId: String! resourceAttributes: [Attribute!]! @@ -2384,7 +2379,7 @@ type OpenTelemetryHttpClientSpan implements OpenTelemetrySpan { spanId: String! spanKind: String! spanName: String! - stage: Stage @cost(weight: "10") + stage: Stage statusCode: String! statusMessage: String! traceId: String! @@ -2401,12 +2396,12 @@ type OpenTelemetryHttpClientSpanAttribute { } type OpenTelemetryHttpServerSpan implements OpenTelemetrySpan { - api: Api @cost(weight: "10") + api: Api clockSkew: Float duration: Float! epoch: Float! events: [OpenTelemetryTraceEvent!]! - http: OpenTelemetryHttpServerSpanAttributes @cost(weight: "10") + http: OpenTelemetryHttpServerSpanAttributes links: [OpenTelemetryTraceLink!]! parentSpanId: String! resourceAttributes: [Attribute!]! @@ -2414,7 +2409,7 @@ type OpenTelemetryHttpServerSpan implements OpenTelemetrySpan { spanId: String! spanKind: String! spanName: String! - stage: Stage @cost(weight: "10") + stage: Stage statusCode: String! statusMessage: String! traceId: String! @@ -2431,7 +2426,7 @@ type OpenTelemetryHttpServerSpanAttributes { } type OpenTelemetryLog { - api: Api @cost(weight: "10") + api: Api body: String! epoch: Float! logAttributes: [OpenTelemetryAttribute!]! @@ -2439,7 +2434,7 @@ type OpenTelemetryLog { severityNumber: Int! severityText: String! spanId: String! - stage: Stage @cost(weight: "10") + stage: Stage traceId: String! } @@ -2486,11 +2481,11 @@ type OpenTelemetryStringAttribute implements OpenTelemetryAttribute { } type OpenTelemetryTrace { - epoch: Float! @cost(weight: "10") - errors: [OpenTelemetryError!]! @cost(weight: "10") - logs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OpenTelemetryLogsConnection @listSize(assumedSize: 200, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) - spans: [OpenTelemetrySpan!]! @cost(weight: "10") - totalDuration: Float! @cost(weight: "10") + epoch: Float! + errors: [OpenTelemetryError!]! + logs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OpenTelemetryLogsConnection + spans: [OpenTelemetrySpan!]! + totalDuration: Float! } type OpenTelemetryTraceEvent { @@ -2509,14 +2504,14 @@ type OpenTelemetryTraceLink { type OpenTelemetryTransactionInsight { averageLatency: Float! errorRate: Float! - id: ID! @cost(weight: "10") + id: ID! impact: Float! - latency: OpenTelemetryTransactionLatencyGraph @cost(weight: "10") + latency: OpenTelemetryTransactionLatencyGraph name: String! opm: Float! spanKind: OpenTelemetrySpanKind! successRate: Float! - throughput: OpenTelemetryTransactionThroughputGraph @cost(weight: "10") + throughput: OpenTelemetryTransactionThroughputGraph totalCount: Long! totalCountWithErrors: Long! } @@ -2613,7 +2608,7 @@ type OpenTelemetryTransactionsThroughputGraphData { } type Operation { - document: RequestDocument @cost(weight: "10") + document: RequestDocument kind: OperationKind! name: String } @@ -2627,14 +2622,14 @@ type OperationInsight { documentId: String! errorRate: Float! hash: String! - id: ID! @cost(weight: "10") + id: ID! impact: Float! kind: OperationKind - latency: OperationLatencyGraph @cost(weight: "10") + latency: OperationLatencyGraph operationName: String! opm: Float! successRate: Float! - throughput: OperationThroughputGraph @cost(weight: "10") + throughput: OperationThroughputGraph totalCount: Long! totalCountWithErrors: Long! } @@ -2762,15 +2757,15 @@ type OperationsThroughputGraphData { } type Organization implements Node { - authorizationEventLogs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int from: DateTime "Returns the last _n_ elements from the list." last: Int to: DateTime): OrganizationAuthorizationEventLogsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - billingInfo: OrganizationBillingInfo @cost(weight: "10") + authorizationEventLogs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int from: DateTime "Returns the last _n_ elements from the list." last: Int to: DateTime): OrganizationAuthorizationEventLogsConnection + billingInfo: OrganizationBillingInfo displayName: String! - groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationGroupsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationGroupsConnection id: ID! - members("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationMembersConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + members("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationMembersConnection name: String! - plan: OrganizationPlan @cost(weight: "10") - usage(from: DateTime to: DateTime): OrganizationUsage @cost(weight: "10") + plan: OrganizationPlan + usage(from: DateTime to: DateTime): OrganizationUsage } "A connection to a list of items." @@ -2822,17 +2817,17 @@ type OrganizationInfo { } type OrganizationMember implements Node { - groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationMemberGroupsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + groups("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): OrganizationMemberGroupsConnection id: ID! isDisabled: Boolean! - userName: String @cost(weight: "10") + userName: String } type OrganizationMemberGroupMember implements GroupMember { assignedAt: DateTime! - group: Group @cost(weight: "10") + group: Group id: ID! - member: OrganizationMember @cost(weight: "10") + member: OrganizationMember type: String! } @@ -2872,7 +2867,7 @@ type OrganizationPaymentIssue { type OrganizationPermissionScope implements PermissionScope { id: ID! - organization: Organization @cost(weight: "10") + organization: Organization type: String! } @@ -2881,8 +2876,8 @@ type OrganizationPlan { } type OrganizationUsage { - cumulativeGigabyteHours: OrganizationUsageCumulativeGigabyteHoursGraph @cost(weight: "10") - gigabyteHours: OrganizationUsageGigabyteHoursGraph @cost(weight: "10") + cumulativeGigabyteHours: OrganizationUsageCumulativeGigabyteHoursGraph + gigabyteHours: OrganizationUsageGigabyteHoursGraph period: OrganizationUsagePeriod! } @@ -2974,7 +2969,7 @@ type PersistedQueryErrorLocation { } type PersistedQueryValidationError implements ClientVersionPublishError & ClientVersionValidationError & SchemaVersionPublishError & SchemaVersionValidationError & FusionConfigurationValidationError & ProcessingError { - client: Client @cost(weight: "10") + client: Client clientId: ID! @deprecated(reason: "Use `client` instead.") hasMoreErrors: Boolean! message: String! @@ -2982,7 +2977,7 @@ type PersistedQueryValidationError implements ClientVersionPublishError & Client } type PersistedQueryValidationFailed { - deployedTags: [String!]! @cost(weight: "10") + deployedTags: [String!]! errors: [PersistedQueryError!]! hash: String! message: String! @@ -3061,7 +3056,7 @@ type ProcessingTaskApproved implements SchemaVersionPublishResult & ClientVersio } type ProcessingTaskIsQueued implements FusionConfigurationPublishingResult & ClientVersionPublishResult & SchemaVersionPublishResult & OpenApiCollectionVersionPublishResult & McpFeatureCollectionVersionPublishResult { - queuePosition: Int! @cost(weight: "10") + queuePosition: Int! state: ProcessingState! } @@ -3100,16 +3095,16 @@ type PublishedClient { type PublishedClientVersion { publishedAt: DateTime! - stage: Stage @cost(weight: "10") - tags: [String!]! @cost(weight: "10") @deprecated(reason: "Use `version.tag` instead.") - version: ClientVersion @cost(weight: "10") + stage: Stage + tags: [String!]! @deprecated(reason: "Use `version.tag` instead.") + version: ClientVersion } type PublishedSchemaVersion { publishedAt: DateTime! - stage: Stage @cost(weight: "10") - tag: String! @cost(weight: "10") @deprecated(reason: "Use `version.tag` instead.") - version: SchemaVersion @cost(weight: "10") + stage: Stage + tag: String! @deprecated(reason: "Use `version.tag` instead.") + version: SchemaVersion } type PushDocumentChangesPayload { @@ -3123,16 +3118,16 @@ type PushWorkspaceChangesPayload { } type Query { - apiById(id: ID!): Api @cost(weight: "10") - fusionConfigurationByApiId(id: ID! stage: String!): FusionConfiguration @cost(weight: "10") + apiById(id: ID!): Api + fusionConfigurationByApiId(id: ID! stage: String!): FusionConfiguration me: Viewer "Fetches an object given its ID." - node("ID of the object." id: ID!): Node @cost(weight: "10") + node("ID of the object." id: ID!): Node "Lookup nodes by a list of IDs." - nodes("The list of node IDs." ids: [ID!]!): [Node]! @cost(weight: "10") - organizationById(id: ID!): Organization @cost(weight: "10") - stageById(id: ID!): Stage @cost(weight: "10") - workspaceById(workspaceId: ID!): Workspace @cost(weight: "10") + nodes("The list of node IDs." ids: [ID!]!): [Node]! + organizationById(id: ID!): Organization + stageById(id: ID!): Stage + workspaceById(workspaceId: ID!): Workspace } type ReadyTimeoutError implements ClientVersionPublishError & ClientVersionValidationError & SchemaVersionPublishError & SchemaVersionValidationError & FusionConfigurationPublishingError & OpenApiCollectionVersionPublishError & OpenApiCollectionVersionValidationError & McpFeatureCollectionVersionPublishError & McpFeatureCollectionVersionValidationError & ProcessingError { @@ -3159,12 +3154,12 @@ type ResolverInsight { averageLatency: Float! coordinate: String! errorRate: Float! - id: ID! @cost(weight: "10") + id: ID! impact: Float! - latency: ResolverLatencyGraph @cost(weight: "10") + latency: ResolverLatencyGraph opm: Float! successRate: Float! - throughput: ResolverThroughputGraph @cost(weight: "10") + throughput: ResolverThroughputGraph totalCount: Long! totalCountWithErrors: Long! } @@ -3229,8 +3224,8 @@ type RoleAssignment { condition: RoleAssignmentCondition effect: RoleEffect! id: ID! - role: Role @cost(weight: "10") - scope: PermissionScope @cost(weight: "10") + role: Role + scope: PermissionScope } type RoleAssignmentStageAuthorizationCondition { @@ -3251,11 +3246,11 @@ type ScalarModifiedChange implements SchemaChange { type SchemaChangeLog implements StageChangeLog & Node { changedAt: DateTime! - changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection id: ID! kind: StageChangeLogKind! - previousSchema: SchemaVersion @cost(weight: "10") - schema: SchemaVersion @cost(weight: "10") + previousSchema: SchemaVersion + schema: SchemaVersion statistic: SchemaChangeLogStatistic! tag: String! } @@ -3290,10 +3285,10 @@ type SchemaChangesEdge { } type SchemaCoordinateMetrics implements CoordinateMetrics { - clientUsage(clientId: ID from: DateTime! to: DateTime!): CoordinateClientUsage @cost(weight: "10") - clientUsages(from: DateTime! to: DateTime!): [CoordinateClientUsage!]! @cost(weight: "10") - requests(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateRequestGraph @cost(weight: "10") - usages(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateUsageGraph @cost(weight: "10") + clientUsage(clientId: ID from: DateTime! to: DateTime!): CoordinateClientUsage + clientUsages(from: DateTime! to: DateTime!): [CoordinateClientUsage!]! + requests(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateRequestGraph + usages(from: DateTime! resolution: Int! = 300 to: DateTime!): CoordinateUsageGraph } type SchemaDeployment implements Node & Deployment { @@ -3309,9 +3304,9 @@ type SchemaDeployment implements Node & Deployment { } type SchemaDeploymentSchemaChanges { - changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) - previousSchema: SchemaVersion @cost(weight: "10") - schema: SchemaVersion @cost(weight: "10") + changes("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int severity: SchemaChangeSeverity): SchemaChangesConnection + previousSchema: SchemaVersion + schema: SchemaVersion statistic: SchemaChangeLogStatistic! } @@ -3328,7 +3323,7 @@ type SchemaRegistrySettings { type SchemaVersion { createdAt: DateTime! - downloadUrl: String! @cost(weight: "1") + downloadUrl: String! id: ID! publishedTo: [PublishedSchemaVersion!]! tag: String! @@ -3346,7 +3341,7 @@ type SchemaVersionPublishFailed implements SchemaVersionPublishResult { } type SchemaVersionPublishSuccess implements SchemaVersionPublishResult { - changeLog: SchemaChangeLog @cost(weight: "10") + changeLog: SchemaChangeLog state: ProcessingState! } @@ -3396,23 +3391,23 @@ type SetActiveWorkspacePayload { } type Stage implements Node { - api: Api @cost(weight: "10") - changeLog("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int kind: [StageChangeLogKind!] "Returns the last _n_ elements from the list." last: Int): StageChangeLogConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + api: Api + changeLog("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int kind: [StageChangeLogKind!] "Returns the last _n_ elements from the list." last: Int): StageChangeLogConnection conditions: [StageCondition!]! - coordinate(coordinate: String!): GraphQLTypeSystemMember @cost(weight: "10") - coordinates("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime isDeprecated: Boolean kinds: [CoordinateKind!] orderBy: [GraphQLCoordinateOrderByInput!] search: String to: DateTime): CoordinatesConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - deployments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): DeploymentsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + coordinate(coordinate: String!): GraphQLTypeSystemMember + coordinates("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime isDeprecated: Boolean kinds: [CoordinateKind!] orderBy: [GraphQLCoordinateOrderByInput!] search: String to: DateTime): CoordinatesConnection + deployments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): DeploymentsConnection displayName: String! - essentials: StageEssentials @cost(weight: "10") + essentials: StageEssentials id: ID! - logDistribution(from: DateTime! to: DateTime!): OpenTelemetryLogsSeverityGraph @cost(weight: "10") - logs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int from: DateTime "Returns the last _n_ elements from the list." last: Int to: DateTime): OpenTelemetryLogsConnection @listSize(assumedSize: 200, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + logDistribution(from: DateTime! to: DateTime!): OpenTelemetryLogsSeverityGraph + logs("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int from: DateTime "Returns the last _n_ elements from the list." last: Int to: DateTime): OpenTelemetryLogsConnection metrics: StageMetrics! name: String! - publishedClients: [PublishedClient!]! @cost(weight: "10") - publishedFusionConfiguration: FusionConfiguration @cost(weight: "10") - publishedSchema: PublishedSchemaVersion @cost(weight: "10") - traceById(seeker: String spanId: String traceId: String!): OpenTelemetryTrace @cost(weight: "10") + publishedClients: [PublishedClient!]! + publishedFusionConfiguration: FusionConfiguration + publishedSchema: PublishedSchemaVersion + traceById(seeker: String spanId: String traceId: String!): OpenTelemetryTrace } "A connection to a list of items." @@ -3434,11 +3429,11 @@ type StageChangeLogEdge { } type StageClientsMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [ClientInsightsOrderByInput!] to: DateTime!): ClientInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [ClientInsightsOrderByInput!] to: DateTime!): ClientInsightsConnection } type StageErrorsMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! search: String to: DateTime!): ErrorInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! search: String to: DateTime!): ErrorInsightsConnection } type StageEssentials { @@ -3476,36 +3471,36 @@ type StageNotFoundError implements Error { } type StageOperationMetrics { - latency(from: DateTime! to: DateTime!): OperationLatencyGraph @cost(weight: "10") - latencyDistribution(from: DateTime! to: DateTime!): OperationLatencyDistributionGraph @cost(weight: "10") - requestDocument: RequestDocument @cost(weight: "10") - samples(from: DateTime! maxLatency: Float minLatency: Float to: DateTime!): [OperationTraceSample!]! @cost(weight: "10") - throughput(from: DateTime! operationKinds: [OperationKind!] @deprecated(reason: "Not longer in use") to: DateTime!): OperationThroughputGraph @cost(weight: "10") + latency(from: DateTime! to: DateTime!): OperationLatencyGraph + latencyDistribution(from: DateTime! to: DateTime!): OperationLatencyDistributionGraph + requestDocument: RequestDocument + samples(from: DateTime! maxLatency: Float minLatency: Float to: DateTime!): [OperationTraceSample!]! + throughput(from: DateTime! operationKinds: [OperationKind!] @deprecated(reason: "Not longer in use") to: DateTime!): OperationThroughputGraph } type StageOperationMetricsSummary { - latency: StageLatencySummary @cost(weight: "10") - throughput: StageThroughputSummary @cost(weight: "10") + latency: StageLatencySummary + throughput: StageThroughputSummary } type StageOperationsMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! operationKinds: [OperationKind!] orderBy: [OperationInsightsOrderByInput!] search: String to: DateTime!): OperationInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - latency(from: DateTime! operationKinds: [OperationKind!] to: DateTime!): OperationsLatencyGraph @cost(weight: "10") - summary(from: DateTime! to: DateTime!): StageOperationMetricsSummary! @cost(weight: "10") - throughput(from: DateTime! operationKinds: [OperationKind!] to: DateTime!): OperationsThroughputGraph @cost(weight: "10") + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! operationKinds: [OperationKind!] orderBy: [OperationInsightsOrderByInput!] search: String to: DateTime!): OperationInsightsConnection + latency(from: DateTime! operationKinds: [OperationKind!] to: DateTime!): OperationsLatencyGraph + summary(from: DateTime! to: DateTime!): StageOperationMetricsSummary! + throughput(from: DateTime! operationKinds: [OperationKind!] to: DateTime!): OperationsThroughputGraph } type StageResolverMetrics { - latency(from: DateTime! to: DateTime!): ResolverLatencyGraph @cost(weight: "10") - throughput(from: DateTime! to: DateTime!): ResolverThroughputGraph @cost(weight: "10") + latency(from: DateTime! to: DateTime!): ResolverLatencyGraph + throughput(from: DateTime! to: DateTime!): ResolverThroughputGraph } type StageResolversMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [ResolverInsightsOrderByInput!] search: String to: DateTime!): ResolverInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [ResolverInsightsOrderByInput!] search: String to: DateTime!): ResolverInsightsConnection } type StageSubgraphsMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [SubgraphInsightsOrderByInput!] to: DateTime!): SubgraphInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [SubgraphInsightsOrderByInput!] to: DateTime!): SubgraphInsightsConnection } type StageThroughputSummary { @@ -3517,22 +3512,22 @@ type StageThroughputSummary { } type StageTransactionMetrics { - latency(from: DateTime! to: DateTime!): OpenTelemetryTransactionLatencyGraph @cost(weight: "10") - latencyDistribution(from: DateTime! to: DateTime!): OpenTelemetryTransactionLatencyDistributionGraph @cost(weight: "10") - samples(from: DateTime! maxLatency: Float minLatency: Float to: DateTime!): [OpenTelemetryTransactionTraceSample!]! @cost(weight: "10") - throughput(from: DateTime! to: DateTime!): OpenTelemetryTransactionThroughputGraph @cost(weight: "10") + latency(from: DateTime! to: DateTime!): OpenTelemetryTransactionLatencyGraph + latencyDistribution(from: DateTime! to: DateTime!): OpenTelemetryTransactionLatencyDistributionGraph + samples(from: DateTime! maxLatency: Float minLatency: Float to: DateTime!): [OpenTelemetryTransactionTraceSample!]! + throughput(from: DateTime! to: DateTime!): OpenTelemetryTransactionThroughputGraph } type StageTransactionMetricsSummary { - latency: StageLatencySummary @cost(weight: "10") - throughput: StageThroughputSummary @cost(weight: "10") + latency: StageLatencySummary + throughput: StageThroughputSummary } type StageTransactionsMetrics { - insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [OpenTelemetryTransactionInsightsOrderByInput!] search: String spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionInsightsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - latency(from: DateTime! spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionsLatencyGraph @cost(weight: "10") - summary(from: DateTime! to: DateTime!): StageTransactionMetricsSummary! @cost(weight: "10") - throughput(from: DateTime! spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionsThroughputGraph @cost(weight: "10") + insights("Returns the elements in the list that come after the specified cursor." after: String "Returns the first _n_ elements from the list." first: Int from: DateTime! orderBy: [OpenTelemetryTransactionInsightsOrderByInput!] search: String spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionInsightsConnection + latency(from: DateTime! spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionsLatencyGraph + summary(from: DateTime! to: DateTime!): StageTransactionMetricsSummary! + throughput(from: DateTime! spanKinds: [OpenTelemetrySpanKind!] to: DateTime!): OpenTelemetryTransactionsThroughputGraph } type StageValidationError implements Error { @@ -3541,7 +3536,7 @@ type StageValidationError implements Error { type StagesHavePublishedDependenciesError implements Error { message: String! - stages: [Stage!]! @cost(weight: "10") + stages: [Stage!]! } type StartFusionConfigurationCompositionPayload { @@ -3554,13 +3549,13 @@ type SubgraphInsight { errorRate: Float id: ID! impact: Float - latency: SubgraphLatencyGraph @cost(weight: "10") + latency: SubgraphLatencyGraph name: String! opm: Float - stage: Stage @cost(weight: "10") - subgraph: Subgraph @cost(weight: "10") + stage: Stage + subgraph: Subgraph successRate: Float - throughput: SubgraphThroughputGraph @cost(weight: "10") + throughput: SubgraphThroughputGraph totalCount: Long totalCountWithErrors: Long } @@ -3627,7 +3622,7 @@ type Subscription { onSchemaVersionValidationUpdate(requestId: ID!): SchemaVersionValidationResult! onStageChanged(apiId: ID! kind: [StageChangeKind!] stageName: String!): StageChangedEvent! onStageChangeLogAdded(apiId: ID! kind: [StageChangeLogKind!] stageName: String!): StageChangeLog! - onStageDeploymentsChanged(stageId: ID!): DeploymentEvent! @cost(weight: "10") + onStageDeploymentsChanged(stageId: ID!): DeploymentEvent! } type ThemeSettings { @@ -3817,34 +3812,34 @@ type ValidationInProgress implements FusionConfigurationPublishingResult & Clien } type Viewer { - activeOrganization: Organization @cost(weight: "10") - activeWorkspace: Workspace @cost(weight: "10") + activeOrganization: Organization + activeWorkspace: Workspace billingUrl: String manageTenantUrl: String organization: OrganizationInfo - personalAccessTokens("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): PersonalAccessTokensConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - preferences: Any! @cost(weight: "10") + personalAccessTokens("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): PersonalAccessTokensConnection + preferences: Any! sessionId: String! - settings: UserSettings! @cost(weight: "10") - user: User @cost(weight: "10") - workspaces("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): WorkspacesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + settings: UserSettings! + user: User + workspaces("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): WorkspacesConnection } type WaitForApproval implements SchemaVersionPublishResult & ClientVersionPublishResult & FusionConfigurationPublishingResult & OpenApiCollectionVersionPublishResult & McpFeatureCollectionVersionPublishResult { - deployment: Deployment @cost(weight: "10") + deployment: Deployment state: ProcessingState! } type Workspace implements Node { - apiDocuments("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApiDocumentsConnection @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - apiKeys("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiKeysConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - apis("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApisConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - changed(version: Version!): Boolean! @cost(weight: "10") - changes("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ChangesConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - documentChanges("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): DocumentChangesConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") @deprecated(reason: "Use changes") - documents("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): DocumentsConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - documentsChanged(version: Version): Boolean! @cost(weight: "10") @deprecated(reason: "Use changed") - environments("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): EnvironmentsConnection @authorize(policy: "DocumentsRead") @listSize(assumedSize: 50, slicingArguments: [ "first" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + apiDocuments("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApiDocumentsConnection + apiKeys("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): ApiKeysConnection + apis("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ApisConnection + changed(version: Version!): Boolean! + changes("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): ChangesConnection + documentChanges("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): DocumentChangesConnection @deprecated(reason: "Use changes") + documents("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): DocumentsConnection + documentsChanged(version: Version): Boolean! @deprecated(reason: "Use changed") + environments("Returns the elements in the list that come after the specified cursor." after: Version "Returns the first _n_ elements from the list." first: Int): EnvironmentsConnection id: ID! members: [WorkspaceMember!]! name: String! @@ -3871,7 +3866,7 @@ type WorkspaceDocument implements Node { path: [String!]! variables: String version: Version! - workspace: Workspace @cost(weight: "10") + workspace: Workspace } type WorkspaceDocumentAuthentication { @@ -3908,7 +3903,7 @@ type WorkspaceDocumentHttpConnection { type WorkspaceMember { role: WorkspaceUserRole! - user: UserInfo! @cost(weight: "10") + user: UserInfo! } type WorkspaceNotFound implements Error { @@ -3929,7 +3924,7 @@ type WorkspaceNotFoundForDocument implements Error { type WorkspacePermissionScope implements PermissionScope { id: ID! type: String! - workspace: Workspace @cost(weight: "10") + workspace: Workspace } "A connection to a list of items." @@ -4016,8 +4011,6 @@ union EnumValueChange = DeprecatedChange | DescriptionChanged union FieldChange = ArgumentAdded | ArgumentChanged | ArgumentRemoved | DeprecatedChange | DescriptionChanged | TypeChanged -union ForceDeleteStageByIdError = ApiNotFoundError | StageNotFoundError | UnauthorizedOperation - union FusionConfigurationDeploymentError = PersistedQueryValidationError | McpFeatureCollectionValidationError | OpenApiCollectionValidationError | SchemaChangeViolationError | InvalidGraphQLSchemaError union InputFieldChange = DeprecatedChange | DescriptionChanged | TypeChanged @@ -4126,6 +4119,7 @@ input ApiCreateChangeInput { name: String! path: [String!]! referenceId: String! + referenceName: String workspaceId: ID! } @@ -4204,6 +4198,7 @@ input ApiUpdateChangeInput { name: String! path: [String!]! referenceId: String! + referenceName: String version: Version! workspaceId: ID! } @@ -4353,11 +4348,6 @@ input EnvironmentVariableInput { value: String! } -input ForceDeleteStageByIdInput { - apiId: ID! - stageName: String! -} - input FusionSubgraphVersionInput { name: String! tag: String! @@ -4952,18 +4942,9 @@ enum WorkspaceUserRole { MEMBER } -"The authorize directive." -directive @authorize("Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER "The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!]) on OBJECT | FIELD_DEFINITION - -"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." -directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION - "The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." directive @defer("Deferred when true." if: Boolean "If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT -"The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." -directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error." requireOneSlicingArgument: Boolean = true "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!]) on FIELD_DEFINITION - "The `@oneOf` directive is used within the type system definition language to indicate that an Input Object is a OneOf Input Object." directive @oneOf on INPUT_OBJECT From 5402d8f6b7b5b5e0685dfa1de15d4fc7a45b0212 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 22 Mar 2026 22:54:25 +0000 Subject: [PATCH 6/7] cleanup --- website/src/docs/docs.json | 5 +- website/src/docs/mocha/v1/index.md | 10 +-- website/src/docs/mocha/v1/mediator/index.md | 34 ++++----- .../v1/mediator/pipeline-and-middleware.md | 74 +++++++++---------- 4 files changed, 63 insertions(+), 60 deletions(-) diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index 039368e4a83..11ffab084e3 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -2899,7 +2899,10 @@ "title": "Mediator", "items": [ { "path": "index", "title": "Overview" }, - { "path": "pipeline-and-middleware", "title": "Pipeline & Middleware" } + { + "path": "pipeline-and-middleware", + "title": "Pipeline & Middleware" + } ] } ] diff --git a/website/src/docs/mocha/v1/index.md b/website/src/docs/mocha/v1/index.md index 91848092d1d..761c2376d00 100644 --- a/website/src/docs/mocha/v1/index.md +++ b/website/src/docs/mocha/v1/index.md @@ -44,8 +44,8 @@ These terms appear throughout the documentation. They are defined once here and | **Transport** | The infrastructure layer connecting Mocha to a message broker, such as RabbitMQ or an in-process channel. | | **Pipeline** | The chain of middleware that processes a message from the transport through to the handler. | | **Saga** | A long-running stateful workflow that coordinates multiple messages and transitions across services. | -| **Mediator** | An in-process dispatcher that routes commands, queries, and notifications to their handlers without a transport layer. Source-generated at compile time. | -| **Command** | A mediator message representing an action. Implements `ICommand` (void) or `ICommand` (with response). Dispatched via `SendAsync`. | +| **Mediator** | An in-process dispatcher that routes commands, queries, and notifications to their handlers without a transport layer. Source-generated at compile time. | +| **Command** | A mediator message representing an action. Implements `ICommand` (void) or `ICommand` (with response). Dispatched via `SendAsync`. | | **Query** | A mediator message representing a read operation. Implements `IQuery`. Dispatched via `QueryAsync`. | # Architecture @@ -160,7 +160,7 @@ Mocha persists saga state, manages transitions, and supports compensation when s ## In-process mediator -For commands and queries that stay within a single service, the mediator provides CQRS dispatch with pipeline behaviors - without a transport layer. Define your messages with marker interfaces, implement handlers, and the source generator wires everything at compile time: +For commands and queries that stay within a single service, the mediator provides CQRS dispatch with middleware - without a transport layer. Define your messages with marker interfaces, implement handlers, and the source generator wires everything at compile time: ```csharp // Define a command and its handler @@ -183,14 +183,14 @@ public class PlaceOrderCommandHandler(AppDbContext db) // Register and use builder.Services .AddMediator() - .AddHandlers() + .AddCatalog() .UseEntityFrameworkTransactions(); app.MapPost("/orders", async (ISender sender) => await sender.SendAsync(new PlaceOrderCommand(productId, 2))); ``` -The mediator supports commands (with and without responses), queries, notifications, pipeline behaviors, pre/post processors, and EF Core transaction wrapping (commands only by default, configurable via delegate). `AddHandlers()` is source-generated - it discovers all handlers in your assembly automatically. See [Mediator](/docs/mocha/v1/mediator) for the full guide. +The mediator supports commands (with and without responses), queries, notifications, middleware, and EF Core transaction wrapping (commands only by default, configurable via delegate). The source generator produces a typed registration method per assembly (e.g. `AddCatalog()`) that wires up all handlers and pre-compiled dispatch pipelines automatically. See [Mediator](/docs/mocha/v1/mediator) for the full guide. # Learning paths diff --git a/website/src/docs/mocha/v1/mediator/index.md b/website/src/docs/mocha/v1/mediator/index.md index 776b506c69e..3d742be0cac 100644 --- a/website/src/docs/mocha/v1/mediator/index.md +++ b/website/src/docs/mocha/v1/mediator/index.md @@ -33,12 +33,12 @@ If you have used [MediatR](https://github.com/jbogard/MediatR), the concepts are Mocha has two dispatch mechanisms. Use the right one for the situation: -| Use the **mediator** when... | Use the **message bus** when... | -| --- | --- | -| Dispatch stays in-process | Messages cross process or service boundaries | -| You want [CQRS](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) separation of commands and queries | You want pub/sub events across services | -| You need a request/response pipeline with middleware | You need transport-level features (retries, outbox) | -| Handlers live in the same assembly or solution | Handlers live in different services | +| Use the **mediator** when... | Use the **message bus** when... | +| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | +| Dispatch stays in-process | Messages cross process or service boundaries | +| You want [CQRS](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) separation of commands and queries | You want pub/sub events across services | +| You need a request/response pipeline with middleware | You need transport-level features (retries, outbox) | +| Handlers live in the same assembly or solution | Handlers live in different services | The mediator and the message bus complement each other. A common pattern is to use the mediator for in-process CQRS dispatch within a service, and the message bus for inter-service event-driven communication. @@ -85,12 +85,12 @@ public record OrderPlacedNotification( ## Message type reference -| Interface | Purpose | Dispatch method | Return type | -| --- | --- | --- | --- | -| `ICommand` | Action, no response | `SendAsync` | `ValueTask` | -| `ICommand` | Action with response | `SendAsync` | `ValueTask` | -| `IQuery` | Read operation | `QueryAsync` | `ValueTask` | -| `INotification` | Multi-handler event | `PublishAsync` | `ValueTask` | +| Interface | Purpose | Dispatch method | Return type | +| --------------------- | -------------------- | --------------- | ---------------------- | +| `ICommand` | Action, no response | `SendAsync` | `ValueTask` | +| `ICommand` | Action with response | `SendAsync` | `ValueTask` | +| `IQuery` | Read operation | `QueryAsync` | `ValueTask` | +| `INotification` | Multi-handler event | `PublishAsync` | `ValueTask` | # Handlers @@ -180,12 +180,12 @@ public sealed class UpdateAnalyticsDashboard(IAnalytics analytics) ## Handler interface reference -| Interface | Message type | Response | -| --- | --- | --- | -| `ICommandHandler` | `ICommand` | void | +| Interface | Message type | Response | +| -------------------------------------- | --------------------- | ----------- | +| `ICommandHandler` | `ICommand` | void | | `ICommandHandler` | `ICommand` | `TResponse` | -| `IQueryHandler` | `IQuery` | `TResponse` | -| `INotificationHandler` | `INotification` | void | +| `IQueryHandler` | `IQuery` | `TResponse` | +| `INotificationHandler` | `INotification` | void | # Dispatching messages diff --git a/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md index 242719b948d..6dd496252d1 100644 --- a/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md +++ b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md @@ -81,10 +81,10 @@ A middleware is a static class with a `Create()` method that returns a `Mediator The factory delegate receives two arguments: -| Argument | Available at | Purpose | -| --- | --- | --- | +| Argument | Available at | Purpose | +| ---------------------------------- | ---------------------- | ----------------------------------------------------------------------------------- | | `MediatorMiddlewareFactoryContext` | Startup (compile time) | Resolve singleton services, inspect message/response types, opt out of the pipeline | -| `MediatorDelegate next` | Startup (compile time) | The next middleware or handler in the chain | +| `MediatorDelegate next` | Startup (compile time) | The next middleware or handler in the chain | The factory returns a `MediatorDelegate` - the runtime function that receives `IMediatorContext` for each dispatch. @@ -130,16 +130,16 @@ builder.Services The `IMediatorContext` available at runtime provides everything you need during dispatch: -| Property | Type | Description | -| --- | --- | --- | -| `Message` | `object` | The message instance being dispatched | -| `MessageType` | `Type` | Runtime type of the message | -| `ResponseType` | `Type` | Expected response type (`Unit` for void commands and notifications) | -| `Result` | `object?` | The handler's return value, readable by middleware after calling `next` | -| `Services` | `IServiceProvider` | Scoped service provider for the current request | -| `CancellationToken` | `CancellationToken` | Cancellation token for the operation | -| `Features` | `IFeatureCollection` | Per-request feature collection for sharing state between middleware | -| `Runtime` | `IMediatorRuntime` | The mediator runtime that owns this context | +| Property | Type | Description | +| ------------------- | -------------------- | ----------------------------------------------------------------------- | +| `Message` | `object` | The message instance being dispatched | +| `MessageType` | `Type` | Runtime type of the message | +| `ResponseType` | `Type` | Expected response type (`Unit` for void commands and notifications) | +| `Result` | `object?` | The handler's return value, readable by middleware after calling `next` | +| `Services` | `IServiceProvider` | Scoped service provider for the current request | +| `CancellationToken` | `CancellationToken` | Cancellation token for the operation | +| `Features` | `IFeatureCollection` | Per-request feature collection for sharing state between middleware | +| `Runtime` | `IMediatorRuntime` | The mediator runtime that owns this context | ## Short-circuiting @@ -263,21 +263,21 @@ public static class TransactionMiddleware ## Message kind checks -| Method | Returns true when | -| --- | --- | -| `IsCommand()` | Void command (`ICommand`) | +| Method | Returns true when | +| ------------------------- | --------------------------------------------- | +| `IsCommand()` | Void command (`ICommand`) | | `IsCommandWithResponse()` | Command with response (`ICommand`) | -| `IsQuery()` | Query (`IQuery`) | -| `IsNotification()` | Notification (`INotification`) | +| `IsQuery()` | Query (`IQuery`) | +| `IsNotification()` | Notification (`INotification`) | ## Type assignability checks -| Method | Returns true when | -| --- | --- | -| `IsMessageAssignableTo()` | Message type is assignable to `T` | -| `IsMessageAssignableTo(Type)` | Message type is assignable to the given type | -| `IsResponseAssignableTo()` | Response type is assignable to `T` (false for void commands and notifications) | -| `IsResponseAssignableTo(Type)` | Response type is assignable to the given type | +| Method | Returns true when | +| ------------------------------ | ------------------------------------------------------------------------------ | +| `IsMessageAssignableTo()` | Message type is assignable to `T` | +| `IsMessageAssignableTo(Type)` | Message type is assignable to the given type | +| `IsResponseAssignableTo()` | Response type is assignable to `T` (false for void commands and notifications) | +| `IsResponseAssignableTo(Type)` | Response type is assignable to the given type | Use `IsMessageAssignableTo` to scope a middleware to a specific message or base type: @@ -334,11 +334,11 @@ Both approaches combine well - filter out entire message kinds at compile time, Register middleware with `Use`, `Prepend`, or `Append` to control where it sits in the pipeline. -| Method | Behavior | -| --- | --- | -| `Use(config)` | Appends to the end of the middleware list | -| `Prepend(config)` | Inserts at the beginning | -| `Prepend("Logging", config)` | Inserts before the middleware with key `"Logging"` | +| Method | Behavior | +| ----------------------------------- | --------------------------------------------------------- | +| `Use(config)` | Appends to the end of the middleware list | +| `Prepend(config)` | Inserts at the beginning | +| `Prepend("Logging", config)` | Inserts before the middleware with key `"Logging"` | | `Append("Instrumentation", config)` | Inserts after the middleware with key `"Instrumentation"` | If the referenced key is not found, `Prepend(key, ...)` falls back to inserting at the beginning and `Append(key, ...)` falls back to appending at the end. @@ -360,10 +360,10 @@ The `Key` property on `MediatorMiddlewareConfiguration` is optional. Middleware ## Built-in middleware keys -| Key | Middleware | Added by | -| --- | --- | --- | -| `"Instrumentation"` | `MediatorDiagnosticMiddleware` | Always present (added by `MediatorBuilder` constructor) | -| `"EntityFrameworkTransaction"` | `EntityFrameworkTransactionMiddleware` | `UseEntityFrameworkTransactions()` | +| Key | Middleware | Added by | +| ------------------------------ | -------------------------------------- | ------------------------------------------------------- | +| `"Instrumentation"` | `MediatorDiagnosticMiddleware` | Always present (added by `MediatorBuilder` constructor) | +| `"EntityFrameworkTransaction"` | `EntityFrameworkTransactionMiddleware` | `UseEntityFrameworkTransactions()` | # Pipeline execution order @@ -389,10 +389,10 @@ The `Instrumentation` middleware is always present as the first entry because `M When a notification has multiple handlers, the **notification strategy** controls how they are invoked. Mocha ships two strategies: -| Strategy | Behavior | Default | -| --- | --- | --- | -| `ForeachAwaitPublisher` | Invokes handlers one at a time, sequentially | Yes | -| `TaskWhenAllPublisher` | Invokes all handlers concurrently with `Task.WhenAll` | No | +| Strategy | Behavior | Default | +| ----------------------- | ----------------------------------------------------- | ------- | +| `ForeachAwaitPublisher` | Invokes handlers one at a time, sequentially | Yes | +| `TaskWhenAllPublisher` | Invokes all handlers concurrently with `Task.WhenAll` | No | The default `ForeachAwaitPublisher` guarantees ordering - handlers execute in registration order. If a handler throws, subsequent handlers do not execute. From 6eb949c52d309940f3ddddf882e652991defc601 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 22 Mar 2026 23:21:50 +0000 Subject: [PATCH 7/7] fix: correct spelling in documentation and comments --- .../Outbox/OutboxServiceCollectionExtensions.cs | 2 +- src/Mocha/src/Mocha.Utilities/Mocha.Utilities.csproj | 2 ++ website/src/docs/mocha/v1/index.md | 2 +- website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Mocha/src/Mocha.EntityFrameworkCore.Postgres/Outbox/OutboxServiceCollectionExtensions.cs b/src/Mocha/src/Mocha.EntityFrameworkCore.Postgres/Outbox/OutboxServiceCollectionExtensions.cs index 33ee57b7d33..46f4ca18721 100644 --- a/src/Mocha/src/Mocha.EntityFrameworkCore.Postgres/Outbox/OutboxServiceCollectionExtensions.cs +++ b/src/Mocha/src/Mocha.EntityFrameworkCore.Postgres/Outbox/OutboxServiceCollectionExtensions.cs @@ -21,7 +21,7 @@ public static class OutboxServiceCollectionExtensions /// backed by direct Npgsql inserts. /// /// - /// This method also calls + /// This method also calls /// to register the EF Core interceptors that signal the processor on save and commit. /// /// The Entity Framework Core builder to configure. diff --git a/src/Mocha/src/Mocha.Utilities/Mocha.Utilities.csproj b/src/Mocha/src/Mocha.Utilities/Mocha.Utilities.csproj index 4b6bd676d78..4989ac14e9a 100644 --- a/src/Mocha/src/Mocha.Utilities/Mocha.Utilities.csproj +++ b/src/Mocha/src/Mocha.Utilities/Mocha.Utilities.csproj @@ -13,5 +13,7 @@ + + diff --git a/website/src/docs/mocha/v1/index.md b/website/src/docs/mocha/v1/index.md index 761c2376d00..e740c7748b9 100644 --- a/website/src/docs/mocha/v1/index.md +++ b/website/src/docs/mocha/v1/index.md @@ -23,7 +23,7 @@ Mocha gives you two dispatch mechanisms. The **message bus** sends messages acro Mocha is a messaging framework for .NET with two complementary dispatch systems: - **Message bus** - sends messages across service boundaries through transports like RabbitMQ. Supports pub/sub events, request/reply, saga orchestration, inbox/outbox reliability, and pluggable transports. Follows the patterns described in [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/patterns/messaging/Introduction.html). -- **Mediator** - dispatches commands, queries, and notifications within a single process. A Roslyn source generator produces a concrete mediator class at compile time with monomorphized dispatch and pre-compiled pipeline delegates. No reflection, no runtime code generation. +- **Mediator** - dispatches commands, queries, and notifications within a single process. A Roslyn source generator produces a concrete mediator class at compile time with specialized dispatch and pre-compiled pipeline delegates. No reflection, no runtime code generation. Both integrate directly into ASP.NET Core's dependency injection and are designed for [event-driven architectures](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven). Use the message bus when messages cross process boundaries. Use the mediator when you want in-process CQRS with pipeline behaviors for cross-cutting concerns like validation, logging, and transactions. Most real-world services use both: the mediator handles internal command/query dispatch, and the message bus handles inter-service events. diff --git a/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md index 6dd496252d1..b5067129844 100644 --- a/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md +++ b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md @@ -48,7 +48,7 @@ That is a middleware. It wraps every command, query, and notification with timin # How the pipeline works -The mediator compiles a middleware pipeline for each registered message type at application startup. Each middleware wraps the next one, forming a [chain of responsibility](https://refactoring.guru/design-patterns/chain-of-responsibility) that terminates at the handler. If you have used [middleware in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/), the mental model is the samethis is the [Pipes and Filters](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PipesAndFilters.html) pattern applied to in-process message dispatch. +The mediator compiles a middleware pipeline for each registered message type at application startup. Each middleware wraps the next one, forming a [chain of responsibility](https://refactoring.guru/design-patterns/chain-of-responsibility) that terminates at the handler. If you have used [middleware in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/), the mental model is the same - this is the [Pipes and Filters](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PipesAndFilters.html) pattern applied to in-process message dispatch. ```text SendAsync(PlaceOrderCommand)