13

I have a simple snowflake schema out of which I generated my Entity Framework model.
The problem is that I am trying to map a child entity to an existing parent and/or grandparent entity, but it still inserts it.

I followed this:

Insert new object with existing object
Prevent Entity Framework to Insert Values for Navigational Properties

The interesting thing is that even though the EntityState of the parent entities is "Unchanged" the Entity Framework still tries to insert it.


The Schema

enter image description here


CarRepository.Save() method

public void Save(Car car)
    {
        using (DBContext context = new DBContext())
        {
            // No need to save if it already exists
            if ( context.Cars.FirstOrDefault(x => x.RegistrationNumber == car.RegistrationNumber) != null)
            {
                return;
            }
            else
            {
                // Check if the parent POCOs exist in the DB. 
                Model existingModel = context.Models.FirstOrDefault(x => x.Name == car.Model.Name);
                Manufacturer existingManufacturer = context.Manufacturers.FirstOrDefault(x=> x.Name == car.Model.Manufacturer.Name)
                Trader existingTrader = context.Traders.FirstOrDefault(x=> x.Name == car.Trader.Name)
                TraderCompany existingTraderCompany = context.TraderCompanys.FirstOrDefault(x=> x.Name == car.Trader.TraderCompany.Name)

                context.ContextOptions.LazyLoadingEnabled = false;

                //Attach to the context if existing in the DB, i.e mark the existing POCOs not to be added the DB
                if (existingModel != null)
                {
                    car.Model = existingModel;
                    Assert.IsTrue(context.ObjectStateManager.GetObjectStateEntry(car.Model).State == EntityState.Unchanged);
                }

                if (existingManufacturer != null)
                {
                    car.Model.Manufacturer = existingManufacturer;
                    Assert.IsTrue(context.ObjectStateManager.GetObjectStateEntry(car.Model.Manufacturer).State == EntityState.Unchanged);
                }

                if (existingTrader != null)
                {
                    car.Trader = existingTrader;
                    Assert.IsTrue(context.ObjectStateManager.GetObjectStateEntry(car.Trader).State == EntityState.Unchanged);
                }

                if (existingTraderCompany != null)
                {
                    car.Trader.TraderCompany = existingTraderCompany;
                    Assert.IsTrue(context.ObjectStateManager.GetObjectStateEntry(car.Trader.TraderCompany).State == EntityState.Unchanged);
                }

                //Mark the Car for Addition to the DB
                context.Cars.AddObject(car);
                context.ObjectStateManager.ChangeObjectState(car, EntityState.Added);


                //If the POCOs do not exist in the DB mark them for addition
                if (existingModel == null)
                {
                   context.ObjectStateManager.ChangeObjectState(car.Model,EntityState.Added);
                }

                if (existingManufacturer == null)
                {
                    context.ObjectStateManager.ChangeObjectState(car.Model.Manufacturer,EntityState.Added);
                }

                if (existingTrader == null)
                {
                    context.ObjectStateManager.ChangeObjectState(car.Trader,EntityState.Added);
                }

                if (existingTraderCompany == null)
                {
                    context.ObjectStateManager.ChangeObjectState(car.Trader.TraderCompany,EntityState.Added);
                }

                context.SaveChanges();

            }
        }
    }

Edit:

After a few days of tinkering I managed to come up with a workaround which worked for me.

It seems that the Car that is being passed to the CarRepository.Save() has some kind of internal context which is undetectable... That being so, it is impossible to detach it from that context/s and add it to the one in CarRepository.Save(). In order to actually add it to this context I deep/lazy copy the Car object and its navigation properties, if existing.


The workaround

public void Save(Car car)
{
    using (DBContext context = new DBContext())
    {
        // No need to save if it already exists
        if ( context.Cars
                    .Any(x => x.RegistrationNumber == car.RegistrationNumber))
        {
            return;
        }
        else
        {
            //Assign scalar properties to the deep copy
            Car carToBeSaved = new Car 
            {
                carToBeSaved.RegistrationNumber = car.RegistrationNumber,
                carToBeSaved.Price = car.Price
            }


            //Car -> Trader -> ...
            if(car.Trader != null)
            {   
                Trader existingTrader = 
                    context.Traders
                           .FirstOrDefault(x => x.Name == car.Trader.Name);

                //If exists in DB assign, if not deep copy
                carToBeSaved.Trader = existingTrader ??
                    new Trader
                    {
                        Name = car.Trader.Name,
                        JobTitle = car.Trader.JobTitle
                    }

                //Car -> Trader -> TraderCompany
                if(car.Trader.TraderCompany != null)
                {
                    TraderCompany existingTraderCompany = 
                        context.TradersCompanys
                               .FirstOrDefault(x => x.Name == car.Trader
                                                                 .TraderCompany
                                                                 .Name);

                    //If exists in DB assign, if not deep copy  
                    carToBeSaved.Trader.TraderCompany = existingTraderCompany ??
                        new TraderCompany
                        {
                            Name = car.Trader.TraderCompany.Name,
                            Address = car.Trader.TraderCompany.Address,
                            PhoneNumber = car.Trader.TraderCompany.PhoneNumber
                        }
                }
            }

            //Car -> Model -> ...
            if(car.Model != null)
            {   
                Model existingModel = 
                    context.Models
                           .FirstOrDefault(x => x.Name == car.Model.Name);

                //If exists in DB assign, if not deep copy
                carToBeSaved.Model = existingModel ??
                    new Model
                    {
                        Name = car.Model.Name
                    }

                //Car -> Model -> Manufacturer
                if(car.Model.Manufacturer != null)
                {
                    Manufacturer existingManufacturer = 
                        context.Manufacturers
                               .FirstOrDefault(x => x.Name == car.Model
                                                                 .Manufacturer
                                                                 .Name);

                    //If exists in DB assign, if not deep copy
                    carToBeSaved.Model.Manufacturer = existingManufacturer ??
                    new Manufacturer
                        {
                            Name = car.Model.Manufacturer.Name
                        }
                }
            }

            //Mark the Car for Addition to the DB
            context.Cars.AddObject(car);
            context.SaveChanges();

        }
    }
}

If someone have any thoughts on this, please share.

Thanks.

Community
  • 1
  • 1
Martin
  • 298
  • 3
  • 10
  • 1
    This `otherCostRecord` is actually the `car`, right? Can you provide an example with which of the four `existingXXX != null` cases you got the insertion? There is, in my opinion, a lot of redundant stuff in the code: The `ChangeObjectState` below `AddObject` is redundant and the whole block below that line until `SaveChanges` is redundant as well. I don't think it's the reason for the problem, but who knows... – Slauma Oct 26 '12 at 13:08
  • Thanks for spotting that one, I edited it. The exact _!= null case_ where the insertion happens is a *grandparent* one - Manufacturer. – Martin Oct 26 '12 at 13:15
  • Two other minor things: 1) Is there a reason why you disable lazy loading that late and not right at the beginning of the `using` block? It should not be a problem in release but in debug mode when you inspect the `existingXXX` entities you could accidentally trigger lazy loading. 2) Don't load the entity with `FirstOrDefault` and test for `null` just to check for existence. You can use `if (context.Cars.Any(x => x.RegistrationNumber == car.RegistrationNumber))` instead which doesn't materialize a full entity. – Slauma Oct 26 '12 at 13:18
  • Deleted my answer. It couldn't work. I missed that you don't query for the primary keys but for the `Name` property. – Slauma Oct 26 '12 at 15:00
  • 1
    Is it EF4? Can it be this: http://support.microsoft.com/kb/2390624?wa=wsignin1.0? – Pawel Nov 07 '12 at 16:52

3 Answers3

3

The most reliable way I've found around this type of issue is to use the Id of the existing object instead of associating the entity instance. So you would set the "foreign" key to the trader like so after finding the existing trader:

car.TraderId = existingTrader.Id;

For some time this seemed like a hack to me but in the April 2013 MSDN mag I read that Julie Lerman also recommends this approach.

MarkG
  • 1,859
  • 1
  • 19
  • 21
0

After a few days of tinkering I managed to come up with a workaround which worked for me.

It seems that the Car that is being passed to the CarRepository.Save() has some kind of internal context which is undetectable... That being so, it is impossible to detach it from that context/s and add it to the one in CarRepository.Save(). In order to actually add it to this context I deep/lazy copy the Car object and its navigation properties, if existing.


The workaround

public void Save(Car car)
{
    using (DBContext context = new DBContext())
    {
        // No need to save if it already exists
        if ( context.Cars
                    .Any(x => x.RegistrationNumber == car.RegistrationNumber))
        {
            return;
        }
        else
        {
            //Assign scalar properties to the deep copy
            Car carToBeSaved = new Car 
            {
                carToBeSaved.RegistrationNumber = car.RegistrationNumber,
                carToBeSaved.Price = car.Price
            }


            //Car -> Trader -> ...
            if(car.Trader != null)
            {   
                Trader existingTrader = 
                    context.Traders
                           .FirstOrDefault(x => x.Name == car.Trader.Name);

                //If exists in DB assign, if not deep copy
                carToBeSaved.Trader = existingTrader ??
                    new Trader
                    {
                        Name = car.Trader.Name,
                        JobTitle = car.Trader.JobTitle
                    }

                //Car -> Trader -> TraderCompany
                if(car.Trader.TraderCompany != null)
                {
                    TraderCompany existingTraderCompany = 
                        context.TradersCompanys
                               .FirstOrDefault(x => x.Name == car.Trader
                                                                 .TraderCompany
                                                                 .Name);

                    //If exists in DB assign, if not deep copy  
                    carToBeSaved.Trader.TraderCompany = existingTraderCompany ??
                        new TraderCompany
                        {
                            Name = car.Trader.TraderCompany.Name,
                            Address = car.Trader.TraderCompany.Address,
                            PhoneNumber = car.Trader.TraderCompany.PhoneNumber
                        }
                }
            }

            //Car -> Model -> ...
            if(car.Model != null)
            {   
                Model existingModel = 
                    context.Models
                           .FirstOrDefault(x => x.Name == car.Model.Name);

                //If exists in DB assign, if not deep copy
                carToBeSaved.Model = existingModel ??
                    new Model
                    {
                        Name = car.Model.Name
                    }

                //Car -> Model -> Manufacturer
                if(car.Model.Manufacturer != null)
                {
                    Manufacturer existingManufacturer = 
                        context.Manufacturers
                               .FirstOrDefault(x => x.Name == car.Model
                                                                 .Manufacturer
                                                                 .Name);

                    //If exists in DB assign, if not deep copy
                    carToBeSaved.Model.Manufacturer = existingManufacturer ??
                    new Manufacturer
                        {
                            Name = car.Model.Manufacturer.Name
                        }
                }
            }

            //Mark the Car for Addition to the DB
            context.Cars.AddObject(car);
            context.SaveChanges();

        }
    }
}

If someone have any thoughts on this, please share.

Martin
  • 298
  • 3
  • 10
0

I think you have the following problem:

If you set the Model to existingModel like here

if (existingModel != null)
{
    car.Model = existingModel;
    Assert.IsTrue(context.ObjectStateManager.GetObjectStateEntry(car.Model).State == EntityState.Unchanged);
}

then the ObjectStateManager detects that the car is new and adds it automatically to your context. Because of this EF detects that e.g. the preset car.Trader is unknown (means new) and adds it also. And so on.

So that is why theses will be save anyway.

Yann Olaf
  • 597
  • 3
  • 12