将新的子实体添加到数据库也会插入一个新的空父实体
Adding a new child entity to database also inserts a new empty parent entity
我最近将我的项目从 ASP.Net Core 2.1 更新到 2.2(并遵循了 Microsoft 的迁移指南),更新了我的一些剃须刀页面以将子对象添加到数据库后,也开始添加新的空父对象同时到数据库。奇怪的是,这种行为在所有父子实体中并不一致,一些剃刀页面继续正常工作。
我挖出以下 link 与我的问题有些相关,但没有提供解决方案:
- https://msdn.microsoft.com/en-us/magazine/dn166926.aspx
- EF Core 2.2.3 child record is not saved after update from 2.2.1
这个 github issue 准确描述了发生在我身上的事情,但我没有使用自定义 ModelBinder。
这里是 Create.cshtml:
<h2>Create</h2>
<h4>Branch</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Branch.BranchName" class="control-label"></label>
<input asp-for="Branch.BranchName" class="form-control" />
<span asp-validation-for="Branch.BranchName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Name" class="control-label"></label>
<input asp-for="Branch.Name" class="form-control" />
<span asp-validation-for="Branch.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Address" class="control-label"></label>
<input asp-for="Branch.Address" class="form-control" />
<span asp-validation-for="Branch.Address" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.City" class="control-label"></label>
<input asp-for="Branch.City" class="form-control" />
<span asp-validation-for="Branch.City" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Province" class="control-label"></label>
<input asp-for="Branch.Province" class="form-control" />
<span asp-validation-for="Branch.Province" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Country" class="control-label"></label>
<input asp-for="Branch.Country" class="form-control" />
<span asp-validation-for="Branch.Country" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.PostalCode" class="control-label"></label>
<input asp-for="Branch.PostalCode" class="form-control" />
<span asp-validation-for="Branch.PostalCode" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Status" class="control-label"></label>
<input asp-for="Branch.Status" class="form-control" />
<span asp-validation-for="Branch.Status" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Phone" class="control-label"></label>
<input asp-for="Branch.Phone" class="form-control" />
<span asp-validation-for="Branch.Phone" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Fax" class="control-label"></label>
<input asp-for="Branch.Fax" class="form-control" />
<span asp-validation-for="Branch.Fax" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Comments" class="control-label"></label>
<textarea asp-for="Branch.Comments" class="form-control"></textarea>
<span asp-validation-for="Branch.Comments" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.DealerId" class="control-label"></label>
<select asp-for="Branch.DealerId" class ="form-control" asp-items="ViewBag.DealerId"></select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>
这里是 Create.cshtml.cs:
public class CreateModel : PageModel
{
private readonly ApplicationDbContext _context;
public CreateModel(ApplicationDbContext context)
{
_context = context;
}
public IActionResult OnGet()
{
Branch = new Branch();
//Usually the DealerId would be passed in the URL, this is just for testing
ViewData["DealerId"] = new SelectList(_context.Dealer, "DealerId", "DealerId");
return Page();
}
//This is weird for debugging purposes
private Branch _branch;
[BindProperty]
public Branch Branch {
get
{
return _branch;
}
set
{
var t = value;
_branch = value;
}}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Branches.Add(Branch);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
Branch.cs:
public class Branch
{
[Key]
public string BranchId { get; set; }
[Display(Name = "Branch Name")]
public string BranchName { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Province { get; set; }
public string Country { get; set; }
[Display(Name = "Postal Code")]
[DataType(DataType.PostalCode)]
public string PostalCode { get; set; }
public string Status { get; set; }
[DataType(DataType.PhoneNumber)]
public string Phone { get; set; }
[DataType(DataType.PhoneNumber)]
public string Fax { get; set; }
[DataType(DataType.MultilineText)]
public string Comments { get; set; }
public string DealerId { get; set; }
[ForeignKey("DealerId")]
public Dealer Dealer { get; set; }
public List<ApplicationUser> Users { get; set; }
}
当页面被 post 回退时,Branch 对象应该有一个空 Dealer 属性(并且 ASP.Net Core 2.1 有),但是有一个新实例化的 Dealer对象(具有空字段)here's a screenshot of the breakpoint on the Branch setter。当分支对象被添加到数据库中时,它应该使用 DealerId 来查找相关对象并将它们 link 起来(在这些 classes/tables 之间建立了适当的功能 FK 关系),但是因为Dealer navigation 属性 不为空,Branch 和 Empty Dealer 都被添加到数据库中,并且它们之间建立了 FK 关系。这可以通过在将 Branch 添加到数据库 _context 之前将 Dealer 属性 设置为 null 来解决,但这显然不是预期的行为,并且对于具有类似关系的其他几个数据库实体来说没有必要这样做。
我一直在绞尽脑汁想找出这个错误的根源。我什至创建了一个新的 ASP.Net 核心项目,复制了必要的模型,并从中搭建了一个新的数据库 运行 到同一个问题。
据我所知,这是 ModelBinder 无法与这些和其他相关 class 兼容的问题。 Binder 应该在 post-back 上将 Dealer 属性 设置为 null,但由于某种原因,它改为调用默认构造函数(然后调用 Dealer 的父级 [=48= 的默认构造函数) ]).
经过大量搜索,我发现此行为是由与模型绑定使用 IFormFile 属性.
处理实体的方式相关的问题引起的
参见:
- .NET Core 2.2 Required Properties when object is not null. Complex Nested Model
- https://github.com/aspnet/AspNetCore/issues/9321
- https://github.com/aspnet/AspNetCore/issues/8917
在我的例子中,Dealer
class 有一个 IFormFile
属性,这导致模型绑定器调用 Dealer
的默认构造函数,结果在 Branch.Dealer
属性 中填充了默认的 Dealer
对象,而不是留空。根据 https://github.com/aspnet/AspNetCore/issues/8917 这已在 ASP.NET Core 3.0
.
中修复
我最近将我的项目从 ASP.Net Core 2.1 更新到 2.2(并遵循了 Microsoft 的迁移指南),更新了我的一些剃须刀页面以将子对象添加到数据库后,也开始添加新的空父对象同时到数据库。奇怪的是,这种行为在所有父子实体中并不一致,一些剃刀页面继续正常工作。
我挖出以下 link 与我的问题有些相关,但没有提供解决方案:
- https://msdn.microsoft.com/en-us/magazine/dn166926.aspx
- EF Core 2.2.3 child record is not saved after update from 2.2.1
这个 github issue 准确描述了发生在我身上的事情,但我没有使用自定义 ModelBinder。
这里是 Create.cshtml:
<h2>Create</h2>
<h4>Branch</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Branch.BranchName" class="control-label"></label>
<input asp-for="Branch.BranchName" class="form-control" />
<span asp-validation-for="Branch.BranchName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Name" class="control-label"></label>
<input asp-for="Branch.Name" class="form-control" />
<span asp-validation-for="Branch.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Address" class="control-label"></label>
<input asp-for="Branch.Address" class="form-control" />
<span asp-validation-for="Branch.Address" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.City" class="control-label"></label>
<input asp-for="Branch.City" class="form-control" />
<span asp-validation-for="Branch.City" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Province" class="control-label"></label>
<input asp-for="Branch.Province" class="form-control" />
<span asp-validation-for="Branch.Province" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Country" class="control-label"></label>
<input asp-for="Branch.Country" class="form-control" />
<span asp-validation-for="Branch.Country" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.PostalCode" class="control-label"></label>
<input asp-for="Branch.PostalCode" class="form-control" />
<span asp-validation-for="Branch.PostalCode" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Status" class="control-label"></label>
<input asp-for="Branch.Status" class="form-control" />
<span asp-validation-for="Branch.Status" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Phone" class="control-label"></label>
<input asp-for="Branch.Phone" class="form-control" />
<span asp-validation-for="Branch.Phone" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Fax" class="control-label"></label>
<input asp-for="Branch.Fax" class="form-control" />
<span asp-validation-for="Branch.Fax" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.Comments" class="control-label"></label>
<textarea asp-for="Branch.Comments" class="form-control"></textarea>
<span asp-validation-for="Branch.Comments" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Branch.DealerId" class="control-label"></label>
<select asp-for="Branch.DealerId" class ="form-control" asp-items="ViewBag.DealerId"></select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>
这里是 Create.cshtml.cs:
public class CreateModel : PageModel
{
private readonly ApplicationDbContext _context;
public CreateModel(ApplicationDbContext context)
{
_context = context;
}
public IActionResult OnGet()
{
Branch = new Branch();
//Usually the DealerId would be passed in the URL, this is just for testing
ViewData["DealerId"] = new SelectList(_context.Dealer, "DealerId", "DealerId");
return Page();
}
//This is weird for debugging purposes
private Branch _branch;
[BindProperty]
public Branch Branch {
get
{
return _branch;
}
set
{
var t = value;
_branch = value;
}}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Branches.Add(Branch);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
Branch.cs:
public class Branch
{
[Key]
public string BranchId { get; set; }
[Display(Name = "Branch Name")]
public string BranchName { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Province { get; set; }
public string Country { get; set; }
[Display(Name = "Postal Code")]
[DataType(DataType.PostalCode)]
public string PostalCode { get; set; }
public string Status { get; set; }
[DataType(DataType.PhoneNumber)]
public string Phone { get; set; }
[DataType(DataType.PhoneNumber)]
public string Fax { get; set; }
[DataType(DataType.MultilineText)]
public string Comments { get; set; }
public string DealerId { get; set; }
[ForeignKey("DealerId")]
public Dealer Dealer { get; set; }
public List<ApplicationUser> Users { get; set; }
}
当页面被 post 回退时,Branch 对象应该有一个空 Dealer 属性(并且 ASP.Net Core 2.1 有),但是有一个新实例化的 Dealer对象(具有空字段)here's a screenshot of the breakpoint on the Branch setter。当分支对象被添加到数据库中时,它应该使用 DealerId 来查找相关对象并将它们 link 起来(在这些 classes/tables 之间建立了适当的功能 FK 关系),但是因为Dealer navigation 属性 不为空,Branch 和 Empty Dealer 都被添加到数据库中,并且它们之间建立了 FK 关系。这可以通过在将 Branch 添加到数据库 _context 之前将 Dealer 属性 设置为 null 来解决,但这显然不是预期的行为,并且对于具有类似关系的其他几个数据库实体来说没有必要这样做。
我一直在绞尽脑汁想找出这个错误的根源。我什至创建了一个新的 ASP.Net 核心项目,复制了必要的模型,并从中搭建了一个新的数据库 运行 到同一个问题。
据我所知,这是 ModelBinder 无法与这些和其他相关 class 兼容的问题。 Binder 应该在 post-back 上将 Dealer 属性 设置为 null,但由于某种原因,它改为调用默认构造函数(然后调用 Dealer 的父级 [=48= 的默认构造函数) ]).
经过大量搜索,我发现此行为是由与模型绑定使用 IFormFile 属性.
处理实体的方式相关的问题引起的参见:
- .NET Core 2.2 Required Properties when object is not null. Complex Nested Model
- https://github.com/aspnet/AspNetCore/issues/9321
- https://github.com/aspnet/AspNetCore/issues/8917
在我的例子中,Dealer
class 有一个 IFormFile
属性,这导致模型绑定器调用 Dealer
的默认构造函数,结果在 Branch.Dealer
属性 中填充了默认的 Dealer
对象,而不是留空。根据 https://github.com/aspnet/AspNetCore/issues/8917 这已在 ASP.NET Core 3.0
.