Skip to content

Instantly share code, notes, and snippets.

@jklemmack
Created January 26, 2023 03:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jklemmack/b57e6667ccfe8b6086076178f38b6ab1 to your computer and use it in GitHub Desktop.
Save jklemmack/b57e6667ccfe8b6086076178f38b6ab1 to your computer and use it in GitHub Desktop.
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;
}
}
}
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;
}
}
@jklemmack
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment