Skip to content

Instantly share code, notes, and snippets.

@cobysy
Created January 23, 2024 10:16
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 class QueryInterceptor : DbCommandInterceptor
{
public const string TagPrefix = "QueryInterceptor:";
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 QueryInterceptorHintsArgs? args)
{
var sqlLines = commandText.ReplaceLineEndings().Split(separator: Environment.NewLine);
// search for the line starting with: -- QueryInterceptor:
var prefix = "-- " + TagPrefix;
foreach (var sqlLine in sqlLines)
{
if (sqlLine.StartsWith(
value: prefix,
comparisonType: StringComparison.Ordinal))
{
var json = sqlLine[prefix.Length..];
args = Json.Json.Deserialize<QueryInterceptorHintsArgs>(json: json)!;
return true;
}
}
args = null;
return false;
}
private static string ApplyHints(
string commandText,
QueryInterceptorHintsArgs 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 static class QueryInterceptorHelper
{
public static IQueryable<T> InterceptWith<T>(
this IQueryable<T> source,
QueryInterceptorHintsArgs hintsArgs)
{
return source
.TagWith(tag: QueryInterceptor.TagPrefix + Json.Json.Serialize(value: hintsArgs));
}
}
public record QueryInterceptorHintsArgs(
string Entity,
string Hints);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment