How to tell when a user leaves the site with Blazor Server Side

Viewed 1376

Blazor server side is built on signalr, so I would assume that it knows when a user leaves the website (closes the connection). Is there any event raised that I can use to log this? Or any other way really!

4 Answers

I think this service can help you...

public class CircuitHandlerService : CircuitHandler 
    {
        public ConcurrentDictionary<string, Circuit> Circuits { get; set; }
        public event EventHandler CircuitsChanged;

        protected virtual void OnCircuitsChanged()
        => CircuitsChanged?.Invoke(this, EventArgs.Empty);

        public CircuitHandlerService()
        {
            Circuits = new ConcurrentDictionary<string, Circuit>();
        }

        public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
        {
            Circuits[circuit.Id] = circuit;
            OnCircuitsChanged();
            return base.OnCircuitOpenedAsync(circuit, cancellationToken);
        }

        public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
        {
            Console.WriteLine("OnCircuitClosedAsync");
            Circuit circuitRemoved;
            Circuits.TryRemove(circuit.Id, out circuitRemoved);
            OnCircuitsChanged();
            return base.OnCircuitClosedAsync(circuit, cancellationToken);
        }

        public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
        {
            Console.WriteLine("OnConnectionDownAsync");
            return base.OnConnectionDownAsync(circuit, cancellationToken);
        }

        public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
        {
            return base.OnConnectionUpAsync(circuit, cancellationToken);
        }
               
    }

Testing

Index.razor

@page "/"

@using Microsoft.AspNetCore.Components.Server.Circuits
@using BlazorCircuitHandler.Services

@inject CircuitHandler circuitHandler
@implements IDisposable



<h1>Hello, world!</h1>

Welcome to your new app.

<p>
    Number of Circuits: @((circuitHandler as <BlazorCircuitHandler is a name space in my app>.Services.CircuitHandlerService).Circuits.Count)
    <ul>
        @foreach (var circuit in (circuitHandler as BlazorCircuitHandler.Services.CircuitHandlerService).Circuits)
        {
            <li>@circuit.Key</li>
        }
    </ul>
</p>

@code {

    protected override void OnInitialized()
    {
        (circuitHandler as CircuitHandlerService).CircuitsChanged += HandleCircuitsChanged;
        
    }

    public void Dispose()
    {
        
        (circuitHandler as CircuitHandlerService).CircuitsChanged -= HandleCircuitsChanged;
       
    }

    public void HandleCircuitsChanged(object sender, EventArgs args)
    {
        // notify the UI that the state has changed
          InvokeAsync(() => StateHasChanged());
    }
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();

            services.AddSingleton<CircuitHandler>(new CircuitHandlerService());
        }

Hope this helps...

I would do and have done the following.

Create an interface and an implementation of that interface that fits your scenario with the following signature. This will be your central service that keeps track of your users online status.

public interface IUserOnlineService
{
    void Connect(string circuitId, User user);
    void DisConnect(string circuitId);
}

Inject your implementation of the interface in your startup as a singleton.

Then create a custom class that derives from CircuitHandler. It should use an instance of your previously created IUserOnlineService.

public class CircuitHandlerService : CircuitHandler
{
    public string CircuitId { get; set; }

    IUserOnlineService useronlineservice;
    public CircuitHandlerService(IUserOnlineService useronlineservice)
    {
        this.useronlineservice = useronlineservice;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        CircuitId = circuit.Id;
        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        useronlineservice.DisConnect(circuit.Id);
        return base.OnCircuitClosedAsync(circuit, cancellationToken);
    }
}

Add this service as a scoped service in your startup. This way there will be an instance pr connection.

services.AddScoped<CircuitHandler>((sp) => new CircuitHandlerService(sp.GetRequiredService<IUserOnlineService>()));

Now inject your CircuitHandler and IUserOnLineservice whereever you have your login-logic and then after user-authentication, use the CircuitId property on the Circuithandler which will hold the Id of your current connection and call Connect() on the IUserOnlineService interface. Disconnect is handled in the CircuitHandler as we here only need (and have) the circuit-Id.

As Enet already commented is that if you want to inject the AuthenticationStateProvider in the TrackingCircuitHandler, the TrackingCircuitHandler needs also to be scoped as the AuthenticationStateProvider.

If you want to track the usernames in your application, you can use the In-memory state container service as a singleton and inject this in the TrackingCircuitHandler.

If you just want to log when a user leaves the website, you can skip the IUsersStateContainer and just inject the Logger.

First create a in-memory 'UsersStateContainer':

 public class UsersStateContainer : IUsersStateContainer
    {
        public ConcurrentDictionary<string, string> UsersByConnectionId { get; set; } =
            new ConcurrentDictionary<string, string>();

        public event Action OnChange;
        public void Update(string connectionId, string name)
        {
            UsersByConnectionId.AddOrUpdate(connectionId, name, (key, oldValue) => name );
            NotifyStateChanged();
        }
        public void Remove(string connectionId)
        {
            UsersByConnectionId.TryRemove(connectionId, out var _);
            NotifyStateChanged();
        }
        private void NotifyStateChanged() => OnChange?.Invoke();
    }

Add this service as a scoped service in your startup:

services.AddSingleton<IUsersStateContainer, UsersStateContainer>();

The TrackingCircuitHandler uses the IUserStateContainer:

 public class TrackingCircuitHandler: CircuitHandler
    {

        private IUsersStateContainer _usersStateContainer;
        private AuthenticationStateProvider  _authenticationStateProvider;

        public TrackingCircuitHandler(IUsersStateContainer usersStateContainer, AuthenticationStateProvider authenticationStateProvider)
        {
            _usersStateContainer = usersStateContainer;
            _authenticationStateProvider = authenticationStateProvider;
        }

        public override async Task OnConnectionUpAsync(Circuit circuit, 
            CancellationToken cancellationToken)
        {
            var state =  await _authenticationStateProvider.GetAuthenticationStateAsync();
            _usersStateContainer.Update(circuit.Id, state.User.Identity.Name);

            return ;
        }

        public override Task OnConnectionDownAsync(Circuit circuit, 
            CancellationToken cancellationToken)
        {
            _usersStateContainer.Remove(circuit.Id);

            return Task.CompletedTask;
        }

       
    }

Add this as scoped service to startup.cs:

services.AddScoped<CircuitHandler, TrackingCircuitHandler>();

To show the active users on a blazor page:

@page "/UserStatePage"
@inject IUsersStateContainer UsersStateContainer
@implements IDisposable

 <h2>Connected users</h2>
        <ul>
            @foreach (var user in UsersStateContainer.UsersByConnectionId)
            {
                <li>@user.Key - @user.Value</li>
            }

        </ul>
@code {
    protected override async Task OnInitializedAsync()
    {
        UsersStateContainer.OnChange += OnMyChangeHandler;
    }
    public void Dispose()
    {
        UsersStateContainer.OnChange -= OnMyChangeHandler;
    }
    
    private async void OnMyChangeHandler()
    {
    // invoke on ui thread
        await InvokeAsync(StateHasChanged);
        
    }
}

In your app, there is probably a class that derives from Microsoft.AspNetCore.SignalR.Hub. If not you can create one. Then in this class

public class MyHub : Hub
{
    public override async Task OnDisconnectedAsync(Exception exception)
}

fires when the user disconnects. The Context property of the Hub has information about the user (at least the connection id).

Related