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 or Task return type

  • Follow the naming conventions:

    • For async handlers: HandleAsync, HandleEventAsync or ConsumeAsync.

    • For sync handlers: Handle, HandleEvent or Consume

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 synchronously

  • Task methods are invoked asynchronously and awaited

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

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.

TIP: If there are multiple handler methods present in the consumer class, you can pass shared dependencies in the class constructor. Otherwise, use method parameters.

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)));
        }
    }
}

For module developers: It is good practice to add a file Events.cs to the root of the module and implement all handler methods in it. The class should be internal. If it becomes too large, you should split/group the methods: either many or just partial classes.

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.

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.

Event
Published

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?