Recently, in a project, a performance issue came up regarding the export of an Excel file. The scenario involved an endpoint that had to query the database and return a large volume of data to be subsequently exported.
The same query, however, in another endpoint, returned the data for the front-end to display to the user, but the performance issue was resolved through pagination. However, the file generation scenario was not well executed, because the requirement was to export everything available based on the applied filter.
Well, after conducting some tests, I realised that the user would have to wait a long time for the entire process to finish. This would not be ideal, as the user might leave the page or even give up on the report. That's when I started thinking about solutions to solve this problem.
Another very common issue in various projects is having a limited budget. So I started thinking about using a queue: this way, the user would make the request and wouldn't be stuck on the page waiting.
Currently, we have many options for queue services, such as RabbitMQ, Kafka, among others, but they are powerful tools that sometimes require complex implementations, which would not be feasible for this scenario.
Then I remembered that .NET natively provides a service called "BackgroundService," which implements the "IHostedService" interface, and this opens up a series of advantages. I don't need to worry about external servers, complex implementations, and best of all: it all runs on the same app service as my API, independently.
As always, we’ll provide a real example!
- Let's create a sample project. To do so, select the "Create a new project" option..
- Name the solution.
- Select the .NET version.
- We'll create the following logical folders:
- Install the Bogus package: "Install-Package bogus".
- Let's create the Customer and Invoice classes. In the Customer class, these are the properties:
- Create the Invoice class.
Note that the properties are read-only. This is a good practice, so to create an Invoice we'll need a constructor like this:
- The Customer class follows the same model, because its setters are private. Therefore, we'll implement a constructor and also the method that will return a list of Customers.
Notice that a Customer is created by instantiating the class and communicating to the constructor the name and whether they are active. The name is represented by text that increments a number with each iteration. Being active depends on whether the iteration number (i) is even or odd.
Invoices work the same way
Note that I'm adding 5 invoices for each created Customer. To do that, we'll create the AddInvoice method in the Customer class.
- Now let's create the interface responsible for enqueuing or dequeuing our objects. The interface will be named "IBackgroundQueue".
- We'll then create the concrete class that will implement this interface.
- Create another interface. We'll call it "ICustomerPublisher." This interface should have a publishing method.
- Next, simply implement the interface in a class named "CustomerPublisher" within the "Services" folder.
- A large part of the work is already done. So far, we have:
- IBackgroundQueue: an interface for including and excluding from the queue;
- ICustomerPublisher: an interface that handles publishing to the queue;
- BackgroundQueue and CustomerPublisher: concrete implementations of the interfaces;
- Models Customer and Invoice.
- The next step is to create our worker, which will be responsible for executing the queue process. This process runs in parallel to the API, so it's a service hosted in the same application, without actually interfering with the API.
We'll create a class called "CustomerBackgroundWorker," which should inherit the abstract class "BackgroundService”. The latter is responsible for providing the functionalities that we need to implement in the class – an example is the "ExecuteAsync" method, which is a required override in our class
It's important to note that we need to inject our queue service. I also include here the ILogger, so that we can see the result in the console.
The "IServiceScopeFactory" interface is very important - it allows us to use the publishing service. Normally, we would use the common dependency injection process when the application starts up. However, the construction of the instance is different when we use Hosted Services. There is a conflict in creating the scope, so the correct way is to do this within this service using the "IServiceScopeFactory" interface.
- Now we implement the method that puts our object in the queue or removes it through publishing.
- Don't forget to add our functionalities to the service scope within the "Program.cs" class.
- Finally, we'll create the "CustomersController" controller. Let's trigger a situation of customers queuing.
- Now, to test it, we'll click multiple times via Swagger to create the requests and observe the logs in the console. We can conclude that we are asynchronously sending the customers that will be processed later, and the consumer, who makes the request, will not be locked waiting for a response from the API.
In my case, I had already clicked more than 10 times, I already had the API response, and the publishing process was still ongoing, as shown in the image.
This is a great native feature that can greatly help us when we want to use a queue without complexity and for specific purposes. A great example would be to create an e-mail sending service for a specific business rule, such as sending payment processing e-mails to customers.