Has your LINQ to SQL repository ever thrown a "cannot access a disposed object" exception? You can fix it by calling ToList on the LINQ query but it will impede your application’s performance and scalability.
This post covers common pitfalls and how to avoid them when dealing with unmanaged resources such as the lifecycle of a database connection in a pull-based IEnumerable repository. An investigation is made to uncover when Entity Framework and LINQ to SQL resources are disposed of and how to implement an effective solution.
Download Source Code
Setup
The following repository class will be used to model the same behaviour as an actual LINQ to SQL database repository.
public class Model
{
public string Message { get; set; }
}
public class Repository : IDisposable
{
public IEnumerable<Model> Records
{
get
{
if (_disposed) throw new InvalidOperationException("Disposed");
Console.WriteLine("Building message one");
yield return new Model() { Message = "Message one" };
if (_disposed) throw new InvalidOperationException("Disposed");
Console.WriteLine("Building message two");
yield return new Model() { Message = "Message two" };
}
}
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
_disposed = true;
}
}
LINQ to SQL: Cannot access a disposed object
Let's execute the LINQ query below to call the repository and write the results to the console.
static void Main(string[] args)
{
var records = GetLinqRecords();
foreach (var record in records)
{
Console.WriteLine(record);
}
Console.ReadLine();
}
private static IEnumerable<string> GetLinqRecords()
{
using (var repository = new Repository())
{
return (from model in repository.Records select model.Message);
}
}
Oops! An InvalidOperationException occurred on line 12 in the repository class.
A LINQ to SQL application would raise the following exception:
An unhandled exception of type 'System.ObjectDisposedException' occurred in System.Data.Linq.dll
Additional information: Cannot access a disposed object.
LINQ to SQL: ToList
Let's execute the LINQ query below by materialising the records to a list first:
static void Main(string[] args)
{
var records = GetLinqRecordsToList();
foreach (var record in records)
{
Console.WriteLine(record);
}
Console.ReadLine();
}
private static IEnumerable>string< GetLinqRecordsToList()
{
using (var repository = new Repository())
{
return (from model in repository.Records select model.Message).ToList();
}
}
Building message one
Building message two
Message one
Message two
Warning! The code works but with bad side-effects. All of the records were loaded into memory immediately and the caller lost the ability to defer exection. The benefits of deferred execution are described in the
Yield IEnumerable vs List Building post.
Yield to the rescue
Let's execute the code below using yield instead:
static void Main(string[] args)
{
var records = GetYieldRecords();
foreach (var record in records)
{
Console.WriteLine(record);
}
Console.ReadLine();
}
private static IEnumerable<string> GetYieldRecords()
{
using (var repository = new Repository())
{
foreach (var record in repository.Records)
{
yield return record.Message;
}
}
}
Building message one
Message one
Building message two
Message two
Success! The connection was also kept alive and the records were constructed in a deferred execution pull-based manner.
Don’t refactor your code
Let's see what happens when we run a refactored version of the code:
static void Main(string[] args)
{
var records = GetRefactoredYieldRecords();
foreach (var record in records)
{
Console.WriteLine(record);
}
Console.ReadLine();
}
private static IEnumerable<string>string<string> GetRefactoredYieldRecords()
{
using (var repository = new Repository())
{
return YieldRecords(repository.Records);
}
}
private static IEnumerable<string> YieldRecords(IEnumerable<Model> records)
{
if (records == null) throw new ArgumentNullException("records");
foreach (var record in records)
{
yield return record.Message;
}
}
Oops! An InvalidOperationException occurred on line 12 in the repository class.
Déjà Vu. The same error occurred as seen in the LINQ to SQL example. Take a closer look at the IL produced by the compiler using a tool such as ILSpy.
In the refactored and the LINQ to SQL version, instead of returning an IEnumerable function directly, a function is returned that points to another IEnumerable function. Effectively, it is an IEnumerable within an IEnumerable. The connection lifecycle is managed in the first IEnumerable function which will be disposed once the second IEnumerable function is returned to the caller.
Keep it simple, return the IEnumerable function directly to the caller.