4

I have developed a webpage that connects to the signalR hub hooking into the jquery code with angularjs. The clients are sent a message when a sqldependency.onchange event occurs, however, for every client connected the message is duplicated to each. So, if two clients are connected, each client receives two messages and so on.

Here are the steps:

  • connect two clients to hub
  • Make DB change
  • sqlDependency.onchange fires
  • Call hub function clients.all.renewProducts()
  • Recreate data respository resetting dependency
  • Client Console: "Database SQL Dependency change detected: Update" app.js:44:12 (Twice)

Hub.cs

public static void SignalRGetData(string data)
{
      IHubContext context = GlobalHost.ConnectionManager.GetHubContext<SignalRGetData>();
      context.Clients.All.renewData(data);

      // Recereate data and sql dependency
      new DataRespository().GetData();
}

DataRespository.cs _dependency_OnChange

public void _dependency_OnChange(object sender, SqlNotificationEventArgs e)
{
    if(e.Info == SqlNotificationInfo.Update)
    {
        ProductHub.GetProducts("Database SQL Dependency change detected: " + e.Info);
    }

}

GetData

public IEnumerable<ProductInventoryDetail> GetData()
{
     using(var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["DynamicPricing"].ConnectionString))
     {
          conn.Open();
          var reposQuery =
          "SELECT [ID], [Program] FROM [DBO].[Detail]";
          using(SqlCommand cmd =  new SqlCommand(reposQuery, conn))
          {
                // remove any command object notifications
                cmd.Notification = null;
                // create dependency
                SqlDependency dependency = new SqlDependency(cmd);
                dependency.OnChange += new OnChangeEventHandler(_dependency_OnChange);

                if (conn.State == System.Data.ConnectionState.Closed)
                    conn.Open();

                // Execute Sql Command
                using(var reader = cmd.ExecuteReader())
                {
                    return reader.Cast<IDataRecord>().Select(x => new ProductInventoryDetail(){

                         ID = x.GetInt32(0),
                         Program = x.GetInt32(1)
                     }
                 }
           }
      }
}

Angular JavaScript

// Apply jQuery SignalR operations to Angular
app.value('$', $);

app.factory('signalRService', ['$', '$rootScope', function ($, $rootScope) {

    var proxy = null;
    var initialise = function () {
        // Get Connection to SignalR Hub
        var connection = $.hubConnection();

        // Create a Proxy
        proxy = connection.createHubProxy('SignalRData');

        // Publish the event when server has a push notification
        proxy.on('renewProducts', function (message) {
            console.log("Database SQL Dependency change detectedgnalRGetData: " + message);
            $rootScope.$emit('renewProducts', message);
        });

        // Start Connection
        connection.start().done(function () {
            console.log("Conenction Ready - invoke proxy");
            proxy.invoke('SignalRGetData');
        });

    };

    return {
        initialise: initialise
    }
}]);
JS1986
  • 1,920
  • 4
  • 29
  • 50
  • Is it possible that you're making the connection twice on the same page? (by calling initialize twice in different controllers). – sirrocco Apr 24 '15 at 03:50
  • Would this be the case by defining an angular service ? – JS1986 Apr 24 '15 at 04:00
  • No, but if you have 2 controllers on the same page and they both get the signalRService and both call the initialise method - you open two connections. – sirrocco Apr 24 '15 at 04:08
  • Correct. There is only one call to the Service and one controller on the page. The duplicating starts when more than one clients are connected, – JS1986 Apr 24 '15 at 04:30

2 Answers2

2

As far as I can see, your code has a couple of problems:

  • Every time when your client connects to the server it invokes SignalRGetData. According to your code SigalRGetData creates a new SqlDependency entity. Thus, if you have two clients you will have two SqlDependency entities or two database subscriptions which means you will have two notifications on the side of each client. If you want to send one notification for each client when a change in the database occurs you should have just only one SqlDependency entity for your application.
  • According to your code you will have a notification just only for the first database change (I know it is strange, but it is true). If you want to receive notifications continuously you should make resubscribtion:

The MSDN contains a sample of how to use the SqlDependency here. Note how, similarly to the SqlNotification usage, the client is expected to subscribe again if it whishes to be further notified

  • SqlDependency has the problems with memory leaks. Hovewer, you can use an open source realization of the SqlDependency class - SqlDependencyEx. It uses a database trigger and native Service Broker notification to receive events about the table changes. With SqlDependecyEx you are able to monitor INSERT, DELETE, UPDATE separately and receive actual changed data (xml) in the event args object. This is an usage example:
int changesReceived = 0;
using (SqlDependencyEx sqlDependency = new SqlDependencyEx(
          TEST_CONNECTION_STRING, TEST_DATABASE_NAME, TEST_TABLE_NAME)) 
{
    sqlDependency.TableChanged += (o, e) => changesReceived++;
    sqlDependency.Start();

    // Make table changes.
    MakeTableInsertDeleteChanges(changesCount);

    // Wait a little bit to receive all changes.
    Thread.Sleep(1000);
}

Assert.AreEqual(changesCount, changesReceived);

Suggestion:

To avoid duplicated client-side notifications it is better to use one notification exemplar for your controller/application. A code example from my last project:

public class HomeController : Controller
{
    // One global subscription for all the controller.
    static HomeController()
    {
        // ITableRowRepository incapsulates SqlDependencyEx usage.
        var repo = (ITableRowRepository)DependencyResolver.Current
                           .GetService(typeof(ITableRowRepository));
        // One global subscription.
        repo.TableChanged += RepoTableChanged;
    }

    // Actions here.

    private static void RepoTableChanged(object sender, TableChangedEventArgs e)
    {
        // Clients notification here.
    }
} 

Hope this helps.

Community
  • 1
  • 1
dyatchenko
  • 2,283
  • 3
  • 22
  • 32
  • you mentioned that there are more than one dependency subscriptions occurring on each client connection. How would one check for an existing? – JS1986 Apr 29 '15 at 01:02
  • @snowcode I don't know. It depends on your business logic. – dyatchenko Apr 29 '15 at 06:46
2

I have a solution 1. Just create a Class with a static attribute bool.

public class OutilContext
{
    public static bool first = true;
}
  1. In your Global.asax.cs, You should control the Session_Start void

    if (OutilContext.first == true)
    {
            OutilContext.first = false;
            NotificationComponent NC = new NotificationComponent();
            var currentTime = DateTime.Now;
    
            NC.RegisterNotification(currentTime); 
    }
    

That control a number of SqlDependency, because when a client accesses the application the Global Class create SqlDependency for each client.