I have a web API project which is configured to connect to Azure SignalR and clients can connect to it through its hub.
Web Api:
[Authorize]
public class ClientHub : Hub
{
private string GroupName
{
get
{
return $"{Context?.User?.GetClaim(ClaimTypes.Role) ?? null}_{ Context?.User?.GetClaim(ClaimType.hdid) ?? null}";
}
}
public ClientHub()
{
}
public async override Task OnConnectedAsync()
{
Console.WriteLine("OnConnectedAsync");
await Groups.AddToGroupAsync(Context?.ConnectionId, GroupName);
await base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
Console.WriteLine("OnDisconnectedAsync");
return base.OnDisconnectedAsync(exception);
}
}
public class SignalRService : IDataPushService
{
private readonly IHubContext<ClientHub> _hubContext;
public SignalRService(IHubContext<ClientHub> hubContext)
{
_hubContext = hubContext;
}
public Task Receive(string data)
{
return Task.FromResult(0);
}
public async Task Send(PushServiceDataPackage dataPackage)
{
await _hubContext.Clients.Group(dataPackage.GroupName)
.SendAsync(dataPackage.EventType.ToString(), dataPackage.Data);
}
}
ConfigureServices method
// Add SignalR service
bool.TryParse(configuration["SignalRSettings:EnableDetailedErrors"], out bool enableDetailedErrors);
services.AddSignalR(config => config.EnableDetailedErrors = enableDetailedErrors).AddAzureSignalR(configuration["SignalRSettings:ConnectionString"]);
Configure method:
endpoints.MapHub<ClientHub>(configuration["SignalRSettings:HubPath"]);
Console App:
I also have a console app that is meant to use the same SignalR server to send data to connected clients.
bool.TryParse(hostContext.Configuration["SignalRSettings:EnableDetailedErrors"], out bool enableDetailedErrors);
services.AddSignalR(config => config.EnableDetailedErrors = enableDetailedErrors).AddAzureSignalR(hostContext.Configuration["SignalRSettings:ConnectionString"]);
public class SignalRService : IDataPushService
{
private readonly IHubContext<Hub> _hubContext;
public SignalRService(IHubContext<Hub> hubContext)
{
_hubContext = hubContext;
}
public Task Receive(string data)
{
return Task.FromResult(0);
}
public async Task Send(PushServiceDataPackage dataPackage)
{
await _hubContext.Clients.Group(dataPackage.GroupName)
.SendAsync(dataPackage.EventType.ToString(), dataPackage.Data);
}
}
How can I access the clients connected to the server in the console app?
Please note that there are two apps and one Azure SignalR server?
Here I am trying to remove ConnectionId from connected connectedId list UserHandler.ConnectedUser in OnDisconnectedAsync method of my hub.
But the problem is that when ever user gets logout from application or close that window the OnDisconnectedAsync method on hub does not get hit.
Any help with my code will be grate. Thank you.
Below is my hub
public class ChatHub : Hub
{
public List<string> SendingUserList = new List<string>();
public async Task SendMessage(string to, string message)
{
var senderConnectionId = Context.ConnectionId;
SendingUserList.Add(to);
SendingUserList.Add(senderConnectionId);
foreach (var connectionId in SendingUserList)
{
await Clients.Client(connectionId).SendAsync("ReceiveMessage", message);
}
}
public override Task OnConnectedAsync()
{
Clients.All.SendAsync("ReciveUser", UserHandler.UserName, Context.ConnectionId);
return base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception e)
{
UserHandler.ConnectedUser.Remove(Context.ConnectionId));
await base.OnDisconnectedAsync(e);
}
}
public static class UserHandler
{
public static HashSet<string> ConnectedUser = new HashSet<string>();
public static string UserName { get; set; }
}
Below is my remaining code
protected override async Task OnInitializedAsync()
{
UserHandler.UserName = httpContextAccessor.HttpContext.User.Identity.Name;
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.ServerTimeout = TimeSpan.FromMinutes(60);
hubConnection.On<string>("ReceiveMessage", BroadcastMessage);
hubConnection.On<string,string>("ReciveUser", RefreshUserList);
await hubConnection.StartAsync();
StateHasChanged();
}
private void RefreshUserList(string connectedUserId, string connectionId )
{
UserHandler.ConnectedUser.Add(connectionId);
connectedUserList = UserHandler.ConnectedUser;
StateHasChanged();
}
private void BroadcastMessage(string message)
{
var encodedMsg = $"{message}";
messages.Add(encodedMsg);
StateHasChanged();
}
public async ValueTask DisposeAsync()
{
await hubConnection.DisposeAsync();
}
You'll need to implement the IAsyncDisposable interface in the Hub's consumer (Razor Component), like this:
#implements IAsyncDisposable
Add these methods:
public void Dispose()
{
hubConnection.DisposeAsync();
}
public async ValueTask DisposeAsync()
{
await hubConnection.DisposeAsync();
}
Now, when you close the client's page the OnDisconnectedAsync method will be called.
In my App, there is an Async Function ProcessOffer(). When I called it in Constructor as ProcessOffer, it works but synchronously. I want to call this Function in constructor asynchronously.
The ProcessOffer() is a function implemented in CredentialViewModel, but I want that, It should be triggered asynchronously everywhere on the App (IndexViewModel e.t.c).
If I'm on the IndexPage, and Web Application sends a request to Mobile Application, ProcessOffer(), should be triggered.. actually what ProcessOffer does is, that it asks the user to enter a PIN, if it's correct, it sends back a response to the Web Application.
I've tried answers from other Posts, but they returned the Error Autofac.Core.DependencyResolutionException: 'An exception was thrown while activating App.Name, when I sends a request to Mobile App from Web Application.
The Solutions I tried.
1- https://stackoverflow.com/a/64012442/14139029
2- Task.Run(() => ProcessOffer()).Wait();
3- ProcessOffer().GetAwatier().GetResult();
CredentialViewModel.cs
namespace Osma.Mobile.App.ViewModels.Credentials
{
public class CredentialViewModel : ABaseViewModel
{
private readonly CredentialRecord _credential;
private readonly ICredentialService _credentialService;
private readonly IAgentProvider _agentContextProvider;
private readonly IConnectionService _connectionService;
private readonly IMessageService _messageService;
private readonly IPoolConfigurator _poolConfigurator;
[Obsolete]
public CredentialViewModel(
IUserDialogs userDialogs,
INavigationService navigationService,
ICredentialService credentialService,
IAgentProvider agentContextProvider,
IConnectionService connectionService,
IMessageService messageService,
IPoolConfigurator poolConfigurator,
CredentialRecord credential
) : base(
nameof(CredentialViewModel),
userDialogs,
navigationService
)
{
_credential = credential;
_credentialService = credentialService;
_agentContextProvider = agentContextProvider;
_connectionService = connectionService;
_messageService = messageService;
_poolConfigurator = poolConfigurator;
_credentialState = _credential.State.ToString();
if (_credentialState == "Offered")
{
ProcessOffer();
}
}
[Obsolete]
public async Task ProcessOffer()
{
foreach (var item in _credential.CredentialAttributesValues)
{
await SecureStorage.SetAsync(item.Name.ToString(), item.Value.ToString());
}
var RegisteredPIN = await SecureStorage.GetAsync("RegisteredPIN");
string PIN = await App.Current.MainPage.DisplayPromptAsync("Enter PIN", null, "Ok", "Cancel", null, 6, Keyboard.Numeric);
if (PIN == RegisteredPIN)
{
try
{
//await _poolConfigurator.ConfigurePoolsAsync();
var agentContext = await _agentContextProvider.GetContextAsync();
var credentialRecord = await _credentialService.GetAsync(agentContext, _credential.Id);
var connectionId = credentialRecord.ConnectionId;
var connectionRecord = await _connectionService.GetAsync(agentContext, connectionId);
(var request, _) = await _credentialService.CreateRequestAsync(agentContext, _credential.Id);
await _messageService.SendAsync(agentContext.Wallet, request, connectionRecord);
await DialogService.AlertAsync("Request has been sent to the issuer.", "Success", "Ok");
}
catch (Exception e)
{
await DialogService.AlertAsync(e.Message, "Error", "Ok");
}
}
else if (PIN != RegisteredPIN && PIN != null)
{
DialogService.Alert("Provided PIN is not correct");
}
}
#region Bindable Command
[Obsolete]
public ICommand ProcessOfferCommand => new Command(async () => await ProcessOffer());
public ICommand NavigateBackCommand => new Command(async () =>
{
await NavigationService.PopModalAsync();
});
#endregion
#region Bindable Properties
private string _credentialState;
public string CredentialState
{
get => _credentialState;
set => this.RaiseAndSetIfChanged(ref _credentialState, value);
}
#endregion
}
}
IndexViewModel.cs
namespace Osma.Mobile.App.ViewModels.Index
{
public class IndexViewModel : ABaseViewModel
{
private readonly IConnectionService _connectionService;
private readonly IMessageService _messageService;
private readonly IAgentProvider _agentContextProvider;
private readonly IEventAggregator _eventAggregator;
private readonly ILifetimeScope _scope;
public IndexViewModel(
IUserDialogs userDialogs,
INavigationService navigationService,
IConnectionService connectionService,
IMessageService messageService,
IAgentProvider agentContextProvider,
IEventAggregator eventAggregator,
ILifetimeScope scope
) : base(
"Index",
userDialogs,
navigationService
)
{
_connectionService = connectionService;
_messageService = messageService;
_agentContextProvider = agentContextProvider;
_eventAggregator = eventAggregator;
_scope = scope;
}
public override async Task InitializeAsync(object navigationData)
{
await base.InitializeAsync(navigationData);
}
public class Post
{
public string Success { get; set; }
public string firstname { get; set; }
}
[Obsolete]
public async Task ScanVerification(object sender, EventArgs e)
{
// Code
}
public async Task SettingsPage(SettingsViewModel settings) => await NavigationService.NavigateToAsync(settings, null, NavigationType.Modal);
#region Bindable Command
public ICommand SettingsPageCommand => new Command<SettingsViewModel>(async (settings) =>
{
await SettingsPage(settings);
});
[Obsolete]
public ICommand ScanVerificationCommand => new Command(async () => await ScanVerification(default, default));
#endregion
}
}
App.xml.cs
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace Osma.Mobile.App
{
public partial class App : Application
{
public new static App Current => Application.Current as App;
public static IContainer Container { get; set; }
// Timer to check new messages in the configured mediator agent every 10sec
private readonly Timer timer;
private static IHost Host { get; set; }
public App()
{
InitializeComponent();
timer = new Timer
{
Enabled = false,
AutoReset = true,
Interval = TimeSpan.FromSeconds(10).TotalMilliseconds
};
timer.Elapsed += Timer_Elapsed;
}
public App(IHost host) : this() => Host = host;
public static IHostBuilder BuildHost(Assembly platformSpecific = null) =>
XamarinHost.CreateDefaultBuilder<App>()
.ConfigureServices((_, services) =>
{
services.AddAriesFramework(builder => builder.RegisterEdgeAgent(
options: options =>
{
options.AgentName = "Mobile Holder";
options.EndpointUri = "http://11.222.333.44:5000";
options.WalletConfiguration.StorageConfiguration =
new WalletConfiguration.WalletStorageConfiguration
{
Path = Path.Combine(
path1: FileSystem.AppDataDirectory,
path2: ".indy_client",
path3: "wallets")
};
options.WalletConfiguration.Id = "MobileWallet";
options.WalletCredentials.Key = "SecretWalletKey";
options.RevocationRegistryDirectory = Path.Combine(
path1: FileSystem.AppDataDirectory,
path2: ".indy_client",
path3: "tails");
// Available network configurations (see PoolConfigurator.cs):
options.PoolName = "sovrin-test";
},
delayProvisioning: true));
services.AddSingleton<IPoolConfigurator, PoolConfigurator>();
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterAssemblyModules(typeof(CoreModule).Assembly);
if (platformSpecific != null)
{
containerBuilder.RegisterAssemblyModules(platformSpecific);
}
containerBuilder.Populate(services);
Container = containerBuilder.Build();
});
protected override async void OnStart()
{
await Host.StartAsync();
// View models and pages mappings
var _navigationService = Container.Resolve<INavigationService>();
_navigationService.AddPageViewModelBinding<MainViewModel, MainPage>();
_navigationService.AddPageViewModelBinding<RegisterViewModel, RegisterPage>();
_navigationService.AddPageViewModelBinding<IndexViewModel, IndexPage>();
_navigationService.AddPageViewModelBinding<SettingsViewModel, SettingsPage>();
_navigationService.AddPageViewModelBinding<CredentialsViewModel, CredentialsPage>();
_navigationService.AddPageViewModelBinding<CredentialViewModel, CredentialPage>();
if (Preferences.Get(AppConstant.LocalWalletProvisioned, false))
{
await _navigationService.NavigateToAsync<MainViewModel>();
}
else
{
await _navigationService.NavigateToAsync<ProviderViewModel>();
}
timer.Enabled = true;
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
// Check for new messages with the mediator agent if successfully provisioned
if (Preferences.Get(AppConstant.LocalWalletProvisioned, false))
{
Device.BeginInvokeOnMainThread(async () =>
{
try
{
var context = await Container.Resolve<IAgentProvider>().GetContextAsync();
await Container.Resolve<IEdgeClientService>().FetchInboxAsync(context);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
});
}
}
protected override void OnSleep() =>
// Stop timer when application goes to background
timer.Enabled = false;
protected override void OnResume() =>
// Resume timer when application comes in foreground
timer.Enabled = true;
}
}
Look, you have public override async Task InitializeAsync(object navigationData) method in the IndexViewModel class. I suppose the framework invoke it to initialize the view model. So, thus CredentialViewModel inherits same ABaseViewModel anyway, why wouldn't you just override InitializeAsync in your CredentialViewModel and call ProcessOffer from it?
public CredentialViewModel(
IUserDialogs userDialogs,
INavigationService navigationService,
ICredentialService credentialService,
IAgentProvider agentContextProvider,
IConnectionService connectionService,
IMessageService messageService,
IPoolConfigurator poolConfigurator,
CredentialRecord credential
) : base(
nameof(CredentialViewModel),
userDialogs,
navigationService
)
{
_credential = credential;
_credentialService = credentialService;
_agentContextProvider = agentContextProvider;
_connectionService = connectionService;
_messageService = messageService;
_poolConfigurator = poolConfigurator;
_credentialState = _credential.State.ToString();
}
public override async Task InitializeAsync(object navigationData)
{
if (_credentialState != "Offered") return;
await ProcessOffer();
}
Anyway, you have to avoid calling asynchronous operations in constructors.
So in my View Models, if i need to initialize asynchronously I usually pass OnAppearing() to the view model instead of the constructor. Someone can tell me if this is unwise but it solved my needs.
Code Behind View:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CredentialView : ContentPage
{
public CredentialView()
{
InitializeComponent();
BindingContext = new CredentialViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
((CredentialViewModel)BindingContext).OnAppearing();
}
}
View Model:
public class CredentialViewModel : INotifyPropertyChanged
{
public CredentialViewModel ()
{
}
public async void OnAppearing()
{
await ProcessOffer();
}
}
Also just FYI, the biggest gotcha i've experienced with asynchronous code on xamarin is to use BeginInvokeOnMainThread. It's my understanding that touched UI should be executed on main thread! (probably why they call it the UI thread. haha)
Example:
Device.BeginInvokeOnMainThread(() =>
{
observableCollection.Clear();
});
As you've already asked before, kicking off async code in the constructor of a class can be a can of worms. You should instead consider doing either:
Starting ProcessOffer in a lifecycle method instead. For instance when the View is showing call ProcessOfferCommand.
Using fire and forget to not block the constructor: Task.Run(ProcessOffer) you should probably avoid this though.
Use something like NotifyTask Stephen Cleary describes here: https://learn.microsoft.com/en-us/archive/msdn-magazine/2014/march/async-programming-patterns-for-asynchronous-mvvm-applications-data-binding you can find the complete code for it here: https://github.com/StephenCleary/Mvvm.Async/blob/master/src/Nito.Mvvm.Async/NotifyTask.cs
Use a CreateAsync pattern with a private constructor. However, this doesn't work well with Dependency Injection usually. Doing IO intensive work during resolution isn't really the correct place to do it.
In my opinion using either 1. or 3. would be the best solutions, perhaps leaning towards a combination of 1. using NotifyTask that can notify you when it is done loading.
I have implemented a REST API Server with asp.NET Core. I need a background task which does some administrative stuff.
I tried the several suggestions in the web to create a timed background task and implemented it as fpllowing:
BackgroundService.cs
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_executingTask = ExecuteAsync(_stoppingCts.Token);
if (_executingTask.IsCompleted)
{
return _executingTask;
}
return Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
if (_executingTask == null)
{
return;
}
try
{
_stoppingCts.Cancel();
}
finally
{
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
public virtual void Dispose()
{
_stoppingCts.Cancel();
}
}
RecureHostedService.cs
public class RecureHostedService : BackgroundService
{
private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private int _executionCount = 0;
private Timer _timer;
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var count = Interlocked.Increment(ref _executionCount);
log.InfoFormat(
"Timed Hosted Service is working. Count: {0}", count);
await Task.Delay(10000, stoppingToken);
}
}
}
SystemController.cs
public class SystemController : Controller
{
private readonly RecureHostedService _recureHostedService;
public SystemController(IHostedService hostedService)
{
_recureHostedService = hostedService as RecureHostedService;
}
[HttpGet(ApiRoutes.System.Start)]
public IActionResult Start()
{
Console.WriteLine("Start Service");
_recureHostedService.StartAsync(new CancellationToken());
return Ok();
}
[HttpGet(ApiRoutes.System.Stop)]
public IActionResult Stop()
{
Console.WriteLine("Stop Service");
Console.WriteLine(_recureHostedService == null);
_recureHostedService.StopAsync(new CancellationToken());
return Ok();
}
}
in Startup.cs I added the Service as Singleton:
services.AddSingleton<IHostedService,RecureHostedService>();
The RecureHostedService starts on startup of my app and does its work. When I do an APIrequest /api/v1/stop from the SystemController (method Stop()) I got an NullReferenceException in
_recureHostedService.StopAsync(new CancellationToken());
I found out that the private member variable _recureHostedService is null even if I had assigned it in the Constructor of the SystemController. So it seems that the Service Injection of _recureHostedService is not working. So the line
_recureHostedService = hostedService as RecureHostedService;
in the Constructor of SystemController assigns null to _recureHostedService.
How can that be? I've a lot of other Services injected to other Controllers and all works fine.
Any idea from anyone, why it is not working with that Hosted Service?
Best
Mic
I used a set of static variables for myself
public class RecureHostedService : BackgroundService {
public static bool isPause = false;
/* for example
public static string ErrorText;
public static bool isError = false;
public static bool isWorked = false;
public static bool firstStart = true; */
protected async override Task ExecuteAsync(CancellationToken stoppingToken){
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
if (isPause)
continue;
var count = Interlocked.Increment(ref _executionCount);
log.InfoFormat(
"Timed Hosted Service is working. Count: {0}", count);
await Task.Delay(10000, stoppingToken);
}
}
}
public class SystemController : Controller
{
[HttpGet(ApiRoutes.System.Start)]
public IActionResult Start()
{
Console.WriteLine("Start Service");
RecureHostedService.isPause = false;
return Ok();
}
[HttpGet(ApiRoutes.System.Stop)]
public IActionResult Stop()
{
Console.WriteLine("Stop Service");
RecureHostedService.isPause = true;
return Ok();
}
}
You need to register you hosted service using AddHostedService instedad of AddSingleton
so it will be something like this
services.AddHostedService<IHostedService,RecureHostedService>();
If you perfer inject IHostedService on constructor, keep _recureHostedService property declared as type of IHostedService, instead of the concrete class RecureHostedService. It's not necessary down-casting here.
[Route("[controller]")]
public class SystemController : Controller
{
private readonly IHostedService _recureHostedService;
public SystemController(IHostedService hostedService)
{
_recureHostedService = hostedService ?? throw new ArgumentNullException(nameof(hostedService));
}
}
Update
I've tested this controller, no null exception any longer in either this way
services.AddSingleton<IHostedService, RecureHostedService>();
or
services.AddHostedService(p =>
{
return new RecureHostedService();
});
Using dependency injection to obtain an IHostedService will inject whichever hosted service was defined first. Which is likely to be the web server itself. Instead you should define two service definitions that return the same singleton;
services.AddSingleton<RecureHostedService>();
services.AddSingleton<IHostedService>(p => p.GetRequiredService<RecureHostedService>());
public class SystemController : Controller
{
private readonly RecureHostedService _recureHostedService;
public SystemController(RecureHostedService hostedService)
{
_recureHostedService = hostedService;
}
}
I need to resolve a DbContext based on tenant's owin value. But in the pipeline of method OnDisconnected of hub, the HttpContext is not accessible.
My hub class:
public class UserTrackingHub : Hub
{
private readonly UserContext _context;
public UserTrackingHub(UserContext context) { ... }
public override async Task OnConnected() { /* OK HERE...*/ }
public override async Task OnDisconnected(bool stopCalled)
{
// NEVER FIRES WITH IF I USE THE CTOR INJECTION.
var connection = await _context.Connections.FindAsync(Context.ConnectionId);
if (connection != null)
{
_context.Connections.Remove(connection);
await _context.SaveChangesAsync();
}
}
}
Here's my Autofac config:
public static IContainer Register(IAppBuilder app)
{
var builder = new ContainerBuilder();
// Other registers...
builder.Register<UserContext>(c =>
{
// Details and conditions omitted for brevity.
var context = HttpContext.Current; // NULL in OnDisconnected pipeline.
var owinContext = context.GetOwinContext();
var tenant = owinContext.Environment["app.tenant"].ToString();
var connection = GetConnectionString(tenant);
return new UserContext(connection);
});
var container = builder.Build();
var config = new HubConfiguration
{
Resolver = new AutofacDependencyResolver(container)
};
app.MapSignalR(config);
return container;
}
Can someone help me to identify the tenant OnDisconnected in this or any other way?
Thanks!
For anyone interested, I end up injecting a context factory instead the context itself:
public class UserTrackingHub : Hub
{
private readonly Func<string, UserContext> _contextFactory;
public UserTrackingHub(Func<string, UserContext> contextFactory) { ... }
public override async Task OnConnected() { ... }
public override async Task OnDisconnected(bool stopCalled)
{
var tenant = Context.Request.Cookies["app.tenant"].Value;
using (var context = _contextFactory.Invoke(tenant))
{
var connection = await context.Connections.FindAsync(Context.ConnectionId);
if (connection != null)
{
context.Connections.Remove(connection);
await context.SaveChangesAsync();
}
}
}
}
And Autofac's config:
// Resolve context based on tenant
builder.Register<Func<string, UserContext>>(c => new Func<string, UserContext>(tenant => UserContextResolver.Resolve(tenant)));