@gustavo-rossi-muller 's answers is useful, but lacks thread safety so can't be used with async methods provided by EF Core such as DbContext.SaveChangesAsync()
since not overriding ScalarExecutingAsync()
and ReaderExecutingAsync()
.
The [ThreadStatic]
attribute on the public static field HintValue
is necessary for forcing each thread using their own variant value of HintInterceptor.HintValue
instead of sharing the same value across all threads(aka global variable).
Document of interceptor had figured out that
Interceptors are often stateless, which means that a single interceptor instance can be used for all DbContext instances.
and if you want to stay some states in interceptor instances of each DbContext
, you'll have to:
This interceptor is stateful: it stores the ID and message text of the most recent daily message queried, plus the time when that query was executed. Because of this state we also need a lock since the caching requires that same interceptor must be used by multiple context instances.
but what we need is to control the state of the interceptor for each query command, since we only need some certain SELECT
commands to be queried with FOR UPDATE
suffix, not for all commands that will cause many syntax errors.
and so far we can only provide some extra info into certain query commands via TagWith()
, then detect the comment added by the tagging in overrides of DbCommandInterceptor
to append the FOR UPDATE
hint for these queries.
In fact the document had already provided an example to do this.
I've modified from that example for appending FOR UPDATE
with MySQL syntax:
private class SelectForUpdateCommandInterceptor : DbCommandInterceptor
{ // https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#example-command-interception-to-add-query-hints
public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
{
ManipulateCommand(command);
return result;
}
public override ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<object> result, CancellationToken cancellationToken = default)
{
ManipulateCommand(command);
return new(result);
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
ManipulateCommand(command);
return result;
}
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
{
ManipulateCommand(command);
return new(result);
}
private static void ManipulateCommand(IDbCommand command)
{
if (command.CommandText.StartsWith("-- ForUpdate", StringComparison.Ordinal))
{
command.CommandText += " FOR UPDATE";
}
}
}
then inject this interceptor when configuring your DbContext:
private static readonly SelectForUpdateCommandInterceptor SelectForUpdateCommandInterceptorInstance = new();
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.AddInterceptors(SelectForUpdateCommandInterceptorInstance);
}
finally, we can do:
var results = (from e in db.Set<SomeEntity>.TagWith("ForUpdate")
where e.SomeField == someValue
select e.SomeField).ToList();
db.Set<SomeEntity>.Add(new SomeEntity {SomeField = 1});
db.SaveChanges();
db.SaveChangesAsync(); // thread safe
Until EF Core 8, they still have no plan on implementing this query hint suffix: https://github.com/dotnet/efcore/issues/26042, but another linq2sql expression translator called linq2db
had already done this:
https://github.com/linq2db/linq2db/issues/1276
https://github.com/linq2db/linq2db/pull/3297
https://github.com/linq2db/linq2db/issues/3905
Blockquote