I am trying to write an OrderBy clause in a Linq to EntityFramework query. My problem is that the entity table I am looking at stores an ID, that relates to a table in a different database and I cannot adjust the database.
MainDatabase.EntityToOrder
ID
Name
OtherID
SecondDatabase.OtherEntity
ID
Name
My C# EntityToOrder Model looks like this, and I need to be able to order by "OtherName"
EntityToOrder.cs
public class EntityToOrder
{
[DataMember]
public long ID { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public long OtherId { get; set; }
public string OtherName { get; set; }
}
So, I would like to Order EntityToOrder by "OtherName" in the most efficient way possible. My existing query looks like this.
var entities = mainContext.EntityToOrder.OrderBy(e => e.Name).Skip(startIndex).Take(pageSize).ToList();
var otherIds = entities.Select(e => e.OtherID).ToList();
Dictionary<long, string> otherNames = secondContext.OtherEntity
.Where(oe => otherIds.Contains(oe.ID))
.Select(oe => new { ID = oe.ID, Name = oe.Name })
.ToDictionary(oe => oe.ID, oe => oe.Name);
entities.ForEach(e => OtherName = otherNames[e.OtherID]);
How can I write the most efficient query to order by "OtherName", preferably avoiding selecting the whole EntityToOrder table into memory.
Update
For clarity, here is some code that achieves the OrderBy, but needs to retrieve the entire EntityToOrder table into memory. I was hoping this could be achieved in a more efficient way. Also, the OtherEntity can belong to many EntityToOrder rows.
var entities = mainContext.EntityToOrder.ToList();
var otherIds = entities.Select(e => e.OtherID).ToList();
Dictionary<long, string> otherNames = secondContext.OtherEntity
.Where(oe => otherIds.Contains(oe.ID))
.Select(oe => new { ID = oe.ID, Name = oe.Name })
.ToDictionary(oe => oe.ID, oe => oe.Name);
entities.ForEach(e => OtherName = otherNames[e.OtherID]);
return entities.OrderBy(e => e.OtherName).Skip(startIndex).Take(pageSize).ToList();
Quite challenging task. I was thinking initially just to switch the roles and perform pagination (OrderBy/Skip/Take) on OtherEntity table, but unfortunately that doesn't work due to one to many relationship. So I ended up with doing some pre pagination in memory on OtherEntity. However, in order to do that I needed counts of the matching items in EnityToOrder, so this is retrieved with additional db query, which makes the solution involving 3 db queries and some memory processing. Here it is
var countByOtherId = db.EntityToOrder
.GroupBy(e => e.OtherId)
.Select(g => new { ID = g.Key, Count = g.Count() })
.ToDictionary(e => e.ID, e => e.Count);
var other = new Dictionary<long, string>();
int skipCount = startIndex, useCount = 0;
foreach (var e in db.OtherEntity.OrderBy(e => e.Name))
{
int count;
if (!countByOtherId.TryGetValue(e.ID, out count)) continue;
if (skipCount > 0 && other.Count == 0)
{
if (skipCount >= count) { skipCount -= count; continue; }
count -= skipCount;
}
other.Add(e.ID, e.Name);
if ((useCount += count) >= pageSize) break;
}
var entities = db.EntityToOrder
.Where(e => other.Keys.Contains(e.OtherId))
.AsEnumerable()
.Select(e => new EntityToOrder { ID = e.ID, Name = e.Name,
OtherId = e.OtherId, OtherName = other[e.OtherId] })
.OrderBy(e => e.OtherName).ThenBy(e => e.Name)
.Skip(skipCount).Take(pageSize)
.ToList();
Now, I'm not quite sure if that's better to what are you doing currently, but it's worth trying.
If you can change the model, then you might try the following:
public class EntityToOrder
{
[DataMember]
public long ID { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public long OtherId { get; set; }
[ForeignKey("OtherId")]
public OtherEntity OtherEntity{ get; set; }
}
Then, you should be able to perform this query:
using System.Data.Entity;
var entities = mainContext
.EntityToOrder
.Include(x => x.OtherEntity)
.OrderBy(e => e.OtherEntity.Name)
.Skip(startIndex)
.Take(pageSize)
.ToList();
Edit :
Sorry, I missed the point that you had 2 databases....
I found an alternative which I thought I would post in case it is useful to anyone. I used a .Join() to merge the dictionary of OtherEntity into my query. This still selects into an IEnumerable so I don't think it is more efficient.
var entities = mainContext.EntityToOrder;
var otherIds = entities.Select(e => e.OtherID).ToList();
Dictionary<long, string> otherNames = secondContext.OtherEntity
.Where(oe => otherIds.Contains(oe.ID))
.Select(oe => new { ID = oe.ID, Name = oe.Name })
.ToDictionary(oe => oe.ID, oe => oe.Name);
Func<EntityToOrder, KeyValuePair<long, string>, EntityToOrder> joinFunc = ((a, b) => {
a.OtherName= b.Value;
return a;
});
return entities.Join(otherNames, e => e.OtherID, oe => oe.Key, joinFunc)
.OrderBy(e => e.OtherName)
.Skip(startIndex)
.Take(pageSize)
.ToList();
Note on Includes
When applying Join you select into an IEnumerable and therefore lose the ability to access properties from a linked table. To counter this you would need to add a .Include() for any linked table you need to access before applying the .Join(). E.g.
var entities = mainContext.EntityToOrder
.Include("LinkedEntity");
return entities.Join(otherNames, e => e.OtherID, oe => oe.Key, joinFunc)
.OrderBy(e => e.OtherName)
.ThenBy(e => e.LinkedEntity.Name) //reference to linked table
.ToList();
Related
So, I have my Products table in SSMS with these properties:
public class Product
{
public int Id {get; set;}
public string Title { get; set; }
public decimal Price { get; set; }
}
and my Reports table:
public class Report
{
public int Id { get; set; }
public int ProductId { get; set; }
public ReportType ReportType { get; set; }
}
I want to return a List<Product> to my View that is sorted based on how many reports each Product has, but I can't figure out how to do it with LINQ. Any help/tip would be appreciated.
If you put nav props in this would be:
context.Products.Include(p => p.Reports).OrderBy(p => p.Reports.Count(*));
But as you have no nav props, perhaps something like:
context.Products.OrderBy(p => context.Reports.Count(r => r.ProductId == p.Id));
The query ends up looking like this for the latter:
SELECT *
FROM p
ORDER BY (SELECT COUNT(*) FROM r WHERE p.id = r.id)
and similar but with a left join, for the former
You could also do it on the client side
var dict = context.Reports.GroupBy(r => ProductId, (k,g) => new { ProductId, Count = g.Count() } )
.ToDictionary(at => at.ProductId, at => at.Count);
Then:
//or OrderByDescending if you want most reported products
var ret = context.Products.ToList().OrderBy(p => dict[p.ProductId]);
If you have some limited list of products:
var prods = context.Products.Where(...).ToList();
var prodIds = prods.Select(p => p.ProductId).ToArray();
var dict = context.Reports
.Where(r => prods.Contains(r.ProductId))
.GroupBy(r => ProductId, (k,g) => new { ProductId, Count = g.Count() } )
.ToDictionary(at => at.ProductId, at => at.Count)
var ret = prods.OrderBy(p => dict[p.ProductId]);
This code is performing well as expected.
var soldQtyForEachItem = await _context.InvoiceProduct
.Where(x => x.ProductId != 0)
.GroupBy(x => x.ProductId)
.Select(grp => new CompanyProducts
{
CompanyProductId = grp.Key,
CompanyProductSoldQuantity = grp.Sum(item => item.QuantitySold)
}).ToListAsync();
Question :
I now need to join another table called Products and filter by Id against InvoiceProduct table and retrieve
ProductsItemName which is a row from Products table; then all need to go to a custom type "CompanyProducts" below.
Please how do I achieve it?
public class CompanyProducts
{
public int CompanyProductId { get; set; }
public int CompanyProductName { get; set; }
public int CompanyProductSoldQuantity { get; set; }
}
InvoiceProduct table is a many to many, meaning that one InvoiceId may have multiple ProductIds.
Products table has property like ProductId, ProductsItemName.
Simplest solutions is to include ProductItemName into grouping:
var soldQtyForEachItem = await _context.InvoiceProduct
.Where(x => x.ProductId != 0)
.GroupBy(x => new { x.ProductId, x.Product.ProductsItemName } )
.Select(grp => new CompanyProducts
{
CompanyProductId = grp.Key.ProductId,
CompanyProductName = grp.Key.ProductsItemName,
CompanyProductSoldQuantity = grp.Sum(item => item.QuantitySold)
})
.ToListAsync();
Model:
public class Ticket {
public Ticket();
public int Id { get; set; }
public virtual TicketUrgency TicketUrgency { get; set; }
public int UrgencyId { get; set; }
}
public class TicketUrgency {
public TicketUrgency();
[Key]
public int Id { get; set; }
[MaxLength(50)]
[Required]
public string Name { get; set; }
public ICollection<Ticket> Tickets { get; set; }
}
I have the following linq statement:
var model = Entities
.Include(x => x.TicketUrgency)
.GroupBy(x => x.UrgencyId)
.Select(g => new {
id = g.Key,
count = g.Count(),
name = g.FirstOrDefault(u => u.UrgencyId == g.Key).TicketUrgency.Name
});
I want to Group Entities by UrgencyId and then return the Key (UrgencyId), and also count of the items in a single group and show the name of the Urgency.
When I run it, the query just gets hung up without any exceptions.
This should work, doing it the other way around, by retrieving all TicketUrgencies first and grouping it.
Entities.Include(e => e.Tickets)
.GroupBy(t => t.Id)
.Select(g => new {
id = g.Key,
name = g.FirstOrDefault().Name,
count = g.FirstOrDefault().Tickets.Count()
});
Very simple. Just try this :
var model = Entities
.Include(x => x.TicketUrgency)
.GroupBy(x => new {UrgencyId = x.UrgencyId ,
Name = x.TicketUrgency.Name})
.Select(x=> new { UrgencyId = x.Key.UrgencyId,
Name = x.Key.Name,
Count = x.Count()});
Since you are grouping by UrgencyId, you know all members of g have the same id as the Key, so to pick up the name just pull the first one. You also know g isn't empty because that wouldn't make a group:
var model = Entities
.Include(x => x.TicketUrgency)
.GroupBy(x => x.UrgencyId)
.Select(g => new {
id = g.Key,
name = g.First().TicketUrgency.Name
count = g.Count(),
});
You could group by those two properties:
var model = Entities
.Include(x => x.TicketUrgency)
.GroupBy(x => new{ x.UrgencyId, x.TicketUrgency.Name })
.Select(g => new {
id = g.Key.UrgencyId,
count = g.Count(),
name = g.Key.Name
});
Another way could be, as #ASpirin suggested,starting the query from TickerUrgency:
var result= TicketUrgencies.Include(t=>t.Tickets)
.Where(t=>t.Tickets.Any())
.Select(t=> new {id=t.Id,name=t.Name, count= t.Tickets.Count()})
I'm trying to do replicate the following query in LINQ using the comprehension method. My SQL is as follows:
SELECT COUNT(Post.Id), User.Id, User.Name
FROM Post
INNER JOIN User ON Post.ModeratorId = User.Id
WHERE Post.Status IN ("Live", "Pending", "Expired")
GROUP BY User.Id, User.Name
My LINQ query is as follows but its still returns a 0 count when no moderator has been assigned to the post. Note a Post.ModeratorId is a nullable value.
I only want a list of moderators and a count of post they are or have moderated.
How can I replicate the above SQL in LINQ?
public IEnumerable<ModeratorPostViewModel> GetModeratorPostCount()
{
var posts = _context.Post
.Include(p => p.Moderator)
.Include(p => p.Status)
.Where(p => p.ModeratorId != null && p.Status.Name IN ("Live", "Pending", "Expired"))
.OrderBy(p => p.Moderator.Name)
.Select(p => new ModeratorPostViewModel
{
Id = p.ModeratorId,
Name = p.Moderator.Name,
CountOfPosts = p.Moderator.ModeratorPosts.Count()
})
.ToList();
// Return list
return posts
}
My models are defined as follows:
public class Post
{
int Id { get; set; }
int StatusId { get; set; }
string ModeratorId { get; set; }
// Navigation properties
public Status Status { get; set; }
public ApplicationUser Moderator { get; set; }
// some other other properties
}
public class ApplicationUser : IdentityUser
{
public string Name { get; set; }
// Navigation property
public ICollection<Post> ModeratorPosts { get; set; }
}
I only want a list of moderators and a count of post they are or have moderated
Then base your query on Moderator (or whatever it's called) entity:
var statuses = new [] { "Live", "Pending", "Expired" };
var posts = _context.Moderator
.OrderBy(m => m.Name)
.Select(m => new ModeratorPostViewModel
{
Id = m.Id,
Name = m.Name,
CountOfPosts = m.ModeratorPosts.Count(p => statuses.Contains(p.Status.Name))
})
.Where(m => m.CountOfPosts != 0)
.ToList();
UPDATE: I have to admit that the above LINQ query does not produce a very good SQL, especially with the last Where condition. So you might resort to the exact LINQ equivalent of your SQL query (you missed the GROUP BY part):
var statuses = new [] { "Live", "Pending", "Expired" };
var posts = _context.Post
.Where(p => p.ModeratorId != null && statuses.Contains(p.Status.Name))
.GroupBy(p => new { Id = p.ModeratorId, Name = p.Moderator.Name })
.Select(g => new ModeratorPostViewModel
{
Id = g.Key.Id,
Name = g.Key.Name,
CountOfPosts = g.Count()
})
.OrderBy(m => m.Name)
.ToList();
You can do a group by after filtering the posts by the statuses you are looking for
var s = new List<string>() {"Live", "Pending", "Expired"};
var grouped = db.Post.Where(x => s.Contains(x.Status)) //Fitler for the statuses
.GroupBy(f => f.ModeratorId, po => po,
(k, items) => new ModeratorPostViewModel
{
Name = items.FirstOrDefault().User.Name,
Id=k,
CountOfPosts = items.Count()
}).ToList();
I've got a method I've been using against IEnumerable no problem. However I want to start using it against IQueryable as well. However, the way I currently have it wont work as its trying to execute my method against the database.
The situation is as follows. I want a property on the object I'm selecting into to be be null if the value selecting from is null or the Id and Name of the property if it exists. For example:
var foos = FooRepository.All().Select(s => new FooBrief()
{
Id = s.Id,
Customer = SimpleData.To(s.Customer, m => m.Id, m => m.Name)
});
where SimpleData.To looks like:
public class SimpleData
{
public int Id { get; set; }
public string Name { get; set; }
public static SimpleData To<T>(T t, Func<T, int> id, Func<T, string> name) where T : class
{
if (t != null)
{
return new SimpleData { Id = id(t), Name = name(t) };
}
return null;
}
}
Is there someway I can get this behaviour whilst allowing it to execute against the database?
NOTE: Because of reasons elsewhere in my code I cannot use .ToList(). I may be adding additional filtering at a later point
The simplest approach is just to perform the selection outside the database, using AsEnumerable:
var foos = FooRepository.All()
.Select(x => new { Id = x.Id,
CustomerId = x.Customer.Id,
CustomerName = x.Name })
.AsEnumerable()
.Select(s => new FooBrief {
Id = s.Id,
Customer = new SimpleData {
Id = s.CustomerId,
Name = s.CustomerName
}
});
The first Select is just to make sure that the database query only pulls out the required fields. If you really still want to use your SimpleData.To method:
// Outdented to avoid scrolling
var foos = FooRepository.All()
.Select(x => new { Id = x.Id,
CustomerId = x.Customer.Id,
CustomerName = x.Name })
.AsEnumerable()
.Select(s => new FooBrief {
Id = s.Id,
Customer = SimpleData.To(s,
s => s.CustomerId,
s => s.CustomerName)
});