SqlDependency OnChange event not firing for SignalR - asp.net

I know there are multiple questions on SO with a near identical subject, but unfortunately, after following countless guides and reading several answers, I'm at a loss to answer why this is occurring as a new user to SignalR / SqlDependencies.
I have an ASP.Net WebForms app that uses SignalR to push a realtime figure to the page. The code executes on initial load, and fires the event, but after that, I cannot get the event to fire when the dependency detects a change using the OnChange event.
I can see the Queue and SPs are created on the server just fine, but I can't ever see the queue receives any notifications when data is either added/removed or updated in the table. I think it may be something to do with the OnChange resubscription, but I'm not entirely sure.
What could be causing the event to not fire after initial load or the notification not be received from SQL?
I've posted all code below:
Hub Code
Namespace SignalR.Models
<HubName("notificationHub")>
Public Class NotificationHub
Inherits Hub
Private notifCount As Integer
<HubMethodName("sendNotifications")>
Public Sub SendNotifications()
Using db As SqlConnection = New SqlConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings("aspCreate_fetch2").ConnectionString)
Dim query As String = " SELECT IIF(COUNT(l.[id]) > 99, 99, COUNT(l.[id]))
FROM pla.[lv_test] as l
WHERE 1=1
AND l.[lv_int_id] = 419"
Using sp As SqlCommand = New SqlCommand(query, db)
Dim dependency As SqlDependency = New SqlDependency(sp)
AddHandler dependency.OnChange, New OnChangeEventHandler(AddressOf dependency_OnChange)
sp.Notification = Nothing
Dim dt As DataTable = New DataTable()
db.Open()
If db.State = ConnectionState.Closed Then db.Open()
Dim reader = sp.ExecuteReader()
dt.Load(reader)
If dt.Rows.Count > 0 Then
notifCount = Int32.Parse(dt.Rows(0)(0).ToString())
End If
Dim context = GlobalHost.ConnectionManager.GetHubContext(Of NotificationHub)()
context.Clients.All.ReceiveNotification(notifCount)
End Using
End Using
End Sub
Private Sub dependency_OnChange(sender As Object, e As SqlNotificationEventArgs)
If e.Type = SqlNotificationType.Change Then
SendNotifications()
End If
End Sub
End Class
End Namespace
Global ASAX
Sub Application_Start(sender As Object, e As EventArgs)
Dim sConn = System.Web.Configuration.WebConfigurationManager.ConnectionStrings("redacted1").ConnectionString
' Fires when the application is started
RouteConfig.RegisterRoutes(RouteTable.Routes)
BundleConfig.RegisterBundles(BundleTable.Bundles)
SqlDependency.[Stop](sConn)
SqlDependency.Start(sConn)
End Sub
Private Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
Dim sConn = System.Web.Configuration.WebConfigurationManager.ConnectionStrings("redacted1").ConnectionString
SqlDependency.[Stop](sConn)
End Sub
JavaScript
$(function () {
var nf = $.connection.notificationHub;
nf.client.receiveNotification = function (notifCount) {
console.log("connection started");
$("#notif-badge").text(notifCount);
}
$.connection.hub.start().done(function () {
nf.server.sendNotifications();
}).fail(function (e) {
alert(e);
});
});

I am no VB or Javascript expert but I believe that your subscription to OnChange is removed immediately after exiting SendNotifications().
In your code, you have the following dependencies:
SqlDependency -> SqlCommand -> SqlConnection
and you're getting attached to SqlDependency object. However, because your SqlConnection is disposed at the end of the method, your subscription is just gone.
Declare your SqlConnection as a private property and keep the connection open. Besides, move the event subscription to a separate initialization method or the constructor to do it only once.
EDIT :
Here is more or less what I have in mind (in C# , sorry ^^ ):
public class DemoSqlSubscriber : Hub
{
readonly string connectionString = System.Web.Configuration.WebConfigurationManager.ConnectionStrings("aspCreate_fetch2").ConnectionString;
private SqlDependency dependency;
public void StartListening()
{
SqlDependency.Start(connectionString);
SqlConnection connection = new SqlConnection(connectionString);
connection.Open();
SqlCommand command=new SqlCommand();
command.CommandText= "SELECT [notification_count] FROM pla.[notif_count]";
command.Connection = connection;
command.CommandType = CommandType.Text;
dependency = new SqlDependency(command);
dependency.OnChange += new OnChangeEventHandler(OnCountChanged);
}
private void OnCountChanged(object s,SqlNotificationEventArgs e)
{
if(e.Type == SqlNotificationType.Change)
{
// Publish
IHubContext<NotificationHub> context = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();
context.Clients.All.ReceiveNotification(notifCount);
}
}
public void StopListening()
{
SqlDependency.Stop(connectionString);
}
}
Could you try to structure your Hub accordingly in VB. NET and let us know ?

The query for notifications has many restrictions as detailed here. One of the limitations is:
The statement must not use any of the following aggregate functions:
AVG, COUNT(*), MAX, MIN, STDEV, STDEVP, VAR, or VARP.
If the query is invalid for notification subscriptions, the OnChange handler will fire immediately with SqlNotificationType.Invalid.
Below are the SqlNotificationEventArgs property values I get when running a query similar to yours (i.e. with a COUNT aggregate function) when OnChange is invoked:
Info=Invalid, Source=Statement, Type=Subscribe
However, your handler code silently ignores the invalid subscription since it checks for only SqlNotificationType.Change.

Related

Send mail on session_start

I want to invoke method (to sending a mail) in session_start.
This is my task function:
Public Shared Async Function SendEmailMessage(ByVal MailFrom As String, ByVal MailTo As String, ByVal Subject As String, ByVal HtmlMessage As String) As Task
Dim mail As MailMessage = New MailMessage
mail.From = New MailAddress(MailFrom)
mail.To.Add(MailTo)
mail.Subject = Subject
mail.Body = HtmlMessage
Dim smtp As SmtpClient = New SmtpClient("smtpclient")
smtp.Port = 587
smtp.DeliveryMethod = SmtpDeliveryMethod.Network
smtp.UseDefaultCredentials = True
Await Task.Run(Function() smtp.SendMailAsync(mail))
'Await result
End Function
In global.asax i have this code:
Public Overrides Sub Init()
MyBase.Init()
Dim wrapper = New EventHandlerTaskAsyncHelper(AsyncSessionStart)
Me.AddOnAcquireRequestStateAsync(wrapper.BeginEventHandler, wrapper.EndEventHandler)
End Sub
Private Async Function AsyncSessionStart() As Task
If Not Session.IsNewSession Then Return
Await funzioni.SendEmailMessage("***#***.**", "*****#***", "Object text",
String.Format("E' stato effettuato un tentativo di accesso da {0} del {1} alla WebApp.",
Session("name"), Session("office")))
End Function
this is the reference text that I have read and followed asp-net-async-global-asax
visual studio, however, gives me the following error in this part of the code:
Dim wrapper = New EventHandlerTaskAsyncHelper(AsyncSessionStart)
Value of type Task cannot be converted to TaskEventHandler.
where am i wrong?
Without having tested, I believe that this:
Private Async Function AsyncSessionStart() As Task
should be this:
Private Async Sub AsyncSessionStart(sender As Object, e As EventArgs)
The method you specify to the EventHandlerTaskAsyncHelper constructor should be an asynchronous event handler, which means using the standard event handler parameters (which the link you posted does but you ignored) and declaring it an Async Sub.
You also have to use the AddressOf operator to create a delegate in VB. You may also have to specify the delegate type explicitly:
Dim wrapper = New EventHandlerTaskAsyncHelper(AddressOf AsyncSessionStart)
or:
Dim wrapper = New EventHandlerTaskAsyncHelper(New TaskEventHandler(AddressOf AsyncSessionStart))

Personalized push notification with SignalR and SqlDependency

We are working adding real time push notification to Asp.net web application.
I'm able to broadcast one message to all the users who is logged in to website.
but I'm not able to send notification to only one particular user based on the value inserted in the database table.
when i try to do this it's updating all the clients whoever is logged currently.
My code sample below:
SqlDependency Component:
Public Sub RegisterNotification(ByVal currentTime As DateTime)
Try
Dim conStr = ConfigurationManager.ConnectionStrings("constr").ConnectionString
Dim sqlCommand = "SELECT [seq_id],[user_id],[create_timestamp],[alert_read] FROM [dbo].[tblAlerts] WHERE [alert_read]=0 AND [create_timestamp] > #AddedOn"
Using con As New SqlConnection(conStr)
Dim cmd As New SqlCommand(sqlCommand, con)
cmd.Parameters.AddWithValue("#AddedOn", currentTime)
If con.State <> Data.ConnectionState.Open Then
con.Open()
End If
cmd.Notification = Nothing
Dim dependency As New SqlDependency(cmd)
AddHandler dependency.OnChange, AddressOf sqlDep_OnChange
Using reader As SqlDataReader = cmd.ExecuteReader()
Do nothing here
End Using
End Using
Catch ex As Exception
Throw ex
End Try
End Sub
Sub sqlDep_OnChange(ByVal sender As Object, ByVal e As SqlNotificationEventArgs)
Try
If e.Info = SqlNotificationInfo.Insert Then
Dim notificationHub = GlobalHost.ConnectionManager.GetHubContext(Of NotificationHub)
Dim userid = Membership.GetUser.ProviderUserKey
notificationHub.Clients.All.notify(userid)
End If
Dim depend = DirectCast(sender, SqlDependency)
RemoveHandler depend.OnChange, AddressOf sqlDep_OnChange
RegisterNotification(DateTime.UtcNow)
Catch ex As Exception
End Try
End Sub
Notification Hub Code
Public Class NotificationHub
Inherits Hub
Public Sub showdata(ByVal obj As Object)
Try
Dim userobj = obj
Dim notificationHub = GlobalHost.ConnectionManager.GetHubContext(Of NotificationHub)
Dim count = 0
take count from database for userid in the object
notificationHub.Clients.All.setcount(count)
Catch ex As Exception
End Try
End Sub
End Class
SignalR Js code
$(function () {
// signalr js code for start hub and send receive notification
var notificationHub = $.connection.notificationHub;
notificationHub.client.setCount = function (data) {
$('span.count').html(data);
}
$.connection.hub.start().done(function () {
console.log('Notification hub started');
});
//signalr method for push server message to client
notificationHub.client.notify = function (message) {
if (message) {
notificationHub.server.showdata(message);
}
}
})
I have also noticed one more thing here is , sqlDep_OnChange event is called more than once if i have opened applicaiton in more than one browser.
I have managed to do the same with following link:
https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/mapping-users-to-connections
Using SignalR v2 you can use the connectionID to find the corresponding user.

RaiseEvent Issue while Converting Code C# to VB.NET

I have following C# function,
private void btnSendtoGeoDecision_Click(object sender, RoutedEventArgs e)
{
List<FenceModel> lst = new List<FenceModel>();
int FencesInsert;
foreach (FenceModel item in FenceList.SelectedItems)
{
lst.Add(item);
}
BackgroundWorker worker = new BackgroundWorker();
//this is where the long running process should go
worker.DoWork += (o, ea) =>
{
//no direct interaction with the UI is allowed from this method
bool result = objFenceRepository.SendFences(lst, out FencesInsert);
if (result)
{
MessageBox.Show(string.Format("The total of {0} fences have been sent to Geo Decision.", FencesInsert));
}
else
{
MessageBox.Show(string.Format("The error occurs in sending fences to Geo Decision."));
}
};
worker.RunWorkerCompleted += (o, ea) =>
{
//work has completed. you can now interact with the UI
// _busyIndicator.IsBusy = false;
};
//set the IsBusy before you start the thread
// _busyIndicator.IsBusy = true;
worker.RunWorkerAsync();
}
And I Convert it into VB.NET Using Language Converter and It gave me below code
Private Sub btnSendtoGeoDecision_Click(sender As Object, e As RoutedEventArgs)
Dim lst As New List(Of FenceModel)()
Dim FencesInsert As Integer
For Each item As FenceModel In FenceList.SelectedItems
lst.Add(item)
Next
Dim worker As New BackgroundWorker()
'this is where the long running process should go
worker.DoWork += Function(o, ea)
'no direct interaction with the UI is allowed from this method
Dim result As Boolean = objFenceRepository.SendFences(lst, FencesInsert)
If result Then
MessageBox.Show(String.Format("The total of {0} fences have been sent to Geo Decision.", FencesInsert))
Else
MessageBox.Show(String.Format("The error occurs in sending fences to Geo Decision."))
End If
End Function
'work has completed. you can now interact with the UI
' _busyIndicator.IsBusy = false;
worker.RunWorkerCompleted += Function(o, ea)
End Function
'set the IsBusy before you start the thread
' _busyIndicator.IsBusy = true;
worker.RunWorkerAsync()
End Sub
But when I build it, I get following two Errors, I know its asking me to use RaiseEvent since += equivalent to RaiseEvent in VB.Net, But can Anybody Show me how?
'Public Event DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs)' is an event, and cannot be called directly. Use a 'RaiseEvent' statement to raise an event.
'Public Event RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs)' is an event, and cannot be called directly. Use a 'RaiseEvent' statement to raise an event.
You need:
AddHandler worker.DoWork, Sub(o, ea)
' code here
End Function
AddHandler worker.RunWorkerCompleted, Sub(o, ea)
' code here
End Function
For your anonymous eventhandler to work.
If you need anonymous event handler follow Ric answer.
Otherwise you have to declare worker as a member variable of the class/module that includes your function like:
Private WithEvents worker As BackgroundWorker
And then you can create named function that handle your event like this:
Private Sub DoWorkHandler (sender As Object, args As DoWorkEventArgs) Handles worker.DoWork

c# asp.net waiting for state in db to change

I am building a web application where a job submitted by user is handled by a background service and the communication between web app and background service is done on database. So when a search starts, web application inserts a record into db and waits till the status field of the record changes. As I understand, if I implement this on the request thread, I am blocking one of the pool threads unnecessarily, but cannot get my head around to do this asynchronously. What is the best practice in this case?
SQLDependency is very useful, but on the server side it provided only half the solution. I overcame the other half by using SignalR to signal the client that the query is over. In the end it was quite an efficient and elegant solution. Thanks for the info!
public class KYHub : Hub
{
void OnChange(object sender, SqlNotificationEventArgs e)
{
SqlDependency dependency = sender as SqlDependency;
dependency.OnChange -= OnChange;
if (e.Info != SqlNotificationInfo.Error && e.Info != SqlNotificationInfo.Delete)
{
string connstr = System.Configuration.ConfigurationManager.ConnectionStrings["cnnStr"].ConnectionString;
SqlConnection conn = new SqlConnection(connstr);
conn.Open();
SearchResult result= SearchResult.Parse(conn, Caller.TaskID);
conn.Close();
Caller.endsearch(result);
}
}
public void Search(SearchParam search)
{
string connstr = System.Configuration.ConfigurationManager.ConnectionStrings["cnnStr"].ConnectionString;
SqlConnection conn = new SqlConnection(connstr);
conn.Open();
search.ClientID = Caller.id;
int QueryID = search.Save(conn);
Caller.TaskID = QueryID;
SqlCommand cmd = new SqlCommand(String.Format("SELECT Completed FROM dbo.Tasks WHERE TaskID={0}", QueryID), conn);
SqlDependency dep = new SqlDependency(cmd);
dep.OnChange += OnChange;
cmd.ExecuteReader();
conn.Close();
}
}
If you are using SQL Server as your DB then you can use a ADO.NET feature called SQLDependency,
What this dose when setup correctly is, when ever there is a change in table (which you will configure) a C# event will be raised.
Here is a article that expains it
Note: You will have to setup your SQL server to enable this.
http://www.codeproject.com/Articles/12335/Using-SqlDependency-for-data-change-events
And this one expains how to refresh page after data in table has changed
http://msdn.microsoft.com/en-us/library/e3w8402y%28v=vs.80%29.aspx

Catching errors or exceptions

I built a mashup of google maps and weather.com and everytime one of these server is not responding my application hangs up too.What do you think I can do to prevent or minimize hanging up of my web apps?Hanging up like you can't navigate away from that page....
I got this code on my app code to access the weather service;
Public Class WeatherIn
Private _path As String
Private _cachedFile As String
Public Sub New(ByVal path As String)
_path = path
_cachedFile = String.Format("{0}\WeatherInCache.xml", _path)
End Sub
Public Function GetWeather(ByVal arg As String) As String
Return _getWebWeather(arg)
End Function
Private Function _getCachedWeather() As String
Dim str As String = String.Empty
Using reader As New StreamReader(_cachedFile)
str = reader.ReadToEnd()
End Using
Return str
End Function
Private Function _getWebWeather(ByVal arg As String) As String
Dim baseUrl As String = "http://xoap.weather.com/weather/local/{0}?cc=*&dayf=5&link=xoap&prod=xoap&par={1}&key={2}"
Dim jane As String = arg
Dim james As String = "api key"
Dim john As String = "another api key"
Dim url As String = String.Format(baseUrl, jane, james, john)
Using client As New WebClient()
Try
Dim xml As New XmlTextReader(client.OpenRead(url))
Dim xslt As New XslCompiledTransform()
xslt.Load(_path + "/Pathto.xslt")
Using writer As New StreamWriter(_cachedFile)
xslt.Transform(xml, Nothing, writer)
End Using
Return _getCachedWeather()
Catch exception As WebException
Dim xmlStr As String = "<errorDoc>"
xmlStr += "<alert>An Error Occurred!</alert>"
xmlStr += [String].Format("<message>{0}</message>", exception.Message)
xmlStr += "</errorDoc>"
Dim doc As New XmlDocument()
doc.LoadXml(xmlStr)
Dim reader As New XmlNodeReader(doc)
Dim xslt As New XslCompiledTransform()
xslt.Load(_path + "/Pathto.xslt")
Dim resultDocument As New XmlDocument()
Using writer As XmlWriter = resultDocument.CreateNavigator().AppendChild()
xslt.Transform(reader, DirectCast(Nothing, XsltArgumentList), writer)
End Using
Return resultDocument.OuterXml
End Try
End Using
End Function
Then I used the class above on my page where I display the weather like this:
'specific zip code or could be retrieved from querystring for dynamic retrieval
var jay="94576"
Dim weather As New WeatherIn(Server.MapPath(String.Empty))
Dim weatherData As String = weather.GetWeather(jay)
Response.ContentType = "text/xml"
Response.CacheControl = "no-cache"
Response.Write(weatherData)
which I retrieve the data and write on the page through javascript.Most of the time its the weather.com that goes down.I got no problem with google map its reliable....anybody got a solution why my page hangs up too if the remote server is not responding?The mashup is running smoothly if the remote server is responding...
When depending on external web services it is best to load asynchronously so that if one of them is slow you can show some kind of loading spinner to the page viewer. If it fails your page could simply report that reading from the web server failed and to try again later.
In this case I would load up the page with the Google map in place and then make an AJAX request for the Weather data.

Resources