├── .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 | --------------------------------------------------------------------------------