5

After days studying EF to understand (kinda..) how it works, I finally realized that I might have a big problem.

Imagine that I have two entities: Pais and UF. The relationship between them is Pais (0..1) ... (*) UF. A screenshot: https://i.stack.imgur.com/ZvA9X.jpg.

Said that, consider that I have a controller called UFController and it has actions for Edit and Create, which are just fine. My views are using the EditorFor helper (or similar ones) for inputs, so when I submit the form the controller will receive a UF object filled with all the data (automatically) with a reference to an almost-empty Pais. My view code (part of it):

@* UF attributes *@
@Html.EditorFor(m => m.Sigla)
@Html.EditorFor(m => m.Descricao)
@Html.EditorFor(m => m.CodigoIBGE)
@Html.EditorFor(m => m.CodigoGIA)
@* Pais primary key ("ID") *@
@Html.EditorFor(m => m.Pais.Codigo) // Pais id

The controller Edit action code:

[HttpPost]
public ActionResult Edit(UF uf)
{
    try
    {
        if (ModelState.IsValid)
        {
            db.UFs.Attach(uf);
            db.ObjectStateManager.ChangeObjectState(uf, EntityState.Modified);
            db.SaveChanges();

            return this.ClosePage(); // An extension. Just ignore it.
        }
    }
    catch (Exception e)
    {
        this.ModelState.AddModelError("Model", e.Message.ToString());
    }

    return View(uf);
}

When I submit the form, this is what the action receives as uf:

{TOTALWeb.UF}
    base {System.Data.Objects.DataClasses.EntityObject}: {TOTALWeb.UF}
    (...)
    CodigoGIA: 0
    CodigoIBGE: 0
    Descricao: "Foobar 2001"
    ID: 936
    Pais: {TOTALWeb.Pais}
    PaisReference: {System.Data.Objects.DataClasses.EntityReference<TOTALWeb.Pais>}

And uf.Pais:

{TOTALWeb.Pais}
    base {System.Data.Objects.DataClasses.EntityObject}: {TOTALWeb.Pais}
    Codigo: 0
    CodigoBACEN: null
    CodigoGIA: null
    CodigoIBGE: null
    Descricao: null
    UF: {System.Data.Objects.DataClasses.EntityCollection<TOTALWeb.UF>}

The original information (the one on the database) is uf.Pais.Codigo == 716. So, right now I'm receiving the updated information. The problem on that the controller is not upading the FK in the database.

I don't want to set the EntityState from uf.Pais to Modified because the entity itself wasn't changed (I didn't changed the information from that entry), but the relationship was.

In other words, what I'm trying to do is change the value of the FK, pointing the uf.Pais to another instance of Pais. Afaik, it's impossible to change the relationship state to Modified (throw an exception), so I'm looking for alternative solutions.

I've read a bunch of topics I've found on Google about this kind of problem, but I still didn't find a simple and elegant solution. The last ones I read here on stackoverflow:

I asked a question a few days ago about a similar problem ( Entity Framework 4.1 - default EntityState for a FK? ). I didn't understand how EF works that time, so now a bunch of things look clear to me (that's why I'm opening a new question).

For the Create action I've been testing this solution (proposed by Ladislav on my other question), but it generates an additional select (which can be eventually slow for us):

// Here UF.Pais is null
db.UFs.AddObject(uf);
// Create dummy Pais
var pais = new Pais { Id = "Codigo" };
// Make context aware of Pais
db.Pais.Attach(pais); // <- Executing a SELECT on the database, which -can- be slow.
// Now make the relation
uf.Pais = pais;
db.SaveChanges();

I can replicate this for the Edit (I guess), but I don't want that additional SELECT.

So, in resume: I'm trying to use navigation properties to send data to the controller and save them directly in the database using a fast and easy way (without messing too much with the entity - these ones are simple, but we have huge and very complex ones with a lot of FKs!). My question is: there's a solution that doesn't involve executing another query in the database (a simple one)?

Thanks,

Ricardo

PS: sorry for any english mistakes and any confusions.

Update 1: using BennyM's solution (kind of..)

I tested the following code, but it doesn't work. It throws an exception: "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key." Probably because Pais is already in the context, I guess?

I'm using a Entities (created by EF) class as context. Also, I don't know what is the method Entry, and I don't know where is it. Just for "fun", I tested this:

// Attach the Pais referenced on editedUF, since editedUF has the new Pais ID, not the old one.
Pais existingPais = new Pais { Codigo = editedUF.Pais.Codigo };
db.Paises.Attach(existingPais);

// Attach the edited UF.
db.UFs.Attach(editedUF);

// Set the correct Pais reference (ignoring the current almost-null one).
editedUF.Pais = existingPais;

// Change the object state to modified.
db.ObjectStateManager.ChangeObjectState(editedUF, EntityState.Modified);

// Save changes.
db.SaveChanges();

return this.ClosePage();

The exception is throwed when I try to attach the editedUF to the current context. I'm working with this idea right now, trying to find other solutions. Also, you're right BennyM, attaching the Pais to the context is not generating an additional SELECT. I don't know what happened that time, it really doesn't do anything with the database.

Still, this is a manual solution: I have to do that for each FK. That's what I'm trying to avoid. You see, some programmers, even if you explain 100 times, won't remember to do that with each FK. Eventually that'll come back to me, so I'm trying to avoid anything that can lead into errors (database or code ones) to make sure everyone can work without any stress. :)

Community
  • 1
  • 1
Ricardo
  • 172
  • 9

2 Answers2

2

I'm answering my own question because I've found a simple solution (at least in my case). My scenario uses a lot of Views for data input (which means that I have a lot of entities). I needed a simple and easy to use solution, so I deleted my entire Entities EDMX file (Ctrl+A, Delete!).

Then I decided to add again Pais and UF entities, but checking the checkbox for exposing the FK attribute. On first I though they can't work together, but they can, but you need to be a little careful on how to use it. They're now linked with navigation properties and the exposed FK.

The reason I couldn't add the FK attribute is because I was doing it manually. Using the "Update model from database" again checking the correct option it worked flawless.

In my edit view, I'm setting the ID of Pais into the FK attribute, not the Pais.Codigo. The reason why I do that is because the FK attribute is a scalar property and then I can detect changes.

This is the current view code for the Pais input (it's not exactly it, but it's similar to this):

@Html.EditorFor(m => m.PaisCodigo)

Btw, PaisCodigo is the FK. Yes, it can get a little confusing with Pais.Codigo, but we didn't decided any naming rules (yet). Any suggestions on this idea would be appreciated.

The final Edit action code is like this (I removed error processing to make it look simple!):

[HttpPost]
public ActionResult Edit(UF editedUF)
{
    if (ModelState.IsValid)
    {
        // Attach the edited UF into the context and change the state to Modified.
        db.UFs.Attach(editedUF);
        db.ObjectStateManager.ChangeObjectState(editedUF, EntityState.Modified);

        // Save changes.
        db.SaveChanges();

        // Call an extension (it's a redirect action to another page, just ignore it).
        return this.ClosePage();
    }
}

This is what is received when I submit the form for editedUF:

{TOTALWeb.UF}
base {System.Data.Objects.DataClasses.EntityObject}: {TOTALWeb.UF}
(...)
CodigoGIA: 0
CodigoIBGE: 0
CodigoPais: 0 <-- new Pais ID!
Descricao: "Foobar 2000"
ID: 902
Pais: {TOTALWeb.Pais}
PaisReference: {System.Data.Objects.DataClasses.EntityReference<TOTALWeb.Pais>}
Sigla: "RI"
Usuarios: {System.Data.Objects.DataClasses.EntityCollection<TOTALWeb.Usuario>}

As you can see, CodigoPais is pointing to the new Pais ID.

About the editedUF.Pais navigation property, there's a small detail. Before attaching it into the context, it's null. But, hey, after adding, this is what happens:

{TOTALWeb.Pais}
base {System.Data.Objects.DataClasses.EntityObject}: {TOTALWeb.Pais}
(...)
Codigo: 0
CodigoBACEN: 1058
CodigoGIA: 0
CodigoIBGE: null
Descricao: "Brasil"
UFs: {System.Data.Objects.DataClasses.EntityCollection<TOTALWeb.UF>}

So, it has been filled. The cost for that should be one query, but I couldn't capture it on the monitor.

In other words, just expose the FK, change it using the View and use the navigation property to make the code a little more clear. That's it! :)

Thanks everyone,

Ricardo

PS: I'm using dotConnect for Oracle as a base for the EF 4.1. We don't use SQL Server (at least for now). The "monitor" I said before was devArt's dbMonitor, so I can see all queries sent to the Oracle database. And, again, sorry for any english mistakes!

Ricardo
  • 172
  • 9
  • You are not using EF 4.1. I would suggest to swith to the POCO generation if you use EF 4.0 btw. – BennyM Aug 12 '11 at 06:00
  • @BennyM I'm kinda new to this, as you may see, but why are you suggesting to switch to POCO? Coding everything from scratch might be overkill for me (I was exactly trying to avoid that when I started using EF). – Ricardo Aug 12 '11 at 11:32
  • Just like the default code generation strategy there is a POCO (and even dbcontext as in code first) code generator available. http://visualstudiogallery.msdn.microsoft.com/23df0450-5677-4926-96cc-173d02752313/ – BennyM Aug 12 '11 at 11:39
  • @BennyM So, one of the greatest features of POCO it's the persistence ignorance. That will improve performance (mosstly in the logical part), I guess. And I read somewhere that's also easier to maintain. Your link has a blog link (see description) explaining how the generator works, which has another link pointing to a recommended way of POCO generation code (something related to the DbContext): [EF 4.1 Model & Database First Walkthrough](http://tinyurl.com/3mu4567). I'm gonna test POCO and see how it works. Is there any advantages creating the code from scratch? Thanks! – Ricardo Aug 12 '11 at 11:56
  • You already have an edmx so you can keep doing it that way. Doing it yourself (dbcontext with code first) is a bit more natural to developers. – BennyM Aug 12 '11 at 13:06
  • Ah, true. I've migrated the code generation to a DbContext generator and, after migrating some functions, it looks fine (didn't test it, though). It looks simpler now, and the classes are way clear then before. Thanks a lot! – Ricardo Aug 12 '11 at 14:04
0

If you include the foreign keys in your model. So add a PaisId property to the UF entity, you can directly set it and it will update the association.

using (var db = new Context())
{
   db.UFs.Attach(editedUF);
   editedUF.PaisId = theForeignKey;
   db.Entry(editedUF).State = EntityState.Modified;
   db.SaveChanges();
}

Also I've tested the approach you already mentioned in your question, and I don't get that extra select when attaching an entity. So this works as well:

using (var db = new Context())
{
   ExistingPais pais = new Pais{Id =theId};
   db.Pais.Attach(pais);
   db.UF.Attach(editedUF);
   editedUF.Pais = pais;
   db.Entry(editedUF).State = EntityState.Modified;
   db.SaveChanges();
}

Assuming your code looks like this:

public class  Pais
{
    public int Id { get; set; }
    public virtual ICollection<UF> UFs { get; set; }
} 

public class UF
{
  public int Id { get; set; }
  public virtual Pais Pais { get; set; }
  public int PaisId { get; set; }
}  
BennyM
  • 2,796
  • 16
  • 24
  • I'm not sure how exactly your `Context` class is. My `db` is a `Entities` class created by EF. Also, both `Pais` and `UF` were generated based on my database code. I edited my question to show what I've tried. – Ricardo Aug 11 '11 at 17:51
  • Btw, I have no `Entry` method here. Weird, since I saw it in a lot of examples. – Ricardo Aug 11 '11 at 18:23
  • Don't think you are using code first then or the dbcontext class, but rather entity framework 4.0 with objectcontext – BennyM Aug 12 '11 at 05:58
  • That I already knew. Code First examples/ideas was just an information resource for me, not how I was working. I think I should have made it clear in the question. Sorry for that. – Ricardo Aug 12 '11 at 11:20