askill
tenant-context-propagation

tenant-context-propagationSafety 90Repository

Tenant context resolution and propagation patterns for multi-tenant applications. Covers middleware, headers, claims, and distributed tracing.

0 stars
1.2k downloads
Updated 2/5/2026

Package Files

Loading files...
SKILL.md

Tenant Context Propagation Skill

When to Use This Skill

Use this skill when:

  • Tenant Context Propagation tasks - Working on tenant context resolution and propagation patterns for multi-tenant applications. covers middleware, headers, claims, and distributed tracing
  • Planning or design - Need guidance on Tenant Context Propagation approaches
  • Best practices - Want to follow established patterns and standards

Overview

Patterns for resolving and propagating tenant context throughout multi-tenant applications.

Tenant context must be available everywhere in the application - from web requests to background jobs to microservice calls. This skill covers strategies for establishing, propagating, and accessing tenant context reliably.

Context Resolution Strategies

Tenant Resolution Methods:
+------------------------------------------------------------------+
| Method          | Source              | Use Case                 |
+-----------------+---------------------+--------------------------+
| Subdomain       | acme.app.com        | SaaS with vanity URLs    |
| Custom Domain   | app.acme.com        | White-label enterprise   |
| Header          | X-Tenant-Id         | API/microservices        |
| JWT Claim       | tenant_id claim     | Authenticated requests   |
| Path Segment    | /tenants/{id}/...   | REST API design          |
| Query Parameter | ?tenant_id=...      | Admin/support tools      |
| Database Lookup | user → tenant       | User-first resolution    |
+-----------------+---------------------+--------------------------+

Context Architecture

+------------------------------------------------------------------+
|                    Tenant Context Flow                            |
+------------------------------------------------------------------+
|                                                                   |
|  +---------+    +------------+    +-------------+                 |
|  | Request |-->| Middleware |-->| Context      |                 |
|  | (HTTP)  |   | Resolver   |   | Provider     |                 |
|  +---------+   +------------+   +-------------+                  |
|                      |                  |                         |
|                      v                  v                         |
|              +-------------+    +---------------+                 |
|              | Validation  |    | AsyncLocal<T> |                 |
|              | (exists?)   |    | (Thread-safe) |                 |
|              +-------------+    +---------------+                 |
|                                        |                          |
|                    +-------------------+-------------------+      |
|                    v                   v                   v      |
|              +---------+        +---------+        +----------+   |
|              | Services|        | EF Core |        | Background|  |
|              |         |        | Filters |        | Jobs      |  |
|              +---------+        +---------+        +----------+   |
|                                                                   |
+------------------------------------------------------------------+

Tenant Context Provider

Interface Definition

public interface ITenantContext
{
    Guid TenantId { get; }
    string TenantName { get; }
    string TenantSlug { get; }
    TenantSettings Settings { get; }
    bool IsResolved { get; }
}

public interface ITenantContextAccessor
{
    ITenantContext? Current { get; set; }
}

AsyncLocal Implementation

public sealed class TenantContextAccessor : ITenantContextAccessor
{
    private static readonly AsyncLocal<TenantContextHolder> _current = new();

    public ITenantContext? Current
    {
        get => _current.Value?.Context;
        set
        {
            var holder = _current.Value;
            if (holder != null)
            {
                holder.Context = null;
            }

            if (value != null)
            {
                _current.Value = new TenantContextHolder { Context = value };
            }
        }
    }

    private sealed class TenantContextHolder
    {
        public ITenantContext? Context { get; set; }
    }
}

Resolution Middleware

Subdomain Resolution

public sealed class SubdomainTenantMiddleware(
    RequestDelegate next,
    ITenantContextAccessor contextAccessor,
    ITenantRepository tenants,
    ILogger<SubdomainTenantMiddleware> logger)
{
    private static readonly string[] ExcludedSubdomains = ["www", "api", "admin"];

    public async Task InvokeAsync(HttpContext context)
    {
        var host = context.Request.Host.Host;
        var subdomain = ExtractSubdomain(host);

        if (!string.IsNullOrEmpty(subdomain) && !ExcludedSubdomains.Contains(subdomain))
        {
            var tenant = await tenants.GetBySubdomainAsync(subdomain);
            if (tenant != null)
            {
                contextAccessor.Current = new TenantContext(tenant);
                logger.LogDebug("Resolved tenant {TenantId} from subdomain {Subdomain}",
                    tenant.Id, subdomain);
            }
            else
            {
                logger.LogWarning("Unknown subdomain: {Subdomain}", subdomain);
                context.Response.StatusCode = 404;
                return;
            }
        }

        await next(context);
    }

    private static string? ExtractSubdomain(string host)
    {
        var parts = host.Split('.');
        return parts.Length >= 3 ? parts[0] : null;
    }
}

Header Resolution (APIs)

public sealed class HeaderTenantMiddleware(
    RequestDelegate next,
    ITenantContextAccessor contextAccessor,
    ITenantRepository tenants)
{
    private const string TenantHeader = "X-Tenant-Id";

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Headers.TryGetValue(TenantHeader, out var tenantIdValue))
        {
            if (Guid.TryParse(tenantIdValue, out var tenantId))
            {
                var tenant = await tenants.GetByIdAsync(tenantId);
                if (tenant != null)
                {
                    contextAccessor.Current = new TenantContext(tenant);
                }
            }
        }

        await next(context);
    }
}

JWT Claim Resolution

public sealed class ClaimsTenantMiddleware(
    RequestDelegate next,
    ITenantContextAccessor contextAccessor,
    ITenantRepository tenants)
{
    public async Task InvokeAsync(HttpContext context)
    {
        if (context.User.Identity?.IsAuthenticated == true)
        {
            var tenantClaim = context.User.FindFirst("tenant_id");
            if (tenantClaim != null && Guid.TryParse(tenantClaim.Value, out var tenantId))
            {
                var tenant = await tenants.GetByIdAsync(tenantId);
                if (tenant != null)
                {
                    contextAccessor.Current = new TenantContext(tenant);
                }
            }
        }

        await next(context);
    }
}

Propagation to Services

EF Core Query Filters

public sealed class AppDbContext(
    DbContextOptions<AppDbContext> options,
    ITenantContextAccessor tenantContext) : DbContext(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Apply tenant filter to all tenant-scoped entities
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (typeof(ITenantScoped).IsAssignableFrom(entityType.ClrType))
            {
                modelBuilder.Entity(entityType.ClrType)
                    .AddQueryFilter<ITenantScoped>(e =>
                        e.TenantId == tenantContext.Current!.TenantId);
            }
        }
    }

    public override Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        // Auto-set TenantId on new entities
        foreach (var entry in ChangeTracker.Entries<ITenantScoped>())
        {
            if (entry.State == EntityState.Added)
            {
                entry.Entity.TenantId = tenantContext.Current!.TenantId;
            }
        }

        return base.SaveChangesAsync(ct);
    }
}

Background Job Propagation

public sealed class TenantJobFilter(ITenantContextAccessor contextAccessor) : IJobFilter
{
    public void OnCreating(CreatingContext context)
    {
        // Capture tenant context when job is created
        if (contextAccessor.Current != null)
        {
            context.SetJobParameter("TenantId", contextAccessor.Current.TenantId);
        }
    }

    public void OnPerforming(PerformingContext context)
    {
        // Restore tenant context when job executes
        var tenantId = context.GetJobParameter<Guid?>("TenantId");
        if (tenantId.HasValue)
        {
            // Resolve and set tenant context
            var tenant = context.ServiceProvider
                .GetRequiredService<ITenantRepository>()
                .GetByIdAsync(tenantId.Value)
                .GetAwaiter().GetResult();

            if (tenant != null)
            {
                contextAccessor.Current = new TenantContext(tenant);
            }
        }
    }
}

HTTP Client Propagation

public sealed class TenantHeaderHandler(
    ITenantContextAccessor tenantContext) : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken ct)
    {
        if (tenantContext.Current != null)
        {
            request.Headers.Add("X-Tenant-Id", tenantContext.Current.TenantId.ToString());
        }

        return base.SendAsync(request, ct);
    }
}

// Registration
services.AddHttpClient("downstream")
    .AddHttpMessageHandler<TenantHeaderHandler>();

Distributed Tracing Integration

public static class TenantTracing
{
    public static Activity? StartTenantActivity(
        ITenantContext context,
        string operationName)
    {
        var activity = Activity.Current?.Source.StartActivity(operationName);

        if (activity != null && context.IsResolved)
        {
            activity.SetTag("tenant.id", context.TenantId.ToString());
            activity.SetTag("tenant.name", context.TenantName);
        }

        return activity;
    }
}

Best Practices

Context Propagation Best Practices:
+------------------------------------------------------------------+
| Practice                    | Benefit                            |
+-----------------------------+------------------------------------+
| AsyncLocal for thread-safe  | Works across async/await           |
| Early resolution (middleware)| Available everywhere downstream   |
| Cached tenant data          | Avoid repeated DB lookups          |
| Validation in middleware    | Fail fast on invalid tenant        |
| Include in traces/logs      | Debugging multi-tenant issues      |
| Propagate to outbound calls | Consistent context in microservices|
+-----------------------------+------------------------------------+

Anti-Patterns

Anti-PatternProblemSolution
Static tenantNot thread-safeAsyncLocal
Late resolutionMissing context in servicesMiddleware
No validationInvalid tenant accessMiddleware validation
Missing propagationLost context in background jobsJob filters
No loggingHard to debug tenant issuesInclude tenant in logs

Related Skills

  • tenancy-models - Overall architecture context
  • database-isolation - Database-level multi-tenancy
  • audit-logging - Tenant-aware audit trails

MCP Research

For current patterns:

perplexity: "multi-tenant context propagation .NET 2024" "AsyncLocal tenant context"
microsoft-learn: "multi-tenant middleware ASP.NET Core" "EF Core query filters"

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

94/100Analyzed 2/7/2026

An excellent, high-density technical reference for implementing multi-tenant context propagation in .NET. It provides clear architectural diagrams, concrete code implementations for various resolution strategies, and best practices for propagation across services and background jobs.

90
95
95
92
95

Metadata

Licenseunknown
Version-
Updated2/5/2026
Publishermajiayu000

Tags

apidatabaseobservability