ASP.NET Core 6: Multi-tenant Multi-Database (Parte 3)
Tags: dotnet, aspnetcore, csharp, multitenant
Published: 2021-09-22 19:11:56

# Introducción

Continuando con la última parte de esta serie de posts en [asp.net](http://asp.net) core, hoy veremos como seguir el approach Multi-Tenant con Multi-Database (Tenant Per Database).

De igual forma, exploraremos las ventajas y desventajas de utilizar múltiples bases de datos para múltiples tenants y veremos que opción es mejor según el escenario.

Te vuelvo a recordar que esta serie de posts viene con muchos code snippets, es mejor que lo leas siguiendo el repositorio en [Github con el ejemplo final](https://github.com/isaacOjeda/DevToPosts/tree/main/MultiTenantSingleDatabase).

Esta serie de posts se dividen en 3 partes:

- [ASP.NET Core 6: Creando una app Multi-tenant (Parte 1)](https://dev.to/isaacojeda/asp-net-core-6-creando-una-app-multi-tenant-parte-1-3df5)
- [ASP.NET Core 6: Multi-tenant Single Database (Parte 2)](https://dev.to/isaacojeda/asp-net-core-6-multi-tenant-single-database-parte-2-4cbh)
- ASP.NET Core 6: Multi-tenant Multi-Database (Parte 3)  (este post)

# Tipos de multi-tenancy con multi-database

Ahora toca ver las modalidades de hacer multi-tenancy con varias bases de datos. Este puede ser o será el approach más usado en la mayoría de los casos, pero como en todo, siempre hay que ver sus ventajas y desventajas y verificar si es lo mejor para la solución que estemos creando.

## Database Per Tenant

En esta modalidad, cada Tenant cuenta con su propia base de datos. Este approach lo consideraría como el mejor y el más usado. veamos por qué.

![Untitled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4umq76r48a0x8536cikk.png) 

### Seguridad

- ✔️ Alto nivel de aislamiento, permite distribuir las bases de datos en distintos servidores.
- ❌ Al contar con más servidores y más bases de datos, también hay que administrarlos y mantenerlos seguros.

### Mantenibilidad

- ✔️ El mantenimiento se lleva acabo por tenant y puede ser personalizado según la carga de la BD
- ✔️ Fácilmente se puede restaurar/reubicar/limpiar la información de cada tenant
- ✔️ No hay complejidad en los Queries
- ❌ Agregar Tenants nuevos requiere de más trabajo, por ser una base de datos totalmente aparte
    - *Solución*: Automatizar el proceso de creación de tenants
- ❌ Con forme vayan creciendo los tenants, existirán muchas bases de datos que mantener y administrar

### Escalabilidad

- ✔️ Scale-out  y Scale-up son opciones viables — Los tenants pueden ser distribuidos en múltiples servidores
- ✔️ Elegir un balance entre costo (alta densidad / menos servidores) y eficiencia (baja densidad / más servidores)
- ✔️ El efecto "noisy neighbor" no nos afecta (tanto).

## Multiple Databases, Multiple Tenants (Shared Schema)

Esta modalidad es un hibrido entre *Table Based* del post pasado y *Database Per Tenant*. Existen múltiples bases de datos y dentro de cada base de datos puede existir 1 o más tenants.

![Untitled 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x1riksravn26ev7ownbs.png) 

### Seguridad

- ✔️ Existe un aislamiento parcial al utilizar múltiples bases de datos
- ❌ Hay tenants que siguen compartiendo el esquema
    - De igual forma el RLS funciona como mitigación

### Mantenibilidad

- ✔️ La opción de elegir si queremos tener muchas bases de datos (densidad de tenants baja) o pocas bases de datos (densidad de tenants alta)
- ✔️ Posibilidad de reubicar la información de un tenant (aunque más difícil que el approach tenant per database)
- ❌ Genera más mantenimiento que si usaramos el puro approach *Table based*

### Escalabilidad

- ✔️ Scale-out  y Scale-up son opciones viables — Los tenants pueden ser distribuidos en múltiples servidores
- ✔️  Elegir un balance entre costo (alta densidad / menos servidores) y eficiencia (baja densidad / más servidores)

## ¿ Por qué Database **Per Tenant**?

Así como en el post anterior comenté porque he usado el modo **table-based** explicaré porque también uso un tenant por base de datos.

La configuración desde ASP.NET Core para la selección de que base de datos usar es relativamente sencilla y esta modalidad la utilizo para aplicaciones **Monoliticas.** En las que estoy seguro que tendremos muchas tablas y el crecimiento de tenants puede ser indefinido.

El aislamiento es la mejor parte, porque sabemos que cada cliente tiene su información totalmente separada que la de otros clientes. Restaurar la información de un cliente sin afectar a otros es buena razón para irnos por este approach.

El mantenimiento será brutal, pero con tanta información, siempre será una tarea de dedicación y cuidado (sin importar que esquema multitenant usemos).

# SingleTenant DbContext

Para continuar con este ejemplo, estoy haciéndolo en un proyecto aparte (Que encontrarás en el repositorio de GitHub) y me estoy basando totalmente en el contenido del [Post 1](https://dev.to/isaacojeda/asp-net-core-6-creando-una-app-multi-tenant-parte-1-3df5).

Necesitaremos ahora, un contexto llamado `SingleTenantDbContext` (muy similar al del [Post 2](https://dev.to/isaacojeda/asp-net-core-6-multi-tenant-single-database-parte-2-4cbh))

```csharp
public class SingleTenantDbContext : DbContext
{
    private readonly MultiTenants.Fx.Tenant _tenant;

    public SingleTenantDbContext(
        DbContextOptions<SingleTenantDbContext> options,
        ITenantAccessor<MultiTenants.Fx.Tenant> tenantAccessor) : base(options)
    {
        _tenant = tenantAccessor.Tenant ?? throw new ArgumentNullException(nameof(MultiTenants.Fx.Tenant));
    }

    public DbSet<Product> Products { get; set; }

    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        foreach (var entry in ChangeTracker.Entries<AuditableEntity>())
        {
            switch (entry.State)
            {
                case EntityState.Added:
                    entry.Entity.CreatedAt = DateTime.UtcNow;
                    break;
                case EntityState.Modified:
                    entry.Entity.ModifiedAt = DateTime.UtcNow;
                    break;
            }
        }

        return base.SaveChangesAsync(cancellationToken);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        optionsBuilder.UseSqlServer(_tenant.Items["ConnectionString"]?.ToString());
    }
}
```

El contenido de `SaveChangesAsync` realmente no es necesario, pero lo quise dejar como en el Post 2, lo importante ocurre en el constructor y en el `OnConfiguring`.

`OnConfiguring` configurará el contexto como lo hacemos usualmente al registrarlo como dependencia, pero como el Connection String será dinámico, lo leeremos del `Tenant` que nos regresa el contenedor de dependencias (todo esto del Post 1).

`AuditableEntity` sigue siendo como en el Post 2, pero sin la propiedad `TenantId` (ya que no necesitamos una columna con ese dato).

En esta ocasión, agregamos algo nuevo llamado `ITenantAccessor` y nos ayuda a acceder al Tenant actual de una forma más elegante y no desde el `HttpContext` como lo hicimos en el Post 2.

```csharp
public interface ITenantAccessor<T> where T : Tenant
{
    public T? Tenant { get; init; }
}
```

```csharp
public class TenantAccessor : ITenantAccessor<Tenant>
{
    public TenantAccessor(IHttpContextAccessor contextAccessor, IConfiguration config, IWebHostEnvironment env)
    {
        Tenant = contextAccessor.HttpContext?.GetTenant();

        if (Tenant is null && env.IsDevelopment())
        {
            // Nota 👀:
            // Si estamos en modo desarrollo y no hay Tenant, 
            // probablemente es alguna inicialización o creación de migración
            // en modo desarrollo
            Tenant = new Tenant(-1, "TBD");
            Tenant.Items["ConnectionString"] = config.GetConnectionString("SingleTenant");
        }
    }

    public Tenant? Tenant { get; init; }
}
```

Aquí sucede un truco y si ustedes tienen una mejor solución, háganmelo saber 😅.

Lo que sucede al ejecutar el comando `dotnet ef migrations add` se compila la solución y se construye el contexto para ver los cambios que existen en el modelo y cuando ejecutamos `dotnet ef database update` consulta la base de datos en físico (la existente en nuestro servidor local) y revisa que migraciones han sido aplicadas.

Por esta razón, necesitamos darle una cadena de conexión de "prueba" o "desarrollo" y se conecte, pero en producción, esto no será así.

```json
{
  "ConnectionStrings": {
    "TenantAdmin": "Server=(localdb)\\mssqllocaldb;Database=MultiTenant_AdminV2;Trusted_Connection=True;MultipleActiveResultSets=true",
    "SingleTenant": "Server=(localdb)\\mssqllocaldb;Database=MultiTenantMultiDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
```

El contexto del Post 1 (`TenantAdminDbContext`) se mantiene igual, solo el Entity Tenant le agregamos una columna `ConnectionString`.

```csharp
public class Tenant
{
    public int TenantId { get; set; }
    public string? Name { get; set; }
    public string? Identifier { get; set; }
    public string? ConnectionString { get; set; }
}
```

Y en el `DbContextTenantStore` que también teníamos del Post 1, queda así.

```csharp
public async Task<Tenant> GetTenantAsync(string identifier)
{
    var cacheKey = $"Cache_{identifier}";
    var tenant = _cache.Get<Tenant>(cacheKey);

    if (tenant is null)
    {
        var entity = await _context.Tenants
            .FirstOrDefaultAsync(q => q.Identifier == identifier)
                ?? throw new ArgumentNullException($"identifier no es un tenant válido");

        tenant = new Tenant(entity.TenantId, entity.Identifier);

        tenant.Items["Name"] = entity.Name;
        tenant.Items["ConnectionString"] = entity.ConnectionString; // UPDATE ⚠️

        _cache.Set(cacheKey, tenant);
    }

    return tenant;
}
```

> ⚠️ **Nota:** Es importante mencionar que la tabla Tenants de esta base de datos es muy importante tenerla bajo llave, ya que cualquier fuga de información de esta sería muy peligroso porque contiene toda la información de las bases de datos.

Como estamos en .NET 6 (al día de hoy en RC1) necesitamos los siguientes paquetes antes de continuar.

```xml
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0-rc.1.21452.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0-rc.1.21452.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0-rc.1.21452.10">
	<PrivateAssets>all</PrivateAssets>
	<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
```

> 👀 Nota: Se puede utilizar cualquier versión reciente de asp.net core sin problema

# Integración con ASP.NET

Aquí hay una diferencia en como configuraremos nuestra base de datos en el `Program.cs`

```csharp
builder.Services.AddDbContext<TenantAdminDbContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("TenantAdmin")));
// NEW!
builder.Services.AddDbContext<SingleTenantDbContext>();
```

Aquí no estamos especificando que cadena de conexión utilizará ni que proveedor. Esto se hace en el contexto mismo y con Dependency Injection lo hacemos dinámico según el tenant. 

Cool ¿verdad?

> 👀 Nota : Para tener una mejor idea del proyecto completo, revisa mi [GitHub](https://github.com/isaacOjeda/DevToPosts/tree/main/MultiTenantSingleDatabase)

Teniendo ya todo configurado, hay que crear las bases de datos de cada contexto.

```bash
dotnet ef migrations add FirstMigration --context SingleTenantDbContext -o Persistence/Migrations/SingleTenant
dotnet ef migrations add FirstMigration --context TenantAdminDbContext -o Persistence/Migrations/TenantAdmin

dotnet ef database update --context SingleTenantDbContext
dotnet ef database update --context TenantAdminDbContext 
```

Esto creará 2 migraciones (y creará 2 bases de datos)

![Untitled 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vpnah4gvqg1gscz8xw43.png) 

Y según los Connection Strings de **appsettings.json**, nos creará estas 2 bases de datos.

![Untitled 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e82lsx47m6m1tt5swt98.png) 

`MultiTenantMultiDb` ya la podríamos usar, pero lo que hice es crear 2 bases de datos aparte copiadas de esta misma (con script o bacpac, hazlo a tu modo).

![Untitled 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3d514jgdb5bl4sj8bl2b.png) 

# Finalizando

Para por fin hacer pruebas, tendremos esta información en nuestra tabla de Tenants

![Untitled 5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g3dm6ehrqj2ckhy1g2us.png) 

Las cadenas de conexión las debes de poner según lo que estés usando (SQL Server Express, SQL Lite, etc)

Y en las BDs individuales, agregamos los productos que queramos para probar.

![Untitled 6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mbhjhejf9c17i3wq9fsh.png) 

![Untitled 7](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7b0h9tjgly7djt66b79l.png) 

Como en el Post 2, crearemos una vista sencilla para mostrar los productos.

```html
@page
@model MultiTenantMultiDatabase.Pages.Products.IndexModel
@{
}

<h1>Productos</h1>

<table class="table">
    <thead>
        <tr>
            <th>Product Id</th>
            <th>Description</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var product in Model.Products)
        {
            <tr>
                <td>@product.ProductId</td>
                <td>@product.Description</td>
            </tr>
        }
    </tbody>
</table>
```

```csharp
public class IndexModel : PageModel
    {
        private readonly SingleTenantDbContext _context;

        public IndexModel(SingleTenantDbContext context)
        {
            _context = context;
        }

        public ICollection<Product> Products { get; set; }

        public void OnGet()
        {
            Products = _context.Products.ToList();
        }
    }
```

Como ven, al Entity `Products` no se le agrega ningún filtro ya que el `DbContext` decidirá automáticamente a que base de datos conectarse.

![Tenant 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ukxufbrk2o1cjz7kkqcd.png)

![Tenant 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cmtazxwzmyb2bo7ad4hs.png)

# Conclusión

De esta forma configuramos la infraestructura de nuestra aplicación para que los Developers simplemente se preocupen en las funcionalidades a desarrollar y todo esto sea transparente para ellos.

Cuéntame ¿Qué opinas de estas propuestas para crear aplicaciones Multi-Tenant?

Espero les sea de utilidad, ya que en los últimos proyectos que he diseñado, me he basado en estas dos modalidades habladas en esta serie de posts y me ha funcionado bastante bien ya en proyectos grandes en producción.

Code4Fun 👍🏽.

# Referencias

[Multi-Tenancy with SQL Server, Part 2: Database Design Approaches](https://www.sentryone.com/blog/multi-tenancy-with-sql-server-part-2-approaches)