Documentation

Cross-module communication

Nomirun supports out-of-the-box cross-module communication that you can develop using Nomirun SDK. You can create senders and receivers in your modules, and they can use different transports automatically.

That means that the transport is selected based on the location where the module is run.

  • If two modules should communicate between each other, and they run on the same host, they will use in-memory communication.
  • If two modules run in separate hosts, they will use network communication. In Nomirun’s case, this is GRCP over HTTP/2 protocol.

This way you can build:

  1. True modular monoliths that have in-memory communication between modules (chapter 2 below).
  2. Modules run as microservices that communicate through GRPC (chapter 3 below).
  3. Or mix of both, as we will see in the example below (chapter 4 below).

1. Create three modules and communication code

Let’s create three modules that we will use in the proof of concept:

  • UserModule
  • ProductModule
  • ShoppingModule

When user buys a product, ShoppingModule fetches user address from the UserModule and fetches product detail from ProductModule. When new product is added to the system, ProductModule generates a notification is sentto the UserModule.

This is how they are connected in a diagram:

graph TD ShoppingModule -- Fetches user address --> UserModule ShoppingModule -- Fetches product details --> ProductModule ProductModule -- Generates notification about new products --> UserModule

Let’s create all three modules by running 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"

Now, let’s add some code to each of the modules.

1.1. Modify CommonLib

Request object is shared between the senders and receivers, so we need to store them in a shared library or NuGet package, in our case a Nomirun module called CommonLib.

First remove all the code from the CommonLib as we will not need anything else there but Request classes.

Then, create all required Request objects.

GetUserAddressRequest needs to implement IRequest<T> or in case below IRequest<UserAddress>.

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

GetProductDetailsRrequest needs to implement IRequest<T> or in case below IRequest<string>.

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

It’s a good practice to end request objects with Request suffix.

Finally, let’s add a notification, which implements INotify and sends a message to all users in UserModule.

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

Now, let’s build the CommonLib by:

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

Now we reference Nuget CommonLib in other three modules.

1.2. Modify UserModule

Let’s add UserAddressHandler handler to fetch user address by UserId. UserAddressHandler implements IRequestHandler<GetUserAddressRequest, string> that receives a GetUserAddressRequest request and returns an address as string.

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

Then we also need to add a UserNotificationHandler handler to send a notification. UserNotificationHandler implements INotificationHandler<NewProductNotification> that sends the notifications to all users.

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

Now let’s register the handlers in the Startup.cs:

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

In the ShoppingModule we need to add 2 controllers that will send down the request to other 2 modules:

  • ProductDetailsController will get product details by sending the GetProductDetailsRequest to the ProductModule.
  • UserInfoController will get the user address by sending the GetUserAddressRequest to the UserModule.
[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. Modify ProductModule

Add ProductDetailsHandler handler to fetch product details by ProductId. ProductDetailsHandler implements IRequestHandler<GetProductDetailsRequest, string> that receives a GetProductDetailsRequest request and returns product details as string.

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

Now let’s register the handlers in the Startup.cs:

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

Now we also need to fetch Users information from the UserModule. We’ll reuse the API controller for that:

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

We inject IMediate interface to the controller and call Notify method to send the notification object NewProductNotification

2. Run modules as three microservices in development mode

Now we can run all three services in development mode. Development mode means that all the modules are not packed to NuGet packages, but source code must be available on your machine.

Since all modules are running on its own hosts, all communication goes through GRPC transport.

We can go to the ShoppingModule /swagger endpoint and execute /api/product/{productId} and /api/users/{userId}/address. We get back the results from the other two modules through GRPC as expected.

We can go to ProductModule /swagger endpoint and execute /api/products to simulate creating new module. A notification NewProductNotification is sent to the UserModule.

3. Run modules as modular monolith in development mode

Now let’s pack all modules to a single Nomirun host and run them as a modular monolith. Without any modifications to the code, modules can now communicate in-memory.

Let’s pack them together in a modular monolith. First we create a cluster:

nomirun cluster new --cluster-name "MyShopMonolith"

Then we can add a MyShopApi host with all three modules:

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

Once modules are added to the host, we can start the host like this:

nomirun cluster start --cluster-name "MyShopMonolith" --dev

This will run a single host as a modular monolith. All three modules will communicate with Memory transport.

To stop the cluster, do:

nomirun cluster stop --cluster-name "MyShopMonolith"

4. Run two modules as modular monolith and 1 as microservice in development mode

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

Create new cluster and two hosts:

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"

Once modules are added to the hosts, we can start the cluster like this:

nomirun cluster start --cluster-name "MyShopMicroservices" --dev

That will run two hosts with selected modules. ShoppingModule will now communicate with UserModule with Memory transport, while ShoppingModule and UserModule will communicate through GRPC communication with ProductModule.

To stop the cluster, do:

nomirun cluster stop --cluster-name "MyShopMicroservices"

5. Run modules in production mode

Production mode is when the Nomirun hosts are running NuGet packages that are taken from some NuGet repository.

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. Run a cluster with Nomirun Hosts on containers

To continue from the previous chapter, we can run the cluster with containers. To do that, we need to do one extra step, and that is to generate configuration for all Nomirun hosts running as containers.

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

7. Conclusion

Cross-module communication gives you a lot of flexibility on how to run, deploy and scale your Nomirun modules.

It’s all based on the configuration approach and how you bundle your modules together.