EF Linq query with join, group and sum - entity-framework

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

Related

Relationship in EF Core by ID and Type

I use EF Core (Code first) I need to make relation between 2 tables by ID and Type
the following are my classes
public class Lead : BaseEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public short Status { get; set; }
public short PhoneType { get; set; }
public string Phone { get; set; }
public short EmailType { get; set; }
public string Email { get; set; }
public string Notes { get; set; }
public List<AddressInformation> AddressInformations { get; set; }
}
public class Opportunity : BaseEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public List<AddressInformation> AddressInformations { get; set; }
}
public class AddressInformationViewModel
{
public int Id { get; set; }
public int SourceID { get; set; } // in this column i need to store ID for Lead or Oppurtunity
public string RelatedTo { get; set; } // in this column i need to store text "Lead" or "Oppurtunity"
public short AddresType { get; set; }
public string Description { get; set; }
}
The AddressInformation class will hold information for Leads or Opportunity based on SourceID and RelatedTo columns
how we can handle this relation?
when I make Data Migrations EF will add new column in Lead Table the column name "LeadID" and I don't need this approach, is there any way to handle like this relation.
I would suggest considering a many-to-many joining table for each relationship rather than effectively a SourceId + SourceType association within Address. I.e. using a LeadAddress table and an OpportunityAddress table. With EF Core 5 and EF 6 you can associate these without a joining entity, just mapping the joining table, or create a joining entity for earlier EF Core, or if you need additional columns in the relationship.
The main advantage of using specific linking tables is that you can maintain FK relationships. With a SourceId + SourceType you cannot use SourceId as a FK to both Lead and Opportunity, however with a joining table, the LeadId can FK to lead while AddressId can FK to address. This helps keep querying address related details efficient.
A benefit, or limitation of this approach to consider is that with linking tables an address can legally be assigned to both a Lead and an Opportunity, or shared between Leads /Opportunities. If you don't want to support addresses being shared across multiple entities you would need to implement checks to prevent it. This does mean treating an address as a distinct location rather than merely a data container for a particular related entity. For example 123 Smithe St. is always 123 Smithe St. To change an address for a Lead would typically mean associating it with a new Address object with different values rather than editing the values of 123 Smithe St. (Unless they actually mean to correct the address, I.e. 123 Smith St.)
SourceId + SourceType can be implemented, but AFAIK this would have to be handled as separate unrelated entities and joined manually with queries, I.e. something like:
var query = context.Leads
.Join(context.Addresses.Where(a => a.RelatedTo == "lead"),
l => l.Id,
a => a.SourceId,
(l,a) => new {Lead = l, Addresses = a})
.Single(x => x.Lead.Id == leadId);
As queries get more involved this gets more complex to deal with the Join, and AFAIK you won't get something as useful as lead.Addresses out of the mapping/navigation properties where you can have lead.Addresses or at least lead.LeadAddresses using the dedicated linking table.

How do I get multi-mapping to work in Dapper?

I am trying to do the following with Dapper (and failing).
My POCOs (all code simplified) are:
public class Company
{
public int CompanyId { get; private set; }
public string CompanyName { get; private set; }
public Person CompanyAddress { get; private set; }
public Person Administrator { get; private set; }
}
public class Person
{
public int PersonId { get; private set; }
public string FirstName { get; private set; }
public string LastName { get; private set; }
}
In the database the Company table has a FK for CompanyAddress and Administrator which maps to the PersonID PK in the Person table. Based on this and this I think the follwoing is how I want to do this:
public static Company Select(IDbConnection connection, int id)
{
Trap.trap();
return connection.Query<Company, Person, Person, Company>("select * from Company left join Person address on Company.CompanyAddress = address.PersonId left join Person admin on Company.Administrator = admin.PersonId where Company.CompanyId = #Id",
(cmpy, addr, admin) => new { PersonId = id }).FirstOrDefault();
}
But that gives me a compile error on the "new { PersonId = id }". What am I getting wrong?
You need to provide the SplitOn parameter to specify where the next table/class starts. You also shouldn't create an anonymous type but use a new scope to initialize the Administrator property of Company:
string sql = #"select c.CompanyId,c.CompanyName, c.CompanyAddress,
address.PersonId, etc. ....
from Company c
left join Person address
on Company.CompanyAddress = address.PersonId
left join Person admin
on Company.Administrator = admin.PersonId
where Company.CompanyId = #Id";
string splitOn = "PersonId"; // maybe two parameters separated by comma, see comment below the answer
return connection.Query<Company, Person, Person, Company>(sql,
(Company cmpy, Person addr, Person admin) => { cmpy.Administrator = admin; return cmpy; }
,null,null,true,splitOn)
.FirstOrDefault();
However, i'm not sure if that works already since you have two joins to the same table. So i think you need an alias for all of the duplicate columns like PersonId. But this migt be helpful anyway.

EF5 List of one to one dont materialized every entities

I am using EF5.0 CF let's consider theses entities (simplified here):
public class Catalog
{
public int Id { get; set; }
public bool IsActive { get; set; }
public string Name { get; set; }
public ICollection<PricedProduct> Products { get; set; }
}
public class PricedProduct
{
public int Id { get; set; }
public bool IsActive { get; set; }
public Product Product { get; set; }
public Price Price { get; set; }
}
public class Price
{
public int Id { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name {get; set;}
}
They are configured with the fluent API :
//For the Catalog entity
ToTable("Catalog", "Catalog");
this.Property(t => t.Name).HasColumnType("nvarchar").HasMaxLength(100).IsRequired();
this.HasMany<PricedProduct>(t => t.Products).WithMany().
Map(mc =>
{
mc.ToTable("CatalogPricedProduct", "Catalog");
mc.MapLeftKey("PricedProductID");
mc.MapRightKey("CatalogID");
});
//For the PricedProduct entity
ToTable("PricedProducts", "Catalog");
HasRequired(t => t.Product).WithOptional().Map(m=>m.MapKey());
HasRequired(t => t.Price).WithOptional().Map(m => m.MapKey());
//For the Product entity
ToTable("Products", "Catalog");
this.Property(t => t.Name).HasColumnType("nvarchar").HasMaxLength(100).IsRequired();
//For the Price entity
ToTable("Prices", "Catalog");
So basically I have a catalog which have n:n relationship with PricedProduct that have two 1:1 relationship with Product and Price
I get those entities with this linq query :
var qy = from cata in this.Set<Catalog>().Include("Products")
.Include("Products.Product")
.Include("Products.Price")
where cata.Name == "name"
select cata;
return qy.FirstOrDefault();
Everything works well as long as two PricedProduct does not share the same product or the same price.
Meaning that, in the PricedProducts table the PriceProduct are retrieved and materialized correctly as long as the Product or the Price FK are "unique", if another PricedProduct have the same FK value on price for instance, price wont be loaded in the concerned PricedProduct.
I have quickly check the SQL query generated and it looks fine, it feels like EF fail to materialize two instances of the same object in a same graph ??
Anyone knows what to do or what is wrong with my code ?
thank a lot
That is because your understanding of your model is not correct. If multiple PricedProduct can have same Price or same Product you cannot map it as one-to-one relationship but as one-to-many (one price can be assigned to many priced products - same for product). You need:
ToTable("PricedProducts", "Catalog");
HasRequired(t => t.Product).WithMany();
HasRequired(t => t.Price).WithMany();

Query a pure join table created in Entity Framework Code First

I've created a pure join table out of the folowing two tables:
User:
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserID { get; set; }
public string UserName { get; set; }
public String FirstName { get; set; }
public String LastName { get; set; }
public virtual ICollection<CrRole> Roles { get; set; }
Role:
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int RoleID { get; set; }
public String RoleName { get; set; }
public virtual ICollection<CrUser> Users { get; set; }
Fluent API to create the UserRole join Table:
modelBuilder.Entity<CrUser>()
.HasMany(c => c.Roles)
.WithMany(i => i.Users)
.Map(t => t.MapLeftKey("UserID")
.MapRightKey("RoleID")
.ToTable("CrUserRole"));
This works fine, and creates the correct CrUserRole table with UserID and RoleID columns.
I am having great difficulty in trying to work out how to do a simple query against this join table though. In SQL the command would be:
SELECT COUNT(*)
FROM UserRole ur
WHERE ur.RoleID = #RoleID
AND ur.UserID = #UserID
I've tried going through the navigation links of either the Role or User entities, but always seem to end up with a load of spaghetti code which refuses to compile :-(
I'd be very grateful if someone could point me in the right direction. Even some tutorials would help, the ones I've found only seem to only go as far as creating the join table and not include CRUD operations.
Thanks!
notice your mapping:
t.MapLeftKey("UserID")
.MapRightKey("RoleID")
.ToTable("CrUserRole"));
this mapping shows that your query's result will always be 1 or 0.
The proper way to do this IMO is as follows:
You have both user id and role id right?
First get the user, then query that specific user's roles. Something like:
var user = ctx.Users.FirstOrDefault(u => u.UserID == myUserId);
var role = user.Roles.FirstOrDefault(r => r.RoleId = myRoleId);
Thanks Kaymar.
The version I eventually used was:
String[] returnUsers = new String[1];
var role = context.Roles.First(r=>r.RoleName.Equals(roleName));
returnUsers = role.Users.Select(s => s.UserName).ToArray();
Thanks for getting me in the right direction, and apologies for taking months to update this!

How to modify this Entity framework code first code?

I am brand new to Entity Framework code first, so any help or direction would be much appreciated.
I currently have the following classes:
public partial class Customer
{
public int Id { get; set; }
private ICollection<Address> _addresses;
}
public partial class Address
{
public int Id { get; set; }
public string Street { get; set; };
public string City { get; set; };
public string Zip { get; set; };
}
and the following
public partial class CustomerMap : EntityTypeConfiguration<Customer>
{
public CustomerMap()
{
this.ToTable("Customer");
this.HasKey(c => c.Id);
this.HasMany<Address>(c => c.Addresses)
.WithMany()
.Map(m => m.ToTable("CustomerAddresses"));
}
}
This works as I would expect, and creates a Customer, Address and CustomerAddresses table for the mapping. Now for my question.. what would I do if I needed to modify the code to produce the following...
I want to add a CompanyCode attribute to the "CustomerAddresses" table... and then instead of constructing a collection of addresses.. i want to be able to construct a hashtable, where the key is the CompanyCode, and the value is the collection of addresses.
So if I had the following:
Customer
ID C1
Address
ID A1
ID A2
CustomerAddresses
CustomerID C1
AddressID A1
CompanyCode ABC
CustomerID C1
AddressID A2
CompanyCode ABC
CustomerID C1
AddressID A2
CompanyCode XYZ
so then, Customer.Addresses["ABC"] would return a collection of addresses with ID, A1 and A2. Whereas Customer.Addresses["XYZ"] would return a collection of addresses with ID A2.
Any direction/help would be much appreciated... thanks.
As far as I can tell it isn't possible to introduce such a navigation property with an indexer. Your indexer is actually a query and you must express this as a query. The only way I see is that you leave the navigation collection as is and introduce a second (not mapped) property that uses the navigation collection for the filter. The big drawback is that such a filter would happen in memory with LINQ-to-Objects and requires that you always load the full collection first from the database (by eager or lazy loading for example) before you filter the collection.
I would probably leave such a filter out of the entity itself and implement it in a repository or service class or generally the place/module where you load the entities from the database.
The first thing you need to do is exposing the CustomerAddresses table as an entity in your model because with your additional custom property CompanyCode you can't use a many-to-many relationship anymore, instead you need two one-to-many relationships. The new entity would look like this:
public partial class CustomerAddress
{
public int CustomerId { get; set; }
// public Customer Customer { get; set; } // optional
public int AddressId { get; set; }
public Address Address { get; set; }
public string CompanyCode { get; set; }
}
And the Customer needs to be changed to:
public partial class Customer
{
public int Id { get; set; }
public ICollection<CustomerAddress> CustomerAddresses { get; set; }
}
You need to change the mapping to:
public CustomerMap()
{
this.ToTable("Customer");
this.HasKey(c => c.Id);
this.HasMany(c => c.CustomerAddresses)
.WithRequired() // or .WithRequired(ca => ca.Customer)
.HasForeignKey(ca => ca.CustomerId);
}
And create a new mapping for the new entity:
public CustomerAddressMap()
{
this.ToTable("CustomerAddresses");
this.HasKey(ca => new { ca.CustomerId, ca.AddressId, ca.CompanyCode });
// or what is the PK on that table?
// Maybe you need an Id property if this key isn't unique
this.HasRequired(ca => ca.Address)
.WithMany()
.HasForeignKey(ca => ca.AddressId);
}
Now, in some service class you could load the filtered addresses:
public List<Address> GetAddresses(int customerId, string companyCode)
{
return context.CustomerAddresses.Where(ca =>
ca.CustomerId == customerId && ca.CompanyCode == companyCode)
.ToList();
}
Or, if you want to load the customer together with the filtered addresses:
public Customer GetCustomer(int customerId, string companyCode)
{
var customer = context.Customer.SingleOrDefault(c => c.Id == customerId);
if (customer != null)
context.Entry(customer).Collection(c => c.CustomerAddresses).Query()
.Where(ca => ca.CompanyCode == companyCode)
.Load();
return customer;
}
The last example are two database queries.
In the Customer entity you could use a helper property that projects the addresses out of the CustomerAddresses collection:
public partial class Customer
{
public int Id { get; set; }
public ICollection<CustomerAddress> CustomerAddresses { get; set; }
public IEnumerable<Address> Addresses
{
get
{
if (CustomerAddresses != null)
return CustomerAddresses.Select(ca => ca.Address);
return null;
}
}
}
Keep in mind that this property does not query the database and the result relies on what is already loaded into CustomerAddresses.