diff --git a/Snippets/Core/Core_10/Core_10.csproj b/Snippets/Core/Core_10/Core_10.csproj index 4930f09401d..37de1147d64 100644 --- a/Snippets/Core/Core_10/Core_10.csproj +++ b/Snippets/Core/Core_10/Core_10.csproj @@ -11,7 +11,7 @@ - + diff --git a/Snippets/Core/Core_10/Sagas/SagaNotFoundHandler.cs b/Snippets/Core/Core_10/Sagas/SagaNotFoundHandler.cs index 7756e12cd54..d025523e233 100644 --- a/Snippets/Core/Core_10/Sagas/SagaNotFoundHandler.cs +++ b/Snippets/Core/Core_10/Sagas/SagaNotFoundHandler.cs @@ -16,11 +16,43 @@ public Task Handle(object message, IMessageProcessingContext context) } } -public class SagaDisappearedMessage +public class SagaDisappearedMessage; + +class SagaUsingNotFoundHandler : Saga, IAmStartedByMessages, IHandleMessages +{ + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureNotFoundHandler(); + #endregion + mapper.MapSaga(saga => saga.CorrelationId) + .ToMessage(msg => msg.CorrelationId); + } + + public Task Handle(StartSagaMessage message, IMessageHandlerContext context) + { + Data.CorrelationId = message.CorrelationId; + return Task.CompletedTask; + } + + public Task Handle(AnotherSagaMessage message, IMessageHandlerContext context) + { + MarkAsComplete(); + return Task.CompletedTask; + } +} + + +class AnotherSagaMessage; + +class StartSagaMessage { + public Guid CorrelationId { get; set; } } -#endregion +class NotFoundSagaData : ContainSagaData +{ + public Guid CorrelationId { get; set; } +} #region saga-not-found-error-queue public sealed class SagaNotFoundException : Exception diff --git a/nservicebus/sagas/analyzers.md b/nservicebus/sagas/analyzers.md index f54ca8aec29..7ae8d9a5d8b 100644 --- a/nservicebus/sagas/analyzers.md +++ b/nservicebus/sagas/analyzers.md @@ -138,17 +138,11 @@ Sagas should not use a base class (i.e. `MySaga : MyAbstractSaga`) to A better way to provide shared functionality to multiple saga types and reduce code duplication is to use [extension methods](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods). -## Saga should not implement IHandleSagaNotFound +## Saga should not implement saga not found handler * **Rule ID**: NSB0015 * **Severity**: Warning, Error starting in NServiceBus version 8 -* **Example message**: A saga should not implement `IHandleSagaNotFound`, as this catch-all handler will handle messages where *any* saga is not found. Implement `IHandleSagaNotFound` on a separate class instead. - -A ["saga not found" handler](/nservicebus/sagas/saga-not-found.md) provides a way to deal with messages that are not allowed to start a saga but cannot find existing saga data. - -"Saga not found" handlers operate on all saga messages within an endpoint, no matter which saga the message was originally bound for. So it is misleading to implement `IHandleSagaNotFound` on a saga because it creates the impression that it will only handle not found messages for that _specific_ saga, which is false. - -Instead, implement `IHandleSagaNotFound` on an independent class. +partial: not-found-handler ## Correlation property must match message mapping expression type diff --git a/nservicebus/sagas/analyzers_not-found-handler_core_[,10).partial.md b/nservicebus/sagas/analyzers_not-found-handler_core_[,10).partial.md new file mode 100644 index 00000000000..a934fc52f88 --- /dev/null +++ b/nservicebus/sagas/analyzers_not-found-handler_core_[,10).partial.md @@ -0,0 +1,7 @@ +* **Example message**: A saga should not implement `IHandleSagaNotFound`, as this catch-all handler will handle messages where *any* saga is not found. Implement `IHandleSagaNotFound` on a separate class instead. + +A [saga not found handler](/nservicebus/sagas/saga-not-found.md) provides a way to deal with messages that are not allowed to start a saga but cannot find existing saga data. + +Saga not found handlers operate on all saga messages within an endpoint, no matter which saga the message was originally bound for. So it is misleading to implement `IHandleSagaNotFound` on a saga because it creates the impression that it will only handle not found messages for that _specific_ saga, which is false. + +Instead, implement `IHandleSagaNotFound` on an independent class. \ No newline at end of file diff --git a/nservicebus/sagas/analyzers_not-found-handler_core_[10,).partial.md b/nservicebus/sagas/analyzers_not-found-handler_core_[10,).partial.md new file mode 100644 index 00000000000..560fc0b1d01 --- /dev/null +++ b/nservicebus/sagas/analyzers_not-found-handler_core_[10,).partial.md @@ -0,0 +1,5 @@ +* **Example message**: A saga should not implement `ISagaNotFoundHandler`, as this gives access to the uninitialized saga data property. Implement `ISagaNotFoundHandler` on a separate class instead. + +A [saga not found handler](/nservicebus/sagas/saga-not-found.md) provides a way to deal with messages that are not allowed to start a saga but cannot find existing saga data. + +Saga not found handlers may require specific dependencies to be injected into the class and may implement non-trivial handling logic. To separate those concerns from the saga an independent class should implement `ISagaNotFoundHandler` and be mapped with `mapper.ConfigureNotFoundHandler()`. \ No newline at end of file diff --git a/nservicebus/sagas/index.md b/nservicebus/sagas/index.md index c148c0a1933..a816dfc1a91 100644 --- a/nservicebus/sagas/index.md +++ b/nservicebus/sagas/index.md @@ -72,7 +72,7 @@ Messages can be delivered out of order, e.g. due to error recovery, network late To ensure messages are not discarded when they arrive out of order: - Implement multiple `IAmStartedByMessages` interfaces for any message type that assumes the saga instance should already exist -- Override the saga not found behavior and throw an exception using `IHandleSagaNotFound` and rely on NServiceBus recoverability capability to retry messages to resolve out-of-order issues. +- Override the saga not found behavior and throw an exception using a saga not found handler and rely on NServiceBus recoverability capability to retry messages to resolve out-of-order issues. #### Multiple message types starting a saga @@ -86,7 +86,7 @@ When messages arrive in reverse order, the handler for the `CompleteOrder` messa In most scenarios, an acceptable solution to deal with out-of-order message delivery is to throw an exception when the saga instance does not exist. The message will be automatically retried, which may resolve the issue; otherwise, it will be placed in the error queue, where it can be manually retried. -To override the default saga not found behavior [implement `IHandleSagaNotFound` and throw an exception](saga-not-found.md). +To override the default saga not found behavior [implement a saga not found handler and throw an exception](saga-not-found.md). ## Correlating messages to a saga @@ -107,13 +107,13 @@ Instance cleanup is implemented differently by the various saga persisters and i ### Outstanding timeouts -Outstanding timeouts requested by the saga instance will be discarded when they expire without triggering the [`IHandleSagaNotFound` API](saga-not-found.md) +Outstanding timeouts requested by the saga instance will be discarded when they expire without triggering the [saga not found handler](saga-not-found.md) ### Messages arriving after a saga has been completed Messages that [are allowed to start a new saga instance](#starting-a-saga) will cause a new instance with the same correlation id to be created. -Messages handled by the saga (`IHandleMessages`) that arrive after the saga has completed will be passed to the [`IHandleSagaNotFound` API](saga-not-found.md). +Messages handled by the saga (`IHandleMessages`) that arrive after the saga has completed will be passed to the [saga not found handler](saga-not-found.md). ### Consistency considerations diff --git a/nservicebus/sagas/saga-not-found.md b/nservicebus/sagas/saga-not-found.md index 09f5c7a32b6..b83a77b794f 100644 --- a/nservicebus/sagas/saga-not-found.md +++ b/nservicebus/sagas/saga-not-found.md @@ -7,29 +7,21 @@ related: - samples/saga --- -The messages that are handled by sagas can either start a new saga (if handled by `IAmStartedByMessages`) or update an existing saga (if handled by `IHandleMessages`). If the incoming message is meant to be handled by a saga but is not expected to start a new one, then NServiceBus uses [correlation rules](/nservicebus/sagas/#correlating-messages-to-a-saga) to find an existing saga. If no existing saga can be found, all implementations of `IHandleSagaNotFound` are executed. If no implementation can be found, the message is discarded without additional notification. +The messages that are handled by sagas can either start a new saga (if handled by `IAmStartedByMessages`) or update an existing saga (if handled by `IHandleMessages`). If the incoming message is meant to be handled by a saga but is not expected to start a new one, then NServiceBus uses [correlation rules](/nservicebus/sagas/#correlating-messages-to-a-saga) to find an existing saga. If no existing saga can be found, configured saga not found handlers are executed. If no handler is configured, the message is discarded without additional notification. snippet: saga-not-found -Note that in the example above, the message will be considered successfully processed and sent to the audit queue even if no saga was found. Throw an exception from the `IHandleSagaNotFound` implementation to move the message to the error queue. +Note that in the example above, the message will be considered successfully processed and sent to the audit queue even if no saga was found. Throw an exception from the saga not found handler implementation to move the message to the error queue. -> [!NOTE] -> If there are multiple saga types that handle a given message type and one of them is found while others are not, the `IHandleSagaNotFound` handlers **will not be executed**. The `IHandleSagaNotFound` handlers are executed only if no saga instances are invoked. The following table illustrates when the `IHandleSagaNotFound` handlers are invoked when a message is mapped to two different saga types, A and B. - -| Saga A found | Saga B found | Not found handler invoked | -|--------|--------|---------| -| ✔️ | ✔️ | ❌ | -| ✔️ | ❌ | ❌ | -| ❌ | ✔️ | ❌ | -| ❌ | ❌ | ✔️ | +partial: multiple-sagas include: non-null-task -The ability to provide an implementation for `IHandleSagaNotFound` is especially useful if compensating actions are needed for messages that arrive after the saga has been marked as complete. This is a common scenario when using timeouts inside the saga. +Implementing a saga not found handler is especially useful if compensating actions are needed for messages that arrive after the saga has been marked as complete. This is a common scenario when using timeouts inside the saga. -For example, consider a saga used to manage the registration process on the website. After a customer registers, they receive an email with a confirmation link. The system will wait for confirmation for a specific period of time, e.g., 24 hours. If the user doesn't click the link within 24 hours, their data is removed from the system, and the saga is completed. However, they might decide to click the confirmation link a few days later. In this case, the related saga instance can't be found, and an exception will be thrown. By implementing `IHandleSagaNotFound`, it is possible to handle the situation differently, e.g., redirect the user to the registration website and ask them to fill out the form again. +For example, consider a saga used to manage the registration process on the website. After a customer registers, they receive an email with a confirmation link. The system will wait for confirmation for a specific period of time, e.g., 24 hours. If the user doesn't click the link within 24 hours, their data is removed from the system, and the saga is completed. However, they might decide to click the confirmation link a few days later. In this case, the related saga instance can't be found, and an exception will be thrown. By implementing a saga not found handler, it is possible to handle the situation differently, e.g., redirect the user to the registration website and ask them to fill out the form again. -The implementation of `IHandleSagaNotFound` should be driven by the business requirements for a specific situation. In some cases, the message might be ignored; in others, it might be useful to track whenever that situation happens (e.g., by logging or sending another message). In other cases, it might make sense to perform a custom compensating action. For example, should it be necessary in some rare cases to move the message that did not find a saga to the error queue, it is possible to introduce a custom exception type (e.g`SagaNotFoundFoundException`) +The implementation of a saga not found handler should be driven by the business requirements for a specific situation. In some cases, the message might be ignored; in others, it might be useful to track whenever that situation happens (e.g., by logging or sending another message). In other cases, it might make sense to perform a custom compensating action. For example, should it be necessary in some rare cases to move the message that did not find a saga to the error queue, it is possible to introduce a custom exception type (e.g. `SagaNotFoundException`). snippet: saga-not-found-error-queue diff --git a/nservicebus/sagas/saga-not-found_multiple-sagas_core_[,10).partial.md b/nservicebus/sagas/saga-not-found_multiple-sagas_core_[,10).partial.md new file mode 100644 index 00000000000..a933ba21421 --- /dev/null +++ b/nservicebus/sagas/saga-not-found_multiple-sagas_core_[,10).partial.md @@ -0,0 +1,9 @@ +> [!NOTE] +> If there are multiple saga types that handle a given message type and one of them is found while others are not, the saga not found handlers **will not be executed**. The saga not found handlers are executed only if no saga instances are invoked. The following table illustrates when the saga not found handlers are invoked when a message is mapped to two different saga types, A and B. + +| Saga A found | Saga B found | Not found handler invoked | +|--------|--------|---------| +| ✔️ | ✔️ | ❌ | +| ✔️ | ❌ | ❌ | +| ❌ | ✔️ | ❌ | +| ❌ | ❌ | ✔️ | \ No newline at end of file diff --git a/nservicebus/sagas/saga-not-found_multiple-sagas_core_[10,).partial.md b/nservicebus/sagas/saga-not-found_multiple-sagas_core_[10,).partial.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/nservicebus/upgrades/9to10/index.md b/nservicebus/upgrades/9to10/index.md index 5bb5cf9c787..6ae7c47910d 100644 --- a/nservicebus/upgrades/9to10/index.md +++ b/nservicebus/upgrades/9to10/index.md @@ -44,11 +44,22 @@ If message contracts are not in a versioned library, a local copy of the message ## Sagas +### Not found handlers + +In Version 10 the `IHandleSagaNotFound` interface has been deprecated in favour of `ISagaNotFoundHandler`. The [saga not found handlers](/nservicebus/sagas/saga-not-found.md) are no longer automatically registered via assembly scanning and must be mapped in the `ConfigureHowToFindSaga` method of the sagas that require the not found handler to be executed: + +```csharp +protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) +{ + mapper.ConfigureNotFoundHandler(); +} +``` + ### Custom finders In Version 10 [custom saga finders](/nservicebus/sagas/saga-finding.md) are no longer automatically registered via assembly scanning and must be mapped in the `ConfigureHowToFindSaga` method: -``` +```csharp protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { mapper.ConfigureFinderMapping(); diff --git a/tutorials/nservicebus-sagas/3-integration/tutorial.md b/tutorials/nservicebus-sagas/3-integration/tutorial.md index 8a1b2244fbd..0051140faa1 100644 --- a/tutorials/nservicebus-sagas/3-integration/tutorial.md +++ b/tutorials/nservicebus-sagas/3-integration/tutorial.md @@ -330,7 +330,7 @@ We also see a new message at the end, similar to what happened when timeout mess This is caused by the `ShipmentAcceptedByAlpine` message being returned _after_ the second timeout has already given up on Alpine, published `ShipmentFailed`, and marked the saga as complete, removing it from storage. As we've defined the saga thus far, this is working as intended, but this is another place where it all comes down to business requirements. -It is possible to handle these instances by [creating an `IHandleSagaNotFound` implementation](/nservicebus/sagas/saga-not-found.md). Another possibility would be to keep the saga alive for longer (by not calling `MarkAsComplete()`) and setting another timeout to clean up the saga later on. It really depends on what the exact rules are for your specific scenario, which you can only discover through consultation with business stakeholders. +It is possible to handle these instances by [creating a saga not found handler](/nservicebus/sagas/saga-not-found.md). Another possibility would be to keep the saga alive for longer (by not calling `MarkAsComplete()`) and setting another timeout to clean up the saga later on. It really depends on what the exact rules are for your specific scenario, which you can only discover through consultation with business stakeholders. ## Summary