I have a Business and a Category model.
Each Business has many Categories via an exposed collection (Category is disregarding the Business entity).
Now here is my controller-action:
[HttpPost]
[ValidateAntiForgeryToken]
private ActionResult Save(Business business)
{
//Context is a lazy-loaded property that returns a reference to the DbContext
//It's disposal is taken care of at the controller's Dispose override.
foreach (var category in business.Categories)
Context.Categories.Attach(category);
if (business.BusinessId > 0)
Context.Businesses.Attach(business);
else
Context.Businesses.Add(business);
Context.SaveChanges();
return RedirectToAction("Index");
}
Now there are several business.Categories that have their CategoryId set to an existing Category (the Title property of Category is missing tho).
After hitting SaveChanges and reloading the Business from server, the Categories are not there.
So my question is what's the proper way to set Business.Categories with a given array of existing CategoryIds.
When create a new Business however, the following DbUpdateException exception is thrown when calling SaveChanges:
An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
Inner exception (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.
Update
after answer, here's the update code:
var storeBusiness = IncludeChildren().SingleOrDefault(b => b.BusinessId == business.BusinessId);
var entry = Context.Entry(storeBusiness);
entry.CurrentValues.SetValues(business);
//storeBusiness.Categories.Clear();
foreach (var category in business.Categories)
{
Context.Categories.Attach(category);
storeBusiness.Categories.Add(category);
}
When calling SaveChanges, I'm getting the following DbUpdateException:
An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
Here's how the Business/Category models look like:
public class Business
{
public int BusinessId { get; set; }
[Required]
[StringLength(64)]
[Display(Name = "Company name")]
public string CompanyName { get; set; }
public virtual BusinessType BusinessType { get; set; }
private ICollection<Category> _Categories;
public virtual ICollection<Category> Categories
{
get
{
return _Categories ?? (_Categories = new HashSet<Category>());
}
set
{
_Categories = value;
}
}
private ICollection<Branch> _Branches;
public virtual ICollection<Branch> Branches
{
get
{
return _Branches ?? (_Branches = new HashSet<Branch>());
}
set
{
_Branches = value;
}
}
}
public class Category
{
[Key]
public int CategoryId { get; set; }
[Unique]
[Required]
[MaxLength(32)]
public string Title { get; set; }
public string Description { get; set; }
public int? ParentCategoryId { get; set; }
[Display(Name = "Parent category")]
[ForeignKey("ParentCategoryId")]
public virtual Category Parent { get; set; }
private ICollection<Category> _Children;
public virtual ICollection<Category> Children
{
get
{
return _Children ?? (_Children = new HashSet<Category>());
}
set
{
_Children = value;
}
}
}
Just to make it clear again, the Category I'm attaching to existing/new Businesses already exist in the DB and have an ID, which is what I'm using to attach it with.
I treat the two cases - updating an existing business and adding a new business - separately because the two problems you mentioned have different reasons.
Updating an existing Business entity
That's the if case (if (business.BusinessId > 0)) in your example. It is clear that nothing happens here and no change will be stored to the database because you are just attaching the Category objects and the Business entity and then call SaveChanges. Attaching means that the entities are added to the context in state Unchanged and for entities that are in that state EF won't send any command to the database at all.
If you want to update a detached object graph - Business plus collection of Category entities in your case - you generally have the problem that a collection item could have been removed from the collection and an item could have been added - compared to the current state stored in the database. It might be also possible that a collection item's properties and that the parent entity Business have been modified. Unless you have tracked all changes manually while the object graph was detached - i.e. EF itself could not track the changes - which is difficult in a web application because you had to do this in the browser UI, your only chance to perform a correct UPDATE of the whole object graph is comparing it with the current state in the database and then put the objects into the correct state Added, Deleted and Modified (and perhaps Unchanged for some of them).
So, the procedure is to load the Business including its current Categories from the database and then merge the changes of the detached graph into the loaded (=attached) graph. It could look like this:
private ActionResult Save(Business business)
{
if (business.BusinessId > 0) // = business exists
{
var businessInDb = Context.Businesses
.Include(b => b.Categories)
.Single(b => b.BusinessId == business.BusinessId);
// Update parent properties (only the scalar properties)
Context.Entry(businessInDb).CurrentValues.SetValues(business);
// Delete relationship to category if the relationship exists in the DB
// but has been removed in the UI
foreach (var categoryInDb in businessInDb.Categories.ToList())
{
if (!business.Categories.Any(c =>
c.CategoryId == categoryInDb.CategoryId))
businessInDb.Categories.Remove(categoryInDb);
}
// Add relationship to category if the relationship doesn't exist
// in the DB but has been added in the UI
foreach (var category in business.Categories)
{
var categoryInDb = businessInDb.Categories.SingleOrDefault(c =>
c.CategoryId == category.CategoryId)
if (categoryInDb == null)
{
Context.Categories.Attach(category);
businessInDb.Categories.Add(category);
}
// no else case here because I assume that categories couldn't have
// have been modified in the UI, otherwise the else case would be:
// else
// Context.Entry(categoryInDb).CurrentValues.SetValues(category);
}
}
else
{
// see below
}
Context.SaveChanges();
return RedirectToAction("Index");
}
Adding a new Business entity
Your procedure to add a new Business together with its related Categories is correct. Just attach all Categories as existing entities to the context and then add the new Business to the context:
foreach (var category in business.Categories)
Context.Categories.Attach(category);
Context.Businesses.Add(business);
Context.SaveChanges();
If all Categories you are attaching really have a key value that exists in the database this should work without exception.
Your exception means that at least one of the Categories has an invalid key value (i.e. it does not exist in the database). Maybe it has been deleted in the meantime from the DB or because it is not correctly posted back from the Web UI.
In case of an independent association - that is an association without FK property BusinessId in Category - you get indeed this OptimisticConcurrencyException. (EF seems to assume here that the category has been deleted from the DB by another user.) In case of a foreign key association - that is an association which has a FK property BusinessId in Category - you would get an exception about a foreign key constraint violation.
If you want to avoid this exception - and if it occurs in fact because another user deleted a Category, not because the Category is empty/0 since it doesn't get posted back to the server (fix this with a hidden input field instead) - you better load the categories by CategoryId (Find) from the database instead of attaching them and if one doesn't exist anymore ignore it and remove it from the business.Categories collection (or redirect to an error page to inform the user or something like that).
I had this exception as well.
The problem with me was that the primary key of the added object was not being set by ADO Entity Framework. This resulted in the problem that the foreign key of the added object in the database could not be set as well.
I solved this problem by making sure the primary key was being set by the database itself.
If you're using SQL Server you can do that by adding the IDENTITY keyword with the column declaration in the CREATE TABLE statement.
Hope this helps
I was getting this exception on a field I was trying to set beyond its MaxLength attribute. The field also had a Required attribute. I'm working against an Oracle back-end database. It wasn't until I increased the length did I finally get a descriptive error message from the underlying database engine that the length was too long. I suspect the foreign key related error message vaguely means that not all relationships could be updated because one of the inserts failed and didn't get their Key updated.
Related
I 'm using EF Core 3.1.10. I have the following entities:
public class Request {
public int Id { get; set; }
public string Title { get; set; }
public string ProjectId { get; set; }
public List<RequestAttachment> Attachments { get; set; } = new List<RequestAttachment> ();
}
public class RequestAttachment {
public int Id { get; set; }
public int RequestId { get; set; }
public Request Request { get; set; }
public byte[] FileStream { get; set; }
public string Filename { get; set; }
public RequestAttachmentType RequestAttachmentType { get; set; }
public int RequestAttachmentTypeId { get; set; }
}
public class RequestAttachmentType {
public int Id { get; set; }
public string Name { get; set; }
}
In my repository, I have a simple Update method:
public async Task UpdateRequest (Request aRequest) {
// I'm attaching aRequest.Attachments because they already exist in the database and I don 't want to update them here
// Option 1 Not working
// aRequest.Attachments.ForEach (a => theContext.RequestAttachments.Attach (a));
// Option 2 Not working
// theContext.RequestAttachments.AttachRange (aRequest.Attachments);
// Option 3 Working
aRequest.Attachments.ForEach (a => theContext.Entry (a).State = EntityState.Unchanged);
theContext.Requests.Update(aRequest);
await theContext.SaveChangesAsync ();
}
Note that I'm attaching "aRequest.Attachments" because I don 't want to update Attachments. I only want to update aRequest. "aRequest.Attachments" already exist in the database that's why I 'm using Attach so they don't get re-added. But Attach and AttachRange do not work when a request has more than one attachment. It throws the following error:
The instance of entity type 'RequestAttachmentType' cannot be tracked
because another instance with the key value '{Id: 1}' is already being
tracked. When attaching existing entities, ensure that only one entity
instance with a given key value is attached.
I don 't understand this error because I did not explicitly attach "RequestAttachmentType". The only thing I did was attaching its parent "aRequest.Attachments".
When I set the state manually like I did in Option 3, no error was thrown. I thought Attach is equivalent to theContext.Entry (a).State = EntityState.Unchanged. Why option 3 works but option 1 and 2 do not?
Working with detached entity graphs is going to continue to cause all kinds of headaches like this. Not only do you need to handle the scenario that you don't want to update/duplicate related entities, but you have to also handle cases where the DbContext is already tracking the entity you want to update. Sergey was on the right track there.
The problem is that you have a complete graph:
Request
Atachment
AttachmentType
Attachment
AttachmentType
where you want to update details in Request and the Attachments...
One issue with "Update" is that it will dive the graph to look for entities that might need to be added/updated. On its own with a detached graph this will usually result in duplicate items being created. Hence "attaching" them first. The trouble here is where the DbContext is already tracking one or more entities in the graph. One key detail to remember about EF is that References are everything. Deserializing entity graphs is a painful exercise.
For example lets say we deserialize a Request Id 1, with 2 attachments, #1, and #2, where both have an AttachmentType of "Document" (AttachmentType ID = 14)
What you will end up is something that looks like:
Document
{
ID:1
...
Attachments
{
Attachment
{
ID:1
...
AttachmentType
{
ID: 14
}
}
Attachment
{
ID:2
...
AttachmentType
{
ID: 14
}
}
}
}
Without considering what the DbContext may or may not already be tracking prior to looking at these entities, there is already a problem. Attachment ID 1 and 2 are distinct objects, however they both reference an AttachmentType ID 14. When de-serialized, these will be 2 completely distinct references to objects that have an ID of 14.
A common surprise is where test code appears to work fine because the two attachments had different attachment types, but then fails unexpectedly when they happen to have the same type. The first attachment would have the DbContext tracking the first attachment's "Type". If the second attachment's Type was a different ID, then attaching that 2nd type would succeed so long as the Context wasn't tracking it. However, when set to the same ID the "already tracking entity with the same ID" pops up.
When dealing with disconnected entities you need to be very deliberate about references and explicitly handle whenever the DbContext is tracking a reference. This means consulting the DbSet Local caches:
public async Task UpdateRequest (Request aRequest)
{
var existingRequest = theContext.Requests.Local.SingleOrDefault(x => x.Id = aRequest.Id);
if (existingRequest != null)
{
// copy values from aRequest -> existingRequest or Leverage something like automapper.Map(aRequest, existingRequest)
}
else
{
theContext.Requests.Attach(aRequest);
theContext.Entity(aRequest).State = EntityState.Modified; // Danger Will Robinson, make 100% sure your entity from client is validated!! This overwrites everything.
}
foreach(var attachment in aRequest)
{
var existingAttachment = theContext.Attachments.Local.SingleOrDefault(x => x.Id == attachment.Id);
// Look for a reference to the attachment type. If found, use it, if not attach and use that...
var existingAttachmentType = theContext.AttachmentTypes.Local.SingleOrDefault(x => x.Id == attachment.AttachmentType.Id);
if (existingAttachmentType == null)
{
theContext.AttachmentTypes.Attach(attachment.AttachmentType);
existingAttachmentType = attachment.AttachmentType;
}
if(existingAttachment != null)
{
// copy values across.
AttachmentType = existingAttachmentType; // in case we change the attachment type for this attachment.
}
else
{
theContext.Attachments.Attach(attachment);
theContext.Entity(attachment).State = EntityState.Modified;
attachment.AttachmentType = existingAttachmentType;
}
}
await theContext.SaveChangesAsync ();
}
Needless to say this is a lot of messing around to check and replace references to either get the DbContext to track detached entities or replace the references with tracked entities.
A simpler option is to leverage Automapper to establish a configuration for what fields can be updated from a source (ideally a ViewModel, but you can use an entity graph as a source) to a destination. (Entities tracked by the DbContext)
Step 1: Configure Automapper with the rules about what to update for a Request -> Attachments graph.. (Not shown)
Step 2: Load tracked entity graph, and the applicable AttachmentTypes:
var existingRequest = theContext.Requests
.Include(x => x.Attachments)
.ThenInclude(x => x.AttachmentType)
.Single(x => x.Id == aRequest.Id);
var referencedAttachmentTypeIds = aRequest.Attachments.Select(x => x.AttachmentTypeId)
.Distinct().ToList();
var referencedAttachmentTypes = theContext.AttachmentTypes
.Where(x => referencedAttachmentTypeIds.Contains(x.Id))
.ToList();
Getting the list of attachment types only applies if we can change an attachment's type, or are adding attachments.
Step 3: Leverage Automapper to copy across values
mapper.Map(aRequest, existingRequest);
If Attachments can be updated, added, and/or removed you will need to handle those scenarios against the existingRequest. Here we reference the loaded set of AttachmentTypes.
Step 4: Save Changes.
The primary benefits of this approach is that you do away with the constant checking for existing references and the consequences of missing a check. You also configure the rules about what values can legally be overwritten when calling the Automapper Map call so only values you expect are copied from the source to the existing data record. This also results in faster Update queries as EF will only build statements for the values that actually changed, where using Update or EntityState.Modified result in SQL UPDATE statements that update every column.
Try this:
var itemExist = await theContext.Requests.FirstOrDefaultAsync ( i=>i.Id == aRequest.Id);
if (itemExist !=null)
{
var attachments=aRequest.Attachments;
aRequest.Attachments=null;
theContext.Entry(itemExist ).CurrentValues.SetValues(aRequest);
await theContext.SaveChangesAsync();
aRequest.Attachments=attachments;
}
I have multiple models and all of these models are in a view model. One of my action methods in controller is used to add data to the database.
I am using Entity Framework with code-first migrations
Primary keys of these tables are using Entity Framework ID's so these are automatically generated.
All these models are interdependent on each other with foreign keys. I am trying to insert the data into these model database tables at same time.
How I can grab the primary key of one table and insert as foreign key in different table?
Booking, Messages, and Items are different model classes
Booking model class
public class Booking
{
[Key]
[Column("lngBookingID")]
public Int32 BookingID { get; set; }
public double BookingCost { get; set; }
}
Messages model class:
public class Messages
{
[Key]
[Column("lngMessageID")]
public Int32 MessageID { get; set; }
public string MessageSubject { get; set; }
[ForeignKey("Booking")]
public Int32 lngBookingID { get; set; }
public Booking Booking { get; set; }
}
Code for action method. BookingViewModel bvm has all the data needed:
_context.Booking.Add(bvm.Booking);
_context.Messages.Add(bvm.Messages);
_context.PetInformation.Add(bvm.items);
_context.SaveChanges();
I would like generated Booking ID to be foreign key in messages table when I add it to database.
You need to ensure that the Booking reference in the Message points to the same instance as the Booking you are adding. EF will manage the FK association from there.
For instance:
bvm.Messages.Booking = bvm.Booking; // Associate the same reference.
_context.Booking.Add(bvm.Booking);
_context.Messages.Add(bvm.Messages);
_context.SaveChanges();
I would avoid passing Entity classes in a view model since this can lead to all kinds of problems as you may think that you are passing an entity, but you are really just passing a POCO instance of data that is not associated to the context. EF will treat each and every de-serialized instance as a new reference, even if they have matching IDs. You have to associate them with the DbContext, and update references to instances that might already be associated. It's ugly, error prone, and also prone to unauthorized data manipulation by malicious users.
For example: Take an scenario that takes the following data:
Booking { Id = 0 }, Message { Id = 0, Booking { Id = 0 }}
So it accepts a new Booking with ID 0, and a new message referencing our new booking.
When we call context.Booking.Add(booking) EF will create a new booking with an auto-generated ID. Lets say, "15". When it get's to the message, the message contains a new, different booking reference, so EF goes and inserts that, and it gets Id 16. (Even though the rest of the data about the booking is identical to the first one passed in) Because the two booking references don't point to the same instance, they are treated as two completely separate instances by EF as well. By setting the message's booking reference to the first one in our method, when EF updates the Booking, the message's booking reference will point to that first one's ID.
To avoid all kinds of ugliness like this, we want to avoid the duplicate references. If we have a method that inserts a new booking with an optional message, then rather than passing Entities which pose problems like this, pass regular C# view models and then handle the entity creation (or load) on the server. Entities passed into methods like this are effectively just POCO classes, but they hold a lot more data than you should not "trust" to be updated to the database. By passing a separate view model we enforce a better practice of:
Loading existing entity references from the current DBContext, and/or creating and associating entities based on the data provided.
So a method that creates a new booking with an optional message might look more like:
public ActionResult AddBooking(BookingViewModel booking, MessageViewModel message)
{
var newBooking = new Booking { BookedDate = booking.BookedDate, /* ... */ };
_context.Bookings.Add(newBooking);
if (message != null)
{
var newMessage = new Message { MessageText = message.Text, Booking = newBooking };
_context.Messages.Add(newMessage);
}
_context.SaveChanges();
}
Even better would be for the booking entity to have a collection of messages. I suspect you probably tried this initially but ran into serialization issues when trying to pass bookings to the client. (Another reason not to pass entities)
public ActionResult AddBooking(BookingViewModel booking, MessageViewModel message)
{
var newBooking = new Booking { BookedDate = booking.BookedDate, /* ... */ };
if (message != null)
{
var newMessage = new Message { MessageText = message.Text, Booking = newBooking };
newBooking.Messages.Add(newMessage);
}
_context.Bookings.Add(newBooking);
_context.SaveChanges();
}
This leverages the relationships between the entities so that the context doesn't have to treat every entity as a top level entity. When the booking is saved, it's messages will be saved as well and the FK references will be filled in automatically.
As you get into edit scenarios and such, continuing to pass entity references will lead to more pain with duplicate data, and context errors about entities with matching IDs already being tracked. It isn't worth the mess. :)
I didn't find any relevant answer here so I will trigger you, thanks in advance :
I have a controller with 2 methods of the Edit action, (I simplified it for better understanding):
MrSaleBeta01.Controllers
{
public class PostsController : Controller
{
private MrSaleDB db = new MrSaleDB();
...
// GET: Posts/Edit/5
public ActionResult Edit(int? id)
{
...
}
// POST: Posts/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit( Post post, int? CategoryIdLevel1, int? CategoryIdLevel2, int? originalCategoryId)
{
...
Category cnew = db.Categories.Find(post.CategoryId);
MoveFromCategory(post, originalCategoryId);
...
db.Entry(post).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
//move post from his old category (fromCategoryId) to a new one (post.CategoryId):
//returns true on success, false on failure.
public bool MoveFromCategory(Post post, int? fromCategoryId)
{
try
{
if (post.CategoryId == fromCategoryId)
return true;
Category cold = null, cnew = null;
if (fromCategoryId!=null)
cold = db.Categories.Find(fromCategoryId);
if (post.CategoryId != 0)
cnew = db.Categories.Find(post.CategoryId);
if (cold != null)
{
cold.Posts.Remove(post);
}
if( cnew != null)
cnew.Posts.Add(post);
db.Entry(cold).State = EntityState.Modified;
db.Entry(cnew).State = EntityState.Modified;
//db.Entry(p).State = EntityState.Modified;
//db.SaveChanges();
return true;
}
catch (Exception)
{
return false;
//throw;
}
}
}
}
So, the idea is very default: The first method is called by Get and returns the View of Edit. Then I need to save the changes by sending the post object from the view to the HttpPost Edit method.
My Model is something like that (I simplified it for better understanding):
MrSaleBeta01.Models
{
public class Post
{
public int Id { get; set; }
[ForeignKey("Category")]
public virtual int CategoryId { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
public Category()
{
this.Categories = new List<Category>();
this.Posts = new List<Post>();
}
#region Primitive Properties
public int CategoryId { get; set; }
public string Name { get; set; }
#endregion
#region Navigation Properties
public virtual IList<Post> Posts { get; set; }
#endregion
}
}
The idea: Every Post needs to have it's Category. Every Category can have multiple Posts or none. (1-N relationship).
The problem:
In the Edit (HttpPost) method, after I update the Category's objects (move the Post from it's category to a different category object. After that I do some other modifications on post object), I get an error in the line of the edit method:
db.Entry(post).State = EntityState.Modified;
saying that:
{"Attaching an entity of type 'MrSaleBeta01.Models.Post' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate."}
The error is beacuse there is a conflict to the line:
cold.Posts.Remove(post);
And even to the line:
cnew.Posts.Add(post);
I tried to use the solution of AsNoTracking() but without success,
I also tried to change the line "db.Entry(post).State = EntityState.Modified" line to:
db.As.Attach(post)
but that line is even cannot be compiled.
What am I doing wrong? How can I solve that issue?
1) You dont have to call .Attach() nor .State = anything.
You have your Entity created as proxy object (cold = db.Categories.Find(fromCategoryId);), its proxy responsibility to track any changes. As exception say, this COULD be your problem.
2) public int CategoryId { get; set; } should be marked with [Key] (i am not sure if convention mark it as primary key, but i doubt it - i think EF conventions take this PK as FK to Category, which could confuse object graph and behave strangely...)
3) Uh, just noticed... Why are you using your FromCategory method at all? I may overlook something, but looks like it just remove Category from collection and add it to another... EF proxy does this automatically for you, right after post.CategoryId = newCatId;
Edit1:
4) Change public virtual IList<Post> Posts { get; set; } to public virtual ICollection<Post> Posts { get; set; }
Edit2:
1) that was created automatically while I scaffold the PostsController according to the Post model. So I guess I need it?
3) It's not just remove Category from collection and add it to another, but remove the post from the collection of posts in one category to another. So I don't think that EF proxy does this automatically.
I am not famillier with ASP, i work with desktop MVP/MVVM, so i am not sure here - but from my point of view, you really dont need to touch EntityState as long as you are using var x = db.Set<X>().Create(); (== db.X.Create();) (NOT var x = new X();) for new entities and db.Set<X>().FetchMeWhatever(); (== db.X.FetchMeWhatever();) for everything else (Otherwise you get only POCO without proxy. From your example, it looks like you are doing it right ;) ).
Then you have entity with proxy (thats why you have your reference properties on model virtual - this new emitted proxy type override them) and this proxy will take care for 1:n, m:n, 1:1 relations for you. I think this is why folks are using mappers (not only EF and not only DB mappers) mainly :) For me it looks like, you are trying to do this manually and it is unnecessary and its just making a mess.
Proxy also take care of change tracking (so as i say, you dont need to set EntityState manually, only in extreme cases - I can not think of any right now... Even with concurrency.)
So my advice is:
Use only ICollection<> for referencing collections
Check and get rid of any var entity = new Entity(); (as i say, looks like you are doing this)
Throw away every db.Entry(x).State = EntityState.whatever; (trust EF and his change tracker)
Set only one side of reference - it doesnt matter if Category.Posts or Post.Category or even Post.CategoryId - and let mapper do the work. Please note that this will work only with proxy types (as i say above) on entities with virtual referencing & id & ICollection<> properties.
Btw, there are 2 types of change tracking, snippet and proxy - snippet have original values in RAM and is comparing them at SaveChanges() time, for proxy tracking, you need to have all your properties marked virtual - and comparing them at x.Prop = "x" time. But thats off-topic ;)
I can not find the cause why I am failing to insert object graph
I have an object graph - Promotion with 1..N Flow records.
When I create a new Promotion record, I need to create a related flow record as well.
This is how I try to do it
var newpromo = new Promotion();
var newflow = new Flow();
newpromo.Flow.Add(newflow);
//i thought this should enough to tell EF that newflow's PromotionId
//should be the newly inserted nepromo's Id
newflow.Promotion = newpromo;
//...
db.Promotions.Attach(newpromo);
db.Entry(newflow).State = System.Data.Entity.EntityState.Added;
but when i call db.SaveChanges() I receive this error
{"The INSERT statement conflicted with the FOREIGN KEY constraint
\"FK_PromotionFlow_Promotions\". The conflict occurred in database \"RepositoryDb\",
table\"dbo.Promotions\", column 'Id'.\r\nThe statement has been terminated."}
What could be the cause of the problem?
DEFINITIONS:
1) This is Promotion and Flow POCO class definitions
class Promotion
{
public Int32 Id { get; set; }
public List<PromotionFlow> Flow { get; set; }
//...other fields
}
class PromotionFlow
{
public Int32 Id { get; set; }
public Int32 PromotionId { get; set; }
public Promotion Promotion { get; set; }
//other fields
}
2) I have set the mapping using fluent api
class PromotionMapping : EntityTypeConfiguration<Promotion>
{
public PromotionMapping()
{
//Mapping both tables
HasMany(x => x.Flow).WithRequired(x=>x.Promotion).HasForeignKey(x => x.PromotionId);
//...other mapping
}
}
3) In the database I have configured PromotionFlow.PromotionId column as a foreign key to Promotion.Id column
CONSTRAINT [FK_PromotionFlow_Promotions] FOREIGN KEY ([PromotionId]) REFERENCES [dbo].[Promotions] ([Id])
Try saving the promo first, then adding the flow to the promo and saving the flow (or the promo).
It sounds like your promo doesn't have an ID to use for the flow because the promo has not yet been inserted into the DB.
I found the cause of the problem... I needed to set object graph parent's (that is Promotion object) entry's state to "Added" (attaching to context was not enough since this was a NEW object)
//(...)
db.Promotions.Attach(newpromo);
I was missing THIS row after attaching new Promotion object to context!
db.Entry(newpromotion).State = System.Data.Entity.EntityState.Added; //<==THIS ROW
//... dealing with the child (flow) entry states
db.SaveChanges();
P.S. the reason I was not using DbContext.DbSet.Add() method was because it sets entity state to Added for all of the objects in the graph (and some of my objects were referencing "Settings" and "Category" type data, that I was not intended to insert once more)
db.Promotions.Add(newpromo);
P.S. Now it is inserting all of the graph in one db.SaveChanges() call, no need to insert Promotion before inserting Flow objects
I have a problem with my code where I try to save a many to many connection between two objects, but for some reason it doesn't get saved.
We used the code first method to create our database, in our database we have the following entities where this problem is about:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ProductTag> ProductTags { get; set; }
}
public class ProductTag
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
The table ProductTagProducts got automatically created, which is of course just a connection table between the two.
Now creating products works fine. We can just run the following and it will create the connnections in the ProductTagProducts table:
Product.ProductTags.Add(productTag);
To make sure no duplicate tasks are in the database, we handle the saving for it ourselves. The productTag always contains a product tag with an existing ID.
The problem occurs when we want to edit the same or another product. There are existing tags for the product. And we use the following process to save it:
List<ProductTag> productTags = new List<ProductTag>();
string[] splittedTags = productLanguagePost.TagList.Split(',');
foreach (string tag in splittedTags) {
ProductTag productTag = new ProductTag();
productTag.Name = tag;
productTags.Add(productTagRepository.InsertAndOrUse(productTag));
}
We split the tags by comma, that's how it is received from the HTML element. Then we define a new entity for it and use InsertAndOrUse to determine if the tag already existed. If the tag already existed, it returns the same entity but with the ID filled in, if it did not exist yet it adds the tag to the database, and then also returns the entity with ID. We create a new list to be sure that the product doesn't have duplicate Id's in there (I have tried it with adding it to the product's existing tag list directly, same result).
product.ProductTags = productTags;
productRepository.InsertOrUpdate(product);
productRepository.Save();
Then we set the list to ProductTags and let the repository handle the insert or update, of course, an update will be done. Just in case, this is the InsertOrUpdate function:
public void InsertOrUpdate(Product product) {
if (product.Id == default(int)) {
context.Products.Add(product);
} else {
context.Entry(product).State = EntityState.Modified;
}
}
The save method just calls the context's SaveChanges method. When I edit the product, and add another tag it doesn't save the new tag. However, when I set a breakpoint on the save function I can see that they are both there:
And when I open the newly added tag 'Oeh-la-la' I can even refer back to the product through it:
But when the save happens, which succeeds with all other values, there are no connections made in the ProductTagProducts table. Maybe it is something really simple, but I am clueless at the moment. I really hope that someone else can give a bright look.
Thanks in advance.
Edit: As requested the ProductTag's InsertAndOrUse method. The InsertOrUpdate method it calls is exactly the same as above.
public ProductTag InsertAndOrUse(ProductTag productTag)
{
ProductTag resultingdProductTag = context.ProductTags.FirstOrDefault(t => t.Name.ToLower() == productTag.Name.ToLower());
if (resultingdProductTag != null)
{
return resultingdProductTag;
}
else
{
this.InsertOrUpdate(productTag);
this.Save();
return productTag;
}
}
You have to know that this line...
context.Entry(product).State = EntityState.Modified;
...has no effect on the state of a relationship. It just marks the entity product being passed into Entry as Modified, i.e. the scalar property Product.Name is marked as modified and nothing else. The SQL UPDATE statement that is sent to the database just updates the Name property. It doesn't write anything into the many-to-many link table.
The only situation where you can change relationships with that line are foreign key associations, i.e. associations that have a foreign key exposed as property in the model.
Now, many-to-many relationships are never foreign key associations because you cannot expose a foreign key in your model since the foreign keys are in the link table that doesn't have a corresponding entity in your model. Many-to-many relationships are always independent associations.
Aside from direct manipulations of relationship state entries (which is rather advanced and requires to go down to the ObjectContext) independent associations can only be added or deleted using Entity Framework's change tracking. Moreover you have to take into account that a tag could have been removed by the user which requires that a relationship entry in the link table must be deleted. To track such a change you must load all existing related tags for the given product from the database first.
To put all this together you will have to change the InsertOrUpdate method (or introduce a new specialized method):
public void InsertOrUpdate(Product product) {
if (product.Id == default(int)) {
context.Products.Add(product);
} else {
var productInDb = context.Products.Include(p => p.ProductTags)
.SingleOrDefault(p => p.Id == product.Id);
if (productInDb != null) {
// To take changes of scalar properties like Name into account...
context.Entry(productInDb).CurrentValues.SetValues(product);
// Delete relationship
foreach (var tagInDb in productInDb.ProductTags.ToList())
if (!product.ProductTags.Any(t => t.Id == tagInDb.Id))
productInDb.ProductTags.Remove(tagInDb);
// Add relationship
foreach (var tag in product.ProductTags)
if (!productInDb.ProductTags.Any(t => t.Id == tag.Id)) {
var tagInDb = context.ProductTags.Find(tag.Id);
if (tagInDb != null)
productInDb.ProductTags.Add(tagInDb);
}
}
}
I was using Find in the code above because I am not sure from your code snippets (the exact code of InsertAndOrUse is missing) if the tags in the product.ProductTags collection are attached to the context instance or not. By using Find it should work no matter if the they are attached or not, potentially at the expense of a database roundtrip to load a tag.
If all tags in product.ProductTags are attached you can replace ...
var tagInDb = context.ProductTags.Find(tag.Id);
if (tagInDb != null)
productInDb.ProductTags.Add(tagInDb);
... just by
productInDb.ProductTags.Add(tag);
Or if it's not guaranteed that they are all attached and you want to avoid the roundtrip to the database (because you know for sure that the tags at least exist in the database, if attached or not) you can replace the code with:
var tagInDb = context.ProductTags.Local
.SingleOrDefault(t => t.Id == tag.Id);
if (tagInDb == null) {
tagInDb = tag;
context.ProductTags.Attach(tagInDb);
}
productInDb.ProductTags.Add(tagInDb);