0

With EF Core 7, the solution I used before doesn't seem to work for .NET 7 projects. See Apply WITH NOLOCK to every table in Entity Framework Core 3.x.

How do I implement WITH(NOLOCK) using a solution that is officially supported by the EF Core team: Interceptors

I tried to fix the error but since WithNoLockQuerySqlGenerator is using a hack to an internal EF Core API, I don't expect it to work for EF Core 7.

Sample error:

Error CS7036
There is no argument given that corresponds to the required parameter 'typeMappingSource' of 'SqlServerQuerySqlGeneratorFactory.SqlServerQuerySqlGeneratorFactory(QuerySqlGeneratorDependencies, IRelationalTypeMappingSource)'

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Edd
  • 284
  • 2
  • 11
  • 2
    Either rewrite your code so you don't need this kludge called `WITH(NOLOCK)` (it's really a bad idea to use it everywhere - for many reasons) - or if you **really must** have it, then use it by specifying the `READ UNCOMMITTED` transaction isolation level in SQL Server for your connection – marc_s Dec 10 '22 at 12:47
  • 1
    So basically you were using this https://stackoverflow.com/questions/53078435/ef-core-what-regex-can-i-use-to-replace-table-names-with-nolock-ones-in-db-int/53082098#53082098. Using internal API is not a hack (as you can see, the classes/interfaces/methods are public), just needs to be updated with EF Core versions, as you can see in the linked answer. I just haven't time recently to check it out with EFC7 and update it. In all the cases, it's much better than using regular expressions to append strings to arbitrary SQL. – Ivan Stoev Dec 10 '22 at 13:43
  • Ivan, thank you for your feedback, let's not focus on the word 'hack'. If you get the chance of verifying what needs to be updated in EF7, kindly let us know, I would really appreciate it. – Edd Dec 10 '22 at 14:08
  • @Edd There you go (linked answer updated). – Ivan Stoev Dec 10 '22 at 14:43

1 Answers1

-1

With the use of Regular Expressions, append 'WITH(NOLOCK)' via DbCommandInterceptor

Step 1 - Inherit from DbCommandInterceptor
  • WithNoLockSqlInterceptor.cs
using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;
using System.Text.RegularExpressions;

namespace Sample.Data;

public class WithNoLockSqlInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command
        , CommandEventData eventData
        , InterceptionResult<DbDataReader> result)
    {
        DoReaderExecuting(command, eventData);

        return result;
    }

    public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command
        , CommandEventData eventData
        , InterceptionResult<DbDataReader> result
        , CancellationToken cancellationToken = new())
    {
        DoReaderExecuting(command, eventData);

        return new ValueTask<InterceptionResult<DbDataReader>>(result);
    }

    private static void DoReaderExecuting(DbCommand command, CommandEventData eventData)
    {
        var commandText = command.CommandText;

        commandText = GetSqlWithNoLock(commandText);
        command.CommandText = commandText;
    }

    private static string GetSqlWithNoLock(string commandText)
    {
        const string regExPattern = @"((FROM|JOIN).* AS \[*\w+\b\]*)";
        const string newValueWithNoLock = "$1 WITH(NOLOCK)";

        if (!Regex.IsMatch(commandText, regExPattern, RegexOptions.IgnoreCase))
        {
            return commandText;
        }

        // APPEND WITH (NOLOCK)
        var sql = Regex.Replace(commandText, regExPattern, newValueWithNoLock, RegexOptions.IgnoreCase);

        return sql;
    }
}

Regular expression explained:

  1. ((FROM|JOIN).* AS \[*\w+\b\]*) <= Capture all SQL that starts with FROM or JOIN and ends with AS [TableName]
  2. $1 WITH(NOLOCK) <= Using the captured group, append WITH(NOLOCK)
Step 2 - Register WithNoLockSqlInterceptor
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace Sample.Api.Extensions;

public static class OrmStartupExtensions
{
    private static readonly WithNoLockSqlInterceptor WithNoLockSqlInterceptor = new();

    /// <summary>
    ///     Initialize Ef Core db contexts.
    /// </summary>
    public static IServiceCollection ConfigureEfCore(this IServiceCollection services, IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString(AppConstants.CoopDbConnectionStringKey);

        return services
            .AddDbContext<SampleDb>((serviceProvider, options) =>
            {
                options
                    .AllowLoggingIfDebug(serviceProvider) //// This is for troubleshooting purposes only
                    .ConfigureWarnings(w =>
                    {
                        w.Ignore(CoreEventId.RowLimitingOperationWithoutOrderByWarning);
                        w.Ignore(CoreEventId.DistinctAfterOrderByWithoutRowLimitingOperatorWarning);
                    })
                    .EnableDetailedErrors()
                    .UseSqlServer(connectionString, o =>
                    {
                        o.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery);
                    })
                    .AddInterceptors(WithNoLockSqlInterceptor); //<= Register the interceptor here.
                ;

            }, ServiceLifetime.Transient, ServiceLifetime.Transient);
    }

    /// <summary>
    /// This is for debugging purposes only and should NEVER enable in Production.
    /// </summary>
    public static DbContextOptionsBuilder AllowLoggingIfDebug(this DbContextOptionsBuilder options, IServiceProvider serviceProvider)
    {
#if DEBUG
        var logger = serviceProvider.GetRequiredService<ILogger<Startup>>();

        options
            .LogTo(message => logger.LogTrace(message), new[] { RelationalEventId.CommandExecuted })
            .EnableSensitiveDataLogging();
#endif
        return options;
    }
}
Edd
  • 284
  • 2
  • 11
  • 3
    This is a ghastly idea. Adding `NOLOCK` everywhere is a [bad habit](https://www.sentryone.com/blog/aaronbertrand/bad-habits-nolock-everywhere) and automating it even more so. Strongly consider the alternatives: use the `UNCOMMITTED` isolation level on specific transactions where you need it, or use snapshot isolation. Adding `NOLOCK` everywhere on the ORM level is telling the database server that you don't really care if you get back results that make sense at all -- possibly OK for a monitoring app, not so good if you're, say, getting customer orders. – Jeroen Mostert Dec 10 '22 at 13:17
  • 1
    In addition, your proposed solution will mangle queries that use subqueries, user-defined functions or table-valued parameters in their `FROM` clauses, and probably other stuff that I can't immediately see, and result in some potentially very obscure error messages that will be hard to track down. – Jeroen Mostert Dec 10 '22 at 13:21
  • I don't want to sound defensive here, but you must be thinking of large overly complex databases. **The question is for a very specific scenario** where there is an existing database and a similar NOLOCK approach is needed for EF7. For the sake of conversation, I wouldn't dare do this in a highly transactional eCommerce database. But for now, it satisfies the question of future proofing the existing solution. – Edd Dec 10 '22 at 13:53
  • Not exactly. My first introduction to the perils of `NOLOCK` was on a database with five tables in total -- not what I'd call complex. Unfortunately, the one table that we used `NOLOCK` on was one where it *did* matter whether or not the `GROUP BY` we did got back results. The day where we got back *nothing*, even though the table was *not* empty, was the day I learned that relying on `NOLOCK` is always worth reconsidering. If your database is *not* highly transactional, locks shouldn't be giving you trouble to the point where `NOLOCK` is needed everywhere in the first place. – Jeroen Mostert Dec 10 '22 at 14:28
  • Thank you for your feedback. I really appreciate it. I'll make sure to keep that in mind. Cheers! – Edd Dec 10 '22 at 14:35
  • Here's another thing that's bad about this "solution": it's running a regex against every SQL statement sent to the DB, which is _slooow_. The `SqlServerQuerySqlGenerator` solution is much faster because it inspects the expression tree. – Ian Kemp Jun 26 '23 at 15:11