EF Core Attach/AttachRange methods not working - c#

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;
}

Related

Entity Framework 5 adding existing entity to nested collection

I've been trying to take advantage of a new way of creating many-to-many relationships - nice article about EF 5 many-to-many relationships.
The article states that you no longer need to define relation class and the framework does the job for you.
However, for a couple of hours now I've been struggling to add an existing entity to the collection of another entity.
My models
public record Bottle
{
[Key]
public int Id { get; set; }
[Required]
public string Username { get; set; }
// some other properties
public Collection<User> Owners { get; set; }
}
public record User
{
[Key]
public int Id { get; set; }
// some other properties
public Collection<Bottle> Bottles { get; set; }
}
Say that I want to add a new bottle to the database. I also know owners of that bottle. I had thought that this bit of code could work:
public async Task<int> AddBottle(BottleForAddition bottle)
{
var bottleEntity = mapper.Map<Bottle>(bottle);
bottleEntity.Owners = bottle
.OwnerIds // List<int>
.Select(id => new User { Id = id })
.ToCollection(); // my extension method
var createdEntity = await context.AddEntityAsync(bottleEntity);
await context.SaveChangesAsync();
return createdEntity.Entity.Id;
}
but sadly it does not work (BottleForAddition is DTO with almost the same properties).
I get this error:
Unable to create bottle (error: Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 19: 'NOT NULL constraint failed: Users.Username'.
at Microsoft.Data.Sqlite.SqliteException.ThrowExceptionForRC(Int32 rc, sqlite3 db)
at Microsoft.Data.Sqlite.SqliteDataReader.NextResult()
at ...
So I came up with this
public async Task<int> AddBottle(BottleForAddition bottle)
{
var bottleEntity = mapper.Map<Bottle>(bottle);
bottleEntity.Owners = (await context.Users
.Where(u => bottle.OwnerIds.Contains(u.Id))
.ToListAsync())
.ToCollection();
var createdEntity = await context.AddEntityAsync(bottleEntity);
await context.SaveChangesAsync();
return createdEntity.Entity.Id;
}
That works but I have to fetch Users from the database.
Do you know about a better way how to deal with it?
The Users table in the database has a Username field does not allow NULL
You are creating new User entities from the OwnerIds which doesn't have Username value set
EF is trying to insert a new user to the Users table
Combining the pieces of information above, you'll get a clear picture why the error message says -
SQLite Error 19: 'NOT NULL constraint failed: Users.Username'.
Then comes the real question, why EF is trying to insert new users at all. Obviously, you created the User entities from the OwnerIds to add already existing users to the list, not to insert them.
Well, I'm assuming that the AddEntityAsync() method you are using (I'm not familiar with it) is an extension method, and inside it, you are using the DbContext.Add() or DbSet<TEntity>.Add() method. Even if that is no the case, apparently AddEntityAsync() at least works similarly as them.
The Add() method causes the entity in question (Bottle) and all it's related entities (Users) present in the entity-graph to be marked as Added. An entity marked as Added implies - This is a new entity and it will get inserted on the next SaveChanges call. Therefore, with your first approach, EF tried to insert the User entities you created. See details - DbSet<TEntity>.Add()
In your second approach, you fetched the existing User entities first. When you fetch existing entities using the DbContext, EF marks them as Unchanged. An entity marked as Unchanged implies - This entity already exists in the database and it might get updated on the next SaveChanges call. Therefore, in this case the Add method caused only the Bottle entity to be marked as Added and EF didn't try to re-insert any User entities you fetched.
As a general solution, in a disconnected scenario, when creating new entity with an entity-graph (with one or more related entities) use the Attach method instead. The Attach method causes any entity to be marked as Added only if it doesn't have the primary-key value set. Otherwise, the entity is marked as Unchanged. See details - DbSet<TEntity>.Attach()
Following is an example -
var bottleEntity = mapper.Map<Bottle>(bottle);
bottleEntity.Owners = bottle
.OwnerIds // List<int>
.Select(id => new User { Id = id })
.ToCollection(); // my extension method
await context.Bottles.Attach(bottleEntity);
await context.SaveChangesAsync();
Not related to the issue :
Also, since you are already using AutoMapper, if you define your BottleForAddition DTO as something like -
public class BottleForAddition
{
public int Id { get; set; }
public string Username { get; set; }
// some other properties
public Collection<int> Owners { get; set; } // the list of owner Id
}
then you will be able to configure/define your maps like -
this.CreateMap<BottleForAddition, Bottle>();
this.CreateMap<int, User>()
.ForMember(d => d.Id, opt => opt.MapFrom(s => s));
which could simplify the operation code like -
var bottleEntity = mapper.Map<Bottle>(bottle);
await context.Bottles.Attach(bottleEntity);
await context.SaveChangesAsync();
Fetching the Users is generally the correct course of action. This allows you to make the associations but also helps validate that the reference IDs passed from the client are valid. Fetching entities by ID is generally quite fast, so I'd consider avoiding async/await for this operation. async is suited for large or high-frequency operations where server responsiveness could be "hung up". Using it everywhere just leads to slower operations overall.
EF will want to use proxies for navigation properties both for lazy loading (not to be relied on as a crutch, but useful to avoid errors as a worst-case) as well as for change tracking.
public record Bottle
{
[Key]
public int Id { get; set; }
[Required]
public string Username { get; set; }
// some other properties
public virtual ICollection<User> Owners { get; set; } = new List<User>();
}
then in the applicable code...
var bottleEntity = mapper.Map<Bottle>(bottle);
var users = context.Users
.Where(u => bottle.OwnerIds.Contains(u.Id))
.ToList();
foreach(var user in users)
bottleEntity.Users.Add(user);
// Or since dealing with a new Entity could do this...
//((List<User>)bottleEntity.Users).AddRange(users);
await context.SaveChangesAsync();
return bottleEntity.Id;
It might be tempting to just create the users and attach them to the DbContext and much of the time this would work, except if there is ever the possibility that the DbContext might have been tracking an instance of any of those to-be-attached users, which will result in a runtime error that an entity with the same ID is already being tracked.
var bottleEntity = mapper.Map<Bottle>(bottle);
var proxyUsers = bottle.OwnerIds
.Select(x => new User { Id = x }).ToList();
foreach(var user in proxyUsers)
{
context.Users.Attach(user);
bottleEntity.Users.Add(user);
}
await context.SaveChangesAsync();
return bottleEntity.Id;
This requires either turning off all entity tracking or remember to always query entities with AsNoTracking which can lead to additional work and intermitted bugs appearing if this isn't adhered to consistently. To deal with possible tracked entities is a fair bit more work:
var bottleEntity = mapper.Map<Bottle>(bottle);
var proxyUsers = bottle.OwnerIds
.Select(x => new User { Id = x }).ToList();
var existingUsers = context.Users.Local
.Where(x => bottle.OwnerIds.Contains(x.Id)).ToList();
var neededProxyUsers = proxyUsers.Except(existingUsers, new UserIdComparer()).ToList();
foreach(var user in neededProxyUsers)
context.Users.Attach(user);
var users = neededProxyUsers.Union(existingUsers).ToList();
foreach(var user in users)
bottleEntity.Users.Add(user);
await context.SaveChangesAsync();
return bottleEntity.Id;
Any existing tracked entity needs to be found and referenced in place of an attached user reference. The other caveat of this approach is that the "proxy" users created for non-tracked entities are not complete user records so later code expecting to get User records from the DbContext could receive these attached proxy rows and result in things like null reference exceptions etc. for fields that were not populated.
Hence, fetching the references from the EF DbContext to get the relatable entities is generally the best/simplest option.

Entity Framework Core: Adding an object with a List to a DbContext results in an empty List

We are attempting to save an Entity Framework Model to the database.
In the model a Track always contains a Message and a Message MAY have any number of Messages in its List of Retransmits.
When saving the model, we check if the Track model already exists in the database; If not, we simply add the Track to the DbContext.
public class Track
{
[Key]
public Guid TrackId { get; set; }
public virtual Message Message { get; set; }
}
public class Message
{
[Key]
public Guid MessageId { get; set; }
public Guid TrackId { get; set; }
public virtual List<Message> Retransmits { get; set; }
}
public void Save (Track track)
{
using (var context = new DatabaseContext())
{
Track foundTrack = Read(track.TrackId); // Returns a Track if it already exists in the database
if (foundTrack == null)
{
context.Add(track);
}
else
{
// Update the Track. Not relevant to this question
}
context.SaveChanges();
}
}
// Returns a Track if it exists in the database or null
public Track Read(Guid trackId)
{
using (var context = new DatabaseContext())
{
return context.Tracks
.Include(t => t.Message)
.ThenInclude(m => m.Retransmits)
.FirstOrDefault(t => t.TrackId == trackId);
}
}
When adding Track to the DbContext Entity Framework will always clear the List of Retransmits. This means that a Message can contain one Message in its List of Retransmits before calling DbContext.Add, but after calling DbContext.Add it will suddenly have 0. Entity Framework is effectively clearing the list and not adding it to the database.
We expected that the List of Retransmits would be added to the DbContext and NOT deleted from the model without warning.
During debugging we discovered that changing the List of Retransmits from List<Message> to List<AnyOtherObject> returned the expected results and rows in the database.
It seems that because the List of Retransmits is of the same type as the class it is contained within (I.e. A Message can contain many Messages), Entity Framework treats it differently.
We have also tried to configure a One-to-Many relationship between the entities where modelBuilder.Entity<Message>().HasMany(m => m.Retransmits) makes no difference. And even
modelBuilder.Entity<Message>().HasMany(m => m.Retransmits).WithOne(m => m.Parent)
where Parent is a reference back to the containing object of a retransmitted Message. However, this resulted in a StackOverflow exception.
What could be the reason for Entity Framework refusing to add the list to the database? Is there any way to configure Entity Framework to allow a Message to contain a List of Retransmits(being of type Message) and have it added to the database as expected? Or are we doing something wrong entirely?
I have attached an image of the behavior in Visual Studio's Debugger.
EDIT: We have tried the latest stable Entity Framework Core version 2.2.6, as well as Entity Framework Core version 3.0.0 - Preview 9. The same behavior exists across both versions.
EDIT EDIT: The following code initializes the model and should reproduce the issue.
public void ReproduceIssue ()
{
// Create a Track and Message
Track track = new Track()
{
Message = new Message()
};
track.Message.TrackId = track.TrackId;
// Create a Retransmitted Message
Message retransmit = new Message()
{
TrackId = track.TrackId
};
// Add the Retransmitted Message to the List
track.Message.Retransmits.Add(retransmit);
Save(track);
}
We made some changes to our model. While I believe Entity Framework should never throw any information away without issuing a warning / exception, we did circumvented the issue.
I would like to post the code, but it is a bit more complex as the original question left out a lot of code details. However, the solution was basically to configure a many-to-many relationship between Messages using a join table.
After that, it behaved as expected.

A 'Required' Collection field could not be updated via `AddOrUpdate`

I'm using Asp.net web api 2 + entity framework 6.
Basically I have 2 models:
public class MyOrderModel
{
public int Id { get; set; }
public string OrderNumber { get; set;}
public string AuthCode { get; set; }
[Required]
public List<MyOrderDetailModel> Details { get; set; }
}
public class MyOrderDetailModel
{
public int Id { get; set; }
public decimal Amount{ get; set;}
}
After ran the Package Manager Console command Enable-Migration, in Configuration.Seed(WaynyCloudTest.Models.ApplicationDbContext context), I was trying to add some pre-loaded data:
context.MyOrderModels.AddOrUpdate(
s => s.OrderNumber,
new MyOrderModel
{
OrderNumber = "0001",
AuthCode = "ABCDE",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
};
After the first(initial) Update-database command, everything is fine and I can see above data persisted to database 2 tables.
Later, I want to update the AuthCode property value from ABCDE to ABCDEXXX,
the only change is the value assignment:
context.MyOrderModels.AddOrUpdate(
s => s.OrderNumber,
new MyOrderModel
{
OrderNumber = "0001",
// THE ONLY CHANGE!
AuthCode = "ABCDEXXX",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
};
I would expect the EntityFramework will find the target data row in database by searching condition on OrderNumber and then update the AuthCode, but now I always got this exception in Seed function:
Entity Validation Failed - errors follow:
MyTest.Models.MyOrderModel failed validation
Details : The Details field is required.
Obviously the value was supplied for field Details, so what I've missed?
The problem is with the Id field of PostPayQRCodeFuelOrderModel. In your environment, the database uses this field as an identity (primary key) field and wants to generate the value itself.
In your case, there is an easy workaround:
context.MyOrderModel.AddOrUpdate(
p => p.OrderNumber,
new PostPayQRCodeFuelOrderModel
{
OrderNumber = "00001",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
}
);
Assumably, OrderNumber is unique, so this should work fine. In addition, running the seed again will not duplicate this data.
UPDATE:
It is possible to keep the original MyOrderModel.AddOrUpdate(), i.e., give the Id explicitly:
First, you need to prevent the auto-generation of the primary key value for MyOrderModel:
public class MyOrderModel
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
Then, you need to re-create the MyOrderModel table. NOTE: the usual approach of adding a migration just to modify the Id field will not work, you need to re-create the table.
Running the seed multiple times works, i.e., there are no duplicates (just checked it myself).
UPDATE 2:
I don't have a full explanation, why your code is not working, but with the code below it should be possible for you to construct the creation and the update of the database objects as you wish.
This code can be run in Seed(), multiple times without duplication. The update of AuthCode is, of course, artificial, but my point was to separate the creation and the update (just in case these need to be separated in the final implementation).
The whole project is available in https://github.com/masa67/AspNetNg, branch SO34252506-1.
Here's the code:
var mom = context.MyOrderModel.Where(m => m.OrderNumber == "00001").FirstOrDefault();
if (mom == null)
{
mom = new PostPayQRCodeFuelOrderModel
{
OrderNumber = "00001",
AuthCode = "ABCDE",
Details = new List<MyOrderDetailModel>() {
new MyOrderDetailModel
{
Amount = 5.67M
}
}
};
context.MyOrderModel.AddOrUpdate(p => p.OrderNumber, mom);
context.SaveChanges();
}
mom.AuthCode = "ABCDEXXX";
context.SaveChanges();
UPDATE 3:
A couple of suggestions, if this is still not working:
Consider dropping the Required constraint for Details and handle the consistency programmatically. I would not use this constraint on navigation properties anyways (but I am only familiar with EF to the extent of how we are using it in our current project at work, so there might be different views on this).
Test your code by re-creating the database first.
Test my Solution (link above). It is working for me, so there might be a difference in configuration somewhere.
Since Details is not virtual, EF is not using lazy loading. I was expecting this to cause problems, as Details becomes null when the object is read from the database, but that was not the case in my environment. You might try eager loading, but I doubt if this has any impact:
Eager loading:
var mom = context.MyOrderModel.Where(m => m.OrderNumber == "00001").Include(m => m.Details).FirstOrDefault();
UPDATE 4:
If this is still not working, then delete the database, but re-create the migrations in addition:
Delete the existing migrations.
Do not let EF make the assumption that it knows the state of your database, but force it to create the migrations from scratch (see other SO questions for advice). However, BEFORE doing that, please do notice that this operation will most probably overwrite the Seed() function as well, so take a copy of that file before the operation.

Entity framework attach: object with same key exists

I am building a windows form application, and I use multiple DBContext instances (mostly one per Business layer call).
After literally days of dealing with an issue (while inserting new entities, the ones they referred to were added as new ones, instead of using the existing entities), I found out that the problem was I had to attach the existing entities to the context.
All was good for about 2 hours, when I then got errors while attaching: the entity with the same key exists in the context.
I tried testing before attaching (similar method for every entity type):
private void attachIfNeeded(POCO.Program myObject, myContext context)
{
if (!context.Set<POCO.Program>().Local.Any(e => e.ID == myObject.ID))
{
context.programs.Attach(myObject);
return true;
}
else
{
myObject = context.Set<POCO.Program>().Local.Single(e => e.ID == myObject.ID);
return false;
}
}
But the tests return false, but it still fails when attaching.
So basically, if I don't attach, it will add a new entity instead of using the existing (and intended) one. If I do attach, there's an error I can't figure out.
I have looked around (doing this the whole day now) and I actually (think I) know what the problem is:
The entity I am trying to add has multiple relationships, and other entities can be reached by multiple paths. Could that cause the problem?
Please help with this, solutions out there really make no sense to me and haven't worked.
I am really close to the point where I will try-catch around the attach statement and be done with it. But I will hate doing it.
Here are my entities (not all of them, but this should be enough):
public class Word
{
[Key]
public int ID {get;set;}
[Required]
public string word { get; set; }
public WordCategories category { get; set; }
public Word parent {get;set;}
public List<Unit> units { get; set; }
public Program program { get; set; }
public List<Lesson> lessons { get; set; }
public Word()
{
units = new List<Unit>();
lessons = new List<Lesson>();
}
}
public class Unit
{
[Key ]
public int ID { get; set; }
[Required]
public string name { get; set; }
public string description { get; set; }
public List<Lesson> lessons { get; set; }
public Program program {get;set;}
public List<Word> words { get; set; }
public Unit()
{
lessons=new List<Lesson>();
words = new List<Word>();
}
}
And here is where I am calling the attach method. The error is thrown on the first attach:
public int addWords(List<POCO.Word > words,int programID, int unitID,int lessonID)
{
CourseHelperDBContext context = getcontext();
int result;
foreach(POCO.Word a in words)
{
foreach (POCO.Unit b in a.units)
attachIfNeeded(b, context);
foreach(POCO.Lesson c in a.lessons )
attachIfNeeded(c,context);
attachIfNeeded(a.program,context);
if (a.parent != null)
attachIfNeeded(a.parent,context);
}
context.words.AddRange(words);
result = context.SaveChanges();
return result;
}
I cannot believe I'm having so many issues with this. I just want to store those entities, add some (I haven't gotten to the point where I would change them) and save it.
So far I've figured:
Some words are new, some exist and some are changed (mostly parent property);
All units exist, as do programs and lessons (so I need to attach them);
The object graph contains multiple paths to entities, some of which exist, some of which are new;
I am using a new context for every request. I run into other issues when I was using the same all the time. I found solutions that pointed to this pattern, and I think it's OK since that's what you'd do on an ASP MVC project.
All these could be causing problems, but I don't know which and how to work around them.
I think I can make this work by adding one word at a time, and pulling programs, lessons and units every time... but that means many many round trips to the DB. This can't be the way.
Back to this after quite some time, the problem in this case was that I needed to retrieve the entities that were present on my relationships.
The solution was neither attach (because it would fail if the entity is already attached) nor add (because it already existed on the DB).
What I should have done was to retrieve every entity related to the one I was adding.
This helped:
Entity Framework creating new entity with relationship to existing entity, results in attempt to create new copy of the existing entity
After attaching the entity, try setting the entity state to modified.
context.programs.Attach(myObject);
context.Entry(myObject).State = EntityState.Modified;
I think there's a mistake in your test logic.
If entity does not exist in database, you should be adding instead of attaching. Your code is attaching if it can't find an entity when it should really be adding.
Code to add a new entity (Create/Insert)
context.Set<T>.Add(entity);
Code to attach an entity (Update)
context.Set<T>.Attach(entity);
context.Entry(entity).State = EntityState.Modified;
If your code is failing on the first attach, that would be attachIfNeeded(b,context); ? I don't think you have shown us the code for this.
I share my experience with the same exception.
First, here is my code:
public void UpdateBulk(IEnumerable<Position> pDokumentPosition, DbDal pCtx)
{
foreach (Position vPos in pDokumentPosition)
{
vPos.LastDateChanged = DateTime.Now;
pCtx.Entry(vPos).State = EntityState.Modified;
}
pCtx.SaveChanges();
}
I got the same exception on the EntityState.Modified line.
In my case the problem was that, when set the vPos state to modified, then all the related objects (vPos.Document and vPos.Produkt) loaded in the context with unchanged state.
In the foreach first step it not makes any exception, just on the second step, because eg. the related Dokument entity has already been loaded in the memory/context (so the key property of the Dokument too).
And how i solve it? (maybe not the best solution):
I detach the related entites in every step with this lines:
if (vPos.Dokument != null)
{
pCtx.Entry(vPos.Dokument).State = EntityState.Detached;
}
if (vPos.Produkt!=null)
{
pCtx.Entry(vPos.Produkt).State = EntityState.Detached;
}
If you have better solution, I'm looking forward to it...
You can try this
context.words.Add(words);
result=context.SaveChanges();

No new many to many connections are made in the database when saving an object in EF

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);

Categories

Resources