.NET Core + Serverless: utilizando injeção de dependências com Azure Functions
O ASP.NET Core conta desde os seus primórdios com um mecanismo nativo para injeção de dependências, com a infraestrutura da plataforma se valendo do mesmo na manipulação de diferentes recursos básicos (itens de configuração, cache, ORMs).
A configuração deste tipo de recurso passa por ajustes no método ConfigureServices da classe Startup, com a possibilidade de se determinar a injeção de dependências em 3 modos:
- Singleton: uma única instância será utilizada na resolução de dependências encontradas durante todo o ciclo de vida da aplicação (trata-se de um exemplo prático de utilização do pattern Singleton);
- Transient: uma nova instância será gerada sempre que se encontrar uma dependência baseada em um tipo específico;
- Scoped: fará com que uma única instância de um tipo seja gerada e, diferentemente do modo anterior, resolvendo as diferentes dependências encontradas durante o processamento de uma solicitação HTTP. Requisições HTTP subsequentes resultarão na criação de novos objetos.
Já abordei inclusive as diferentes possibilidades de injeção de dependências com ASP.NET Core no seguinte exemplo:
ASP.NET Core 3.1 + REST API + Injeção de Dependências
Por default, uma aplicação baseada em Azure Functions e .NET Core ao ser gerada não dispõe de uma implementação da classe Startup. Seria possível então implementar o mesmo mecanismo de injeção de dependências que integra o ASP.NET Core?
A resposta a esta pergunta é SIM! Com algumas adaptações descritas na próxima seção teremos condições de injetar dependências em uma Function App baseada no .NET Core. Para os testes descritos neste artigo fiz uso da versão 3.x das Azure Functions.
Implementando injeção de dependências em uma Function App
O exemplo detalhado nesta seção é o de uma Function App (ServerlessDI) contendo a implementação de um HTTP Trigger (chamado Teste). Ao criar um novo projeto baseado na versão 3.x das Azure Functions teríamos uma estrutura de arquivos como a detalhada na imagem:
Após os ajustes aqui descritos estarão também no projeto ServerlessDI os arquivos Interfaces.cs, Implementations.cs e Startup.cs, com implementações de estruturas demonstrando como injetar dependências:
O primeiro ajuste consiste em adicionar o package Microsoft.Azure.Functions.Extensions ao projeto de testes, com isso acontecendo por meio da instrução:
dotnet add package Microsoft.Azure.Functions.Extensions
Na listagem a seguir temos o arquivo ServerlessDI.csproj já referenciando este pacote:
No projeto ServerlessDI constarão as interfaces ITesteA e ITesteB, com as mesmas indicando dependências a serem resolvidas:
Já na próxima listagem temos:
- As classes TesteA e TesteB, correspondentes a implementações das interfaces ITesteA e ITesteB;
- A classe TesteC que, embora não implemente nenhuma interface, também será empregada na demonstração do uso de injeção de dependências com Azure Functions;
- A presença da propriedade IdReferencia em TesteA, TesteB e TesteC, com um GUID sendo atribuído como valor a tal elemento em cada um dos construtores destas classes;
- A classe TesteInjecao que receberá em seu construtor instâncias resolvendo as dependências representadas por ITesteA, ITesteB e TesteC. O método RetornarValoresInjecao será utilizado posteriormente, a fim de que seu retorno permita um melhor entendimento do uso de injeção de dependências em uma FunctionApp.
Em ASP.NET Core dependências são configuradas através de uma instância de IServiceCollection (namespace Microsoft.Extensions.DependencyInjection), com isto acontecendo no método ConfigureServices da classe Startup. Mas é necessário lembrar que ao criar uma nova Function App não encontramos uma implementação similar a esta (método ConfigureServices) no projeto gerado.
Como resolver isso então com Azure Functions?
A resposta a tal questionamento passa pela codificação de uma classe Startup baseada no tipo FunctionsStartup (namespace Microsoft.Azure.Functions.Extensions.DependencyInjection):
- O método Configure receberá como parâmetro uma instância de IFunctionsHostBuilder (namespace Microsoft.Azure.Functions.Extensions.DependencyInjection), através da qual teremos acesso a um objeto baseado em IServiceCollection;
- Chamadas aos métodos AddSingleton, AddTransient e AddScoped possibilitarão que se configurem as dependências a serem resolvidas quando da execução de uma Azure Function;
- Foram configuradas neste exemplo dependências envolvendo os tipos ITesteA, ITesteB, TesteC e TesteInjecao. No caso de TesteInjecao, instâncias desta classe também possuem dependências para ITesteA, ITeste e TesteC (o que permitirá entender os diferentes resultados possíveis).
E finalmente chegamos à implementação da Function Teste:
- As dependências envolvendo os tipos TesteInjecao, ITesteA, ITesteB e TesteC serão resolvidas no construtor da classe Teste;
- No caso específico de TesteInjecao, ao se criar uma nova instância desta classe também serão resolvidas dependências de ITesteA, ITesteB e TesteC;
- A chamada ao método RetornarValoresInjecao de TesteInjecao permitirá a geração de um resultado contendo o retorno correspondente às 2 dependências encontradas (nos construtores das classes Teste e TesteInjecao) para cada um dos tipos empregados nos testes (ITesteA, ITesteB e TesteC).
O código-fonte do projeto detalhado nesta seção já está disponível no GitHub:
Testes
Na próxima imagem temos exemplos de 2 requisições enviadas à Function Teste (com o projeto executando localmente, a partir do Visual Studio Code):
- Nota-se que o valor que os valores para ITesteA foram os mesmos em todas as dependências encontradas para as 2 requisições (Singleton);
- No caso de ITesteB, cada dependência encontrada resultou na geração de uma nova instância e consequentemente em um novo valor (Transient);
- Já para as dependências de TesteC o mesmo valor (associado a uma instância desta classe) é mantido para as 2 dependências encontradas, variando somente por requisição (Scoped).
Os testes com a Function App publicada na nuvem também apresentarão um resultado similar: