LINQ GroupBy 3 properties? - c#

I've got the following class:
public class ProductInventory : Entity
{
public virtual Product Product { get; set; }
public DateTime ExpirationDate { get; set; }
public Manager Manager { get; set; }
}
and a class used in my ViewModels that looks like so:
public class ProductInventoryCountModel
{
public Product Product { get; set; }
public DateTime ExpirationDate { get; set; }
public int Count { get; set; }
}
I want to get an output of Dictionary<Manager, List<ProductInventoryCountModel> that basically displays Products by manager and then by expiration date so that I can print out actual data looking something like this:
ManagerBoB
--ProductA, Expires 8/15/13, Count: 10
--ProductA, Expires 10/10/13, Count: 40
--ProductB, Expires 6/13/13, Count: 30
ManagerTim
--ProductA, Expires 10/10/13, Count: 5
--ProductB, Expires 5/25/13, Count: 10
--ProductB, Expires 6/13/13, Count 40
how would I write this query in LINQ when starting with a list of ProductInventory? I tried using multiple .GroupBy but that didn't work.

You need to group by an object with multiple properties:
list.GroupBy(p => new { p.Manager, p.Product.Name, p.ExpirationDate.Date })
This works because anonymous types implement Equals() and GetHashCode() to compare by value.

You stated that you want a Dictionary<Manager, List<ProductInventoryCountModel>>, so I think you'd have to do something like this:
var dictionary =
db.ProductInventory.GroupBy(x => new { x.Manager, x.Product, x.ExpirationDate })
.ToDictionary(g => g.Key.Manager,
g => g.Select(x => new ProductInventoryCountModel
{
Product = x.Key.Product,
ExpirationDate = x.Key.ExpirationDate,
Count = x.Count()
}).ToList());

Related

Using LINQ to build a new collection

At the moment I have a collection of 'Visit' classes. The Visit class has a number of properties but here are the relevant ones for my problem:
public class Visit
{
public int Id { get; set; }
public DateTime VisitDate { get; set; }
public int VisitDuration { get; set; }
// Other irrelevant properties...
}
I also have created a new class called 'VisitDuration':
public class VisitDuration
{
public DateTime VisitDate { get; set; }
public int Duration { get; set; }
{
I have a function which fetches a collection of visits between two dates like so:
var visits = GetVisitsCollection();
What I want to do is to group up the visits by VisitDate, and then create a new instance of the VisitDuration class which should contain the VisitDate and the average visit duration for that date.
So for example, if there were 3 visits occurring on the date 28/11/2014, with durations of 20, 40 and 60 respectively, I'd want to create a new instance of VisitDuration with VisitDate: 28/11/2014 and Duration: 40 (the average of the 3). How can I do this using linq?
I've done something similar using counts that uses GroupBy ie:
var collection = visits.GroupBy(v => v.VisitDate).Select(x => new
{
VisitDate = x.Key.ToString(),
Count = x.Count()
});
But the difference is rather than getting the count, I want to get a value that's an average of a property of all records that have that VisitDate.
I hope I've explained myself well and appreciate any responses! Thanks
Try this:-
var collection = visits.GroupBy(x => x.VisitDate)
.Select(x => new VisitDuration
{
VisitDate = x.Key,
Duration = Convert.ToInt32(x.Average(z => z.VisitDuration))
});
Working Fiddle.

RavenDB index to return most recent entry

I am new to RavenDB and I am trying to query the document model below with the index below. The index is almost working as desired, except now I need to only include the most recent status for a date in the total. For example, a client could have multiple import statuses for a date, but only the last status should be counted in the resulting totals.
public class Client
{
public int Id { get; set; }
public string Name { get; set; }
public IList<ImportStatusMessage> ImportStatuses { get; set; }
}
public class ImportStatusMessage
{
public DateTime TimeStamp { get; set; }
public ImportStatus Status { get; set; }
}
public enum ImportStatus
{
Complete,
Running,
Failed,
Waiting,
NoReport
}
I am using the following index:
public class Client_ImportSummaryByDate : AbstractIndexCreationTask<Client, ImportSummary>
{
public Client_ImportSummaryByDate()
{
Map = clients => from client in clients
from status in client.ImportStatuses
select new
{
status.Status,
Date = status.TimeStamp.Date,
Count = 1
};
Reduce = results => from result in results
group result by new { result.Status, result.Date }
into g
select new
{
g.Key.Status,
g.Key.Date,
Count = g.Sum(x => x.Count)
};
}
}
public class ImportSummary
{
public ImportStatus Status { get; set; }
public DateTime Date { get; set; }
public int Count { get; set; }
}
Can this be accomplished with an index? Do I need a different approach to solve this problem?
Instead of:
from status in client.ImportStatuses
Consider:
let status = client.ImportStatuses.Last()
If they might be out of order in the list, you could do:
let status = client.ImportStatuses.OrderBy(x => x.TimeStamp).Last()
You could also use First instead of Last if they were so ordered that way.
Any of these would index just a single status per client. If instead you mean that you want multiple status, but only the last on any given date, you could do:
Map = clients => clients.SelectMany(x => x.ImportStatuses, (x, y) => new {x.Id, y.Status, y.TimeStamp})
.GroupBy(x => new {x.Id, x.TimeStamp.Date})
.Select(g => g.OrderBy(x => x.TimeStamp).Last())
.Select(x => new
{
x.Status,
x.TimeStamp.Date,
Count = 1
});
All of this would be in the map part of the index, since the list is self-contained in each document.

20 most viewed "codes" with include "User" and "Language"

I have two tables "Codes" and "Views" and want to get a list of the 20 Codes with most views since x days ago (thats my variable dt).
Im able to get the list and sort it, but i have a problem i also would like to include 2 other tables "User" and "Language".
var query = from f in _db.Codes
select new
{
Code = f,
PostCount = f.ViewModels.Count(p => p.timestamp > dt)
};
var result = query.OrderByDescending(x => x.PostCount).Select(y => y.Code).Take(20)
But after doing like this i'm not able to Include my other tables. I tried to convert the result to a ObjectQuery but then it becomes null (there are 20 Codes in the result before trying to convert it).
The Code model looks like this
[Key]
public int CodeID { get; set; }
public int UserID { get; set; }
public UserModel User { get; set; }
[DisplayName("Language")]
public int LanguageID { get; set; }
public LanguageModel Language { get; set; }
public virtual ICollection<ViewModel> ViewModels { get; set; }
Easy solution:
IEnumerable<CodeModel> posts = _db.Codes
.Include("User")
.Include("Language")
.OrderByDescending(f => f.ViewModels.Count(p => p.timestamp > dt))
.Take(20);

LINQ - SelectMany from multiple properties in same object

Assume you have the following simple objects:
class Order
{
public Customer[] Customers { get; set; }
}
class Customer
{
public SaleLine[] SaleLines { get; set; }
}
class SaleLine
{
public Tax[] MerchTax { get; set; }
public Tax[] ShipTax { get; set; }
}
class Tax
{
public decimal Rate { get; set; }
public decimal Total { get; set; }
}
With these objects, I want to be able to get a list of all the unique tax rates used on the entire order, including both merchandise and shipping tax rates.
The following LINQ query will get me the list I need, but only for merchandise taxes:
var TaxRates = MyOrder.Customers
.SelectMany(customer => customer.SaleLines)
.SelectMany(saleline => saleline.MerchTax)
.GroupBy(tax => tax.Rate)
.Select(tax => tax.First().Rate
How can I get a list that contains list of unique tax rates that contains both merchandise and shipping rates?
It sounds like you want this:
var TaxRates = MyOrder.Customers
.SelectMany(customer => customer.SaleLines)
.SelectMany(saleline => saleline.MerchTax.Concat(saleline.ShipTax))
.GroupBy(tax => tax.Rate)
.Select(group => group.Key);
Basically the change is the call to Concat which will concatenate two sequences together.
It can also be used like this;
var TaxRates = MyOrder.Customers
.ToList()
.SelectMany(x => new[] { x.SaleLines, x.MerchTax })
.GroupBy(tax => tax.Rate)
.Select(group => group.Key);

How can I get two different aggregates in a single LINQ?

I have a list of this object:
public class Customer
{
public int Id { get; set; }
public string EmailAddress { get; set; }
public DateTime ServiceStartDate { get; set; }
public DateTime? BillingStartDate { get; set; }
public string Status { get; set; }
}
In preparation for making a chart displayed on a dashboard I am trying to condense this list into another list of this object:
public class DashboardCustomerConversions
{
public string Month { get; set; }
public int Trials { get; set; }
public int Purchased { get; set; }
}
Where the end result looks something like:
Month Trials Purchases
--------- ------ ---------
Dec 2010 390 250
Jan 2011 345 190
Feb 2011 576 340
I am having a hard time coming up with a LINQ statement that can achieve the desired end result. This statement is very close:
var list = from b in results
group b by new { b.ServiceStartDate.Year, b.ServiceStartDate.Month } into g
select new Test
{
Month = string.Format("{0} {1}", CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(g.Key.Month), g.Key.Year),
Trials = g.Count(),
Purchased = g.Count()
};
The obvious problem in is the "Purchased = g.Count()" line in that it just repeats the Trials result. I would like to count objects where the BillingStartDate.HasValue is true.
Is there a way to restructure the LINQ to make this work?
Edit: I would prefer a fluent style of syntax but I was unable to get the above to work. Answer in any variation would be great.
You need to pass a condition to the Count method.
Purchased = g.Count(q => q.BillingStartDate.HasValue)
So SLaks had the right solution. Here it is written in fluent syntax:
listOfCustomer.GroupBy(c => new { c.ServiceStartDate.Year, c.ServiceStartDate.Month })
.Select(group => new DashboardCustomerConversions()
{
Month = string.Format("{0} {1}", CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(group.Key.Month), group.Key.Year),
Trials = group.Count(),
Purchased = group.Count(c => c.BillingStartDate.HasValue)
});

Categories