12

To deal with SQL timeouts I'm trying to use SqlAzureExecutionStrategy (https://msdn.microsoft.com/en-us/data/dn456835.aspx)

The problem I am running into is it prevents "user initiated transactions" which seem to be the recommended way to implement "with (nolock)" in EF (http://www.hanselman.com/blog/GettingLINQToSQLAndLINQToEntitiesToUseNOLOCK.aspx, NOLOCK with Linq to SQL).

example code

    public AspnetUser GetAspnetUserByUserName(string userName)
    {
        using (var tx = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted }))
        {
            return context.AspnetUsers.Where(x => x.UserName == userName).FirstOrDefault();
        }
    }

throws error

The configured execution strategy 'SqlAzureExecutionStrategy' does not support user initiated transactions. See http://go.microsoft.com/fwlink/?LinkId=309381 for additional information.

I've seen the answers that say to turn off the SqlAzureExecutionStrategy on a per call basis, but that would defeat the purpose of using it, if all my reads ignored the strategy. It is possible to have both "NoLock" and SqlAzureExecutionStrategy

Community
  • 1
  • 1
Aaron Sherman
  • 3,789
  • 1
  • 30
  • 33

1 Answers1

19

SqlAzureExecutionStrategy doesn't support transactions initiated outside the action to be retried. To work around this restriction you would need to suspend the strategy, create the transaction scope and do the work as an action that you manually pass to the execution strategy to be retried:

public AspnetUser GetAspnetUserByUserName(string userName)
{
    new SuspendableSqlAzureExecutionStrategy().Execute(() =>
        {
            using (var tx = new TransactionScope(
                    TransactionScopeOption.Required,
                    new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted }))
            {
                return context.AspnetUsers.Where(x => x.UserName == userName).FirstOrDefault();
            }
        });
}

Here I am using an alternative to the suspendable strategy from https://msdn.microsoft.com/en-us/data/dn307226 that will suspend any nested invocations automatically:

using System.Data.Entity.Infrastructure;
using System.Data.Entity.SqlServer;
using System.Data.Entity.Utilities;
using System.Runtime.Remoting.Messaging;
using System.Threading;
using System.Threading.Tasks;

public class SuspendableSqlAzureExecutionStrategy : IDbExecutionStrategy
{
    private readonly IDbExecutionStrategy _azureExecutionStrategy;

    public SuspendableSqlAzureExecutionStrategy()
    {
        _azureExecutionStrategy = new SqlAzureExecutionStrategy();
    }

    private static bool Suspend
    {
        get { return (bool?)CallContext.LogicalGetData("SuspendExecutionStrategy") ?? false; }
        set { CallContext.LogicalSetData("SuspendExecutionStrategy", value); }
    }

    public bool RetriesOnFailure
    {
        get { return !Suspend; }
    }

    public virtual void Execute(Action operation)
    {
        if (!RetriesOnFailure)
        {
            operation();
            return;
        }

        try
        {
            Suspend = true;
            _azureExecutionStrategy.Execute(operation);
        }
        finally
        {
            Suspend = false;
        }
    }

    public virtual TResult Execute<TResult>(Func<TResult> operation)
    {
        if (!RetriesOnFailure)
        {
            return operation();
        }

        try
        {
            Suspend = true;
            return _azureExecutionStrategy.Execute(operation);
        }
        finally
        {
            Suspend = false;
        }
    }

    public virtual async Task ExecuteAsync(Func<Task> operation, CancellationToken cancellationToken)
    {
        if (!RetriesOnFailure)
        {
            await operation();
            return;
        }

        try
        {
            Suspend = true;
            await _azureExecutionStrategy.ExecuteAsync(operation, cancellationToken);
        }
        finally
        {
            Suspend = false;
        }
    }

    public virtual async Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> operation, CancellationToken cancellationToken)
    {
        if (!RetriesOnFailure)
        {
            return await operation();
        }

        try
        {
            Suspend = true;
            return await _azureExecutionStrategy.ExecuteAsync(operation, cancellationToken);
        }
        finally
        {
            Suspend = false;
        }
    }
}

public class MyConfiguration : DbConfiguration
{
    public MyConfiguration()
    {
        SetExecutionStrategy("System.Data.SqlClient", () => new SuspendableSqlAzureExecutionStrategy());
    }
}
Andriy Svyryd
  • 2,021
  • 1
  • 19
  • 26
  • I will try to test this, this week to let you know how it works thanks – Aaron Sherman Aug 22 '15 at 15:30
  • Only thing that I'm not clear about on this is why Suspend has to be public. Also giving this a try now, seems cleaner than the MSDN version. – Robert Noack Feb 09 '16 at 01:47
  • Working just fine. Updated suspend to be private on my side, but this is a pretty nice implementation, thanks for that. – Robert Noack Feb 09 '16 at 03:14
  • @RobertNoack Any chance you could share your code? – CShark May 22 '16 at 17:17
  • @AndriySvyryd What is the name and type for "Check"? – CShark May 22 '16 at 18:45
  • @RobertNoack I presume it is a generic method which does the same as this... if (operation == null) throw new ArgumentNullException(); – CShark May 22 '16 at 20:47
  • @DaffyPunk Yeah the 'Check' was not working for me either obviously as the code is not included, but I think it's totally valid to replace with an if statement like you suggested. You could just do this check in the Execute method itself really, it's not an uncommon pattern. – Robert Noack May 23 '16 at 15:02
  • Very elegant. Thank you – Jason Dufair Jun 08 '16 at 15:44