Skip to content

messaging commands

Brian Greco edited this page Apr 5, 2023 · 5 revisions

IMAGE Messaging - Commands

The IMessagingService interface is used to send Commands. Commands are used to update the state managed by a microservice. The processing of a command can also result in domain-events being published to notify other components or microservices changes. For in-process command consumers, the command handler can be either synchronous or asynchronous.

Define Command

In this example, the consumer of the command will download data from a remote JSON file providing a more involved example besides writing to the log. Based on the make and model specified within the command, a corresponding response will be returned from the consumer's handler method. Define the command's response type in the following directory: Examples.Messaging.Domain/Entities

using System;

namespace Examples.Messaging.Domain.Entities;

public class RegistrationStatus
{
    public string ReferenceNumber { get; }
    public bool IsSuccess { get; }
    public DateTime DateProcessed { get; }

    public RegistrationStatus(string referenceNumber, bool isSuccess, DateTime dateProcessed)
    {
        ReferenceNumber = referenceNumber;
        IsSuccess = isSuccess;
        DateProcessed = dateProcessed;
    }
}

Then create the corresponding command here: Examples.Messaging.Domain/Commands

using Examples.Messaging.Domain.Entities;
using NetFusion.Messaging.Types;

namespace Examples.Messaging.Domain.Commands;

public class RegisterAutoCommand : Command<RegistrationStatus>
{
    public string Make { get; }
    public string Model { get; }
    public int Year { get; set; }
    public string State { get; set;}

    public RegisterAutoCommand(
        string make,
        string model,
        int year,
        string state)
    {
        Make = make;
        Model = model;
        Year = year;
        State = state;
    }
}

Define Message Consumer

The following defines a stub for the handler of the command that will be completed after the data access logic has been written. Define the hander in the following directory: Examples.Messaging.App/Handlers

using System;
using System.Threading.Tasks;
using Examples.Messaging.Domain.Commands;
using Examples.Messaging.Domain.Entities;

namespace Examples.Messaging.App.Handlers;

public class AutoRegistrationHandler
{
    public Task<RegistrationStatus> RegisterAuto(RegisterAutoCommand command)
    {
        throw new NotImplementedException();
    }
}

Define Message Router

Add an entry to the InMemoryRouter class specifying that when a RegisterAutoCommand with a result of RegistrationStatus is sent it should be routed to the RegisterAuto method of the AutoRegistrationHandler class:

using Examples.Messaging.App.Handlers;
using Examples.Messaging.Domain.Commands;
using Examples.Messaging.Domain.Entities;
using NetFusion.Messaging.InProcess;

namespace Examples.Messaging.Infra.Routers;

public class InMemoryRouter : MessageRouter
{
    protected override void OnConfigureRoutes()
    {
        OnCommand<RegisterAutoCommand, RegistrationStatus>(route =>
            route.ToConsumer<AutoRegistrationHandler>(c => c.RegisterAuto));
    }
}

Now that the command and handler have been defined with a routing, the last step is to create the code that will download the data and call it from the handler.

Define Adapter

This example is more extensive in that the message consumer will not just write a log but will inject an adapter to download data. The first step is to create the adapter and register it for injection into the above AutoRegistrationHandler class.

Create Adapter Interface

Since an adapter transforms an external data structure into one known by the business domain, it returns an entity defined within the Domain project at the following location:

Examples.Messaging.Domain/Entities

namespace Examples.Messaging.Domain.Entities;

public class AutoInfo
{
    public string Make { get; }
    public string Model { get; }
    public int Year { get; }

    public AutoInfo(string make, string model, int year)
    {
        Make = make;
        Model = model;
        Year = year;
    }
}

Adapters integrate an external data source for use by the application's business logic so the interface will be defined in the Application project at the following location:

Examples.Messaging.App/Adapters

using System.Threading.Tasks;
using Examples.Messaging.Domain.Entities;

namespace Examples.Messaging.App.Adapters;

public interface IRegistrationDataAdapter
{
    Task<AutoInfo[]> GetValidModelsAsync(int forYear);
}

Implement Adapter

The adapters implementation can change and is not specific to the microservice, the implementation is placed in the Infrastructure project within the following directory:

Examples.Messaging.Infra/Adapters

using System;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Examples.Messaging.App.Adapters;
using Examples.Messaging.Domain.Entities;
using Microsoft.Extensions.Logging;

namespace Examples.Messaging.Infra.Adapters;

public class RegistrationDataAdapter : IRegistrationDataAdapter
{
    private readonly ILogger _logger;

    public RegistrationDataAdapter(
        ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger("Registration Adapter");
    }

    public async Task<AutoInfo[]> GetValidModelsAsync(int forYear)
    {
        _logger.LogDebug("Attempting to download data...");

        var httpClient = new HttpClient();

        HttpResponseMessage response = await httpClient.GetAsync(
            @"https://raw.githubusercontent.com/grecosoft/NetFusion-Examples/master/Examples/Data/valid_autos.json");

        response.EnsureSuccessStatusCode();
        string responseBody = await response.Content.ReadAsStringAsync();

        var data = JsonSerializer.Deserialize<AutoRegDataResponse>(responseBody, 
            new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });

        var results = data == null ? Array.Empty<AutoInfo>() : 
            data.AutoInfo.Where(a => a.Year == forYear).ToArray();
        
        _logger.LogDebug("Retrieved {numberResults} results.", results.Length);

        return results;
    }

    private class AutoRegDataResponse
    {
        public AutoInfo[] AutoInfo { get; set; } = Array.Empty<AutoInfo>();
    }
}

The data returned from the above web request is as follows:

IMAGE

Register Adapter

Register the adapter within the microservice's dependency-injection container by defining a new module within the following directory:

Examples.Messaging.Infra/Plugin/Modules

using Examples.Messaging.App.Adapters;
using Examples.Messaging.Infra.Adapters;
using Microsoft.Extensions.DependencyInjection;
using NetFusion.Core.Bootstrap.Plugins;

namespace Examples.Messaging.Infra.Plugin.Modules;

public class AdapterModule : PluginModule
{
    public override void RegisterServices(IServiceCollection services)
    {
        services.AddSingleton<IRegistrationDataAdapter, RegistrationDataAdapter>();
    }
}

Since this is a new module, add a reference to the InfraPlugin class located in the following directory:

Examples.Messaging.Infra/Plugin

using NetFusion.Core.Bootstrap.Plugins;
using Examples.Messaging.Infra.Plugin.Modules;

namespace Examples.Messaging.Infra.Plugin;

public class InfraPlugin : PluginBase
{
    public override string PluginId => "6D4DA473-2331-48CE-A49F-328D9F2CF852";
    public override PluginTypes PluginType => PluginTypes.AppPlugin;
    public override string Name => "Infrastructure Application Component";

    public InfraPlugin() {
        AddModule<RepositoryModule>();
        AddModule<AdapterModule>();		// <-- Add this line

        Description = "Plugin component containing the application infrastructure.";
    }
}

Implement Message Consumer

With the adapter implemented, we can now complete the AutoRegistrationHandler class:

using System;
using System.Linq;
using System.Threading.Tasks;
using Examples.Messaging.App.Adapters;
using Examples.Messaging.Domain.Commands;
using Examples.Messaging.Domain.Entities;

namespace Examples.Messaging.App.Handlers;

public class AutoRegistrationHandler
{
    private readonly IRegistrationDataAdapter _adapter;

    public AutoRegistrationHandler(IRegistrationDataAdapter adapter)
    {
        _adapter = adapter;
    }
    
    public async Task<RegistrationStatus> RegisterAuto(RegisterAutoCommand command)
    {
        AutoInfo[] validModels = await _adapter.GetValidModelsAsync(command.Year);
        return new RegistrationStatus(
            Guid.NewGuid().ToString(),
            IsValidMakeAndModel(command, validModels),
            DateTime.UtcNow);
    }
    
    private static bool IsValidMakeAndModel(RegisterAutoCommand command,
        AutoInfo[] validModels)
    {
        return validModels.Any(
            m => m.Make == command.Make &&
                 m.Model == command.Model);
    }
}

Define Api Model

To test the above command, an API method will be added to the controller. Define the following model to receive the posted data:

using System.ComponentModel.DataAnnotations;

namespace Examples.Messaging.WebApi.Models;

public class AutoRegistrationModel
{
    [Required] public string Make { get; set; } = string.Empty;
    [Required] public string Model { get; set; } = string.Empty;
    [Required] public string State { get; set; } = string.Empty;
    public int Year { get; set; } 
}

Define Api Controller

Lastly, add a method to the controller to create an instance of the RegisterAutoCommand class from the received model's data. After the command is created, it is then dispatched by calling the SendAsync method defined on IMessagingService. The received response is then returned as the result.

using Examples.Messaging.Domain.Commands;
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/registration")]
    public async Task<IActionResult> SubmitAutoRegistration([FromBody] AutoRegistrationModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var command = new RegisterAutoCommand(model.Make, model.Model, model.Year, model.State);
        var result = await _messaging.SendAsync(command);
        return Ok(result);
    }
}

Execute Example

The following will post two requests. The first request will contain valid data found in the downloaded JSON document and the second request will be invalid. Run the microservice and post the following requests.

cd ./src/Examples.Messaging.WebApi
dotnet run
{
    "make": "VW",
    "model": "Alltrack",
    "year": 2017,
    "state": "NC"
}

IMAGE

{
    "make": "Subaru",
    "model": "Forester",
    "year": 2018,
    "state": "CT"
}

IMAGE

Clone this wiki locally