23

sql

select * from table1 with(index=IX_table1_1)

Linq to sql using ado.net entity would like to write the above code. I could not find entity in particular, the use of the index hint.

linq

var querysample = from a in db.table1
select a;
7
  • you can not specify query hints in EF LINQ ... you will have to use ExecuteStoreQuery Commented Nov 6, 2011 at 23:15
  • @Sam is ExecuteStoreQuery the EF version of L2S ExecuteQuery<T>? Commented Nov 6, 2011 at 23:18
  • yeah ... except that it is 2* slower @MarcGravell Commented Nov 6, 2011 at 23:19
  • I changed the title - does that accurately represent the question ? Commented Nov 6, 2011 at 23:20
  • 1
    @Sam if only someone would write a tool that could perform these SQL-based queries without the overheads normally associated with ORMs! Commented Nov 6, 2011 at 23:21

4 Answers 4

32

Solution is simple. Let's add an Interceptor !!!

    public class HintInterceptor : DbCommandInterceptor
{
    private static readonly Regex _tableAliasRegex = new Regex(@"(?<tableAlias>AS \[Extent\d+\](?! WITH \(*HINT*\)))", RegexOptions.Multiline | RegexOptions.IgnoreCase);

    [ThreadStatic] public static string HintValue;

    public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        if (!String.IsNullOrWhiteSpace(HintValue))
        {
            command.CommandText = _tableAliasRegex.Replace(command.CommandText, "${tableAlias} WITH (*HINT*)");
            command.CommandText = command.CommandText.Replace("*HINT*", HintValue);
        }

        HintValue = String.Empty;
    }

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        if (!String.IsNullOrWhiteSpace(HintValue))
        {
            command.CommandText = _tableAliasRegex.Replace(command.CommandText, "${tableAlias} WITH (*HINT*)");
            command.CommandText = command.CommandText.Replace("*HINT*", HintValue);
        }

        HintValue = String.Empty;
    }
}

The regex could be better, i know. Let's register our Interceptor in Config class

public class PbsContextConfig : DbConfiguration
{
    public PbsContextConfig()
    {
        this.AddInterceptor(new HintInterceptor());
    }
}

Let's make nice Hint Extension for DbSet

public static class HintExtension
{
    public static DbSet<T> WithHint<T>(this DbSet<T> set, string hint) where T : class
    {
        HintInterceptor.HintValue = hint;
        return set;
    }
}

How to use ?

context.Persons.WithHint("INDEX(XI_DOWNTIME_LOCK)").Where( x => x.ID == ....

Modifications are welcomed!

Sign up to request clarification or add additional context in comments.

1 Comment

Interesting idea, but when I try it I find there's many context setup queries executed prior to my query and on the first context setup query it clears HintValue and never applies it to my query. Also, because it's executing context setup queries that are being passed through ReaderExecuting and ScalarExecuting it's very possible the "hint" will be applied to the wrong query. Not sure how to change the code to make it specific to my query, I'll post an update if I find how.
8

Adding to stop-crans answer, this works for (me on) EF Core 5.0 and supports parameter for WITH(INDEX(param)).

public class QueryHintInterceptor : DbCommandInterceptor
{
    private static readonly Regex _tableAliasRegex = new Regex(@"(FROM[\s\r\n]+\S+(?:[\s\r\n]+AS[\s\r\n]+[^\s\r\n]+)?)", 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 IQueryable<T> WithHint<T>(this IQueryable<T> source, TableHint hint, string param) =>
        source.TagWith(HintTag + hint + " (" + param+")");

    public static DbContextOptionsBuilder AddQueryHints(this DbContextOptionsBuilder builder) =>
        builder.AddInterceptors(new QueryHintInterceptor(HintTag));
}

public enum TableHint
{
    Index,
    KeepIdentity,
    KeepDefaults,
    HoldLock,
    Ignore_Constraints,
    Ignore_Triggers,
    Nolock,
    NoWait,
    PagLock,
    ReadCommitted,
    ReadCommittedLock,
    ReadPast,
    RepeatableRead,
    RowLock,
    Serializable,
    Snapshot,
    TabLock,
    TabLockX,
    UpdLock,
    Xlock
}

Comments

6

Neither L2S nor EF will provide direct support for SQL as bespoke as that (index hints, etc), although with L2S you can achieve it via ExecuteQuery<T>(...) (which takes raw TSQL). If you need that level of control, consider either stored procedures or an alternative ORM.

One problem in particular here is that query hints are pretty platform specific, but EF tries to be platform-neutral.

1 Comment

"If you need that level of control, consider either stored procedures or an alternative ORM" => Sure, but not practical. By the time you know for a fact that this is the case, it is WAY too late
6

In addition to wh1sp3r's answer see below another interceptor, which relies on EF query tags rather than thread-static variables:

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);
        }
    }
}

Extension methods:

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
}

Sample usage:

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();

Update

The above way to customize SQL generation does not affect .ToQueryString() since command interceptors work after actual SQL generation. It cause problems with tests and logging, e.g. in ASP.Net Core. So one more solution is to use custom 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;
    }
}

However, warnings says that

Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerQuerySqlGenerator is an internal API that supports the Entity Framework Core infrastructure and not subject to the same compatibility standards as public APIs. It may be changed or removed without notice in any release.

2 Comments

FYI: selectExpression.Tags.Remove(tag); should be removed. EFC6 seems to cache some parts of the queries. As a result, if tags are removed, then subsequent calls with the same query (but different parameters) results in hints not being generated at all!
I had to update my regex to be ( AS [^ \n]+) as my query had the table alias as the last component on the line. Without including newlines in the exclusion, the index hint was incorrectly added after the WHERE on the subsequent line.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.