Search Wiki:

Caching Linq results using SqlCacheDependency

In early 2008 I needed to cache the results from LinqToSql-queries. I like the SqlCacheDependency (especially in SQL 2K5 Server).
I also like the new extension methods posibillity... So this is what I came up with.

SqlCacheDependency

With SqlServer 2005 SqlCacheDependency became very useful. In Sql 2000 the dependency could only be created on full tables (not rows). The Cache was destroyed as a table being cached changed even if your query did not affect the row changed, but Sql 2005 can handle SqlCacheDependency on row-level... Hence SqlCacheDependency is destroyed only if a row that my query affects is being changed.

The thing you do to use SqlCacheDependency is to create a SqlCommand that contains the Sql-query you want to cache the result of.
Then you create a SqlCacheDependency with the SqlCommand as a parameter.

LinqToSql

It´s hard not to love LinqToSql... Sure, I know some DBA´s that do not love the thought of foreign code executing queries in there databases (if we ignore the option to use stored procedures with LinqToSql). I personally think that it´s ok to only use permissions in BLL on method level like:
PrincipalPermission(System.Security.Permissions.SecurityAction.Demand,Role"ADMIN",Authenticatedtrue)

The simplicity of development and the oppurtunity to quick changes in DAL and database makes it hard to resist LinqToSql :)

SqlCacheDependency with LinqToSql

Why cache LinqQueries?
Everytime you iterate over a LinqToSql-query it will execute...
Like -> foreach(MyObj o in MyLinqQuery) will execute the query against the SqlServer. One solution I´ve seen people using is: List = MyLiqToSqlQuery.ToList(); and then use cache on the result. This will ofcourse work, but you´ll not know if anything changes in the database unless you use SqlCacheDependency.

The challenge of LinqToSql & SqlCacheDependency is to extract the SqlQuery to be able to store it in a SqlCommand and then use the SqlCommand in the SqlCacheDependency. You will also have to provide functionality for parameters that the LinqQuery may hold... I created a generic ExtensionMethod for IQueryable to solve my problem.... one method to cache all LinqQueries

The code should be explained enough by the comments I think.
    public static class Cache
    {
        /// <summary>
        /// Caches Linq query´s that is created for LinqToSql.
        /// Limitations are the same as SqlCacheDependency
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="q">The linq query</param>
        /// <param name="dc">Your LinqToSql DataContext</param>
        /// <param name="CacheId">The unique Id for the cache</param>
        /// <returns></returns>
        public static List<T> LinqCache<T>(this System.Linq.IQueryable<T> q, System.Data.Linq.DataContext dc, string CacheId)
        {
            try
            {
                List<T> objCache = (List<T>)System.Web.HttpRuntime.Cache.Get(CacheId);
 
                if (objCache == null)
                {
                    /////////No cache... implement new SqlCacheDependeny//////////
                    //1. Get connstring from DataContext
                    string connStr = dc.Connection.ConnectionString;
                    //2. Get SqlCommand from DataContext and the LinqQuery
                    string sqlCmd = dc.GetCommand(q).CommandText;                    
                    //3. Create Conn to use in SqlCacheDependency
                    using (System.Data.SqlClient.SqlConnection conn = new System.Data.SqlClient.SqlConnection(connStr))
                    {
                        conn.Open();
                        //4. Create Command to use in SqlCacheDependency
                        using (System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient.SqlCommand(sqlCmd, conn))
                        {
                            //5.0 Add all parameters provided by the Linq Query
                            foreach (System.Data.Common.DbParameter dbp in dc.GetCommand(q).Parameters)
                            {
                                cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter(dbp.ParameterName, dbp.Value));                                
                            }
                            //5.1 Enable DB for Notifications... Only needed once per DB...
                            System.Web.Caching.SqlCacheDependencyAdmin.EnableNotifications(connStr);
                            //5.2 Get ElementType for the query
                            string NotificationTable = q.ElementType.Name;
                            //5.3 Enable the elementtype for notification (if not done!)
                            if (!System.Web.Caching.SqlCacheDependencyAdmin.GetTablesEnabledForNotifications(connStr).Contains(NotificationTable))
                                System.Web.Caching.SqlCacheDependencyAdmin.EnableTableForNotifications(connStr, NotificationTable);
                            //6. Create SqlCacheDependency
                            System.Web.Caching.SqlCacheDependency sqldep = new System.Web.Caching.SqlCacheDependency(cmd);
                            // - removed 090506 - 7. Refresh the LinqQuery from DB so that we will not use the current Linq cache
                            // - removed 090506 - dc.Refresh(System.Data.Linq.RefreshMode.OverwriteCurrentValues, q);
                            //8. Execute SqlCacheDepency query...
                            cmd.ExecuteNonQuery();
                            //9. Execute LINQ-query to have something to cache...
                            objCache = q.ToList();
                            //10. Cache the result but use the already created objectCache. Or else the Linq-query will be executed once more...
                            System.Web.HttpRuntime.Cache.Insert(CacheId, objCache, sqldep);
                        }
                    }
                }
                //Return the created (or cached) List
                return objCache;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
    }

IMPORTANT

You need to call SqlDependency.Start for example in global.asax
   //In Application Start Event
   System.Data.SqlClient.SqlDependency.Start("DataContextConnectionString");
   //In Application End Event
   System.Data.SqlClient.SqlDependency.Stop("DataContextConnectionString");

HowTo: Call the Extension Method

So now yo can get call LinqCache<T> on any Linq.IQueryable<T>...
Check the image below for an example from my project.
203430687.png
You can also set some breakpoints in the Extension Method to really see that we collect from the cache until the DB is changed in a way that invalidate our LinqQuery.

Happy Coding
UTB
Last edited May 6 at 5:14 PM  by UTB, version 20
Comments
MAS wrote  Sep 18 2008 at 10:49 PM  
thanks for publishing this.

Can I use it in a query that selects data from multiple tables?

the following lines look like they will only enable the first table:

//5.2 Get ElementType for the query
string NotificationTable = q.ElementType.Name;
//5.3 Enable the elementtype for notification (if not done!)
if (!System.Web.Caching.SqlCacheDependencyAdmin.GetTablesEnabledForNotifications(connStr).Contains(NotificationTable))
System.Web.Caching.SqlCacheDependencyAdmin.EnableTableForNotifications(connStr, NotificationTable);

how can I modify the code to enable all the tables in the query?

UTB wrote  Sep 29 2008 at 10:10 PM  
There is quite a few limitations/rules when writing queries that will work with sqlcachedep. See the list here http://msdn.microsoft.com/en-us/library/ms181122.aspx
You can enable notifications for your tables with this snippet... Once they are enabled the condition will not be true so this will only happen once per table... You could put it in a httpmodule instead (or in global.asax).
Replace the code you posted above with this:
/*foreach (System.Data.Linq.Mapping.MetaTable mt in dc.Mapping.GetTables())
if (!System.Web.Caching.SqlCacheDependencyAdmin.GetTablesEnabledForNotifications(connStr).Contains(mt.TableName))
System.Web.Caching.SqlCacheDependencyAdmin.EnableTableForNotifications(connStr, mt.TableName);*/

You can use joins with LINQ... But I have not been able to get the sqlcachedep. to work with a join if I create a anonymous type... The type returned by the LINQ-query have to be recognized by sql-server as a table to map it to...
Maybe you can solve. Good luck!

//UTB

FBelmonte wrote  Dec 3 2008 at 11:59 PM  
Very very cool your post!! very good!

I not understand this line...

//7. Refresh the LinqQuery from DB so that we will not use the current Linq cache
dc.Refresh(System.Data.Linq.RefreshMode.OverwriteCurrentValues, q);

I remove this line and still works

Thanks
Fbelmonte

Brendan_CSEL wrote  Apr 24 at 5:48 AM  

Sounds great however when I use SQL Profiler to see what this is doing in the database...
1) dc.Refresh(System.Data.Linq.RefreshMode.OverwriteCurrentValues, q); >>>>> runs a select for each record in the table??
2) cmd.ExecuteNonQuery(); >>>>> runs the full select although doesn't return any records (I guess this registers the notification request?)
3) objCache = q.ToList(); >>>>> runs the full select and actually returns the data to be cached.

Does anyone have any idea how to attach the dependancy notification to the Linq-To-SQL data source WITHOUT have to run the query twice? It would be nice to get into the bit where Linq is just about to send the query to the database and attach the notification request then somehow?

Thanks, Brendan.

UTB wrote  Apr 27 at 5:54 PM  
Nice work Brendan!
I remember adding the dc.Refresh(...) because I was getting old values from the database when caching... I did not look into it deep enough to see what the refresh did :(
However... Try to just remove the dc.refresh(...). That should get the sql to only execute once! Do you agree?

Regards
UTB

Updating...
Page view tracker