Recentemente, em um projeto, surgiu uma questão de performance para a exportação de um arquivo Excel: o cenário era o de um endpoint que tinha de fazer uma pesquisa na base de dados e retornar um grande volume de dados, para que posteriormente fosse exportado.
Esta mesma query, porém em um outro endpoint, retornava os dados para que o front-end pudesse mostrá-los ao utilizador, mas a questão da performance foi resolvida com paginação. Porém, o cenário de geração do arquivo não foi bem executado, pois o requisito era exportar tudo o que estava disponível, consoante o filtro aplicado.
Bem, então fazendo alguns testes percebi que o utilizador ficaria muito tempo aguardando todo o processo. Isso não seria bom, além de que ele poderia sair da página, ou até mesmo desistir do relatório. Foi aí que comecei a pensar em soluções para resolver este problema.
Outra questão muito comum em diversos projetos é ter um budget limitado. Logo comecei a pensar em utilizar uma fila: desta forma, o utilizador faria o pedido e não ficaria trancado na página aguardando.
Atualmente temos muitas opções de serviços para filas, como RabbitMQ, Kafka, entre outros, mas são ferramentas muito poderosas, que exigem implementações por vezes complexas, e para este cenário não seria viável.
Então lembrei que o .NET possui de forma nativa um serviço chamado “BackgroundService”, que implementa a interface “IHostedService”, e isso abre uma série de vantagens. Não preciso me preocupar com servidores externos, implementações complexas, e o melhor de tudo é que isso tudo corre no mesmo app service da minha API, de forma independente.
Vamos praticar...
Como sempre, faremos um exemplo real!
- Vamos criar um projeto de exemplo. Para tal, selecione a opção “Create a new project”.
- Dê um nome para a solução.
- Selecione a versão do .NET.
- Criaremos as seguintes pastas lógicas:
- Instale o pacote Bogus: "Install-Package bogus".
- Vamos criar a classe Customer e Invoice. Em Customer, estas são as propriedades:
- Crie a classe Invoice.
Note que as propriedades são somente de leitura. Esta é uma boa prática, portanto para conseguir criar uma Invoice vamos precisar de um construtor, como este: - A classe Customer segue o mesmo modelo, pois seus setters são privados. Sendo assim vamos implementar um construtor e também o método que irá nos retornar uma listagem de Customers.
Perceba que Customer é criado instanciando a classe e passando para o construtor o nome e se está ativo. Note que o nome é representado por um texto que a cada iteração incrementa um número. Estar ativo vai depender se o número da iteração (i) é par ou ímpar.
As Invoices funcionam da mesma forma.
Perceba que eu estou adicionando 5 invoices para cada Customer criado. Para isso, criaremos o método AddInvoice na classe Customer. - Vamos criar agora a interface responsável por enfileirar ou remover da fila nossos objetos. O nome da interface será “IBackgroundQueue”.
- Criaremos então a classe concreta que implementará esta interface.
- Crie uma outra interface. Chamaremos ela de “ICustomerPublisher”. Esta interface deve possuir um método de publicação.
- Na sequência basta implementar a interface em uma classe chamada “CustomerPublisher”, dentro da pasta “Services”.
- Grande parte do trabalho já está feito. Temos então:
- IBackgroundQueue: interface para incluir e excluir da fila;
- ICustomerPublisher: interface que trata da publicação na fila;
- BackgroundQueue e CustomerPublisher: as implementações concretas das interfaces;
- Models Customer e Invoice. - Próximo passo é criar nosso worker, que vai ser o responsável por executar o processo da fila. Este processo ocorre em paralelo à API, sendo assim um serviço hospedado na mesma aplicação, não interferindo de fato na API.
Criaremos uma classe chamada “CustomerBackgroundWorker”, que por sua vez deve herdar a classe abstrata “BackgroundService”. Esta última é a responsável por fornecer as funcionalidades que vamos precisar implementar na classe – exemplo é o método “ExecuteAsync”, que é uma sobrecarga obrigatória na nossa classe.
Importante salientar que precisamos injetar nosso serviço de fila. Também incluo aqui o ILogger, para que possamos visualizar o resultado na consola.
A interface “IServiceScopeFactory” é muito importante – é ela que vai nos permitir utilizar o serviço de publicação. Normalmente iríamos utilizar o processo comum de injeção de dependência, quando a aplicação sobe, porém a construção da instância, quando utilizamos Hosted Services, é diferente. Há conflito na criação de escopo, portando a forma correta é fazendo isso dentro deste serviço com a interface “IServiceScopeFactory”. - Agora implementamos o método que colocar nosso objeto na fila, ou tirar dela, por meio da publicação.
- Não esqueça de adicionar nossas funcionalidades ao escopo de serviços dentro da classe “Program.cs”.
- Finalmente, agora criaremos a Controller “CustomersController”. Vamos provocar um enfileiramento de clientes.
- Agora, para testar, iremos via swagger clicar inúmeras vezes para criar as requests e observar os logs na consola. Podemos concluir que enviamos de forma assíncrona os clientes que posteriormente serão processados, e o consumer, que é quem faz a requisição, não ficará trancado esperando resposta da API.
No meu caso, eu já havia clicado mais de 10 vezes, já tinha o retorno da API e o processo de publicação ainda ocorria, conforme imagem.
Conclusão
Este é um ótimo recurso nativo e que pode nos ajudar imensamente quando queremos utilizar fila sem complexidade e para temas específicos. Um ótimo exemplo seria criar um serviço de envio de e-mails para uma determinada regra de negócio, por exemplo e-mail de processamento de pagamentos aos clientes.
Links úteis:
- GitHub: @leosul BackgroundServiceQueueExample;
- LinkedIn: Background Service Queue.