top of page

Distributed .NET application with Masstransit and RabbitMQ

Since it supports docker it should be able to run dotnet even though I can’t install dotnet directly on the NAS. Given the context I will try to go for free and lightweight software that should mirror what I’m used to working with.


I had to reconsider some of my usual choices. The stack I am most familiar with has a SQL Server database that we are using as a broker for NServiceBus with the website hosted in IIS. All of those aren’t new, free and/or open-source, so I get the chance to play around with an entirely new stack.


I’ve opted for a MySQL database, since my NAS has that pre-installed and use RabbitMQ as a broker for my services. For background services, I’m opting for MassTransit, as it seems like an obvious alternative to NServiceBus. Our hosting is going to be in docker using one of the dotnet containers available on DockerHub.

RabbitMQ is actually available as a docker image from DockerHub as well. It was very easy to setup. Just select the image in Container Station and you have a RabbitMQ instance running.


The application I’m going to try and set up is really just a ‘Hello world’ for this stack. I’m going to setup a dotnet Web API that will publish a message to a MassTransit service over RabbitMQ. That will generate an excel file that we can then download afterwards, once the service completes.


We first need to set-up our solution and create the projects we’ll need. Assuming we have dotnet installed, we just open a folder in powershell and get started.

dotnet new sln
dotnet new console –-name Service
dotnet sln add Service
dotnet new web --name Web
dotnet sln add Web
dotnet new classlib --name Message
dotnet sln add Message
dotnet new classlib --name Model
dotnet sln add Model

We’ll start by creating a model. We’ll just create a simple “Request” class with an Id and a File property.

public class Request
{
  public int Id { get; set; }
  public byte[] File { get; set; }
}

Next we need to set up the database integration with MySQL using Entity Framework. We add the dependencies we need to our Model project.

dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package MySql.EntityFrameworkCore --version 5.0.0-m8.0.23

I’m using a preview version of MySql.EntityFrameworkCore here, because at the time of writing the .NET5 version was still in beta. We can now setup our mapping and our DbContext, assuming we have the MySQL database already setup.

public class RequestContext : DbContext
{
    public DbSet<Request> Requests { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder 
    optionsBuilder)   
    { 
        optionsBuilder.UseMySQL("server=*;database=DB;user=*;password=*");   
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)   
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<Request>(entity=>       
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.File).HasColumnType("blob");       
        });   
    } 
}

We can use this DbContext within both our Web- and Service project. So we’ll set it up for dependency injection using an extension method on IServiceCollection.

public static class ServiceExtentions
{
  public static void RegisterModelServices(this IServiceCollection
  serviceCollection)
  {
    serviceCollection.AddDbContext<RequestContext>();
  }
}

Now we can let Entity Framework generate our migration scripts and database automatically. We will need to install the ef tool to be able to do this.

dotnet tool install --global dotnet-ef
dotnet ef migrations add InitialDb
dotnet ef database update

Let’s continue by setting up a simple web api that will queue the generation of our excel file and provide the option to download that file. First we need to add our dependencies. We’ll need EntityFramework, DI and a reference to our Model and Message project.

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Relational
dotnet add reference ..\Message\Message.csproj ..\Model\Model.csproj

We can then setup API support and add our DbContext to the container of the Web application by updating the Startup class.

public void ConfigureServices(IServiceCollection services)
{
 services.RegisterModelServices();
 services.AddControllers();
}public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
 **other stuff
 app.UseEndpoints(endpoints =>
 {
   endpoints.MapControllers();
 });
}

We can now create our controller that will simply create a request when generating and returns an id. We also add a download endpoint that will return a file, based on that id. Because we wired up the DbContext in the RegisterModelServices, we can simply inject it in the controller.

public class RequestController : ControllerBase
{
    private readonly RequestContext ctx;
    public RequestController(RequestContext ctx)  
    {
        this.ctx = ctx;  
    }  
    
    [HttpGet, Route("generate")]
    public async Task<int> Generate()  
    {
        var request = new Request();
        ctx.Requests.Add(request);
        ctx.SaveChanges();
        return request.Id;  
    }  
    
    [HttpGet, Route("download/{id}")]
    public IActionResult Download(int id)  
    {
        var file = ctx.Requests.Find(id).File;
        var ms = new MemoryStream(file);
        return new FileStreamResult(ms, "application/vnd.openxmlformats-
        officedocument.spreadsheetml.sheet")     
        {
            FileDownloadName="File.xlsx"     
        };  
    }
}


We are still missing the code that will send our request to MassTransit. To do that, let’s first create our message DTO that we’ll add to our Message project.

namespace Message
{
  public class GenerateMessage
  {
    public int Id { get; set; }
  }
}

Next we can add MassTransit support to our Web project and then wire up MassTransit support for AspNetCore in the ConfigureServices method

dotnet add package MassTransit.AspNetCore
dotnet add package MassTransit.Extensions.DependencyInjection
dotnet add package MassTransit.RabbitMQservices.AddMassTransit(x =>
{
  x.UsingRabbitMq((r,c) =>
 {
   c.Host("rabbitmq--url");
 });
});
services.AddMassTransitHostedService();

This will expose the IBus interface of MassTransit so we can use it in our controller. In the generate method we can publish our GenerateRequest message passing in the id of the request record we just saved to the database.

private readonly RequestContext ctx;
private readonly IBus bus;public RequestController(RequestContext ctx, IBus bus)
{
  this.ctx = ctx;
  this.bus = bus;
}//Generate method
await bus.Publish(new GenerateMessage() { Id = request.Id });

Now our web application is done. We can now set-up the consumer of our message on the service side. For our background service host, we’ll use the generic host for dotnet. This will allow us to easily change how we want to deploy it in the future and facilitates dependency injection.


Let’s start again by adding all the dependencies we’ll need for our background service.

dotnet add package MassTransit
dotnet add package MassTransit.Extensions.DependencyInjection
dotnet add package MassTransit.RabbitMQ
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Relational
dotnet add package SpreadsheetLight
dotnet add reference ..\Message\Message.csproj ..\Model\Model.csproj

Creating a consumer for our message is really simple. You just need to implement IConsumer<T> for you message. We’ll simply add our DbContext in the constructor so we can save the generated file.

I am using SpreadsheetLight here, it’s a simple library to manage excel files. It’ll do for this project.

The generic host exposes provides a IHostedService interface we can easily implement to manage our MassTransit service. IBusControl will allow us to start and stop our bus using the generic host.

public class RequestConsumer : IConsumer<GenerateMessage>
{
    privater ead onlyRequestContext ctx;
    public RequestConsumer(RequestContext ctx)  
    {
        this.ctx = ctx;  
    }
    public Task Consume
                (ConsumeContext<GenerateMessage> context)  
    {
        var msg = ctx.Requests.Find(context.Message.Id);
        var memoryStream = new MemoryStream();
        using (var document = new SLDocument())    
        {
            document.SetCellValue("A1", "Hello world");
            document.SaveAs(memoryStream);    
        }
        memoryStream.Position = 0;
        msg.File = memoryStream.ToArray();
        ctx.SaveChanges();
        return Task.CompletedTask;  
    }
}
        
public class MassTransitService : IHostedService
{
    private IBusControl _busControl;
    public MassTransitService(IBusControl busControl)   
    {
        _busControl=busControl;   
    }
    public async Task StartAsync
                    (CancellationToken cancellationToken)   
    {
        var source = new CancellationTokenSource
                              (TimeSpan.FromSeconds(10));
        await _busControl.StartAsync(source.Token);   
    }
        
    public async Task StopAsync
                    (CancellationToken cancellationToken)   
    { 
        await _busControl.StopAsync();
    }
}


Both the HostedService and the BusControl aren’t wired up yet, we can do this by changing our Program.cs code. We can call ConfigureServices and setup our own DI and setup MassTransit. We’ll add our consumer here, and setup the receiving endpoint with the name ‘Service’. Finally we add our MassTransitService as a hosted service using AddHostedService.

class Program
{
    public static void Main(string[] args)   
    {
        CreateHostBuilder(args).Build().Run();   
    }
    
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args).ConfigureServices((hostContext,         
        services) =>   
    {
        services.RegisterModelServices();
        services.AddMassTransit(x=> 
        {
        x.AddConsumers(typeof(RequestConsumer).Assembly);
        x.UsingRabbitMq((context, cfg) =>        
        {
            cfg.Host("rabbitmq--url");
            cfg.ReceiveEndpoint("Service", e=>          
            {
                e.ConfigureConsumer<RequestConsumer>(context);           
            });        
       });      
   });
   services.AddHostedService<MassTransitService>();  
});
}

Now our background service is done. We can now test it end-to-end by executing ‘dotnet run’ in both project folders. The web project will host a website locally at port 5000.


When executing a request to ‘api/request/generate’ we’ll get an id (ex. 1) and when executing ‘api/request/download/1’ we’ll download our excel file containing “Hello world” in cell A1.



Source: Medium - Maarten De Wilde


The Tech Platform

0 comments

Comments


bottom of page