6

I have an MVC application that uses Entity Framework 5. In few places I have a code that creates or updates the entities and then have to perform some kind of operations on the updated data. Some of those operations require accessing navigation properties and I can't get them to refresh.

Here's the example (simplified code that I have)

Models

class User : Model
{
    public Guid Id { get; set; }
    public string Name { get; set; }
}

class Car : Model
{
    public Guid Id { get; set; }
    public Guid DriverId { get; set; }
    public virtual User Driver { get; set; }

    [NotMapped]
    public string DriverName
    {
        get { return this.Driver.Name; }
    }
}

Controller

public CarController
{
    public Create()
    {
       return this.View();
    }

    [HttpPost]
    public Create(Car car)
    {
        if (this.ModelState.IsValid)
        {
            this.Context.Cars.Create(booking);
            this.Context.SaveChanges();

            // here I need to access some of the resolved nav properties
            var test = booking.DriverName;
        }

        // error handling (I'm removing it in the example as it's not important)
    }
}

The example above is for the Create method but I also have the same problem with Update method which is very similar it just takes the object from the context in GET action and stores it using Update method in POST action.

public virtual void Create(TObject obj)
{
    return this.DbSet.Add(obj);
}

public virtual void Update(TObject obj)
{
    var currentEntry = this.DbSet.Find(obj.Id);
    this.Context.Entry(currentEntry).CurrentValues.SetValues(obj);
    currentEntry.LastModifiedDate = DateTime.Now;
}

Now I've tried several different approaches that I googled or found on stack but nothing seems to be working for me.

In my latest attempt I've tried forcing a reload after calling SaveChanges method and requerying the data from the database. Here's what I've done.

I've ovewrite the SaveChanges method to refresh object context immediately after save

public int SaveChanges()
{
    var rowsNumber = this.Context.SaveChanges();
    var objectContext = ((IObjectContextAdapter)this.Context).ObjectContext;
    objectContext.Refresh(RefreshMode.StoreWins, this.Context.Bookings);

    return rowsNumber;
}

I've tried getting the updated object data by adding this line of code immediately after SaveChanges call in my HTTP Create and Update actions:

car = this.Context.Cars.Find(car.Id);

Unfortunately the navigation property is still null. How can I properly refresh the DbContext immediately after modifying the data?

EDIT

I forgot to originally mention that I know a workaround but it's ugly and I don't like it. Whenever I use navigation property I can check if it's null and if it is I can manually create new DbContext and update the data. But I'd really like to avoid hacks like this.

class Car : Model
{    
    [NotMapped]
    public string DriverName
    {
        get
        {
            if (this.Driver == null)
            {
                using (var context = new DbContext())
                {
                    this.Driver = this.context.Users.Find(this.DriverId);
                }
            }

            return this.Driver.Name;
         }
    }
}
RaYell
  • 69,610
  • 20
  • 126
  • 152

2 Answers2

4

The problem is probably due to the fact that the item you are adding to the context is not a proxy with all of the necessary components for lazy loading. Even after calling SaveChanges() the item will not be converted into a proxied instance.

I suggest you try using the DbSet.Create() method and copy across all the values from the entity that you receive over the wire:

public virtual TObject Create(TObject obj)
{
    var newEntry = this.DbSet.Create();
    this.Context.Entry(newEntry).CurrentValues.SetValues(obj);
    return newEntry;
}

UPDATE

If SetValues() is giving an issue then I suggest you try automapper to transfer the data from the passed in entity to the created proxy before Adding the new proxy instance to the DbSet. Something like this:

private bool mapCreated = false;
public virtual TObject Create(TObject obj)
{
    var newEntry = this.DbSet.Create();

    if (!mapCreated)
    {
        Mapper.CreateMap(obj.GetType(), newEntry.GetType());
        mapCreated = true;
    }
    newEntry = Mapper.Map(obj, newEntry);

    this.DbSet.Add(newEntry;
    return newEntry;
}
qujck
  • 14,388
  • 4
  • 45
  • 74
  • What about my `Update` method? Should I also fetch the original object other than `this.Context.Cars.Find(id)`? – RaYell Jul 05 '13 at 11:31
  • The `Find` will return a proxy object from the database, but if you are finding and updating an object that has been added locally during this transaction it will find the object you've added. – qujck Jul 05 '13 at 11:57
  • I tried following your solution but that lead me to `System.InvalidOperationException: Member 'CurrentValues' cannot be called for the entity of type 'Car' because the entity does not exist in the context. To add an entity to the context call the Add or Attach method of DbSet.` I've added `this.Context.Set().Attach(newEntry);` just after your first line and now I'm getting `System.InvalidOperationException: The property 'Id' is part of the object's key information and cannot be modified.` – RaYell Jul 05 '13 at 12:10
  • Just for a test can you copy all the values manually before `Attach`? – qujck Jul 05 '13 at 12:30
  • I just did that and now I'm getting this exception on `SaveChanges`. `System.Data.Entity.Core.OptimisticConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.` – RaYell Jul 05 '13 at 12:45
  • I think the problem now is that you're not `Add`ing the new entity. – qujck Jul 05 '13 at 13:10
  • To avoid the error message "The property 'Id' is part of the object's key information and cannot be modified." try setting the Id explicitly before calling SetValues. There is an example of working code here: http://stackoverflow.com/a/16811976/150342 – Colin Jul 08 '13 at 09:28
  • 1
    @Colin I considered that option but it complicated the answer because it's not generic without a common base class/interface that exposes the id property. – qujck Jul 08 '13 at 10:13
  • 1
    @qujck it's true that this solution requires a base class that exposes the Id property. I assumed that the questionner must have this too since Car and User both implement Model and they refer to obj.Id in the Update method. I have found a single base class for my Models very useful for other purposes too but I am sure it is not for everyone so thanks for highlighting the point. – Colin Jul 08 '13 at 11:25
0

I use next workaround: detach entity and load again

public T Reload<T>(T entity) where T : class, IEntityId
{
    ((IObjectContextAdapter)_dbContext).ObjectContext.Detach(entity);
    return _dbContext.Set<T>().FirstOrDefault(x => x.Id == entity.Id);
}
rnofenko
  • 9,198
  • 2
  • 46
  • 56