├── .gitignore ├── Controllers └── ValuesController.cs ├── Database ├── DbSeeder.cs ├── GlobalDbContext.cs └── TenantDbContext.cs ├── Middleware └── TenantIdentifier.cs ├── Models ├── Tenant.cs └── TenantConfig.cs ├── MultiTenant.csproj ├── MultiTenant.sln ├── Program.cs ├── Properties └── launchSettings.json ├── README.md ├── Startup.cs ├── appsettings.Development.json └── appsettings.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/visualstudio 3 | 4 | ### VisualStudio ### 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Ll]og/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | #wwwroot/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 46 | 47 | # DNX 48 | project.lock.json 49 | project.fragment.lock.json 50 | artifacts/ 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # Visual Studio code coverage results 118 | *.coverage 119 | *.coveragexml 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | *.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignoreable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.jfm 201 | *.pfx 202 | *.publishsettings 203 | node_modules/ 204 | orleans.codegen.cs 205 | 206 | # Since there are multiple workflows, uncomment next line to ignore bower_components 207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 208 | #bower_components/ 209 | 210 | # RIA/Silverlight projects 211 | Generated_Code/ 212 | 213 | # Backup & report files from converting an old project file 214 | # to a newer Visual Studio version. Backup files are not needed, 215 | # because we have git ;-) 216 | _UpgradeReport_Files/ 217 | Backup*/ 218 | UpgradeLog*.XML 219 | UpgradeLog*.htm 220 | 221 | # SQL Server files 222 | *.mdf 223 | *.ldf 224 | 225 | # Business Intelligence projects 226 | *.rdl.data 227 | *.bim.layout 228 | *.bim_*.settings 229 | 230 | # Microsoft Fakes 231 | FakesAssemblies/ 232 | 233 | # GhostDoc plugin setting file 234 | *.GhostDoc.xml 235 | 236 | # Node.js Tools for Visual Studio 237 | .ntvs_analysis.dat 238 | 239 | # Visual Studio 6 build log 240 | *.plg 241 | 242 | # Visual Studio 6 workspace options file 243 | *.opt 244 | 245 | # Visual Studio LightSwitch build output 246 | **/*.HTMLClient/GeneratedArtifacts 247 | **/*.DesktopClient/GeneratedArtifacts 248 | **/*.DesktopClient/ModelManifest.xml 249 | **/*.Server/GeneratedArtifacts 250 | **/*.Server/ModelManifest.xml 251 | _Pvt_Extensions 252 | 253 | # Paket dependency manager 254 | .paket/paket.exe 255 | paket-files/ 256 | 257 | # FAKE - F# Make 258 | .fake/ 259 | 260 | # JetBrains Rider 261 | .idea/ 262 | *.sln.iml 263 | 264 | # CodeRush 265 | .cr/ 266 | 267 | # Python Tools for Visual Studio (PTVS) 268 | __pycache__/ 269 | *.pyc 270 | 271 | ### VisualStudio Patch ### 272 | build/ -------------------------------------------------------------------------------- /Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using MultiTenant.Database; 5 | using MultiTenant.Models; 6 | 7 | namespace MultiTenant.Controllers 8 | { 9 | [Route("api/[controller]")] 10 | public class ValuesController : Controller 11 | { 12 | private readonly TenantDbContext _tenantDbContext; 13 | private readonly Tenant _tenant; 14 | 15 | public ValuesController(TenantDbContext tenantDbContext, IHttpContextAccessor httpContextAccessor) 16 | { 17 | _tenantDbContext = tenantDbContext; 18 | if (httpContextAccessor.HttpContext != null) 19 | { 20 | _tenant = (Tenant) httpContextAccessor.HttpContext.Items["TENANT"]; 21 | } 22 | } 23 | 24 | // GET api/values - use X-Tenant-Guid Header (43ce6f06-a472-461f-b990-3a25c7f44b7a for TenantOne or 199b625e-6ac6-4757-a38f-9a0391866469 for TenantTwo) 25 | [HttpGet] 26 | public string Get() 27 | { 28 | _tenantDbContext.Database.EnsureCreated(); 29 | if (!_tenantDbContext.TenantConfig.Any()) 30 | { 31 | _tenantDbContext.TenantConfig.Add(new TenantConfig() {Config = $"This is the config for {_tenant.Name}. We are using the ConnectionString {_tenant.ConnectionString}"}); 32 | _tenantDbContext.SaveChanges(); 33 | } 34 | 35 | return _tenantDbContext.TenantConfig.FirstOrDefault()?.Config; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Database/DbSeeder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using MultiTenant.Models; 5 | 6 | namespace MultiTenant.Database 7 | { 8 | public class DbSeeder 9 | { 10 | private readonly GlobalDbContext _context; 11 | 12 | public DbSeeder(GlobalDbContext context) 13 | { 14 | _context = context; 15 | } 16 | 17 | public void Seed() 18 | { 19 | _context.Database.EnsureCreated(); 20 | var tenants = new List() 21 | { 22 | new Tenant() 23 | { 24 | Guid = new Guid("43ce6f06-a472-461f-b990-3a25c7f44b7a"), 25 | Name = "TenantOne", 26 | ConnectionString = "Server=(localdb)\\mssqllocaldb;Database=MT_TenantOne;Trusted_Connection=true;MultipleActiveResultSets=true" 27 | }, 28 | new Tenant() 29 | { 30 | Guid = new Guid("199b625e-6ac6-4757-a38f-9a0391866469"), 31 | Name = "TenantTwo", 32 | ConnectionString = "Server=(localdb)\\mssqllocaldb;Database=MT_TenantTwo;Trusted_Connection=true;MultipleActiveResultSets=true" 33 | } 34 | }; 35 | if (!_context.Tenants.Any()) 36 | { 37 | _context.Tenants.AddRange(tenants); 38 | _context.SaveChanges(); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Database/GlobalDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using MultiTenant.Models; 3 | 4 | namespace MultiTenant.Database 5 | { 6 | public class GlobalDbContext : DbContext 7 | { 8 | public DbSet Tenants { get; set; } 9 | 10 | public GlobalDbContext(DbContextOptions options) : base(options) 11 | { 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Database/TenantDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.EntityFrameworkCore; 3 | using MultiTenant.Models; 4 | 5 | namespace MultiTenant.Database 6 | { 7 | public class TenantDbContext : DbContext 8 | { 9 | private readonly Tenant _tenant; 10 | public DbSet TenantConfig { get; set; } 11 | 12 | public TenantDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options) 13 | { 14 | if (httpContextAccessor.HttpContext != null) 15 | { 16 | _tenant = (Tenant) httpContextAccessor.HttpContext.Items["TENANT"]; 17 | } 18 | } 19 | 20 | 21 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 22 | { 23 | optionsBuilder.UseSqlServer(_tenant.ConnectionString); 24 | base.OnConfiguring(optionsBuilder); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Middleware/TenantIdentifier.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Http; 5 | using MultiTenant.Database; 6 | 7 | namespace MultiTenant.Middleware 8 | { 9 | public class TenantIdentifier 10 | { 11 | private readonly RequestDelegate _next; 12 | 13 | public TenantIdentifier(RequestDelegate next) 14 | { 15 | _next = next; 16 | } 17 | 18 | public async Task Invoke(HttpContext httpContext, GlobalDbContext dbContext) 19 | { 20 | var tenantGuid = httpContext.Request.Headers["X-Tenant-Guid"].FirstOrDefault(); 21 | if (!string.IsNullOrEmpty(tenantGuid)) 22 | { 23 | var tenant = dbContext.Tenants.FirstOrDefault(t => t.Guid.ToString() == tenantGuid); 24 | httpContext.Items["TENANT"] = tenant; 25 | } 26 | 27 | await _next.Invoke(httpContext); 28 | } 29 | } 30 | 31 | 32 | public static class TenantIdentifierExtension 33 | { 34 | public static IApplicationBuilder UseTenantIdentifier(this IApplicationBuilder app) 35 | { 36 | app.UseMiddleware(); 37 | return app; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Models/Tenant.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace MultiTenant.Models 5 | { 6 | public class Tenant 7 | { 8 | [Key] 9 | public Guid Guid { get; set; } 10 | 11 | public string ConnectionString { get; set; } 12 | public string Name { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /Models/TenantConfig.cs: -------------------------------------------------------------------------------- 1 | namespace MultiTenant.Models 2 | { 3 | public class TenantConfig 4 | { 5 | public int Id { get; set; } 6 | public string Config { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /MultiTenant.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MultiTenant.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiTenant", "MultiTenant.csproj", "{6B679CF7-5AAD-4AA4-977F-B55B7206AF40}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {6B679CF7-5AAD-4AA4-977F-B55B7206AF40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {6B679CF7-5AAD-4AA4-977F-B55B7206AF40}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {6B679CF7-5AAD-4AA4-977F-B55B7206AF40}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {6B679CF7-5AAD-4AA4-977F-B55B7206AF40}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {2CB1E9D0-7172-4823-B4FE-230B01033B6D} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace MultiTenant 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5000/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "MultiTenant": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:5000/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP .NET Core API Multi Tenant Example Project 2 | **Description** 3 | This is a sample implementation for using multiple Tenants within an ASP .NET Core API Project. 4 | Using: 5 | * EF Core 6 | * Middleware to identify Tenants 7 | * One global DB (MT_Global) 8 | * One DB per Tenant (in this example: MT_TenantOne and MT_TenantTwo) 9 | 10 | **Example Usage** 11 | * Make a GET request to `http://localhost:5000/api/values` 12 | * Set a http header for the request named `X-Tenant-Guid` to either `43ce6f06-a472-461f-b990-3a25c7f44b7a` (TenantOne) or ` 199b625e-6ac6-4757-a38f-9a0391866469` (TenantTwo) 13 | 14 | **Example Content of MT_Global (Table: Tenants)** 15 | 16 | | GUID | ConnectionString | Name | 17 | |---|---|---| 18 | | 43ce6f06-a472-461f-b990-3a25c7f44b7a|Server=(localdb)\mssqllocaldb;Database=MT_TenantOne;Trusted_Connection=true;MultipleActiveResultSets=true|TenantOne| 19 | |199b625e-6ac6-4757-a38f-9a0391866469|Server=(localdb)\mssqllocaldb;Database=MT_TenantTwo;Trusted_Connection=true;MultipleActiveResultSets=true|TenantTwo | 20 | 21 | 22 | **Example Content of MT_TenantOne (Table: TenantConfig)** 23 | 24 | |Id|Config| 25 | |---|---| 26 | |1|This is the config for TenantOne....| 27 | -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using MultiTenant.Database; 8 | using MultiTenant.Middleware; 9 | 10 | namespace MultiTenant 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | // This method gets called by the runtime. Use this method to add services to the container. 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddSingleton(); // To access HttpContext 25 | services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); 26 | services.AddDbContext(); 27 | services.AddScoped(); 28 | 29 | 30 | services.AddMvc(); 31 | } 32 | 33 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 34 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, DbSeeder dbSeeder) 35 | { 36 | if (env.IsDevelopment()) 37 | { 38 | app.UseDeveloperExceptionPage(); 39 | dbSeeder.Seed(); 40 | } 41 | 42 | app.UseTenantIdentifier(); 43 | 44 | app.UseMvc(); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MT_Global;Trusted_Connection=true;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "IncludeScopes": false, 7 | "LogLevel": { 8 | "Default": "Debug", 9 | "System": "Information", 10 | "Microsoft": "Information" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | } 15 | } 16 | --------------------------------------------------------------------------------