Events
Pub/sub system for loosely coupled communication
Overview
The application publishes event messages on various occasions, such as when a customer signs in or registers, places or pays an order, or performs a catalog search. These event messages can be of any complex type and do not have to adhere to any specific interface or base class. You can consume these event messages from anywhere, including from your custom modules.
However, there are two interfaces that are important for consuming and publishing events (which we will discuss in more detail later in this topic):
IEventPublisher interface is responsible for dispatching event messages to subscribers.
IConsumer interface makes a class a consumer (aka handler or subscriber) for one or more events.
Consuming Events
Event handler methods are used to perform pre- or post-processing tasks for an event. These methods must meet the following criteria:
Be public
Be non-static
Have a
void
orTask
return typeFollow the naming conventions:
For async handlers:
HandleAsync, HandleEventAsync
orConsumeAsync.
For sync handlers:
Handle, HandleEvent
orConsume
The first parameter of the method must always be the event message, or an instance of ConsumeContext<TMessage>.
The IConsumerInvoker interface decides how to call the method based on its signature:
void
methods are invoked synchronouslyTask
methods are invoked asynchronously and awaitedwith the FireForgetAttribute the method is executed in the background without awaiting. This can be advantageous in long running processes, because the current request thread is not blocked.
Use FireForgetAttribute
with caution and only if you know what you are doing 😊. A class that includes a Fire & forget consumer should not take dependencies on request scoped services, because task continuation happens on another thread, and context gets lost. Instead, pass the required dependencies as method parameters. The consumer invoker spawns a new private context for the unit of work and resolves dependencies from this context.
You can declare additional dependency parameters in the handler method:
public async Task HandleEventAsync(SomeEvent message,
IDbContext db,
ICacheManager cache,
CancellationToken cancelToken)
{
// Your code
}
Order of parameters does not matter. The invoker automatically resolves the appropriate instances and passes them to the method. Any unregistered dependency or a primitive type throws an exception, except for CancellationToken
, which always resolves to the application shutdown token.
All types that implement the IConsumer
interface are automatically detected on application startup and there is no need to register them in the service container. The class itself is registered as a scoped dependency, so it can also take dependencies in the constructor.
For example, the ValidatingCartEventConsumer class contains a HandleEventAsync
implementation. The method receives a ValidatingCartEvent
message that contains the shopping cart context as well as any warnings. The method validates the cart context and adds warnings to the message whenever the cart total is below the minimum or above the maximum allowed amount.
internal class ValidatingCartEventConsumer : IConsumer
{
private readonly IOrderProcessingService _orderProcessingService;
private readonly ILocalizationService _localizationService;
private readonly ICurrencyService _currencyService;
private readonly IWorkContext _workContext;
public ValidatingCartEventConsumer(
IOrderProcessingService orderProcessingService,
ILocalizationService localizationService,
ICurrencyService currencyService,
IWorkContext workContext)
{
_orderProcessingService = orderProcessingService;
_localizationService = localizationService;
_currencyService = currencyService;
_workContext = workContext;
}
public async Task HandleEventAsync(ValidatingCartEvent message)
{
// Order total validation.
var roleMappings = _workContext.CurrentImpersonator?.CustomerRoleMappings
?? message.Cart.Customer.CustomerRoleMappings;
var result = await _orderProcessingService
.ValidateOrderTotalAsync(message.Cart, roleMappings
.Select(x => x.CustomerRole).ToArray());
if (!result.IsAboveMinimum)
{
var convertedMin = _currencyService.ConvertFromPrimaryCurrency(result.OrderTotalMinimum, _workContext.WorkingCurrency);
message.Warnings.Add(_localizationService.GetResource("Checkout.MinOrderSubtotalAmount").FormatInvariant(convertedMin.ToString(true)));
}
if (!result.IsBelowMaximum)
{
var convertedMax = _currencyService.ConvertFromPrimaryCurrency(result.OrderTotalMaximum, _workContext.WorkingCurrency);
message.Warnings.Add(_localizationService.GetResource("Checkout.MaxOrderSubtotalAmount").FormatInvariant(convertedMax.ToString(true)));
}
}
}
Publishing events
To publish an event, you will need to create an event message of any type and populate it with the necessary data. Use the IEventPublisher service, which provides the PublishAsync
method for publishing an event and dispatching the message to all subscribers of that event.
Don't call the synchronous Publish
method, unless you absolutely cannot avoid it. It blocks the thread if any subscriber has real asynchronous code.
In the next example, the ValidatingCartEvent
is published in the Index
method of the CheckoutController. This method sends the current status of the cart and a list of any warnings that may have occurred. The same event is also handled in the previously mentioned example.
// ...
var storeId = _storeContext.CurrentStore.Id;
var customer = _workContext.CurrentCustomer;
var cart = await _shoppingCartService.GetCartAsync(customer, storeId: storeId);
if (!cart.Items.Any())
{
return RedirectToRoute("ShoppingCart");
}
if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed)
{
return new UnauthorizedResult();
}
// Validate checkout attributes.
var warnings = new List<string>();
if (!await _shoppingCartValidator.ValidateCartAsync(cart, warnings, true))
{
warnings.Take(3).Each(x => NotifyWarning(x));
return RedirectToRoute("ShoppingCart");
}
// Create event message...
var validatingCartEvent = new ValidatingCartEvent(cart, warnings);
// ...and publish
await _eventPublisher.PublishAsync(validatingCartEvent);
// ...
Message Bus
A message bus can be used for inter-server communication between nodes in a web farm, which is a group of servers that work together to host a website or application. In Smartstore, the IMessageBus service represents the message bus system. It activates when, for example, the REDIS plugin is installed, because the plugin delivers a message bus provider. By default it falls back to NullMessageBus
, which actually does nothing.
Messages sent through a message bus must be simple string
values and do not support complex data types. It is guaranteed that the server that published a message will not consume it, meaning that the message will only be passed along to other nodes for processing.
The following example shows the MemoryCacheStore class. The constructor subscribes to a channel in message bus called cache. The Subscribe
method accepts the channel name and the handler method (OnCacheEvent
in this case) that handles the message.
public MemoryCacheStore(IOptions<MemoryCacheOptions> optionsAccessor,
IMessageBus bus,
ILoggerFactory loggerFactory)
{
_optionsAccessor = optionsAccessor;
_bus = bus;
_loggerFactory = loggerFactory;
_cache = CreateCache();
// Subscribe to cache events sent by other nodes in a web farm
_bus.Subscribe("cache", OnCacheEvent);
}
// ...
private void OnCacheEvent(string channel, string message)
{
var parameter = string.Empty;
string action;
var index = message.IndexOf('^');
if (index >= 0 && index < message.Length - 1)
{
action = message[..index];
parameter = message[(index + 1)..];
}
else
{
action = message;
}
switch (action)
{
case "clear":
Clear();
break;
case "remove":
Remove(parameter);
break;
case "removebypattern":
RemoveByPattern(parameter);
break;
}
}
List of all core events
All event messages in alphabetical order. The Event suffix is omitted for brevity. Modules may provide more events than listed here. This is not a complete reference. Analyze the corresponding classes in the source code to learn more about properties and usage.
ApplicationInitialized
After the application has been initialized
CatalogSearching
Before a search request is executed
CatalogSearched
After a search request has been executed
CategoryTreeChanged
An entity that affects the category tree display has changed
CustomerAnonymized
After a customer row has been anonymized by the GDPR tool
CustomerRegistered
After a user/customer has registered
CustomerSignedIn
After a user/customer has signed in
GdprCustomerDataExported
After a customer row has been exported by the GDPR tool
ImageQueryCreated
After an image query has been created and initialized by the media middleware with data from the current query string. Implies that a thumbnail is about to be created
ImageProcessed
After image processing has finished
ImageProcessing
Before image processing begins, but after the source has been loaded
ImageUploaded
After an image - that does NOT exceed maximum allowed size - has been uploaded. This gives subscribers the chance to still process the image, e.g. to achieve better compression before saving image data to storage. This event does NOT get published when the uploaded image is about to be processed anyway
ImportBatchExecuted<T>
After a batch of data of type T has been imported
ImportExecuted
After an import process has completed
ImportExecuting
Before an import process begins
IndexingCompleted
After an indexing process has completed
IndexSegmentProcessed
After an index segment (batch) has been processed
MessageModelPartCreated<T>
After the model part T for a mail message has been created
MessageModelCreated
After a mail message has been completely created
MessageModelPartMapping
When a system mapper cannot resolve a particular model type (e.g. a custom entity in a module)
MessageQueuing
Before a mail message is put to the send queue
MenuBuilt
After a UI menu has been built (but before being cached)
MigrateShoppingCart
After a shopping cart has been migrated
ModelBound
After a model has been bound
NewsletterSubscribed
After a user subscribed to a newsletter
NewsletterUnsubscribed
After a user unsubscribed from a newsletter
OrderPaid
After an order's status has changed to Paid
OrderPlaced
After an order has been placed
OrderUpdated
After an order entity has been changed
ProductCopied
After a product has been copied/cloned
RenderingOrderTotals
Before rendering the order totals widget
RowExporting
Before exporting a data row, e.g. a product
SeedingDbMigration
Before seeding migration data
TabStripCreated
After a UI tab strip has been created
ThemeSwitched
After the main theme has been switched
ValidatingCart
Before validating the shopping cart
ViewComponentExecuting<T>
When a view component is about to create/prepare its model (of type T)
ViewComponentResultExecuting
When a view component is about to render the view
ZoneRendering
When a mail template zone is about to be rendered
Last updated
Was this helpful?