Entity Framework - Avoid "lookup" objects getting added state on initial load in a different context

Here are some test codes that show my problem:

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using NUnit.Framework;

namespace EFGraphInsertLookup
{
    public class GraphLookup
    {
        public int ID { get; set; }
        public string Code { get; set; }
    }

    public class GraphChild
    {
        public int ID { get; set; }
        public virtual GraphRoot Root { get; set; }
        public virtual GraphLookup Lookup { get; set; }
    }

    public class GraphRoot
    {
        public int ID { get; set; }
        public virtual ICollection<GraphChild> Children { get; set; }
    }

    public class TestDbContext : DbContext
    {
        public DbSet<GraphRoot>   GraphRoots    { get; set; }
        public DbSet<GraphChild>  GraphChildren { get; set; }
        public DbSet<GraphLookup> GraphLookups  { get; set; }

        public TestDbContext()
        {
            GraphLookups.ToList();
        }
    }

    public class TestDbInit : DropCreateDatabaseAlways<TestDbContext>
    {
        protected override void Seed(TestDbContext context)
        {
            base.Seed(context);
            context.GraphLookups.Add(new GraphLookup { Code = "Lookup" });
            context.SaveChanges();
        }
    }

    [TestFixture]
    public class Tests
    {
        [Test]
        public void MainTest()
        {
            Database.SetInitializer<TestDbContext>(new TestDbInit());

            var lookupCtx = new TestDbContext();
            var firstLookup = lookupCtx.GraphLookups.Where(l => l.Code == "Lookup").Single();

            var graph = new GraphRoot
            {
                Children = new List<GraphChild> { new GraphChild { Lookup = firstLookup } }
            };
            var ctx = new TestDbContext();
            ctx.GraphRoots.Add(graph); // Creates a new lookup record, which is not desired
            //ctx.GraphRoots.Attach(graph); // Crashes due to dupe lookup IDs
            ctx.SaveChanges();

            ctx = new TestDbContext();
            graph = ctx.GraphRoots.Single();
            Assert.AreEqual(1, graph.Children.First().Lookup.ID, "New lookup ID was created...");
        }
    }
}

      

My desire is for GraphLookup to act like a lookup table where records are linked to other records, but records are never created using the application.

The problem I'm running into is that the search object is loaded in a different context, like when it is cached. Thus, the context performing the saving of the entry does not track this entity, and when the Add method from the GraphRoot DbSet is called, the search ends with the EntityState of Added, but in fact it should be immutable.

If I try to use attach instead, it fails due to duplicate keys because the two search objects are hitting the context.

What is the best way to solve this problem? Please note that I have simplified the real problem a bit. In my actual application, this happens across several different tiers of storage, units of work, and business service classes that sit on top of the EF DBContext. So a one-size-fits-all solution that I can somehow apply in DBC text would be very preferable.

+3


source to share


2 answers


If you are migrating existing objects (for example from a cache) to another DbContext

, you will need to manage the state of the entity explicitly. This leads to two simple conclusions: don't mix objects from multiple contexts unless you really need to, and when you do, explicitly sets the object state of everything you attach.

One way to cache is to try this. Create a simple cache manager class, possibly static. For each entity type that you want to cache, use a method GetMyEntity(int myEntityId, DbContext context)

that looks something like this:

public MyEntity GetMyEntity(int entityId, MyContext context)
{
    MyEntity entity;

    // Get entity from context if it already loaded.
    entity = context.Set<MyEntity>().Loaded.SingleOrDefault(q => q.EntityId == entityId);

    if (entity != null)
    {
        return entity;
    }
    else if (this.cache.TryGetValue("MYENTITY#" + entityId.ToString(), out entity)
    {
        // Get entity from cache if it present.  Adapt this to whatever cache API you're using.
        context.Entry(entity).EntityState = EntityState.Unchanged;
        return entity;
    }
    else
    {
        // Load entity if it not in the context already or in the cache.
        entity = context.Set<MyEntity>().Find(entityId);

        // Add loaded entity to the cache.  Adapt this to specify suitable rules for cache item expiry if appropriate.
        this.cache["MYENTITY#" + entityId.ToString()] = entity;
        return entity;
    }
}

      

Please excuse any typos, but hopefully you get the idea. You can probably see that this can be generalized, so you don't need to have one method for each entity type.

Edit:



The following code might be helpful to show you how you can detach everything except the creature you really want to add.

// Add a single entity.
context.E1s.Add(new1);

var dontAddMeNow = (from e in context.ChangeTracker.Entries()
                    where !object.ReferenceEquals(e.Entity, new1)
                    select e).ToList();

foreach (var e in dontAddMeNow)
{
    e.State = System.Data.EntityState.Unchanged;  // Or Detached.
}

      

Edit2:

Here is some code showing how preloading reference data can work around your problem.

E2 child = new E2 { Id = 1 };

context.Entry(child).State = System.Data.EntityState.Unchanged;

E1 new1 = new E1
{
    Child = child
};

// Add a single entity.
context.E1s.Add(new1);

Debug.Assert(context.Entry(new1.Child).State == System.Data.EntityState.Unchanged);
Debug.Assert(context.Entry(new1).State == System.Data.EntityState.Added);

      

+3


source


is a search defined as a foreign key? Is this the first code? If so, try modifying the child to have a LookupID property, not just a navigation property.
Then put only GraphLookiD. (better for performance, since the lookup shouldn't be loaded first.)

public class GraphChild
{
    public int ID { get; set; }
    public int GraphLookupId  { get; set; } //<<<<< add this an SET ONLY this
    public virtual GraphRoot Root { get; set; }
    public virtual GraphLookup Lookup { get; set; }
}

      

free api snippet for GraphCHILD object



  .HasRequired(x => x.Lookup).WithMany().HasForeignKey(x => x.graphlookupID);

      

OR

if you want the current approach to work you can try Attach Lookup Element to FIRST Context. make sure it is not marked for adding a graph;)

+1


source







All Articles