EF Many To Many Join - Lazy Loading Disabled - entity-framework

Here is my scenario:
public class Contact
{
public Guid ContactId { get; set; }
........
public Guid WorkspaceId { get; set; }
public Workspace Workspace { get; set; }
}
public class Workspace
{
public Guid WorkspaceId { get; set; }
........
public ICollection<Contact> ReferencedContacts { get; set; }
public ICollection<Contact> OwnedContacts { get; set; }
}
The logic is the following one: A contact can't exists outside a workspace (means that the workspace owns the contact).
A user has a workspace, and it places the 'owned contacts' in it (owned contact = the contact information describes the users such as profession, name, address, etc).
Also a user workspace can hold a reference to contacts owned by other users (here comes the join table which stores the relation between a workspace and the referenced contacts).
public WorkspaceMap()
{
....
HasMany(w => w.ReferencedContacts).WithMany().Map(mp =>
{
mp.ToTable("WorkspaceReferencedContacts");
mp.MapLeftKey("WorkspaceId");
mp.MapRightKey("ContactId");
});
}
public ContactMap()
{
......
HasRequired(c => c.Workspace).WithMany(w => w.OwnedContacts).HasForeignKey(c => c.WorkspaceId).WillCascadeOnDelete(false);
Property(c => c.WorkspaceId).HasColumnName("WorkpaceId");
}
What I am trying to do is to get all referenced contacts for a specific workspace using query methods. The SQL version of the query would be the following one:
SELECT * FROM dbo.Contacts c
INNER JOIN dbo.WorkspaceReferencedContacts wc ON wc.ContactId = c.ContactId
WHERE wc.WorkspaceId = '57F685C0-428C-44C3-8708-F30B5AF34CAE';
I have approached many ways without any success. Please note that lazy loading is disabled (there is no point to discuss why...).

...get all referenced contacts for a specific workspace...
I think:
var contacts = context.Workspaces
.Where(w => w.WorkspaceId == "57F685C0-428C-44C3-8708-F30B5AF34CAE")
.Select(w => w.ReferencedContacts)
.SingleOrDefault();

Related

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.

.NET Core - join 2 tables with AsQueryable

I have two nested classes: Partner contains a Company (that has a field "Name")
I do a search by Id on the partner's Id
I want to do a search on the company's "Name" field
here is my poco:
public class Partner
{
[Required]
public int? Id { get; set; }
[Required]
public Company Company { get; set; }
using AsQueryable, I can then stack filters one by one
I try to have a query that joins the second table to do a search on that entity's name field
public DbSet<Partner> Partners { get; set; }
...
var data = _context.Partners.AsQueryable();
if (partnersSearch.SearchById != null)
{
data = data.Where(p => p.Id == partnersSearch.SearchById.GetValueOrDefault());
}
if (partnersSearch.SearchByName != null)
{
data = data.Include(a => a.Company.Select(b => b.Name = partnersSearch.SearchByName));
but for the join between the tables, the last line cannot compile
it complains that Company does not contain a definition of has no Select
what am I doing wrong ?
thanks for helping me on this
If you try a where after your include. Does that help?
data.Include(a => a.Company).Where(partner=>partner.Company.Name.equals(partnersSearch.SearchByName))

Many to Many - Entity Framework Core Where clause

I have a complex model, in which I am building a system which has the following model:
Competition
- Id
- Name
Team
- Id
- Name
CompetitionTeam
- Competition
- Team
Player
- Id
- Name
- Age
TeamPlayers
- Team
- Player
Essentially the team can enter many competitions, and can share players in different competitions.
I am trying to query from the TeamPlayers table to get the competition in which they are competing for said team:
var players = await _context.Set<TeamPlayer>()
.Include(tp => tp.Player)
.Include(tp => tp.Team)
.ThenInclude(t => t.CompetitionTeam)
.Where(tp.TeamId == teamId && t.CompetitionId == competitionId)
.OrderBy(tp.TeamId)
.ToListAsync(ct);
This is essentially what I want to achieve, but obviously it doesn't run. Can anyone suggest where I am going wrong?
Many to many relationships are typically cases where you will want navigation properties on both sides.
For instance:
public class Competition
{
// ...
public virtual ICollection<CompetitionTeam> CompetitionTeams { get; set; } = new List<CompetitionTeam>();
}
public class Team
{
// ...
public virtual ICollection<CompetitionTeam> CompetitionTeams { get; set; } = new List<CompetitionTeam>();
public virtual ICollection<TeamPlayer> TeamPlayers { get; set; } = new List<TeamPlayer>();
}
public class CompetitionTeam
{
public virtual Competition Competition { get; set; }
public virtual Team Team { get; set; }
}
public class Player
{
// ...
public virtual ICollection<TeamPlayer> TeamPlayers { get; set; } = new List<TeamPlayer>();
}
public class TeamPlayer
{
public virtual Team Team { get; set; }
public virtual Player Player { get; set;}
}
From there you can get the players through the relationships.
Now in your example if you have the Team ID you really just need to select the players in that team, and assert that the team was actually in the competition:
bool teamWasInCompetition = context.Competitions
.Any(c => c.CompetitionId == competitionId
&& c.CompetionTeams.Any(t => t.TeamId == teamId));
IList<Player> players = teamWasInComeption
? context.Teams.Where(t => t.TeamId == teamId).SelectMany(t => t.Players.Select(p => p.Player)).ToList()
: new List<Player>();
The first statement asserts that the Team actually took part in the competition. If it didn't, the second statement to get players returns an empty list. Otherwise, the second statement finds the Team by ID and then selects the players through the Team.TeamPlayers collection.
If you need those players to include other relationships you can add .Include() statements, or better, project the properties into a view model or such using .Select(). Note that with EF you do not need to explicitly use .Include() like a JOIN in SQL in order to reference related entities in a Linq query, only if you actually want to pre-fetch that data (eager load) in the returned entities.

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

Many to Many between IdentityUser and other table in a separate context

I am having difficulty creating a join table relationship between my Identity Framework IdentityContext(the IdentityUser) and one of my other tables Let's call it Entry. The problem is, Entry is in an entirely separate context doing it's own thing as well.
What is the proper way to associate these two? Where do I define the Join Table in fluent api?
Right now, I am getting the following error.
The key {'ApplicationUserId'} contains properties in shadow state and is referenced by a relationship from 'ApplicationUser.ApplicationUserEntries' to 'ApplicationUserEntry.ApplicationUser'. Configure a non-shadow principal key for this relationship.
These are how my tables are defined.
public class ApplicationUser : IdentityUser
{
...
public virtual List<ApplicationUserEntry> ApplicationUserEntries { get; set; }
}
public class Entry
{
public int Id { get; set; }
...
public virtual List<ApplicationUserEntry> ApplicationUserEntries { get; set; }
}
And the join table as follows.
public class ApplicationUserEntry
{
public int ApplicationUserId { get; set; }
public ApplicationUser ApplicationUser { get; set; }
public int EntryId { get; set; }
public Entry Entry { get; set; }
}
For the IdentityContext I have just some generic setup for other properties
var users = modelBuilder.Entity<ApplicationUser>();
users.Property(u => u.Name).IsRequired().HasMaxLength(65);
users.Property(u => u.FirstName).HasMaxLength(32);
users.Property(u => u.LastName).HasMaxLength(32);
And in my GoalsContext I have some general setup for other unrelated stuff, and the join table defined for ApplicationUserEntry
// Entry Configuration
var entries = modelBuilder.Entity<Entry>();
entries.HasKey(e => e.Id);
entries.HasAlternateKey(e => new { e.MilestoneId, e.CategoryId, e.MetricId });
entries.Property(e => e.Value).IsRequired();
entries.Property(e => e.Locked).IsRequired().HasDefaultValue(false);
entries.ToTable("GoalsEntries");
// ApplicationUserEntry Join Table
modelBuilder.Entity<ApplicationUserEntry>()
.ToTable("GoalsApplicationUserEntry")
.HasKey(se => new { se.ApplicationUserId, se.EntryId });
modelBuilder.Entity<ApplicationUserEntry>()
.HasOne(se => se.ApplicationUser)
.WithMany(s => s.ApplicationUserEntries)
.HasForeignKey(se => se.ApplicationUserId);
modelBuilder.Entity<ApplicationUserEntry>()
.HasOne(se => se.Entry)
.WithMany(e => e.ApplicationUserEntries)
.HasForeignKey(se => se.EntryId);
Now I'm sure I'm obviously missing something but I can't figure out what. I've never attempted to create a many to many relationship between two tables that are defined in two different contexts... and not even sure if that's wise or not to do.
My ultimate goal is to be able to associate owners with Entry records, so they can only be modified by the owners, which I verify with Identity Framework.
Ideally I would just prefer a unidirectional relationship, so I can find the owner from the Entry, but I'm not intending to get a list of Entry by looking at the IdentityUser