4

I am working on a Blazor Server project which uses ASP.NET Core Identity. I'm trying to implement a component to manage current active users. I can access the current identity user for current client, but I can't seem to access the circuit associated with the current client, because I want to track when each user leaves the browser (the circuit is disconnected).

From an answer by '@enet' (link), I tried implementing CircuitHandler, and I can get all the current circuits, but how can I know which circuit is associated with which user?

using Microsoft.AspNetCore.Components.Server.Circuits;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace BlazorCircuitHandler.Services
{
    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)
        {
            Circuit circuitRemoved;
            Circuits.TryRemove(circuit.Id, out circuitRemoved);
            OnCircuitsChanged();
            return base.OnCircuitClosedAsync(circuit, cancellationToken);
        }

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

        public override Task OnConnectionUpAsync(Circuit circuit, 
                             CancellationToken cancellationToken)
        {
            return base.OnConnectionUpAsync(circuit, cancellationToken);
        }
    }
}
LightBulb
  • 964
  • 1
  • 11
  • 27
Kinin Roza
  • 1,908
  • 1
  • 7
  • 17
  • You could try to access the httpcontextaccessor and get the user related information. – Brando Zhang Aug 02 '21 at 08:16
  • 1
    @BrandoZhang I tried actually, but in the HttpContextAccessor object that I injected, there's no information related to Circuit. – Kinin Roza Aug 02 '21 at 09:51
  • Can't you just implement `IDisposable` in you __app.razor__? lifecycle of you root component is bound to lifecycle of your circuit. I'm doing something similar but on page level. If user navigates away or closes browser tab or circuit is otherwise disconnected, the blazor server always calls dispose on your component. Even when there is unhandled exception on server, which breaks the circuit – Liero Dec 13 '22 at 19:25

2 Answers2

3

If you use the AddHttpContextAccessor service

services.AddHttpContextAccessor();

You can get the the user when the circuit is created:

public class MyCircuitIdStashingHandler: CircuitHandler
{
    public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        IHttpContextAccessor accessor = new HttpContextAccessor();
        accessor.HttpContext.User.Identity; // store in dictionary somewhere
        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }
}

...and then you can keep track on what user is using which circuit

LightBulb
  • 964
  • 1
  • 11
  • 27
Hans Karlsen
  • 2,275
  • 1
  • 15
  • 15
  • Microsoft says using `IHttpContextAccessor` in a Blazor app is a security risk: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-6.0#blazor-and-shared-state – user3071284 Jan 21 '22 at 22:34
  • 2
    Yes - it would be a risk using it after creation (it will be undefined later on in the lifecycle) - but creation of circuit will coincide with a controller action call - and getting the user Identity at that point is safe. – Hans Karlsen Feb 24 '22 at 22:05
1
  1. Register your services:
builder.Services.AddSingleton<UserRegisterService>();
builder.Services.AddScoped<CircuitHandler, CircuitHandlerService>();
  1. Move ConcurrentDictionary and event to UserRegisterService:
public class UserRegisterService
{
    public ConcurrentDictionary<Circuit, UserStatus> Users { get; } = new();

    public event Action<Circuit>? UsersChanged;
    public void OnUsersChanged(Circuit circuit)
    {
        UsersChanged?.Invoke(circuit);
    }
}
  1. Inject UserRegisterService and AuthenticationStateProvider into CircuitHandlerService and use within:
public class CircuitHandlerService : CircuitHandler
{
    private readonly UserRegisterService userRegisterService;
    private readonly AuthenticationStateProvider authenticationStateProvider;

    public CircuitHandlerService(UserRegisterService userRegisterService, AuthenticationStateProvider authenticationStateProvider)
    {
        this.userRegisterService = userRegisterService;
        this.authenticationStateProvider = authenticationStateProvider;
    }

    public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        if (userRegisterService.Users.TryGetValue(circuit, out var userStatus))
        {
            userStatus.Status = "Closed";
            userRegisterService.OnUsersChanged(circuit);
        }
        
        return Task.CompletedTask;
    }

    public override async Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        var authState = await authenticationStateProvider.GetAuthenticationStateAsync();
        if (userRegisterService.Users.TryAdd(circuit, new() { UserName = authState.User.Identity?.Name ?? "Anonymous user", Status = "New" }))
        {
            userRegisterService.OnUsersChanged(circuit);
        }
    }

    public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        if (userRegisterService.Users.TryGetValue(circuit, out var userStatus))
        {
            userStatus.Status = "Lost";
            userRegisterService.OnUsersChanged(circuit);
        }

        return Task.CompletedTask;
    }

    public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        if (userRegisterService.Users.TryGetValue(circuit, out var userStatus))
        {
            userStatus.Status = "Up";
            userRegisterService.OnUsersChanged(circuit);
        }

        return Task.CompletedTask;
    }
}
  1. Store anything you want about your users in UserStatus class:
public class UserStatus
{
    private string status;

    required public string UserName { get; init; }
    required public string Status {
        get => status;
        set {
            status = value;
            StatusChange = DateTime.UtcNow;
        }
    }
    public DateTime StatusChange { get; private set; }
}
  1. Since UserRegisterService is registered as a singleton it is guaranteed to hold all users' circuits - current and historical (until your app dies). If you want you can use UserRegisterService in any Razor Component:
@using Microsoft.AspNetCore.Components.Server.Circuits;
@implements IDisposable;
@inject UserRegisterService UserRegisterService;

<h3>User register</h3>

@foreach (var u in UserRegisterService.Users.Where(u => u.Value.Status == "Up"))
{
    <li>@u.Key.Id : @u.Value.UserName : @u.Value.Status : @u.Value.StatusChange</li>
}

@code {
    private void Refresh(Circuit circuit) => InvokeAsync(StateHasChanged);

    protected override void OnInitialized()
    {
        UserRegisterService.UsersChanged += Refresh;
    }
    public void Dispose()
    {
        UserRegisterService.UsersChanged -= Refresh;
    }
}

Hope this helps!

Pawel
  • 891
  • 1
  • 9
  • 31