Last active
November 25, 2024 10:18
-
-
Save cobysy/3dd87dc9bf1ea5a4018c650e987d283c to your computer and use it in GitHub Desktop.
QueryInterceptor for Ef Core to apply SQL hints (Sql Server)
This file contains hidden or 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
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