Skip to content

A small library to demonstrate multi tenant. One instance of a web application can serve multiple tenants with multiple databases.

License

Notifications You must be signed in to change notification settings

TownSuite/TownSuite.MultiTenant

Repository files navigation

nuget package

Build the project in Release mode. It will produce a nuget package in the bin folder. Upload it to your nuget repository or point the nuget source at the folder. Have fun.

dotnet add package "TownSuite.MultiTenant" --source "C:\the\folder\with\the\nuget\package\TownSuite.MultiTenant.nupkg"

Data Formats

Connection string tenant naming convention:

{tenant/alias}_{name/dbType}

Remove the {} and replace the tenant and connectionstring with the real values.

DI setup

program.cs add services

services.AddSingleton<TownSuite.MultiTenant.Settings>((s) => new TownSuite.MultiTenant.Settings()
{
    return s.GetService<IConfiguration>().GetSection("TenantSettings").Get<Settings>(),
});
services.AddSingleton<IUniqueIdRetriever, SqlUniqueIdRetriever>();
services.AddSingleton<TsWebClient>((s) =>
{
    var config = s.GetService<TownSuite.MultiTenant.Settings>();
    var webClient = new TsWebClient(new HttpClient(), userAgent: config.UserAgent);
    return webClient;
});
services.AddSingleton<IConfigReader, HttpConfigReader>();
services.AddSingleton<TenantResolver>();

AppSettingsConfigReader - Example

Read tenant information from a appsettings.json file.

{
  "ConnectionStrings": {
    "tenant1_app1": "Server=tcp:myserver.example.townsuite.com,1433;Initial Catalog=mydatabase1;Persist Security Info=False;User ID=myuser;Password=mypassword;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;",
    "alias1_app1": "Server=tcp:myserver.example.townsuite.com,1433;Initial Catalog=mydatabase1;Persist Security Info=False;User ID=myuser;Password=mypassword;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;",
    "alias2_app1": "Server=tcp:myserver.example.townsuite.com,1433;Initial Catalog=mydatabase1;Persist Security Info=False;User ID=myuser;Password=mypassword;MultipleActiveResultSets=False;",
    "tenant1_app2": "Server=tcp:myserver.example.townsuite.com,1433;Initial Catalog=second1;Persist Security Info=False;User ID=myuser;Password=mypassword;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;",
    "a.site.example.townsuite.com_app1": "Server=tcp:myserver.example.townsuite.com,1433;Initial Catalog=mydatabase1;Persist Security Info=False;User ID=myuser;Password=mypassword;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;",
    "tenant2_app1": "Server=tcp:myserver.example.townsuite.com,1433;Initial Catalog=mydatabase2;Persist Security Info=False;User ID=myuser;Password=mypassword;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
  },
  "TenantSettings": {
    // AppSettingsConfigReader supports only 1 record in the ConfigPairs
    "ConfigPairs": [
        {
            "Id": 1,
            "DecryptionKey": "PLACEHOLDER",
            "UniqueIdDbPattern": ".*_Web",
            "SqlUniqueIdLookup": "SELECT Top 1 Id FROM ExampleTable"
        }
    ],
    "UserAgent": "TownSuite-MultiTenant-Console Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/115.0"
  }
}

The UniqueIdDbPattern will be used to compare against {tenant/alias}. The current implementation assummed with the unique id of a tenant is stored in one of the databases.

asp.net core example reading settings from appsetting.json

using Dapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using TownSuite.MultiTenant;

namespace ExampleApplication.Controllers;

[ApiController]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
    private readonly TenantResolver _resolver;

    public ExampleController(TenantResolver resolver)
    {
        _resolver = resolver;
    }

    [HttpGet()]
    public async Task<IActionResult> Get(string tenantId)
    {
        var tenant = await _resolver.Resolve(tenantId);

        await using var conn = new SqlConnection(tenant.Connections["app1"]);
        await conn.OpenAsync();
        var data = await conn.QueryAsync("SELECT * FROM exampleTable2");

        return Ok(data);
    }
}

HttpConfigReader - Example

Settings that are required to make an http call and read the output

"TenantSettings": {
    "ConfigPairs": [
        {
            "Id": 1,
            "ConfigReaderUrls": [
                "http://localhost:5000/api/ConfigReader"
            ],
            "ConfigReaderUrlBearerToken": "PLACEHOLDER",
            "DecryptionKey": "PLACEHOLDER",
            "UniqueIdDbPattern": ".*_Web",
            "SqlUniqueIdLookup": "SELECT Top 1 Id FROM ExampleTable"
        }
    ],
    "UserAgent": "TownSuite-MultiTenant-Console Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/115.0"
}

The expected data format from an http service is json that matches the below example.

[
    {
        "tenantId": "tenant1",
        "connections": [
            {
                "key": "app1",
                "value": "CONNECTIONSTRING PLACEHOLDER"
            },
            {
                "key": "app2",
                "value": "CONNECTIONSTRING PLACEHOLDER"
            }
        ]
    },
    {
        "tenantId": "tenant2",
        "connections": [
            {
                "key": "app1",
                "value": "CONNECTIONSTRING PLACEHOLDER"
            },
            {
                "key": "app2",
                "value": "CONNECTIONSTRING PLACEHOLDER"
            }
        ]
    }
]

asp.net core example

using Dapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using TownSuite.MultiTenant;

namespace ExampleApplication.Controllers;

[ApiController]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
    private readonly TenantResolver _resolver;

    public ExampleController(TenantResolver resolver)
    {
        _resolver = resolver;
    }

    [HttpGet()]
    public async Task<IActionResult> Get(string tenantId)
    {
        var tenant = await _resolver.Resolve(tenantId);

        await using var conn = new SqlConnection(tenant.Connections["app1"]);
        await conn.OpenAsync();
        var data = await conn.QueryAsync("SELECT * FROM exampleTable2");

        return Ok(data);
    }
}

Worker service connecting to all tenants

use an extension method to create connections

public static class TenantExtensions
{
    public static DbConnection CreateConnection(this Tenant tenant, string appName)
    {
        return new SqlConnection(tenant.Connections[appName]);
    }
}

Worker background service looping through all tenants and reading data.

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly TenantResolver _resolver;

    public Worker(ILogger<Worker> logger, TenantResolver resolver)
    {
        _logger = logger;
        _resolver= resolver;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        const int oneHour = 1000 * 60 * 60;
        
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);

            await _resolver.ResolveAll();

            foreach (var tenant in _resolver.Tenants)
            {
                // example: do stuff with tenants app1 databases
                await using var conn =  tenant.CreateConnection("app1");
                await conn.OpenAsync();
                var data = await conn.QueryAsync("SELECT * FROM exampleTable2");
            }

            await Task.Delay(oneHour, stoppingToken);
        }
    }
}

About

A small library to demonstrate multi tenant. One instance of a web application can serve multiple tenants with multiple databases.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages