Já pensou em criar um sistema onde vários clientes usam o mesmo aplicativo, mas cada um tem seus dados totalmente separados? Essa é a ideia por trás de uma aplicação cloud multitenant. E o mais legal é que, com as ferramentas certas, dá para fazer isso de forma bem mais simples e eficiente. Quer saber como? Vamos te mostrar como usar o Azure Functions e o Neon Postgres para construir uma solução multitenant na nuvem.
Multitenancy: Uma Introdução Rápida
Multitenancy é uma arquitetura de software que permite que uma única instância de um aplicativo sirva a múltiplos clientes, chamados tenants. Imagine um prédio de apartamentos: cada apartamento (tenant) usa a mesma estrutura, mas tem seu próprio espaço privado e seguro.
Para garantir que os dados de cada tenant fiquem isolados e invisíveis para os outros, existem algumas abordagens:
- Banco de Dados por Tenant: Cada cliente tem seu próprio banco de dados. É o modelo mais seguro, mas pode ser complicado de gerenciar e aumentar os custos.
- Esquema por Tenant: Um único banco de dados com esquemas separados para cada cliente. É um bom equilíbrio entre segurança e uso de recursos.
- Tabela por Tenant: Um único banco de dados e esquema, com tabelas específicas para cada cliente. É eficiente, mas pode complicar a gestão dos dados.
- Coluna Discriminatória: Um único banco de dados, esquema e tabelas, com uma coluna que indica a qual cliente pertence cada dado. É o mais simples, mas o menos seguro.
Hoje, vamos mostrar como o Neon simplifica a abordagem de Banco de Dados por Tenant. Com as APIs do Neon, você pode automatizar a criação, atualização e exclusão de bancos de dados para cada cliente.
Pré-requisitos
Antes de começar, você vai precisar de:
- Assinatura do Azure: Uma conta ativa no Azure para criar e gerenciar seus recursos.
- Conta no Neon Postgres: Inscreva-se e configure sua instância do Neon Postgres. Dá para começar de graça!
Para o banco de dados Neon, você tem duas opções:
- Configurar um banco de dados no Neon Cloud.
- Configurar o “Neon Serverless Postgres” como um container nativo do Azure.
Em ambas as opções, você pode começar com uma assinatura gratuita e ir aumentando conforme necessário.
Em um dos artigos anteriores, explicamos como criar uma API de Produtos e implantá-la no Azure com .NET Aspire e Neon. Esse aplicativo permite gerenciar produtos e criar um carrinho de compras com vários itens.
Agora, vamos evoluir esse aplicativo e torná-lo multitenant. Cada tenant será uma loja com seus próprios produtos, armazenados em bancos de dados separados. Os usuários poderão fazer pedidos em cada uma dessas lojas.
Para construir essa aplicação cloud multitenant, vamos seguir estes passos:
- Adicionar modelos de dados e APIs de Tenant ao serviço da API de Produtos.
- Criar uma Azure Function para gerenciar os bancos de dados dos tenants com a API do Neon.
- Configurar o projeto .NET Aspire.
- Adicionar comunicação entre a API de Produtos e a Azure Function de gerenciamento de tenants.
- Migrar o banco de dados de um tenant e gerenciar as connection strings no Azure KeyVault com suporte a Caching.
- Implantar e testar nossa solução no Azure.
Você pode baixar o código fonte da solução completa no final deste artigo.
Passo 1: Adicionando Tenant à API de Produtos
Primeiro, crie uma entidade Tenant
na API de Produtos (uma versão simplificada para facilitar):
public class Tenant
{
public Guid Id { get; set; }
public string Name { get; set; }
}
Em seguida, nas entidades Product
e ProductCart
, adicione uma chave estrangeira para o Tenant
:
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Description { get; set; } = string.Empty;
public Guid? TenantId { get; set; }
}
public class ProductCart
{
public Guid Id { get; set; }
public int Quantity { get; set; }
public int UserId { get; set; }
public User User { get; set; } = null!;
public List<ProductCartItem> CartItems { get; set; } = [];
public Guid? TenantId { get; set; }
}
Agora, defina as APIs para gerenciar os tenants no seu aplicativo:
public record CreateTenantRequest(string Name);
public record UpdateTenantRequest(string CurrentName, string NewName);
app.MapPost("/tenants", (CreateTenantRequest request) => { });
app.MapPatch("/tenants/{currentName}", (string currentName, UpdateTenantRequest request) => { });
app.MapDelete("/tenants/{name}", (string name) => { });
app.MapGet("/tenants", () => { });
Pronto, o primeiro passo está completo! Voltaremos para implementar esses endpoints mais tarde.
Passo 2: Criando a Azure Function de Gerenciamento de Tenant
Para criar uma Azure Function no Visual Studio, selecione o modelo de projeto “Azure Function” e defina o tipo da Function para “HTTP” trigger. Você pode encontrar um manual detalhado aqui.
Se estiver usando o JetBrains Rider, instale o Azure Toolkit for Rider e crie um projeto “Azure Function”. Você pode encontrar um manual detalhado aqui.
Na sua Tenant Management Function, certifique-se de que os seguintes pacotes Nuget estejam instalados:
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
</ItemGroup>
Antes de mergulhar no código, vamos falar um pouco sobre a estrutura do Neon.
Primeiro, você precisa criar um projeto no Neon:
Cada projeto tem uma ou mais branches (como no GIT):
Cada branch contém um banco de dados.
Precisamos implementar as seguintes requisições de API:
- criar um banco de dados
- atualizar um banco de dados
- excluir um banco de dados
- obter todos os bancos de dados para referência
- obter a connection string do banco de dados
Usaremos a biblioteca Refit para simplificar as chamadas de API para o Neon para gerenciar os bancos de dados. O Refit é um wrapper em torno do HttpClientFactory que transforma chamadas REST em chamadas de método simples, tornando nosso código mais limpo e fácil de trabalhar.
Vamos explorar a interface INeonApi
usada para comunicação com o Neon:
public interface INeonApi
{
[Get("/api/v2/projects/{projectId}/branches/{branchId}/databases")]
Task<NeonDatabaseListResponseJson> GetDatabasesAsync(
[AliasAs("projectId")] string projectId,
[AliasAs("branchId")] string branchId);
[Post("/api/v2/projects/{projectId}/branches/{branchId}/databases")]
Task<NeonDatabaseCreateResponse> CreateDatabaseAsync(
[AliasAs("projectId")] string projectId,
[AliasAs("branchId")] string branchId,
[Body] CreateNeonDatabaseRequest request);
[Patch("/api/v2/projects/{projectId}/branches/{branchId}/databases/{databaseName}")]
Task<NeonDatabaseCreateResponse> UpdateDatabaseAsync(
[AliasAs("projectId")] string projectId,
[AliasAs("branchId")] string branchId,
[AliasAs("databaseName")] string databaseName,
[Body] UpdateNeonDatabaseRequest request);
[Delete("/api/v2/projects/{projectId}/branches/{branchId}/databases/{databaseName}")]
Task<NeonDatabaseCreateResponse> DeleteDatabaseAsync(
[AliasAs("projectId")] string projectId,
[AliasAs("branchId")] string branchId,
[AliasAs("databaseName")] string databaseName);
[Get("/api/v2/projects/{projectId}/connection_uri")]
Task<ConnectionStringResponse> GetConnectionStringAsync(
[AliasAs("projectId")] string projectId,
[AliasAs("branch_id")] string? branchId,
[AliasAs("database_name")] string databaseName,
[AliasAs("role_name")] string roleName);
}
Cada chamada de API precisa de um projeto e um identificador de branch.
Você pode encontrar o identificador do projeto na aba settings
:
E o identificador da branch na aba overview
:
Para saber mais sobre a API do Neon, comece com a documentação aqui.
Você também pode examinar todas as requisições e respostas em detalhes e até executá-las na referência completa da API do Neon.
Para conseguir enviar requisições de API para o Neon, você precisa criar uma API Key. Você pode criar uma em “Account settings” > “API keys“:
Usamos Azure Functions para lidar com as requisições de banco de dados dos tenants. Cada Azure Function tem HTTP triggers e é projetada para lidar com uma tarefa.
Vamos explorar a função de criar banco de dados:
internal record CreateTenantDatabaseRequest(string DatabaseName);
internal record CreateTenantDatabaseResponse(TenantDatabaseDetails Database,
string ConnectionString);
public class CreateTenantDatabaseFunction(
ILogger<CreateTenantDatabaseFunction> logger,
NeonConfigurationProvider configurationProvider,
INeonApi neonApi)
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
[Function(nameof(CreateNeonDatabase))]
public async Task<HttpResponseData> CreateNeonDatabase(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "database/create")]
HttpRequestData requestData)
{
try
{
var requestBody = await new StreamReader(requestData.Body).ReadToEndAsync();
var createRequest = JsonSerializer.Deserialize<CreateTenantDatabaseRequest>(
requestBody