EF Core 2.0 - Circular Dependency with required FK on either end - entity-framework

I have a fairly simple data model consisting of two entities:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int CurrentLocationId { get; set; }
public List<Location> Locations { get; set; }
public Location CurrentLocation { get; set; }
}
and
public class Location
{
public int Id { get; set; }
[Required]
public int UserId { get; set; }
public string Address { get; set; }
public User User { get; set; }
}
then in order to get a successful migration to run I needed the following model builder code:
builder.Entity<Location>()
.HasOne(x => x.User)
.WithMany(x => x.Locations)
.HasForeignKey(x => x.UserId);
This has generated a database as I'd expect and how I need it to be. However, I'm unable to save entities due to the following circular dependency error:
InvalidOperationException: Unable to save changes because a circular dependency was detected in the data to be saved: 'ForeignKey: User {'CurrentLocationId'} -> Location {'Id'} ToPrincipal: CurrentLocation, ForeignKey: Location {'UserId'} -> User {'Id'} ToDependent: Locations ToPrincipal: User'.
Is there a way around this in EF Core 2.0?
I have a few options to circumnavigate it by changing my data model, but this is the preferred approach as I can use DB constraints to ensure that all Locations link back to a User, and that every user must have a CurrentLocation set. I know it would solve the issue but I can't really allow nulls on the CurrentLocation field!
The code I'm using to try and store users is as follows (simplified for demo purposes):
var location = new Location
{
Address = "Some address"
};
_context.Locations.Add(location);
var user = new User
{
Name = "Stu"
};
_context.Users.Add(user);
user.Locations = new List<Location>
{
location
};
user.CurrentLocation = location;
await _context.SaveChangesAsync();
and I've also tried
var location = new Location
{
Address = "Some address"
};
var user = new User
{
Name = "Stu",
Locations = new List<Location>
{
location
},
CurrentLocation = location
};
_context.Users.Add(user);
await _context.SaveChangesAsync();
But the error remains the same. Can this be fixed by some kind of fancy ModelBuilder overrides? Or am I restricted to changing my data model / allowing nulls where I shouldn't really be allowing nulls?
Thanks in advance!

To answer my own question - just checked and SQL server doesn't support deferrable constraints, so nothing EF can do anyway! Change to the data model it is.

Related

What is the proper way of updating navigation properties in EF Core?

In my EF Core solution I have the following model:
public class Deal
{
public string Id { get; set; }
public ResponsiblePerson ResponsiblePerson1 { get; set; }
public ResponsiblePerson ResponsiblePerson2 { get; set; }
public ResponsiblePerson ResponsiblePerson3 { get; set; }
}
public class ResponsiblePerson
{
public string Id { get; set; }
public string Name { get; set; }
}
When I am trying to update Deal navigations properties:
private void UpdateResponsiblePersons(string dealId, string person1Id, string person2Id, string person3Id)
{
var existingdeal = _dbContext.Deals
.Include(d => d.ResponsiblePerson1)
.Include(d => d.ResponsiblePerson2)
.Include(d => d.ResponsiblePerson3)
.Single(d => d.Id == dealId);
existingDeal.ResponsiblePerson1 = new ResponsiblePerson { Id = person1Id };
existingDeal.ResponsiblePerson2 = new ResponsiblePerson { Id = person2Id };
existingDeal.ResponsiblePerson3 = new ResponsiblePerson { Id = person3Id };
_dbContext.Entry(deal.ResponsiblePerson1).State = EntityState.Unchanged;
_dbContext.Entry(deal.ResponsiblePerson3).State = EntityState.Unchanged;
_dbContext.Entry(deal.ResponsiblePerson3).State = EntityState.Unchanged;
_dbContext.SaveChanges();
}
EF often fails with
System.InvalidOperationException: The instance of entity type 'ResponsiblePerson' 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.
That is because sometimes existingdeal already contains the link to ResponsiblePerson with one of provided IDs in either ResponsiblePerson1 of ResponsiblePerson2 or ResponsiblePerson3 Navigation properties.
I know that one of possible solutions will be first to get ResponsiblePersons used for update from dbContext like
existingDeal.ResponsiblePerson1 = _dbContext.ResponsiblePersons.Find(person1Id)
But that means extra DB roundtrips.
Another solution is to expose foreign keys instead of navigation properties but it would make Deal model quite ugly.
Please advice me what is the best way of updating such references?

EF Core tracking problem when adding Entity to a List

I ran into a problem while developing my small Blazor WASM app.
A part of my app is where users can create teams, and invite other users to join their team. The relevant Entity Classes is:
Team.cs
public class Team
{
[Key]
public Guid TeamID { get; set; }
public string Name { get; set; }
public string Abbreviation { get; set; }
public Guid? BadgeID { get; set; }
public Guid TownID { get; set; }
public Guid StatisticsID { get; set; }
public Guid CaptainID { get; set; }
public List<AppUserDTO> Players { get; set; } = new();
}
When a User accepts an invitation he should be added to the List<AppUserDTO> Players List, I do this this way on the client side:
private async Task AcceptInvite()
{
Team.Players.Add(Player);
await TeamDataService.UpdateTeam(Team);
}
public async Task UpdateTeam(Team team)
{
var teamJson =
new StringContent(JsonSerializer.Serialize(team), Encoding.UTF8, "application/json");
await _httpClient.PutAsync("api/team", teamJson);
}
But I get the following exception on the server side when I'd like to save the changes to the server:
System.InvalidOperationException: The instance of entity type 'AppUserDTO' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
With the server-side code being:
public Team UpdateTeam(Team team)
{
var updatedTeam = _appDbContext.Teams.Include(t => t.Players).FirstOrDefault(t => t.TeamID == team.TeamID);
if (updatedTeam == null) return null;
updatedTeam.TeamID = team.TeamID;
updatedTeam.Name = team.Name;
updatedTeam.Abbreviation = team.Abbreviation;
updatedTeam.TownID = team.TownID;
updatedTeam.StatisticsID = team.StatisticsID;
updatedTeam.Players = team.Players;
updatedTeam.CaptainID = team.CaptainID;
_appDbContext.SaveChanges();
return updatedTeam;
}
The exception pops up at the _appDbContext.SaveChanges() method.
What I noticed is the following: When I add an Entity to an empty list and save it, I get no exception, but if the list already has Entities I get this error.
What would be the solution for this, I believe is quite common what I try to do, but I didn't find a solution anywhere for this.
When you execute:
var updatedTeam = _appDbContext.Teams
..Include(t => t.Players).FirstOrDefault(t => t.TeamID == team.TeamID);
... you are retrieving existing Players from the Db and _appDbContext is tracking them (by "ID").
Now, when you set Players:
updatedTeam.Players = team.Players;
... I suspect that team.Players includes Players that are already being tracked by the _appDbContext. Hence your error.
You could try:
List<Player> playersToAdd = team.Players.Except(updatedTeam.Players);
updatedTeam.AddRange(playersToAdd);
In this way, you are not adding duplicate players to the context that are already being tracked from the initial database retrieval.

Include in EF Core 2.0 create infinite nested child Entities

I have 2 entities with a One-to-One relationship, the models are:
public class Asset
{
public int Id { get; set; }
public string Name { get; set; }
public TrackingDevice TrackingDevice { get; set; }
}
public class TrackingDevice
{
public int Id { get; set; }
public string Imei { get; set; }
public int? AssetId { get; set; }
public Asset Asset { get; set; }
}
I have entered data so when I make a simple query as follows:
var list = _appContext.TrackingDevices.Include(td => td.Asset).ToListAsync();
I get correctly the list of tracking devices that include their asset, however, the asset child again includes the tracking device, and this in turn the asset and so on, which creates an infinite structure that when applying the AutoMapper failure.
How can I do the Include and only get the 2 levels I need?
Tracking Device -> Asset
This is really annoying in EF Core.
I could not solve it completely but here is a wrok-around about that Not a complete solution
var list = _appContext.TrackingDevices.Include(td => td.Asset).ToListAsync();
foreach(var l in list)
l.Asset.TrackingDevice = null;
What you want to build is a 1-to-1 relationship between your Asset and your Tracking device.
Your code, if not configured properly, really tells EF Core that each of you entity have an entity linked to it.
To make EF Core understand that, this is how you need to declare your relationship:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Asset>()
.HasOne(a => a.TrackingDevice)
.WithOne(t => t.Asset)
.HasForeignKey<TrackingDevice>(b => b.AssetId);
}
Please have a look here to get more information on how to set this up.
Additionally, to avoid having EF to avoid returning nested objects, try to use Select instead of include (you can then export exactly what you need):
var list = _appContext.TrackingDevices
.Select(t => new {
TrackingDevice = t,
...
Asset = Asset,
});

EF6:How to include subproperty with Select so that single instance is created. Avoid "same primary key" error

I'm trying to fetch (in disconnected way) an entity with its all related entities and then trying to update the entity. But I'm getting the following error:
Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
public class Person
{
public int PersonId { get; set; }
public string Personname { get; set }
public ICollection Addresses { get; set; }
}
public class Address
{
public int AddressId { get; set; }
public int PersonId { get; set; }
public string Line1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person Person { get; set; }
public ICollection<Feature> Features { get; set; }
}
// Many to Many: Represented in database as AddressFeature (e.g Air Conditioning, Central Heating; User could select multiple features of a single address)
public class Feature
{
public int FeatureId { get; set; }
public string Featurename { get; set; }
public ICollection<Address> Addresses { get; set; } // Many-To-Many with Addresses
}
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
var person = dbContext.People.AsNoTracking().Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}
public void UpdateCandidate(Person newPerson)
{
Person existingPerson = GetPerson(person.Id); // Loading the existing candidate from database with ASNOTRACKING
dbContext.People.Attach(existingPerson); // This line is giving error
.....
.....
.....
}
Error:
Additional information: Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Kindly suggest.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Since you are using a short lived DbContext for retrieving the data, all you need is to remove AsNoTracking(), thus allowing EF to use the context cache and consolidate the Feature entities. EF tracking serves different purposes. One is to allow consolidating the entity instances with the same PK which you are interested in this case, and the second is to detect the modifications in case you modify the entities and call SaveChanges(), which apparently you are not interested when using the context simply to retrieve the data. When you disable the tracking for a query, EF cannot use the cache, thus generates separate object instances.
What you really not want is to let EF create proxies which hold reference to the context used to obtain them and will cause issues when trying to attach to another context. I don't see virtual navigation properties in your models, so most likely EF will not create proxies, but in order to be absolutely sure, I would turn ProxyCreationEnabled off:
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
dbContext.Configuration.ProxyCreationEnabled = false;
var person = dbContext.People.Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}

EF code first MVC4 - ArgumentNullException on edit - what am I doing wrong here?

Let's say I have 3 models:
[Table("UserProfile")]
public class UserProfile //this is a standard class from MVC4 Internet template
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public string UserName { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
public int UserProfileId { get; set; }
[ForeignKey("UserProfileId")]
public virtual UserProfile UserProfile { get; set; }
}
Now, I'm trying to edit Post
[HttpPost]
public ActionResult Edit(Post post)
{
post.UserProfileId = context.UserProfile.Where(p => p.UserName == User.Identity.Name).Select(p => p.UserId).FirstOrDefault();
//I have to populate post.Category manually
//post.Category = context.Category.Where(p => p.Id == post.CategoryId).Select(p => p).FirstOrDefault();
if (ModelState.IsValid)
{
context.Entry(post.Category).State = EntityState.Modified; //Exception
context.Entry(post.UserProfile).State = EntityState.Modified;
context.Entry(post).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(post);
}
And I'm getting ArgumentNullException.
Quick look into debug and I can tell that my Category is null, although CategoryId is set to proper value.
That commented out, nasty-looking trick solves this problem, but I suppose it shouldn't be there at all. So the question is how to solve it properly.
I would say it's something with EF lazy-loading, beacuse I have very similar code for adding Post and in debug there is same scenerio: proper CategoryId, Category is null and despite of that EF automagically resolves that Post <-> Category dependency, I don't have to use any additional tricks.
On edit method, EF has some problem with it, but I cannot figure out what I'm doing wrong.
This is working as intended. Your Post object is not attached to the Context, so it has no reason to do any lazy loading. Is this the full code? I don't understand why you need to set Category as Modified since you're not actually changing anything about it.
Anyway, I recommend you query for the existing post from the Database and assign the relevant fields you want to let the user modify, like such:
[HttpPost]
public ActionResult Edit(Post post)
{
var existingPost = context.Posts
.Where(p => p.Id == post.Id)
.SingleOrDetault();
if (existingPost == null)
throw new HttpException(); // Or whatever you wanna do, since the user send you a bad post ID
if (ModelState.IsValid)
{
// Now assign the values the user is allowed to change
existingPost.SomeProperty = post.SomeProperty;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(post);
}
This way you also make sure that the post the user is trying to edit actually exists. Just because you received some parameters to your Action, doesn't mean they're valid or that the post's Id is real. For example, some ill intended user could decide to edit posts he's not allowed to edit. You need to check for this sort of thing.
UPDATE
On a side note, you can also avoid manually querying for the current user's Id. If you're using Simple Membership you can get the current user's id with WebSecurity.CurrentUserId.
If you're using Forms Authentication you can do Membership.GetUser().ProviderUserKey.