I'm looking to fill an object model with the count of a linq-to-sql query that groups by its key.
The object model looks somewhat like this:
public class MyCountModel()
{
int CountSomeByte1 { get; set; }
int CountSomeByte2 { get; set; }
int CountSomeByte3 { get; set; }
int CountSomeByte4 { get; set; }
int CountSomeByte5 { get; set; }
int CountSomeByte6 { get; set; }
}
This is what I have for the query:
var TheQuery = from x in MyDC.TheTable
where ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7
group x by x.SomeByte into TheCount
select new MyCountModel()
{
CountSomeByte1 = TheCount.Where(TheCount => TheCount.Key == 1)
.Select(TheCount).Count(),
CountSomeByte2 = TheCount.Where(TheCount => TheCount.Key == 2)
.Select(TheCount).Count(),
.....
CountSomeByte6 = TheCount.Where(TheCount => TheCount.Key == 6)
.Select(TheCount).Count(),
}.Single();
ListOfRecordIDs is list of longs that's passed in as a parameter. All the CountSomeByteN are underlined red. How do you do a count of grouped elements with the group's key mapped to an object model?
Thanks for your suggestions.
The select is taking each element of your group and projecting them to identical newly created MyCountModels, and you're only using one of them. Here's how I'd do it:
var dict = MyDC.TheTable
.Where(x => ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7)
.GroupBy(x => x.SomeByte)
.ToDictionary(grp => grp.Key, grp => grp.Count());
var result = new MyCountModel()
{
CountSomeByte1 = dict[1];
CountSomeByte2 = dict[2];
CountSomeByte3 = dict[3];
CountSomeByte4 = dict[4];
CountSomeByte5 = dict[5];
CountSomeByte6 = dict[6];
}
EDIT: Here's one way to do it in one statement. It uses an extension method called Into, which basically works as x.Into(f) == f(x). In this context, it can be viewed as like a Select that works on the whole enumerable rather than on its members. I find it handy for eliminating temporary variables in this sort of situation, and if I were to write this in one statement, it's probably how I'd do it:
public static U Into<T, U>(this T self, Func<T, U> func)
{
return func(self);
}
var result = MyDC.TheTable
.Where(x => ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7)
.GroupBy(x => x.SomeByte)
.ToDictionary(grp => grp.Key, grp => grp.Count())
.Into(dict => new MyCountModel()
{
CountSomeByte1 = dict[1];
CountSomeByte2 = dict[2];
CountSomeByte3 = dict[3];
CountSomeByte4 = dict[4];
CountSomeByte5 = dict[5];
CountSomeByte6 = dict[6];
});
Your range variable is not correct in the subqueries:
CountSomeByte6 = TheCount.Where(TheCount => TheCount.Key == 6)
.Select(TheCount).Count(),
In method notation you don't need the extra select:
CountSomeByte6 = TheCount.Where(theCount => theCount.Key == 6).Count(),
If you want to use it anyway:
CountSomeByte6 = TheCount.Where(theCount => theCount.Key == 6).Select(theCount => theCount).Count(),
Related
This works fine. But how do I customize the conditions for adding filters?input is the value entered by the user
var resultList = dbContext.BuyerBill
.Include(x=>x.BuyerBillItems.Where(x=>x.Status == input.Status && x.BuildTime > input.BeginTime && x.BuildTime < input.EndTime))
.ToList();
the way I want:
var query = WhereIf(input.Status!=null,x=>x.Status == input.Status);
query = WhereIf(input.BeginTime!=null,x=>x.BuildTime > input.BeginTime);
query = WhereIf(input.EndTime!=null,x=>x.BuildTime > input.EndTime);
this is my entity
public class BuyerBill
{
public BuyerBill()
{
BuyerBillItems = new List<BuyerBillItems>();
}
public int Id {get;set;}
public int BuyUserId {get;set;}
public int OrderId {get;set;}
public List<BuyerBillItems> BuyerBillItems { get; set; }
....
}
public class BuyerBillItems
{
public int Id {get;set;}
public int BuyerBillId {get;set;}
public decimal Fee {get;set;}
public int Status {get;set;}
public dateTime CreateTime {get;set;}
public BuyerBill BuyerBill {get;set;}
....
}
1、If the user does not select the time query
Select * from BuyerBill as buy inner join BuyerBillItems As item On buy.Id=item.BuyerBillId
where item.Status=1
2、If the user selects the time query
Select * from BuyerBill as buy inner join BuyerBillItems as item on buy.Id=item.BuyerBillId
where item.Status=1 and item.BuildTime > '2022-7-19' and item.BuildTime < '2022-7-19'
How to use efcore to implement the SQL conditions I described?
Mainly, I want to filter sub entities according to conditions. If there is only one entity, I know Where() method can be used for filtering, but I don't know how to use conditional filtering for sub entities
I solved the above problem with LinqKit, but now I have a new problem。
var predicatess = PredicateBuilder.New<BuyerBillItems>(true);
predicatess = predicatess.And(x => x.CreateTime > StringHelper.AddDateTime("2022-07-16"));
predicatess = predicatess.And(x => x.Status == 2);
//I'm dumb and this line of code seems redundant. But I don't know how to convert implicitly
var convertPredicate = (Expression<Func<BuyerBillItems, bool>>)predicatess;
var query = dbContext.BuyerBill.AsExpandable().Include(x => x.BuyerBillItems.Where(x => convertPredicate.Invoke(x)))
.Where(x => x.BuyerBillItems.Any(s => convertPredicate.Invoke(s)))
.Where(x => x.BuyUserId == 4);
//If you don't use Select, everything is normal
var result1 = query.ToList();
//BuyerBillItemsDto result is incorrect after using Select
var result2 = query.Select(x => new BuyerBillDto
{
Id = x.Id,
BuyUserId = x.BuyUserId,
OrderId = x.OrderId,
BuyerBillItemsDto = mapper.Map<List<BuyerBillItems>, List<BuyerBillItemsDto>>(x.BuyerBillItems)
}).ToList();
I have to use select to filter the columns to avoid performance loss
Curently you cannot use own extensions in Include body. So, consider to write query in the following way:
var resultList = dbContext.BuyerBill
.Include(x => x.BuyerBillItems.Where(x =>
(input.Status == null || x.Status == input.Status && x.BuildTime > input.BeginTime && x.BuildTime < input.EndTime)) &&
(input.BeginTime == null || x.BuildTime > input.BeginTime) &&
(input.EndTime == null || x.BuildTime > input.EndTime)
)
.ToList();
Update based on updated reuiqrements
For fully custom projection, there is not needed to build Include, because Select specifies which data is needed to retrieve from database.
Query can be rewritten using WhereIf extension:
var result = dbContext.BuyerBill
.Select(b => new BuyerBillDto
{
Id = b.Id,
BuyUserId = b.BuyUserId,
OrderId = b.OrderId,
BuyerBillItemsDto = b.BuyerBillItems
.AsQueryable()
.WhereIf(input.Status != null, x => x.Status == input.Status)
.WhereIf(input.BeginTime != null, x => x.BuildTime > input.BeginTime)
.WhereIf(input.EndTime != null, x => x.BuildTime > input.EndTime)
.Select(item => new BuyerBillItemsDto
{
Id = item.Id,
// other properties
})
.ToList()
}).ToList();
Could someone help me to write a code to get data from grouped and filtered query?
Data are from simple datatable, and what I need is how to solve a problem if user enter only one of search param, but also have a possibility to enter more param?
public class Journal
{
public int ID {get; set;}
public string Field1 { get; set; }
public string Field2 { get; set; }
}
/*variables entered bu user:
searchParam1
searchParam2
...
searchParamN
*/
using (var dbContext = new databaseContext())
{
var serchresult = dbContext.Journals
.Where(p => p.Field1.StartsWith(SearchParam1) &&
p.Field2.StartsWith(SearchPParam2))
.GroupBy(f => f.ID)
.ToList();
}
To get filtered data I'v tried:
result = from tr in dbContext.Journals select tr;
if (!String.IsNullOrEmpty(SearchParam1)) {
result = result.Where(tr => tr.Field1.StartsWith(SearchParam1));
}
if (!String.IsNullOrEmpty(SearchParam2)) {
result = result.Where(tr => tr.Field2.StartsWith(SearchParam2));
}
But I need to add grouping :(
You are almost there. Split your query in two parts. In the first part do the dynamic filtering and in the second part do the rest.
var source = dbContext.Journals.AsQueryble();
if (!string.IsNullOrEmpty(SearchParam1))
source = source.Where(tr => tr.Field1.StartsWith(SearchParam1));
if (!string.IsNullOrEmpty(SearchParam2))
source = source.Where(tr => tr.Field2.StartsWith(SearchParam2));
var serchresult = source
.GroupBy(f => f.ID)
.ToList();
You could simplify like this.
result = dbContext.Journals.ToList();
result.Where(tr => ((!String.IsNullOrEmpty(SearchParam1) && tr.Field1.StartsWith(SearchParam1))
|| ( !String.IsNullOrEmpty(SearchParam2) && tr.Field2.StartsWith(SearchParam2)))
.GroupBy(f => f.ID)
.SelectMany(g => g.Select(x=> new { ID = g.Key.ID, x.Field1, x.Field2 }))
.ToList();
I have the below class:
public class FactoryOrder
{
public string Text { get; set; }
public int OrderNo { get; set; }
}
and collection holding the list of FactoryOrders
List<FactoryOrder>()
here is the sample data
FactoryOrder("Apple",20)
FactoryOrder("Orange",21)
FactoryOrder("WaterMelon",42)
FactoryOrder("JackFruit",51)
FactoryOrder("Grapes",71)
FactoryOrder("mango",72)
FactoryOrder("Cherry",73)
My requirement is to merge the Text of FactoryOrders where orderNo are in sequence and retain the lower orderNo for the merged FactoryOrder
- so the resulting output will be
FactoryOrder("Apple Orange",20) //Merged Apple and Orange and retained Lower OrderNo 20
FactoryOrder("WaterMelon",42)
FactoryOrder("JackFruit",51)
FactoryOrder("Grapes mango Cherry",71)//Merged Grapes,Mango,cherry and retained Lower OrderNo 71
I am new to Linq so not sure how to go about this. Any help or pointers would be appreciated
As commented, if your logic depends on consecutive items so heavily LINQ is not the easiest appoach. Use a simple loop.
You could order them first with LINQ: orders.OrderBy(x => x.OrderNo )
var consecutiveOrdernoGroups = new List<List<FactoryOrder>> { new List<FactoryOrder>() };
FactoryOrder lastOrder = null;
foreach (FactoryOrder order in orders.OrderBy(o => o.OrderNo))
{
if (lastOrder == null || lastOrder.OrderNo == order.OrderNo - 1)
consecutiveOrdernoGroups.Last().Add(order);
else
consecutiveOrdernoGroups.Add(new List<FactoryOrder> { order });
lastOrder = order;
}
Now you just need to build the list of FactoryOrder with the joined names for every group. This is where LINQ and String.Join can come in handy:
orders = consecutiveOrdernoGroups
.Select(list => new FactoryOrder
{
Text = String.Join(" ", list.Select(o => o.Text)),
OrderNo = list.First().OrderNo // is the minimum number
})
.ToList();
Result with your sample:
I'm not sure this can be done using a single comprehensible LINQ expression. What would work is a simple enumeration:
private static IEnumerable<FactoryOrder> Merge(IEnumerable<FactoryOrder> orders)
{
var enumerator = orders.OrderBy(x => x.OrderNo).GetEnumerator();
FactoryOrder previousOrder = null;
FactoryOrder mergedOrder = null;
while (enumerator.MoveNext())
{
var current = enumerator.Current;
if (mergedOrder == null)
{
mergedOrder = new FactoryOrder(current.Text, current.OrderNo);
}
else
{
if (current.OrderNo == previousOrder.OrderNo + 1)
{
mergedOrder.Text += current.Text;
}
else
{
yield return mergedOrder;
mergedOrder = new FactoryOrder(current.Text, current.OrderNo);
}
}
previousOrder = current;
}
if (mergedOrder != null)
yield return mergedOrder;
}
This assumes FactoryOrder has a constructor accepting Text and OrderNo.
Linq implementation using side effects:
var groupId = 0;
var previous = Int32.MinValue;
var grouped = GetItems()
.OrderBy(x => x.OrderNo)
.Select(x =>
{
var #group = x.OrderNo != previous + 1 ? (groupId = x.OrderNo) : groupId;
previous = x.OrderNo;
return new
{
GroupId = group,
Item = x
};
})
.GroupBy(x => x.GroupId)
.Select(x => new FactoryOrder(
String.Join(" ", x.Select(y => y.Item.Text).ToArray()),
x.Key))
.ToArray();
foreach (var item in grouped)
{
Console.WriteLine(item.Text + "\t" + item.OrderNo);
}
output:
Apple Orange 20
WaterMelon 42
JackFruit 51
Grapes mango Cherry 71
Or, eliminate the side effects by using a generator extension method
public static class IEnumerableExtensions
{
public static IEnumerable<IList<T>> MakeSets<T>(this IEnumerable<T> items, Func<T, T, bool> areInSameGroup)
{
var result = new List<T>();
foreach (var item in items)
{
if (!result.Any() || areInSameGroup(result[result.Count - 1], item))
{
result.Add(item);
continue;
}
yield return result;
result = new List<T> { item };
}
if (result.Any())
{
yield return result;
}
}
}
and your implementation becomes
var grouped = GetItems()
.OrderBy(x => x.OrderNo)
.MakeSets((prev, next) => next.OrderNo == prev.OrderNo + 1)
.Select(x => new FactoryOrder(
String.Join(" ", x.Select(y => y.Text).ToArray()),
x.First().OrderNo))
.ToList();
foreach (var item in grouped)
{
Console.WriteLine(item.Text + "\t" + item.OrderNo);
}
The output is the same but the code is easier to follow and maintain.
LINQ + sequential processing = Aggregate.
It's not said though that using Aggregate is always the best option. Sequential processing in a for(each) loop usually makes for better readable code (see Tim's answer). Anyway, here's a pure LINQ solution.
It loops through the orders and first collects them in a dictionary having the first Id of consecutive orders as Key, and a collection of orders as Value. Then it produces a result using string.Join:
Class:
class FactoryOrder
{
public FactoryOrder(int id, string name)
{
this.Id = id;
this.Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
}
The program:
IEnumerable<FactoryOrder> orders =
new[]
{
new FactoryOrder(20, "Apple"),
new FactoryOrder(21, "Orange"),
new FactoryOrder(22, "Pear"),
new FactoryOrder(42, "WaterMelon"),
new FactoryOrder(51, "JackFruit"),
new FactoryOrder(71, "Grapes"),
new FactoryOrder(72, "Mango"),
new FactoryOrder(73, "Cherry"),
};
var result = orders.OrderBy(t => t.Id).Aggregate(new Dictionary<int, List<FactoryOrder>>(),
(dir, curr) =>
{
var prevId = dir.SelectMany(d => d.Value.Select(v => v.Id))
.OrderBy(i => i).DefaultIfEmpty(-1)
.LastOrDefault();
var newKey = dir.Select(d => d.Key).OrderBy(i => i).LastOrDefault();
if (prevId == -1 || curr.Id - prevId > 1)
{
newKey = curr.Id;
}
if (!dir.ContainsKey(newKey))
{
dir[newKey] = new List<FactoryOrder>();
}
dir[newKey].Add(curr);
return dir;
}, c => c)
.Select(t => new
{
t.Key,
Items = string.Join(" ", t.Value.Select(v => v.Name))
}).ToList();
As you see, it's not really straightforward what happens here, and chances are that it performs badly when there are "many" items, because the growing dictionary is accessed over and over again.
Which is a long-winded way to say: don't use Aggregate.
Just coded a method, it's compact and quite good in terms of performance :
static List<FactoryOrder> MergeValues(List<FactoryOrder> dirtyList)
{
FactoryOrder[] temp1 = dirtyList.ToArray();
int index = -1;
for (int i = 1; i < temp1.Length; i++)
{
if (temp1[i].OrderNo - temp1[i - 1].OrderNo != 1) { index = -1; continue; }
if(index == -1 ) index = dirtyList.IndexOf(temp1[i - 1]);
dirtyList[index].Text += " " + temp1[i].Text;
dirtyList.Remove(temp1[i]);
}
return dirtyList;
}
I'm using Entity Framework. The class below represents a table from the database.
public partial class TermBucket
{
public short ID { get; set; }
public byte Period { get; set; }
public byte Min { get; set; }
public byte Max { get; set; }
}
The PK is ID and Period, so there can be multiple ID's in the table.
When querying the entity, I have at my disposal the period + time (the number associated with the period). Period is set up as an enum, so I would use it as:
Time: 3
Period: 3 (Days)
What I want to do is to find the bucket that matches by requirements and then get all entries for that bucket. Here's what I currently do:
Step 1: Get the ID
return r.Find() // this is my repository
.AsNoTracking()
.Where(x => (int)tp.Period == x.Period && tp.Time >= x.Min && tp.Time <= x.Max)
.Select(x => x.ID)
.Single();
Step 2: Get all entries for the ID, using the retrieved ID
return r.Find()
.AsNoTracking()
.Where(x => x.ID == ID );
So, there are 2 distinct queries, but is it possible to retrieve this data in one go?
Can't you just combine them?
return r.Find()
.AsNoTracking()
.Where(x => x.ID == r.Find()
.AsNoTracking()
.Where(x => (int)tp.Period == x.Period && tp.Time >= x.Min && tp.Time <= x.Max)
.Select(x => x.ID)
.Single());
You can do it using join. Example
public partial class TermBucket
{
public short ID { get; set; }
public byte Period { get; set; }
public byte Min { get; set; }
public byte Max { get; set; }
}
static void Main(string[] args)
{
List<TermBucket> l = new List<TermBucket>();
l.Add(new TermBucket() { ID = 1, Period = 3, Min = 10, Max = 14 });
l.Add(new TermBucket() { ID = 1, Period = 4, Min = 10, Max = 13 });
l.Add(new TermBucket() { ID = 1, Period = 5, Min = 100, Max = 25 });
l.Add(new TermBucket() { ID = -1, Period = 3, Min = 10, Max = 12 });
int period = 3;
int minV = 10;
int maxV = 13;
var res = from e in l
join e2 in l on e.ID equals e2.ID
where e.Period == period && minV >= e.Min && maxV <= e.Max
select e2;
foreach (var r in res)
{
Console.WriteLine(r.ID + " " + r.Period);
}
Console.ReadLine();
}
Will output
1 3
1 4
1 5
Yes, it is:
return r.Find()
.AsNoTracking()
.Where(x => x.ID == r.Find()
.AsNoTracking()
.Where(x => (int)tp.Period == x.Period && tp.Time >= x.Min && tp.Time <= x.Max)
.Select(x => x.ID)
.Single());
But I would recommend breaking it up into two queries, as you already have it, to handle the case where the first query returns no results. (As it is, currently, .Single() will throw an exception if .Select() is empty).
I have a list that contains Categories and their accept/reject counts but there is a problem with this list. I'm using a LINQ query to access the data, and I grouped them by both category name and Accept/Reject Code(ResultCode). So the data is in this form:
Almost all of the Categories have both AP counts and RJ counts. And what I'm trying to do is to show each Category's accept and reject count. What should I use? Hashtables don't fit in this problem, I tried Dictionary with int List as value but couldn't add when the same key appeared.
UPDATE:
List<ModReportingDM.ReportObjects.AllCategories> allcats = new List<ModReportingDM.ReportObjects.AllCategories>();
Dictionary<string, ModReportingDM.ReportObjects.ResultCode> dict = new Dictionary<string, ModReportingDM.ReportObjects.ResultCode>();
ModReportingDM.ReportObjects.ResultCode x = new ModReportingDM.ReportObjects.ResultCode();
allcats = reportBLL.GetAllCats(model.ModId, model.ReportStartDate, model.ReportEndDate);
if (allcats != null)
{
model.AllCatsList = new List<ModReportingDM.ReportObjects.AllCategories>();
foreach (var item in allcats)
{
x.Accepted = item.Count;
x.Rejected = item.Count;
dict.Add(item.Category, x);
}
}
Query:
public List<AllCategories> GetAllCats(int modId, DateTime startDate, DateTime endDate)
{
using (entities = new ModReportingEntities())
{
var query = (from c in entities.Content
where c.ModId == modId && c.CreatedTime >= startDate && c.CreatedTime <= endDate && c.Category != null
group c by new { c.Category, c.ResultCode } into g
orderby g.Count() ascending
select new AllCategories
{
Category = g.Key.Category,
ResultCode = g.Key.ResultCode,
AcceptCount = g.Count(),
RejectCount = g.Count()
});
return query.ToList();
}
}
What i would do is create a ResultCode class:
public class ResultCode
{
public int Ap { get; set; }
public int Rj { get; set; }
}
and then use a Dictionary<string, ResultCode> which maps each category to its report.
You could also take a different approach using a Tuple<T1, T2> (which personally i like less) which simply maps your key to two distinct values:
Dictionary<string, Tuple<int, int>> categoryToResultCode;
List<Tuple<string, string, int>> listOfTuples = new List<Tuple<string, string, int>>();
Tuple<string, string, int> tupleItem1 = new Tuple<string, string, int>("A", "AP", 1);
listOfTuples.Add(tupleItem1);
You can use Tuple. Please refer http://msdn.microsoft.com/en-us/library/system.tuple%28v=vs.110%29.aspx
My colleague and I realized that we can keep track of the Category's and if same Category occurs, it means that only a field of it should be changed(either AcceptCount or RejectCount). So we've created a lambda expression like this:
foreach(var item in allcats)
{
if (model.AllCategories.Select(m => m).Where(x => x.Category == item.Category).ToList().Count == 0)
{
if (item.ResultCode == "AP") {
model.AllCategories.Add(new ModReportingDM.ReportObjects.AllCategories()
{
Category = item.Category,
AcceptCount = item.Count
});
}
else
{
model.AllCategories.Add(new ModReportingDM.ReportObjects.AllCategories()
{
Category = item.Category,
RejectCount = item.Count
});
}
}
else
{
ModReportingDM.ReportObjects.AllCategories x = model.AllCategories.Select(n => n).Where(y => y.Category == item.Category).ToList().First();
if (item.ResultCode == "AP")
{
x.AcceptCount = item.Count;
}
else
{
x.RejectCount = item.Count;
}
}
}
If same Category occurs, go ahead and change its AcceptCount or RejectCount accordingly. That's the way I solved the problem.