Introduction
Object context acts as a unit of work in entity framework based applications and should have a short life time. It's easy to forget not disposing object contexts, which leads to memory leaks and also leaves too many related connection objects not disposed as well.
Here, we have 2 simple methods. First one disposes the context correctly and the second one does not have a using
statement. Therefore, its context won't be disposed at the end of the method automatically.
private static void disposedContext()
{
using (var context = new MyContext())
{
Debug.WriteLine("Posts count: " + context.BlogPosts.Count());
}
}
private static void nonDisposedContext()
{
var context = new MyContext();
Debug.WriteLine("Posts count: " + context.BlogPosts.Count());
}
Question: How can I find all of the non-disposed contexts in a large Entity framework 6.x application?
To answer this question, we can use the new Interception mechanism of Entity framework 6.x.
public class DatabaseInterceptor : IDbConnectionInterceptor
{
public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
{
}
public void Disposed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
{
}
public void Opened(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
{
}
}
By defining a custom database interceptor and implementing the IDbConnectionInterceptor
interface, it's possible to receive notifications of opening, closing and disposing different DbConnection
objects. When a context is disposed, its connections will be disposed too.
Problem! How can I track a DbConnection
object? Which connections are closed or disposed here? How can I relate the DbConnection
object of the Opened
method to the Disposed
method?
DbConnection
object does not have an Id
property. It's possible to add a new property to an existing object using extension properties concept.
We can't use GetHashCode()
here. Because it's possible to have different references with the same hash values and it is not guaranteed to be unique per object.
.NET 4.0 introduced ConditionalWeakTable<key, value>
. Its original purpose is to attach some extra data to an existing object, which allows us to track managed objects too. Also it doesn't interfere with garbage collector, because it uses weak references to objects.
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Threading;
namespace EFNonDisposedContext.Core
{
public static class UniqueIdExtensions<T> where T : class
{
static readonly ConditionalWeakTable<T, string> _idTable =
new ConditionalWeakTable<T, string>();
private static int _uniqueId;
public static string GetUniqueId(T obj)
{
return _idTable.GetValue(obj, o => Interlocked.Increment(ref _uniqueId).ToString(CultureInfo.InvariantCulture));
}
public static string GetUniqueId(T obj, string key)
{
return _idTable.GetValue(obj, o => key);
}
public static ConditionalWeakTable<T, string> RecordedIds
{
get { return _idTable; }
}
}
}
The main part of this class is _idTable
as ConditionalWeakTable
which manages a dictionary to store object references in conjunction with their unique IDs.
Now it's possible to create a list of connections and their status. IDbConnectionInterceptor
interface provides Opened
, Closed
and Disposed
events. During the call of each event, UniqueIdExtensions<DbConnection>.GetUniqueId(connection)
returns the unique Id of the connection. Based on this value, we can add a new ConnectionInfo
to the list or update its status from Opened
to Closed
or Disposed
.
public enum ConnectionStatus
{
None,
Opened,
Closed,
Disposed
}
public class ConnectionInfo
{
public string ConnectionId { set; get; }
public string StackTrace { set; get; }
public ConnectionStatus Status { set; get; }
public override string ToString()
{
return string.Format("{0}:{1} [{2}]",ConnectionId, Status, StackTrace);
}
}
To construct a new ConnectionInfo
, we also need the StackTrace
of the caller methods. It allows us to find which method calls are responsible to open this connection.
new ConnectionInfo
{
ConnectionId = UniqueIdExtensions<dbconnection>.GetUniqueId(connection),
StackTrace = CallingMethod.GetCallingMethodInfo(),
Status = status
}
To extract the caller methods information, we can start with the new instance of the StackTrace
class. Its FrameCount
property defines different levels of callers. Each frame consists of the information of the calling method, plus its filename and line number.
Filename and line number won't be present the in final log, if the involved assemblies don't have the valid .PDB files.
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
namespace EFNonDisposedContext.Core
{
public static class CallingMethod
{
public static string GetCallingMethodInfo()
{
var stackTrace = new StackTrace(true);
var frameCount = stackTrace.FrameCount;
var info = new StringBuilder();
var prefix = "-- ";
for (var i = frameCount - 1; i >= 0; i--)
{
var frame = stackTrace.GetFrame(i);
var methodInfo = getStackFrameInfo(frame);
if (string.IsNullOrWhiteSpace(methodInfo))
continue;
info.AppendLine(prefix + methodInfo);
prefix = "-" + prefix;
}
return info.ToString();
}
private static bool isFromCurrentAsm(MethodBase method)
{
return method.ReflectedType == typeof(CallingMethod) ||
method.ReflectedType == typeof(DatabaseInterceptor) ||
method.ReflectedType == typeof(Connections);
}
private static bool isMicrosoftType(MethodBase method)
{
if (method.ReflectedType == null)
return false;
return method.ReflectedType.FullName.StartsWith("System.") ||
method.ReflectedType.FullName.StartsWith("Microsoft.");
}
private static string getStackFrameInfo(StackFrame stackFrame)
{
if (stackFrame == null)
return string.Empty;
var method = stackFrame.GetMethod();
if (method == null)
return string.Empty;
if (isFromCurrentAsm(method) || isMicrosoftType(method))
{
return string.Empty;
}
var methodSignature = method.ToString();
var lineNumber = stackFrame.GetFileLineNumber();
var filePath = stackFrame.GetFileName();
var fileLine = string.Empty;
if (!string.IsNullOrEmpty(filePath))
{
var fileName = Path.GetFileName(filePath);
fileLine = string.Format("[File={0}, Line={1}]", fileName, lineNumber);
}
var methodSignatureFull = string.Format("{0} {1}", methodSignature, fileLine);
return methodSignatureFull;
}
}
}
With this sample output:
--- Void Main(System.String[]) [File=Program.cs, Line=28]
--- Void disposedContext() [File=Program.cs, Line=76]
Now we can create an in-memory repository to get, add or update the connections list.
public static class Connections
{
private static readonly ICollection<ConnectionInfo> _connectionsInfo = new List<ConnectionInfo>();
public static ICollection<ConnectionInfo> ConnectionsInfo
{
get { return _connectionsInfo; }
}
public static void AddOrUpdate(ConnectionInfo connection)
{
var info = _connectionsInfo.FirstOrDefault(x => x.ConnectionId == connection.ConnectionId);
if (info!=null)
{
info.Status = connection.Status;
}
else
{
_connectionsInfo.Add(connection);
}
}
public static void AddOrUpdate(DbConnection connection, ConnectionStatus status)
{
AddOrUpdate(
new ConnectionInfo
{
ConnectionId = UniqueIdExtensions<DbConnection>.GetUniqueId(connection),
StackTrace = CallingMethod.GetCallingMethodInfo(),
Status = status
});
}
}
Then, we can track the different states of each connection in DatabaseInterceptor
class.
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
namespace EFNonDisposedContext.Core
{
public class DatabaseInterceptor : IDbConnectionInterceptor
{
public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
{
Connections.AddOrUpdate(connection, ConnectionStatus.Closed);
}
public void Disposed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
{
Connections.AddOrUpdate(connection, ConnectionStatus.Disposed);
}
public void Opened(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
{
Connections.AddOrUpdate(connection, ConnectionStatus.Opened);
}
}
}
To register this new interceptor, we can call the following line at the beginning of the program.
DbInterception.Add(new DatabaseInterceptor());
At the end, to find out which contexts are not disposed, we can query connections list and filter connections with Status != ConnectionStatus.Disposed
.
var connectionsTable = UniqueIdExtensions<DbConnection>.RecordedIds;
var valuesInfo = connectionsTable.GetType().GetProperty("Values", BindingFlags.NonPublic | BindingFlags.Instance);
var aliveConnectionsKeys = (ICollection<string>)valuesInfo.GetValue(connectionsTable, null);
var nonDisposedItems = Connections.ConnectionsInfo
.Where(info => info.Status != ConnectionStatus.Disposed &&
aliveConnectionsKeys.Contains(info.ConnectionId));
foreach (var connectionInfo in nonDisposedItems)
{
Debug.WriteLine("+--------ConnectionId:" + connectionInfo.ConnectionId + "----------+");
Debug.WriteLine(connectionInfo.StackTrace);
}
Also it's important to check the internal properties of ConditionalWeakTable
object. Because, the real disposed objects won't be present in its Values
collection property.
This is a sample output of the above query:
List of non-disposed ObjectContexts:
+--------ConnectionId:15----------+
-- Void Main(System.String[]) [File=Program.cs, Line=23]
--- Void nonDisposedContext() [File=Program.cs, Line=69]
<p>+--------ConnectionId:16----------+
-- Void Main(System.String[]) [File=Program.cs, Line=24]
--- Void nonDisposedContext() [File=Program.cs, Line=69]
+--------ConnectionId:17----------+
-- Void Main(System.String[]) [File=Program.cs, Line=25]
--- Void nonDisposedContext() [File=Program.cs, Line=69]
+--------ConnectionId:19----------+
-- Void Main(System.String[]) [File=Program.cs, Line=27]
--- Void nonDisposedContext() [File=Program.cs, Line=69]
+--------ConnectionId:22----------+
-- Void Main(System.String[]) [File=Program.cs, Line=30]
--- Void nonDisposedContext() [File=Program.cs, Line=69]
Here nonDisposedContext()
method is called 5 times from different locations or lines.