Entity Framework 5 adding existing entity to nested collection - c#

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.

Related

why ef lost relationship once SaveChanges?

If I simply do this:
var medical = ctx.Medicals.FirstOrDefault(p => p.ID == medicalViewModel.ID);
var sizeClinics = medical.Clinics.Count;
The amount is (for example) 10 (i.e. I have 10 clinics for that medical).
Now, if I do this:
var medical = mapper.Map<MedicalViewModel, Medicals>(medicalViewModel);
ctx.Entry(medical).State = medical.ID == 0 ? EntityState.Added : EntityState.Modified;
ctx.SaveChanges();
medical = ctx.Medicals.FirstOrDefault(p => p.ID == medicalViewModel.ID);
var sizeClinics = medical.Clinics.Count;
The size is 0. Why? It seems it remove relationship after SaveChanges?
Here's the Medicals object:
public partial class Medicals
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Medicals()
{
this.Activities = new HashSet<Activities>();
this.MedicalsRefunds = new HashSet<MedicalsRefunds>();
this.Clinics = new HashSet<Clinics>();
}
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Activities> Activities { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<MedicalsRefunds> MedicalsRefunds { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Clinics> Clinics { get; set; }
}
I thing I've noticed: if I analyze medical object with QuickWatch the first time (without SaveChanges part) its as {System.Data.Entity.DynamicProxies.Medicals_650D310387E78A83885649345ED0FB2870EC304BF647B59321DFA0E4FBC78047}.
Instead, if I do SaveChanges and then I retrieve that medical, it is as {MyNamespace.Models.Medicals}.
What can it be?
This question is answered by understanding how Entity Framework works internally. I'll try to highlight the key features here.
Change tracking
Entity Framework has a sort of cache of entities in-memory, called the change tracker.
In your first example, when you fetch an entity from the database:
var medical = ctx.Medicals.FirstOrDefault(p => p.ID == medicalViewModel.ID);
Entity Framework creates the Medicals instance that you receive. When it does so, it also uses that opportunity to store a reference to that object, for its own reasons. It will keep an eye on those objects and track any changes made to them.
For example, if you now call ctx.SaveChanges(); at any point, it's going to look at everything in its change tracker, see which things have been changed, and update those in the database.
There are several benefits attached to this: you don't have to explicitly tell EF that you made changes to some of the entities it was already tracking in its cache, and EF can also spot which specific fields have changed, so it only has to update those specific fields and it can ignore the unchanged fields.
Update from comments: EF only allows the tracking of one instance of a given entity, based on the PK value. So if you've already tracked the Medical with ID 123, you can't track another instance of the same Medical entity with ID 123.
Lazy loading
The code you use suggests that you are lazy loading. I'm going to gloss over the intricate details here, to keep it simple. If you don't know what lazy/eager loading is, I suggest you look this up, as the explanation is too long to write down here. Lazy/eager loading is a key concept in Entity Framework for dealing with entity relations and how to fetch related entities.
When dealing with lazy loading, EF slightly tinkers with your entity when it fetches it for you. It puts a special lazy collection in all the entity's navigational properties (such as medical.Clinics), so that it will fetch the related data only when you actually try to access it, i.e. by enumerating the collection in any way.
Comparatively, if you were using eager loading, EF wouldn't do this for you and the nav prop simply wouldn't be filled in with anything unless you explicitly called Include on it.
Updating untracked entities
In your second example, you are working with an entity object which was not created by Entity Framework. You made it yourself:
var medical = mapper.Map<MedicalViewModel, Medicals>(medicalViewModel);
And now you manually add it to the change tracker:
ctx.Entry(medical).State = medical.ID == 0 ? EntityState.Added : EntityState.Modified;
There's nothing wrong with this, but you have to realize that the entity in the change tracker was not generated by EF, and therefore it doesn't contain these special "lazy navigational properties". And because it doesn't contain these lazy navigational properties...
var sizeClinics = medical.Clinics.Count;
... the above code doesn't actually try to fetch the data from the database. It simply works with the entity object you generated and what it already contains in-memory.
And since you didn't add anything to medical.Clinics yourself, the collection is therefore empty.
The answer
Lazy loading only works on entity objects generated by EF, not on entity objects generated by you, regardless of whether you manually added it to EF's change tracker afterwards or not.
So to get the count, you can specifically query the clinics from the database:
var medical = mapper.Map<MedicalViewModel, Medicals>(medicalViewModel);
var clinicCount = ctx.Clinics.Count(p => p.MedicalId == medical.ID);
Or you could detach the entity and fetch it from the db, though I'm not a fan of this:
var medical = mapper.Map<MedicalViewModel, Medicals>(medicalViewModel);
ctx.Entry(medical).State = medical.ID == 0 ? EntityState.Added : EntityState.Modified;
ctx.SaveChanges();
// Detach
ctx.Entry(medical).State = EntityState.Detached;
// Now fetch from db
var medical2 = ctx.Medicals.FirstOrDefault(p => p.ID == medical.ID);
var sizeClinics = medical2.Clinics.Count;
Why detach? Remember how I mentioned that EF only allows tracking of one entity of a given type and PK. Since the object referred to by medical is already being tracked, you can't fetch and track another new instance of Medicals with the same PK.
By detaching the first, medical2 can be fetched and tracked since the change tracker "forgot" the other instance.
But to be honest, it would be easier to just open a new context instead of trying to manually detach and re-query.
var medical = mapper.Map<MedicalViewModel, Medicals>(medicalViewModel);
ctx.Entry(medical).State = medical.ID == 0 ? EntityState.Added : EntityState.Modified;
ctx.SaveChanges();
using(var ctx2 = new MyContext())
{
var medical2 = ctx2.Medicals.FirstOrDefault(p => p.ID == medical.ID);
var sizeClinics = medical2.Clinics.Count;
}
More info if you're interested
If you're using code first, lazy loading is why EF requires you to make these properties virtual. EF needs to be able to inherit from your entity class and make a special derived class which overrides the navigational property behavior.
You already stumbled on this, when you said:
I thing I've noticed: if I analyze medical object with QuickWatch the first time (without SaveChanges part) its as {System.Data.Entity.DynamicProxies.Medicals_650D310387E78A83885649345ED0FB2870EC304BF647B59321DFA0E4FBC78047}.
Instead, if I do SaveChanges and then I retrieve that medical, it is as {MyNamespace.Models.Medicals}.
That System.Data.Entity.DynamicProxies.Medicals_65 (and so on) class was dynamically generated by Entity Framework, inherits the Medicals class, and overrides the virtual navigational properties so that it lazily loads this information when the collection is enumerated.
This is the hidden magic of how EF achieves lazy loading.

EF Core Attach/AttachRange methods not working

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

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.

Stackoverflow Exception with many-to-many relationship object in entity framework

I'm using entity framework in .net core, Note that my knowledge of entity framework is somewhat limited, so take my assumptions with a pinch of salt.
Teams & Users are the two objects that I have trouble with.
They have a joining table, UserTeam because Users can have many Teams, and Teams can have many Users.
I figured out how to have them saved in a sqlite database using entityframework, and it's all fine. It took me a while to realize I had to use Include to get the joining property, but now it works in that part.
At some point, I have to expose that object on the API, so I have matching UserDTO & TeamDTO. There is my issue. UserDTOs have a list of TeamDTOs, and TeamDTOs have a list of UserDTOs, because that's how they should look like. Right?
But when I am mapping them, I get into a StackOverflowException, which is normal, because I'm looping on the list of teams, which contains Users, and for each of those Users, I end up recreating Teams, and so on.
Right now, I just added a boolean check in the loop, so when I call .ToDTO() I can decide to skip the users/teams from inner levels, but that does not look like a proper fix to me. Is it?
What do you guys suggest ?
Here is what I'm doing exactly :
public static TeamDTO ToDTO(this Team team, EatupContext context, bool doOnce = false)
{
var dto = new TeamDTO
{
Id = team.Id,
Name = team.Name,
};
var users = new List<UserDTO>();
foreach (var userTeam in team.UsersTeams)
{
var user = context.Users.Find(userTeam.UserId);
if (doOnce)
user.TeamsUsers = new List<UserTeam>();
users.Add(user.ToDTO(context, true));
}
dto.Users = users;
return dto;
}
public static UserDTO ToDTO(this User user, EatupContext context, bool doOnce = false)
{
var dto = new UserDTO
{
Id = user.Id,
NickName = user.NickName,
Email = user.Email,
Image = user.Image,
};
var teams = new List<TeamDTO>();
foreach (var userTeam in user.TeamsUsers)
{
var team = context.Teams.Find(userTeam.TeamId);
if (doOnce)
team.UsersTeams = new List<UserTeam>();
teams.Add(team.ToDTO(context, true));
}
dto.Teams = teams;
return dto;
}
Because I have a strong feeling my issue might come from there, I'm gonna share this piece of information :
When I'm looping through the entity objects UserTeams(of either a team or a user), the Team object is null, so I have to fetch the Team from the context using its ID, and then I have everything I need. It's very strange because everything appears to be working fine and the database is complete and healthy. Its just the UserTeam object that is missing the Team property.
But my assumption is that between fetching a new Team from the ID, and getting the Team from the UserTeam, both would create the same result I have now when I call ToDTO(): I still would need to loop through its users and have that overflow exception.
This seems like a very common issue that should have been dealt with by a lot of people, but I can't really find my answer using the keywords of my title.
What's my next step ?
You're getting the stackoverflow due to the fact that you're circularly calling the 2 methods.
In the Team Version of the ToDTO method, you call the user version, which in turn calls the team version and so on.
If your model is not structured something like below you should consider it. There's no need to actually model UserTeam as EFs mapping should take care of this. That's considering that you've specified one end of the relationship as the primary.
public class Team
{
public Team()
{
Users = new List<User>();
}
//properties we don't care about
public virtual ICollection<User> Users {get;set;}
}
public class User
{
public User()
{
Teams = new List<User>();
}
//properties we don't care about
public virtual ICollection<Team> Teams {get;set;}
}
just turn off lazy loading and use include method when you need to fetch lists of objects.
this is for DbContext class
public class BloggingContext : DbContext
{
public BloggingContext()
{
this.Configuration.LazyLoadingEnabled = false;
}
}
and this is for the controllers
using (var context = new BloggingContext())
{
// Load all blogs, all related posts, and all related comments.
var blogs1 = context.Blogs
.Include(b => b.Posts.Select(p => p.Comments))
.ToList();
// Load all users, their related profiles, and related avatar.
var users1 = context.Users
.Include(u => u.Profile.Avatar)
.ToList();
// Load all blogs, all related posts, and all related comments
// using a string to specify the relationships.
var blogs2 = context.Blogs
.Include("Posts.Comments")
.ToList();
// Load all users, their related profiles, and related avatar
// using a string to specify the relationships.
var users2 = context.Users
.Include("Profile.Avatar")
.ToList();
}
source : https://learn.microsoft.com/en-us/ef/ef6/querying/related-data

Why is my Seed() method not saving and connecting entities correctly?

I am building a registration site for a conference for my organization, with multiple VIPs and guest speakers. The requirement is to track many details about each attendee including their arrival and departure plans and their local lodging information. In order to facilitate discussion with stakeholders on the types of reports we need to build, I want to populate my dev database with a batch of records from a CSV containing randomly generated information like name, arrival/departure date/time, etc. This will allow us to look at a working site without having to register and re-register many times.
However, I simply cannot get the Seed method to persist the relevant records properly, yet my controller which handles the registration works perfectly.
My database structure is basically an Attendee entity with child entities for TravelSchedule, LodgingArrangement, and various lookups. Here are excerpts from my entities:
public class Attendee
{
public int Id { get; set; }
... other strings/etc ...
public virtual TravelSchedule TravelSchedule { get; set; }
public int TravelScheduleId { get; set; }
public virtual LodgingArrangment LodgingArrangement { get; set; }
public int LodgingArrangementId { get; set; }
}
public class TravelSchedule
{
public int Id { get; set; }
... other properties ...
public virtual Attendee Attendee { get; set; }
public int AttendeeId { get; set; }
}
public class LodgingArrangement
{
public int Id { get; set; }
... other properties ...
public virtual Attendee Attendee { get; set; }
public int AttendeeId { get; set; }
}
Here is the content of my context's OnModelCreating method:
modelBuilder.Entity<Attendee>()
.HasOptional(a => a.TravelSchedule)
.WithRequired(r => r.Attendee);
modelBuilder.Entity<TravelSchedule>()
.HasRequired(m => m.ArrivalMode)
.WithMany(m => m.Arrivals)
.HasForeignKey(m => m.ArrivalModeId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<TravelSchedule>()
.HasRequired(m => m.DepartureMode)
.WithMany(m => m.Departures)
.HasForeignKey(m => m.DepartureModeId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Attendee>()
.HasOptional(a => a.LodgingArrangement)
.WithRequired(l => l.Attendee);
The following is an excerpt from my Seed method.
var attendees = GetAttendeesFromCsv();
context.Attendees.AddOrUpdate(a => a.Email, attendees.ToArray());
context.SaveChanges();
var dbAttendees = context.Attendees.ToList();
foreach (var attendee in dbAttendees)
{
attendee.TravelSchedule = CreateTravelSchedule();
context.Entry<Attendee>(attendee).State = EntityState.Modified;
context.SaveChanges();
}
GetAttendeesFromCsv() extracts the records from the CSV into Attendee objects, thanks to the CsvHelper package. CreateTravelSchedule creates a new TravelSchedule entity and populates it with data from lookup tables using the SelectRandom() method from extensionmethod.com. The bottom line is that I extract the CSV rows into Attendee objects, add a new randomly-generated TravelSchedule entity, and save the resulting Attendee with attached TravelSchedule.
Except this does not work. Instead the above code adds the TravelSchedule records to the database, but the AttendeeId is always set to 0 in the table. Also the Attendees table is populated with all of the records from the CSV, but the TravelScheduleId on each row is always 0 as well. However, when stepping through the update-database call with the debugger the attendee.Id is populated properly, so by my understanding EF should pick up that the two are related and persist the related TravelSchedule at the same time as the Attendee. So why isn't EF connecting the two records?
Changing the loop to this:
foreach (var attendee in dbAttendees)
{
var travel = CreateTravelSchedule();
travel.AttendeeId = attendee.Id; // I also tried just travel.Attendee = attendee, without success
context.TravelSchedules.Add(travel);
context.SaveChanges();
}
Results in this error:
System.Data.Entity.Core.UpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Data.SqlClient.SqlException: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.TravelSchedules_dbo.Attendees_Id". The conflict occurred in database "MySite.DAL.MyContext", table "dbo.Attendees", column 'Id'.
So it appears I cannot add the TravelSchedule entity to the Attendee, and I also cannot go "backwards" by creating the TravelSchedule and then attaching the Attendee.
The frustrating part is that the registration form logic in my controller works perfectly fine, excerpt below. The walkthrough is that the registration controller stores each screen's data (view models) in the session using a static WorkflowManager class which handles persistence between screens. After user confirmation the controller pulls each screen's details from the WorkflowManager, runs them through AutoMapper to convert them to the relevant populated DAL entities, attaches those entities to the attendee entity, and saves it all to the database.
Again, this works perfectly fine, saving the attendee and its two child entities without error. Here is the relevant excerpt of the controller action:
var attendeeRegistration = WorkflowManager.GetAttendeeRegistration();
var travelRegistration = WorkflowManager.GetTravelRegistration();
using (var db = new MyContext())
{
var attendee = Mapper.Map<Attendee>(attendeeRegistration);
attendee.AnotherChildEntity = db.ChildEntities.Find(attendeeRegistration.SelectedChildEntityId);
var travel = Mapper.Map<TravelSchedule>(travelRegistration);
travel.ArrivalMode = db.TravelModes.Find(travelRegistration.SelectedArrivalModeId);
travel.DepartureMode = db.TravelModes.Find(travelRegistration.SelectedDepartureModeId);
var lodging = Mapper.Map<LodgingArrangement>(lodgingRegistration);
lodging.LodgingLocation = db.LodgingLocations.Find(lodgingRegistration.SelectedLodgingLocationId);
attendee.Comments = comments;
attendee.TravelSchedule = travel;
attendee.LodgingArrangement = lodging;
db.Attendees.Add(attendee);
db.SaveChanges();
}
This works perfectly. None of these objects are in the database until after the user confirms the registration is complete. So I don't understand why I can persist new entities to the database here, yet I can't do what appears to me to be the same thing in the Seed method above.
Any help or ideas much appreciated. This has been causing me endless grief for days now. Thanks.
The association between Attendee and TravelSchedule is 1:1. EF implements 1:1 associations by creating a primary key in the dependent entity (here: TravelSchedule) that's also a foreign key to its principal entity (Attendee).
So Attendee.TravelScheduleId and TravelSchedule.AttendeeId are not used for the association and their values remain 0. Which means: the Seed works without these fields/properties (and I'd even expect it to work with them), it establishes the associations through the Id fields.

Categories

Resources