Complex graphing in entity framework - c#

I can't seem to find the correct where clause to get only the items I need.
I have Divisions, these contain Categories en these contain Items.
These are the classes:
public class Division {
public string Description { get; set; }
public List<Category> Categories { get; set; }
}
public class Category : IdEntity
{
public string Description { get; set; }
public Guid DivisionId { get; set; }
public List<Item> Items { get; set; }
}
public class Item
{
public Guid CategoryId { get; set; }
public DateTime Date { get; set; }
public string Description { get; set; }
}
What I need is a division with the Id in a parameter, all the categories from this division and the items that have a certain date for each category.
So right now I do this:
public Division GetFullDivisionByIdAndDate(Guid id, DateTime date)
{
using (new ChangeTrackingScope(ChangeTracking.Disabled))
{
var divisionGraph = new Graph<Division>().Include(d => d.Categories.Select(c => c.Items));
var division = _divisionDL.GetFullDivisionByIdAndDate(id, divisionGraph, date);
return division;
}
}
And than in the DL I do
public Division GetFullDivisionByIdAndDate(Guid id, Graph<Division> graph, DateTime date)
{
using (var db = new ContextScope<DatabaseContext>())
{
var q = graph.ApplySetReferences(db.Context.Divisions).AsNoTracking();
return q.SingleOrDefault(p => p.Id == id);
}
}
Here I get the division with all its categories (so far so good) but I also get all items and I need only the items with the date given as parameter. Anyone has an idea how to do this?

Your code is not very accessible because of a few missing methods (Graph, ApplySetReferences) so I can't hook into it. But I can show a common way to query an object graph, which is by navigation properties. In you model, a basic query body could look like this:
from d in Divisions
from c in d.Categories
from i in c.Items
select new { Div = d.Description, Cat = c.Description, Item = i.Description }
Starting from here you can add other filters and properties, like
from d in Divisions.Where(div => div.Id == id)
from c in d.Categories
from i in c.Items.Where(item => item.Date == date)
select new { ... }
Or if you want a result with items in a filtered collection:
from d in Divisions.Where(div => div.Id == id)
from c in d.Categories
select new new { Div = d.Description,
Cat = c.Description,
Items = c.Items.Where(item => item.Date == date)
}

Related

Calculated property for model EF Core - Property or Method?

I am new to EF core. I have a Customer model with the usual properties (Name,Address,Email).
I need a property to calculate the current balance for the customer.
This will be quite an intensive computation (once many records are stored) so am I correct in thinking that it should be stored in a Method, rather than a calculated property?
I am assuming I need to add a method such as .GetCurrentBalance().
Where would I put this method?
Simplified code below:
My Customer Model
public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; }
public string EmailAddress { get; set; }
public virtual IEnumerable<SalesInvoice> SalesInvoices{ get; set; }
}
My Sales Invoice Model
public class SalesInvoice
{
public int SalesInvoiceId { get; set; }
public int CustomerId { get; set; }
public virtual IEnumerable<SalesInvoiceDetail> SalesInvoiceDetails{ get; set; }
}
My Sales Invoice Detail Model
public class SalesInvoiceDetail
{
public int SalesInvoiceDetailId { get; set; }
public int Qty { get; set; }
public decimal UnitPrice { get; set; }
}
Create helper methods which returns desired results. Everything should play around IQueryable:
public class CustomerIdWithBalance
{
public int CustomerId { get; set; }
public decimal Balance { get; set; }
}
public class CustomerWithBalance
{
public Customer Customer { get; set; }
public decimal Balance { get; set; }
}
public static class BusinessLogicExtensions
{
public static IQueryable<CustomerIdWithBalance> GetCustomerIdAndBalance(this IQueryable<Customer> customers)
{
var grouped =
from c in customers
from si in c.SalesInvoices
from sid in si.SalesInvoiceDetails
group sid by new { c.CustomerId } into g
select new CustomerIdWithBalance
{
g.Key.CustomerId,
Balance = x.Sum(x => x.Qty * x.UnitPrice)
}
return grouped;
}
public static IQueryable<CustomerWithBalance> GetCustomerAndBalance(this IQueryable<CustomerIdWithBalance> customerBalances, IQueryable<Customer> customers)
{
var query =
from b in customerBalances
join c in customers on b.CustomerId equals c.CustomerId
select new CustomerWithBalance
{
Customer = c,
Balance = b.Balance
};
return query;
}
}
Later when you need to return that with API call (hypothetic samples)
var qustomerIdsWithHighBalance =
from c in ctx.Customers.GetCustomerIdAndBalance()
where c.Balance > 1000
select c.CustomerId;
var qustomersWithHighBalance =
ctx.Customers.GetCustomerIdAndBalance()
.Where(c => c.Balance > 1000)
.GetCustomerAndBalance(ctx.Customers);
var customersByMatchAndPagination = ctx.Customers
.Where(c => c.Name.StartsWith("John"))
.OrderBy(c => c.Name)
.Skip(100)
.Take(50)
.GetCustomerAndBalance(ctx.Customers);
You will get desired results without additional database roundtrips. With properties you may load too much data into the memory.
It is everything about using EF with its limitations. But world is not stopped because EF team is too busy to create performance effective things.
Let's install https://github.com/axelheer/nein-linq
And create extension methods around Customer
public static class CustomerExtensions
{
[InjectLambda]
public static TotalBalance(this Customer customer)
=> throw new NotImplmentedException();
static Expression<Func<Customer, decimal>> TotalBalance()
{
return customer =>
(from si in customer.SalesInvoices
from sid in si.SalesInvoiceDetails
select sid)
.Sum(x => x.Qty * x.UnitPrice));
}
}
And everything become handy:
var customersWithHighBalance =
from c in ctx.Customers.ToInjectable()
where c.TotalBalance() > 1000
select c;
var customersWithHighBalance =
from c in ctx.Customers.ToInjectable()
let balance = c.TotalBalance()
where balance = balance > 1000
select new CustomerWithBalance
{
Customer = c,
Balance = balance
};
var customersWithBalance =
from c in ctx.Customers.ToInjectable()
where c.Name.StartsWith("John")
select new CustomerWithBalance
{
Customer = c,
Balance = c.TotalBalance()
};
var paginated =
.OrderBy(c => c.Name)
.Skip(100)
.Take(50);
If you would prefer to calculate at property level. Add an InvoiceBal readonly field to the SalesInvoice model.
public class SalesInvoice
{
public double InvoiceBal => SalesInvoiceDetails.Sum(x => x.Qty * x.UnitPrice)
public virtual IEnumerable<SalesInvoiceDetail> SalesInvoiceDetails{ get; set; }
}
Add another TotalBalance readonly field to the Customer that sums the whole thing
public class Customer
{
public double TotalBal => SalesInvoices.Sum(x => x.InvoiceBal)
public virtual IEnumerable<SalesInvoice> SalesInvoices{ get; set; }
}

Why is my linq query adding the same item to a list instead of all items?

I am working on a project at work and am attempting to get all classes for a student from our database, however the same class keeps getting added to the list instead of all of the classes. I have practically the same query relating to another table which gets the classes of all professors, which for some reason works.
The entries in the database are not duplicates.
Here is the code I'm having issues with:
semester = "SP";
year = "19;
id = "0000001";
var tempCourses = (from b in db2.StuEnrolls where b.studentID == id && b.semester == semester && b.year == year orderby b.courseID select b).ToList();
I've also tried this:
var tempCourses = (from b in db2.StuEnrolls
where b.studentID == id && b.semester == semester && b.year == year
orderby b.courseID
select new StuEnrolls
{
studentID = b.studentID,
courseID = b.courseID,
year = b.year,
semester = b.semester,
dept = b.dept,
course = b.course,
section = b.section,
sectionTitle = b.sectionTitle,
courseGrade = b.courseGrade,
courseCredits = b.courseCredits
}).ToList();
Here is the similar code for professors that works:
var tempCourses = (from b in db2.CourseSectionsInstructor where b.instructorID == id orderby b.courseID select b).ToList();
As of now, I am getting 5 courses, all the same entry from the database. I should however be getting 5 unique entries. Here is an example of the output. The studentID has been edited out.
The DatabaseStructure looks like so:
public partial class StuEnrolls
{
public string studentID_courseID
{
get
{
return studentID + " " + courseID;
}
}
[Key]
public string studentID { get; set; }
public string courseID { get; set; }
public string year { get; set; }
public string semester { get; set; }
public string dept { get; set; }
public string course { get; set; }
public string section { get; set; }
public string sectionTitle { get; set; }
public string courseGrade { get; set; }
public decimal courseCredits { get; set; }
}
The table in the database is made up of:
Table Design
The problem seems to have had something to do with the table having two primary keys, and the Database Structure only having one key.
This code seems to have fixed the issue:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<StuEnrolls>().HasKey(k => new { k.studentID, k.courseID });
}

Nested foreach loops to LINQ

Is it possible to rewrite this code to LINQ? I'm new to linq and it seems difficult to understand.
foreach (Employee employee in EmployeeList)
{
Earnings earnings = new Earnings(employee.Name, employee.LastName, employee.Bank, employee.Account);
if (!EarningsList.Contains(earnings))
{
EarningsList.Add(earnings);
}
foreach (DaysData item in ProductList)
{
foreach (Product product in item.Products)
{
if (product.EmployeeName == employee.Name && product.EmployeeLastName == employee.LastName)
{
double money = product.Count * product.Price;
earnings.AddMoney(money);
}
}
}
}
The first part isn't so easy to convert because of the conditional EarningsList.Add()
But You can rewrite the last 2 rather easily.
Assuming that AddMoney() does just what it says, you can use Sum(). Otherwise, omit the Sum() and run a separate foreach on the list of amounts. That would make it a lot less Linq.
var amount = ProductList
.SelectMany(item => item.Products)
.Where(product => product.EmployeeName == employee.Name && product.EmployeeLastName == employee.LastName)
.Sum(product => product.Count * product.Price)
;
earnings.AddMoney(amount);
Not knowing exactly what the .AddMoney method does you could do this:
var query =
from employee in EmployeeList
let earnings = new Earnings(employee.Name, employee.LastName, employee.Bank, employee.Account)
from item in ProductList
from product in item.Products
where product.EmployeeName == employee.Name
where product.EmployeeLastName == employee.LastName
group product.Count * product.Price by earnings;
List<Earnings> EarningsList =
query
.Aggregate(new List<Earnings>(), (a, x) =>
{
a.Add(x.Key);
foreach (var y in x)
{
x.Key.AddMoney(y);
}
return a;
});
If .AddMoney simply adds the money arithmetically then you could do this:
List<Earnings> EarningsList =
query
.Aggregate(new List<Earnings>(), (a, x) =>
{
a.Add(x.Key);
x.Key.AddMoney(x.Sum());
return a;
});
Just a small note. You're using double to represent money. It's best to use decimal as this will help prevent rounding errors in your calculations.
I think this is what you want if you just used LINQ(no foreach). This should be compatible with IQueryable as well and you really don't want to do foreach on IQueryable.
var newEarnings = from employee in EmployeeList
select new Earnings
{
Name = employee.Name,
LastName = employee.LastName,
Bank = employee.Bank,
Account = employee.Account,
Money = (from daysData in ProductList
from product in daysData.Products
where employee.Name == product.EmployeeName && employee.LastName == product.EmployeeLastName
select product).Sum(p => p.Count * p.Price)
};
EarningsList = EarningsList.Union(newEarnings).ToList();
Now in regards to normalization. My guess is that you made you POCO models like this in order to show it in some sort of a grid. You really should not let your UI dictate what you data models look like. There can be others reasons to do this but they are related to performance and I don't think you need to worry about this just jet. So here is my advice on how to change this.
Add Id property to all the classes. This is always a good idea no
matter what you are doing. This can be a random string or an auto
increment, just to have a unique value so you can play with this
object.
Add reference properties in you classes. Don't copy the values from
Employee to Product and Earnings. Just add a Property of type
Employee and/or add the EmployeeId property
So your POCO should look something like this
public class Employee
{
//It can be a Guid, string or what ever. I am not nesseserly recomending using Guids and you should look into this a bit more
public Guid Id { get; set; }
public string Name { get; set; }
public string LastName { get; set; }
public string Bank { get; set; }
public string Account { get; set; }
}
public class DaysData
{
public Guid Id { get; set; }
public IEnumerable<Product> Products { get; set; }
}
public class Product
{
public Guid Id { get; set; }
public Guid EmployeeId { get; set; }
public Employee Employee { get; set; }
public double Count { get; set; }
public double Price { get; set; }
}
public class Earnings
{
public Guid Id { get; set; }
public Guid EmployeeId { get; set; }
public Employee Employee { get; set; }
public double Money { get; set; }
}
And the query
var newEarnings = from employee in EmployeeList
select new Earnings
{
Id = Guid.NewGuid(),
EmployeeId = employee.Id,
Employee = employee,
Money = (from daysData in ProductList
from product in daysData.Products
where employee.Id == product.EmployeeId
select product).Sum(p => p.Count * p.Price)
};
Once you try to implement data persistence this will help you a lot no matter what you use EF, Mongo, Elsatic, AzureTables or anything else even a simple saving to files.

Getting Sum by looping a list within a list c#

public class GRNMaster
{
public string ID { get; set; }
public string GRNNo { get; set; }
public List<GRNDetails> GRNDetails { get; set; }
}
public class GRNDetails
{
public string GRNID { get; set; }
public string ItemID { get; set; }
public string ItemType { get; set; }
public int RecevedQty { get; set; }
}
above classes contains some of the properties of GRN header class and Detail class. i Grn can consist of many items so that "List GRNDetails" is there to keep them.
I take a GRN List from a method which will store in the variable GrnList
public List<GRNMaster> GrnList
I have a list of Items IDs
public List<string> ItemIDList
In the controller I want to loop the ItemIDList (List ItemIDList) and get sum for that particular item based on the List
int ItemQty = 0;
foreach (var item in ItemIDList)
{
ItemQty = 0;
var ItemQty = //some code using GrnList
// rest of the programming code based on the
// item qty
}
Using LINQ
var totalQty = 0;
foreach (var item in ItemIDList)
{
var sumOfCurrentItem = GrnList.SelectMany(s => s.GRNDetails
.Where(f => f.ItemID == item)).Select(f => f.RecevedQty).Sum();
totalQty += sumOfCurrentItem ;
}
Or even a one liner replacement of the foreach loop (Credit goes to ReSharper :) )
int totalQty = ItemIDList.Sum(item => GrnList
.SelectMany(s => s.GRNDetails.Where(f => f.ItemID == item))
.Select(f => f.RecevedQty).Sum());
If I've understood your requirements correctly then this is what you need:
IEnumerable<int> query =
from grn in GrnList
from grnDetail in grn.GRNDetails
join itemID in ItemIDList on grnDetail.ItemID equals itemID
select grnDetail.RecevedQty;
int ItemQty = query.Sum();

LINQ Query (Group BY) one column

Consider the following classes
public class DashboardTile
{
public int ID { get; set; }
public int? CategoryID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class DashboardTileBO : DashboardTile
{
public bool IsChecked { get; set; }
public List<DashboardTileBO> DashboardTiles { get; set; }
}
I have list of tiles in which some tiles are child of other.Now I want to show my list of tiles in such a way that if it has childs it gets added to the list.
query I am trying
var allDashBoardTiles = (from a in context.DashboardTiles
group a by a.CategoryID into b
select new BusinessObjects.DashboardTileBO
{
ID = a.ID,
Name = a.Name,
Description = b.Description,
DashboardTiles = b.ToList(),
}).ToList();
var list = context.DashboardUserTiles.Where(a => a.UserID == userId).Select(a => a.DashboardTileID).ToList();
allDashBoardTiles.ForEach(a => a.IsChecked = list.Contains(a.ID));
Now in above query when I use group clause and in select if I use a.ID,a.Name etc it says that it doesnot contain definitionor extension method for it.
Table
You can't access the properties of a directly because GroupBy returns IGrouping<TKey,T>. You can include other columns also in your group by and access them like this:-
(from a in context.DashboardTiles
group a by new { a.CategoryID, a.ID, a.Name } into b
select new BusinessObjects.DashboardTileBO
{
ID = b.Key.ID,
Name = b.Key.Name,
DashboardTiles = b.ToList(),
}).ToList();
Edit:
Also, I guess the property DashboardTiles in DashboardTileBO class should be List<DashboardTile> instead of List<DashboardTileBO>, otherwise we cannot fetch it from DashboardTiles data.

Categories