├── .gitignore
├── ASPNETCoreDapperRLS.csproj
├── ASPNETCoreDapperRLS.sln
├── Controllers
└── ProductsController.cs
├── Product.cs
├── Program.cs
├── Properties
└── launchSettings.json
├── Startup.cs
├── Tenant.cs
├── TenantMiddleware.cs
├── appsettings.Development.json
└── appsettings.json
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.*~
3 | project.lock.json
4 | .DS_Store
5 | *.pyc
6 |
7 | # Visual Studio Code
8 | .vscode
9 |
10 | # User-specific files
11 | *.suo
12 | *.user
13 | *.userosscache
14 | *.sln.docstates
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | build/
24 | bld/
25 | [Bb]in/
26 | [Oo]bj/
27 | msbuild.log
28 | msbuild.err
29 | msbuild.wrn
30 |
31 | # Visual Studio 2015
32 | .vs/
--------------------------------------------------------------------------------
/ASPNETCoreDapperRLS.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/ASPNETCoreDapperRLS.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29319.158
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ASPNETCoreDapperRLS", "ASPNETCoreDapperRLS.csproj", "{5A549639-EEA8-4B88-88D5-889C488E69BD}"
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 | {5A549639-EEA8-4B88-88D5-889C488E69BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {5A549639-EEA8-4B88-88D5-889C488E69BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {5A549639-EEA8-4B88-88D5-889C488E69BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {5A549639-EEA8-4B88-88D5-889C488E69BD}.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 = {DE42DB42-2CF5-4AE9-A2A8-E3283A2A0C94}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Controllers/ProductsController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Microsoft.Extensions.Configuration;
3 | using System.Data.SqlClient;
4 | using Dapper;
5 | using System.Collections.Generic;
6 | using System.Threading.Tasks;
7 | using System;
8 | using Microsoft.AspNetCore.Http;
9 | using System.Linq;
10 |
11 | namespace ASPNETCoreDapperRLS.Controllers
12 | {
13 | [Route("api/[controller]")]
14 | [ApiController]
15 | public class ProductsController: ControllerBase
16 | {
17 |
18 | [HttpGet]
19 | public async Task> GetAll()
20 | {
21 | var connection = (SqlConnection)HttpContext.Items["TenantConnection"]; // HttpContext not available in constructor
22 | return await connection.QueryAsync("SELECT * FROM Product");
23 | }
24 |
25 |
26 | [HttpGet("{productId}", Name = "ProductGet")]
27 | public async Task> GetById(Guid productId)
28 | {
29 | var connection = (SqlConnection)HttpContext.Items["TenantConnection"];
30 | var product = await connection.QueryFirstOrDefaultAsync("SELECT * FROM Product WHERE ProductId = @ProductId", new { ProductId = productId });
31 | if (product == null) return NotFound();
32 |
33 | return Ok(product);
34 | }
35 |
36 | [HttpPost]
37 | public async Task> Post([FromBody]Product product)
38 | {
39 | var connection = (SqlConnection)HttpContext.Items["TenantConnection"];
40 | var tenant = (Tenant)HttpContext.Items["Tenant"];
41 | product.ProductId = Guid.NewGuid();
42 | product.TenantId = tenant.TenantId;
43 | await connection.ExecuteAsync(@"INSERT INTO Product(ProductID, TenantId, ProductName, UnitPrice, UnitsInStock, ReorderLevel, Discontinued)
44 | VALUES(@ProductID, @TenantId, @ProductName, @UnitPrice, @UnitsInStock, @ReorderLevel, @Discontinued)",
45 | product);
46 |
47 | var url = Url.Link("ProductGet", new { productId = product.ProductId });
48 | return Created(url, product);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Product.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace ASPNETCoreDapperRLS
7 | {
8 | public class Product
9 | {
10 | public Guid ProductId { get; set; }
11 | public Guid TenantId { get; set; }
12 | public string ProductName { get; set; }
13 | public Decimal UnitPrice { get; set; }
14 | public Int16 UnitsInStock { get; set; }
15 | public Int16 UnitsOnOrder { get; set; }
16 | public Int16 ReorderLevel { get; set; }
17 | public bool Discontinued { get; set; }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.Hosting;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace ASPNETCoreDapperRLS
11 | {
12 | public class Program
13 | {
14 | public static void Main(string[] args)
15 | {
16 | CreateHostBuilder(args).Build().Run();
17 | }
18 |
19 | public static IHostBuilder CreateHostBuilder(string[] args) =>
20 | Host.CreateDefaultBuilder(args)
21 | .ConfigureWebHostDefaults(webBuilder =>
22 | {
23 | webBuilder.UseStartup();
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:2270",
8 | "sslPort": 44345
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "products",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "ASPNETCoreDapperRLS": {
21 | "commandName": "Project",
22 | "launchBrowser": true,
23 | "launchUrl": "products",
24 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
25 | "environmentVariables": {
26 | "ASPNETCORE_ENVIRONMENT": "Development"
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.AspNetCore.Http;
4 | using Microsoft.Extensions.Configuration;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Hosting;
7 |
8 | namespace ASPNETCoreDapperRLS
9 | {
10 | public class Startup
11 | {
12 | public Startup(IConfiguration configuration)
13 | {
14 | Configuration = configuration;
15 | }
16 |
17 | public IConfiguration Configuration { get; }
18 |
19 | // This method gets called by the runtime. Use this method to add services to the container.
20 | public void ConfigureServices(IServiceCollection services)
21 | {
22 | services.AddSingleton();
23 |
24 | services.AddControllers();
25 | }
26 |
27 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
28 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
29 | {
30 | app.UseTenant();
31 |
32 | if (env.IsDevelopment())
33 | {
34 | app.UseDeveloperExceptionPage();
35 | }
36 |
37 | app.UseHttpsRedirection();
38 |
39 | app.UseRouting();
40 |
41 | app.UseAuthorization();
42 |
43 | app.UseEndpoints(endpoints =>
44 | {
45 | endpoints.MapControllers();
46 | });
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tenant.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 |
6 | namespace ASPNETCoreDapperRLS
7 | {
8 | public class Tenant
9 | {
10 | public Guid TenantId { get; set; }
11 | public Guid APIKey { get; set; }
12 | public string TenantName { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/TenantMiddleware.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Http;
3 | using System;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using System.Data.SqlClient;
7 | using Dapper;
8 | using Microsoft.Extensions.Configuration;
9 |
10 | namespace ASPNETCoreDapperRLS
11 | {
12 | public class TenantMiddleware
13 | {
14 | private readonly RequestDelegate next;
15 |
16 | public TenantMiddleware(RequestDelegate next)
17 | {
18 | this.next = next;
19 | }
20 |
21 | public async Task Invoke(HttpContext context, IConfiguration configuration)
22 | {
23 | context.Items["TenantConnection"] = null;
24 | context.Items["Tenant"] = null;
25 | var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();
26 | if (string.IsNullOrEmpty(apiKey))
27 | {
28 | return;
29 | }
30 | Guid apiKeyGuid;
31 | if (!Guid.TryParse(apiKey, out apiKeyGuid))
32 | {
33 | return;
34 | }
35 | using (var connection = new SqlConnection(configuration["ConnectionStrings:DefaultConnection"]))
36 | {
37 | await connection.OpenAsync();
38 | var tenant = await SetTenant(connection, apiKeyGuid);
39 | context.Items["TenantConnection"] = connection;
40 | context.Items["Tenant"] = tenant;
41 | await next.Invoke(context);
42 | }
43 | }
44 |
45 | private async Task SetTenant(SqlConnection connection, Guid apiKey)
46 | {
47 | var tenant = await connection.QueryFirstOrDefaultAsync("SELECT * FROM Tenant WHERE APIKey = @APIKey", new { APIKey = apiKey });
48 | await connection.ExecuteAsync(@"EXEC dbo.sp_set_session_context @key = N'TenantId', @value = @value", new { value = tenant.TenantId });
49 | return tenant;
50 | }
51 | }
52 |
53 | public static class TenantMiddlewareExtension
54 | {
55 | public static IApplicationBuilder UseTenant(this IApplicationBuilder app)
56 | {
57 | app.UseMiddleware();
58 | return app;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "DefaultConnection": "Server=your-server;Database=ProductDb;Trusted_Connection=True;"
4 | },
5 | "Logging": {
6 | "LogLevel": {
7 | "Default": "Information",
8 | "Microsoft": "Warning",
9 | "Microsoft.Hosting.Lifetime": "Information"
10 | }
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------