-
Notifications
You must be signed in to change notification settings - Fork 647
Explicit custom saga finder registration API #7445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
@danielmarbach still a bit rought and some types might have to be added back but please take a first look and see if you can spot something obvious |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the saga finder mechanism in NServiceBus by introducing a new ConfigureFinderMapping method to explicitly configure custom finders in the saga mapping configuration. The changes consolidate saga metadata creation logic, remove the need for automatic finder scanning, and modernize the codebase with primary constructors and expression-bodied members.
- Introduces
ConfigureFinderMapping<TMessage, TFinder>()method to explicitly configure custom saga finders - Refactors saga metadata creation by moving logic from
SagaMetadatato a newSagaMapperclass - Removes automatic custom finder registration and scanning in favor of explicit configuration
- Modernizes code style with primary constructors, expression-bodied members, and collection expressions
Reviewed Changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
SagaPropertyMapper.cs |
Adds new ConfigureFinderMapping method for explicit custom finder configuration |
SagaMapper.cs |
New class that consolidates saga mapping logic previously in SagaMetadata |
SagaMetadata.cs |
Simplified by moving mapping logic to SagaMapper and removing finder scanning |
SagaMetadataCollection.cs |
Removes overload that accepted Conventions parameter |
CustomFinderAdapter.cs |
Refactored to support disposal of custom finders and use ObjectFactory |
PropertySagaFinder.cs |
Changed to use DI to resolve ISagaPersister rather than constructor injection |
HeaderPropertySagaFinder.cs |
Changed to use DI to resolve ISagaPersister rather than constructor injection |
Sagas.cs |
Removed RegisterCustomFindersInContainer method as finders are no longer pre-registered |
| Test files | Updated to use explicit finder mapping and removed Conventions parameter usage |
| Acceptance tests | Updated sagas to explicitly configure finder mappings using new API |
Comments suppressed due to low confidence (1)
src/NServiceBus.AcceptanceTests/Core/SelfVerification/When_running_saga_tests.cs:33
- This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
foreach (var property in entity.GetProperties())
{
if (!property.GetGetMethod().IsVirtual)
{
offenders++;
Console.WriteLine("ERROR: {0}.{1} must be marked as virtual for NHibernate tests to succeed.", entity.FullName, property.Name);
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| if (correlationProperty != null && correlationProperty.Name != sagaProp.Name) | ||
| { | ||
| throw new ArgumentException($"Saga already have a mapping to property {correlationProperty.Name} and sagas can only have mappings that correlate on a single saga property. Use a custom finder to correlate {typeof(TMessage)} to saga {sagaType.Name}"); |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammar error: "Saga already have" should be "Saga already has" (subject-verb agreement).
| { | ||
| if (sagaMessageFindingConfiguration is not IConfigureHowToFindSagaWithFinder sagaMapperFindingConfiguration) | ||
| { | ||
| throw new Exception($"Unable to configure header mapping. To fix this, ensure that {sagaMessageFindingConfiguration.GetType().FullName} implements {nameof(IConfigureHowToFindSagaWithFinder)}."); |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message says "Unable to configure header mapping" but this method is for configuring finder mapping, not header mapping. The error message should say "Unable to configure finder mapping" instead.
| foreach (var sagaMessage in sagaMessages) | ||
| { | ||
| if (sagaMessage.IsAllowedToStartSaga) | ||
| { | ||
| var match = finders.FirstOrDefault(m => m.MessageType.IsAssignableFrom(sagaMessage.MessageType)); | ||
| if (match == null) | ||
| { | ||
| var simpleName = sagaMessage.MessageType.Name; | ||
| throw new Exception($"Message type {simpleName} can start the saga {sagaType.Name} (the saga implements IAmStartedByMessages<{simpleName}>) but does not map that message to saga data. In the ConfigureHowToFindSaga method, add a mapping using:{Environment.NewLine} mapper.ConfigureMapping<{simpleName}>(message => message.SomeMessageProperty).ToSaga(saga => saga.MatchingSagaProperty);"); | ||
| } | ||
| } | ||
| } |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| if (fieldInfo != null) | ||
| { | ||
| if (fieldInfo.FieldType != sagaProp.PropertyType) | ||
| { | ||
| throw new ArgumentException($"When mapping a message to a saga, the member type on the message and the saga property must match. {fieldInfo.DeclaringType.FullName}.{fieldInfo.Name} is of type {fieldInfo.FieldType.Name} and {sagaProp.DeclaringType.FullName}.{sagaProp.Name} is of type {sagaProp.PropertyType.Name}."); | ||
| } | ||
| } |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 'if' statements can be combined.
| if (correlationProperty != null) | ||
| { | ||
| if (!AllowedCorrelationPropertyTypes.Contains(correlationProperty.Type)) | ||
| { | ||
| var supportedTypes = string.Join(",", AllowedCorrelationPropertyTypes.Select(t => t.Name)); | ||
|
|
||
| throw new Exception($"{correlationProperty.Type.Name} is not supported for correlated properties. Change the correlation property {correlationProperty.Name} on saga {sagaType.Name} to any of the supported types, {supportedTypes}, or use a custom saga finder."); | ||
| } | ||
| } |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 'if' statements can be combined.
b853d56 to
16ea023
Compare
Co-authored-by: andreasohlund <[email protected]>
| { | ||
| void ConfigureMapping<TSagaEntity, TMessage>(System.Linq.Expressions.Expression<System.Func<TSagaEntity, object?>> sagaEntityProperty, System.Linq.Expressions.Expression<System.Func<TMessage, object?>> messageProperty) | ||
| where TSagaEntity : NServiceBus.IContainSagaData; | ||
| where TSagaEntity : class, NServiceBus.IContainSagaData; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any net effect of this on the user? We already don't/can't allow saga data as struct, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that is my understanding
Co-authored-by: David Boike <[email protected]>
This PR introduces a significant refactoring of the saga finder mechanism in NServiceBus, replacing automatic assembly scanning for custom saga finders with an explicit registration API.
Summary
The changes introduce a new explicit mapping API for custom saga finders through the
ConfigureFinderMapping<TMessage, TFinder>()method. This is a breaking change that removes the previous automatic discovery of saga finders via assembly scanning. Additionally, the PR aligns exception types for validation to useArgumentExceptionconsistently.Key Changes
Explicit Finder Registration: Custom saga finders must now be explicitly registered using
mapper.ConfigureFinderMapping<TMessage, TFinder>()in the saga'sConfigureHowToFindSagamethod.Removal of Assembly Scanning: The framework no longer automatically discovers and registers custom finders through assembly scanning.
Exception Type Alignment: Validation exceptions have been standardized to throw
ArgumentExceptioninstead of mixed exception types.Code Modernization: Extensive use of C# modern features including:
Breaking Changes
1. Custom Finder Registration
Before:
After:
2. Exception Type Changes
Validation errors that previously threw different exception types now consistently throw
ArgumentException:API Changes
New Public APIs
IConfigureHowToFindSagaWithFinder Interface
SagaPropertyMapper.ConfigureFinderMapping Method
Modified APIs
IConfigureHowToFindSagaWithMessage
classconstraint toTSagaEntitygeneric parameterIConfigureHowToFindSagaWithMessageHeaders
classconstraint toTSagaEntitygeneric parameterRemoved/Obsoleted APIs
IEnumerable<Type> availableTypesandConventionsConventionsparameterTypepropertyMessageTypeNamepropertyPropertiesdictionaryInternal Refactoring
Several internal classes were removed or significantly refactored:
CorrelationSagaToMessageMap- RemovedCustomFinderSagaToMessageMap- RemovedHeaderFinderSagaToMessageMap- RemovedPropertyFinderSagaToMessageMap- RemovedSagaFinderabstract class - RemovedSagaToMessageMap- RemovedImpact on Different Areas
1. Core Saga Framework
ObjectFactory<TFinder>for strongly-typed finder instantiationISagaPersistervia DI instead of constructor injectionISagaPersistervia DI instead of constructor injectionSagaMetadataSagaMapper2. Finder Disposal
The PR adds proper disposal support for custom finders:
IDisposableare synchronously disposedIAsyncDisposableare asynchronously disposed3. Test Updates
All acceptance tests and unit tests were updated to use the new explicit finder registration:
When_adding_state_to_context.csWhen_finder_cant_find_saga_instance.csWhen_finder_returns_existing_saga.cs4. Public API Surface
The public API surface was reduced by removing:
Migration Guide
For Users with Custom Saga Finders
If you have custom saga finders in your codebase:
Identify Custom Finders: Find all classes implementing
ISagaFinder<TSagaData, TMessage>Add Explicit Registration: In each saga's
ConfigureHowToFindSagamethod, add:Update Validation: If you have custom validation that was catching specific exception types, update to catch
ArgumentExceptionExample Migration
Before:
After:
Rationale
The move from implicit scanning to explicit registration provides several benefits:
Review Feedback
From the Copilot review summary:
Testing
The PR includes comprehensive test updates:
Related Work
This PR is part of a broader effort in NServiceBus 10 to:
Similar changes have been made for:
Documentation Needs
The following documentation should be updated: