9

As far as I've understood it, there is no option in EF (and EF Core) to explicitly lock resources which I'm querying, but I'll need this functionality quite often and don't really feel like falling back to writing select statements every time I'll need it.

Since I only need it for postgres and according to the spec FOR UPDATE is the last item in the query, the easiest I thought about implementing it was to get the select statement as described here: In Linq to Entities can you convert an IQueryable into a string of SQL? and append FOR UPDATE and directly execute it. However this will either give me a query with parameter placeholders or not a prepared query meaning that caching for execution plan won't really work on postgres, so in either way it's a no go.

Linq to SQL had the method DataContext.GetCommand but there doesn't seem to be anything equivalent in EF and specially EF Core. I also had a look at EntityFramework.Extended and their batch updates / deletes but since they have to transform the select statement into a different statement they need to deal with far more complexity than me and so I hope for a simpler solution.

Update:

In case it wasn't clear from the description, I want to create an extension method like this:

public static IList<T> ForUpdate (this IQueryable<T> me)
{
    // this line is obviously what is missing for me :)
    var theUnderlyingCommand = me.GetTheUnderlyingDbCommandOrSimilar();

    theUnderlyingCommand.Text += "FOR UPDATE";
    return me.ToList();
}

This way, other developers can use EF via Linq as with all other procedures and instead of running .ToList() they'd run .ForUpdate(). (For Update executes the query on purpose to make the implementation easier, and also because FOR UPDATE is the last option supported by postgres, afterwards there shouldn't be anything else anymore)

Community
  • 1
  • 1
peter
  • 14,348
  • 9
  • 62
  • 96
  • 1
    The EFCore main approach is optimistic concurrency rather than locking, i.e. using some column as a concurrency token (or row version) to make sure that the row wasn't updated since it was read. Note that this currently isn't supported in the Npgsql provider but may be soon (https://github.com/npgsql/Npgsql.EntityFrameworkCore.PostgreSQL/issues/19). `SELECT FOR UPDATE` seems like it would require quite a lot of effort at the EFCore level so I doubt it's going to be done soon, but try opening an issue with them. – Shay Rojansky Jun 23 '16 at 07:28
  • If you're just looking to execute your own raw SQL, where you would append `FOR UPDATE`, EFCore provides the `FromSql` method, or you can drop down to ADO.NET and do whatever you want (here's a good article: http://www.elanderson.net/2016/04/execute-raw-sql-in-entity-framework-core/) – Shay Rojansky Jun 23 '16 at 07:32
  • @ShayRojansky I've updated the question since it probably wasn't clear enough. I know that I can execute raw SQL, but I don't want to deal with raw SQL in the rest of my application - I have no problem dealing with it in a dedicated and well tested extension method though. – peter Jun 23 '16 at 07:44
  • I'll have a transaction, but I want read committed on all my tables except one – peter Jun 23 '16 at 09:33
  • @GertArnold and a serializable transaction works completely differently if two concurrent operations read an item and try to update it later on, one of them will simply fail - however if I have read committed (or serializable) and both read with `FOR UPDATE` I get the expected result without any issues – peter Jun 23 '16 at 09:48

3 Answers3

8

This work's for me using SQLServer (no tested async methods):

First, create a DbCommandInterceptor (I called HintInterceptor.cs)

using System;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using System.Text.RegularExpressions;

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

    [ThreadStatic]
    public static string HintValue;

    private static string Replace(string input)
    {
        if (!String.IsNullOrWhiteSpace(HintValue))
        {
            if (!_tableAliasRegex.IsMatch(input))
            {
                throw new InvalidProgramException("Não foi possível identificar uma tabela para ser marcada para atualização(forupdate)!", new Exception(input));
            }
            input = _tableAliasRegex.Replace(input, "${tableAlias} WITH (*HINT*)");
            input = input.Replace("*HINT*", HintValue);
        }
        HintValue = String.Empty;
        return input;
    }

    public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        command.CommandText = Replace(command.CommandText);
    }

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        command.CommandText = Replace(command.CommandText);
    }
}

So into Web.config register the your interceptor class

<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
<providers>
  <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
</providers>
<interceptors> 
  <interceptor type="Full.Path.Of.Class.HintInterceptor, Dll.Name" />
</interceptors>
</entityFramework>

Now I create a static class called HintExtension

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

That's All, I can use inside a database transaction like:

using(var trans = context.Database.BeginTransaction())
{
        var query = context.mydbset.Where(a => a.name == "asd").ForUpdate();
        // not locked yet
        var mylist = query.ToList();
        // now are locked for update
        // update the props, call saveChanges() and finally call commit ( or rollback)
        trans.Commit();
        // now are unlocked
}

Sorry for my English, I hope my example will help.

Gustavo Rossi Muller
  • 1,062
  • 14
  • 18
  • 1
    Thanks for your great post. I think the Regex is not correct. Shouldn't the asterisk around HINT not be escaped: WITH \(\*HINT\*\)))" Is the + necassary after the blank behind FROM? – M. Koch May 06 '22 at 16:47
  • @M.Koch About escaping the asterisks, you are correct. This will avoid using the function more than once. It might even be interesting to specifically catch this error and display a friendlier exception. About the + after the from, I don't know why, but I had a case that there were several spaces between the from and the table. – Gustavo Rossi Muller May 13 '22 at 12:21
4

According to this issue there is no easy way to implement locks hints and other database oriented calls in ef core

I implemented UPDLOCK with MsSQL and ef core in my project this way:

public static class DbContextExtensions
{
    public static string GetUpdLockSqlForEntity<T>(this DbContext dbContext, int entityPk, bool pkContainsTableName = true) where T : class
    {
        var mapping = dbContext.Model.FindEntityType(typeof(T)).Relational();
        var tableName = mapping.TableName;
        var entityPkString = entityPk.ToString();
        string idPrefix = pkContainsTableName ? tableName.Substring(0, tableName.Length - 1) : string.Empty;
        return $"Select 1 from {tableName} with (UPDLOCK) where {idPrefix}Id = {entityPkString}";
    }
}

We are using this method in database transaction as raw sql call(lock will be released after commit or rollback):

using (var dbTran = await DataContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted))
{
    try
    {
        await DataContext.Database.ExecuteSqlCommandAsync(DataContext.GetUpdLockSqlForEntity<Deposit>(entityId));
        dbTran.Commit();
    }
    catch (Exception e)
    {
        dbTran.Rollback();
        throw;
    }
}
spottedmahn
  • 14,823
  • 13
  • 108
  • 178
Andrey Nikolaev
  • 368
  • 3
  • 9
1

@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

n0099
  • 520
  • 1
  • 4
  • 12