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.
This allows you to:
Table of Contents
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.
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"
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:
CommonLib
GetUserAddressRequest
and GetProductDetailsRequest
requires IRequest<T>
Request
suffixINotify
, 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.
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>();
}
Implement two API controllers that use IMediate
to send requests:
ProductDetailsController
: Retrieves product detailsUserAddressController
: 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);
}
}
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 IMediate
to the controller and by calling the Notify
method we are able to send the notification object NewProductNotification
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
.
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"
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
.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 clusterMyShopMonolith
is run.
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
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.