Quantcast
Channel: West Wind Message Board Messages
Viewing all articles
Browse latest Browse all 10393

Re: Help with NewEntity in EFCodeFirst wrapper

$
0
0
Re: Help with NewEntity in EFCodeFirst wrapper
West Wind Web Toolkit for ASP.NET
Re: Help with NewEntity in EFCodeFirst wrapper
Dec. 29, 2012
08:53 pm
3OG18RIVCShow this entire thread in new window
Gratar Image based on email address
From:Matt Slay
To:Rick Strahl
Ok, boss... Good news:

I have now implemented every single detail you have explained in these code samples. (I copied your code exactly, even the ability to share Contexts in the contstructor.) So, I'm happy to report that it does work EXACTLY as you have said it would/should.

What a big relief.

I won't bore you with the details, but I can now see that I was working between 2 different Contexts as I was building up the new Quote, and that's why I was having problems. You've helped me (begin) to get my brain around this strict Context boundary stuff.

Thank you very much.

West Wind Forever!!!!


Matt,

I think the issue is this: If you create a new entity and automatically add child objects in the constructor, they are then automatically marked as EntityMode.Added. If you then assign values to that object, that won't change the entity state, so they will always be assumed to be added even if you assign an ID explicitly.

The way that this can be fixed is to ensure you assign an entity retrieved from the DbContext in the first place, and not just assign the values to the new entity that was added implicitly.

Here's an example of an order placed with a previously created customer attached (which is similar to what you're doing):

[TestMethod]publicvoid AttachWithChildExistingTest() {var orderBo = new busOrder();var custBo = new busCustomer(orderBo); // share context// same as this to share context//custBo.Context = orderBo.Context;var order = orderBo.NewEntity(); order.OrderId = StringUtils.NewStringId();// this is easiest and most efficient//order.CustomerPk = 1;// manually load customer instance from contextvar cust = custBo.Load(1);//orderBo.Context.Customers.Find(1); cust.Updated = DateTime.Now; // make a change order.Customer = cust; // add a new line item order.LineItems.Add(new LineItem() { Description = "Cool new Item", Price = 40M, Sku="COOLNEW", Quantity = 1, Total = 40m }); Assert.IsTrue(orderBo.Save(), orderBo.ErrorMessage); }

Note that for the bus object to work the context needs to be shared. Typically operations like this would actually occur in the business object so the BO method would do the composition typically and you might not actually use two business objects to perform such an operation.

FWIW, I tend to add an overloaded constructor for sharing the context between bus objects:

public class busCustomer : EfCo

publicclass busCustomer : EfCodeFirstBusinessBase<Customer,WebStoreContext> {public busCustomer() { }public busCustomer(IBusinessObject<WebStoreContext> parentBusinessObject) : base(parentBusinessObject) { } }publicclass busOrder : EfCodeFirstBusinessBase<Order, WebStoreContext> {public busOrder() { }public busOrder(IBusinessObject<WebStoreContext> parentBusinessObject) : base(parentBusinessObject) { } }

There's no code here but unfortunately constructors of child objects don't automatically inherit their base constructors so these constructors have to be explicitly implemented.


BTW, for your models I highly recommend that you add an integer ID for foreign keys in addition to the entity itself. Yes that's not purely clean for OO, but it makes a lot of things vastly easier. Updates among them because instead of to explicitly having to load and assign object you can just update the ID. So if customer is not updated you can simplify the code to:

[TestMethod]publicvoid AttachWithChildExistingAndCustIdTest() {var orderBo = new busOrder();var order = orderBo.NewEntity(); order.OrderId = StringUtils.NewStringId();// this is easiest and most efficient order.CustomerPk = 1;// add a new line item order.LineItems.Add(new LineItem() { Description = "Cool new Item", Price = 40M, Sku = "COOLNEW", Quantity = 1, Total = 40m }); Assert.IsTrue(orderBo.Save(), orderBo.ErrorMessage); }


In short the important thing to remember for EF is that EF tracks state of an entity and unless you explicitly change that state either explicitly or by assigning a different entity that has its own state that state stays with the entity.

This stuff is confusing I agree, and I think it sucks that EF isn't smart enough to figure out that if you assign an object with an ID that exists that it should update the record regardless especially since EF knows what the key is in the model, but that's just how EF works. Everything is tied to the context.

It's not obvious but it's also not difficult to work around - ultimately I think the only time this behavior becomes 'weird' is in Web update scenarios when you're model binding. The trick is to always load up fully loaded entities when doing updates - never bind to empty ones unless you are indeed using a new record for everything.


It may seem that using the business object is more work, but there's good reason to use it IMHO: It abstracts the db logic considerably plus gives you many hook points to add additional functionality easily. So when you save you can fire OnBeforeSave() OnAfterSave() etc. or when you call Load() or NewEntity() you can automatically do some calculations on properties. All these are things that you can't do directly with the EF objects. Further the goal is to keep the EF specific code completely out of the front end application. Other than IQueryable() result filtering, the front end application should not ever see any EF specific code. This makes code somewhat more portable. In fact I recently ported a smallish time tracking application from Linq to SQL to EF and was able to easily reuse most of the front end code and even a good chunk of the business object code.


+++ Rick ---



Rick - I believe I have discovered the issue here...

The deal is that you have to make all your nested object assignments *BEFORE* the new entity is added to the Context, or else they will appear to be new also, even if they are existing and came from the same Context.

So, with NewEntity(), in EfCodeFirst base code you immediately call .Add() to add it to the Context, then I am mapping an existing Customer to it, but it treats the Customer as new too. So, even though I am plucking that existing Customer out of the same Context, then assign it to the new entity, it is still treated like a new Customer, apparently because Add() had already been called on the main object.

However, when I use the basic old " new Quote(); " to start out with a fresh plain object, and, before I add it to the Context, I first assign the existing related Customer object (which I pluck from the same Context that I will eventually add the new Quote object to), it works just fine. When I Add() that fully decorated complex object to the Context, it knows that the Quote is *new*, but the Customer *already exists*. For some reason, setting it all up *before* adding it to the Context allows it to work properly!!!

I discovered this as a guess, and I can make it fail with " new Quote();" also, if I add it to the Context first, then assign the Customer. I get the same extact results as I was getting with NewEntity().

A big lesson learned for me, and hopefuly for others too!

Perhaps NewEntity() should return an entity, but not attach it to the Context yet??? I dunno. Heck, it's not more code to say " new Quote();" than it is to say "quoteBO.NewEntity();" But everyone will have to know the pattern... That is, they cannot add existing related entities to NewEntity(), or they will get errors upon Save().



NewEntity() only does this:

It creates the object instance (new T()), and then adds the new entity to the object context and passes it back.

publicvirtual TEntity NewEntity() { Entity = new TEntity(); Entity = Context.Set<TEntity>().Add(Entity) as TEntity; OnNewEntity(Entity); if (Entity == null)returnnull;return Entity; }

In order for the entity to save it has to be added to the context so I'm not sure what you could possibly be doing different with just new Quote(); DbSet.Add(quote); unless you're not calling DbSet.Add() in which case the new entity won't get added on Save()/SubmitChanges().

If your constructor of the order creates a new Customer object then that customer is treated like a new customer when added to the context, because it's not a tracked entity. IOW, you have to be very careful when you create a new entity in the constructor of related objects. Generally this is a bad idea - only pre-create child collections (because they are empty by default).

But if this was the case you'd still have the same problem with plain entities without calling NewEntity(). Something else is different here.

+++ Rick ---


I'm clearing out the PK on the new Quote before calling Save().

That's not the issue...

The problem is the way the related *CUSTOMER* object is being handled. It's trying to create a new CUSTOMER record also, but the Customer is not new; only the Quote is new and it uses an existing Customer.

You see: QuoteBO.Entity.Customer is an object itslef, and it too gets mapped over from the existing Quote to the new Quote, then Save() is trying to create a new Customer record for it, which is not necessary.

As you can see in my code sample, if I start with "new Quote();" it works fine (this error doesn't happen), but if I start with ".NewEntity()", this error happens.

So, I certainly have a work-around, it's just that I'm wanting to understand what it is about starting with NewEntity() that is causing this issue.


Are you mapping the PK with AutoMapper? If so then yes that would fail.

I don't really understand why you're doing things quite this way. If you have a new Entity you need to create just create a new entity and map the values from the View or DTO or whatever you're mapping from. If you have an existing entity, load the original entity then map to the existing entity in the same way.

Whatever you do with a new entity, don't manually assign the PK to an existing PK or rightfully the insert will fail.

+++ Rick ---



Hi Rick - I'm trying to learn house to make use of NewEntity() in the EF Code First wrapper.... The case I am studying is a common task for may app, where I copy one Quote to a new Quote and edit it from there.

So, I'm using one BO to fetch the existing quote, then I create a new BO instance and use NewEntity(), then I map the existing Quote property values to new Quote entity, and call BO.Save().

However, it is giving an error because it is trying to create a new Customer record also, but it already exists in the table, so it should not be created again.

Here is the code:

public ActionResult test3() { var existingQuoteBO = new busQuote(9001); // Loads existing Quote# 9001 var newQuoteBO = new busQuote(); quote newQuote = newQuoteBO.NewEntity(); AutoMapper.Mapper.Map(existingQuoteBO.Entity, newQuote); // Copy existing values to newly created Quote newQuote.LineItems = new List<quoteitem>(); // Blank out the QuoteLineItems collection that was copied over newQuoteBO.Save(); <---See error message below.return RedirectToAction("edit", "quotes", new { id = newQuote.id }); }

Here is the error I am getting after calling Save():

Error: Violation of PRIMARY KEY constraint 'PK_customers_1'. Cannot insert duplicate key inobject'dbo.customers'. The statement has been terminated.} Westwind.BusinessFramework.EfCodeFirst.EfCodeFirstBusinessBase<MVC4_App1_EFData.Models.quote,MVC4_App1_EFData.QuoteContext> {MVC4_App1_EFData.busQuote}

Surely the EF thing is smart enough not to create a new Customer.???


So, if I skip the NewEntity() technique, and use this approach instead:

var newQuote = new quote();


Then copy the properties from the existing Quote to this new Quote (again using AutoMapper), then I call:

newQuoteBO.Context.Quotes.Add(newQuote); newQuoteBO.Save()

then it works just fine!!!


So, clearly there is something about using NewEntity() that I am missing.








Viewing all articles
Browse latest Browse all 10393

Trending Articles