User specific setting cached in a Blazor Server application

Viewed 99

I have to obtain quite in every "page" (razor component) the EmployeeId of the logged User.

I have a MyComponentBase class that derives from ComponentBase that makes the query on the DB based on the domain username (windows security) and every page derives from this MyComponentBase

 protected override async Task OnInitializedAsync()
{
    UserName = (await AuthenticationStateTask).User.Identity.Name;
    CurrentEmployee = await EmployeeService.GetByShortName(UserName);
    if (CurrentEmployee == null)
    {
        NavigationManager.NavigateTo("/accessdenied");
    }
    //... EmployeeId = CurrentEmployee.EmployeeId; ecc...

Obviously this is a waste, but I cannot find where to put the code (in a middleware? program.cs? ) and where to store the data (ideally not only the ID but a more complex object with more info, its Roles collection, etc.)

Thanks in advance

2 Answers

With so little background, I'm shooting a bit in the dark on the context. You'll need to add some error checking and logging code to suit your situation.

First a UserData class to manage the User data. It registers with the AuthenticationStateProvider for AuthenticationStateChanged events and updates the DI UserData instance.

public class UserData : IDisposable
{
    public ClaimsPrincipal? User = null;

    // Your specific User Data

    private AuthenticationStateProvider _authenticationStateProvider;

    public UserData(AuthenticationStateProvider auth)
    {
       _authenticationStateProvider = auth;
        _authenticationStateProvider.AuthenticationStateChanged += this.AuthStateChanged;
    }

    public async ValueTask GetUser()
        => await this.GetUser(_authenticationStateProvider.GetAuthenticationStateAsync());

    public async ValueTask GetUser(Task<AuthenticationState> task)
    {
        var state = await task;
        User = state.User;
        // Get your user data
    }

    public async void AuthStateChanged(Task<AuthenticationState> task)
        => await this.GetUser(task);

    public void Dispose()
        =>  _authenticationStateProvider.AuthenticationStateChanged -= this.AuthStateChanged;
}

This is a managed DI object set up in Services:

builder.Services.AddScoped<UserData>();

We then add some code to App to load the initial user when the SPA session starts.

@inject UserData UserDataService

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        //....
    </Router>
</CascadingAuthenticationState>

@code {
    protected async override Task OnInitializedAsync()
        => await UserDataService.GetUser();
}

This gets the initial user if there is one, which in your case will almost certainly be logged in Windows AD account, and populates the UserData instance in the SPA scoped DI container.

Here's my test page to show the data in UserData.

@page "/"
@inject UserData UserData
<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<div class="bg-dark text-white m-2 p-2">
    @(UserData?.User?.Identity?.Name ?? "Invalid Account")
</div>

Here's my test page:

enter image description here

I think Shaun Curtis' solution is elegant because it doesn't mix data with UI component. I happen to have implemented it in a different way already in my Blazor Server application, and it does work. So I'll post my solution here in case it's helpful for people who couldn't implement Shaun's solution for whatever reason.

In MainLayout.razor, get whatever data you need just like what you would do on your normal razor component. Then pass the value to all child components like this:

<CascadingValue Value="@UserData" Name="UserData" IsFixed="true">
   <Layout>
   .....
   </Layout>
</CascadingValue>

The "IsFixed" above tells Blazor to not keep monitoring for changes.

Related