Created
January 26, 2023 03:06
-
-
Save jklemmack/b57e6667ccfe8b6086076178f38b6ab1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.SqlServer.TransactSql.ScriptDom; | |
using ServiceStack.OrmLite.SqlServer; | |
using System; | |
using System.Collections.Generic; | |
using System.Data; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace ServiceStack.OrmLite.Tenant | |
{ | |
public static class OrmLiteTenantReadExpressionsApi | |
{ | |
public static SqlExpression<T> WithTenant<T>(this SqlExpression<T> expression, long? tenantId) | |
{ | |
if (tenantId.HasValue) | |
expression.AddTag($"TenantId:{tenantId}"); | |
return expression; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.SqlServer.TransactSql.ScriptDom; | |
using ServiceStack.OrmLite.SqlServer; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
using System.Threading.Tasks; | |
namespace ServiceStack.OrmLite.Tenant | |
{ | |
public class SqlServer2019TenantOrmLiteDialectProvider : SqlServer2019OrmLiteDialectProvider | |
{ | |
public Dictionary<string, TableMetadata> TableMetadata { get; private set; } = new Dictionary<string, TableMetadata>(); | |
public SqlServer2019TenantOrmLiteDialectProvider() | |
{ | |
} | |
public new static SqlServer2019TenantOrmLiteDialectProvider Instance = new(); | |
public override string ToSelectStatement(QueryType queryType, ModelDefinition modelDef, string selectExpression, string bodyExpression, string orderByExpression = null, int? offset = null, int? rows = null, ISet<string> tags = null) | |
{ | |
// Default scenario | |
string sql = base.ToSelectStatement(queryType, modelDef, selectExpression, bodyExpression, orderByExpression, offset, rows, tags); | |
var tenantTag = tags.FirstOrDefault(t => t.StartsWith("TenantId:")); | |
if (tenantTag == null) | |
{ | |
return sql; | |
} | |
if (long.TryParse(tenantTag.SplitOnFirst(":")[1], out long tenantId)) | |
{ | |
tags.Remove(tenantTag); // Remove this, since we used it magical like | |
sql = base.ToSelectStatement(queryType, modelDef, selectExpression, bodyExpression, orderByExpression, offset, rows, tags); | |
sql = MakeSqlTenantAware(sql, tenantId); | |
return sql; | |
} | |
return sql; | |
} | |
public string MakeSqlTenantAware(string sql, long? tenantId) | |
{ | |
if (tenantId == null) return sql; | |
var parser = new TSql160Parser(true, Microsoft.SqlServer.TransactSql.ScriptDom.SqlEngineType.All); | |
using (var sr = new StringReader(sql)) | |
{ | |
var fragment = parser.Parse(sr, out var errors); | |
fragment.Accept(new CustomVisitor(Instance.TableMetadata, tenantId.Value)); | |
var sb = new StringBuilder(); | |
fragment.ScriptTokenStream.Each(token => sb.Append(token.Text)); | |
return sb.ToString(); | |
} | |
} | |
class CustomVisitor : TSqlFragmentVisitor | |
{ | |
long? tenantId = null; | |
Dictionary<string, TableMetadata> metadata; | |
public CustomVisitor(Dictionary<string, TableMetadata> metadata, long? tenantId) | |
{ | |
this.metadata = metadata; | |
this.tenantId = tenantId; | |
} | |
public override void Visit(NamedTableReference node) | |
{ | |
var token = node.ScriptTokenStream[node.FirstTokenIndex]; | |
var tableName = node.SchemaObject.Identifiers[0].Value; | |
var alias = node.Alias?.Value ?? tableName; | |
var metaData = GetTableMetadata(tableName); | |
if (metaData?.IsTenantAware ?? false == true) | |
{ | |
token.Text = $"(SELECT * FROM \"{tableName}\" WHERE TenantId = {tenantId})"; | |
if (node.Alias?.Value == null) | |
token.Text += $" AS \"{alias}\""; | |
} | |
base.Visit(node); | |
} | |
TableMetadata GetTableMetadata(string table) | |
{ | |
var filtered = table.ToLower().Replace("\"", ""); | |
if (metadata.TryGetValue(filtered, out var value)) | |
return value; | |
return null; | |
} | |
} | |
} | |
public class TableMetadata | |
{ | |
public string Schema { get; set; } | |
public string TableName { get; set; } | |
public bool IsTenantAware { get; set; } | |
} | |
public static class SqlServer2019TenantDialect | |
{ | |
public static IOrmLiteDialectProvider Provider => SqlServer2019TenantOrmLiteDialectProvider.Instance; | |
public static SqlServer2019TenantOrmLiteDialectProvider Instance => SqlServer2019TenantOrmLiteDialectProvider.Instance; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A ServiceStack dialect provider, based on SqlServer2019, which supports automatic filtering by, in this case, TenantId.
Use like
Db.From<T>.WithTenant(5);
to do runtime replacement to inject Tenant level filtering. This is meant to be a base, and injected into a broader query pipeline, possibly pulling TenantId from context, or specific tables from DB metadata.