Documentation

Cross-Module Communication

The Nomirun SDK enables seamless cross-module communication with minimal setup. Modules can send and receive messages using either in-memory or network-based transports, selected automatically depending on the execution context.

  • When modules run on the same Nomirun Host, communication is handled in-memory.
  • When modules are deployed across different hosts, Nomirun uses gRPC over HTTP/2 for inter-module communication. By default cross-module gRPC communication has automatic authentication built in.

This allows you to:

  1. Build modular monoliths with efficient in-memory messaging.
  2. Deploy independent microservices with network-based communication.
  3. Mix both approaches within a single architecture.

Table of Contents


1. Create Modules and Define Communication Contracts

We’ll create a simple proof-of-concept where we want to integrate three modules together for a basic webshop. The modules we’ll be using are:

  • UserModule
  • ProductModule
  • ShoppingModule

The communication flow is as follows:

  • ShoppingModule retrieves user address data from UserModule and ProductModule.
  • ProductModule sends a notification to UserModule when a new product is added.


graph TD ShoppingModule -- Fetches user address --> UserModule ShoppingModule -- Fetches product details --> ProductModule ProductModule -- Sends new product notification --> UserModule


Create the modules using Nomirun CLI:

nomirun module new --module-name CommonLib --net-version net9.0 --type WebApi --output "c:\MyServices"
nomirun module new --module-name UserModule --net-version net9.0 --type WebApi --output "c:\MyServices"
nomirun module new --module-name ShoppingModule --net-version net9.0 --type WebApi --output "c:\MyServices"
nomirun module new --module-name ProductModule --net-version net9.0 --type WebApi --output "c:\MyServices"


1.1 CommonLib

Request objects are shared between senders and receivers, so we need to store them in a shared library or NuGet package. With Nomirun, these objects are stored in CommonLib.

Here’s what you need to do:

  • Remove all the code from the CommonLib
  • Create all required Request objects: GetUserAddressRequest and GetProductDetailsRequest requires IRequest<T>
  • Make sure to end the requests with Request suffix
  • Add a notification, which implements INotify, to notify users about new products.

public class UserAddress
{
    public string Address { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}
    
public class GetUserAddressRequest: IRequest<UserAddress>
{
    public long UserId { get; set; }
}

public class GetProductDetailsRequest: IRequest<string>
{
    public long ProductId { get; set; }
}

public class NewProductNotification: INotify
{
    public string Message { get; set; }
}

Build the module:

nomirun module build --module-name CommonLib --version 1.0.0 --nuget-server-name "Local Folder"

Reference CommonLib in all other modules.


1.2 UserModule

Register the followinghandlers for processing requests and notifications:

  • UserAddressHandler: returns user address.
  • UserNotificationHandler: sends a notification to all users.
public class UserAddressHandler : IRequestHandler<GetUserAddressRequest, UserAddress>
{
    public Task<UserAddress> Handle(GetUserAddressRequest request, CancellationToken cancellationToken)
    {
        return Task.FromResult(new UserAddress
        {
            Address = "Hardenbergplatz 8",
            City = "10787 Berlin",
            Country = "Germany"
        });
    }
}

public class UserNotificationHandler : INotificationHandler<NewProductNotification>
{
    public Task Handle(NewProductNotification notification, CancellationToken cancellationToken)
    {
        return Task.FromResult("Message Sent to all users");
    }
}

Register handlers in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IRequestHandler<GetUserAddressRequest, UserAddress>, UserAddressHandler>();
    services.AddTransient<INotificationHandler<NewProductNotification>, UserNotificationHandler>();
}



1.3 ShoppingModule

Implement two API controllers that use IMediate to send requests:

  • ProductDetailsController: Retrieves product details
  • UserAddressController: Retrieves user addresses
[Route("api")]
public class ProductDetailsController : NomirunApiController
{
    private readonly ILogger<ProductDetailsController> _logger;
    private readonly IMediate _mediate;

    public ProductDetailsController(ILogger<ProductDetailsController> logger, IMediate mediate) : base(logger)
    {
        _logger = logger;
        _mediate = mediate;
    }

    [HttpGet("product/{productId:long}")]
    public async Task<IActionResult> GetUserAddress(long productId)
    {
        var request = new GetProductDetailsRequest()
        {
            ProductId = productId
        };

        var users = await _mediate.Send<GetProductDetailsRequest, string>(request);
        return Ok(users);
    }
}
[Route("api")]
public class UserAddressController : NomirunApiController
{
    private readonly ILogger<UserAddressController> _logger;
    private readonly IMediate _mediate;

    public UserAddressController(ILogger<UserAddressController> logger, IMediate mediate) : base(logger)
    {
        _logger = logger;
        _mediate = mediate;

    }
    [HttpGet("users/{userId:long}/address")]
    public async Task<IActionResult> GetUserAddress(long userId)
    {
        var request = new GetUserAddressRequest()
        {
            UserId = userId
        };

        var users = await _mediate.Send<GetUserAddressRequest, UserAddress>(request);
        return Ok(users);
    }
}


1.4 ProductModule

Add a handler to fetch product details and a controller that receives the request:

public class ProductDetailsHandler : IRequestHandler<GetProductDetailsRequest, string>
{
    public Task<string> Handle(GetProductDetailsRequest request, CancellationToken cancellationToken)
    {
        return Task.FromResult("Inflatable boat XTR 1000");
    }
}


Register the handler in Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IRequestHandler<GetProductDetailsRequest, string>, ProductDetailsHandler>();
}

Create an API controller to fetch User information.

[Route("api")]
public class NewProductController : NomirunApiController
{
    private readonly ILogger<NewProductController> _logger;
    private readonly IMediate _mediate;

    public NewProductController(ILogger<NewProductController> logger, IMediate mediate) : base(logger)
    {
        _logger = logger;
        _mediate = mediate;
    }

    [HttpPost("products")]
    public async Task<IActionResult> CreateNewProduct()
    {
        //Create new product
        //...
        //Send Notification
        await _mediate.Notify(new NewProductNotification
        {
            Message = "New product created."
        });

        return Ok();
    }
}

By injecting IMediateto the controller and by calling the Notify method we are able to send the notification object NewProductNotification

2. Running as Microservices (Development Mode)

Everything is ready to run. Start by running in development mode, where all modules aren’t yet packed to NuGet packages and the the source code is available on your machine.

Since all the modules are running in their own hosts, all communication goes through the GRPC transport.

By going to /swagger, we can try various endpoints such as /api/product/{productId}, /api/users/{userId}/address. Visiting /api/products will trigger a notification NewProductNotification to UserModule.

3. Running as Modular Monolith (Development Mode)

By running all modules in a single Nomirun Host using in-memory communication we can achieve in-memory communication without any code modifications.

# Add a new Cluster
nomirun cluster new --cluster-name "MyShopMonolith" 

# Add new modules
nomirun cluster add-host --cluster-name "MyShopMonolith" --host-name "MyShopApi" --modules "ShoppingModule;UserModule;ProductModule" 

# Start the Cluster, All modules will communicate with Memory Transport
nomirun cluster start --cluster-name "MyShopMonolith" --dev 

# Optionally, stop the Cluster
nomirun cluster stop --cluster-name "MyShopMonolith" 


4. Hybrid: Two Modules Together, One as Microservice

To run ShoppingModule and UserModule together in the same host and ProductModule in a separate host, we need to:

Create new cluster and two hosts:

# Add a new Cluster
nomirun cluster new --cluster-name "MyShopMicroservices"

# Add modules to the respective hosts:
nomirun cluster add-host --cluster-name "MyShopMicroservices" --host-name "ServicesOne" --modules "ShoppingModule;UserModule"
nomirun cluster add-host --cluster-name "MyShopMicroservices" --host-name "ServicesTwo" --modules "ProductModule"

# Start the Cluster
nomirun cluster start --cluster-name "MyShopMicroservices" --dev

This two-host setup consist of

  • ShoppingModule communicating via Memory with UserModule
  • ShoppingModule and UserModule will communicate with ProductModule via GRPC.


5. Running in Production Mode

The difference between Production and Development is that hosts include NuGet packages instead of local source code.

To modify the host from chapter 3 above, we need to build all the modules, and then we can assign them like this:

nomirun module build --module-name ShoppingModule --version 1.0.0 --nuget-server-name "Feedz.io"
nomirun module build --module-name UserModule --version 1.0.0 --nuget-server-name "Feedz.io"
nomirun module build --module-name ProductModule --version 1.0.0 --nuget-server-name "Feedz.io"
nomirun cluster change-host --cluster-name "MyShopMonolith" --host-name "MyShopApi" --modules "ShoppingModule@1.0.0;UserModule@1.0.0;ProductModule@1.0.0"
nomirun cluster configure --cluster-name "MyShopMonolith" --nuget-server-name "Feedz.io" --target-format DockerEnvFile


Now we can run the cluster with (notice we use cluster start without --dev switch):

nomirun cluster start --cluster-name "MyShopMonolith"

You need to define --nuget-server-name as built NuGet packages will be downloaded from there when cluster MyShopMonolith is run.


6. Running in Containers

If you want to deploy the Cluster into containers, you need to generate configuration for all Nomirunhosts.

nomirun cluster configure --cluster-name "MyShopMonolith" --nuget-server-name "Feedz.io" --target-format DockerEnvFile
nomirun cluster start --cluster-name "MyShopMonolith" --type Container


7. Summary

Nomirun’s cross-module communication system enables you to choose between in-memory and gRPC messaging, depending on how your modules are deployed. This flexibility makes it easy to develop locally, scale in production, and switch between architectures using only configuration changes.