Step-by-step Guide to Implementing Multi-Tenancy in WebAPI Using ASP.NET Core Identity
Tags: ddd, programming, development, csharp
Published: 2024-02-22 05:34:23

[Original article on Medium](https://medium.com/sharpassembly/step-by-step-guide-to-implementing-multi-tenancy-in-webapi-using-asp-net-core-identity-f11b3bf88c56)

Building web applications for financial services requires a keen focus on security and scalability. One key challenge is managing and protecting client data while serving multiple clients from a single application instance. Multi-tenancy offers a solution by allowing a single application to serve different clients, or "tenants," thereby keeping their data isolated and secure.

Implementing a multi-tenant model in **ASP.NET Core**, with **ASP.NET Core Identity** managing user authentication and authorization, requires specific steps that we will discuss below.

## Multi-Tenancy: An Overview

Multi-tenancy is commonly seen in cloud computing and **SaaS (Software as a Service)** applications, enabling efficient resource utilization, simplified application management, and reduced operational costs. Multi-tenancy's complexity and implementation requirements vary depending on the application's needs.

### Types of Multi-Tenancy

1. **Single-Tenant Architecture:** Each tenant has a dedicated instance of the software and underlying infrastructure. While not multi-tenancy in the strict sense, it's often discussed in contrast to actual multi-tenant architectures.
2. **Separate Database:** Tenants are separated into different databases with a single application instance. This keeps data separate and requires a new database environment for each new tenant.
3. **Shared Database, Separate Schemas:** Tenants share the same database but have their own schemas and tables. This keeps data separate but within the same database environment.
4. **Shared Database, Shared Schema:** The most complex form of multi-tenancy, where all tenants share the same database and schema. Tenant-specific identifiers differentiate data within the tables but require careful implementation to ensure data isolation.

### Benefits of Multi-Tenancy

- **Cost Efficiency:** Sharing resources among tenants helps reduce hardware and software costs.
- **Ease of Maintenance:** Updates and patches must be applied to only one application instance, simplifying maintenance.
- **Scalability:** Scaling a single application instance is more manageable than maintaining multiple instances for each tenant.
- **Resource Utilization:** Multi-tenancy can result in more efficient resource use, as idle capacity can be reallocated.

### Drawbacks of Multi-Tenancy

- **Complexity:** Implementing a multi-tenant architecture, particularly ensuring data isolation and security, can be complex.
- **Performance Risks:** Resource contention among tenants can impact performance.
- **Security Concerns:** Maintaining data isolation when using shared resources requires strict security measures to prevent potential data leaks and meet regulatory requirements.
- **Customization Limitations:** Providing deep customization options to tenants poses more challenges in a shared environment.

## Practical example

Now, let's dive into a practical example from **_InWestMan_**, a software application for tracking personal investments that requires isolating sensitive client data to be accessible only to users who created them. In the example, we will add multitenancy support to ensure clients can access and manipulate only portfolios created by them. We will use the last multitenancy type, "**Shared Database, Shared Schema**," using **ASP.NET Core Identity**.

**1.** First, in our **Infrastructure** project, we switch to using ASP.NET Core Identity's `DbContext`, which includes all required identity models, by changing the inheritance of our `AppDbContext` from the basic **Entity Framework**'s `DbContext` to `IdentityDbContext`.

```csharp
public class AppDbContext : IdentityDbContext
```

**2.** Next, in our **WebAPI** project, we need to register the **Identity** service in the `Program.cs` file.

```csharp
services.AddIdentity<IdentityUser, IdentityRole>()
            .AddEntityFrameworkStores<AppDbContext>();
```

**3.** Subsequently, in our **Domain** project, we add a `TenantId` property to **Aggregate Roots**, which, according to our domain model, are sensitive, for example, a `Portfolio` entity that stores the client's `Positions`.


```csharp
public abstract class BaseEntityWithMultitenancy<TId>
{
    public List<BaseDomainEvent> Events = [];

    public string TenantId { get; set; } = string.Empty;

    public TId Id { get; set; } = default!;
}
```
```csharp
public class Portfolio : BaseEntityWithMultitenancy<Guid>, IAggregateRoot
{
    public string Name { get; set; } = string.Empty;
    public ICollection<Position> Positions { get; set; } = new List<Position>();

    // other logic and sensitive fields...
}
```

**4.** Furthermore, in the **Application** project, we need to create `MultiTenancyService` that will keep a current tenant in the scope of the WebAPI call.

```csharp
public interface IMultiTenancyService
{
    string CurrentTenant { get; }
    void SetCurrentTenant(string tenant);
}
```
```csharp
public class MultiTenancyService : IMultiTenancyService
{
    private string _tenant = string.Empty;

    public string CurrentTenant
    {
        get => _tenant;
        private set
        {
            if (_tenant != value)
            {
                var oldTenant = _tenant;
                _tenant = value;
            }
        }
    }

    public void SetCurrentTenant(string tenant)
    {
        if (string.IsNullOrWhiteSpace(tenant))
            throw new ArgumentException("CurrentTenant cannot be null or whitespace.", nameof(tenant));
        CurrentTenant = tenant;
    }
}
```

**5.** It is crucial to remember to register the new `MultiTenancyService` in the service collection in the `Program.cs` file in the **WebAPI** project.

```csharp
services.AddScoped(typeof(IMultiTenancyService), typeof(MultiTenancyService));
```

**6.** Now, let's return to our **Application** project. I utilize the **MediatR**'s `AddPortfolioCommand` to add a new `Portfolio` to the database; therefore, I need to update my `AddPortfolioHandler` to include the client's tenant when saving the created entity. To achieve this, I pass an `IMultiTenancyService` instance to my `AddPortfolioHandler` class and explicitly assign the current `TenantId` to the newly created `Portfolio` object before saving.

```csharp
public class AddPortfolioCommand : IRequest<Result<Guid>>
{
    public string Name { get; set; }
    public CurrencyCode CurrencyCode { get; set; }
}
```
```csharp
public class AddPortfolioHandler : IRequestHandler<AddPortfolioCommand, Result<Guid>>
{
    private readonly IMultiTenancyService _multiTenancyService;
    private readonly IRepository<Portfolio> _repository;

    public AddPortfolioHandler(IRepository<Portfolio> repository,
        IMultiTenancyService multiTenancyService)
    {
        _repository = repository;
        _multiTenancyService = multiTenancyService;
    }

    public async Task<Result<Guid>> Handle(AddPortfolioCommand request, CancellationToken cancellationToken)
    {
        try
        {
            var portfolio = new Portfolio(request.Name, request.CurrencyCode);
            portfolio.TenantId = _multiTenancyService.CurrentTenant;

            await _repository.AddAsync(portfolio, cancellationToken);
            return Result.Ok(portfolio.Id);
        }
        catch (ArgumentException ex)
        {
            return Result.Fail<Guid>(ex.Message);
        }
    }
}
```

**7.** Returning to our **Infrastructure** project, we must ensure that the user will be provided only with data associated with her and only her tenancy. To achieve this, we add a global filter in `AppDbContext` for `Portfolio` entity into the overridden method `OnModelCreating()`. Additionally, we specify a new index based on the `TenantId` field to improve read query performance.

```csharp
public class AppDbContext : IdentityDbContext
{
    private readonly IMultiTenancyService _multiTenancyService;

    public AppDbContext(DbContextOptions<AppDbContext> options,
        IMultiTenancyService multiTenancyService)
        : base(options)
    {
        _multiTenancyService = multiTenancyService;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Portfolio>()
            .HasQueryFilter(b => b.TenantId == _multiTenancyService.CurrentTenant)
            .HasIndex(b => b.TenantId)
            .IsUnique(false);    
    }
}
```

**8.** Next, we switch to the **WebAPI** project, where it's necessary to read a tenancy claim from **HTTP cookies** and set the `TenantId` in `MultiTenancyService` using a specially created `MultiTenancy` middleware.

```csharp
public class MultiTenancy
{
    private readonly RequestDelegate _next;

    public MultiTenancy(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext, IMultiTenancyService multiTenancyService)
    {
        var tenantId = DetermineTenant(httpContext);

        multiTenancyService.SetCurrentTenant(tenantId);

        await _next(httpContext);
    }

    private string DetermineTenant(HttpContext context)
    {
        var user = context.User;
        if (user.HasClaim(c => c.Type == "tenant"))
        {
            return user.FindFirst("tenant")?.Value ?? string.Empty;
        }

        return "default-tenant";
    }
}
```

**9.** Subsequently, we must incorporate our `MultiTenancy` middleware in the `Program.cs` file. It should be added after the authentication process, as we will read a `TenantId` from the authenticated user's **HttpContext**.

```csharp
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<Middlewares.MultiTenancy>();
app.MapControllers();
```

**10.** Finally, we must ensure the `TenantId` is generated and stored as a `Claim` in the user's profile during the client registration process in the `Authentication` controller, and then returned to HTTP cookies upon successful authentication.

```csharp
[HttpPost("Register")]
public async Task<IActionResult> Register([FromBody] UserDto model)
{
    if (ModelState.IsValid)
    {
        var user = new IdentityUser { UserName = model.Name, Email = model.Email };
        var result = await _userManager.CreateAsync(user, model.Password);

        if (result.Succeeded)
        {
            var tenantId = Guid.NewGuid().ToString();
            var tenantClaim = new Claim("tenant", tenantId);
            await _userManager.AddClaimAsync(user, tenantClaim);

            return Ok(new { message = "User registered successfully" });
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    return BadRequest(ModelState);
}
```

## Conclusion

Implementing multi-tenancy in an **ASP.NET Core WebAPI** is a strategic approach to managing and protecting client data across multiple tenants within a single application instance. We explored the essential steps to set up a multi-tenant architecture using **ASP.NET Core Identity** and **Entity Framework Core**, ensuring data isolation and security while maintaining performance and scalability.

We've demonstrated how multi-tenancy can be achieved through a shared database and shared schema approach, leveraging **ASP.NET Core Identity** for user management and custom **MultiTenancy** middleware for tenant identification. Following the practical example, you can adapt these concepts for your projects, ensuring each client's data remains secure and isolated, even in a shared environment.

Multi-tenancy offers numerous benefits, including cost efficiency, ease of maintenance, and scalability, making it an attractive option for **SaaS** applications and services. However, it's crucial to be mindful of the potential complexities and security implications, planning your implementation carefully to avoid performance issues and ensure robust data protection.

Whether you're building a new application or enhancing an existing one, I hope the principles and practices outlined in this guide will help you navigate multi-tenancy challenges, enabling you to deliver a more flexible, secure, and scalable solution to your users.

[Original article on Medium](https://medium.com/sharpassembly/step-by-step-guide-to-implementing-multi-tenancy-in-webapi-using-asp-net-core-identity-f11b3bf88c56)

Stay tuned for more insights and detailed analyses, and feel free to share your thoughts or questions in the comments below!

[SharpAssembly on Dev.to](https://dev.to/sharpassembly)
[SharpAssembly on Medium](https://sharpassembly.medium.com/)
[SharpAssembly on Telegram](https://t.me/SharpAssembly)

Cover credits: DALL·E generated