Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Snippets/Core/Core_10/Core_10.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
<PackageReference Include="NServiceBus" Version="10.0.0-alpha.20" />
<PackageReference Include="NServiceBus" Version="10.0.0-alpha.21" />
<PackageReference Include="NServiceBus.Callbacks" Version="6.0.0-alpha.3" />
<PackageReference Include="NServiceBus.ClaimCheck" Version="2.0.0-alpha.5" />
<PackageReference Include="NServiceBus.Encryption.MessageProperty" Version="6.0.0-alpha.3" />
Expand Down
36 changes: 34 additions & 2 deletions Snippets/Core/Core_10/Sagas/SagaNotFoundHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,43 @@ public Task Handle(object message, IMessageProcessingContext context)
}
}

public class SagaDisappearedMessage
public class SagaDisappearedMessage;

class SagaUsingNotFoundHandler : Saga<NotFoundSagaData>, IAmStartedByMessages<StartSagaMessage>, IHandleMessages<AnotherSagaMessage>
{
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<NotFoundSagaData> mapper)
{
mapper.ConfigureNotFoundHandler<SagaNotFoundHandler>();
#endregion
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simply made this v10 snippet larger, also containing the registration. To not overwhelm the page with too much noise, I moved the region inside the ConfigureHowToFindSaga method. I figured doing a partial in the page for the snippet to add another sentence seems to be overkill.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate split snippets but I get it. I wish we had some sort of #region snippet-ignore that turned multiple lines into ...

mapper.MapSaga(saga => saga.CorrelationId)
.ToMessage<StartSagaMessage>(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
Expand Down
10 changes: 2 additions & 8 deletions nservicebus/sagas/analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,11 @@ Sagas should not use a base class (i.e. `MySaga : MyAbstractSaga<TSagaData>`) 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

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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<MyNotFoundHandler>()`.
8 changes: 4 additions & 4 deletions nservicebus/sagas/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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

Expand All @@ -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

Expand All @@ -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<T>`) that arrive after the saga has completed will be passed to the [`IHandleSagaNotFound` API](saga-not-found.md).
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).

### Consistency considerations

Expand Down
20 changes: 6 additions & 14 deletions nservicebus/sagas/saga-not-found.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,21 @@ related:
- samples/saga
---

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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -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 |
|--------|--------|---------|
| ✔️ | ✔️ | ❌ |
| ✔️ | ❌ | ❌ |
| ❌ | ✔️ | ❌ |
| ❌ | ❌ | ✔️ |
Empty file.
13 changes: 12 additions & 1 deletion nservicebus/upgrades/9to10/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<MySagaData> mapper)
{
mapper.ConfigureNotFoundHandler<MyNotFoundHandler>();
}
```

### 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<MySagaData> mapper)
{
mapper.ConfigureFinderMapping<MyMessage, MySagaFinder>();
Expand Down
2 changes: 1 addition & 1 deletion tutorials/nservicebus-sagas/3-integration/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down