Core SQL transformation engine for the EFCore.NoLock ecosystem. Provides shared, ORM-agnostic WITH (NOLOCK) hint injection using the official Microsoft.SqlServer.TransactSql.ScriptDom parser.
$ dotnet add package EFCore.NoLock.CoreEFCore.NoLock is a professional extension that allows you to apply the WITH (NOLOCK) table hint to specific LINQ queries using a fluent .WithNoLock() API. Supports both Entity Framework Core and LinqToDB.
Unlike simple regex-based solutions, this library uses the official Microsoft.SqlServer.TransactSql.ScriptDom parser to safely modify the SQL syntax tree. This ensures that hints are applied correctly even in complex queries involving Joins, Subqueries, or CTEs, without breaking the SQL structure.
ScriptDom to parse and reconstruct SQL, ensuring 100% valid syntax.ConcurrentDictionary) to avoid re-parsing identical queries. The overhead is negligible after the first execution..WithNoLock() extension method for IQueryable.ToListAsync, FirstOrDefaultAsync, and other async operations.| Package | Description | NuGet |
|---|---|---|
EFCore.NoLock | EF Core interceptor | |
EFCore.NoLock.LinqToDb | LinqToDB interceptor | |
EFCore.NoLock.Core | Shared engine (auto-installed) |
For Entity Framework Core:
dotnet add package EFCore.NoLock
For LinqToDB:
dotnet add package EFCore.NoLock.LinqToDb
Both packages automatically include
EFCore.NoLock.Coreas a transitive dependency.
Add the WithNoLockInterceptor to your DbContext configuration in Program.cs or Startup.cs.
using EFCore.NoLock;
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(connectionString)
.AddInterceptors(new WithNoLockInterceptor()));
using EFCore.NoLock.Core;
public async Task<List<Order>> GetActiveOrdersAsync()
{
var orders = await _context.Orders
.Include(o => o.OrderLines)
.Where(o => o.IsActive)
.WithNoLock()
.ToListAsync();
return orders;
}
using EFCore.NoLock.LinqToDb;
using LinqToDB;
using LinqToDB.DataProvider.SqlServer;
var options = new DataOptions()
.UseSqlServer(connectionString)
.UseInterceptor(new LinqToDbWithNoLockInterceptor());
using var db = new DataConnection(options);
using EFCore.NoLock.Core;
var products = db.GetTable<Product>()
.Where(p => p.IsActive)
.WithNoLock()
.ToList();
When you use .WithNoLock(), the interceptor captures the generated SQL before it hits the database. It parses the SQL into an Abstract Syntax Tree (AST), identifies the physical tables, and injects the WITH (NOLOCK) hint to every table in the query.
Example Scenario:
Fetching an Order and its related OrderLines.
SELECT [o].[Id],
[o].[CustomerName],
[o0].[Id],
[o0].[OrderId],
[o0].[Product]
FROM [Orders] AS [o]
LEFT OUTER JOIN
[OrderLines] AS [o0]
ON [o].[Id] = [o0].[OrderId]
WHERE [o].[Id] = 1
ORDER BY [o].[Id];
SELECT [o].[Id],
[o].[CustomerName],
[o0].[Id],
[o0].[OrderId],
[o0].[Product]
FROM [Orders] AS [o] WITH (NOLOCK)
LEFT OUTER JOIN
[OrderLines] AS [o0] WITH (NOLOCK)
ON [o].[Id] = [o0].[OrderId]
WHERE [o].[Id] = 1
ORDER BY [o].[Id];
EFCore.NoLock.Core ← Shared SQL transformation engine (ScriptDom + Cache)
├── EFCore.NoLock ← EF Core DbCommandInterceptor
└── EFCore.NoLock.LinqToDb ← LinqToDB CommandInterceptor
The core engine is ORM-agnostic. Each ORM package provides a thin interceptor that delegates SQL transformation to EFCore.NoLock.Core.
Parsing SQL is an expensive operation. To ensure high performance in production environments:
ConcurrentDictionary (Cache).ScriptDom) runs only once per unique query. Subsequent calls are virtually instantaneous.Using WITH (NOLOCK) is equivalent to using the READ UNCOMMITTED isolation level for the specific tables in the query.
Contributions are welcome! Please feel free to submit a Pull Request or open an issue on GitHub.
This project is licensed under the MIT License.