Marketplace

tenant-context-propagation

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

allowed_tools: Read, Glob, Grep, Task, mcp__perplexity__search, mcp__perplexity__reason, mcp__microsoft-learn__microsoft_docs_search, mcp__microsoft-learn__microsoft_docs_fetch

$ Installer

git clone https://github.com/melodic-software/claude-code-plugins /tmp/claude-code-plugins && cp -r /tmp/claude-code-plugins/plugins/saas-patterns/skills/tenant-context-propagation ~/.claude/skills/claude-code-plugins

// tip: Run this command in your terminal to install the skill


name: tenant-context-propagation description: Tenant context resolution and propagation patterns for multi-tenant applications. Covers middleware, headers, claims, and distributed tracing. allowed-tools: Read, Glob, Grep, Task, mcp__perplexity__search, mcp__perplexity__reason, mcp__microsoft-learn__microsoft_docs_search, mcp__microsoft-learn__microsoft_docs_fetch

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"

Repository

melodic-software
melodic-software
Author
melodic-software/claude-code-plugins/plugins/saas-patterns/skills/tenant-context-propagation
3
Stars
0
Forks
Updated3d ago
Added1w ago