SignalR connection to old browser drops if new browser opens - c#

I have application in which I am showing data from sensors using SignalR. It uses ASP.net membership to authenticate the users. It all works fine if I only open one browser window(e.g. Firefox). If I open same website in another browser e.g. Chrome at the same time then signalR connection to firefox browser drops even if the user is different. This is what I am using to broadcast message:
Hub
[Authorize]
public class DataHub:Hub
{
private readonly RealTimeData _sensor;
public DataHub() : this(RealTimeData.Instance) { }
public DataHub(RealTimeData data)
{
_sensor = data;
}
public override Task OnConnected()
{
// _sensor.UserId = Context.ConnectionId; changed to
_sensor.UserId = Membership.GetUser().ProviderUserKey.ToString();
return base.OnConnected();
}
}
public class RealTimeData
{
//User Id
public String UserId { get; set; }
private readonly static Lazy<RealTimeData> _instance = new Lazy<RealTimeData>(() => new RealTimeData(GlobalHost.ConnectionManager.GetHubContext<DataHub>().Clients));// Singleton instance
private IHubConnectionContext Clients;
private void BroadcastDataOfAllSensors(List<SensorDetails> sensor)
{
//Clients.Client(UserId).updateDashboard(sensor);changed to
Clients.User(UserId).updateDashboard(sensor);
}
}
Application Startup
public class StartUp
{
public void Configuration(IAppBuilder app)
{
var idProvider = new UserIdProvider();
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);
app.MapSignalR();
}
}
UserId
public class UserIdProvider : IUserIdProvider
{
public string GetUserId(IRequest request)
{
var userId = Membership.GetUser().ProviderUserKey;
return userId.ToString();
}
}

Related

hubconnection start per every request and change context

I need to connect to my SignalR Core hub in my controller
(I don't want to use dependency injection for invoking my hub methods)
but I don't want to start my HubConnection every time I call my controller
what should I do?
My controller:
private static readonly string url = "http://localhost:10580/sgr/rtc";
public HubConnection Connection { get; private set; } = new HubConnectionBuilder().WithUrl(url).Build();
public SampleDataController()
{
Task.Run(async () => await Connection.StartAsync());
}
[HttpGet("[action]")]
public async Task<IActionResult> GetConnectionId()
{
try
{
var str = await Connection.InvokeAsync<string>("GetConnectionId");
return Ok(str);
}
catch (Exception)
{
return BadRequest("Connection is not established");
}
}
and my hub:
public string GetConnectionId()
{
return Context.ConnectionId;
}
Now I get different connectionId for each request but I want to get just one connectionId for a client.
You could try to register HubConnection as Singleton like
public class Startup
{
private static readonly string url = "https://localhost:44379/ChatHub";
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<HubConnection>((serviceProvider) => {
var hubConnection = new HubConnectionBuilder().WithUrl(url).Build();
hubConnection.StartAsync().Wait();
return hubConnection;
});
}
}
And then access HubConnection with
public class HomeController : Controller
{
private readonly HubConnection Connection;
public HomeController(HubConnection connection)
{
Connection = connection;
}
}

How to configure amazon s3 client for multi region in .net core?

I have asp.net core application. Given S3 urls, the application needs to download file. These S3 urls may belongs to different AWS regions in the US.
public class Downloader
{
public void DownloadFile(string s3Url, string destFolder)
{
//download files
}
}
The DownloadFile() method gets called concurrently. Each url pass to this method may belong to different region.
AWS documentation shows how to Configure the AWS SDK for .NET with .NET Core. In my case AWS credentials are stored in profile file on server and the same credentials can be used across all US regions. So appsettings.json looks like
"AWS": {
"Profile": "default",
"ProfilesLocation": "C:\\aws\\awsprofile"
},
Issue
Since URLS can belong to different region, i cannot follow documentation code. I cannot register IAmazonS3 with DI framework and inject that instance into Downloader to download files from different regions. Because IAmazonS3 instance tried to a particular region.
Solution
So i created a factory which provides instance of IAmazonS3 given region name.
public interface IS3ClientFactory : IDisposable
{
IAmazonS3 GetS3Client(RegionEndpoint region);
}
public class S3ClientFactory : IS3ClientFactory
{
private bool _disposed = false;
private IDictionary<string, IAmazonS3> _container = null;
private S3ClientFactory()
{
_container = new Dictionary<string, IAmazonS3>();
}
public static IS3ClientFactory Configure(AWSOptions option, RegionEndpoint[] regions)
{
var factory = new S3ClientFactory();
foreach (RegionEndpoint region in regions)
{
option.Region = region;
factory._container.Add(region.SystemName, option.CreateServiceClient<IAmazonS3>());
}
return factory;
}
public IAmazonS3 GetS3Client(RegionEndpoint region)
{
return _container[region.SystemName];
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (_container != null && _container.Any())
{
foreach (var s3Client in _container)
{
if (s3Client.Value != null)
{
s3Client.Value.Dispose();
}
}
}
_disposed = true;
}
}
}
}
and in startup.cs register factory with DI
services.AddSingleton<IS3ClientFactory>(S3ClientFactory.Configure(Configuration.GetAWSOptions(),
new RegionEndpoint[]
{
RegionEndpoint.USWest1,
RegionEndpoint.USWest2,
RegionEndpoint.USEast1,
RegionEndpoint.USEast2
}));
and Downloader class would look like
public class Downloader : IDownloader
{
private readonly IS3ClientFactory _factory;
public Downloader(IS3ClientFactory factory)
{
_factory = factory;
}
public void DownloadFile(string s3Url, string destFolder)
{
var s3Uri = new AmazonS3Uri(s3Url);
var s3Client = _factory.GetS3Client(s3Uri.Region);
// use s3Client to download file
}
}
Questions
In S3ClientFactory's Configure method i am dynamically assigning RegionEndpoint to AWSOptions and then call option.CreateServiceClient<IAmazonS3>() is this a correct way of creating region specific instance of IAmazonS3? The code need to be unit testable so i could not use new AmazonS3Client(RegionEndpoint).
foreach (RegionEndpoint region in regions)
{
option.Region = region;
factory._container.Add(region.SystemName, option.CreateServiceClient<IAmazonS3>());
}
Is it okay to have singleton instance of IAmazonS3?
You can just inject IEnumerable of your service interface. And then find the instance that you want using LINQ.
Startup
foreach (string snsRegion in Configuration["SNSRegions"].Split(',', StringSplitOptions.RemoveEmptyEntries))
{
services.AddAWSService<IAmazonSimpleNotificationService>(
string.IsNullOrEmpty(snsRegion) ? null :
new AWSOptions()
{
Region = RegionEndpoint.GetBySystemName(snsRegion)
}
);
}
services.AddSingleton<ISNSFactory, SNSFactory>();
services.Configure<SNSConfig>(Configuration);
SNSConfig
public class SNSConfig
{
public string SNSDefaultRegion { get; set; }
public string SNSSMSRegion { get; set; }
}
appsettings.json
"SNSRegions": "ap-south-1,us-west-2",
"SNSDefaultRegion": "ap-south-1",
"SNSSMSRegion": "us-west-2",
SNS Factory
public class SNSFactory : ISNSFactory
{
private readonly SNSConfig _snsConfig;
private readonly IEnumerable<IAmazonSimpleNotificationService> _snsServices;
public SNSFactory(
IOptions<SNSConfig> snsConfig,
IEnumerable<IAmazonSimpleNotificationService> snsServices
)
{
_snsConfig = snsConfig.Value;
_snsServices = snsServices;
}
public IAmazonSimpleNotificationService ForDefault()
{
return GetSNS(_snsConfig.SNSDefaultRegion);
}
public IAmazonSimpleNotificationService ForSMS()
{
return GetSNS(_snsConfig.SNSSMSRegion);
}
private IAmazonSimpleNotificationService GetSNS(string region)
{
return GetSNS(RegionEndpoint.GetBySystemName(region));
}
private IAmazonSimpleNotificationService GetSNS(RegionEndpoint region)
{
IAmazonSimpleNotificationService service = _snsServices.FirstOrDefault(sns => sns.Config.RegionEndpoint == region);
if (service == null)
{
throw new Exception($"No SNS service registered for region: {region}");
}
return service;
}
}
public interface ISNSFactory
{
IAmazonSimpleNotificationService ForDefault();
IAmazonSimpleNotificationService ForSMS();
}
Now you can get the SNS service for the region that you want in your custom service or controller
public class SmsSender : ISmsSender
{
private readonly IAmazonSimpleNotificationService _sns;
public SmsSender(ISNSFactory snsFactory)
{
_sns = snsFactory.ForSMS();
}
.......
}
public class DeviceController : Controller
{
private readonly IAmazonSimpleNotificationService _sns;
public DeviceController(ISNSFactory snsFactory)
{
_sns = snsFactory.ForDefault();
}
.........
}

Managing SignalR connections for Anonymous user

I am using SignalR version 2.1.2 with ASP.Net MVC 5 & NServiceBus and have following requirement
There is a signup page (anonymous authentication) in which SignalR is used to send notifications. Every form submit will generate a new connection id which needs to be kept in a collection so that I can send response to the client. Context.User.Identity.Name is empty hence _connections.Add(name, Context.ConnectionId); cannot be used in OnConnected() hub event as given in this post
Similar problem exists in Login page.
If there is a possibility to control the ConnectionId then I could overcome this situation but it looks like new version of SignalR has got rid of connection factory.
I am using Redis cache so one option is to write my own connection management code to keep these connection ids in it.
Second option is to use Forms Authentication in such a way that a 'Anonymous Role' is assigned to these users which restricts the usage to anonymous views/controllers but gives a 'Name' to the user so that Context.User.Identity.Name is not empty. With this I can use built in SignalR mechanism to manage connection ids for me.
This is what we did in BaseAnonymousController
public class BaseAnonymousController : Controller
{
protected override void OnAuthentication(System.Web.Mvc.Filters.AuthenticationContext filterContext)
{
if (filterContext.Controller.GetType().Name == "AccountController" && filterContext.ActionDescriptor.ActionName == "login")
{
Guid result;
if (!string.IsNullOrEmpty(SessionVariables.UserId) && Guid.TryParse(SessionVariables.UserId, out result))
{
//Already a anonymous user, so good to go.
}
else
{
//Seems to be a logged in a user. So, clear the session
Session.Clear();
}
}
//Perform a false authentication for anonymous users (signup, login, activation etc. views/actions) so that SignalR will have a user name to manage its connections
if (!string.IsNullOrEmpty(SessionVariables.UserId))
{
filterContext.HttpContext.User = new CustomPrincipal(new CustomIdentity(SessionVariables.UserId, "Anonymous"));
}
else
{
string userName = Guid.NewGuid().ToString();
filterContext.HttpContext.User = new CustomPrincipal(new CustomIdentity(userName, "Anonymous"));
FormsAuthentication.SetAuthCookie(userName, false);
SessionVariables.UserId = userName;
}
base.OnAuthentication(filterContext);
}
}
and used this class as base class for all of anonymous controllers.
public class AccountController : BaseAnonymousController
{
[AllowAnonymous]
public ActionResult Signup()
{
//Your code
}
[AllowAnonymous]
public ActionResult Login()
{
//Your code
}
[AllowAnonymous]
public ActionResult ForgotPassword()
{
//Your code
}
[AllowAnonymous]
public ActionResult ForgotUsername()
{
//Your code
}
}
In the SignalR hub (nothing extraordinary than what is in SignalR documentation)
public override Task OnConnected()
{
SignalRConnectionStore.Add(Context.User.Identity.Name, Context.ConnectionId);
return base.OnConnected();
}
public override Task OnReconnected()
{
string name = Context.User.Identity.Name;
//Add the connection id if it is not in it
if (!SignalRConnectionStore.GetConnections(name).Contains(Context.ConnectionId))
{
SignalRConnectionStore.Add(name, Context.ConnectionId);
}
return base.OnReconnected();
}
public override Task OnDisconnected(bool stopCalled)
{
SignalRConnectionStore.Remove(Context.User.Identity.Name, Context.ConnectionId);
return base.OnDisconnected(stopCalled);
}
This works for both anonymous and authenticated users.
SignalRConnectionStore class and Interface
public interface ISignalRConnectionStore
{
int Count { get; }
void Add(string userName, string connectionId);
IEnumerable<string> GetConnections(string userName);
void Remove(string userName, string connectionId);
}
internal class SignalRConnectionStore : ISignalRConnectionStore
{
private readonly Dictionary<string, HashSet<string>> _connections = new Dictionary<string, HashSet<string>>();
public int Count
{
get
{
return _connections.Count;
}
}
public void Add(string userName, string connectionId)
{
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(connectionId))
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(userName, out connections))
{
connections = new HashSet<string>();
_connections.Add(userName, connections);
}
lock (connections)
{
connections.Add(connectionId);
}
}
}
}
public IEnumerable<string> GetConnections(string userName)
{
if (!string.IsNullOrEmpty(userName))
{
HashSet<string> connections;
if (_connections.TryGetValue(userName, out connections))
{
return connections;
}
}
return Enumerable.Empty<string>();
}
public void Remove(string userName, string connectionId)
{
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(connectionId))
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(userName, out connections))
{
return;
}
lock (connections)
{
connections.Remove(connectionId);
if (connections.Count == 0)
{
_connections.Remove(userName);
}
}
}
}
}
}
Declare a static variable of SignalRConnectionStore in Hub class as below.
public class ProvisioningHub : Hub
{
private static ISignalRConnectionStore SignalRConnectionStore;
public ProvisioningHub(ISignalRConnectionStore signalRConnectionStore)
: base()
{
SignalRConnectionStore = signalRConnectionStore; //Injected using Windsor Castle
}
}
Use Forms Authentication, store a Federated Cookie and store the hub region in the cookie as well..
In SignalR jQuery code, use a jQuery plugin to read HTTP cookie and get the region name and subscribe to notifications.
Alternatively, in your .cshtml, render jQuery with region populated from your View Model.
Note: Use FormsAuthentication.SetAuthCookie as this will create HTTP Only cookie and will be sent in Ajax and non-Ajax calls.

SignalR 2 Not working on azurewebsites

I have a simple SignalR based timer on the server which updates my dashboard panel. Everything works fine on VS2012, however when I deploy to azure or IIS7.5 it can't stablish a connection. Here are the errors from console.
This is my hub:
public class BroadCastHub : Hub
{
// Is set via the constructor on each creation
//private readonly Broadcaster _broadcaster;
private readonly TimeSpan BroadcastInterval = TimeSpan.FromMilliseconds(AppConfig.Instance.Tracing.RefreshRate * 1000);
public BroadCastHub() {
if (AppConfig.Instance.Tracing.EnableServerAutoUpdates)
{
// Start the broadcast loop
var _broadcastLoop = new Timer(
UpdatePanel,
null,
BroadcastInterval,
BroadcastInterval);
}
}
public void UpdatePanel()
{
UpdatePanel(null);
}
public void UpdatePanel(object o)
{
var appService = DependencyResolver.Current.GetService<IApplicationService>();
var applications = appService.GetSummaryCollection();
var model = applications.Select(c => new ApplicationState
{
id = c.id,
lastRunTime = c.lastRunTime.GetValueOrDefault(),
totalTraces = c.totalTraces,
status = appService.GetStatus(c.lastRunTime, c.lastTraceType, c.traceInterval)
}.InjectFrom(c)).Cast<ApplicationState>().AsQueryable();
Clients.All.updatePanel(model);
}
}
This is my JS code (Angular):
function init() {
var broadCastHub = $.connection.broadCastHub;
broadCastHub.client.updatePanel = function(apps) {
console.log('Broadcasting');
};
$.connection.hub.start()
.done(function () {
console.log('Now connected, connection ID=' + $.connection.hub.id);
})
.fail(function () { console.log('Could not Connect!'); });
//listenBroadcast();
}
EDIT:
Curiously, I saw a similar article and implemented like this:
public class Broadcaster
{
private readonly IApplicationService _applicationService;
private readonly static Lazy<Broadcaster> _instance = new Lazy<Broadcaster>(() => new Broadcaster());
// We're going to broadcast to all clients a maximum of 25 times per second
private readonly TimeSpan BroadcastInterval = TimeSpan.FromMilliseconds(AppConfig.Instance.Tracing.RefreshRate * 1000);
private readonly IHubContext _hubContext;
private Timer _broadcastLoop;
public Broadcaster()
{
_applicationService = DependencyResolver.Current.GetService<IApplicationService>();
// Save our hub context so we can easily use it
// to send to its connected clients
_hubContext = GlobalHost.ConnectionManager.GetHubContext<BroadCastHub>();
if (AppConfig.Instance.Tracing.EnableServerAutoUpdates) {
// Start the broadcast loop
_broadcastLoop = new Timer(
UpdatePanel,
null,
BroadcastInterval,
BroadcastInterval);
}
}
public void UpdatePanel(object state)
{
var applications = _applicationService.GetSummaryCollection();
var model = applications.Select(c => new ApplicationState
{
id = c.id,
lastRunTime = c.lastRunTime.GetValueOrDefault(),
totalTraces = c.totalTraces,
status = _applicationService.GetStatus(c.lastRunTime, c.lastTraceType, c.traceInterval)
}.InjectFrom(c)).Cast<ApplicationState>().AsQueryable();
_hubContext.Clients.All.updatePanel(model);
}
public static Broadcaster Instance
{
get
{
return _instance.Value;
}
}
}
My hub:
public class BroadCastHub : Hub
{
// Is set via the constructor on each creation
private readonly Broadcaster _broadcaster;
public BroadCastHub()
: this(Broadcaster.Instance)
{
}
public BroadCastHub(Broadcaster broadcaster)
{
_broadcaster = broadcaster;
}
public void UpdatePanel()
{
_broadcaster.UpdatePanel(null);
}
}
Thanks for the article, I'll take a deeper look.

global object that needs to be referenced in a message handler

I have a signalr client that I want to be global.
I think creating the signalr client in the Init() of the endpointconfig would be best.
public class EndpointConfig : IConfigureThisEndpoint, AsA_Server, IWantCustomInitialization
{
public static HubConnection hubConnection;
public static IHubProxy hubProxy;
public void Init()
{
Configure.With()
.DefiningEventsAs(t => t.Namespace != null && t.Namespace.Contains(".Events."))
.DefiningMessagesAs(t => t.Namespace != null && t.Namespace.Contains(".Messages."))
.StructureMapBuilder(new Container(new DependencyRegistry()));
Configure.Serialization.Json();
hubConnection = new HubConnection("http://localhost:58120");
hubProxy = hubConnection.CreateHubProxy("AmsHub");
hubProxy.On<string>("receiveServerPush", x => System.Diagnostics.Debug.WriteLine(x));
hubConnection.Start().Wait();
}
public class DependencyRegistry : Registry
{
public DependencyRegistry()
{
Scan(x =>
{
x.AssembliesFromApplicationBaseDirectory();
x.ExcludeNamespace("StructureMap");
x.WithDefaultConventions();
});
}
}
}
What I'm confused about, is how am I supposed to reference the hubConnection and hubProxy in a message handler? I seems like I'm jerry rigging NServicebus.
public class TestHandler : IHandleMessages<AMS.Infrastructure.Events.IEvent>
{
public void Handle(AMS.Infrastructure.Events.IEvent message)
{
EndpointConfig.hubProxy.Invoke("ServerFunction", "yodle");
}
}
PS: the reason I need the connection and proxy to be global is because spawning up a new hubConnection is expensive according to the signalr people. They highly discourage creating and destroying hubconnections over and over again. They found that making the hubconnection global/static(?) ok though.
In this case, your Hub Connection/Proxy really are unrelated to the EndPointConfiguration class. They don't use nor require any data from this type in order to function.
I would recommend placing them in their own lazy initialized singleton, and start them automatically upon first access. This would look like:
public class Hub
{
private static Lazy<Hub> instance = new Lazy<Hub>(() => new Hub());
public static Hub Instance { get { return instance.Value; } }
private Hub()
{
this.Connection = new HubConnection("http://localhost:58120");
this.Proxy = Connection.CreateHubProxy("AmsHub");
this.Proxy.On<string>("receiveServerPush", x => System.Diagnostics.Debug.WriteLine(x));
this.Connection.Start().Wait();
}
public HubConnection Connection { get; private set; }
public IHubProxy Proxy { get; private set; }
}
Your consumers then just use:
public class TestHandler : IHandleMessages<AMS.Infrastructure.Events.IEvent>
{
public void Handle(AMS.Infrastructure.Events.IEvent message)
{
Hub.Instance.Proxy.Invoke("ServerFunction", "yodle");
}
}
This has the benefit of not creating and starting until first use, and isolates this type into it's own class.
Given that you're also handling the subscription internally, you also could, optionally, encapsulate your methods to simplify usage:
public class Hub
{
private static Lazy<Hub> instance = new Lazy<Hub>(() => new Hub());
public static Hub Instance { get { return instance.Value; } }
private Hub()
{
this.Connection = new HubConnection("http://localhost:58120");
this.Proxy = Connection.CreateHubProxy("AmsHub");
this.Proxy.On<string>("receiveServerPush", x => System.Diagnostics.Debug.WriteLine(x));
this.Connection.Start().Wait();
}
private HubConnection Connection { get; set; }
private IHubProxy Proxy { get; set; }
public static Task Invoke(string method, params Object[] args)
{
return Instance.Proxy.Invoke(method, args);
}
public static Task<T> Invoke<T>(string method, params Object[] args)
{
return Instance.Proxy.Invoke<T>(method, args);
}
}
With the above, you could just use: Hub.Invoke("ServerFunction", "yodle");
#reed-copsey Old post, but thanks for your reply, it helped me a lot.
In my case I am creating an Azure Function, which will connect to an SignalR Hub which is part of an ASP.NET MVC site. I needed the connection to be secure / authenticated before sending a notification.
So my example included authenticating and getting a cookie.
public class Hub
{
private static readonly string HOMEPAGE = ConfigurationManager.AppSettings["Homepage"];
private static readonly string NOTIFICATION_USER = ConfigurationManager.AppSettings["NotificationUser"];
private static readonly string NOTIFICATION_PASSWORD = ConfigurationManager.AppSettings["NotificationPassword"];
private static Lazy<Hub> instance = new Lazy<Hub>(() => new Hub());
public static Hub Instance { get { return instance.Value; } }
private Hub()
{
ClientHandler = new HttpClientHandler();
ClientHandler.CookieContainer = new CookieContainer();
using (Client = new HttpClient(ClientHandler))
{
var content = string.Format("Email={0}&Password={1}", NOTIFICATION_USER, NOTIFICATION_PASSWORD);
var response = this.Client.PostAsync(HOMEPAGE + "/Account/Login", new StringContent(content, Encoding.UTF8, "application/x-www-form-urlencoded")).Result;
}
Connection = new HubConnection($"{HOMEPAGE}/");
Connection.CookieContainer = ClientHandler.CookieContainer;
Proxy = Connection.CreateHubProxy("notificationsHub");
//this.Proxy.On<string>("receiveServerPush", x => System.Diagnostics.Debug.WriteLine(x));
Connection.Start().Wait();
}
public HttpClientHandler ClientHandler { get; private set; }
public HttpClient Client { get; private set; }
public HubConnection Connection { get; private set; }
public IHubProxy Proxy { get; private set; }
public static Task Invoke(string method, params Object[] args)
{
return Instance.Proxy.Invoke(method, args);
}
public static Task<T> Invoke<T>(string method, params Object[] args)
{
return Instance.Proxy.Invoke<T>(method, args);
}
}

Categories