sql-serverado.netsql-server-2008-r2sqldependencyquery-notifications

How to figure out which SQLDependency triggered change function?


I'm exploring query notifications with the SQLDependency class. Building a simple working example is easy, but I feel like I'm missing something. Once I step past a simple one-table/one-dependency example I'm left wondering how can I figure out which dependency triggered my callback?

I'm having a bit of trouble explaining, so I included the simple example below. When AChange() is called I cannot look at the sql inside the dependency, and i don't have a reference to the associated cache object.

So what's a boy to do?

Am I just missing something? Is this a deficiency in the SQLDependency structure? I've I've looked at 20 different articles on the topic and all of them seem to have the same hole. Suggestions?

Code Sample

public class DependencyCache{
   public static  string       cacheName  = "Client1";
   public static  MemoryCache  memCache   = new MemoryCache(cacheName);

   public DependencyCache() {
      SqlDependency.Start(connString);
   }

   private static string GetSQL() {
      return "select  someString FROM dbo.TestTable";
   }

   public void DoTest() {
      if (memCache["TEST_KEY"] != null ) {
         Debug.WriteLine("resources found in cache");
         return;
      }
      Cache_GetData();
   }

   private void Cache_GetData() {
      SqlConnection         oConn;
      SqlCommand            oCmd;
      SqlDependency         oDep;
      SqlDataReader         oRS;
      List<string>          stuff    = new List<string>();
      CacheItemPolicy       policy   = new CacheItemPolicy();

      SqlDependency.Start(connString);
      using (oConn = new SqlConnection(connString) ) {
         using (oCmd = new SqlCommand(GetSQL(), oConn) ) {
            oDep = new SqlDependency(oCmd);
            oConn.Open();
            oRS = oCmd.ExecuteReader();

            while(oRS.Read() ) {
                  resources.Add( oRS.GetString(0) );
            }

            oDep.OnChange += new OnChangeEventHandler (AChange);
         }
      }
      memCache.Set("TEST_KEY", stuff, policy);
   }

   private void AChange(  object sender, SqlNotificationEventArgs e) {
      string msg= "Dependency Change \nINFO: {0} : SOURCE {1} :TYPE: {2}";
      Debug.WriteLine(String.Format(msg, e.Info, e.Source, e.Type));

      // If multiple queries use this as a callback how can i figure 
      // out WHAT QUERY TRIGGERED the change?
      // I can't figure out how to tell multiple dependency objects apart

      ((SqlDependency)sender).OnChange -= Cache_SqlDependency_OnChange; 
      Cache_GetData(); //reload data
   }
}

Solution

  • First and foremost: the handler has to be set up before the command is executed:

     oDep = new SqlDependency(oCmd);
     oConn.Open();
     oDep.OnChange += new OnChangeEventHandler (AChange);
     oRS = oCmd.ExecuteReader();
     while(oRS.Read() ) {
         resources.Add( oRS.GetString(0) );
     }
    

    Otherwise you have a window when the notification may be lost and your callback never invoked.

    Now about your question: you should use a separate callback for each query. While this may seem cumbersome, is actually trivial by using a lambda. Something like the following:

    oDep = new SqlDependency(oCmd);
    oConn.Open();
    oDep.OnChange += (sender, e) =>
    {
       string msg = "Dependency Change \nINFO: {0} : SOURCE {1} :TYPE: {2}";
       Debug.WriteLine(String.Format(msg, e.Info, e.Source, e.Type));
    
       // The command that trigger the notification is captured in the context:
       //  is oCmd
       //
       // You can now call a handler passing in the relevant info:
       //
       Reload_Data(oCmd, ...);
    };
    oRS = oCmd.ExecuteReader();
    ...
    

    And remember to always check the notification source, info and type. Otherwise you run the risk of spinning ad-nauseam when you are notified for reasons other than data change, like invalid query. As a side comment I would add that a good cache design does not refresh the cache on invalidation, but simply invalidates the cached item and lets the next request actually fetch a fresh item. With your 'proactive' approach you are refreshing cached items even when not needed, refresh multiple times before they are accessed etc etc. I left out from the example error handling and proper thread synchronization (both required).

    Finally, have a look at LinqtoCache which does pretty much what you're trying to do, but for LINQ queries.