.Net Aspire - Parte 11: Explorando o recurso de Database em um projeto .Net Aspire utilizando o PostgreSQL
Neste artigo, explorar a utilização do recurso de Banco de Dados em um projeto .Net Aspire com a integração com o PostgreSQL
Neste artigo vamos realizar o estudo de uma integração com o banco de dados PostgreSQL em um projeto .Net Aspire.
Após o estudo, vamos modelo que foi criado neste artigo ou pode ser baixado neste repositório. O Objetivo final será termos um CRUD completo com o PostgreSql em nosso projeto modelo.
Usando as integrações do .NET Aspire para o PostgreSQL
As integrações do .NET Aspire PostgreSQL exigem alterações no projeto app host
e em quaisquer microsserviços que usam os bancos de dados.
- Configurando o
app host
Comece instalando a integração apropriada no projeto app host
do aplicativo:
dotnet add package Aspire.Hosting.PostgreSQL --prerelease
Em seguida, para registrar um banco de dados e criar um contêiner para ele, adicione este código ao arquivo Program.cs
no projeto app host
aplicativo:
var postgres = builder.AddPostgres("aspirepostgres");
var postgresdb = postgres.AddDatabase("productdb");
Algumas das integrações de banco de dados .NET Aspire também permitem criar um contêiner para ferramentas de gerenciamento de banco de dados. Para adicionar o PgAdmin
à sua solução para gerenciar o banco de dados PostgreSQL, use o código abaixo. Este é um código alternativo ao apresentado acima.
var postgresdb = builder.AddPostgres("pg")
.AddDatabase("postgresdb")
.WithPgAdmin();
A vantagem de deixar o .NET Aspire criar o container é que você não precisa fazer nenhuma configuração para conectar o PgAdmin
ao banco de dados PostgreSQL
, é tudo automático.
Você também deve passar uma referência ao serviço de banco de dados para qualquer projeto que o consuma:
var productCatalogAPI = builder.AddProject<Projects.NorthernTraders_CatalogAPI>("product-api")
.WithReference(postgresdb);
- Configurando os projetos que irão consumir o banco de dados:
Para instalar a integração .NET Aspire PostgreSQL, use um comando como este em seus projetos .NET Aspire:
dotnet add package Aspire.Npgsql --prerelease
Ou para usar a integração do .NET Aspire PostgreSQL Entity Framework Core, use este comando:
dotnet add package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL --prerelease
- Usando o Database PostgreSQL
Em qualquer projeto onde você deseja usar o banco de dados, você adiciona uma fonte de dados para representar a conexão com o PostgreSQL
. No arquivo Program.cs
, este código registra o banco de dados:
builder.AddNpgsqlDataSource("postgresdb");
Ou para usar a integração do Entity Framework Core, registre o contexto do banco de dados:
builder.AddNpgsqlDbContext<YourDbContext>("postgresdb");
Depois que o banco de dados estiver registrado no projeto de consumo, você poderá interagir com a fonte de dados sempre que precisar usando injeção de dependência, segue um código de exemplo:
public class YourService(NpgsqlDataSource dataSource)
{
public async Task<IEnumerable<Catalog>> GetCatalog()
{
const string query = "SELECT * FROM catalog";
using var dbConnection = dataSource.OpenConnection();
var results = await dbConnection.QueryAsync<Catalog>(command);
return queryResult.ToArray();
}
}
Ou você pode recuperar o contexto do banco de dados YourDbContext
para interagir com o banco de dados:
public class YourService(YourDbContext context)
{
public async Task<IEnumerable<Catalog>> GetCatalog()
{
var items = await context.ObjectItems;
if (item is null)
{
return Results.NotFound();
}
else
{
return items;
}
}
}
- Configurando a integração com o PostgreSQL
A stack do .NET Aspire tenta reduzir a quantidade de configurações necessárias. Usando injeção de dependência e descoberta de serviço, você pode acessar o banco de dados sem precisar configurar as cadeias de conexão em seus projetos.
Usar o projeto app host
da solução para criar o contêiner do banco de dados e passá-lo como referência aos projetos permite que os projetos receptores acessem o local do banco de dados, as strings de conexão e as portas. Não há necessidade de gerenciar variáveis de ambiente ou arquivos appsettings.json
.
Mas, se você quiser ou precisar de mais controle sobre como o banco de dados está configurado, há mais opções.
Usando uma string de conexão:
No projeto que requer o banco de dados, você usa uma string de conexão para conectar-se ao banco de dados. Essa abordagem é útil quando você precisa se conectar a um banco de dados que não está registrado no app host
da solução.
builder.AddNpgsqlDataSource("NpgsqlConnectionString");
Em seguida, no arquivo de configuração, você pode adicionar a string de conexão:
{
"ConnectionStrings": {
"NpgsqlConnectionString": "Host=myserver;Database=postgresdb;User id=myuser;Password=mypassword"
}
}
Usando a configuração do provider:
O .NET Aspire possui um recurso de integrações que permite suportar Microsoft.Extensions.Configuration
. A integração do PostgreSQL
suporta esse recurso e, por padrão, procura configurações usando a chave Aspire:Npgsql
. Em projetos que usam appsettings.json
, um exemplo de configuração pode ser assim:
{
"Aspire": {
"Npgsql": {
"ConnectionString": "Host=myserver;Database=postgresdb;User id=myuser;Password=mypassword",
"HealthChecks": true,
"Tracing": true,
"Metrics": true
}
}
}
A configuração anterior define a string de conexão, habilitando health checks
, tracing
e metrics
para a integração do PostgreSQL
. Seu código não precisa mais especificar a string de conexão, basta usar builder.AddNpgsqlDataSource();
.
Usando inline delegates
The last option is to pass a configureSettings inline delegate to the AddNpgsqlDataSource method. This delegate allows you to configure the settings for the database integration directly with code:
A última opção é passar a configureSettings
através de um delegate para o método AddNpgsqlDataSource
. Este delegate permite que você defina as configurações para a integração do banco de dados diretamente com o código:
builder.AddNpgsqlDataSource(
"postgresdb", static settings => settings.HealthChecks = false);
Utilizando o modelo para realizarmos a integração com o PostgreSql
Como descrito no início do artigo, você pode criar seu modelo no artigo mencionado ou baixar o fonte no repositório informado.
Com a solução aberta, vamos executar os seguintes passos para executar a integração:
- Projeto
AppHost
(DatabaseIntegrationBase.AppHost)
Iniciando, faça a instalação do pacote necessário para a instalação
dotnet add package Aspire.Hosting.PostgreSQL --prerelease
No arquivo Program.cs
, adicione a inclusão do PostgreSql ao projeto:
// Add PostgreSql integration
var postgres = builder.AddPostgres("pg")
.WithPgAdmin()
.WithBindMount("./Service.API/Seed", "/docker-entrypoint-initdb.d")
.AddDatabase("postgres");
Explorando o código acima:
.AddPostgres
: Irá adicionar a integração com o PostgrSql, adicionando um servidor chamado pg
.
.WithPgAdmin
: Irá adicionar um contêiner com o PgAdmin, para administração do banco, execução de queries e demais operações.
.WithBindMount
: Irá adicionar um script de inicialização para nosso banco, criando uma tabela para nosso CRUD. Esta parte ficou como script pois o objetivo é a integração e não mergulharmos no mundo SQL.
.AddDatabase
: Adiciona o database com o nome postgres
. Fiz testes com outros nomes e estudei a execução de um script criando bancos, schemas e databases diferentes. Isso é possível? Sim, porém, esta implementação foi realizada através do recurso de service discovery
, onde nem a connection string precisamos configurar. A intenção de apresentar desta forma é justamente para mostrar como nós, desenvolvedores(as) podemos ganhar tempo sabendo utilizar os recursos do .Net Aspire.
Após a inclusão do PostgreSql no arquivo Program.cs
, vamos alterar o registro do serviço da cama de API, ou seja o projeto DatabaseIntegrationBase.ApiService. Faça a substituição da linha abaixo:
var apiService = builder.AddProject<Projects.DatabaseIntegrationBase_ApiService>("apiservice");
Por esta, adicionando a referência do PostgreSql:
var apiService = builder.AddProject<Projects.PostgreExample_ApiService>("apiservice")
.WithReference(postgres);
Para finalizar a configuração de nosso projeto AppHost
, vamos adicionar o script sql responsável por criar a tabela para que possamos integrar com o banco de dados.
Na raiz do projeto AppHost
, crie uma pasta chamada Service.API
. Dentro desta pasta crie uma outra pasta chamada Seed
. Este é o caminho que o .Net Aspire irá mapear um arquvo de nossa solução para um volume com script de execução dentro do conteiner. salve o arquivo dentro da pasta Seed
com o nome initdatabase.sql
e insira o conteúdo abaixo dentro do arquivo:
-- Conceder todas as permissões no schema 'movie' ao usuário postgres
GRANT ALL PRIVILEGES ON SCHEMA public TO postgres;
-- Criação da tabela 'movies' no schema 'movie' se não existir
CREATE TABLE IF NOT EXISTS public.movies (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
gender VARCHAR(50),
is_active BOOLEAN NOT NULL
);
-- Conceder todas as permissões na tabela 'movies' ao usuário postgres
GRANT ALL PRIVILEGES ON TABLE public.movies TO postgres;
Pronto, em nosso projeto AppHost
estamos com tudo configurado para a implementação do PostgreSql, agora vamos incluir o mesmo nos projetos que irão consumir, no nosso caso o projeto ApiService
- Projeto
ApiService
(DatabaseIntegrationBase.ApiService)
Vamos adicionar o pacote do PostgreSql no nosso projeto da API. Para tal, acesse a pasta do projeto ApiService
DatabaseIntegrationBase.ApiService
, onde se encontra o arquivo .csproj do mesmo e execute o comando para adicionar o pacote. Para facilitar os testes com a integração com o banco de dados, vamos adicionar o Swagger
ao projeto da API também, desta forma poderemos ver e valdiar todas as nossas operações. Execute o segundo comando para adicionar o pacote do Swagger
.
dotnet add package Aspire.Npgsql --prerelease
dotnet add package Swashbuckle.AspNetCore
Com os dois pacotes adicionados, vamos incluir a instrução responsável pelo service discovery
para realizar toda a implementação realizada no projeto AppHost
com a referência ao projeto ApiService
. Podemos adicionar a instrução logo abaixo da linha var builder = WebApplication.CreateBuilder(args);
. Já iremos aproveitar e adicionar também a refeẽncia do Swagger
, como poderemos ver nas linhas de código:
// Add postgres integration
builder.AddNpgsqlDataSource("postgresdb");
// Add swagger to API
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "Movie API",
Description = "API for managing a list of movies and their active status.",
TermsOfService = new Uri("https://example.com/terms")
});
});
Agora vamos adicionar o middleware do Swagger para interagirmos com nossos endpoints. Logo acima da linha app.Run()
, adicione o código abaixo:
app.UseSwagger();
app.UseSwaggerUI();
Adicionando os endpoints para testes
Se tratando de uma minimal api e um projeto de teste, no arquivo Program.cs
, vamos fazer uma alterações em nosso arquivo Program.cs
.
Exclusão de linhas:
Procure em seu código as linhas abaixo e exclua as mesmas, a intenção é deixar a solução menos poluída para estudo e não atrapalhar nossa nova implementação:
// Primeiro comando pra exlcuir
string[] summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];
// Segundo comando para exlcuir
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
// Terceiro comando para excluir
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Logo após a linha var app = builder.Build();
, adicione o código abaixo:
app.MapGet("/movieitems", async (NpgsqlDataSource dataSource) =>
{
using var dbConnection = dataSource.OpenConnection();
await using var command = dbConnection.CreateCommand();
command.CommandText = "SELECT * FROM public.movies";
await using var reader = await command.ExecuteReaderAsync();
var movies = new List<Movie>();
while (await reader.ReadAsync())
{
movies.Add(new Movie
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Gender = reader.IsDBNull(2) ? null : reader.GetString(2),
IsActive = reader.GetBoolean(3)
});
}
return Results.Ok(movies);
}).WithName("GetAllMovies");
app.MapGet("/movieitems/active", async (NpgsqlDataSource dataSource) =>
{
using var dbConnection = dataSource.OpenConnection();
await using var command = dbConnection.CreateCommand();
command.CommandText = "SELECT * FROM public.movies WHERE is_active = TRUE";
await using var reader = await command.ExecuteReaderAsync();
var movies = new List<Movie>();
while (await reader.ReadAsync())
{
movies.Add(new Movie
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Gender = reader.IsDBNull(2) ? null : reader.GetString(2),
IsActive = reader.GetBoolean(3)
});
}
return Results.Ok(movies);
}).WithName("GetActiveMovies");
app.MapGet("/movieitems/{id}", async (int id, NpgsqlDataSource dataSource) =>
{
using var dbConnection = dataSource.OpenConnection();
await using var command = dbConnection.CreateCommand();
command.CommandText = "SELECT * FROM public.movies WHERE id = @id";
command.Parameters.AddWithValue("id", id);
await using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
var movie = new Movie
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Gender = reader.IsDBNull(2) ? null : reader.GetString(2),
IsActive = reader.GetBoolean(3)
};
return Results.Ok(movie);
}
return Results.NotFound();
}).WithName("GetMovieById");
app.MapPost("/movieitems", async (Movie movie, NpgsqlDataSource dataSource) =>
{
using var dbConnection = dataSource.OpenConnection();
await using var cmd = dataSource.CreateCommand("INSERT INTO public.movies (name, gender, is_active) VALUES (@name, @gender, @is_active) RETURNING id");
cmd.Parameters.AddWithValue("name", movie.Name);
cmd.Parameters.AddWithValue("gender", (object)movie.Gender ?? DBNull.Value);
cmd.Parameters.AddWithValue("is_active", movie.IsActive);
movie.Id = (int)await cmd.ExecuteScalarAsync();
return Results.Ok(movie);
}).WithName("CreateMovie");
app.MapPut("/movieitems/{id}", async (int id, Movie inputMovie, NpgsqlDataSource dataSource) =>
{
using var dbConnection = dataSource.OpenConnection();
await using var cmd = dataSource.CreateCommand("UPDATE public.movies SET name = @name, gender = @gender, is_active = @is_active WHERE id = @id");
cmd.Parameters.AddWithValue("name", inputMovie.Name);
cmd.Parameters.AddWithValue("gender", (object)inputMovie.Gender ?? DBNull.Value);
cmd.Parameters.AddWithValue("is_active", inputMovie.IsActive);
cmd.Parameters.AddWithValue("id", inputMovie.Id);
var result = await cmd.ExecuteNonQueryAsync();
return result > 0 ?
Results.Ok(inputMovie) :
Results.NotFound();
});
app.MapDelete("/movieitems/{id}", async (int id, NpgsqlDataSource dataSource) =>
{
using var dbConnection = dataSource.OpenConnection();
await using var cmd = dataSource.CreateCommand("DELETE FROM public.movies WHERE id = @id");
cmd.Parameters.AddWithValue("id", id);
var result = await cmd.ExecuteNonQueryAsync();
return result > 0 ?
Results.Ok() :
Results.NotFound();
}).WithName("DeleteMovie");
Pronto, ocorrendo tudo bem, estaremos com o postgres na solução referenciado pelo projeto da API, ou seja, com a informação da utilizaçao da mesma e o Swagger também. Vamos dar build em nossa solução e executar as mesmas, para observar os recursos adicionados no dashboard. Após validarmos a implementação, vamos adicionar nossos endpoints de teste.
Basta executar a aplicação, como já realizado anteriormente, mas agora, teremos um resultado semelhante ao abaixo:
Podemos esperar (por um tempo) que um serviço (no nosso caso será o pg-pgadmin) possa demorar um pouco mais para subir. O campo State
dele ficaria como este , mas logo ele estará pronto para uso.
Com a visualizaçao do dashboard, podemoas observar os seguintes recursos:
pg
: Tipo container para levantar o servidor postgres de nome pg
. Iremos confirmar logo abaixo a criação do mesmo e execução do script para iniciar o banco de dados.
pg-admin
: Tipo container para disponibilizar a ferramenta pg-admin
, assim sendo possível realizar o gerenciamento do banco de dados.
postgres
: Tipo PostgresDatabaseResource, ou seja, nosso banco de dados de nome postgres
.
apiservice
: Já mencionado acima. Projeto da API de nossa solução.
webfrontend
: Já mencionado acima. Projeto frontend de nossa solução, mas não iremos abordar o front end neste artigo.
Repare que para cada recurso (menos o resource do database), temos um endpoint, vamos clicar no endpoint responsável pelo pg-admin
e verificar sua abertura, podemos esperar um resultado como o abaixo:
Repare na imagem nos seguintes aspectos:
pg
: Servidor criado, conforme instrução em nosso projeto AppHost
.
postgres
: Database padrão criada.
movies
: Tabela criada de acordo com o script que inserimos na pasta Seed
, conforme orientado acima.
Agora vamos validar o Swagger que adicionamos, para verificar e testar todos nossos endpoints e a integração com o postgresql.
No dashboard do .Net Aspire, teremos o recurso apiservice
. Vamos clicar no link da coluna endpoints
adicionando o caminho /swagger
. Devemos esperar um resutado igual ao abaixo:
Testando a aplicação
Com a aplicação rodando e o swagger aberto, vamos executar essa rotina para validar todos os endpoints, seus retornos e os dados armazenados no postgres.
- Execute o método post com o json abaixo
POST/movieitems
:
{
"name": "Homem de Ferro",
"gender": "Ação",
"isActive": true
}
- Adicione um segundo filme para a lista com o json abaixo
POST/movieitems
:
{
"name": "Homem de Ferro 2",
"gender": "Ação",
"isActive": true
}
- Execute a consulta para obter todos os filmes
GET/movieitems
. Teremos o seguinte resultado:
[
{
"id": 1,
"name": "Homem de Ferro",
"gender": "Ação",
"isActive": true
},
{
"id": 2,
"name": "Homem de Ferro 2",
"gender": "Ação",
"isActive": true
}
]
- Execute a consulta para obter um filme por id
GET/movieitems/{id}
utilizando o id1
como parâmetro de busca. Teremos o seguinte resultado:
{
"id": 1,
"name": "Homem de Ferro",
"gender": "Ação",
"isActive": true
}
- Volte para a guia anteriormente aberta do
pg-pgadmin
, que nos dá acesso ao gerenciamento do banco de dados. Abra uma janela de query nova e utilize a query abaixo para buscar os filmes do banco:
SELECT * FROM public.movies
Observação: Para abrir a janela de query, clique no database postgres e no botão Query Tool
, como demonstrado na imagem abaixo. Deveremos esperar um resultado como o da imagem abaixo:
Botão Query Tool:
Resultado esperado:
Estes testes já nos mostra que a integração com o banco de dados foi realizada com sucesso. Fica o convite de realizar atualização e deleção para verificar, enquanto realiza queries no banco de dados.
Conclusão
Fica o convite para continuar testando e alterar a forma de conexão com o postgres, pois esta que implementamos foi através do service discovery
. Como mencionado no artigo logo acima, temos várias outras formas de realizarmos esta conexão. Este código final executando com sucesso ficará no meu repositório, na branch feature/postgres-integration.
Este foi um artigo grande, denso e fica o convite de voltar e repetir a implementação para praticar. Lembre-se de interagir com o post dando sua opinião, tirando dúvidas ou dando um feedback, que é muito importante para a continuação do projeto.
Se você gostou do conteúdo, deixe um comentário ou uma reação para apoiar o projeto! Compartilhe com alguém e ajude a divulgar!
Até a próxima!
[ ]´s Degas.