Skip to content

Instantly share code, notes, and snippets.

@superlloyd
Created November 20, 2019 01:51
Show Gist options
  • Save superlloyd/1946468c47e6601084f1e3fba51216e7 to your computer and use it in GitHub Desktop.
Save superlloyd/1946468c47e6601084f1e3fba51216e7 to your computer and use it in GitHub Desktop.
Extend EntityFramework with Table.With(options such as table index) and query options
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure.Interception;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Service.Data.Extensions
{
public class HintScope : IDisposable
{
/// <summary>
/// Scope to use for current query
/// </summary>
[ThreadStatic]
public static HintScope CurrentScope;
public HintScope()
{
CurrentScope = this;
}
public void AddDefaultPerformanceOptions()
{
// performance tips
// https://docs.microsoft.com/en-us/sql/t-sql/statements/set-arithabort-transact-sql?redirectedfrom=MSDN&view=sql-server-ver15
// https://blogs.msdn.microsoft.com/robinlester/2016/08/10/improving-query-performance-with-option-recompile-constant-folding-and-avoiding-parameter-sniffing-issues/
EnvironmentOptions.Add("ARITHABORT ON");
QueryOptions.Add("RECOMPILE");
}
public void Dispose()
{
IsDisposed = true;
if (CurrentScope == this)
CurrentScope = null;
}
public void Reset()
{
TableHints.Clear();
EnvironmentOptions.Clear();
QueryOptions.Clear();
}
Dictionary<string, string[]> TableHints = new Dictionary<string, string[]>();
public List<string> EnvironmentOptions { get; } = new List<string>();
public List<string> QueryOptions { get; } = new List<string>();
public bool IsDisposed { get; private set; }
// TODO: support schema
public void AddTableHint(string table, string[] hints)
{
TableHints[table] = hints;
}
public string[] GetTableHint(string table)
{
if (!TableHints.TryGetValue(table, out var results))
return null;
return results;
}
}
public static class HintExtension
{
public static DbSet<T> With<T>(this DbSet<T> set, params string[] hint) where T : class
{
var scope = HintScope.CurrentScope;
if (scope == null)
throw new InvalidOperationException("No surrounding HintScope created");
scope.AddTableHint(typeof(T).Name, hint);
return set;
}
}
/// <summary>
/// This hint interceptor need be registered in DBConfiguration to be used!
/// </summary>
public class HintInterceptor : DbCommandInterceptor
{
private static readonly Regex ReplaceTableRegex = new Regex(
@"\[(?<table>\w[\w\d]*)\] (?<alias>AS \[Extent\d+\](?<with>(?! WITH \((?<hints>\.+)\))))"
, RegexOptions.Multiline | RegexOptions.IgnoreCase
);
static void UpdateCommand(DbCommand command)
{
var scope = HintScope.CurrentScope;
if (scope == null || scope.IsDisposed)
return;
var text = command.CommandText;
var matches = ReplaceTableRegex.Matches(text);
if (matches.Count == 0 && scope.QueryOptions.Count == 0)
return;
for (int i = matches.Count - 1; i >= 0; i--)
{
var match = matches[i];
var table = match.Groups["table"].Value;
var uHints = scope.GetTableHint(table);
if (uHints == null || uHints.Length == 0)
continue;
var alias = match.Groups["alias"].Value;
var with = match.Groups["with"];
var ehints = match.Groups["hints"];
var sb = new StringBuilder();
sb.Append(" WITH (");
for (int k = 0; k < uHints.Length; k++)
{
if (k > 0)
sb.Append(", ");
sb.Append(uHints[k]);
}
if (ehints.Length > 0)
{
sb.Append(", ");
sb.Append(ehints.Value);
}
sb.Append(")");
text =
text.Substring(0, with.Index)
+ sb.ToString()
+ text.Substring(with.Index + with.Length)
;
}
if (scope.EnvironmentOptions.Count > 0 || scope.QueryOptions.Count > 0)
{
var prefix = (
from option in scope.EnvironmentOptions
select $"SET {option}\r\n"
).Aggregate((s1, s2) => s1 + s2);
var suffix = scope.QueryOptions.Count == 0
? ""
: "\r\n OPTION (" + scope.QueryOptions.Aggregate((s1, s2) => s1 + ", " + s2) + ")";
text = prefix + text + suffix;
}
command.CommandText = text;
}
public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
=> UpdateCommand(command);
public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
=> UpdateCommand(command);
public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
=> UpdateCommand(command);
public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
{
base.NonQueryExecuted(command, interceptionContext);
}
public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
base.ReaderExecuted(command, interceptionContext);
}
public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
{
base.ScalarExecuted(command, interceptionContext);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment