EF Core 中断开连接的实体

axe*_*ter 5 c# asp.net-core-2.1 entity-framework-core-2.2

我一直在寻找在断开连接的情况下记录实体的更改。

原始问题:我调用DbContext.Update(Entity)了一个更新的复杂实体,即使没有任何变化,但所有内容都被 ChangeTracker 标记为已更改,并且我的并发RowVersion列计数。

POST 用户没有变化

"userSubPermissions": [],
"fidAdresseNavigation": {
    "idAdresse": 1,
    "fidPlzOrt": 3,
    "strasse": "Gerold Str.",
    "hausnr": "45",
    "rowVersion": 10,
    "isDeleted": 0,
    "fidPlzOrtNavigation": {
        "idPlzOrt": 3,
        "plz": "52062",
        "ort": "Aachen",
        "rowVersion": 9,
        "isDeleted": 0
    }
},
"idUser": 35,
"fidAnrede": null,
"fidAdresse": 1,
"fidAspnetuser": "a7ab78be-859f-4735-acd1-f06cd832be7e",
"vorname": "Max",
"nachmname": "Leckermann",
"eMail": "kunde@leasing.de",
"rowVersion": 11,
"isDeleted": 0
Run Code Online (Sandbox Code Playgroud)

POST 返回用户

"userSubPermissions": [],
"fidAdresseNavigation": {
    "idAdresse": 1,
    "fidPlzOrt": 3,
    "strasse": "GeroldPenis Str.",
    "hausnr": "45",
    "rowVersion": 11,
    "isDeleted": 0,
    "fidPlzOrtNavigation": {
        "idPlzOrt": 3,
        "plz": "52062",
        "ort": "Aachen",
        "rowVersion": 10,
        "isDeleted": 0
    }
},
"idUser": 35,
"fidAnrede": null,
"fidAdresse": 1,
"fidAspnetuser": "a7ab78be-859f-4735-acd1-f06cd832be7e",
"vorname": "Max",
"nachmname": "Leckermann",
"eMail": "kunde@leasing.de",
"rowVersion": 12,
"isDeleted": 0
Run Code Online (Sandbox Code Playgroud)

RowVersion 逻辑位于DBContext 中,仅在修改实体状态时更改行版本。

foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified))
        {
            var saveEntity = entity.Entity as ISavingChanges;
            saveEntity.OnSavingChanges();
        }
Run Code Online (Sandbox Code Playgroud)

从我过去 2 天的调查来看,EF Core 似乎有两种选择。

1.- 正如在这个和许多类似的帖子中提到的,你可以使用 TrackGraph https://www.mikesdotnetting.com/article/303/entity-framework-core-trackgraph-for-disconnected-data

问题:当实体的 ID 存在时,即使没有任何更改,此解决方案也会将整个实体标记为已更改。我得到与上述完全相同的结果,RowVersion计数意味着我的实体及其所有属性都已标记为已修改。

2.- 正如这篇文章中提到的 https://blog.tonysneed.com/2017/10/01/trackable-entities-for-ef-core/你可以下载一个 NuGet 包并 在我的机器上实现IMergeableITrackable接口实体。

问题:客户端需要跟踪模型中的更改并将它们传递给 API,我想避免这种情况,因为 Angular 似乎没有为此提供一个好的解决方案。

3.-通过记录原始值第 4 章:记录原始值,关于 EF 6 如何处理断开连接的场景,编程实体框架一书中还有另一个解决方案。当更新进来时,上下文从数据库中读取实体,然后将传入的实体与数据库实体进行比较,以查看是否对属性进行了任何更改,然后将它们标记为已修改。

https://www.oreilly.com/library/view/programming-entity-framework/9781449331825/ch04.html

问题:书中描述的某些代码在 EF Core 中无法实现,例如一个例子。

((IObjectContextAdapter)this).ObjectContext
.ObjectMaterialized += (sender, args) =>
{
  var entity = args.Entity as IObjectWithState;
  if (entity != null)
  {
    entity.State = State.Unchanged;

    entity.OriginalValues =
      BuildOriginalValues(this.Entry(entity).OriginalValues);
  }
};
Run Code Online (Sandbox Code Playgroud)

必须有一种方法来实现这种巧妙的解决方案,在断开连接的情况下处理 API 而不是客户端上的更改,否则,这可能是 EF Core 中非常有用的功能,因为大多数开发人员都将它用于断开连接的场景。

小智 2

我的回答是使用TrackGraph

概要

只想修改实体的几列以及添加\修改嵌套子实体

  • 在这里,我正在更新Scenario实体并仅修改ScenarioDate.
  • 在其子实体(即导航属性)中TempScenario,我添加一条新记录
  • 在嵌套子实体中Scenariostation,我添加并修改记录
public partial class Scenario
{
    public Scenario()
    {
        InverseTempscenario = new HashSet<Scenario>();
        Scenariostation = new HashSet<Scenariostation>();
    }
    public int Scenarioid { get; set; }
    public string Scenarioname { get; set; }
    public DateTime? Scenariodate { get; set; }
    public int Streetlayerid { get; set; }
    public string Scenarionotes { get; set; }
    public int? Modifiedbyuserid { get; set; }
    public DateTime? Modifieddate { get; set; }
    public int? Tempscenarioid { get; set; }

    
    public virtual Scenario Tempscenario { get; set; }
    public virtual ICollection<Scenario> InverseTempscenario { get; set; }
    public virtual ICollection<Scenariostation> Scenariostation { get; set; }
}


public partial class Scenariostation
{
    public Scenariostation()
    {
        Scenariounit = new HashSet<Scenariounit>();
    }

    public int Scenariostationid { get; set; }
    public int Scenarioid { get; set; }
    public int Stationid { get; set; }
    public bool? Isapplicable { get; set; }
    public int? Createdbyuserid { get; set; }
    public int? Modifiedbyuserid { get; set; }
    public DateTime? Modifieddate { get; set; }
    
    public virtual Scenario Scenario { get; set; }
    public virtual Station Station { get; set; }
}

public partial class Station
{
    public Station()
    {
        Scenariostation = new HashSet<Scenariostation>();
    }

    public int Stationid { get; set; }
    public string Stationname { get; set; }
    public string Address { get; set; }
    public NpgsqlPoint? Stationlocation { get; set; }
    public int? Modifiedbyuserid { get; set; }
    public DateTime? Modifieddate { get; set; }

    public virtual ICollection<Scenariostation> Scenariostation { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
  • 使用 EF Core,如果您不想进行 2 次数据库往返,那么在断开连接的情况下更新数据会很棘手。

  • 尽管 2 次数据库访问看起来并不重要,但如果数据表有数百万条记录,则可能会影响性能。

  • 另外,如果只有很少的列需要更新,包括嵌套子实体的列,则Usual Approach不起作用

通常的方法

public virtual void Update(T entity)
{
    if (entity == null)
        throw new ArgumentNullException("entity");

    var returnEntity = _dbSet.Attach(entity);
    _context.Entry(entity).State = EntityState.Modified;
}

Run Code Online (Sandbox Code Playgroud)

但这里的问题是断开连接的 EF Core 更新,如果您使用此 DbContext.Entry(entity).EntityState = EntityState.IsModified,所有列都将更新。因此某些列将更新为其默认值,即 null 或默认数据类型值。

此外, 的一些记录ScenarioStation根本不会更新,因为实体状态将为UnChanged

因此,为了仅更新从客户端发送的列,需要以某种方式告知 EF Core

使用 ChangeTracker.TrackGraph

最近我发现了这个DbConetxt.ChangeTracker.TrackGraph方法,可以用来标记Added实体UnChanged的状态。

不同 之处在于TrackGraph,您可以添加自定义逻辑,因为它会迭代地导航实体的导航属性。

我使用 TrackGraph 的自定义逻辑

public virtual void UpdateThroughGraph(T entity, Dictionary<string, List<string>> columnsToBeUpdated)
{
    if (entity == null)
        throw new ArgumentNullException("entity");

    _context.ChangeTracker.TrackGraph(entity, e =>
    {
        string navigationPropertyName = e.Entry.Entity.GetType().Name;

        if (e.Entry.IsKeySet)
        {
            e.Entry.State = EntityState.Unchanged;
            e.Entry.Property("Modifieddate").CurrentValue = DateTime.Now;

            if (columnsToBeUpdated.ContainsKey(navigationPropertyName))
            {
                foreach (var property in e.Entry.Properties)
                {
                    if (columnsToBeUpdated[e.Entry.Entity.GetType().Name].Contains(property.Metadata.Name))
                    {
                        property.IsModified = true;
                    }
                }
            }
        }
        else
        {
            e.Entry.State = EntityState.Added;
        }

    });

}
Run Code Online (Sandbox Code Playgroud)

通过这种方法,我能够轻松地仅处理所需的列更新以及任何嵌套子实体及其列的新添加/修改。