2

I have two entities Employee and Asset. They are defined as:

public class Employee
{
    public Employee()
    {
        this.Assets = new List<Asset>();
    }

    public int EmployeeID { get; set; }
    public string Name { get; set; }
    public virtual List<Asset> Assets { get; set; }
}

public class Asset
{
    public string FacilityAssetID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

Fluent defines the schema as:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>().ToTable("Employees");
    modelBuilder.Entity<Employee>().HasKey(e => e.EmployeeID);
    modelBuilder.Entity<Employee>().Property(e => e.Name).HasMaxLength(50).IsRequired();
    modelBuilder.Entity<Employee>().HasMany(e => e.Assets).WithMany().Map(m => m.MapLeftKey("EmployeeID").MapRightKey("FacilityAssetID").ToTable("EmployeeToAsset"));

    modelBuilder.Entity<Asset>().ToTable("Assets");
    modelBuilder.Entity<Asset>().HasKey(a => a.FacilityAssetID);
    modelBuilder.Entity<Asset>().Property(a => a.Name).HasMaxLength(50).IsRequired();
    modelBuilder.Entity<Asset>().Property(a => a.Description).HasMaxLength(200).IsRequired();

    base.OnModelCreating(modelBuilder);
}

Now, I have some employees in the tables as well as some assets. The assets are populated using a web service. Suppose I have the following assets:

  • {FacilityAssetID}, {Name}, {Description}

  • Asset1F32, Spindle Head, SomeDesc
  • Asset1C53, Milling Block, SomeDesc

There are two assets with IDs: Asset1F32 and Asset1C53. If I try to add an asset to an employee using the following code, I can successfully add an asset:

using (VCContext context = new VCContext())
{
    Employee emp = context.Employees.First();
    Asset assetDetached = new Asset() { FacilityAssetID = "Asset1F32" };
    context.Assets.Attach(assetDetached);
    emp.Assets.Add(assetDetached);

    context.SaveChanges();
}

If I loop through some of the assets beforehand and try to add an asset, it will throw an exception. Here's that code:

using (VCContext context = new VCContext())
{
    foreach (Employee employee in context.Employees)
    {
        Console.WriteLine(employee.Name);
        foreach (Asset asset in employee.Assets)
        {
            Console.WriteLine(string.Format("  - {0}", asset.FacilityAssetID));
        }
    }

    Employee emp = context.Employees.First();
    Asset assetDetached = new Asset() { FacilityAssetID = "Asset1F32" };
    context.Assets.Attach(assetDetached);  // EXCEPTION HERE!!!
    emp.Assets.Add(assetDetached);

    context.SaveChanges();
}

The exception thrown is:

Attaching an entity of type 'ASTC.Asset' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.

Why would it throw an exception in the second case but not the first case? Additionally, how can I add the detached asset in the second scenario?

BenMorel
  • 34,448
  • 50
  • 182
  • 322
Trecius Veil
  • 121
  • 8
  • Why are you calling Attach() here? Your entities are created and queried within the same context, so no need to attach them. – Florian Haider Jun 16 '16 at 06:35
  • The only entity being created and queried is the `Employee.` The `Asset` is not being queried. It's a short example of what is crashing on me. In reality, I'm looping through the DBs current asset list and comparing to another offsite list. If there's one that needs to be added, I'm trying to add it. – Trecius Veil Jun 16 '16 at 16:16
  • Yes but my point is, if you want to add an asset that is not yet in the DB, just call `emp.Assets.Add(new Asset())`, there is no reason to attach it first. The `Attach()` function is used for entites that are already in the DB, but have been queried in another context. – Florian Haider Jun 16 '16 at 17:57
  • Hi, Florian. It may already be in the database, but it may not be associated with that employee yet. – Trecius Veil Jun 17 '16 at 00:03
  • Ok, I get it :-) you want to use entity stubs to build your associations. This exception is thrown because you try to attach a stub for an entity that has already been loaded in your `foreach` loop. So to fix this, you have to check your loaded entities and use the full entity instead of the stub and associate it with an employee. – Florian Haider Jun 17 '16 at 06:49
  • Thanks, Florian! That helps a lot. I understand now; I will do that. – Trecius Veil Jun 17 '16 at 16:06

2 Answers2

2

By looping through context.Employees and their assets, Asset "Asset1F32" is loaded into the context, i.e. attached, so you can't attach another instance with the same primary key value.

I don't know why you loop through the assets, but a quick fix is to use context.Employees.AsNoTracking(), so the entities from that statement won't be attached to the context.

A more robust fix would be:

var id = "Asset1F32";
var asset = context.Assets.Local.FirstOrDefault(a => FacilityAssetID == id)
                ?? new Asset() { FacilityAssetID = id };
emp.Assets.Add(asset);

By this you check in the context's cache if the asset already is attached and if it isn't, you create it as a stub entity, so you can establish the association efficiently (i.e. without unnecessarily fetching the asset from the database).

BenMorel
  • 34,448
  • 50
  • 182
  • 322
Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
0

See if this answer helps:

It shows how to check if the object is already attached.

This version works for me:

public static bool Exists<T>(T entity) where T : class
{
    return dbContext.Set<T>().Local.Any(e => e == entity);
}

called like this:

if (!Exists<Person>(p))
{
    dbContext.People.Attach(p);
}
Community
  • 1
  • 1
Peter Bill
  • 508
  • 3
  • 12