ASP.NET Core 6: Creando una app Multi-tenant (Parte 1)
Tags: csharp, dotnet, aspnetcore, visualstudio
Published: 2021-09-11 22:46:04

# Introducción

En esta serie de posts estaremos viendo una de las formas que se pueden realizar aplicaciones multi-tenant en ASP.NET Core (Razor Pages en esta ocación).

Utilizaremos distintos estilos de patrones para emplear mecanismos que nos facilitarán el día a día en una aplicación multi-tenant.

Esta serie de posts se dividen en 3 partes:

- ASP.NET Core 6: Creando una app Multi-tenant (Parte 1) (este post)
- [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)](https://dev.to/isaacojeda/asp-net-core-6-multi-tenant-multi-database-parte-3-2onc)

Te recomiendo que este tutorial lo veas junto con el [código de ejemplo](https://github.com/isaacOjeda/DevToPosts/tree/main/MultiTenantSingleDatabase) ya que hay muchos snippets y se volverá un poco más extenso con las demás partes.

Si tienes alguna pregunta, no dudes en contactarme por mi twitter [@balunatic](https://twitter.com/balunatic).

## ¿Qué es una aplicación multi-tenant?

Es una aplicación que responde diferente dependiendo de cual "tenant" se está accesando, existen distintas formas de crear aplicaciones multi-tenant:

- **multi-aplicación:** Cada tenant tiene sus propios recursos y dependencias y se ejecuta todo por separado.
- **single database**: Todos los tenants corren en la misma aplicación y en la misma base de datos. Aquí hay que tener cuidado para nunca exponer información de un tenant en otro, lo veremos en otro post.
- **multi database:** Todos los tenants tienen su propia base de datos pero utilizan la misma aplicación.

Cada estilo de multi-tenant apps tiene sus beneficios y se deben de considerar distintos factores (como escalabilidad, cantidad de tenants, almacenamiento por tenant, etc)

Este [artículo](https://docs.microsoft.com/en-us/azure/azure-sql/database/saas-tenancy-app-design-patterns) explica muy bien las formas de hacer multi-tenancy y lo que hay que considerar. 

## ¿Qué requerimientos tiene una aplicación multi-tenant?

Hay un un par de requerimientos que deberíamos cumplir para crear una aplicación multi-tenant.

### Resolución del Tenant

Según la solicitud HTTP que llegue a nuestro servicio, debemos de determinar que tenant se está accesando y así establecer cadenas de conexión a bases de datos, configuración y entre otras cosas.

### Configuración del Tenant

La aplicación podría configurarse diferente según el tenant que se está accediendo, como private keys de servicios externos y entre otras cosas.

### Aislamiento del Tenant

Cada tenant debe de poder acceder a su información y solo a su información. Ya sea que utilicemos una sola base de datos o varias bases de datos por tenant, es importante establecer la infraestructura adecuada para hacer más difícil a los developers de que se equivoquen y mostrar información de otro tenant por algún error de código.

# Resolver el tenant

Para resolver un tenant primero necesitamos su representación en una clase, aquí podemos agregar lo que más nos sea útil de un tenant. Pero por practicidad podemos utilizar un diccionario y los datos que se quieran, ahí se agregan:

```csharp
public class Tenant
{
    public Tenant(int id, string identifier)
    {
        Id = id;
        Identifier = identifier;
        Items = new Dictionary<string, object>();
    }

    public int Id { get; }
    public string Identifier { get; }
    public Dictionary<string, object> Items { get; }
}
```

Utilizaremos el campo `Identifier` para poder saber que tenant se está tratando de usar en la solicitud actual (Ejemplo. `https://{identifier}.contoso.com`).

La propiedad `Id` será nuestro identificador interno (el cual podría ser la llave primaria de la base de datos) y este no cambiará, `Identifier` podría cambiar sin problema.

Y por último tenemos el diccionario `Items`, que como mencionaba arriba, nos ayudará agregar cualquier propiedad adicional que creamos conveniente.

## Formas comunes de resolver un tenant

Utilizaremos una estrategia para resolver el tenant según el request, la estrategia no debe basarse en ningún servicio o dato externo, así lo hacemos mejor estructurado y rápido.

### Según el Host

El tenant se determinará según el host que es enviado por el navegador, este para mi es el mejor porque cada cliente (tenant) podrá tener su propio dominio o al menos un subdominio. Ejemplos: `https://cliente1.contoso.com`, `https://cliente2.contoso.com`. 

En este caso, solo está cambiando el subdominio, pero podríamos soportar dominios personalizados para cada tenant.

### Según un Header

El tenant podría ser determinado según un valor de algún HTTP Header, por ejemplo `X-Tenant: cliente1`. Este es más común cuando la aplicación multi-tenant es una API como `https://api.contoso.com` y la aplicación cliente especifica el valor del tenant.

### Según el URL

Otro también muy común es por el path del request. Se utiliza un mismo dominio pero según la estructura del path (el url) se puede determinar el tenant que se quiere acceder. Por ejemplo `https://contoso.com/cliente1/...`.

## Definiendo una estrategia para resolver el tenant

Para permitir que la aplicación sepa que estrategia utilizar, deberíamos de poder implementar un servicio de `ITenantResolutionStrategy` el cual según el request, no se regresará el tenant (el identifier).

```csharp
public interface ITenantResolutionStrategy
{
    Task<string> GetTenantIdentifierAsync();
}
```

En este post, implementaremos la resolución de tenants según el Host.

```csharp
public class HostResolutionStrategy : ITenantResolutionStrategy
{
    private readonly HttpContext? _httpContext;

    public HostResolutionStrategy(IHttpContextAccessor httpContext)
    {
        _httpContext = httpContext.HttpContext;
    }

    public async Task<string> GetTenantIdentifierAsync()
    {
        if (_httpContext is null)
        {
            return string.Empty;
        }

        return await Task.FromResult(_httpContext.Request.Host.Host);
    }
}
```

# Almacenamiento de Tenants

Ahora ya sabemos que tenant debemos resolver, pero ahora la pregunta es ¿De dónde obtenemos los tenants? Para eso necesitamos un repositorio o "store" para consultar los tenants que tenemos disponibles. Para hacerlo independiente a la persistencia, implementaremos un `ITenantStore` el cual aceptará el `Identifier` del tenant para buscarlo en algún origen de datos.

```csharp
public interface ITenantStore<T> where T : Tenant
{
    Task<T> GetTenantAsync(string identifier);
}
```

> ¿Por qué hicimos el store genérico? Realmente estamos diseñando una solución reutilizable, alguien más en nuestra organización podría usar nuestra librería y debemos de permitir que pueda adaptarla a las necesidades del proyecto.

La clase `Tenant` puede almacenar cualquier tipo de información. Si tuviéramos muchas bases de datos probablemente vamos a querer guardar cadenas de conexión del tenant en este mismo objeto, pero podría ser algo inseguro ya que estamos trabajando con información sensible y lo recomendable es utilizar el patrón Options por tenant o algún Vault como el de Azure.

En este post vamos a guardar los tenants en una base de datos y en otros posts tendremos otra(s) base de datos para la información propia de los tenants.

Por ahora solo necesitaremos un `DbContext` de Entity Framework: `TenantAdminDbContext` (el que administra los tenants) y posteriormente crearemos más.

Para trabajar con Entity Framework necesitamos los siguientes paquetes (al día de este post, siguen estando en preview).

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

Y nuestro contexto que administrará los tenants quedará de la siguiente forma:

```csharp
/// <summary>
/// Entity Tenant (diferente a Infrastructure.Tenant)
/// </summary>
public class Tenant
{
    public int TenantId { get; set; }
    public string Name { get; set; }
    public string Identifier { get; set; }
}
```

```csharp
using Microsoft.EntityFrameworkCore;
using MultiTenantSingleDatabase.Models;

public class TenantAdminDbContext : DbContext
{
    public TenantAdminDbContext(DbContextOptions<TenantAdminDbContext> options)
        : base(options) { }

    public DbSet<Tenant> Tenants { get; set; }
}
```

Aquí estamos definiendo un Entity **Tenant** que es diferente al `Tenant` que encontramos dentro de Infrastructure > Multitenancy (uno es Dto y otro Domain Object).

La propiedad Name es para tener una descripción del tenant (Ejemplo: **Contoso Crafts**) y el Identifier (Ejemplo: **contoso**).

Ahora que ya tenemos nuestro origen de datos (Una base de datos con una tabla Tenants) podemos escribir nuestro `TenantStore`.

```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using MultiTenantSingleDatabase.Persistence;

public class DbContextTenantStore : ITenantStore<Tenant>
{
    private readonly TenantAdminDbContext _context;
    private readonly IMemoryCache _cache;

    public DbContextTenantStore(TenantAdminDbContext context, IMemoryCache cache)
    {
        _context = context;
        _cache = cache;
    }

    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 ArgumentException($"identifier no es un tenant válido");

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

            tenant.Items["Name"] = entity.Name;

            _cache.Set(cacheKey, tenant);
        }

        return tenant;
    }
}
```

Esta implementación puede variar a como lo necesites, este es solo un ejemplo práctico. Podemos ver que incluso estamos agregando a caché los Tenants que se van consultando, porque esto se hará en cada request y si siempre consultamos a la BD esto será nada eficiente.

# Integración con ASP.NET Core

Apenas vamos a mitad de camino. Ya tenemos lo esencial para resolver los tenants pero ahora falta conectar algunos cables para que esto empiece a funcionar.

## Registrando los servicios

Ahora que ya tenemos la forma de diferenciar los tenants y un lugar donde consultarlos, necesitamos registrar estos servicios como dependencias de nuestra aplicación.

Queremos que esto funcione como una librería que se pueda extender, por eso haremos uso de estilos "fluent" y "builders".

Primero, crearemos una extension siguiendo el estilo de registrar servicios de [asp.net](http://asp.net) core con una sintaxis `.AddMultiTenancy()`.

```csharp
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Agrega los servicios (con clase específica)
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public static TenantBuilder<T> AddMultiTenancy<T>(this IServiceCollection services) where T : Tenant
        => new(services);

    /// <summary>
    /// Agrega los servicios (con clase default)
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public static TenantBuilder<Tenant> AddMultiTenancy(this IServiceCollection services)
        => new(services);
}
```

Y ahora el Builder.

```csharp
public class TenantBuilder<T> where T : Tenant
{
    private readonly IServiceCollection _services;

    public TenantBuilder(IServiceCollection services)
    {
        _services = services;
    }

    /// <summary>
    /// Registrar la implementación de Resolución de Tenants
    /// </summary>
    /// <typeparam name="V"></typeparam>
    /// <param name="lifetime"></param>
    /// <returns></returns>
    public TenantBuilder<T> WithResolutionStrategy<V>(ServiceLifetime lifetime = ServiceLifetime.Transient)
        where V : class, ITenantResolutionStrategy
    {
        _services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        _services.Add(ServiceDescriptor.Describe(typeof(ITenantResolutionStrategy), typeof(V), lifetime));
        return this;
    }

    /// <summary>
    /// Registrar la implementación del Repositorio de Tenants
    /// </summary>
    /// <typeparam name="V"></typeparam>
    /// <param name="lifetime"></param>
    /// <returns></returns>
    public TenantBuilder<T> WithStore<V>(ServiceLifetime lifetime = ServiceLifetime.Transient)
        where V : class, ITenantStore<T>
    {
        _services.Add(ServiceDescriptor.Describe(typeof(ITenantStore<T>), typeof(V), lifetime));
        return this;
    }
}
```

Ahora dentro de nuestro `Program.cs` registraremos estas dependencias (estamos con .NET 6 por lo que las plantillas default ya no incluyen un `Startup` como antes).

```csharp
builder.Services.AddMultiTenancy()
    .WithResolutionStrategy<HostResolutionStrategy>()
    .WithStore<DbContextTenantStore>();
```

Hasta este punto ya "casi" podríamos consultar el tenant según el request, pero aparte de que nos falta configurar la base de datos (y crear unos tenants de ejemplo) sería muy latoso siempre estar usando el `ITenantResolutionStrategy` junto con el `ITenantStore` para estar consultando el tenant actual.

Por lo que la solución será, un middleware.

## Registrando el middleware

Los middlewares son muy útiles cuando queremos que algo se procese en el pipeline de la solicitud HTTP. En este caso, queremos que el tenant esté resuelto antes de que cualquier Controlador o Razor Page quiera usarlo, eso significa que este middleware debe de ir antes de Controllers o Razor Pages.

Primero creamos nuestra clase middleware para que inyecte el Tenant actual en la solicitud Http.

```csharp
public class TenantMiddleware<T> where T : Tenant
{
    private readonly RequestDelegate next;

    public TenantMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        if (!context.Items.ContainsKey(AppConstants.HttpContextTenantKey))
        {
            var tenantStore = context.RequestServices.GetService(typeof(ITenantStore<T>)) as ITenantStore<T>;
            var resolutionStrategy = context.RequestServices.GetService(typeof(ITenantResolutionStrategy)) as ITenantResolutionStrategy;

            var identifier = await resolutionStrategy.GetTenantIdentifierAsync();
            var tenant = await tenantStore.GetTenantAsync(identifier));

            context.Items.Add(AppConstants.HttpContextTenantKey, tenant);
        }

        //Continue processing
        if (next != null)
            await next(context);
    }
}
```

Y ahora para registrarlo al estilo [ASP.NET](http://asp.NET) Core, creamos la siguiente extensión.

```csharp
public static class ApplicationBuilderExtensions
{
    /// <summary>
    /// Use the Teanant Middleware to process the request
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="builder"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseMultiTenancy<T>(this IApplicationBuilder builder) where T : Tenant
        => builder.UseMiddleware<TenantMiddleware<T>>();

    /// <summary>
    /// Use the Teanant Middleware to process the request
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="builder"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseMultiTenancy(this IApplicationBuilder builder)
        => builder.UseMiddleware<TenantMiddleware<Tenant>>();
}
```

Para terminar, registramos este middleware en el pipeline dentro del `Program.cs`.

```csharp
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMultiTenancy(); // <--- custom middleware
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
```

En este caso estamos usando Razor Pages, pero realmente eso no importa podría ser MVC clásico o una Web API.

Ahora que el Tenant ya se encuentra accessible dentro del `HttpContext` podemos escribir la siguiente extensión (y última) para poder acceder a él de una manera más práctica.

```csharp
/// <summary>
/// Extensiones de HttpContext para hacer multi-tenancy más fácil de usar
/// </summary>
public static class HttpContextExtensions
{
    /// <summary>
    /// Regresa el Tenant actual
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="context"></param>
    /// <returns></returns>
    public static T? GetTenant<T>(this HttpContext context) where T : Tenant
    {
        if (!context.Items.ContainsKey(AppConstants.HttpContextTenantKey))
            return null;

        return context.Items[AppConstants.HttpContextTenantKey] as T;
    }

    /// <summary>
    /// Regresa el Tenant actual
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public static Tenant? GetTenant(this HttpContext context) => context.GetTenant<Tenant>();
}
```

## Creando la Base de Datos

Para por fin crear la base de datos, debemos registrar el contexto dentro del `Program.cs`.

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

Y podemos utilizar el siguiente connection string (junto con el otro que utilizaremos más adelante).

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

Y para crear la base datos, hacemos una migración inicial y actualizamos la base de datos (solito creará la base de datos ya que esta no existirá inicialmente).

Lo siguiente, lo ejecutamos estando el proyecto principal.

```bash
dotnet ef migrations add InitTenantAdmin -o Persistence/Migrations/TenantAdmin
dotnet ef database update
```

Esto ya creará la base de datos (dentro de `C:\Users\<user>\MultiTenant_Admin.mdf`)

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

Lo estamos organizando de esta manera porque todavía falta otro `DbContext` que haremos en otro post.

# Finalizando

Para poder probar que todo lo que hicimos funciona, podemos modificar cualquier controlador o Page que tengamos. En mi caso, como estoy usando Razor Pages, pues modificaré el `Index.cshtml`.

```html
@page
@model IndexModel

@using MultiTenantSingleDatabase.Infrastructure.Multitenancy

@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">Welcome @HttpContext.GetTenant()?.Items["Name"] </h1>    
</div>
```

Y el resultado.

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

Ya que estoy mostrando el nombre del tenant (no el Identifier) se muestra "My localhots Tenant".

Así tengo mi BD.

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

Para probar el segundo tenant, hay que hacer un pequeño truco para modificar el archivo hosts y poner un host que apunte a 127.0.0.1 (al igual que lo hace localhost). Puedes intentarlo [aquí](https://docs.rackspace.com/support/how-to/modify-your-hosts-file/).

En fin, navegando al segundo tenant, me muestra el resultado esperado.

![Untitled 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vzr87vo4mfhewe6a8jet.png)
 
Lo que falta ahora, será crear un **DbContext** que realice queries de forma dinámica a los Entitites que corresponden a cada quien según el Tenant, pero esto quedará para el siguiente post.

# Conclusión

En este post vimos como crear los mecanismos de detección de tenants y su implementación para el escenario de multi-tenant que elegimos.

Gracias a las interfaces se pueden implementar las estrategias de resolución de tenants como se desee y también el repositorio de tenants.

Gracias a las extensiones y middlewares, de una forma muy sencilla (`HttpContext`) podemos acceder al Tenant actual según el request.

Existen distintas formas de hacer esto, pero me gustó esta solución que originalmente propone [Michal McKenna](https://michael-mckenna.com/) que en este [post](https://michael-mckenna.com/multi-tenant-asp-dot-net-core-application-tenant-resolution) explica esta solución en ingles, en la cual me basé principalmente (más del 99% 😅). Thanks Micke!.

Muchos saludos y sigue aprendiendo 💪🏽.