web-dev-qa-db-ja.com

同じキーを持つこのタイプの別のインスタンスがすでに追跡されているため、エンティティタイプ...のインスタンスを追跡できません

Entity Framework CoreとSQLデータベースを使用するASP.NET Core webappがあるとします。

データベース内のエンティティを更新しようとすると、絶対的な単純なアクションでこの例外がスローされます。本番環境のバグレポートで最初に気づきました。

_[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(string id, [Bind("Group")] EditViewModel model)
{
    if (id != model.Group.Id) return NotFound();

    if (!ModelState.IsValid) return View(model);

    _context.Update(model.Group);
    await _context.SaveChangesAsync();

    return RedirectToAction("Index");
}
_

次の行で例外がスローされます:_context.Update(model.Group);

InvalidOperationException:同じキーを持つこのタイプの別のインスタンスがすでに追跡されているため、エンティティタイプ「Group」のインスタンスは追跡できません。新しいエンティティを追加するとき、ほとんどのキータイプでは、キーが設定されていない場合(つまり、キープロパティにそのタイプのデフォルト値が割り当てられている場合)に、一意の一時キー値が作成されます。新しいエンティティのキ​​ー値を明示的に設定する場合は、既存のエンティティや他の新しいエンティティに対して生成された一時的な値と衝突しないようにしてください。既存のエンティティをアタッチするときは、特定のキー値を持つ1つのエンティティインスタンスのみがコンテキストにアタッチされるようにしてください。

明らかに他のインスタンスはありません。その行にブレークポイントを設定してコードを停止し、__context.Group_オブジェクトのResultsプロパティを展開すると、開発環境で例外を再現できました: screenshot of expanded Results property of the _context.Group object 結果を展開するときに、更新が必要なインスタンスをロードすることは理解できます。そのため、例外がスローされます。しかし、デプロイされた本番環境についてはどうでしょうか?

助けてくれてありがとう!

UPDATE1Groupモデル:

_public class Group
{
    [Display(Name = "ID")]
    public string Id { get; set; }

    public virtual Country Country { get; set; }

    [Required]
    [Display(Name = "Country")]
    [ForeignKey("Country")]
    public string CountryCode { get; set; }

    [Required]
    [Display(Name = "Name")]
    public string Name { get; set; }
}
_

UPDATE2@Mithgrothの回答に基づいて、使用するたびにtry-catchを必要としないように関数_context.Update()をオーバーライドすることができました:

_public interface IEntity 
{
    string Id { get; }
}

public override EntityEntry<TEntity> Update<TEntity>(TEntity entity)
{
    if (entity == null)
    {
        throw new System.ArgumentNullException(nameof(entity));
    }

    try
    {
        return base.Update(entity);
    }
    catch (System.InvalidOperationException)
    {
        var originalEntity = Find(entity.GetType(), ((IEntity)entity).Id);
        Entry(originalEntity).CurrentValues.SetValues(entity);
        return Entry((TEntity)originalEntity);
    }
}
_
4
Mark Szabo

代わりに以下を使用してください。

var group = _context.Group.First(g => g.Id == model.Group.Id);
_context.Entry(group).CurrentValues.SetValues(model.Group); 
await _context.SaveChangesAsync();

例外は多くの異なるシナリオによって引き起こされる可能性がありますが、実際には、別の方法でマークされているオブジェクトの状態を変更しようとしています。

たとえば、これは同じ例外を生成します:

var group = new Group() { Id = model.Id, ... };
db.Update(group);

または、分離されたN層の子がいる可能性もあります。

これにより、既存のエンティティの値が上書きされるだけです。

8
Mithgroth

アイデアをありがとう。私のコンテキストでこのオーバーライドされた関数を例外なしで動作させました。これは少し良い方法です。また、主キー名は、定義されたモデルを使用して取得されます。

また、これはEntityFramework 3.0 .NET Coreです。

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System.Linq;



    public override EntityEntry<TEntity> Update<TEntity>(TEntity entity) where TEntity : class
    {
        if (entity == null)
        {
            throw new System.ArgumentNullException(nameof(entity));
        }

        var type = entity.GetType();
        var et = this.Model.FindEntityType(type);
        var key = et.FindPrimaryKey();

        var keys = new object[key.Properties.Count];
        var x = 0;
        foreach(var keyName in key.Properties)
        {
            var keyProperty = type.GetProperty(keyName.Name, BindingFlags.Public | BindingFlags.Instance);
            keys[x++] = keyProperty.GetValue(entity);
        }

        var originalEntity = Find(type, keys);
        if (Entry(originalEntity).State == EntityState.Modified)
        {
            return base.Update(entity);
        }

        Entry(originalEntity).CurrentValues.SetValues(entity);
        return Entry((TEntity)originalEntity);
    }
1
Egbert Nierop