Skip to content

Instantly share code, notes, and snippets.

@cobysy
Last active November 25, 2024 10:18
Show Gist options
  • Save cobysy/3dd87dc9bf1ea5a4018c650e987d283c to your computer and use it in GitHub Desktop.
Save cobysy/3dd87dc9bf1ea5a4018c650e987d283c to your computer and use it in GitHub Desktop.
QueryInterceptor for Ef Core to apply SQL hints (Sql Server)
public static class QueryTableHintInterceptorExtensions
{
public static IQueryable<T> WithTableHint<T>(
this DbSet<T> source,
string? hints) where T : class
{
if (hints == null)
{
return source;
}
return source
.TagWith(
tag: QueryTableHintInterceptor.TagPrefix + System.Text.Json.JsonSerializer.Serialize(
value: new TableHintsArgs(
$"[{source.EntityType.GetSchema()}].[{source.EntityType.GetTableName()}]",
hints)));
}
}
public class QueryTableHintInterceptor : DbCommandInterceptor
{
public const string TagPrefix = "QueryTableHintInterceptor:";
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
ManipulateCommand(command: command);
return result;
}
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
ManipulateCommand(command: command);
return new ValueTask<InterceptionResult<DbDataReader>>(result: result);
}
internal static void ManipulateCommand(
IDbCommand command)
{
if (command.Connection is not SqlConnection)
{
return;
}
var commandText = command.CommandText;
if (!TryGetQueryInterceptorHintArgs(
commandText: commandText,
args: out var hint))
{
return;
}
commandText = ApplyHints(
commandText: commandText,
args: hint!);
command.CommandText = commandText;
}
private static bool TryGetQueryInterceptorHintArgs(
string commandText,
out TableHintsArgs? args)
{
var sqlLines = commandText.ReplaceLineEndings().Split(separator: Environment.NewLine);
// search for the line starting with: -- QueryTableHintInterceptor:
var prefix = "-- " + TagPrefix;
foreach (var sqlLine in sqlLines)
{
if (sqlLine.StartsWith(
value: prefix,
comparisonType: StringComparison.Ordinal))
{
var json = sqlLine[prefix.Length..];
args = System.Text.Json.JsonSerializer.Deserialize<TableHintsArgs>(json: json)!;
return true;
}
}
args = null;
return false;
}
private static string ApplyHints(
string commandText,
TableHintsArgs args)
{
var fromPartRegex = new Regex(
pattern: $@"^(.*?FROM.*?{args.Entity}.*?)(\s.*?AS.*?)?$",
options: RegexOptions.Multiline);
var match = fromPartRegex.Match(input: commandText);
if (match.Success)
{
var oldValue = match.Groups[groupnum: 0].Value;
var newValue = match.Groups[groupnum: 0].Value + $" WITH ({args.Hints})";
commandText = commandText
.Replace(
oldValue: oldValue,
newValue: newValue,
comparisonType: StringComparison.Ordinal);
}
return commandText;
}
}
public record TableHintsArgs(
string Entity,
string Hints);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment