Skip to content

Commit accff7a

Browse files
Saga not found handler updates (#7976)
* Remove usage of the interface * Move multiple sagas stuff into partial * Analyzer partials for not found handler changes * Fix text * Extend snippet with not found handler mapping * Tweak tutorial to no longer use the interface * Upgrade guide * Grammar Co-authored-by: Copilot <[email protected]> * Configured handlers * Be more specific --------- Co-authored-by: Daniel Marbach <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 3b8d43c commit accff7a

File tree

11 files changed

+81
-31
lines changed

11 files changed

+81
-31
lines changed

Snippets/Core/Core_10/Core_10.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
14-
<PackageReference Include="NServiceBus" Version="10.0.0-alpha.20" />
14+
<PackageReference Include="NServiceBus" Version="10.0.0-alpha.21" />
1515
<PackageReference Include="NServiceBus.Callbacks" Version="6.0.0-alpha.3" />
1616
<PackageReference Include="NServiceBus.ClaimCheck" Version="2.0.0-alpha.5" />
1717
<PackageReference Include="NServiceBus.Encryption.MessageProperty" Version="6.0.0-alpha.3" />

Snippets/Core/Core_10/Sagas/SagaNotFoundHandler.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,43 @@ public Task Handle(object message, IMessageProcessingContext context)
1616
}
1717
}
1818

19-
public class SagaDisappearedMessage
19+
public class SagaDisappearedMessage;
20+
21+
class SagaUsingNotFoundHandler : Saga<NotFoundSagaData>, IAmStartedByMessages<StartSagaMessage>, IHandleMessages<AnotherSagaMessage>
22+
{
23+
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<NotFoundSagaData> mapper)
24+
{
25+
mapper.ConfigureNotFoundHandler<SagaNotFoundHandler>();
26+
#endregion
27+
mapper.MapSaga(saga => saga.CorrelationId)
28+
.ToMessage<StartSagaMessage>(msg => msg.CorrelationId);
29+
}
30+
31+
public Task Handle(StartSagaMessage message, IMessageHandlerContext context)
32+
{
33+
Data.CorrelationId = message.CorrelationId;
34+
return Task.CompletedTask;
35+
}
36+
37+
public Task Handle(AnotherSagaMessage message, IMessageHandlerContext context)
38+
{
39+
MarkAsComplete();
40+
return Task.CompletedTask;
41+
}
42+
}
43+
44+
45+
class AnotherSagaMessage;
46+
47+
class StartSagaMessage
2048
{
49+
public Guid CorrelationId { get; set; }
2150
}
2251

23-
#endregion
52+
class NotFoundSagaData : ContainSagaData
53+
{
54+
public Guid CorrelationId { get; set; }
55+
}
2456

2557
#region saga-not-found-error-queue
2658
public sealed class SagaNotFoundException : Exception

nservicebus/sagas/analyzers.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,11 @@ Sagas should not use a base class (i.e. `MySaga : MyAbstractSaga<TSagaData>`) to
138138

139139
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).
140140

141-
## Saga should not implement IHandleSagaNotFound
141+
## Saga should not implement saga not found handler
142142

143143
* **Rule ID**: NSB0015
144144
* **Severity**: Warning, Error starting in NServiceBus version 8
145-
* **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.
146-
147-
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.
148-
149-
"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.
150-
151-
Instead, implement `IHandleSagaNotFound` on an independent class.
145+
partial: not-found-handler
152146

153147
## Correlation property must match message mapping expression type
154148

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
* **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.
2+
3+
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.
4+
5+
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.
6+
7+
Instead, implement `IHandleSagaNotFound` on an independent class.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
* **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.
2+
3+
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.
4+
5+
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<MyNotFoundHandler>()`.

nservicebus/sagas/index.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Messages can be delivered out of order, e.g. due to error recovery, network late
7272
To ensure messages are not discarded when they arrive out of order:
7373

7474
- Implement multiple `IAmStartedByMessages<T>` interfaces for any message type that assumes the saga instance should already exist
75-
- 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.
75+
- 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.
7676

7777
#### Multiple message types starting a saga
7878

@@ -86,7 +86,7 @@ When messages arrive in reverse order, the handler for the `CompleteOrder` messa
8686

8787
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.
8888

89-
To override the default saga not found behavior [implement `IHandleSagaNotFound` and throw an exception](saga-not-found.md).
89+
To override the default saga not found behavior [implement a saga not found handler and throw an exception](saga-not-found.md).
9090

9191
## Correlating messages to a saga
9292

@@ -107,13 +107,13 @@ Instance cleanup is implemented differently by the various saga persisters and i
107107

108108
### Outstanding timeouts
109109

110-
Outstanding timeouts requested by the saga instance will be discarded when they expire without triggering the [`IHandleSagaNotFound` API](saga-not-found.md)
110+
Outstanding timeouts requested by the saga instance will be discarded when they expire without triggering the [saga not found handler](saga-not-found.md)
111111

112112
### Messages arriving after a saga has been completed
113113

114114
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.
115115

116-
Messages handled by the saga (`IHandleMessages<T>`) that arrive after the saga has completed will be passed to the [`IHandleSagaNotFound` API](saga-not-found.md).
116+
Messages handled by the saga (`IHandleMessages<T>`) that arrive after the saga has completed will be passed to the [saga not found handler](saga-not-found.md).
117117

118118
### Consistency considerations
119119

nservicebus/sagas/saga-not-found.md

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,21 @@ related:
77
- samples/saga
88
---
99

10-
The messages that are handled by sagas can either start a new saga (if handled by `IAmStartedByMessages<T>`) or update an existing saga (if handled by `IHandleMessages<T>`). 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.
10+
The messages that are handled by sagas can either start a new saga (if handled by `IAmStartedByMessages<T>`) or update an existing saga (if handled by `IHandleMessages<T>`). 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.
1111

1212
snippet: saga-not-found
1313

14-
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.
14+
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.
1515

16-
> [!NOTE]
17-
> 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.
18-
19-
| Saga A found | Saga B found | Not found handler invoked |
20-
|--------|--------|---------|
21-
| ✔️ | ✔️ ||
22-
| ✔️ |||
23-
|| ✔️ ||
24-
||| ✔️ |
16+
partial: multiple-sagas
2517

2618
include: non-null-task
2719

28-
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.
20+
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.
2921

30-
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.
22+
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.
3123

32-
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`)
24+
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`).
3325

3426
snippet: saga-not-found-error-queue
3527

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
> [!NOTE]
2+
> 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.
3+
4+
| Saga A found | Saga B found | Not found handler invoked |
5+
|--------|--------|---------|
6+
| ✔️ | ✔️ ||
7+
| ✔️ |||
8+
|| ✔️ ||
9+
||| ✔️ |

nservicebus/sagas/saga-not-found_multiple-sagas_core_[10,).partial.md

Whitespace-only changes.

nservicebus/upgrades/9to10/index.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,22 @@ If message contracts are not in a versioned library, a local copy of the message
4444

4545
## Sagas
4646

47+
### Not found handlers
48+
49+
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:
50+
51+
```csharp
52+
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MySagaData> mapper)
53+
{
54+
mapper.ConfigureNotFoundHandler<MyNotFoundHandler>();
55+
}
56+
```
57+
4758
### Custom finders
4859

4960
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:
5061

51-
```
62+
```csharp
5263
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MySagaData> mapper)
5364
{
5465
mapper.ConfigureFinderMapping<MyMessage, MySagaFinder>();

0 commit comments

Comments
 (0)