Home > OS >  Blazor Server App - Error while validating the service descriptor
Blazor Server App - Error while validating the service descriptor

Time:11-01

While attempting to create a custom authentication for a Blazor server app, I received the following error while building the app:

System.AggregateException: 'Some services are not able to be constructed (Error while 
validating the service descriptor 'ServiceType: 
FAApp_Server.Authentication.UserAccountService Lifetime: Singleton ImplementationType: 
FAApp_Server.Authentication.UserAccountService': Unable to resolve service for type 
'FAApp_Server.Authentication.User' while attempting to activate 
'FAApp_Server.Authentication.UserAccountService'.)'

I have checked my classes and I believe I've accounted for the needed constructors. I've added the appropriate builder services in Program.cs. I'm honestly completely stumped, as no obvious errors are being display in the code prior to saving and building.

Here are the applicable bits of code.

Program.cs:

using FAApp_Practical.Repository;
using FAApp_Practical.Repository.iRepository;
using FAApp_Server.Authentication;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.EntityFrameworkCore;
using Radzen;
using Tangy_DataAccess.Data;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddAuthenticationCore();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<ProtectedSessionStorage>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthentication>();
builder.Services.AddSingleton<UserAccountService>();
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IAssetRepository, AssetRepository>();
// Radzen Components
builder.Services.AddScoped<DialogService>();
builder.Services.AddScoped<NotificationService>();
builder.Services.AddScoped<TooltipService>();
builder.Services.AddScoped<ContextMenuService>();
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

Custom Authentication:

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using System.Security.Claims;

namespace FAApp_Server.Authentication
{
    public class CustomAuthentication : AuthenticationStateProvider
    {

        private readonly ProtectedSessionStorage _sessionStorage;
        private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());

        public CustomAuthentication(ProtectedBrowserStorage sessionStorage)
        {
            _sessionStorage = (ProtectedSessionStorage?)sessionStorage;
        }

        public CustomAuthentication() { }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            try
            {
                var userSessionStorageResult = await _sessionStorage.GetAsync<UserSession>("UserSession");
                var userSession = userSessionStorageResult.Success ? userSessionStorageResult.Value : null;

                if (userSession == null)
                {
                    return await Task.FromResult(new AuthenticationState(_anonymous));
                }

                var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
            {
                new Claim(ClaimTypes.Name, userSession.UserName),
                new Claim(ClaimTypes.Role, userSession.Role),
            }, "CustomAuth"));
                return await Task.FromResult(new AuthenticationState(claimsPrincipal));
            }
            catch
            {
                return await Task.FromResult(new AuthenticationState(_anonymous));
            }
        }

        public async Task UpdateAuthenticationState(UserSession userSession)
        {
            ClaimsPrincipal claimsPrincipal;

            if (userSession != null)
            {
                await _sessionStorage.SetAsync("UserSession", userSession);
                claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
                {
                    new Claim(ClaimTypes.Name, userSession.UserName),
                    new Claim(ClaimTypes.Role, userSession.Role)
                }));
            }
            else
            {
                await _sessionStorage.DeleteAsync("UserSession");
                claimsPrincipal = _anonymous;
            }

            NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
        }
    }
}

User:

namespace FAApp_Server.Authentication
{

    public class User
    {
        public string UserName { get; set; }
        public string Role { get; set; }

        public User() { }
        public User(string userName, string role)
        {
            UserName = userName;
            Role = role;
        }
    }
}

User Account Service:

using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;
using System.Text;
using Tangy_DataAccess.Data;

namespace FAApp_Server.Authentication
{
    public class UserAccountService
    {
        private readonly ApplicationDbContext _db;
        private User _user;

        public UserAccountService(ApplicationDbContext db, User user)
        {
            _db = db;
            _user = user;
        }

        public async Task<User> GetUser(string userName, string password)
        {
            var obj = await _db.tblUsers.FirstOrDefaultAsync(u => u.LoginID == userName);
            if (obj != null)
            {
             -----ommited
            }
            return null;
        }

    }

}

User Session:

namespace FAApp_Server.Authentication
{
    public class UserSession
    {

        public string UserName { get; set; }
        public string Role { get; set; }
    }
}

Login:

@page "/login"
@using FAApp_Server.Authentication

@inject UserAccountService userAccountSerivce
@inject IJSRuntime js
@inject AuthenticationStateProvider authStateProvider
@inject NavigationManager navManager


<div >
    <div >
        <div >
            <h3>LOGIN</h3><br />
            <h1>Please login below using your credentials</h1>
        </div>
        <div >
            <label>Username</label>
            <input @bind="user.UserName"  placeholder="User Name" />
        </div>
        <div >
            <label>Password</label>
            <input @bind="user.Password"  placeholder="Password" />
        </div>
        <div >
            <button @onclick="Authenticate" >Login</button>
        </div>

    </div>
</div>

@code {
    private class User
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }

    private User user = new User();

    private async Task Authenticate()
    {
        var userAccount = userAccountSerivce.GetUser(user.UserName, user.Password);
        if (userAccount == null)
        {
            await js.InvokeVoidAsync("alert", "Invalid Username or Password");
            return;
        }

        var customAuthStateProvider = (CustomAuthentication)authStateProvider;
        await customAuthStateProvider.UpdateAuthenticationState(new UserSession
        {
            UserName = user.UserName,
            Role = "User"
        });
        navManager.NavigateTo("/", true);
    }

}

CodePudding user response:

You've added the 'UserAccountService' as a singleton. All requests from all users share this one service. However, the constructor for that service expects to receive a specific 'User' when it is created, before any requests have been made. Where should that user come from?

public class UserAccountService
{
    private readonly ApplicationDbContext _db;

    // This field is private, and never used. Why does it exist?
    private User _user;

    // This constructor expects a 'User'. Where should the user come from?
    // This is what the application is telling you:
    //   Unable to resolve service for type 'FAApp_Server.Authentication.User' while
    //   attempting to activate 'FAApp_Server.Authentication.UserAccountService'.
    public UserAccountService(ApplicationDbContext db, User user)
    {
        _db = db;
        _user = user;
    }

    public async Task<User> GetUser(string userName, string password)
    {
        var obj = await _db.tblUsers.FirstOrDefaultAsync(u => u.LoginID == userName);
        if (obj != null)
        {
         -----ommited
        }
        return null;
    }
}

Aside from there being no current user, your UserAccountService also has a dependency on a database context. Every time you call GetUser(userName, password) the context will load a user from the database, and add it to the change tracker. It will not remove it until the context is disposed. Since it's a singleton, the context will not be disposed unless you stop the application. This is a memory leak.

This class should be scoped, not singleton. When you change the registration, you can inject IHttpContextAccessor to get the current user.

  • Related