Search code examples
c#service-brokersqldependency

Cannot receive change notifications using SqlDependency


I'm using Sql Server Service Broker for the first time, trying to subscribe to notifications whenever there are changes in a particular table. I'm getting the following exception when I call command.ExecuteReader():

System.InvalidOperationException : When using SqlDependency without providing an options value, SqlDependency.Start() must be called prior to execution of a command added to the SqlDependency instance.

I've created a test to reproduce the scenario (leaving out a few of the related methods for brevity), as follows:

private string _queueName = "EventsToPublishChangeMessages";
    private bool _notificationReceived;

    [Test]
    public void WhyDoesExceptionIndcateSqlDependencyStartHasNotBeenCalledPriorToCommandExecuteReader()
    {
        Console.WriteLine($"canRequestNotifications: {CanRequestNotifications()}"); // returns true
        var connectionString = GetConnectionString();
        var started = SqlDependency.Start(connectionString, _queueName); // exception below seems to suggest that I haven't started the SqlDependency. Am I doing something wrong on this line?
        Console.WriteLine($"Started:{started}"); // returns true
        var connection = new SqlConnection(connectionString);
        var command = new SqlCommand("SELECT Id, EventType, [Data], Created FROM dbo.EventsToPublish", connection);
        var sqlDependency = new SqlDependency(command);
        sqlDependency.OnChange += OnChange;
        connection.Open();

        // The following line causes the exception:
        // System.InvalidOperationException : When using SqlDependency without providing an options value, SqlDependency.Start() must be called prior to execution of a command added to the SqlDependency instance.
        // But you can see that I *did* call SqlDependency.Start() above.
        using (var reader = command.ExecuteReader())
        {
            while (reader.Read())
            {
                Process(reader);
            }
        }

        TryToTriggerANotification(connectionString);
        Thread.Sleep(5000); // ie. wait a few seconds to ensure notification-handling background thread has had a chance to complete.

        Assert.That(_notificationReceived, Is.True);
    }

    private void TryToTriggerANotification(string connectionString)
    {
        using (var connection = new SqlConnection(connectionString))
        {
            using (var command = new SqlCommand(
                    "INSERT INTO dbo.EventsToPublish(Id, EventType, [Data], Created) VALUES(newid(), 'StackOverflowQuestionTest', '', GETDATE())",
                    connection))
            {
                connection.Open();
                command.ExecuteNonQuery();
            }
        }
    }

    private void OnChange(object sender, SqlNotificationEventArgs e)
    {
        _notificationReceived = true;
    }

    private bool CanRequestNotifications()
    {
        try
        {
            var sqlClientPermission = new SqlClientPermission(PermissionState.Unrestricted);
            sqlClientPermission.Demand();
            return true;
        }
        catch
        {
            return false;
        }
    }

I've issued the following SQL queries to try to get a better understanding of what's happening:

select * from sys.service_queues -- can see my EventsToPublishChangeMessages queue
select is_broker_enabled from sys.databases where database_id=db_id() -- returns 1
select * from sys.dm_qn_subscriptions -- returns nothing
select * from sys.transmission_queue -- returns nothing

Has anyone got any ideas on what I might be doing wrong?


Solution

  • The error message says:

    SqlDependency.Start() must be called prior to execution of a command added to the SqlDependency instance

    The help on SqlDependency.Start states:

    Starts the listener for receiving dependency change notifications.

    ...

    Ensure that Start is only called once per AppDomain

    So place a Start() call somewhere in your app startup. Since this looks like a test method, perhaps in a [ClassInitialize] decorated method.