Trying to construct a LINQ query that performs a simple inner join, groups the data and sums two of the columns. From the examples I've seen it looks fairly straightforward but I must have missed something along the way.
public class Employee
{
public int Id { get; set; }
public int Name { get; set; }
}
public class Inventory
{
public int Id { get; set; }
public int EmployeeId { get; set; }
public decimal OffSite { get; set; }
public decimal OnSite { get; set; }
}
public class InventoryTotal
{
public int EmployeeId { get; set; }
public string EmployeeName { get; set; }
public decimal EmployeeOffSite { get; set; }
public decimal EmployeeOnSite { get; set; }
}
The query I have created looks like this
var result = from a in db.Inventory
join b in db.Employee on a.EmployeeId equals b.Id
group new { a, b } by a.EmployeeId into c
select new InventoryTotal
{
EmployeeId = c.Key,
EmployeeName = c.Name,
EmployeeOffSite = c.Sum(d => d.a.OffSite),
EmployeeOnSite = c.Sum(d => d.a.OnSite)
};
One issue appears to be with the Name column, the only value I want to obtain from the join with Employee. I would like to understand how to properly access that column and better understand how to construct this query as a whole.
EmployeeName = c.Name is not valid, nor are a few other combination I've tried.
So you have two tables: Employees and Inventories. There is a one-to-many relation between these two: Every Employee has zero or more Inventories; every Inventory is the Inventory of exactly one Employee, namely the Employee that the foreign key EmployeeId refers to.
Requirement: from every Employee get his Id and Name, and the total of all his OffSite and OnSite inventories.
Since you are using entity framework, there are three methods to do this. One is to do the (Group-)Join yourself, the other is to let entity framework do the (Group-)Join, and finally, the most intuitive part is to use the virtual ICollection<Inventory.
Do the GroupJoin yourself
Whenever you have a one-to-many relation, like Schools with their Students, Customers with their Orders, or Employees with their Inventories, and you want to start at the "one" side, consider to use one of the overloads of Queryable.GroupJoin.
On the other hand, if you want to start on the "Many" side, if you want the Student with the School he attends, the Order with the Customer who placed the order, consider to use Queryable.Join
You want to fetch "Employees with (some information about) their Inventories, so we'll use a GroupJoin. I'll use the overload of GroupJoin with a parameter resultSelector, so we can specify what we want as result.
var inventoryTotals = dbContext.Employees.GroupJoin(dbContext.Inventories,
employee => employee.Id, // from every Employee take the primary key
inventory => inventory.EmployeeId, // from every Inventory take the foreign key
// parameter resultSelector: from every Employee, and all Inventories that have a foreign
// key that refers to this Employee, make one new
(employee, inventoriesOfThisEmployee) => new InventoryTotal
{
EmployeeId = employee.Id,
EmployeeName = employee.Name,
EmployeeOffSite = inventoriesOfThisEmployee
.Select(inventory => inventory.OffSite).Sum(),
EmployeeOnSite = inventoriesOfThisEmployee
.Select(inventory => inventory.OnSite).Sum(),
});
Let Entity Framework do the GroupJoin
This one feels a bit more natural, for every Employee we Select one InventoryTotal, as requested.
var inventoryTotals = dbContext.Employees.Select(employee => new InventoryTotal
{
// Select the Employee properties that you want.
EmployeeId = employee.Id,
EmployeeName = employee.Name,
// Get the inventories of this Employee:
EmployeeOffSite = dbContext.Inventories
.Where(inventory => inventory.EmployeeId == employee.Id)
.Select(inventory => inventory.OffSite).Sum(),
EmployeeOnSite = dbContext.Inventories
.Where(inventory => inventory.EmployeeId == employee.Id)
.Select(inventory => inventory.OnSite).Sum(),
});
Use the virtual ICollections
This one feels the most natural. It is also very easy to unit test your usage without a real database.
If you've followed the entity framework conventions, you will have classes similar to:
public class Employee
{
public int Id { get; set; }
public int Name { get; set; }
... // other properties
// Every Employee has zero or more Inventories (one-to-many)
public ICollection<Inventory> Inventories {get; set;}
}
public class Inventory
{
public int Id { get; set; }
public decimal OffSite { get; set; }
public decimal OnSite { get; set; }
... // other properties
// Every Inventory is the Inventory of exactly one Employee, using foreign key
public int EmployeeId { get; set; }
public virtual Employee Employee {get; set;}
}
This is enough for entity framework to detect the tables, the columns of the tables and the relations with the tables (one-to-many, many-to-many, ...). Only if you want to deviate from the conventions: different identifiers for tables and columns, non-default column types etc Attributes or fluent API is needed.
In Entity framework the columns of the tables are represented by the non-virtual properties. The virtual properties represent the relations between the tables.
The foreign key is a column in the table, hence it is non-virtual. The Inventory has no Employee column, hence property Employee is virtual.
Once you've defined the virtual ICollection, the query is simple:
Requirement: from every Employee get his Id and Name, and the total of all his OffSite and OnSite inventories.
var inventoryTotals = dbContext.Employees.Select(employee => new InventoryTotal
{
// Select the Employee properties that you want.
EmployeeId = employee.Id,
EmployeeName = employee.Name,
EmployeeOffSite = employee.Inventories
.Select(inventory => inventory.OffSite).Sum(),
EmployeeOnSite = employee.Inventories
.Select(inventory => inventory.OnSite).Sum(),
});
Simple comme bonjour!
You have to add Name to grouping key:
var result = from a in db.Inventory
join b in db.Employee on a.EmployeeId equals b.Id
group a by new { a.EmployeeId, a.Name } into c
select new InventoryTotal
{
EmployeeId = c.Key.EmployeeId,
EmployeeName = c.Key.Name,
EmployeeOffSite = c.Sum(d => d.OffSite),
EmployeeOnSite = c.Sum(d => d.OnSite)
};
This is the EF CORE setup I have at the moment.
Telephone model
[ForeignKey("TelNoTypeID")]
public int? TelNoTypeID { get; set; }
public DropDown TelNoType { get; set; }
Person model
public ICollection<Telephone> TelephoneIDs { get; set; }
I've also tried using
public virtual ICollection<Telephone> TelephoneIDs { get; set; }
DropDown model
public int DropDownID { get; set; }
public string DisplayText { get; set; }
Person controller
public async Task<IActionResult> Details(int? id)
{
var person = await _context.Persons
.Include(p => p.Gender)
.Include(p => p.Title)
.Include(p => p.AddressIDs)
.Include(p => p.TelephoneIDs)
.SingleOrDefaultAsync(m => m.PersonID == id);
So a collection of Telephone numbers are stored against the Person model.
This brings back from the database and displays on the screen the numeric value from the field
Telephones.TelNoTypeID
The equivalent SQL to what I have is
select Telephones.TelNoTypeID from Person
inner join Telephones on Telephones.PersonID = Person.PersonID
I need to display the linked text value instead which is stored in
DropDown.displaytext
The equivalent SQL to what I need is
select DropDown.displaytext from Person
inner join Telephones on Telephones.PersonID = Person.PersonID
inner join DropDown on DropDown.DropDownID = Telephones.TelNoTypeID
I've tried using
.ThenInclude(p => p.<something>)
but the Dot notation just refers to the generic collection properties/methods, not back to my person, dropdown or telephone models
I've linked the Person model directly to the Dropdown model using and displaying the dropdown.displaytext with
Person model
[ForeignKey("GenderID")]
public int? GenderID { get; set; }
public virtual DropDown Gender { get; set; }
but despite putting the same code into telephone, it's not working
How can I link a third model (DropDown) to a second model (Telephone)?
Thanks
I was trying to link to the DropDown model, turns out I needed to link to the property, so
var person = await _context.Persons
.Include(p => p.Gender)
.Include(p => p.Title)
.Include(p => p.AddressIDs)
.Include(p => p.TelephoneIDs)
.ThenInclude(p => p.TelNoType)
//.Include("TelephoneIDs.TelNoType.DisplayText")
.SingleOrDefaultAsync(m => m.PersonID == id);
I don't understand why, I'm presuming it's because the links already exist between the 3 models and the ThenInclude points to the exact property I'm trying to link with/to.
I have a Mentorship entity, which has Student and Mentor as FKs:
[Required]
public int MentorId { get; set; }
public virtual User Mentor { get; set; }
[Required]
public int StudentId { get; set; }
public virtual User Student { get; set; }
User model:
public virtual ICollection<Mentorship> Mentorships { get; set; }
Fluent API:
modelBuilder.Entity<Mentorship>()
.HasRequired(c => c.Mentor)
.WithMany()
.HasForeignKey(u => u.MentorId);
modelBuilder.Entity<Mentorship>()
.HasRequired(c => c.Student)
.WithMany()
.HasForeignKey(u => u.StudentId);
In my database, I see StudentId and MentorId columns which have been populated correctly, but I also see a User_UserId column that is not being used by anything. What have I done wrong?
You have used the WithMany() overload that configures the relationship to be required:many without a navigation property on the other side of the relationship - but you do have a navigation property on the other side of the relationship.
Try this:
modelBuilder.Entity<Mentorship>()
.HasRequired(c => c.Mentor)
.WithMany(d => d.Mentorships)
.HasForeignKey(u => u.MentorId);
modelBuilder.Entity<Mentorship>()
.HasRequired(c => c.Student)
.WithMany(d => d.Mentorships)
.HasForeignKey(u => u.StudentId);//oops! just realised that we just
//specified that Mentorships is using MentorId
//as the FK
References:
Required WithMany Method
Why do I get an extra foreign key?
Edit
Scratch that. Just realised you are trying to create two relationships with only one navigation property on the many side. You can't have a navigation property with 2 foreign keys. You need to introduce inheritance on the User side or remove the Mentorships navigation property from the User class or introduce separate StudentMentorships and MentorMentorships navigation properties
Edit 2
Finding a user once you've defined separate navigation properties
int userId = 123;//the one we want to find
var user = Users.Where(x => x.StudentMentorships.Any(s => s.StudentID == userId)
|| x.MentorMentorships.Any(s => s.MentorID == userId);
I am trying to figure out how to make this work in EF. I have two entities Employee and User. I need to make it so the User has an optional mapping to the Employee. If there is a mapping, then it would be 1:1.
Basically this system can be accessed by employees and also by outside vendors, but I want one table to manage logons: Users.
How do I define this realtionship in EF with fluid configurations?
You just need to set simple fluent configuration:
modelBuilder.Entity<User>()
.HasOptional(u => u.Employee)
.WithRequired(e => e.User);
or in reverse order:
modelBuilder.Entity<Employee>()
.HasRequired(e => e.User)
.WithOptional(u => u.Employee);
EF will use Employess's PK as FK to user - that is mandatory requirement for EF to correctly use one-to-one relation. In case of Data annotation it is enough to mark Employee's PK with ForeignKey attribute pairing it with User navigation property:
public class Employee {
[Key, ForeignKey("User")]
public int Id { get; set; }
public virtual User User { get; set; }
}
public class User
{
...
public virtual Employee Employee { get; set; }
}
This tutorial may help.
I have two POCOs participating in a many-to-many relationship. "A" has a collection of "B" but there is no need for "B" to have a collection of "A". When I delete "B" the records in the Join table are not removed. It appears that Entity Framework code-first only deletes the Join records if there is a Navigation property. Is this correct or is there another way?
Example:
public class User() {
public int Id { get; set; }
public string Name { get; set; }
public List<Role> Roles { get; set; }
}
public class Role() {
public int Id { get; set; }
public string Name { get; set; }
}
//... Mapping Config ...//
this.HasMany(x => x.Roles)
.WithMany(/*can't be expressed without navigation property*/)
.Map(m => {
m.MapLeftKey("Users_Id");
m.MapRightKey("Roles_Id");
m.ToTable("UserRoleLinks");
});
//... Deleting a Role that is in use ...//
using(var ctx = new MyDbContext()) {
var role = ctx.Roles.Find(1);
ctx.Roles.Remove(role);
ctx.SaveChanges();
}
In this scenario, the UserRoleLinks records will be orphaned when the Role is deleted. Mabye there is a different way to configure it?
This should work without any problem. Default ManyToManyCascadeDeleteConvention will force EF to create junction table with relations using cascade deletes. Missing navigation property in Role entity has no impact on that.
So possible reasons for your problem:
You removed mentioned convention
You are missing cascade deletes on relations in junction table - this can for example happend when using Fluent API with existing database.