SkillAgentSearch skills...

Orleans.Multitenant

Secure, flexible tenant separation for Microsoft Orleans

Install / Use

/learn @VincentH-Net/Orleans.Multitenant

README

<img src="img/CSharp-Toolkit-Icon.png" alt="Backend Toolkit" width="64px" />Orleans.Multitenant

Secure, flexible tenant separation for Microsoft Orleans 10 on .NET 10

Nuget (with prereleases)<br /> (install in silo client and grain implementation projects)

(Note: this repo was transferred from Applicita to VincentH-Net on March 17, 2025 to reflect who actively maintains it)

Summary

Microsoft Orleans 10 is a great technology for building distributed, cloud-native applications. It was designed to reduce the complexity of building this type of applications for C# developers.

However, creating multi tenant applications with Orleans out of the box requires careful design, complex coding and significant testing to prevent unintentional leakage of communication or stored data across tenants. Orleans.Multitenant adds this capability to Orleans for free, as an uncomplicated, flexible and extensible API that lets developers:

  • Separate storage per tenant in any Orleans storage provider by configuring the storage provider options per tenant:<br /> Example Azure Table Storage

  • Separate communication across tenants for grain calls and streams, or allow specific access between tenants:<br /> Example Access Authorizer<br />

  • Choose where to use - for part or all of an application; combine regular stream/storage providers with multitenant ones, use tenant-specific grains/streams and tenant unaware ones. Want to add multitenant storage to an existing application? You can bring along existing grain state in the null tenant. Or add a multitenant storage provider and keep the existing non-multitenant provider as well

  • Secure against development mistakes: unauthorized access to a tenant specific grain or stream throws an UnauthorizedException, and using a non-tenant aware API on a tenant aware stream is blocked and logged.

Requirements

  • .NET 10 SDK for building and testing this repository
  • Microsoft Orleans 10 packages in consuming applications

Installation

Install the package in silo, client and grain implementation projects:

dotnet add package Orleans.Multitenant

Scope and limitations

  • Tenant id's are part of the key for a GrainId or StreamId and can be any string; the same goes for keys within a tenant. The creation and lifecycle management of tenant id's is the responsibility of the application developer; as far as Orleans.Multitenant is concerned, tenants are virtual just like grains and streams - so conceptually all possible tenant id's always exist

  • Orleans.Multitenant guards against unauthorized access from grains that have a GrainId, since only there a tenant-specific context exists (the grain key contains the tenant id). Guarding against unauthorized tenant access that is not initiated from a tenant grain (e.g. when using a cluster client in an ASP.NET controller, or in a stateless worker grain or a grain service) is the responsibility of the application developer, since what constitutes a tenant context there is application specific

  • Only IGrainWithStringKey grains can be tenant specific

Usage

All multitenant features can be independently enabled and configured at silo startup, with the ISiloBuilder AddMultitenant* extension methods. See the inline documentation for more details on how to use the APIs that are mentioned in this readme.

Add multitenant storage

To add tenant storage separation to any Orleans storage provider, use AddMultitenantGrainStorage and AddMultitenantGrainStorageAsDefault on an ISiloBuilder or IServiceCollection:

siloBuilder
.AddMultitenantGrainStorageAsDefault<AzureTableGrainStorage, AzureTableStorageOptions, AzureTableGrainStorageOptionsValidator>(
    (silo, name) => silo.AddAzureTableGrainStorage(name, options =>
        options.ConfigureTableServiceClient(tableStorageConnectionString)),
        // Called during silo startup, to ensure that any common dependencies
        // needed for tenant-specific provider instances are initialized

    configureTenantOptions: (options, tenantId) => {
        options.ConfigureTableServiceClient(tableStorageConnectionString);
        options.TableName = $"OrleansGrainState{tenantId}";
    }   // Called on the first grain state access for a tenant in a silo,
        // to initialize the options for the tenant-specific provider instance
        // just before it is instantiated
 )

Customize storage provider constructor parameters

By default, the parameters passed into the storage provider instance for a tenant are the tenant provider name (which contains the tenant Id) and the tenant options. Some storage providers may expect a different (wrapper) type for the options, or you may want to pass in additional parameters (e.g. ClusterOptions).

To do this, you can pass in an optional GrainStorageProviderParametersFactory<TGrainStorageOptions>? getProviderParameters parameter.

Example: .NET Aspire with Azure Blob Storage for grain state

If you are using the .NET Aspire Orleans Integration to configure the default grain storage for the silo like this:

// Add the resources which you will use for Orleans clustering and
// grain state storage.
var storage = builder.AddAzureStorage("orleans").RunAsEmulator();
var clusteringTable = storage.AddTables("clustering");
var grainStorage = storage.AddBlobs("grain-state");

// Add the Orleans resource to the Aspire DistributedApplication
// builder, then configure it with Azure Table Storage for clustering.
var orleans = builder.AddOrleans("default")
                     .WithClustering(clusteringTable);
// Note that we don't call WithGrainStorage here, since Multitenant grain storage is added in the silo code
// where we use the blob client that Aspire configures from above grainStorage resource, which is passed to apis using WithReference below

var yourProject = builder
    .AddProject<Projects. ...>("...")
    .WithReference(orleans)
    .WithReference(grainStorage) // We use this instead of .WithGrainStorage(grainStorage) to pass only the blob client to the Orleans silo
    .WaitFor(clusteringTable)
    .WaitFor(grainStorage);

then you can use the following code to configure the tenant-specific storage provider instances for Azure Blob Storage:

// Use Aspire to configure the clients for clustering and grain state
builder.AddKeyedAzureTableClient("clustering");
builder.AddKeyedAzureBlobClient("grain-state");

builder.UseOrleans(
    silo => silo
    .AddMultitenantGrainStorageAsDefault<AzureBlobGrainStorage, AzureBlobStorageOptions, AzureBlobStorageOptionsValidator>(
        // Called during silo startup, to ensure that any common dependencies
        // needed for tenant-specific provider instances are initialized
        (silo, name) => silo.AddAzureBlobGrainStorage(name, (OptionsBuilder<AzureBlobStorageOptions> options) =>
            options.Configure<IServiceProvider>((options, services) => 
                options.BlobServiceClient = services.GetRequiredKeyedService<BlobServiceClient>("grain-state")) // Use the BlobServiceClient registered by AddKeyedAzureBlobClient above
        ),

        // Called on the first grain state access for a tenant in a silo,
        // to initialize the options for the tenant-specific provider instance just before it is instantiated
        configureTenantOptions: (options, tenantId) =>
        {
            #pragma warning disable CA1308 // Normalize strings to uppercase
            options.ContainerName += "-" + tenantId.ToLowerInvariant();
            #pragma warning restore CA1308 // Normalize strings to uppercase
        },

        getProviderParameters: (services, providerName, tenantProviderName, options) =>
        {
            options.BlobServiceClient = services.GetRequiredKeyedService<BlobServiceClient>("grain-state"); // Use the BlobServiceClient registered by AddKeyedAzureBlobClient above
            return [options, options.BuildContainerFactory(services, options)];
        }
    )
);

Note that you do not need to include the tenantProviderName in the returned provider parameters; it is added automatically.

The parameters passed to getProviderParameters allow to access relevant services from DI to retrieve additional provider parameters, if needed.

Example: ADO.NET for grain state

E.g. the Orleans ADO.NET storage provider constructor expects an IOptions<AdoNetGrainStorageOptions> instead of an AdoNetGrainStorageOptions. You can use getProviderParameters to wrap the AdoNetGrainStorageOptions in an IOptions<AdoNetGrainStorageOptions>:

.AddMultitenantGrainStorageAsDefault<AdoNetGrainStorage, AdoNetGrainStorageOptions, AdoNetGrainStorageOptionsValidator>(
    (silo, name) => silo.AddAdoNetGrainStorage(name, options => options.ConnectionString = sqlConnectionString),

    configureTenantOptions: (options, tenantId) => options.ConnectionString = sqlConnectionString.Replace("[DatabaseName]", tenantId, StringComparison.Ordinal),

    getProviderParameters: (services, providerName, tenantProviderName, options) => [Options.Create(options)]
)

Add multitenant streams

To configure a silo to use a specific stream provider type as a named stream provider with tenant separation, use AddMultitenantStreams. Any Orleans stream provider can be used:

.AddMultitenantStreams(
    "provider_name", (silo, name) => silo
    .AddMemoryStreams<DefaultMemoryMessageBodySerializer>(name)
    .AddMemoryGrainStorage(name)
 )

Both implicit and explicit stream subscriptions are supported.

Add multitenant communication

View on GitHub
GitHub Stars95
CategoryDevelopment
Updated8d ago
Forks14

Languages

C#

Security Score

100/100

Audited on Mar 31, 2026

No findings