messaging domain events - grecosoft/NetFusion GitHub Wiki
Messaging - Domain Events
The IMessagingService interface is used to publish Domain-Events. For in-process message consumers, the message handler can be either synchronous or asynchronous. Domain-events are used to notify other components or microservices of changes to the state managed by the microservice. This allows components within the microservice to be loosely coupled.
Define Domain Event
Publishing a domain event is similar to sending a command. However, a domain-event does not have a result type and can have multiple consumer handlers.
There can be a combination of both synchronous and asynchronous handlers for a given domain event.
This example will publish a domain-event to notify other application components when an automobile is sold. Create the following domain-event within the Events directory of the Examples.Messaging.Domain project:
using NetFusion.Messaging.Types;
namespace Examples.Messaging.Domain.Events;
public class AutoSoldEvent: DomainEvent
{
public string Make { get; }
public string Model { get; }
public int Year { get; }
public AutoSoldEvent(
string make,
string model,
int year)
{
Make = make;
Model = model;
Year = year;
}
}
Domain-Events derive from the base DomainEvent class.
Define Message Consumer
The following will define two consumers for the above domain-event. While both methods can be placed within the same handler, two separate handler classes will be created. The first handler will have a synchronous message handler while the second handler will be asynchronous. Message consumers typically go within the application project of the microservice solution.
Create the following two classes within the Handlers directory of the Examples.Messaging.App project:
using Examples.Messaging.Domain.Events;
using Microsoft.Extensions.Logging;
namespace Examples.Messaging.App.Handlers;
public class AmericanAutoSalesHandler
{
private readonly ILogger<GermanAutoSalesHandler> _logger;
public AmericanAutoSalesHandler(ILogger<GermanAutoSalesHandler> logger)
{
_logger = logger;
}
public void OnRegistration(AutoSoldEvent domainEvent)
{
_logger.LogInformation("Domain Event Received by {handler} for {make} and {model}.",
nameof(AmericanAutoSalesHandler), domainEvent.Make, domainEvent.Model);
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
using Examples.Messaging.Domain.Events;
using Microsoft.Extensions.Logging;
namespace Examples.Messaging.App.Handlers;
public class GermanAutoSalesHandler
{
private readonly ILogger<GermanAutoSalesHandler> _logger;
public GermanAutoSalesHandler(ILogger<GermanAutoSalesHandler> logger)
{
_logger = logger;
}
public async Task OnRegistration(AutoSoldEvent domainEvent, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
_logger.LogInformation("Domain Event Received by {handler} for {make} and {model}.",
nameof(GermanAutoSalesHandler), domainEvent.Make, domainEvent.Model);
}
}
Notice that the second handler method returns a task and accepts a cancelation token in addition the the domain-event. The router provides different overrides based on the types of supported messaging patterns. This includes overrides for synchronous and asynchronous message handlers. This allows a method handler to be changed without having to refactor any of the calling code. Also, the consumer uses the IMessagingService to send messages which always assumes the call is asynchronous.
Define Message Routes
Add the following two lines of code to the InMemoryRouter class defined within the Routers directory of the Examples.Messaging.Infra project:
using System.Linq;
using Examples.Messaging.App.Handlers;
using Examples.Messaging.Domain.Events;
using NetFusion.Messaging.InProcess;
namespace Examples.Messaging.Infra.Routers;
public class InMemoryRouter : MessageRouter
{
protected override void OnConfigureRoutes()
{
OnDomainEvent<AutoSoldEvent>(route =>
route.ToConsumer<GermanAutoSalesHandler>(c => c.OnRegistration, meta =>
meta.When(de => new[]
{
"BMW",
"Audi",
"Mercedes"
}.Contains(de.Make))
)
);
OnDomainEvent<AutoSoldEvent>(route =>
route.ToConsumer<AmericanAutoSalesHandler>(c => c.OnRegistration, meta =>
meta.When(de => new[]
{
"Ford",
"Jeep",
"GMC"
}.Contains(de.Make))
)
);
}
}
The above completes the following:
- Routes the AutoSoldEvent to the OnRegistration handler of the GermanRegistrationHandler when the make of the published domain-event is BMW, Audi, or Mercedes.
- Routes the AutoSoldEvent to the OnRegistration handler of the AmericanRegistrationHandler when the make of the published domain-event is Ford, Jeep, or GMC.
If all domains events are to be dispatched to a consumer's handler method, then the above code would be:
OnDomainEvent<AutoSoldEvent>(route =>
route.ToConsumer<AmericanAutoSalesHandler>(c => c.OnRegistration)
);
Define Api Model
To test the above domain-event, define the following model used to post data to the controller:
using System.ComponentModel.DataAnnotations;
namespace Examples.Messaging.WebApi.Models;
public class AutoSalesModel
{
[Required] public string Make { get; set; } = string.Empty;
[Required] public string Model { get; set; } = string.Empty;
public int Year { get; set; }
}
Define Api Controller
Lastly, define the following controller method to create the domain-event from the posted data and publish it using the injected IMessagingService.
using Examples.Messaging.Domain.Events;
using Examples.Messaging.WebApi.Models;
using Microsoft.AspNetCore.Mvc;
using NetFusion.Messaging;
namespace Examples.Messaging.WebApi.Controllers;
[ApiController, Route("api/messaging")]
public class MessageController : ControllerBase
{
private readonly IMessagingService _messaging;
public MessageController(IMessagingService messaging)
{
_messaging = messaging;
}
[HttpPost("auto/sales")]
public async Task<IActionResult> AutoSalesCompleted([FromBody]AutoSalesModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var domainEvt = new AutoSoldEvent(
model.Make,
model.Model,
model.Year);
await _messaging.PublishAsync(domainEvt);
return Ok();
}
}
Domain-Events are published by any component by injecting the IMessagingService and calling the PublishAsync method.
Execution Example
The above endpoint will be called with different make automobiles so each of the routings can be tested. Start the microservice at the command line or within the IDE:
cd ./src/Examples.Messaging.WebApi
dotnet run
Post the following messages to http://localhost:5670/api/messaging/auto/sales and view the logs written to the console:
{
"make": "BMW",
"model": "325es",
"year": 1996
}
{
"make": "Honda",
"model": "HR-V",
"year": 2023
}
{
"make": "Jeep",
"model": "Compas",
"year": 2022
}