Search code examples
asp.net-coreentity-framework-coreasp.net-core-mvcasp.net-core-8

Null exception during user login ASP.NET Core Identity


I am fairly new to ASP.NET Core. I am working on a simple social media web app and decided to add Identity to my existing User. I am having trouble with signing the user in. The problem occurs in both Login and Register methods. In the Register method, the user is actually added to the database.

Database

But when trying to sign in inside the very same method I get the following:

ArgumentNullException: Value cannot be null. (Parameter 'value')
System.ArgumentNullException.Throw(string paramName)

System.ArgumentNullException.ThrowIfNull(object argument, string paramName)  
System.Security.Claims.Claim..ctor(string type, string value, string valueType, string issuer, string originalIssuer, ClaimsIdentity subject, string propertyKey, string propertyValue)  
System.Security.Claims.Claim..ctor(string type, string value)
Microsoft.AspNetCore.Identity.UserClaimsPrincipalFactory<TUser>.GenerateClaimsAsync(TUser user)  
Microsoft.AspNetCore.Identity.UserClaimsPrincipalFactory<TUser, TRole>.GenerateClaimsAsync(TUser user)  
Microsoft.AspNetCore.Identity.UserClaimsPrincipalFactory<TUser>.CreateAsync(TUser user)   
Microsoft.AspNetCore.Identity.SignInManager<TUser>.CreateUserPrincipalAsync(TUser user)  
Microsoft.AspNetCore.Identity.SignInManager<TUser>.SignInWithClaimsAsync(TUser user, AuthenticationProperties authenticationProperties, IEnumerable<Claim> additionalClaims)  
Microsoft.AspNetCore.Identity.SignInManager<TUser>.SignInOrTwoFactorAsync(TUser user, bool isPersistent, string loginProvider, bool bypassTwoFactor)  
Microsoft.AspNetCore.Identity.SignInManager<TUser>.PasswordSignInAsync(TUser user, string password, bool isPersistent, bool lockoutOnFailure)  
Microsoft.AspNetCore.Identity.SignInManager<TUser>.PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)  
ImagiArtInfrastructure.Controllers.AccountController.Login(LoginViewModel model) in AccountController.cs

My app already had User model class, so I added the missing tables to the database.

Database

My current User model, which inherits from IdentityUser:

using Microsoft.AspNetCore.Identity;

namespace ImagiArtInfrastructure;

public partial class User : IdentityUser<int>
{
    public virtual ICollection<Comment> Comments { get; set; } = new List<Comment>();

    public virtual ICollection<Like> Likes { get; set; } = new List<Like>();
    
    public virtual ICollection<Post> Posts { get; set; } = new List<Post>();

    public virtual ICollection<UserFollower> UserFollowerFollowers { get; set; } = new List<UserFollower>();

    public virtual ICollection<UserFollower> UserFollowerUsers { get; set; } = new List<UserFollower>();
}

Register and Login methods:

[HttpPost]
public async Task<IActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        User user = new User
        {
            Email = model.Email,
            UserName = model.Email
        };

        var result = await _userManager.CreateAsync(user, model.Password);

        if (result.Succeeded)
        {
            // cookies
            // This line causes the exception
            await _signInManager.SignInAsync(user, false);
            return RedirectToAction("Index", "Home");
        }
        else
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }
        }
    }

    return View(model);
}

[HttpGet]
public IActionResult Login(string? returnUrl = null)
{
    returnUrl ??= "/";
    return View(new LoginViewModel { ReturnUrl = returnUrl });
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model)
{
    if (ModelState.IsValid)
    {
        // This line causes the exception too
        var result =
        await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);

        if (result.Succeeded)
        {
            if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
            {
                return Redirect(model.ReturnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", "Invalid login credentials");
        }
    }
    return View(model);
}

Program.cs

using ImagiArtInfrastructure;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<CloneContext>(option => option.UseSqlServer(
    builder.Configuration.GetConnectionString("DefaultConnection")
    ));

// Adding Identity
builder.Services.AddIdentity<User, IdentityRole<int>>().AddEntityFrameworkStores<CloneContext>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/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.UseAuthentication();
app.UseRouting();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Database context:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace ImagiArtInfrastructure;

public partial class CloneContext : IdentityDbContext<User, IdentityRole<int>, int>
{
    public CloneContext()
    {
    }

    public CloneContext(DbContextOptions<CloneContext> options)
    : base(options)
    {
        Database.EnsureCreated();
    }
    public virtual DbSet<Comment> Comments { get; set; }

    public virtual DbSet<Like> Likes { get; set; }

    public virtual DbSet<Post> Posts { get; set; }

    public virtual DbSet<UserFollower> UserFollowers { get; set; }

    // Warning  CS0114  'CloneContext.Users' hides inherited member 'IdentityUserContext<User, int, IdentityUserClaim<int>, IdentityUserLogin<int>, IdentityUserToken<int>>.Users'.
    // if Users is uncommented
    //public virtual DbSet<User> Users { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlServer("Server=DESKTOP-6SAKF27\\SQLEXPRESS; Database=Clone; Trusted_Connection=True; TrustServerCertificate=True; ");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Comment>(entity =>
        {
            entity.HasKey(e => e.CommentId).HasName("PK__Comments__E7957687FE981422");

            entity.Property(e => e.Caption)
                .HasMaxLength(64)
                .HasColumnName("caption");
            entity.Property(e => e.PostId).HasColumnName("post_id");
            entity.Property(e => e.UserId).HasColumnName("user_id");

            entity.HasOne(d => d.Post).WithMany(p => p.Comments)
                .HasForeignKey(d => d.PostId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("comments_post_id_foreign");

            entity.HasOne(d => d.User).WithMany(p => p.Comments)
                .HasForeignKey(d => d.UserId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("FK_Comments_AspNetUsers");
        });

        modelBuilder.Entity<Like>(entity =>
        {
            entity.HasKey(e => e.LikeId).HasName("PK__Likes__992C79308223B6DD");

            entity.Property(e => e.PostId).HasColumnName("post_id");
            entity.Property(e => e.UserId).HasColumnName("user_id");

            entity.HasOne(d => d.Post).WithMany(p => p.Likes)
                .HasForeignKey(d => d.PostId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("likes_post_id_foreign");

            entity.HasOne(d => d.User).WithMany(p => p.Likes)
                .HasForeignKey(d => d.UserId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("FK_Likes_AspNetUsers");
        });

        modelBuilder.Entity<Post>(entity =>
        {
            entity.HasKey(e => e.PostId).HasName("PK__Posts__3ED78766917B0BC9");

            entity.Property(e => e.Caption)
                .HasMaxLength(32)
                .HasColumnName("caption");
            entity.Property(e => e.Description)
                .HasMaxLength(64)
                .HasColumnName("description");
            entity.Property(e => e.ImageUrl)
                .HasMaxLength(50)
                .IsUnicode(false)
                .HasColumnName("image_url");
            entity.Property(e => e.UserId).HasColumnName("user_id");

            entity.HasOne(d => d.User).WithMany(p => p.Posts)
                .HasForeignKey(d => d.UserId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("FK_Posts_AspNetUsers");
        });

        modelBuilder.Entity<UserFollower>(entity =>
        {
            entity.HasKey(e => e.UserFollowId).HasName("PK__UserFoll__15A691442B4B1096");

            entity.Property(e => e.FollowerId).HasColumnName("follower_id");
            entity.Property(e => e.UserId).HasColumnName("user_id");

            entity.HasOne(d => d.Follower).WithMany(p => p.UserFollowerFollowers)
                .HasForeignKey(d => d.FollowerId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("FK_UserFollowers_AspNetUsers1");

            entity.HasOne(d => d.User).WithMany(p => p.UserFollowerUsers)
                .HasForeignKey(d => d.UserId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("FK_UserFollowers_AspNetUsers");
        });

        modelBuilder.Entity<User>(entity =>
        {
            entity.Property(e => e.Id).ValueGeneratedNever();
            entity.Property(e => e.Email).HasMaxLength(256);
            entity.Property(e => e.NormalizedEmail).HasMaxLength(256);
            entity.Property(e => e.NormalizedUserName).HasMaxLength(256);
            entity.Property(e => e.UserName).HasMaxLength(256);
        });

        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

I tried debugging to see what exactly is causing this. SignInManager class Microsoft.AspNetCore.Identity:

/// <summary>
/// Creates a <see cref="ClaimsPrincipal"/> for the specified <paramref name="user"/>, as an asynchronous operation.
/// </summary>
/// <param name="user">The user to create a <see cref="ClaimsPrincipal"/> for.</param>
/// <returns>The task object representing the asynchronous operation, containing the ClaimsPrincipal for the specified user.</returns>
public virtual async Task<ClaimsPrincipal> CreateUserPrincipalAsync(TUser user) => await ClaimsFactory.CreateAsync(user); // ClaimsFactory.CreateAsync(user) causes the exception

I noticed that the user parameter is passed correctly everywhere, so no idea what causing this.


Solution

  • My solution was to create a new Identity database and add whatever tables you need for the web app. The problem might have been that I added Identity tables to the database using queries inside SSMS and that screwed something up.

    I tried it in a new project. The EF can create the Identity database for you. I added a connection string and create a new class:

    public IdentityContext(DbContextOptions<IdentityContext> options)
    : base(options)
    {
        Database.EnsureCreated();
    }
    

    After that, I added the Posts table in SSMS and scaffolded the database. In a newly generated context all Identity related DbSets and model configuring should be removed:

    public partial class CloneIdentityContext : IdentityDbContext<User>
    {
    // ...
    
    // Also, there is no need to add DbSet for Users, you will get warning that it hides the inherited one. 
    public virtual DbSet<Post> Posts { get; set; }
    
            modelBuilder.Entity<User>(entity =>
            {
                entity.HasIndex(e => e.NormalizedEmail, "EmailIndex");
    
                entity.HasIndex(e => e.NormalizedUserName, "UserNameIndex")
                    .IsUnique()
                    .HasFilter("([NormalizedUserName] IS NOT NULL)");
    
                entity.Property(e => e.Email).HasMaxLength(256);
                entity.Property(e => e.NormalizedEmail).HasMaxLength(256);
                entity.Property(e => e.NormalizedUserName).HasMaxLength(256);
                entity.Property(e => e.UserName).HasMaxLength(256);
            });
    
    // Configuration for any additional entities, Posts in my case
    }
    

    Program.cs:

    builder.Services.AddDbContext<IdentityContext>(option => option.UseSqlServer(
        builder.Configuration.GetConnectionString("IdentityConnection")
        ));
    builder.Services.AddControllersWithViews();
    builder.Services.AddIdentity<User, IdentityRole>().AddEntityFrameworkStores<IdentityContext>();
    

    This time I decided to leave Id type as default for the sake of time:

    [Table("AspNetUsers")]
    public class User : IdentityUser
    {
        public int Age { get; set; }
        public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
    }
    

    Thanks for the help. I think I'll mark this answer as accepted.