none
Code First proxies: problem setting an unloaded optional navigation property to null RRS feed

  • Question

  • Hi, 

       I'm using EF 4.3.1 Code First with lazy-load and proxies enabled. I've made a simple project to show the problem I'm facing: Product and Category entities, with an optional unidirectional association from Product to Category. When I try to set product.Category = null, and that association is not loaded, EF simply ignores the assignment. If the association is loaded before attempting to set it to null, then it works (including the fix-up of the FK CategoryId property). ¿I'm missing something? Thanks, this is my code:

    using System.ComponentModel.DataAnnotations;
    using System.Data;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure;
    using System.Data.Objects;
    using System.Linq;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    namespace EFTests
    {
       public class Product
       {
          public virtual int ProductId { get; set; }
          public virtual string Description { get; set; }
          public virtual Category Category { get; set; }
          public virtual int? CategoryId { get; set; }
       }

       public class Category
       {
          public virtual int CategoryId { get; set; }
          public virtual string Description { get; set; }
       }

       public class TestContext : DbContext
       {
          public TestContext()
          {
             Configuration.AutoDetectChangesEnabled = true;
             Configuration.LazyLoadingEnabled = true;
             Configuration.ProxyCreationEnabled = true;
             Configuration.ValidateOnSaveEnabled = true;
          }

          public virtual DbSet<Product> Products { get; set; }
          public virtual DbSet<Category> Categories { get; set; }
       }

       [TestClass]
       public class EFTests
       {
          [TestMethod()]
          public void ImUsingProxies()
          {
             Product product;
             DbEntityEntry<Product> entry;

             using (TestContext ctx = new TestContext())
             {
                product = ctx.Products.FirstOrDefault(p => p.Category != null);

                Assert.IsNotNull(product);

                // product is a proxy.
                Assert.AreNotSame(ObjectContext.GetObjectType(product.GetType()), product.GetType());

                // it is a lazy-load proxy.
                Assert.IsFalse(ctx.Entry(product).Reference(p => p.Category).IsLoaded);
                string category = product.Category.Description;
                Assert.IsTrue(ctx.Entry(product).Reference(p => p.Category).IsLoaded);

                // it is a change-tracking proxy.
                entry = ctx.Entry(product);
                Assert.AreEqual(EntityState.Unchanged, entry.State);
                product.Description = product.Description + 'x';
                Assert.AreEqual(EntityState.Modified, entry.State);
             }
          }

          [TestMethod()]
          public void RemoveProductCategory()
          {
             Product product;
             
             using (TestContext ctx = new TestContext())
             {
                product = ctx.Products.FirstOrDefault(p => p.Category != null);
                
                Assert.IsNotNull(product);
                Assert.IsTrue(product.CategoryId.HasValue);
                
                Assert.IsFalse(ctx.Entry(product).Reference(p => p.Category).IsLoaded);

                //Uncomment the following line and everything works as expected (to me)
                //ctx.Entry(product).Reference(p => p.Category).Load();
                            
                product.Category = null;

                Assert.IsFalse(ctx.Entry(product).Reference(p => p.Category).IsLoaded);

                Assert.IsNull(product.Category); // lazy-load occurs here; product.Category is not null!
             }
          }
       }
    }


    Thursday, May 3, 2012 3:31 PM

Answers

  • Diego,

    if you are using full change tracking proxies, you will be able to set the nav property to null without loading the property in .NET 4.5\EF 5.0.

    -Julia


    This posting is provided "AS IS" with no warranties, and confers no rights.

    Wednesday, May 9, 2012 4:35 AM
    Moderator

All replies

  • Hi,

    I just tried your code with 'EntityFramework 5.0.0-beta2

    and setting the nav proeprty to null worked. Let me find out if that was a known issue in 4.3.1

    using (TestContext ctx = new TestContext())
    {
        product = ctx.Products.FirstOrDefault(p => p.Category != null);
        product.Category = null;
    
    thanks,

    Julia


    This posting is provided "AS IS" with no warranties, and confers no rights.

    Thursday, May 3, 2012 6:36 PM
    Moderator
  • Thank you Julia, I'm waiting for your response.
    Thursday, May 3, 2012 6:53 PM
  • There were no issues related to what you are seeing.

    This behavior was the same in 4.3.1. If you are using proxies then setting to null will work… if it’s just POCO then it won’t.

    I used your types to write a small test program and before product.Category = null line product.Category has a value,

    but after product.Category and product.CategoryId are set to null.

        public class Product
        {
           public virtual int ProductId { get; set; }
           public virtual string Description { get; set; }
           public virtual Category Category { get; set; }
           public virtual int? CategoryId { get; set; }
        }
     
       public class Category
        {
           public virtual int CategoryId { get; set; }
           public virtual string Description { get; set; }
        }
     
       public class TestContext : DbContext
        {
           public TestContext()
           {
              Configuration.AutoDetectChangesEnabled = true;
              Configuration.LazyLoadingEnabled = true;
              Configuration.ProxyCreationEnabled = true;
              Configuration.ValidateOnSaveEnabled = true;
           }
     
          public virtual DbSet<Product> Products { get; set; }
           public virtual DbSet<Category> Categories { get; set; }
        }
      
        class Program
        {
            static void Main(string[] args)
            {
                using (TestContext ctx = new TestContext())
                {
    
                    var newC = ctx.Categories.Create();
                    newC.Description = "cat1";
                    newC.CategoryId = 1;
    
                    var newP = ctx.Products.Create();
                    newP.Description = "p1";
                    newP.Category = newC;
                    ctx.Products.Add(newP);
                    ctx.SaveChanges();
                }
                using (TestContext ctx = new TestContext())
                {
                    Product product = ctx.Products.FirstOrDefault(p => p.Category != null);
                    product.Category = null;
                }
            }
        }

     thanks,

    Julia


    This posting is provided "AS IS" with no warranties, and confers no rights.


    Friday, May 4, 2012 7:08 AM
    Moderator
  • Hi Julia,

       Thank you for your time. I've added your code to a new test in my test project, and also as a console application like you did, but in both cases it doesn't work.

       This is the test code:

          [TestMethod]
          public void SlightlyModifiedJuliaCode()
          {
             int productId;
    
             using (TestContext ctx = new TestContext())
             {
    
                var newC = ctx.Categories.Create();
                newC.Description = "cat1";
                newC.CategoryId = 1;
    
                var newP = ctx.Products.Create();
                newP.Description = "p1";
                newP.Category = newC;
                ctx.Products.Add(newP);
                ctx.SaveChanges();
    
                productId = newP.ProductId;
             }
             
             using (TestContext ctx = new TestContext())
             {
                Product product = ctx.Products.Find(productId); // .FirstOrDefault(p => p.Category != null);
                product.Category = null;
    
                Assert.AreEqual("cat1", product.Category.Description);   // this shouldn't pass, but it does
                Assert.IsNull(product.Category);                         // this should pass, but it doesn't
             }
          }

    And this is the console application and its output:

    public class Program { static void Main(string[] args) { int productId; using (TestContext ctx = new TestContext()) { var newC = ctx.Categories.Create(); newC.Description = "cat1"; newC.CategoryId = 1; var newP = ctx.Products.Create(); newP.Description = "p1"; newP.Category = newC; ctx.Products.Add(newP); ctx.SaveChanges(); productId = newP.ProductId; } using (TestContext ctx = new TestContext()) { Product product = ctx.Products.Find(productId); //.FirstOrDefault(p => p.Category != null); product.Category = null; Console.WriteLine(product.Category == null); Console.WriteLine(product.Category); Console.ReadKey(); } }

    }


    ¿Can I send you my test project in any way?

    Friday, May 4, 2012 11:22 AM
  • Yes, please do at  juliako@microsoft.com

    This posting is provided "AS IS" with no warranties, and confers no rights.

    Friday, May 4, 2012 4:07 PM
    Moderator
  • Done, thank you!
    Friday, May 4, 2012 5:34 PM
  • Deigo,

    If you want to delete the relationship by setting the nav prop to null then the relationship needs to be loaded.

    Other ways to delete the relationship include:

    • Setting the FK to null
    • In EF5 on .NET 4.5 setting the current value to null using the property API:
      context.Entry(product).Reference(p => p.Category).CurrentValue = null;

    Thanks,

    Julia


    This posting is provided "AS IS" with no warranties, and confers no rights.

    Friday, May 4, 2012 8:56 PM
    Moderator
  • Julia,

       Thank you again for your time. I have to say that I'm very disappointed with your response, because you asserted that my code worked well in your tests, in both EF 4.3.1 and EF 5 beta.

       In my opinion, the natural way of deleting this kind of relationships is setting the navigation property to null. Transparent lazy-load means that I don't need to be aware of the load state of relationships. Persistence ignorance means that I don't need a "property API" to delete a relationship. I've read that FK properties in EF Code First are optional... but I now see that are required for this kind of associations.

       I think this is a bug. Based on my way of working with domain objects, this is a big one. If you agree with me, please give me instructions to report this as a bug, or do it yourself if you can.

       I'm very interested in your opinion, and obviously in the opinión of my collegues.

    Friday, May 4, 2012 11:43 PM
  • Hi Diego,

    The basic rule for POCO (proxy on non-proxy) is that setting a property to the same value does not count as a change. There are plenty of opinions on whether this is the correct behavior or not (even within our own team :) ). But we had to pick one and accept that whichever one we chose some folks would like it and others would not. In the non-proxy case there is no way for EF to detect a property being set to the same value so we decided to keep the behavior of proxies and non-proxies the same.

    Given that the foreign key property is always loaded you can set product.CategoryId to null and the change will go through as expected.

    Hope this helps clear things up a little.

    ~Rowan


    The *Pre-Release* Entity Framework Forum will be closing soon. We are seeing a lot of great Entity Framework questions (and answers) from the community on Stack Overflow. As a result, our team is going to spend more time reading and answering questions posted on Stack Overflow. We would encourage you to post questions on Stack Overflow using the entity-framework tag. We will also continue to monitor the Entity Framework forum.

    Monday, May 7, 2012 5:56 PM
    Moderator
  • Hi Rowan,

    I agree with that rule, but I don't see the point in keep the same behavior of proxies and non-proxies. Proxies have all they need to work well in this scenario.

    Yes, EF team chose one behavior, but we should be able to change it, using configuration or extending the proxies.

    Apart from this, if this behavior is by design, I think that EF should throw an exception when I try to remove an association using the navigation property (always, no matter if the association is loaded or not), because simply ignoring the assignment when not loaded induce bugs in our applications.

            Will remain this behavior in the next version of EF? You see any possible workaround, besides setting the FK, in this or next version of EF?

            Thanks.

    Tuesday, May 8, 2012 12:22 PM
  • Hi Diego,

    > I don't see the point in keep the same behavior of proxies and non-proxies

    The biggest advantage is consistency and not having to reason about whether an object is a proxy or not. If you are using proxies, it's very easy to end up with a mix of proxies and non-proxies attached to the context. This would mean that some objects behave differently to others.

    > ...You see any possible workaround, besides setting the FK...

    Yes, the other option is to temporarily set the reference to another category.

    var temp = new Category { CategoryId = -1 };
    ctx.Categories.Attach(temp);
    prod.Category = temp;
    prod.Category = null;

    ~Rowan


    The *Pre-Release* Entity Framework Forum will be closing soon. We are seeing a lot of great Entity Framework questions (and answers) from the community on Stack Overflow. As a result, our team is going to spend more time reading and answering questions posted on Stack Overflow. We would encourage you to post questions on Stack Overflow using the entity-framework tag. We will also continue to monitor the Entity Framework forum.

    Tuesday, May 8, 2012 6:58 PM
    Moderator
  • Rowan,

    This is my first post, so I don't know if this is the right place to discuss this, but... proxies and non-proxies already behave differently:

          [TestMethod]
          public void ProxiesAndNonProxiesBehaveDifferently()
          {
             Product proxy, nonProxy;
             Category category;
    
             using (TestContext ctx = new TestContext())
             {
                category = ctx.Categories.First();
                
                proxy = ctx.Products.Create();
                nonProxy = new Product();
    
                ctx.Products.Add(proxy);
                ctx.Products.Add(nonProxy);
    
                proxy.CategoryId = category.CategoryId;
                nonProxy.CategoryId = category.CategoryId;
    
                Assert.AreSame(category, proxy.Category);    // succeed: proxies fixed-up the navigation property
                Assert.AreSame(category, nonProxy.Category); // fail: non-proxies doesn't fix-up
             }
          }

    By workaround I meant a generic one, like a base class for my domain objects.

    Thanks.


    Tuesday, May 8, 2012 8:19 PM
  • Diego,

    if you are using full change tracking proxies, you will be able to set the nav property to null without loading the property in .NET 4.5\EF 5.0.

    -Julia


    This posting is provided "AS IS" with no warranties, and confers no rights.

    Wednesday, May 9, 2012 4:35 AM
    Moderator
  • Hi Julia,

       That's good news. Can you tell me the planned date of release of EF5 RTM?

       Thanks.

    Wednesday, May 9, 2012 10:46 AM
  • There were quite a few bug fixes after the EF5 Beta 2 release and so the EF team decided to release an RC before the final RTM. The RC should be available in the next couple of weeks and will have a go-live license. The final RTM will follow within a few months, the exact timing will depend on the feedback we get from the RC.

    -Julia


    This posting is provided "AS IS" with no warranties, and confers no rights.

    Wednesday, May 9, 2012 5:28 PM
    Moderator
  • Thank you and thanks to Rowan.
    Wednesday, May 9, 2012 5:57 PM