ab_732
ab_732

Reputation: 3887

Blazor Server: Issue with scoped service

I am experiencing a weird behavior while trying to use a Scoped Service that I am initializing in the _Host.cshtml file. This is the service, which is very simple:

public interface IUserSessionService
{
    void SetUserData(UserSessionData userData);
    UserSessionData GetUserData();
}
public class UserSessionService : IUserSessionService
{
    private UserSessionData userSessionData;
    public void SetUserData(UserSessionData userData)
    {
        userSessionData = userData;
    }
    public UserSessionData GetUserData()
    {
        return userSessionData;
    }
}

The service is registered as Scoped service in the Startup.cs file

services.AddScoped<IUserSessionService, UserSessionService>();

The service gets user information from a database in the following fashion in the code behind the host.cshtml page:

public class _Host : PageModel
{
    private readonly IUserSessionService userSessionService;

    public _Host(IUserSessionService userSessionService)
    {
        this.userSessionService = userSessionService;
    }
    public async Task OnGet()
    {
        var authenticatedUser = Request.HttpContext.User;
        var userData = await GetUserDataFromDb(authenticatedUser);
        await userSessionService.SetUserData(userData);
    }
}

Ideally, I would expect this to initialize the service, so that from now on I can inject it in my components and access to the user's information whenever I need it.

This is not working though: when the page loads I am able to see the user information in the screen for a second, then it disappears. Initially I thought it was caused by the page render mode ServerPrerendered, however in Server mode the issue persists.

The only fix I could find is to register the service as Singleton; that is not ideal as that would be shared between all the users (at least that is my understanding!).

Is there a better place where I can store this information?


UPDATE

The issue was mostly the consequence of my bad understanding of how the Blazor circuit works :) I got it working following @enet's advice. What I did is:

  1. Got the information from the HttpContext in a HostModel class created for _Host.cshtml and made it available to the html code with a property
public class HostModel : PageModel
{
    public UserSessionData UserSessionData { get; private set; }
    // ... set the information up
}
  1. create a parameter in the ComponentTagHelper and pass the property data
@model HostModel
<component type="typeof(App)" render-mode="ServerPrerendered" param-UserSessionData="@Model.UserSessionData" />
  1. Access the information as [Parameter] in the App.razor
@code { 
    [Parameter]
    public UserSessionData UserSessionData { get; set; }
}
  1. Setup the information in the scoped service inside the OnInitializedAsync() method
    protected override async Task OnInitializedAsync()
    {
        await UserSessionService.SetUserData(UserSessionData);
    }

Now the UserSessionData will be available everywhere in the application through the injection of the IUserSessionService

Upvotes: 6

Views: 4805

Answers (2)

Bennyboy1973
Bennyboy1973

Reputation: 4208

I can't answer your question , but I can say that when it comes to using authentication and services, things get a little dicey. I was trying a similar thing: making a service that would return user info based on authentication, and then injecting it into whatever controls needed it-- sometimes multiple Components on one page were accessing membership during initialization.

That led to a LOT of conflicts-- trying to access DbContext while it was already open, etc. Basically, race condition crashes.

My recommendation is simply not to do this at all. But if you find a solution that works, I look forward to seeing it.

Upvotes: 0

enet
enet

Reputation: 45586

You shouldn't use Singleton from the very reason you've mentioned.

Do public async Task OnGetAsync() instead of public async Task OnGet()

The recommended way to pass data to your Blazor app is via parameters provided with the component html tag helper...

  1. Add your service as scoped.
  2. Define parameter properties in the App component, inject your service into the component, and when the App component is initialized, read the passed parameters to your injected service...Now it is available in your Spa.
  3. Add parameter attributes to your component html tag helper in the _Host.cshtml file.

See complete sample how to do that here

Note: There's also a code sample in the docs how to do that. It is more or less the same like mine

UPDATE:

The following is a service I've once created. It works fine. Compare it with what you did, or even better use it instead as it actually does what your service does to a great extent.

UserEditService.cs

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Security.Claims;


public class UserEditService
    {
        private readonly AuthenticationStateProvider authProvider;
        private readonly UserManager<IdentityUser> userManager;
        public UserEditService ( AuthenticationStateProvider 
                                                authProvider,
                         UserManager<IdentityUser> userManager)
        {
        // Execute only when the app is served. The app is served 
       // when it is initially accessed, or after a user has logged-in
            
            this.authProvider = authProvider;
            this.userManager = userManager;

        }

        public async Task<string> GetEmail()
        {
            var authenticationStateTask = await 
                   authProvider.GetAuthenticationStateAsync();
                     
            var user = authenticationStateTask.User;
            // Executed whenever the GetMail method is called 
            
            if (user.Identity.IsAuthenticated)
            {
                // Executed only when the user is authenticated
                var userId = user.Claims.Where(c => c.Type == 
                     ClaimTypes.NameIdentifier).FirstOrDefault();

                var applicationUser = await 
                         userManager.FindByIdAsync(userId.Value);

                return applicationUser.Email;
            }

            return "User not authenticated";
               
        }
    } 

Startup.ConfigureServices

 services.AddScoped<UserManager<IdentityUser>>();
 services.AddScoped<UserEditService>();

Usage

@page "/"
@inject UserEditService UserEditService


<div>Your email: @email</div>


@code
{
    private string email;

    protected override async Task OnInitializedAsync()
    {
        email = await UserEditService.GetEmail();
    }
   
}

Note: In my service I returns only the email field, but of course you can return the complete identity user...

Upvotes: 1

Related Questions