diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs index 62b832a1f73..55328308a79 100644 --- a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs @@ -204,7 +204,8 @@ public MediatorRuntime Build(IServiceProvider applicationServices) config.Kind, MediatorUrn.Message(serviceName, messageName), messageName, - config.ResponseType is { } responseType ? MediatorDescriptionHelpers.GetTypeName(responseType) : null)); + config.ResponseType is { } responseType ? MediatorDescriptionHelpers.GetTypeName(responseType) : null, + config.Source)); var terminal = config.Delegate ?? BuildPipelineViaReflection(config); diff --git a/src/Mocha/src/Mocha.Mediator/Descriptions/MediatorHandlerDescription.cs b/src/Mocha/src/Mocha.Mediator/Descriptions/MediatorHandlerDescription.cs index 0a1ac53d795..d7d05fa7528 100644 --- a/src/Mocha/src/Mocha.Mediator/Descriptions/MediatorHandlerDescription.cs +++ b/src/Mocha/src/Mocha.Mediator/Descriptions/MediatorHandlerDescription.cs @@ -9,10 +9,12 @@ namespace Mocha.Mediator; /// The URN identity of the message the handler handles. /// The readable message type name. /// The readable response type name, or null for void commands and notifications. +/// Source metadata captured from the handler declaration, or null when not provided. public sealed record MediatorHandlerDescription( string Id, string Name, MediatorHandlerKind Kind, string MessageId, string MessageName, - string? ResponseTypeName); + string? ResponseTypeName, + SourceMetadata? Source); diff --git a/src/Mocha/src/Mocha/Consumers/Consumer.cs b/src/Mocha/src/Mocha/Consumers/Consumer.cs index 4000d00ee2c..edd78425a0c 100644 --- a/src/Mocha/src/Mocha/Consumers/Consumer.cs +++ b/src/Mocha/src/Mocha/Consumers/Consumer.cs @@ -197,7 +197,8 @@ public virtual ConsumerDescription Describe() DescriptionHelpers.GetTypeName(Identity), Identity.FullName, null, - false); + false, + Configuration?.Source); } /// diff --git a/src/Mocha/src/Mocha/Consumers/Implementations/BatchConsumer.cs b/src/Mocha/src/Mocha/Consumers/Implementations/BatchConsumer.cs index 2b8ea35a5b8..cb7bfaec94b 100644 --- a/src/Mocha/src/Mocha/Consumers/Implementations/BatchConsumer.cs +++ b/src/Mocha/src/Mocha/Consumers/Implementations/BatchConsumer.cs @@ -155,7 +155,14 @@ private async Task ProcessBatchAsync(MessageBatch batch, CancellationTok public override ConsumerDescription Describe() { - return new ConsumerDescription(Urn, Name, DescriptionHelpers.GetTypeName(Identity), Identity.FullName, null, true); + return new ConsumerDescription( + Urn, + Name, + DescriptionHelpers.GetTypeName(Identity), + Identity.FullName, + null, + true, + Configuration?.Source); } public override async ValueTask DisposeAsync() diff --git a/src/Mocha/src/Mocha/Descriptions/ConsumerDescription.cs b/src/Mocha/src/Mocha/Descriptions/ConsumerDescription.cs index cd217b377e0..15c1136a925 100644 --- a/src/Mocha/src/Mocha/Descriptions/ConsumerDescription.cs +++ b/src/Mocha/src/Mocha/Descriptions/ConsumerDescription.cs @@ -9,10 +9,12 @@ namespace Mocha; /// The fully qualified type name of the handler, or null if unavailable. /// The name of the associated saga, or null if this consumer is not part of a saga. /// Whether this consumer processes messages in batches. +/// Source metadata captured from the consumer declaration, or null when not provided. public sealed record ConsumerDescription( string Id, string Name, string IdentityType, string? IdentityTypeFullName, string? SagaName, - bool IsBatch); + bool IsBatch, + SourceMetadata? Source); diff --git a/src/Mocha/src/Mocha/Descriptions/MessageTypeDescription.cs b/src/Mocha/src/Mocha/Descriptions/MessageTypeDescription.cs index 2508a8cd288..d18d9e5be4e 100644 --- a/src/Mocha/src/Mocha/Descriptions/MessageTypeDescription.cs +++ b/src/Mocha/src/Mocha/Descriptions/MessageTypeDescription.cs @@ -11,6 +11,7 @@ namespace Mocha; /// Whether this message type is internal to the bus and not exposed to user code. /// The default serialization content type, or null if using the bus default. /// The identities of message types enclosed by this type, or null if none. +/// Source metadata captured from the message type declaration, or null when not provided. public sealed record MessageTypeDescription( string Id, string Identity, @@ -19,4 +20,5 @@ public sealed record MessageTypeDescription( bool IsInterface, bool IsInternal, string? DefaultContentType, - IReadOnlyList? EnclosedMessageIdentities); + IReadOnlyList? EnclosedMessageIdentities, + SourceMetadata? Source); diff --git a/src/Mocha/src/Mocha/Descriptions/SagaDescription.cs b/src/Mocha/src/Mocha/Descriptions/SagaDescription.cs index e9a0f68702a..d0509dff282 100644 --- a/src/Mocha/src/Mocha/Descriptions/SagaDescription.cs +++ b/src/Mocha/src/Mocha/Descriptions/SagaDescription.cs @@ -9,10 +9,12 @@ namespace Mocha; /// The fully qualified type name of the saga state, or null if unavailable. /// The name of the consumer that drives this saga. /// The descriptions of all states in this saga. +/// Source metadata captured from the saga declaration, or null when not provided. public sealed record SagaDescription( string Id, string Name, string StateType, string? StateTypeFullName, string ConsumerName, - IReadOnlyList States); + IReadOnlyList States, + SourceMetadata? Source); diff --git a/src/Mocha/src/Mocha/MessageTypes/MessageType.cs b/src/Mocha/src/Mocha/MessageTypes/MessageType.cs index 2fc14af118d..5644ea32924 100644 --- a/src/Mocha/src/Mocha/MessageTypes/MessageType.cs +++ b/src/Mocha/src/Mocha/MessageTypes/MessageType.cs @@ -234,7 +234,8 @@ public MessageTypeDescription Describe() IsInterface, IsInternal, DefaultContentType?.Value, - EnclosedMessageIdentities.IsDefaultOrEmpty ? null : EnclosedMessageIdentities); + EnclosedMessageIdentities.IsDefaultOrEmpty ? null : EnclosedMessageIdentities, + Source); } [RequiresUnreferencedCode("Uses GetInterfaces and BaseType traversal which may reference trimmed types.")] diff --git a/src/Mocha/src/Mocha/Sagas/Saga.cs b/src/Mocha/src/Mocha/Sagas/Saga.cs index 60400006c3e..1ed04f29d1b 100644 --- a/src/Mocha/src/Mocha/Sagas/Saga.cs +++ b/src/Mocha/src/Mocha/Sagas/Saga.cs @@ -156,7 +156,8 @@ state.Response is not null DescriptionHelpers.GetTypeName(StateType), StateType.FullName, Consumer.Name, - states); + states, + Configuration?.Source); } /// diff --git a/src/Mocha/src/Mocha/Sagas/SagaEventListener.cs b/src/Mocha/src/Mocha/Sagas/SagaEventListener.cs index 50e69baad2c..46c558ccb42 100644 --- a/src/Mocha/src/Mocha/Sagas/SagaEventListener.cs +++ b/src/Mocha/src/Mocha/Sagas/SagaEventListener.cs @@ -79,7 +79,8 @@ public override ConsumerDescription Describe() DescriptionHelpers.GetTypeName(Identity), Identity.FullName, saga.Name, - false); + false, + Configuration?.Source ?? saga.Configuration?.Source); } /// diff --git a/src/Mocha/test/Mocha.Mediator.Tests/MediatorDescriptionTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/MediatorDescriptionTests.cs index c5124a4c6e4..2e6bcac7a2a 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/MediatorDescriptionTests.cs +++ b/src/Mocha/test/Mocha.Mediator.Tests/MediatorDescriptionTests.cs @@ -24,4 +24,30 @@ public void Describe_Should_ReturnMediatorDescription_When_HandlersRegistered() // assert description.MatchSnapshot(); } + + [Fact] + public void Describe_Should_ReturnSource_When_HandlerConfigurationHasSource() + { + // arrange + var source = new SourceMetadata + { + Assembly = "Mocha.Mediator.Tests", + RepositoryUrl = "https://github.com/example/mocha", + Commit = "abc123", + XmlDocumentation = "Manual query handler.", + DeclarationLocation = new DeclarationLocation("ManualQueryHandler.cs", 1, 1, 5, 2) + }; + var services = new ServiceCollection(); + services.AddMediator() + .ConfigureOptions(o => o.ServiceName = "orders") + .AddHandler(d => d.Extend().Configuration.Source = source); + using var provider = services.BuildServiceProvider(); + + // act + var description = provider.GetRequiredService().Describe(); + + // assert + var handler = description.Handlers.Single(h => h.Name == nameof(ManualQueryHandler)); + Assert.Same(source, handler.Source); + } } diff --git a/src/Mocha/test/Mocha.Mediator.Tests/__snapshots__/MediatorDescriptionTests.Describe_Should_ReturnMediatorDescription_When_HandlersRegistered.snap b/src/Mocha/test/Mocha.Mediator.Tests/__snapshots__/MediatorDescriptionTests.Describe_Should_ReturnMediatorDescription_When_HandlersRegistered.snap index 46789d31c57..e4ffaf231e2 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/__snapshots__/MediatorDescriptionTests.Describe_Should_ReturnMediatorDescription_When_HandlersRegistered.snap +++ b/src/Mocha/test/Mocha.Mediator.Tests/__snapshots__/MediatorDescriptionTests.Describe_Should_ReturnMediatorDescription_When_HandlersRegistered.snap @@ -8,7 +8,8 @@ "Kind": "CommandResponse", "MessageId": "urn:mocha:svc:orders:mediator:message:ManualCommand", "MessageName": "ManualCommand", - "ResponseTypeName": "ManualResponse" + "ResponseTypeName": "ManualResponse", + "Source": null }, { "Id": "urn:mocha:svc:orders:mediator:handler:ManualNotificationHandler1", @@ -16,7 +17,8 @@ "Kind": "Notification", "MessageId": "urn:mocha:svc:orders:mediator:message:ManualNotification", "MessageName": "ManualNotification", - "ResponseTypeName": null + "ResponseTypeName": null, + "Source": null }, { "Id": "urn:mocha:svc:orders:mediator:handler:ManualQueryHandler", @@ -24,7 +26,8 @@ "Kind": "Query", "MessageId": "urn:mocha:svc:orders:mediator:message:ManualQuery", "MessageName": "ManualQuery", - "ResponseTypeName": "ManualResponse" + "ResponseTypeName": "ManualResponse", + "Source": null }, { "Id": "urn:mocha:svc:orders:mediator:handler:ManualVoidCommandHandler", @@ -32,7 +35,8 @@ "Kind": "Command", "MessageId": "urn:mocha:svc:orders:mediator:message:ManualVoidCommand", "MessageName": "ManualVoidCommand", - "ResponseTypeName": null + "ResponseTypeName": null, + "Source": null } ] } diff --git a/src/Mocha/test/Mocha.Sagas.Tests/SagaInitializationTests.cs b/src/Mocha/test/Mocha.Sagas.Tests/SagaInitializationTests.cs index 927d4ebbae4..eae02ff3862 100644 --- a/src/Mocha/test/Mocha.Sagas.Tests/SagaInitializationTests.cs +++ b/src/Mocha/test/Mocha.Sagas.Tests/SagaInitializationTests.cs @@ -268,6 +268,70 @@ public void Describe_Should_UseStateAndTransitionUrns_When_SagaInitialized() Assert.Equal(startedCancel.Urn, startedCancelDescription.Id); } + [Fact] + public void Describe_Should_ReturnSource_When_SagaConfigurationHasSource() + { + // Arrange + var source = new SourceMetadata + { + Assembly = "Mocha.Sagas.Tests", + RepositoryUrl = "https://github.com/example/mocha", + Commit = "abc123", + XmlDocumentation = "Test saga.", + DeclarationLocation = new DeclarationLocation("TestSaga.cs", 1, 1, 10, 2) + }; + var saga = CreateInitializedSagaWithSource(source); + + // Act + var description = saga.Describe(); + + // Assert + Assert.Same(source, description.Source); + } + + [Fact] + public void ConsumerDescribe_Should_FallBackToSagaSource_When_ConsumerConfigurationHasNoSource() + { + // Arrange - the saga consumer never sets Source on its own ConsumerConfiguration, so + // Consumer.Describe() must fall back to the saga's own Source. + var source = new SourceMetadata + { + Assembly = "Mocha.Sagas.Tests", + RepositoryUrl = "https://github.com/example/mocha", + Commit = "abc123", + XmlDocumentation = "Test saga.", + DeclarationLocation = new DeclarationLocation("TestSaga.cs", 1, 1, 10, 2) + }; + var saga = CreateInitializedSagaWithSource(source); + + // Act + var description = saga.Consumer.Describe(); + + // Assert + Assert.Same(source, description.Source); + } + + private Saga CreateInitializedSagaWithSource(SourceMetadata source) + { + var saga = + Saga.Create(descriptor => + { + descriptor.Extend().Configuration.Source = source; + + descriptor + .Initially() + .OnEvent() + .TransitionTo("Started") + .StateFactory(s => new TestState(Guid.NewGuid(), "Started")); + descriptor.During("Started").OnEvent().TransitionTo("Success"); + descriptor.Finally("Success"); + }); + + saga.Initialize(_context); + + return saga; + } + private Saga CreateInitializedSagaWithCancelTransitions() { var saga = diff --git a/src/Mocha/test/Mocha.Tests/Consumers/ConsumerBehaviorTests.cs b/src/Mocha/test/Mocha.Tests/Consumers/ConsumerBehaviorTests.cs index 8db5189b720..c7619409d30 100644 --- a/src/Mocha/test/Mocha.Tests/Consumers/ConsumerBehaviorTests.cs +++ b/src/Mocha/test/Mocha.Tests/Consumers/ConsumerBehaviorTests.cs @@ -22,6 +22,29 @@ public void Describe_Should_ReturnConsumerDescription_When_EventHandlerRegistere description.MatchSnapshot(); } + [Fact] + public void Describe_Should_ReturnSource_When_ConsumerConfigurationHasSource() + { + // arrange + var source = new SourceMetadata + { + Assembly = "Mocha.Tests", + RepositoryUrl = "https://github.com/example/mocha", + Commit = "abc123", + XmlDocumentation = "Handles order created events.", + DeclarationLocation = new DeclarationLocation("OrderCreatedHandler.cs", 1, 1, 5, 2) + }; + var runtime = CreateRuntime( + b => b.AddEventHandler(d => d.Extend().Configuration.Source = source)); + var consumer = runtime.Consumers.First(c => c.Name == nameof(OrderCreatedHandler)); + + // act + var description = consumer.Describe(); + + // assert + Assert.Same(source, description.Source); + } + [Fact] public async Task ProcessAsync_Should_ThrowInvalidOperationException_When_ContextIsNotConsumeContext() { diff --git a/src/Mocha/test/Mocha.Tests/Consumers/__snapshots__/ConsumerBehaviorTests.Describe_Should_ReturnConsumerDescription_When_EventHandlerRegistered.snap b/src/Mocha/test/Mocha.Tests/Consumers/__snapshots__/ConsumerBehaviorTests.Describe_Should_ReturnConsumerDescription_When_EventHandlerRegistered.snap index 1feb3a87fbb..4d678ce6d88 100644 --- a/src/Mocha/test/Mocha.Tests/Consumers/__snapshots__/ConsumerBehaviorTests.Describe_Should_ReturnConsumerDescription_When_EventHandlerRegistered.snap +++ b/src/Mocha/test/Mocha.Tests/Consumers/__snapshots__/ConsumerBehaviorTests.Describe_Should_ReturnConsumerDescription_When_EventHandlerRegistered.snap @@ -4,5 +4,6 @@ "IdentityType": "OrderCreatedHandler", "IdentityTypeFullName": "Mocha.Tests.Consumers.ConsumerBehaviorTests+OrderCreatedHandler", "SagaName": null, - "IsBatch": false + "IsBatch": false, + "Source": null } diff --git a/src/Mocha/test/Mocha.Tests/MessageTypes/MessageTypeTests.cs b/src/Mocha/test/Mocha.Tests/MessageTypes/MessageTypeTests.cs index ab3a30af856..2e99e45f749 100644 --- a/src/Mocha/test/Mocha.Tests/MessageTypes/MessageTypeTests.cs +++ b/src/Mocha/test/Mocha.Tests/MessageTypes/MessageTypeTests.cs @@ -17,6 +17,33 @@ public void EventHandler_Should_RegisterMessageType_When_AddingEventHandler() Assert.Equal(typeof(OrderCreated), messageType.RuntimeType); } + [Fact] + public void Describe_Should_ReturnSource_When_MessageTypeConfigurationHasSource() + { + // arrange + var source = new SourceMetadata + { + Assembly = "Mocha.Tests", + RepositoryUrl = "https://github.com/example/mocha", + Commit = "abc123", + XmlDocumentation = "Raised when an order is created.", + DeclarationLocation = new DeclarationLocation("OrderCreated.cs", 1, 1, 3, 2) + }; + var runtime = CreateRuntime(b => + { + b.AddEventHandler(); + b.AddMessage(d => d.Extend().Configuration.Source = source); + }); + var messageType = runtime.Messages.GetMessageType(typeof(OrderCreated)); + Assert.NotNull(messageType); + + // act + var description = messageType.Describe(); + + // assert + Assert.Same(source, description.Source); + } + [Fact] public void EventHandlerMessageType_Should_HaveUrnIdentity_When_Registered() {