Skip to main content
Version: 2.4.1

Finbuckle

Overview​

RCommon.Finbuckle integrates Finbuckle.MultiTenant with RCommon's persistence layer. Finbuckle handles all tenant resolution — inspecting HTTP headers, route values, host names, JWT claims, or any custom strategy you configure. RCommon's side of the integration is a single adapter class, FinbuckleTenantIdAccessor<TTenantInfo>, which implements ITenantIdAccessor by reading the tenant ID from Finbuckle's IMultiTenantContextAccessor<TTenantInfo>.

When you call WithMultiTenancy<FinbuckleMultiTenantBuilder<TTenantInfo>> at startup, FinbuckleMultiTenantBuilder<TTenantInfo> replaces the default NullTenantIdAccessor registration with FinbuckleTenantIdAccessor<TTenantInfo>. From that point on every repository operation that touches an IMultiTenant entity is automatically scoped to whichever tenant Finbuckle has resolved for the current request.

How tenant resolution flows​

  1. An HTTP request arrives. ASP.NET Core runs the Finbuckle middleware which identifies the tenant using the strategy you configured (header, route segment, hostname, etc.) and sets IMultiTenantContextAccessor<TTenantInfo>.MultiTenantContext.
  2. A repository method executes. It calls ITenantIdAccessor.GetTenantId().
  3. FinbuckleTenantIdAccessor<TTenantInfo> reads MultiTenantContext.TenantInfo.Id and returns it.
  4. The repository appends a tenant filter to the query or stamps the entity's TenantId property.

Installation​

NuGet Package
dotnet add package RCommon.Finbuckle

This package depends on RCommon.MultiTenancy and Finbuckle.MultiTenant.Core, which are pulled in automatically.

Configuration​

Basic setup​

Configure Finbuckle using its own API first, then wire it into RCommon with WithMultiTenancy<T>:

using RCommon;
using RCommon.Finbuckle;
using Finbuckle.MultiTenant;

var builder = WebApplication.CreateBuilder(args);

// 1. Configure Finbuckle tenant resolution.
builder.Services.AddMultiTenant<TenantInfo>()
.WithHeaderStrategy("X-Tenant-Id")
.WithConfigurationStore();

// 2. Configure RCommon, including the Finbuckle multi-tenancy adapter.
builder.Services.AddRCommon(config =>
{
config
.WithClaimsAndPrincipalAccessorForWeb()
.WithPersistence<EFCorePerisistenceBuilder>(ef =>
{
ef.AddDbContext<AppDbContext>(
"App",
options => options.UseSqlServer(
builder.Configuration.GetConnectionString("Default")));

ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "App");
})
.WithMultiTenancy<FinbuckleMultiTenantBuilder<TenantInfo>>(mt =>
{
// FinbuckleTenantIdAccessor<TenantInfo> is registered automatically.
});
});

var app = builder.Build();

// 3. Add the Finbuckle middleware so tenant resolution runs on every request.
app.UseMultiTenant();

app.MapControllers();
app.Run();

Custom TenantInfo​

Finbuckle requires a class that implements ITenantInfo. You can use the built-in TenantInfo or define your own:

using Finbuckle.MultiTenant.Abstractions;

public class AppTenantInfo : ITenantInfo
{
public string? Id { get; set; }
public string? Identifier { get; set; }
public string? Name { get; set; }

// Custom properties for your application.
public string? ConnectionString { get; set; }
public string? Theme { get; set; }
}

Pass your custom type as the generic argument everywhere:

builder.Services.AddMultiTenant<AppTenantInfo>()
.WithRouteStrategy("tenant")
.WithEFCoreStore<TenantDbContext, AppTenantInfo>();

builder.Services.AddRCommon(config =>
{
config.WithMultiTenancy<FinbuckleMultiTenantBuilder<AppTenantInfo>>(mt => { });
});

Tenant resolution strategies​

Finbuckle ships with several built-in strategies. Choose the one that fits your application's URL scheme:

// Resolve from a request header (e.g. X-Tenant-Id: acme).
builder.Services.AddMultiTenant<TenantInfo>()
.WithHeaderStrategy("X-Tenant-Id")
.WithConfigurationStore();

// Resolve from a route parameter (e.g. /acme/orders).
builder.Services.AddMultiTenant<TenantInfo>()
.WithRouteStrategy("tenant")
.WithConfigurationStore();

// Resolve from the hostname (e.g. acme.myapp.com).
builder.Services.AddMultiTenant<TenantInfo>()
.WithHostStrategy("__tenant__.*")
.WithConfigurationStore();

// Resolve from a JWT claim.
builder.Services.AddMultiTenant<TenantInfo>()
.WithClaimStrategy("tenantid")
.WithConfigurationStore();

Tenant stores​

Finbuckle resolves tenant configuration from a store. Common options:

// Store tenant definitions in appsettings.json under "Finbuckle:MultiTenant:Stores:ConfigurationStore".
builder.Services.AddMultiTenant<TenantInfo>()
.WithHeaderStrategy("X-Tenant-Id")
.WithConfigurationStore();

// Store tenants in an EF Core database table.
builder.Services.AddMultiTenant<TenantInfo>()
.WithHeaderStrategy("X-Tenant-Id")
.WithEFCoreStore<TenantDbContext, TenantInfo>();

// In-memory store — useful for testing.
builder.Services.AddMultiTenant<TenantInfo>()
.WithHeaderStrategy("X-Tenant-Id")
.WithInMemoryStore(options =>
{
options.Tenants.Add(new TenantInfo
{
Id = "tenant-1",
Identifier = "acme",
Name = "Acme Corp"
});
});

Usage​

Defining tenant-scoped entities​

Implement IMultiTenant on entities that should be isolated per tenant:

using RCommon.Entities;

public class Order : BusinessEntity<Guid>, IMultiTenant
{
public string CustomerName { get; set; } = string.Empty;
public decimal Total { get; set; }
public OrderStatus Status { get; set; }

// Populated automatically by the repository.
public string? TenantId { get; set; }
}

Reading tenant-scoped data​

Inject the repository and query as normal. Tenant filtering is applied transparently:

public class OrderService
{
private readonly IGraphRepository<Order> _orders;

public OrderService(IGraphRepository<Order> orders)
{
_orders = orders;
}

public async Task<ICollection<Order>> GetPendingOrdersAsync(CancellationToken ct)
{
// Only returns orders for the tenant resolved by Finbuckle on this request.
return await _orders.FindAsync(o => o.Status == OrderStatus.Pending, ct);
}
}

Writing tenant-scoped data​

public async Task<Guid> PlaceOrderAsync(PlaceOrderRequest request, CancellationToken ct)
{
var order = new Order
{
CustomerName = request.CustomerName,
Total = request.Total,
Status = OrderStatus.New
// TenantId is not set here; the repository stamps it automatically.
};

await _orders.AddAsync(order, ct);
return order.Id;
}

Accessing the current tenant directly​

If you need the tenant information outside of a repository, inject Finbuckle's context accessor:

using Finbuckle.MultiTenant.Abstractions;

public class TenantAwareService
{
private readonly IMultiTenantContextAccessor<TenantInfo> _tenantAccessor;

public TenantAwareService(IMultiTenantContextAccessor<TenantInfo> tenantAccessor)
{
_tenantAccessor = tenantAccessor;
}

public string? GetCurrentTenantName()
{
return _tenantAccessor.MultiTenantContext?.TenantInfo?.Name;
}
}

Or use RCommon's ITenantIdAccessor when you only need the ID:

using RCommon.Security.Claims;

public class AuditService
{
private readonly ITenantIdAccessor _tenantIdAccessor;

public AuditService(ITenantIdAccessor tenantIdAccessor)
{
_tenantIdAccessor = tenantIdAccessor;
}

public void LogAction(string action)
{
var tenantId = _tenantIdAccessor.GetTenantId();
// Use tenantId in your audit log entry.
}
}

API Summary​

TypeDescription
FinbuckleMultiTenantBuilder<TTenantInfo>Registers FinbuckleTenantIdAccessor and implements IMultiTenantBuilder; use as the type argument to WithMultiTenancy<T>
IFinbuckleMultiTenantBuilder<TTenantInfo>Marker interface extending IMultiTenantBuilder; scoped to TTenantInfo : ITenantInfo
FinbuckleTenantIdAccessor<TTenantInfo>Implements ITenantIdAccessor by reading IMultiTenantContextAccessor<TTenantInfo>.MultiTenantContext.TenantInfo.Id
ITenantIdAccessorRCommon's abstraction; GetTenantId() returns the current tenant ID or null
RCommonRCommon