ASP.NET Core MVC注册表单邮箱字段无法正确保存问题排查
我开发了一个小型电影应用,支持用户注册、登录、添加电影条目及评论。但注册表单的邮箱字段无法正确传递并保存到数据库。
调试发现,在AccountController的var identityResult = await userManager.CreateAsync(user, registerViewModel.Password);行,user对象的UserName、Email、Password均正常传入,但执行后identityResult显示失败。原错误处理代码返回“Email '' is invalid”,修改为显示user.Email时则正常显示输入的邮箱(如tim123@gmail.com)。
相关代码
AccountController
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using MovieOrganizer.Models.ViewModels; using MovieOrganizer.Models.Domain; namespace MovieOrganizer.Controllers { public class AccountController : Controller { private readonly UserManager<User> userManager; private readonly SignInManager<User> signInManager; public AccountController(UserManager<User> userManager, SignInManager<User> signInManager) { this.userManager = userManager; this.signInManager = signInManager; } [HttpGet] public IActionResult Register() { return View(); } [HttpPost] public async Task<IActionResult> Register(RegisterViewModel registerViewModel) { var user = new User { UserName = registerViewModel.UserName, Email = registerViewModel.Email, //PasswordHash = registerViewModel.Password }; var identityResult = await userManager.CreateAsync(user, registerViewModel.Password); if (identityResult.Succeeded) { var roleIdentityResult = await userManager.AddToRoleAsync(user, "User"); if (roleIdentityResult.Succeeded) { await signInManager.SignInAsync(user, isPersistent: false); return RedirectToAction("Index", "Movies"); } } else { foreach (var error in identityResult.Errors) { ModelState.AddModelError(string.Empty, user.Email); } } return View(registerViewModel); } [HttpGet] public IActionResult Login() { var model = new LoginViewModel { }; return View(model); } [HttpPost] public async Task<IActionResult> Login(LoginViewModel loginViewModel) { if (!ModelState.IsValid) { return View(loginViewModel); } var user = await userManager.FindByNameAsync(loginViewModel.UserName); if (user == null) { ModelState.AddModelError(string.Empty, "Invalid username."); return View(loginViewModel); } var signInResult = await signInManager.PasswordSignInAsync(loginViewModel.UserName, loginViewModel.Password, false, false); if (signInResult != null && signInResult.Succeeded) { return RedirectToAction("Index", "Movies"); } if (signInResult.IsLockedOut) { ModelState.AddModelError(string.Empty, "User account locked out."); } else if (signInResult.IsNotAllowed) { ModelState.AddModelError(string.Empty, "User account not allowed."); } else { ModelState.AddModelError(string.Empty, "Invalid password."); } return View(loginViewModel); } [HttpGet] public async Task<IActionResult> Logout() { await signInManager.SignOutAsync(); return RedirectToAction("Index", "Home"); } [HttpGet] public IActionResult AccessDenied() { return View(); } } }
RegisterViewModel
using System.ComponentModel.DataAnnotations; namespace MovieOrganizer.Models.ViewModels { public class RegisterViewModel { [Required] public string UserName { get; set; } [Required] [EmailAddress] public string Email { get; set; } [Required] [MinLength(6, ErrorMessage = "Password has to be at least 6 characters")] public string Password { get; set; } } }
Register.cshtml
@model MovieOrganizer.Models.ViewModels.RegisterViewModel <h1>Sign Up for Movie Library</h1> @if (ViewData.ModelState.IsValid == false && ViewData.ModelState.ErrorCount > 0) { foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors)) { <p class="error">@error.ErrorMessage</p> } } <div class="container mx-auto"> <div class="row justify-content-center"> <div class="col-12 col-lg-6"> <h1 class="mt-3 mb-3 h3"> Register </h1> <form method="post"> <div class="mb-3"> <label class="form-label">Username</label> <input type="text" id="username" class="form-control" asp-for="UserName" registered /> <span class="text-danger" asp-validation-for="UserName"></span> </div> <div class="mb-3"> <label class="form-label">Email</label> <input type="email" id="email" class="form-control" asp-for="Email" required /> <span class="text-danger" asp-validation-for="Email"></span> </div> <div class="mb-3"> <label class="form-label">Password</label> <input type="password" id="password" class="form-control" asp-for="Password" required minlength="6" /> <span class="text-danger" asp-validation-for="Password"></span> </div> <div class="mb-3"> <label class="form-label">Password Confirmation</label> <input type="password" id="PasswordConfirmation" name="PasswordConfirmation" class="form-control" asp-for="Password" required minlength="6" /> <span class="text-danger" asp-validation-for="Password"></span> </div> <div class="mb-3"> <button type="submit" class="btn btn-dark">Register</button> </div> </form> </div> <h4><a href="/auth/google_oauth2">Log in with Google</a></h4> </div> </div>
Program.cs
using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using MovieOrganizer.Data; using MovieOrganizer.Models.Domain; using MovieOrganizer.Repositories; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddDbContext<MovieDBContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("MovieOrganizerCon"))); builder.Services.AddIdentity<User, IdentityRole<Guid>>(options => { options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.MaxFailedAccessAttempts = 5; options.User.RequireUniqueEmail = true; }) .AddEntityFrameworkStores<MovieDBContext>() .AddDefaultTokenProviders(); builder.Services.AddScoped<IMovieRepository, MovieRepository>(); builder.Services.AddScoped<IMovieLogRepository, MovieLogRepository>(); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); builder.Services.AddScoped<UserManager<User>>(); builder.Services.AddScoped<SignInManager<User>>(); var app = builder.Build(); using (var scope = app.Services.CreateScope()) { var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole<Guid>>>(); var userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>(); string[] roleNames = { "User" }; foreach(var roleName in roleNames) { var roleExist = await roleManager.RoleExistsAsync(roleName); if (!roleExist) { await roleManager.CreateAsync(new IdentityRole<Guid>(roleName)); } } } // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseSession(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Movies}/{action=Index}/{id?}"); app.Run();
问题排查与解决步骤
修复错误处理逻辑,查看真实错误
当前错误处理代码把user.Email当成错误信息返回,无法看到Identity的真实失败原因。修改AccountController中的错误处理代码:else { foreach (var error in identityResult.Errors) { ModelState.AddModelError(string.Empty, error.Description); } }这样就能看到具体的失败原因,比如邮箱格式不符合Identity的验证规则、数据库中已存在该邮箱,或者User实体类的Email属性配置问题。
检查自定义User实体类
确保你的User类正确继承了IdentityUser<Guid>,并且Email属性没有被错误覆盖或隐藏。例如:public class User : IdentityUser<Guid> { // 自定义属性,不要重写Email属性除非有特殊需求 }验证数据库迁移与字段配置
若使用自定义User类,需确保已生成并应用正确的数据库迁移,数据库表中的Email字段存在、类型为可变长度字符串(如nvarchar(256)),且允许非空(因为你在Program.cs中配置了options.User.RequireUniqueEmail = true)。完善密码确认验证
视图中的密码确认输入框绑定错误,会导致ModelState无效。修改RegisterViewModel,添加密码确认属性:[Required] [Compare("Password", ErrorMessage = "密码与确认密码不匹配")] public string PasswordConfirmation { get; set; }同时修改Register.cshtml中的密码确认框:
<div class="mb-3"> <label class="form-label">Password Confirmation</label> <input type="password" id="PasswordConfirmation" class="form-control" asp-for="PasswordConfirmation" required minlength="6" /> <span class="text-danger" asp-validation-for="PasswordConfirmation"></span> </div>并在Register方法开头添加ModelState验证:
[HttpPost] public async Task<IActionResult> Register(RegisterViewModel registerViewModel) { if (!ModelState.IsValid) { return View(registerViewModel); } // 后续代码... }确认前端数据传递
在调用userManager.CreateAsync前添加日志输出,确认registerViewModel.Email确实接收到了前端输入的值:Console.WriteLine($"Received Email: {registerViewModel.Email}"); var user = new User { UserName = registerViewModel.UserName, Email = registerViewModel.Email, };
内容的提问来源于stack exchange,提问作者bradd

