9

I have a model that I get from a POST request. Since my view defines its POCO type, the object created from the submitted data is also a POCO. Being a POCO, it doesn't have various virtual properties overridden. Therefore, those virtual properties return null. This, in turn, means that I have to make separate queries based on the foreign keys to navigate through its properties (if I want to do anything more complex than just saving it).

Can I, given the POCO of my model, get the proxy that has all the overridden functionality?

(I had assumed that this is what db.Entry().Entity was for, but it still returns me the POCO object, not the proxy. I'm inspecting the runtime type of the object by mouse-over during breakpoint pauses.)

Theodoros Chatzigiannakis
  • 28,773
  • 8
  • 68
  • 104
  • possible duplicate of [Convert POCO object to Proxy object in EntityFramework](http://stackoverflow.com/questions/8174200/convert-poco-object-to-proxy-object-in-entityframework) – Dennis Jun 18 '13 at 12:23

3 Answers3

7

Something along the lines of this code will do what you need. I've used automapper to copy values from the passed in entity to the proxied version.

The code checks whether the passed in entity is a proxy or not and handles it accordingly.

public class Repository<T> where T : class
{
    private readonly Context context;
    private bool mapCreated = false;
    public Repository(Context context)
    {
        this.context = context;
    }

    protected virtual T InsertOrUpdate(T e, int id)
    {
        T instance = context.Set<T>().Create();
        if (e.GetType().Equals(instance.GetType()))
            instance = e;
        else
        {
            if (!mapCreated)
            {
                Mapper.CreateMap(e.GetType(), instance.GetType());
                mapCreated = true;
            }
            instance = Mapper.Map(e, instance);
        }

        if (id == default(int))
            context.Set<T>().Add(instance);
        else
            context.Entry<T>(instance).State = EntityState.Modified;

        return instance;
    }
}

UPDATE version as described by @Colin in the comments that does not need automapper

public class Repository<T> where T : class
{
    private readonly Context context;
    public Repository(Context context)
    {
        this.context = context;
    }

    protected virtual T InsertOrUpdate(T e, int id)
    {
        T instance = context.Set<T>().Create();
        if (e.GetType().Equals(instance.GetType()))
        {
            instance = e;
        }
        else
        {
            DbEntityEntry<T> entry = context.Entry(instance);
            entry.CurrentValues.SetValues(e);
        }

        context.Entry<T>(instance).State =
            id == default(int)
                ? EntityState.Added
                : EntityState.Modified;

        return instance;
    }
}
qujck
  • 14,388
  • 4
  • 45
  • 74
  • 1
    Thank you so much! This is exactly what I needed to elegantly get the appropriate proxy with correct values for various model types. – Theodoros Chatzigiannakis Jun 18 '13 at 13:01
  • 1
    @TheodorosChatzigiannakis I've updated the code to use the generic version of `Entry`. – qujck Jun 18 '13 at 13:11
  • Yes, I noticed. I've already customized your initial code to what I had in my hands (which was a library of generic handlers for various boilerplate procedures with the EF). Your idea really helped me move along. Thank you for your high quality answer! – Theodoros Chatzigiannakis Jun 18 '13 at 13:21
  • 3
    The Context.Entry.CurrentValues object has a SetValues method that lets you set properties without having to employ automapper. See this solution - http://stackoverflow.com/a/16811976/150342 – Colin Jun 19 '13 at 07:59
  • 1
    The updated version throws `InvalidOperationException` on the line `DbEntityEntry entry = context.Entry(instance);` if `e` is not a proxy object, because `instance` has not been added to `context` yet: "Member '`CurrentValues`' cannot be called for the entity of type '`T`' because the entity does not exist in the context. To add an entity to the context call the `Add` or `Attach` method of `DbSet`." – Korijn Feb 04 '15 at 12:09
  • I've been looking for this for a few hours. I overlooked the `.CurrentValue.SetValues(object)` overload. Thank you so much... – Pluc Mar 11 '15 at 18:55
1

db.Entry().Entity will always return you a POCO, and will not return the proxy object that handles the implementation of virtual navigation properties:

var o = db.Entry(myPoco).Entity;   // always returns a POCO

You will normally get a proxy object instead of a POCO when calling Find() or Where() against the database context. However, within the context in which an object is first added to the database, these methods will (unexpectedly?) return the POCO instead of the proxy object. You actually have to leave the context and open a new one to get the proxy:

        // create a new POCO object, and connect to it to another object already in the DB
        MyPoco myPoco = new MyPoco();
        myPoco.MyOtherPocoId = myPoco2.MyOtherPocoId;   // make reference to existing object

        using (var db = new MyContext())
        {
            // Add myPoco to database.
            db.MyPocos.Add(myPoco);
            db.SaveChanges();

            // One would think you get a proxy object here, but you don't: just a POCO
            var test10 = db.MyPocos.Find(myPoco.Id);                        // returns a MyPoco                        
            var test11 = db.MyPocos.Where(x => x.Id == myPoco.Id).First();  // returns a MyPoco
            var test12 = db.Entry(myPoco).Entity;                           // returns a MyPoco

            // ...so, you can't access the referenced properties through virtual navigation properties:
            MyOtherPoco otherPoco1 = myPoco.Poco2;  // returns NULL
        }

        // leave the context and build a new one

        using (var db = new MyContext())
        {
            // Now, the same Find() and Where() methods return a proxy object
            var test20 = db.MyPocos.Find(myPoco.Id);    // returns a proxy object
            var test21 = db.MyPocos.Where(x => x.Id == myPoco.Id).First();  // returns a proxy object

            // ...which means the virtual properties can be accessed as expected:
            MyOtherPoco otherPoco = myPoco.Poco2;   // works as expected

            // Note that db.Entry().Entity still returns a POCO:
            var test22 = db.Entry(myPoco).Entity;   // returns a MyPoco
        }

There may be some magic incantation to make the context in which the object is added give you back a proxy object, but I haven't come across it.

Myk Willis
  • 12,306
  • 4
  • 45
  • 62
  • Thanks! This resembles what I had already. It's such a pity that EF doesn't have anything to give a relevant proxy automatically. But I guess requesting an empty proxy and mapping it manually (or perhaps) with a tool such as Automapper, as was suggested in another answer) is the closest we can get to this functionality. – Theodoros Chatzigiannakis Jun 18 '13 at 13:29
0

If you want to do this via the MVC controller, you might use something like this as an action:

    [HttpPost]
    public ActionResult Update(int? id, FormCollection form)
    {
        // assumes repository will handle
        // retrieving the entity and
        // including and navigational properties
        var entity = repository.Get(id);
        if (entity == null)
        {
            throw new InvalidOperationException(string.Format("Not found: {0}", id));
        }
        if (TryUpdateModel(entity))
        {
            try
            {
                //
                // do other stuff, additional validation, etc
                repository.Update(entity);
            }
            catch (Exception ex)
            {
                //
                // exception cleansing/handling
                // additional model errors
                return View(entity);
            }
            return View("Success", entity);
        }            

        return View(entity);
    }
Tim
  • 5,435
  • 7
  • 42
  • 62
Ken
  • 834
  • 9
  • 25
  • Thank you for you answer! This is a valid approach and it is, indeed, similar to what I was using already. However, I felt it somewhat wasteful to re-query the database just to get around EF's limitations. That's why I was exploring other possible options for retrieving the proxy. – Theodoros Chatzigiannakis Jun 18 '13 at 13:26