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.
This way you can build:
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:
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.
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.
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>();
}
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);
}
}
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
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
.
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"
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"
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 clusterMyShopMonolith
is run.
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
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.