Linq To SQL Caching Adventures Part 2

Before picking up where I left off yesterday, I have to first make a small disclaimer. I received a couple of comments stating that the caching solution I presented wasn’t that sophisticated or complete or that the cache keys that I’ve used were not the right and the answer is of course “yes”. The solution I presented was far from perfect but it served well as a simplified demonstration of the problems one might face when caching Linq to SQL entities.

Ok now that we’re done with the typicalities let’s see what was wrong with the solution shown earlier. To demonstrate the problem I’m going to add a single button to my web form and attach an event handler to it. In the event handler I’m going to ask the Person from the Cache and Lazy load his phones.

public partial class _Default : System.Web.UI.Page
{
	protected void Page_Load(object sender, EventArgs e)
	{
		if (!IsPostBack)
		{
			DataBind();
		}
	}

	public override void DataBind()
	{
		base.DataBind();

		employeesRepeater.DataSource = new List<Person>() { GetPerson("M") };
		employeesRepeater.DataBind();

	}

	protected void btnGetPhone_Click(object sender, EventArgs e)
	{
		Person person = GetPerson("M"); //This person comes from the cache
		var phones = person.PersonPhones.ToList(); //Attempt to lazy load throws exception as Context has been disposed
	}

	protected Person GetPerson(string lastNameStartsWith)
	{
		using (AdventureworksDataContext context = new AdventureworksDataContext())
		{
			return CacheGet<Person>(
					() => context.Persons.Where(p => p.LastName.StartsWith(lastNameStartsWith)).FirstOrDefault(),
					string.Format("Person_LastName_StartsWith:{0}", lastNameStartsWith));
		}
	}

	protected T CacheGet<&lt;T>(Func<T> loader, string cacheKey) where T : class
	{
		var cachedObject = this.Cache[cacheKey] as T;
		if (cachedObject == null)
		{
			cachedObject = loader.Invoke();
		}
		return cachedObject;
	}
}

CaptureIf you ran this code and click the button you’ll end up with the exception
Cannot access a disposed object.
Object name: ‘DataContext accessed after Dispose.’.

That’s the real problem when caching Linq to SQL entities, the fact that they have a dependency (reference) on the data context that created them and as such they can not be cached. There are ways you can work-around this problem by detaching and re-attaching the entities to the currently loaded data context. But since there is no way you can actually detach an entity from the context you can only rely on hacks to do that. A way to detach an entity is serialize it (of course entities must be declared serializable) cache it and then before retrieving it from the cache deserialize it and attach it to the context. Another is to manually detach an entity (set certain properties to null) and another to manually clone it. Each of these methods has advantages and disadvantages, like the fact that you’ll loose all object’s graph if you serialize it or the complexity if you follow the manual detaching path. One thing is for sure though, there is no easy way around it.

What’s my preffered way you ask? I guess the most painful way (;-)) but at the same time the most scalable and robust. Build my own custom POCO entities model, populate it using whatever data access technology and cache those entities.

 
Comments

I recreated your example and i also ran into the same problem. However, my question is, why don”t we initialize the context as a class variable and then call the dispose method in the Page_Unload event? This seems to solve the problem without any hacks or workarounds…

Hey Markos, your question is perfectly valid and it could work if you”re really careful and make certain assumptions (like I”m going to prefetch the person”s phones) but you have to think of the cache as a global repository and not just a page one. A repository that can be used at any time and from any page, handler, module even machine (if the cache is distributed) and so on.
For example you would have the same exception if the call to get the person”s phone numbers was made from a second page (Page A loads and caches the person, Page B retrieves from cache the person and tries to lazy load person”s phone numbers).

Of course!! I should have known better…

You should really check out PLINQO. It is a big set of enhancements to LINQ to SQL including detach and caching. http://www.plinqo.com

To tell you the truth I was kind of expecting an answer like that, and yes the answer is that there are a couple of frameworks that promise to help you overcome these issues, like the http://linq2sqleb.codeplex.com/ for instance.
The problem is that they”re using the detaching methods I described earlier which is either entity serialization or cloning, they just do it for you.
As for PLINQO, I”ve looked at this a while back but there were two things I didn”t quite like in that solution.
1. You have to buy codesmith in order to use it (to generate the classes)
2. This is no longer LinqToSQL. Although classes (entities) are similar to LinqToSQL, they contain lot more information and API than Linq To SQL.

I read an interesting article written by Rick Strahl “LinqToSql, Lazy loading and prefetcing” (http://www.west-wind.com/Weblog/posts/38838.aspx). It seems that the DataLoadOptions class can take care of the problem. For example, in the using statement of the GetPerson() method:
//
DataLoadOptions options = new DataLoadOptions();
options.LoadWith(p => p.PersonPhones);
context.LoadOptions = options;
//
Am i correct or not? Could another counterexample be constructed?

Leave a Reply