c# - 当模型属性具有 [ConcurrencyCheck] 属性时,ASP.NET Core MVC ConcurrencyCheck 失败
问题描述
我是 ASP.NET Core MVC 中并发检查的新手,并且必须在这里错误地实现某些东西,但是 Microsoft 文档似乎很薄弱。有人能指出我正确的方向吗?
这是一个简单的用户类,可以由管理员编辑。没有任何并发检查,表单更改会毫无问题地保存到数据库中,但是,当我将 [ConcurrencyCheck] 属性添加到模型属性之一时,即使更改了字段值,保存到数据库也会失败(我是预计这只会在用户 A 在用户 B 之前更新字段时失败,等等)
任何帮助将不胜感激。
这里有一些片段。
模型
public class MyUser : IdentityUser
{
[ConcurrencyCheck]
[PersonalData]
[DisplayName("First Name")]
[Column(TypeName = "nvarchar(100)")]
public string FirstName { get; set; }
[PersonalData]
[DisplayName("Last Name")]
[Column(TypeName = "nvarchar(100)")]
public string LastName { get; set; }
[DisplayName("Created Date")]
[Column(TypeName = "DateTime")]
public DateTime CreatedDateTime { get; set; }
[DisplayName("Customer ID")]
[Column(TypeName = "int")]
public int CustomerId { get; set; }
public string GetFullName()
{
return $"{FirstName} {LastName}";
}
}
控制器
// GET: UserController/Edit/5
public ActionResult Edit(string id)
{
// New user view model.
UserViewModel userViewModel = new UserViewModel();
// Get user details.
MyUser user = _context.Users.Where(u => u.Id == id).Single();
var roles = _context.UserRoles.Where(r => r.UserId == id).ToList();
if (user != null)
{
// Set viewmodel user with user details.
userViewModel.thisUser = user;
// userViewModel.thisUserRoles = _context.UserRoles.Where()
// Get roles assigned to the user.
//userViewModel.thisUserRoles = _context.userRoles.Where(r => r.UserId == id).ToList();
// Set viewmodel variables based on which roles the user has.
if (roles.Count > 0)
{
foreach(var role in roles)
{
switch (_context.Roles.Where(r => r.Id == role.RoleId).Select(r => r.Name).Single().ToString())
{
case "User":
userViewModel.UserRole = true;
break;
case "Sales User":
userViewModel.SalesUserRole = true;
break;
case "Sales Branch User":
userViewModel.SalesBranchUserRole = true;
break;
case "Sales Administrator":
userViewModel.SalesAdministrator = true;
break;
case "Administrator":
userViewModel.Administrator = true;
break;
}
}
}
return View(userViewModel);
}
else
{
ViewBag.ErrorMessage = "The user could not be found.";
return RedirectToAction("Index");
}
}
// POST: UserController/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(UserViewModel userViewModel)
{
if (ModelState.IsValid)
{
userViewModel.thisUser.UserName = userViewModel.thisUser.Email;
userViewModel.thisUser.NormalizedUserName = userViewModel.thisUser.Email.ToUpper();
userViewModel.thisUser.NormalizedEmail = userViewModel.thisUser.Email.ToUpper();
try
{
_context.Entry(userViewModel.thisUser).State = EntityState.Modified;
_context.SaveChanges();
}
catch (Exception ex)
{
// Prompt the user to try again.
ModelState.AddModelError("", "The information has been updated by someone else before you saved. Please try again.");
return View(userViewModel);
}
}
return RedirectToAction("Index");
}
看法
@model MyApp.ViewModels.UserViewModel
@{
ViewData["Title"] = "Edit";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@*<h1>Edit</h1>*@
@*<h5>
Details
</h5>
<hr />*@
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="All" class="text-danger"></div>
@Html.HiddenFor(m => m.thisUser.Id)
@Html.HiddenFor(m => m.thisUser.PasswordHash)
@Html.HiddenFor(m => m.thisUser.LockoutEnd)
@Html.HiddenFor(m => m.thisUser.LockoutEnabled)
@Html.HiddenFor(m => m.thisUser.AccessFailedCount)
@Html.HiddenFor(m => m.thisUser.CreatedDateTime)
@Html.HiddenFor(m => m.thisUser.SecurityStamp)
@Html.HiddenFor(m => m.thisUser.ConcurrencyStamp)
@Html.HiddenFor(m => m.thisUser.EmailConfirmed)
<div class="form-group">
<label asp-for="thisUser.FirstName" class="control-label"></label>
<input asp-for="thisUser.FirstName" class="form-control" />
<span asp-validation-for="thisUser.FirstName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="thisUser.LastName" class="control-label"></label>
<input asp-for="thisUser.LastName" class="form-control" />
<span asp-validation-for="thisUser.LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="thisUser.Email" class="control-label"></label>
<input asp-for="thisUser.Email" class="form-control" />
<span asp-validation-for="thisUser.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="thisUser.PhoneNumber" class="control-label"></label>
<input asp-for="thisUser.PhoneNumber" class="form-control" />
<span asp-validation-for="thisUser.PhoneNumber" class="text-danger"></span>
</div>
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="thisUser.PhoneNumberConfirmed" /> @Html.DisplayNameFor(model => model.thisUser.PhoneNumberConfirmed)
</label>
</div>
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="thisUser.TwoFactorEnabled" /> @Html.DisplayNameFor(model => model.thisUser.TwoFactorEnabled)
</label>
</div>
<div class="form-group">
<label asp-for="thisUser.CustomerId" class="control-label"></label>
<input asp-for="thisUser.CustomerId" class="form-control" />
<span asp-validation-for="thisUser.CustomerId" class="text-danger"></span>
</div>
<h5>
Roles
</h5>
<hr />
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="UserRole" /> @Html.DisplayNameFor(model => model.UserRole)
</label>
</div>
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="SalesUserRole" /> @Html.DisplayNameFor(model => model.SalesUserRole)
</label>
</div>
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="SalesBranchUserRole" /> @Html.DisplayNameFor(model => model.SalesBranchUserRole)
</label>
</div>
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="SalesAdministrator" /> @Html.DisplayNameFor(model => model.SalesAdministrator)
</label>
</div>
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="Administrator" /> @Html.DisplayNameFor(model => model.Administrator)
</label>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
解决方案
根据 ms 官方文档,建议使用行版本来处理并发冲突。
从文档中总结一下,我们需要添加一个名为“RowVersion”的列,其数据类型为“rowversion”,并将其添加到您的实体中。
[Timestamp]
public byte[] RowVersion { get; set; }
接下来,在调用之前,await _context.SaveChangesAsync();
我们需要将原始 RowVersion 属性值放入实体的 OriginalValues 集合中
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> EditAsync(UserViewModel userViewModel)
{
if (ModelState.IsValid)
{
try
{
_context.Entry(userViewModel.thisUser).Property("RowVersion").OriginalValue = userViewModel.thisUser.RowVersion;
_context.Update(userViewModel.thisUser);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
// Prompt the user to try again.
ModelState.AddModelError("", "The information has been updated by someone else before you saved. Please try again.");
return View(userViewModel);
}
}
return RedirectToAction("Index");
}