The answer is no. Apparently, the main problem is that there are sp_reset_connection calls whenever a merged connection is reused from the pool (and not when released back to the pool), which should release any session-based locks. This social.msdn link claims that sp_reset_connection is even called between EF commands.
The simplest solution is to use a dedicated connection that remains open for the duration of the lock and not use it for another SQL if the API can alternate any calls to sp_resest_connection (profiling can prove this).
Also see SqlConnection vs Sql Session. Are their lifespan compatible?
and what-do-exec-sp- reset -connection-mean-in-sql-server-profiler
Here is the C # sp_GetAppLock () wrapper we are trying (disclaimer - not fully tested):
Using:
using (new globalApplicationSqlServerLock(connectionString, "theUniqueLockName") {
Implmentation:
using System; using System.Data; using System.Data.SqlClient; namespace CompanyNamespace.Server.DataAdaptersCommon { /// <summary> /// A wrapper around sp_GetAppLock (SQL Server), useful for global locking (by arbitrary name) across multiple machines. /// For instance: Include the compatibility version number within the lock resource to create a version specific lock. /// </summary> public class GlobalApplicationSqlServerLock : SimplifiedDisposableBase { /// <summary> /// Last returned value from sp_GetAppLock() or sp_ReleaseAppLock() /// </summary> public Int32 LastReturnCode { get; private set; } /// <summary> /// The SQL Connection to use. /// </summary> readonly SqlConnection _connection; /// <summary> /// The name of the lock chosen by the callse /// </summary> readonly string _lockName; /// <summary> /// The cumulative times that Lock() has been called. /// </summary> int _lockCount; // Refer to sp_GetAppLock() // const string _lockOwner = "session"; const string _lockMode = "Exclusive"; const string _dbPrincipal = "public"; /// <summary> /// Wait a maximum of this many seconds. /// </summary> Int32 _waitForLockMaxSeconds; /// <summary> /// Constructor accepting a Connection String /// </summary> /// <param name="connectionString">Connection string should include "...;AppName=AppType,WebPid" to improve DB side logging.</param> /// <param name="lockName"></param> /// <param name="waitForLockMaxSeconds">Throw an exception if the lock cannot be acquired within this time period.</param> /// <param name="lockNow">True to obtain the lock via the contructor call. Lock is always released in Dipose()</param> /// <param name="excludeFromOpenTransactionScope">True to exclude from any open TransactionScope</param> public GlobalApplicationSqlServerLock( string connectionString, string lockName, Int32 waitForLockMaxSeconds = 30, bool lockNow = true, bool excludeFromOpenTransactionScope = true) { SqlConnectionStringBuilder conStrBuilder = new SqlConnectionStringBuilder(connectionString); if (excludeFromOpenTransactionScope) conStrBuilder.Enlist = false; _waitForLockMaxSeconds = waitForLockMaxSeconds; // The lock must use a dedicated connection that stays open for the duration of the lock. // Otherwise, since the lock owner is "session", when the connection is closed the lock "may" be // released since connection pooling could have inconsistent side effects. So caller must ensure // lock is released (using IDisposable, etc). // _connection = new SqlConnection(conStrBuilder.ConnectionString); _connection.Open(); _lockName = lockName; if (lockNow) Lock(); } /// <summary> /// Lock /// </summary> public void Lock() { string errMsg; if (!TryLock(out errMsg)) throw new Exception(errMsg); } /// <summary> /// Try lock /// </summary> /// <param name="errMsg"></param> /// <returns>True if lock obtained, false if not with error message.</returns> public bool TryLock(out string errMsg) { using (SqlCommand command = new SqlCommand()) { command.Connection = _connection; command.CommandType = CommandType.StoredProcedure; command.CommandText = "sp_GetAppLock"; command.Parameters.Add(new SqlParameter("@Resource", SqlDbType.NVarChar, 255) { Value = _lockName }); command.Parameters.Add(new SqlParameter("@LockMode", SqlDbType.NVarChar, 32) { Value = _lockMode }); command.Parameters.Add(new SqlParameter("@LockOwner", SqlDbType.NVarChar, 32) { Value = _lockOwner }); command.Parameters.Add(new SqlParameter("@LockTimeout", SqlDbType.Int) { Value = _waitForLockMaxSeconds }); command.Parameters.Add(new SqlParameter("@DBPrincipal", SqlDbType.NVarChar, 128) { Value = _dbPrincipal }); command.Parameters.Add(new SqlParameter("@Result", SqlDbType.Int) { Direction = ParameterDirection.ReturnValue }); command.ExecuteNonQuery(); LastReturnCode = (int)command.Parameters["@Result"].Value; } switch (LastReturnCode) { case 0: case 1: _lockCount++; errMsg = null; return true; case -1: errMsg = "The lock request timed out."; break; case -2: errMsg = "The lock request was canceled."; break; case -3: errMsg = "The lock request was chosen as a deadlock victim."; break; case -999: errMsg = "Indicates a parameter validation or other call error."; break; default: errMsg = "Unexpected return value"; break; } return false; } /// <summary> /// Release the lock /// </summary> public void Release() { string errMsg; using (SqlCommand command = new SqlCommand()) { command.Connection = _connection; command.CommandType = CommandType.StoredProcedure; command.CommandText = "sp_ReleaseAppLock"; command.Parameters.Add(new SqlParameter("@Resource", SqlDbType.NVarChar, 255) { Value = _lockName }); command.Parameters.Add(new SqlParameter("@LockOwner", SqlDbType.NVarChar, 32) { Value = _lockOwner }); command.Parameters.Add(new SqlParameter("@DBPrincipal", SqlDbType.NVarChar, 128) { Value = _dbPrincipal }); command.Parameters.Add(new SqlParameter("@Result", SqlDbType.Int) { Direction = ParameterDirection.ReturnValue }); command.ExecuteNonQuery(); LastReturnCode = (int)command.Parameters["@Result"].Value; } switch (LastReturnCode) { case 0: _lockCount--; return; case -999: errMsg = "Indicates a parameter validation or other call error."; break; default: errMsg = "Unexpected return value"; break; } throw new Exception(errMsg); } /// <summary> /// Disposable implmentation /// </summary> protected override void FreeManagedResources() { try { while (_lockCount > 0) Release(); } finally { try { if (_connection != null && _connection.State != ConnectionState.Closed) _connection.Close(); } finally { base.FreeManagedResources(); } } } } } using System; using System.Collections.Generic; using System.Diagnostics; namespace CompanyNamespace.Common { /// <summary> /// To support IDisposable, pass true to constructor and call: /// /// AutoDispose(IDisposable) for each disposable at time of creation, /// /// Or override these as needed: /// /// FreeManagedResources() and /// FreeUnmanagedResources() /// /// Multi-thread safe. /// </summary> public abstract class SimplifiedDisposableBase : IDisposable { /// <summary> /// Flag for IDisposable /// </summary> protected bool _isDisposed = false; /// <summary> /// List of items that should be Dispose() when the instance is Disposed() /// </summary> private List<IDisposable> _autoDisposables = new List<IDisposable>(); /// <summary> /// Constructor /// </summary> public SimplifiedDisposableBase() { } /// <summary> /// Finalizer (needed for freeing unmanaged resources and adds a check a Dispose() check for managed resources). /// </summary> ~SimplifiedDisposableBase() { // Warning: An exception here will end the application. // Do not attempt to lock to a possibly finalized object within finalizer // http://stackoverflow.com/questions/4163603/why-garbage-collector-takes-objects-in-the-wrong-order string errMessages = string.Empty; try { errMessages = String.Format("Warning: Finalizer was called on class '{0}' (base class '{1}'). " + "IDisposable should usually call Dispose() to avoid this. (IsDisposed = {2})", GetType().FullName, typeof(SimplifiedDisposableBase).FullName, _isDisposed); Debug.WriteLine(errMessages); Dispose(false); // free any unmanaged resources }