No .NET ASPIRE existe uma forma de você expressar para o Orquestrador, um database dentro do ASPIRE para que o serviço de service discovery entregue uma conexão corretamente para nossas aplicações.
O problema é que o .NET Aspire não cria esse banco de dados, então é isso que vamos abordar hoje.
Contexto
Aqui na Academia Dev estou trabalhando diariamente, de segunda-a-sexta para criar um Gateway de Pagamentos Fake, com a intenção de explorar aspectos de arquitetura de software, arquitetura de soluções e design de solução.
Transformei a criação desse projeto em uma jornada diária onde AO VIVO mostramos desde a criação do primeiro repositório GIT até sua entrada em produção. Desde a primeira à última linha de código até a criação da infra de CI/CD e os primeiros servidores.
Ao escolher uma implementação com multi-tenant com nível de maturidade SaaS 4, temos o desafio de criar novos recursos dinamicamente.
- Servidores
- Instâncias de banco
- Bancos de dados
- Objetos (tabelas sequences etc)
Mas antes de chegar nesse ponto, temos a demanda de criar o database principal, que não é dinâmico. Esse database é onde ficam as definições de workspace, informações de quais tenants estão em quais servidores, e dados administrativos sobre cada tenant.
Saiba mais sobre a Jornada Academia Pay
Entendendo o Aspire
O .NET Aspire oferece uma camada consistente porém simples de service discovery.
O orquestrador (ferramenta apenas de desenvolvimento) define quais são os recursos e ao executar os projetos ele injeta configurações.
A injeção de configurações acontece injetando variáveis de ambiente, que pela natureza do mecanismo de configuração do .NET, com múltiplas fontes, respeita a precedência e sobrescreve, em memória os dados de configuração que poderiam existir nos arquivos de configuração appsettings.json e similares, por aquilo que foi injetado via variável de ambiente.
Esse mecanismo usa apenas o que sempre existiu no .NET (Core). Assim para sobrescrever uma configuração { ConnectionStrings: { Key: "Value1" } } com Value2, basta adicionar como variável de ambiente ConnectionStrings__Key = Value2.
A criação do Servidor PostgreSQL
Um dos recursos principais que temos a demanda aqui é o banco de dados primário, aquele que armazena informações administrativas.
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("mainpostgres")
.WithPgAdmin();
var mainDB = postgres.AddDatabase("maindb");
builder.AddProject<Projects.AcademiaPay_Backoffice_WebApi>("academiapay-backoffice-webapi")
.WithReference(mainDB);
builder.Build().Run();
Com essa configuração mínima somos capazes de criar um container do PostgreSQL (mainpostgres) com um PgAdmin do lado, também como container.
Fazemos isso nas linhas 3 e 4.
Na linha 6 definimos temos a chance de dizer que precisamos de um novo banco de dados dentro desse PostgreSQL. Fazemos isso chamando var mainDB = postgres.AddDatabase("maindb"); onde produzimos um resource que conseguimos referenciar na linha 9.
Parece ótimo certo?
A Api recebe na connectionstring o servidor correto com o database correto, entretanto o Aspire não cria o tal database.
A issue Aspire.Npgsql component does not create database #1170 descreve isso
E Damian Edwards confirma.



De fato é prudente para o Aspire não se meter nesse vespeiro. Não é responsabilidade do ASPIRE realizar esse tipo de operação, visto que, estamos diante de algo que pode ser complexo, e com muitas variáveis que fogem ao propósito do projeto.
- Ao criar um database, com qual usuário?
- Qual datafile ou tablespace?
- Tem de criar usuário?
- Tem de dar permissão?
- Roles?
Não é papel nem escopo do ASPIRE lidar com essas coisas.
Mas nada impede que a comunidade faça o seu papel de comunidade e crie quem faça!
O principal ponto que debuta contra a criação de algo tão complexo com Aspire, é que o orquestrador é um componente que não será implantado com sua aplicação, portanto se você atribuir responsabilidades demais, ficará em maus lençóis pois não tem quem exerça esse papel dessa forma em produção.
E a solução não é pensar que o orquestrador do Aspire devesse ser um componente de produção.
Essa é uma ideia tão simplista quanto estúpida: Se o orquestrador do Aspire chegasse perto de ser um componente de produção, então ele precisaria de mais de 1 década para chegar perto do nível de maturidade necessário.
Então como lidar com a criação física desse database?
Projetos de bootstrap são a solução.
Nesse momento, em que estamos na segunda semana de trabalho, com aproximadamente 14 horas de trabalho, nosso Program.cs do orquestrador do Aspire está assim.
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("mainpostgres")
.WithPgAdmin();
var mainDB = postgres.AddDatabase("maindb");
var postgresDB = postgres.AddDatabase("postgres");
var cache = builder.AddRedis("cache")
.WithRedisCommander();
var bootstrapProject = builder.AddProject<Projects.AcademiaPay_Backoffice_Bootstrap>("academiapay-backoffice-bootstrap")
.WithReference(mainDB)
.WithReference(postgresDB)
.WithReference(cache);
builder.AddProject<Projects.AcademiaPay_Backoffice_WebApi>("academiapay-backoffice-webapi")
.WithReference(bootstrapProject)
.WithReference(mainDB)
.WithReference(cache);
builder.AddProject<Projects.AcademiaPay_Backoffice_Worker>("academiapay-backoffice-worker")
.WithReference(bootstrapProject)
.WithReference(mainDB)
.WithReference(cache);
builder.Build().Run();
Ainda falta
- Keycloak para Identidade
- RabbitMQ como Message Broker
- Apache APISix como Api Gateway
nomes já são confirmados nessa festa!
Nem chegamos lá, e já temos alguns desafios, como por exemplo criar o bendito database.
Estruturando o pensamento ao redor da solução
Quais são as possibilidades?
1) Parametrizar a criação do PostgreSQL
Considerando que o Aspire usa a imagem default do PostgreSQL, lá do docker hub, sabemos que criar um único database com essa imagem é ridiculamente simples, basta parametrizar, como a documentação de uso da imagem descreve.
Assim a configuração abaixo resolveria:
var postgres = builder.AddPostgres("mainpostgres")
.WithEnvironment("POSTGRES_DB", "maindb")
.WithPgAdmin();
Entretanto, a imagem não oferece suporte para a criação de múltiplos bancos. O que é muito necessário em contexto de microsserviços.
Desenvolva separado, e implante junto se quiser.
Se desenvolver junto, dificilmente implantará separado!
2) Criar script SQL de Inicialização da Instância
Os principais bancos de dados containerizados, exceto o SQL Server, possuem uma pasta padrão do qual na primeira vez que inicializarem o banco, executarão seus scripts.
Essa padronização é útil para permitir configurações mais complexas de ambiente. Bastaria adicionar scripts SQL ou SH na pasta /docker-entrypoint-initdb.d e o próprio container faria esse trabalho na primeira execução (somente na primeira execução).
Esse foi o motivo pelo qual criei o projeto e a imagem luizcarlosfaria/mssql-server-linux:2019-latest em 2019.
Para dar ao SQL Server o mesmo comportamento que MariaDB, Postgres e MySQL possuem.
3) Criar um bootstrapper
Um projeto C# que criasse os recursos necessários para a execução da aplicação. Esse projeto criaria o banco e tudo mais que fosse necessário.
Escolhendo a solução
Para esse projeto escolhi a criação de um bootstrapper por alguns motivos:
- Para esse projeto, não basta criar o banco de dados, existem outras configurações que são dinâmicas, como o API Gateway que dinamicamente deve receber interações para a criação de credenciais de acesso. Portanto um projeto de bootstrap se faz necessário.
- Dada a necessidade de mais tarde criar outros servidores, containers e bancos de dados dinamicamente, fazia sentido criar o próprio database via C#, permitindo ter um reaproveitamento desse código mais tarde, evitando duplicações.
- Independente de usar um script ou criar um bootstrap para criar o database, um outro bootstrap deve ser criado para aplicar as migrations com o projeto. Demandando novamente um projeto de bootstrap.
Criar um bootstrapper normalmente não é a opção óbvia, mas nesse caso, nesse projeto, por conta das demais variáveis e necessidades, se tornou.
Execução
Via Aspire o projeto bootstrapper recebe como informação de conexão, 2 connectionstrings:
- Uma para o Database postgres (default), pronta para uso.
- Uma para o Database maindb (o Database que criaremos), ainda precisando que o Database seja criado para poder ser usada.
Na linha 6 definimos o banco de dados alvo, aquele que queremos criar.
Na linha 7 definimos o banco de dados padrão, aquele que permite conexões por ser o banco default do postgres.
Sob a ConnectionString postgres, o projeto Bootstrapper se conecta com sucesso ao banco e executa a criação do database maindb.
Já nossa Migration implementada com FluentMigrator executa a criação dos objetos na conexão maindb, que a partir do passo anterior já está pronta para ser usada.
private void ExecuteDDL()
{
ArgumentNullException.ThrowIfNull(configuration);
var connectionString = configuration.GetValue<string>("ConnectionStrings:maindb");
ArgumentNullException.ThrowIfNullOrWhiteSpace(connectionString);
var serviceProvider = new ServiceCollection().AddFluentMigratorCore()
.ConfigureRunner(rb => rb
.AddPostgres15_0()
.WithGlobalConnectionString(connectionString)
.ScanIn(typeof(BootstrapperSetup).Assembly).For.Migrations())
.AddLogging(lb => lb.AddFluentMigratorConsole())
.BuildServiceProvider(false);
using (var scope = serviceProvider.CreateScope())
{
var runner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();
runner.MigrateUp();
}
}
Assim conseguimos lidar com essa inicialização sem muita dor de cabeça, já endereçando os próximos assuntos.









0 comentários