除了 wh1sp3r 的回答,请参见下面的另一个拦截器,它依赖于 EF query tags 而不是线程静态变量:
public class QueryHintInterceptor : DbCommandInterceptor
{
private static readonly Regex _tableAliasRegex = new Regex("( AS [^ ]+)",
RegexOptions.Multiline | RegexOptions.IgnoreCase);
private readonly string _hintPrefix;
public QueryHintInterceptor(string hintPrefix)
{
_hintPrefix = "-- " + hintPrefix;
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command,
CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
PatchCommandText(command);
return base.ReaderExecuting(command, eventData, result);
}
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command, CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
PatchCommandText(command);
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
public override InterceptionResult<object> ScalarExecuting(DbCommand command,
CommandEventData eventData, InterceptionResult<object> result)
{
PatchCommandText(command);
return base.ScalarExecuting(command, eventData, result);
}
public override ValueTask<InterceptionResult<object>> ScalarExecutingAsync(
DbCommand command, CommandEventData eventData, InterceptionResult<object> result,
CancellationToken cancellationToken = default)
{
PatchCommandText(command);
return base.ScalarExecutingAsync(command, eventData, result, cancellationToken);
}
private void PatchCommandText(DbCommand command)
{
if (command.CommandText.StartsWith(_hintPrefix, StringComparison.Ordinal))
{
int index = command.CommandText.IndexOfAny(Environment.NewLine.ToCharArray(),
_hintPrefix.Length);
command.CommandText = _tableAliasRegex
.Replace(command.CommandText, "${0} WITH (" + command.CommandText
.Substring(_hintPrefix.Length, index - _hintPrefix.Length) + ")")
.Substring(index);
}
}
}
扩展方法:
public static class QueryHintsDbContextOptionsBuilderExtensions
{
private const string HintTag = "Use hint: ";
public static IQueryable<T> WithHint<T>(this IQueryable<T> source,
TableHint hint) =>
source.TagWith(HintTag + hint);
public static DbContextOptionsBuilder<TContext> AddQueryHints<TContext>(
this DbContextOptionsBuilder<TContext> builder)
where TContext : DbContext =>
builder.AddInterceptors(new QueryHintInterceptor(HintTag));
}
public enum TableHint
{
KeepIdentity,
KeepDefaults,
HoldLock,
Ignore_Constraints,
Ignore_Triggers,
Nolock,
NoWait,
PagLock,
ReadCommitted,
ReadCommittedLock,
ReadPast,
RepeatableRead,
RowLock,
Serializable,
Snapshot,
TabLock,
TabLockX,
UpdLock,
Xlock
}
示例用法:
await using var context = new TestDbContext(
new DbContextOptionsBuilder<TestDbContext>()
.UseSqlServer("<connection string>")
.AddQueryHints()
.LogTo(message => Console.WriteLine("EF: {0}", message))
.Options);
var result = await context.SomeEntities
.WithHint(TableHint.TabLock)
.ToListAsync();
更新
上述自定义 SQL 生成的方式不会影响.ToQueryString(),因为命令拦截器在实际 SQL 生成之后才起作用。它会导致测试和日志记录出现问题,例如在 ASP.Net 核心中。所以另一种解决方案是使用自定义QuerySqlGenerator:
public static class TableHintsDbContextOptionsBuilderExtensions
{
public static IQueryable<T> WithHint<T>(this IQueryable<T> source,
TableHint hint) =>
source.TagWith(hint.ToString());
public static DbContextOptionsBuilder UseTableHints(
this DbContextOptionsBuilder builder) =>
builder.ReplaceService<IQuerySqlGeneratorFactory,
HintTagSqlServerQuerySqlGeneratorFactory>();
}
class HintTagSqlServerQuerySqlGeneratorFactory : IQuerySqlGeneratorFactory
{
private readonly QuerySqlGeneratorDependencies dependencies;
public HintTagSqlServerQuerySqlGeneratorFactory(
QuerySqlGeneratorDependencies dependencies)
{
this.dependencies = dependencies;
}
public QuerySqlGenerator Create()
{
return new HintTagSqlServerQuerySqlGenerator(dependencies);
}
}
class HintTagSqlServerQuerySqlGenerator : SqlServerQuerySqlGenerator
{
private readonly HashSet<TableHint> tableHints = new();
public HintTagSqlServerQuerySqlGenerator(
QuerySqlGeneratorDependencies dependencies) :
base(dependencies)
{
}
protected override void GenerateTagsHeaderComment(
SelectExpression selectExpression)
{
foreach (var tag in selectExpression.Tags)
if (Enum.TryParse(typeof(TableHint), tag, out var hint))
{
tableHints.Add((TableHint)hint!);
selectExpression.Tags.Remove(tag);
}
base.GenerateTagsHeaderComment(selectExpression);
}
protected override Expression VisitTable(
TableExpression tableExpression)
{
var result = base.VisitTable(tableExpression);
if (tableHints.Count > 0)
Sql.Append($" WITH ({string.Join(", ", tableHints).ToUpperInvariant()})");
return result;
}
}
但是,警告表明
Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerQuerySqlGenerator 是一个内部 API,它支持 Entity Framework Core 基础结构,并且不受与公共 API 相同的兼容性标准的约束。它可能会在任何版本中更改或删除,恕不另行通知。