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()
{