From a20e63228a99dc8381e84a471f5ee3f98587e325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 10 May 2022 08:45:43 +0300 Subject: [PATCH 1/5] Resolved #12163: Document update for "Support for exporting generic interfaces" --- docs/en/Dependency-Injection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Dependency-Injection.md b/docs/en/Dependency-Injection.md index 019366b3be..668eac6644 100644 --- a/docs/en/Dependency-Injection.md +++ b/docs/en/Dependency-Injection.md @@ -131,7 +131,7 @@ public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransie If you do not specify which services to expose, ABP expose services by convention. So taking the ``TaxCalculator`` defined above: * The class itself is exposed by default. That means you can inject it by ``TaxCalculator`` class. -* Default interfaces are exposed by default. Default interfaces are determined by naming convention. In this example, ``ICalculator`` and ``ITaxCalculator`` are default interfaces of ``TaxCalculator``, but ``ICanCalculate`` is not. +* Default interfaces are exposed by default. Default interfaces are determined by naming convention. In this example, ``ICalculator`` and ``ITaxCalculator`` are default interfaces of ``TaxCalculator``, but ``ICanCalculate`` is not. A generic interface (e.g. `ICalculator`) is also considered as a default interface if the naming convention is satisfied. ### Combining All Together From 19b5ae03bae130eaa1a6bb296d85d8667779b72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 10 May 2022 09:57:48 +0300 Subject: [PATCH 2/5] Documented how to enable outbox/inbox #12391 --- docs/en/Distributed-Event-Bus.md | 112 ++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index 387fece078..1bcb7ca360 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -294,4 +294,114 @@ This example; * Adds a selector to allow to publish the create, update and delete events for the `Product` entity. * Configure to use the `ProductEto` as the event transfer object to publish for the `Product` related events. -> Distributed event system use the [object to object mapping](Object-To-Object-Mapping.md) system to map `Product` objects to `ProductEto` objects. So, you need to configure the object mapping (`Product` -> `ProductEto`) too. You can check the [object to object mapping document](Object-To-Object-Mapping.md) to learn how to do it. \ No newline at end of file +> Distributed event system use the [object to object mapping](Object-To-Object-Mapping.md) system to map `Product` objects to `ProductEto` objects. So, you need to configure the object mapping (`Product` -> `ProductEto`) too. You can check the [object to object mapping document](Object-To-Object-Mapping.md) to learn how to do it. + +## Outbox / Inbox for Transactional Events + +The **[transactional outbox pattern](https://microservices.io/patterns/data/transactional-outbox.html)** is used to publishing distributed events within the **same transaction** that manipulates the application's database. When you enable outbox, distributed events are saved into the database inside the same transaction with your data changes, then sent to the actual message broker by a separate [background worker](Background-Workers.md) with a re-try system. In this way, it ensures the consistency between your database state and the published events. + +The **transactional inbox pattern**, on the other hand, saves incoming events into database first. Then (in a [background worker](Background-Workers.md)) executes the event handler in a transactional manner and removes the event from the inbox queue in the same transaction. It ensures that the event is only executed one time by keeping the processed messages for a while and discarding the duplicate events received from the message broker. + +Enabling the event outbox and inbox systems require a few manual steps for your application. Please apply the instructions in the following sections to make them running. + +> Outbox and Inbox can be separately enabled and configured, so you may only use one of them if you want. + +### Pre-requirements + +* The outbox/inbox system uses the distributed lock system to handle concurrency when you run multiple instances of your application/service. So, you should **configure the distributed lock system** with one of the providers as [explained in this document](Distributed-Locking.md). +* The outbox/inbox system supports [Entity Framework Core](Entity-Framework-Core.md) (EF Core) and [MongoDB](MongoDB.md) **database providers** out of the box. So, your applications should use one of these database providers. For other database providers, see the *Implementing a Custom Database Provider* section. + +> If you are using MongoDB, be sure that you enabled multi-document database transactions that was introduced in MongoDB version 4.0. See the *Transactions* section of the [MongoDB](MongoDB.md) document. + +### Enabling event outbox + +Open your `DbContext` class (EF Core or MongoDB), implement the `IHasEventOutbox` interface. You should end up by adding a `DbSet` property into your `DbContext` class: + +```csharp +public DbSet OutgoingEvents { get; set; } +``` + +Add the following lines inside the `OnModelCreating` method of your `DbContext` class (only for EF Core): + +```csharp +builder.ConfigureEventOutbox(); +``` + +For EF Core, use the standard `Add-Migration` and `Update-Database` commands to apply changes into your database (you can skip this step for MongoDB). If you want to use the command-line terminal, run the following commands in the root directory of the database integration project: + +```bash +dotnet ef migrations add "Added_Event_Outbox" +dotnet ef database update +``` + +Finally, write the following configuration code inside the `ConfigureServices` method of your [module class](Module-Development-Basics.md) (replace `YourDbContext` with your own `DbContext` class): + +````csharp +Configure(options => +{ + options.Outboxes.Configure(config => + { + config.UseDbContext(); + }); +}); +```` + +### Enabling event inbox + +Open your `DbContext` class (EF Core or MongoDB), implement the `IHasEventInbox` interface. You should end up by adding a `DbSet` property into your `DbContext` class: + +```csharp +public DbSet IncomingEvents { get; set; } +``` + +Add the following lines inside the `OnModelCreating` method of your `DbContext` class (only for EF Core): + +```csharp +builder.ConfigureEventInbox(); +``` + +For EF Core, use the standard `Add-Migration` and `Update-Database` commands to apply changes into your database (you can skip this step for MongoDB). If you want to use the command-line terminal, run the following commands in the root directory of the database integration project: + +```bash +dotnet ef migrations add "Added_Event_Inbox" +dotnet ef database update +``` + +Finally, write the following configuration code inside the `ConfigureServices` method of your [module class](Module-Development-Basics.md) (replace `YourDbContext` with your own `DbContext` class): + +````csharp +Configure(options => +{ + options.Inboxes.Configure(config => + { + config.UseDbContext(); + }); +}); +```` + +### Advanced Topics + +#### Implementing a Custom Database Provider + +If your application or service is using a database provider other than EF Core and MongoDB, you should manually integrate outbox/inbox system with your database provider. + +> Outbox and Inbox table/data must be stored in the same database with your application's data (since we want to create a single database transaction that includes application's database operations and outbox/inbox table operations). Otherwise, you should care about distributed (multi-database) transaction support which is not provided by most of the vendors and may require additional configuration. + +ABP provides `IEventOutbox` and `IEventInbox` abstractions as extension point for the outbox/inbox system. You can create classes by implementing these interfaces and register them to [dependency injection](Dependency-Injection.md). + +Once you implement your custom event boxes, you can configure `AbpDistributedEventBusOptions` to use your event box classes: + +````csharp +Configure(options => +{ + options.Outboxes.Configure(config => + { + config.ImplementationType = typeof(MyOutbox); //Your Outbox class + }); + + options.Inboxes.Configure(config => + { + config.ImplementationType = typeof(MyInbox); //Your Inbox class + }); +}); +```` \ No newline at end of file From 359d272a1dc60752d8aa6516b278a37d8c60f6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 10 May 2022 15:16:46 +0300 Subject: [PATCH 3/5] Documented outbox and inbox configuration --- docs/en/Distributed-Event-Bus.md | 83 +++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index 1bcb7ca360..33fc414dda 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -379,11 +379,90 @@ Configure(options => }); ```` +### Additional Configuration + +> The default configuration will be enough for most cases. However, there are some options you may want to set for outbox and inbox. + +#### Outbox configuration + +Remember how outboxes are configured: + +````csharp +Configure(options => +{ + options.Outboxes.Configure(config => + { + // TODO: Set options + }); +}); +```` + +Here, the following properties are available on the `config` object: + +* `IsSendingEnabled` (default: `true`): You can set to `false` to disable sending outbox events to the actual event bus. If you disable this, events can still be added to outbox, but not sent. This can be helpful if you have multiple applications (or application instances) writing to outbox, but use one of them to send the events. +* `Selector`: A predicate to filter the event (ETO) types to be used for this configuration. Should return `true` to select the event. It selects all the events by default. This is especially useful if you want to ignore some ETO types from the outbox, or want to define named outbox configurations and group events within these configurations. See the *Named Configurations* section. +* `ImplementationType`: Type of the class that implements the database operations for the outbox. This is normally set when you call `UseDbContext` as shown before. See *Implementing a Custom Outbox/Inbox Database Provider* section for advanced usages. + +#### Inbox configuration + +Remember how inboxes are configured: + +````csharp +Configure(options => +{ + options.Inboxes.Configure(config => + { + // TODO: Set options + }); +}); +```` + +Here, the following properties are available on the `config` object: + +* `IsProcessingEnabled` (default: `true`): You can set to `false` to disable processing (handling) events in the inbox. If you disable this, events can still be received, but not executed. This can be helpful if you have multiple applications (or application instances), but use one of them to execute the event handlers. +* `EventSelector`: A predicate to filter the event (ETO) types to be used for this configuration. This is especially useful if you want to ignore some ETO types from the inbox, or want to define named inbox configurations and group events within these configurations. See the *Named Configurations* section. +* `HandlerSelector`: A predicate to filter the event handled types (classes implementing the `IDistributedEventHandler` interface) to be used for this configuration. This is especially useful if you want to ignore some event handler types from inbox processing, or want to define named inbox configurations and group event handlers within these configurations. See the *Named Configurations* section. +* `ImplementationType`: Type of the class that implements the database operations for the inbox. This is normally set when you call `UseDbContext` as shown before. See *Implementing a Custom Outbox/Inbox Database Provider* section for advanced usages. + ### Advanced Topics -#### Implementing a Custom Database Provider +#### Named Configurations + +> All the concepts explained in this section is also valid for inbox configurations. We will show examples only for outbox to keep the document shorter. + +See the following outbox configuration code: + +````csharp +Configure(options => +{ + options.Outboxes.Configure(config => + { + //TODO + }); +}); +```` + +This is equivalent of the following code: + +````csharp +Configure(options => +{ + options.Outboxes.Configure("Default", config => + { + //TODO + }); +}); +```` + +`Default` is this code indicates the configuration name. If you don't specify it (like in the previous code block), `Default` is used as the configuration name. + +That means you can define more than one configuration for outbox (also for inbox) with different names. ABP runs all the configured outboxes. + +Multiple outboxes can be needed if your application have more than one database and you want to run different outbox queues for different databases. In this case, you can use the `Selector` option to decide the events should be handled by an outbox. See the *Additional Configurations* section above. + +#### Implementing a Custom Outbox/Inbox Database Provider -If your application or service is using a database provider other than EF Core and MongoDB, you should manually integrate outbox/inbox system with your database provider. +If your application or service is using a database provider other than [EF Core](Entity-Framework-Core.md) and [MongoDB](MongoDB.md), you should manually integrate outbox/inbox system with your database provider. > Outbox and Inbox table/data must be stored in the same database with your application's data (since we want to create a single database transaction that includes application's database operations and outbox/inbox table operations). Otherwise, you should care about distributed (multi-database) transaction support which is not provided by most of the vendors and may require additional configuration. From 7dae467621a685d1b6036dd84417c7b26062521a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 10 May 2022 15:41:31 +0300 Subject: [PATCH 4/5] Documented Transaction and Exception Handling for distributed events. --- docs/en/Distributed-Event-Bus.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index 33fc414dda..046a39ba0a 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -20,7 +20,7 @@ For example, [pre-built application modules](Modules/Index.md) is designed to wo There are two ways of publishing distributed events explained in the following sections. -### IDistributedEventBus +### Using IDistributedEventBus to Publish Events `IDistributedEventBus` can be [injected](Dependency-Injection.md) and used to publish a distributed event. @@ -57,7 +57,7 @@ namespace AbpDemo } ```` -`PublishAsync` method gets a single parameter: the event object, which is responsible to hold the data related to the event. It is a simple plain class: +`PublishAsync` method gets the event object, which is responsible to hold the data related to the event. It is a simple plain class: ````csharp using System; @@ -80,15 +80,15 @@ Even if you don't need to transfer any data, you need to create a class (which i #### Event Name -`EventName` attribute is optional, but suggested. If you don't declare it, the event name will be the full name of the event class, `AbpDemo.StockCountChangedEto` in this case. +`EventName` attribute is optional, but suggested. If you don't declare it for an event type (ETO class), the event name will be the full name of the event class, `AbpDemo.StockCountChangedEto` in this case. #### About Serialization for the Event Objects -Event transfer objects **must be serializable** since they will be serialized/deserialized to JSON or other format when it is transferred to out of the process. +Event transfer objects (ETOs) **must be serializable** since they will be serialized/deserialized to JSON or other format when it is transferred to out of the process. Avoid circular references, polymorphism, private setters and provide default (empty) constructors if you have any other constructor as a good practice (while some serializers may tolerate it), just like the DTOs. -### Inside Entity / Aggregate Root Classes +### Publishing Events Inside Entity / Aggregate Root Classes [Entities](Entities.md) can not inject services via dependency injection, but it is very common to publish distributed events inside entity / aggregate root classes. @@ -296,6 +296,16 @@ This example; > Distributed event system use the [object to object mapping](Object-To-Object-Mapping.md) system to map `Product` objects to `ProductEto` objects. So, you need to configure the object mapping (`Product` -> `ProductEto`) too. You can check the [object to object mapping document](Object-To-Object-Mapping.md) to learn how to do it. +## Transaction and Exception Handling + +Distributed event bus works in-process (since default implementation is `LocalDistributedEventBus`) unless you configure an actual provider (e.g. [Kafka](Distributed-Event-Bus-Kafka-Integration.md) or [Redis](Distributed-Event-Bus-RabbitMQ-Integration.md)). In-process event bus always executes event handlers in the same [unit of work](Unit-Of-Work.md) scope that you publishes the events in. That means, if an event handler throws an exception, then the related unit of work (the database transaction) is rolled back. In this way, your application logic and event handling logic becomes transactional (atomic) and consistent. If you want to ignore errors in an event handler, you must use a `try-catch` block in your handler and shouldn't re-throw the exception. + +When you switch to an actual distributed event bus provider (e.g. [Kafka](Distributed-Event-Bus-Kafka-Integration.md) or [Redis](Distributed-Event-Bus-RabbitMQ-Integration.md)), then the event handlers will be executed in different processes/applications as their purpose is to create distributed systems. In this case, the only way to implement transactional event publishing is to use the outbox/inbox patterns as explained in the *Outbox / Inbox for Transactional Events* section. + +If you don't configure outbox/inbox pattern or use the `LocalDistributedEventBus`, then events are published at the end of the unit of work by default, just before the unit of work is completed (that means throwing exception in an event handler still rollbacks the unit of work), even if you publish them in the middle of unit of work. If you want to immediately publish the event, you can set `onUnitOfWorkComplete` to `false` while using `IDistributedEventBus.PublishAsync` method. + +> Keeping the default behavior is recommended unless you don't have a unique requirement. `onUnitOfWorkComplete` option is not available when you publish events inside entity / aggregate root classes (see the *Publishing Events Inside Entity / Aggregate Root Classes* section). + ## Outbox / Inbox for Transactional Events The **[transactional outbox pattern](https://microservices.io/patterns/data/transactional-outbox.html)** is used to publishing distributed events within the **same transaction** that manipulates the application's database. When you enable outbox, distributed events are saved into the database inside the same transaction with your data changes, then sent to the actual message broker by a separate [background worker](Background-Workers.md) with a re-try system. In this way, it ensures the consistency between your database state and the published events. @@ -424,6 +434,10 @@ Here, the following properties are available on the `config` object: * `HandlerSelector`: A predicate to filter the event handled types (classes implementing the `IDistributedEventHandler` interface) to be used for this configuration. This is especially useful if you want to ignore some event handler types from inbox processing, or want to define named inbox configurations and group event handlers within these configurations. See the *Named Configurations* section. * `ImplementationType`: Type of the class that implements the database operations for the inbox. This is normally set when you call `UseDbContext` as shown before. See *Implementing a Custom Outbox/Inbox Database Provider* section for advanced usages. +### Skipping Outbox + +`IDistributedEventBus.PublishAsync` method provides an optional parameter, `useOutbox`, which is set to `true` by default. If you bypass outbox and immediately publish an event, you can set it to `false` for a specific event publishing operation. + ### Advanced Topics #### Named Configurations From 9243a1ced6dc2ce410e47500d0767c3c384f5768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 10 May 2022 16:15:15 +0300 Subject: [PATCH 5/5] Updated event bus for transaction changes --- docs/en/Distributed-Event-Bus.md | 6 ++++- docs/en/Local-Event-Bus.md | 44 ++++++++++++-------------------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index 046a39ba0a..17dd69072a 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -497,4 +497,8 @@ Configure(options => config.ImplementationType = typeof(MyInbox); //Your Inbox class }); }); -```` \ No newline at end of file +```` + +## See Also + +* [Local Event Bus](Local-Event-Bus.md) \ No newline at end of file diff --git a/docs/en/Local-Event-Bus.md b/docs/en/Local-Event-Bus.md index 90e11c3baf..57c12d4c3b 100644 --- a/docs/en/Local-Event-Bus.md +++ b/docs/en/Local-Event-Bus.md @@ -6,7 +6,7 @@ The Local Event Bus allows services to publish and subscribe to **in-process eve There are two ways of publishing local events explained in the following sections. -### ILocalEventBus +### Publishing Events Using the ILocalEventBus `ILocalEventBus` can be [injected](Dependency-Injection.md) and used to publish a local event. @@ -64,7 +64,7 @@ namespace AbpDemo Even if you don't need to transfer any data, you need to create a class (which is an empty class in this case). -### Inside Entity / Aggregate Root Classes +### Publishing Events Inside Entity / Aggregate Root Classes [Entities](Entities.md) can not inject services via dependency injection, but it is very common to publish local events inside entity / aggregate root classes. @@ -109,7 +109,7 @@ namespace AbpDemo `AggregateRoot` class defines the `AddLocalEvent` to add a new local event, that is published when the aggregate root object is saved (created, updated or deleted) into the database. -> If an entity publishes such an event, it is a good practice to change the related properties in a controlled manner, just like the example above - `StockCount` can only be changed by the `ChangeStockCount` method which guarantees publishing the event. +> Tip: If an entity publishes such an event, it is a good practice to change the related properties in a controlled manner, just like the example above - `StockCount` can only be changed by the `ChangeStockCount` method which guarantees publishing the event. #### IGeneratesDomainEvents Interface @@ -149,10 +149,10 @@ namespace AbpDemo } ```` -That's all. `MyHandler` is **automatically discovered** by the ABP Framework and `HandleEventAsync` is called whenever a `StockCountChangedEvent` occurs. You can inject any service and perform any required logic here. +That's all. `MyHandler` is **automatically discovered** by the ABP Framework and `HandleEventAsync` is called whenever a `StockCountChangedEvent` occurs. You can inject any service and perform any required logic in your handler class. -* **Zero or more handlers** can subscribe to the same event. -* A single event handler class can **subscribe to multiple events** but implementing the `ILocalEventHandler` interface for each event type. +* **One or more handlers** can subscribe to the same event. +* A single event handler class can **subscribe to multiple events** by implementing the `ILocalEventHandler` interface for each event type. If you perform **database operations** and use the [repositories](Repositories.md) inside the event handler, you may need to create a [unit of work](Unit-Of-Work.md), because some repository methods need to work inside an **active unit of work**. Make the handle method `virtual` and add a `[UnitOfWork]` attribute for the method, or manually use the `IUnitOfWorkManager` to create a unit of work scope. @@ -160,10 +160,11 @@ If you perform **database operations** and use the [repositories](Repositories.m ### Transaction & Exception Behavior -When an event published, subscribed event handlers are immediately executed. So; +Event handlers are always executed in the same [unit of work](Unit-Of-Work.md) scope, that means in the same database transaction with the code that published the event. If an event handler throws an exception, the unit of work (database transaction) is rolled back. So, **use try-catch yourself** in the event handler if you want to hide the error. -* If a handler **throws an exception**, it effects the code that published the event. That means it gets the exception on the `PublishAsync` call. So, **use try-catch yourself** in the event handler if you want to hide the error. -* If the event publishing code is being executed inside a [Unit Of Work](Unit-Of-Work.md) scope, the event handlers also covered by the unit of work. That means if your UOW is transactional and a handler throws an exception, the transaction is rolled back. +When you call `ILocalEventBus.PublishAsync`, the event handlers are not immediately executed. Instead, they are executed just before the current unit of work completed (an unhandled exception in the handler still rollbacks the current unit of work). If you want to immediately execute the handlers, set the optional `onUnitOfWorkComplete` parameter to `false`. + +> Keeping the default behavior is recommended unless you don't have a unique requirement. `onUnitOfWorkComplete` option is not available when you publish events inside entity / aggregate root classes (see the *Publishing Events Inside Entity / Aggregate Root Classes* section). ## Pre-Built Events @@ -195,35 +196,22 @@ namespace AbpDemo } ```` -This class subscribes to the `EntityCreatedEventData`, which is published just after a user was created. You may want to send a "Welcome" email to the new user. - -There are two types of these events: events with past tense and events with continuous tense. - -### Events with Past Tense +This class subscribes to the `EntityCreatedEventData`, which is published just after a user was created (but before the current transaction is completed). For example, you may want to send a "Welcome" email to the new user. -Events with past tense are published when the related unit of work completed and the entity change successfully saved to the database. If you throw an exception on these event handlers, it **can not rollback** the transaction since it was already committed. - -The event types are; +The pre-built event types are; * `EntityCreatedEventData` is published just after an entity was successfully created. * `EntityUpdatedEventData` is published just after an entity was successfully updated. * `EntityDeletedEventData` is published just after an entity was successfully deleted. * `EntityChangedEventData` is published just after an entity was successfully created, updated or deleted. It can be a shortcut if you need to listen any type of change - instead of subscribing to the individual events. -### Events with Continuous Tense - -Events with continuous tense are published before completing the transaction (if database transaction is supported by the database provider being used). If you throw an exception on these event handlers, it **can rollback** the transaction since it is not completed yet and the change is not saved to the database. - -The event types are; - -* `EntityCreatingEventData` is published just before saving a new entity to the database. -* `EntityUpdatingEventData` is published just before an existing entity is being updated. -* `EntityDeletingEventData` is published just before an entity is being deleted. -* `EntityChangingEventData` is published just before an entity is being created, updated or deleted. It can be a shortcut if you need to listen any type of change - instead of subscribing to the individual events. - #### How It Was Implemented? Pre-build events are published when you save changes to the database; * For EF Core, they are published on `DbContext.SaveChanges`. * For MongoDB, they are published when you call repository's `InsertAsync`, `UpdateAsync` or `DeleteAsync` methods (since MongoDB has not a change tracking system). + +## See Also + +* [Distributed Event Bus](Distributed-Event-Bus.md)