在 EF Core 中自动设置每条记录的创建和修改日期的问题

Issues with Automatically setting created and modified date on each record in EF Core

将 ASP.NET Core 2.2 与 EF Core 结合使用,我遵循各种指南尝试在创建新记录或 editing/updating 现有记录时实现 date/time 值的自动创建一.

当前结果是当我最初创建新记录时,CreatedDate 和 UpdatedDate 列将填充当前 date/time。 然而,我第一次编辑同一条记录时,UpdatedDate 列会被赋予一个新的 date/time 值(如预期的那样)BUT原因是,EF Core 正在清除原始 CreatedDate 的值,这导致 SQL 分配默认值。

我需要的结果如下:

第 1 步:创建新行,CreatedDate 和 UpdatedDate 列都被赋予 date/time 值(这已经有效) 第 2 步:编辑和保存现有行时,我希望 EF Core 仅使用更新的 date/time 更新 UpdatedDate 列,但保留其他 CreatedDate 列未修改的原始创建日期。

我首先使用 EF Core 代码,不想走流畅的 API 路线。

我部分遵循的指南之一是 https://www.entityframeworktutorial.net/faq/set-created-and-modified-date-in-efcore.aspx,但我尝试过的这个或其他解决方案都没有给出我想要的结果。

基础class:

public class BaseEntity
{
    public DateTime? CreatedDate { get; set; }
    public DateTime? UpdatedDate { get; set; }
}

DbContext Class:

 public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
    {

        var entries = ChangeTracker.Entries().Where(E => E.State == EntityState.Added || E.State == EntityState.Modified).ToList();

        foreach (var entityEntry in entries)
        {
            if (entityEntry.State == EntityState.Modified)
            {
                entityEntry.Property("UpdatedDate").CurrentValue = DateTime.Now;
            }
            else if (entityEntry.State == EntityState.Added)
            {
                entityEntry.Property("CreatedDate").CurrentValue = DateTime.Now;
                entityEntry.Property("UpdatedDate").CurrentValue = DateTime.Now;
            }

        }

        return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

根据史蒂夫在下方评论中提出的建议进行更新

我今天花了更多时间调试,结果我上面发布的方法似乎按预期运行,即当编辑现有行并保存它时,只有 entityEntry.State == EntityState.Modified IF 语句被调用。所以我发现在保存实体后,CreatedDate 列被 Null 值覆盖,我可以在刷新后通过查看 SQL 资源管理器看到这一点。我认为问题与史蒂夫在下面提到的内容一致 "If it is #null then this might also explain the behavior in that it is not being loaded with the entity for whatever reason."

但是我有点迷失在跟踪这个 CreatedDate 值通过 edit/save 过程被丢弃到哪里的过程中。

下图显示了更新后保存实体之前的结果。在调试器中,我不太确定在哪里可以找到 CreatedDate 的条目以查看在此步骤中保留的值,但它似乎从调试器列表中丢失,所以徘徊是否以某种方式不知道它的存在保存时的这个字段。

下面是我在表单中使用的方法 'Edit' Razor 页面模型 class:

 public class EditModel : PageModel
{
    private readonly MyProject.Data.ApplicationDbContext _context;

    public EditModel(MyProject.Data.ApplicationDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public RuleParameters RuleParameters { get; set; }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        RuleParameters = await _context.RuleParameters
            .Include(r => r.SystemMapping).FirstOrDefaultAsync(m => m.ID == id);

        if (RuleParameters == null)
        {
            return NotFound();
        }
       ViewData["SystemMappingID"] = new SelectList(_context.SystemMapping, "ID", "MappingName");
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(RuleParameters).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!RuleParametersExists(RuleParameters.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool RuleParametersExists(int id)
    {
        return _context.RuleParameters.Any(e => e.ID == id);
    }
}

这个问题的原因之一可能是我没有在我的编辑 Razor 页面表单中包含 CreatedDate 字段,所以当我更新实体时,实体又会 运行 PostAsync 方法服务器一方面,没有为 CreatedDate 字段存储任何值,因此在我的 DbContext Class 中调用 savechangesasync 方法时包中没有任何内容。但我也觉得没有这个必要?否则我很难看到在使用继承的 BaseEntity class 的过程中有什么价值,即不必手动将 CreatedDate 和 UpdatedDate 属性添加到我想使用它的每个模型 class ...

给你的 BaseEntity 构造函数可能更容易:

public BaseEntity()
{
    UpdatedDate = DateTime.Now;
    CreatedDate = CreatedDate ?? UpdatedDate;
}

然后您可以让 DbContext 覆盖 SaveChangesAsync,例如:

public override Task<int> SaveChangesAsync(
    bool acceptAllChangesOnSuccess,
    CancellationToken token = default)
{
    foreach (var entity in ChangeTracker
        .Entries()
        .Where(x => x.Entity is BaseEntity && x.State == EntityState.Modified)
        .Select(x => x.Entity)
        .Cast<BaseEntity>())
    {
        entity.UpdatedDate = DateTime.Now;
    }

    return base.SaveChangesAsync(acceptAllChangesOnSuccess, token);
}

我不怀疑 EF 正在这样做,而是你的数据库,或者你无意中插入了记录而不是更新它们。

一个简单的测试:在 Modified 和 Added 处理程序中的 SaveChangesAsnc 方法中放置断点,然后 运行 加载实体、编辑实体并保存的单元测试。命中了哪个断点?如果通过简单的单元测试该行为似乎正常,请再次重复您的代码。

如果命中 Modified 断点,并且仅命中 Modified 处理程序,则检查已修改实体中 CreatedDate 值的状态。它是否仍然反映原始的 CreatedDate?如果是,那么您的架构中的某些内容似乎会在保存时覆盖它。如果不是,那么您的代码中存在导致更新的错误。如果它是 #null 那么这也可以解释这种行为,因为无论出于何种原因它都没有与实体一起加载。检查 属性 是否没有被配置为 Computed 属性.

之类的东西

如果完全命中添加的断点,那么这将指向您正在处理分离实体的场景,例如从不同的数据库上下文中读取并关联到另一个实体的实体当前的数据库上下文并保存为副产品。当 DbContext 遇到一个已加载并与另一个 DbContext 解除关联的实体时,它将将该实体视为一个全新的实体并插入一条新记录。最大的罪魁祸首总是人们传递实体 to/from 视图的 MVC 代码。实体引用在一个请求中加载,序列化到视图,然后在另一个请求中传回。开发人员假设他们正在接收一个实体,他们可以将其关联到一个新实体并保存,但此请求的上下文不知道该实体,并且 "entity" 实际上不是一个实体,它现在是序列化程序创建的数据的 POCO shell。这与新建 class 和填充字段没有什么不同。 EF 不会知道其中的区别。这样做的结果是您将触发实体的添加条件,完成后您将拥有重复的记录。 (如果 EF 配置为将 PK 视为身份,则具有不同的 PK)

订单屏幕就是一个例子:在显示创建新订单的屏幕时,我可能已经加载了客户并将其传递给视图以显示客户信息,并希望关联到新订单:

var customer = context.Customers.Single(x => x.CustomerId == 15);
var newOrder = new Order { Customer = customer };
return View(newOrder);

这看起来很天真。当我们在设置他们的详细信息后去保存新订单时:

public ActionResult Save(Order newOrder)
{
   context.Orders.Add(newOrder);
   newOrder.Customer.Orders.Add(newOrder);
   context.SaveChanges();
   // ...
}

newOrder 引用了客户 #14,所以一切看起来都不错。我们甚至将新订单与客户的订单集合相关联。我们甚至可能希望更新客户记录中的字段以反映对修改日期的更改。但是,本例中的 newOrder 和 all 相关数据(包括 .Customer)此时都是普通的 'ol C# 对象。我们已将新订单添加到上下文中,但就上下文而言,引用的客户也是 也是 一条新记录。如果将客户 ID 设置为身份列,它将忽略客户 ID,并将保存一个全新的客户记录(例如 ID #15),其中包含与客户 ID 14 相同的所有详细信息,并将其与新订单相关联。在您开始查询客户并发现重复的行之前,它可能很微妙并且很容易错过。

如果您要传递实体 to/from 视图,我会非常警惕这个问题。附加和设置修改后的状态是一种选择,但这涉及到相信数据没有被篡改。作为一般规则,更新实体的调用不应该传递实体并附加它们,而是重新加载这些实体,验证行版本,验证传入的数据,并且只在保存实体之前应该修改的字段之间复制关联到 DbContext。

希望这能为您提供一些想法,帮助您了解问题的根源。

Possibly one of the reasons for this issue is the fact that I have not included the CreatedDate field in my Edit Razor Page form, so when I update the entity which in turn will run the PostAsync method server side, there is no value stored for the CreatedDate field and therefore nothing in the bag by the tine the savechangesasync method is called in my DbContext Class.

那是true.Your post的数据不包含原来的CreatedDate,所以存到数据库的时候是null,不知道具体值是多少,除非你赋值过saving.It 是必须的。

您可以在剃须刀表单中添加以下代码。

<input type="hidden" asp-for="CreatedDate" />

更新:

要在服务器端克服它,您可以手动分配数据:

public async Task<IActionResult> OnPostAsync()
{
    RuleParameters originalData = await _context.RuleParameters.FirstOrDefaultAsync(m => m.ID == RuleParameters.ID);

    RuleParameters.CreatedDate = originalData.CreatedDate;

    _context.Attach(RuleParameters).State = EntityState.Modified;


    await _context.SaveChangesAsync();
 }