Linq Return grouped result - c#

I have the following database entity containing ingredients to put on pizza. Column Type is a foreign key to another table containing information on the ingredient category (cheese, meat, vegetables etc.)
public class Ingredient
{
public int IngredientId { get; set; }
public string Name { get; set; }
public bool IsVegetarian { get; set; }
public int Type { get; set; }
}
Some sample data:
sample ingredients
For each unique 'Type' value, I want to return a List of Ingredients with that type.
The final result will be stored in an ICollection of type IngredientViewModel.
public class IngredientViewModel : Model
{
public IngredientCategory Category { get; set; }
public List<Ingredient> Ingredients { get; set; }
}
How can I best get all ingredients, grouped per category in a single database trip using linq or lambda expressions?
I currently have the following snippet which I find a bit messy:
public async Task<IEnumerable<IngredientViewModel>> GetIngredientsPerCategoryAsync()
{
IEnumerable<Ingredient> result = await _repo.GetAllAsync();
IEnumerable<IngredientViewModel> viewModels = result.GroupBy(
x => x.Type,
(category, ingredients) => new IngredientViewModel()
{
Category = (IngredientCategory)category,
Ingredients = MapFromEntities(ingredients)
})
.OrderBy(x => x.Category.ToString())
.ToList();
return viewModels;
}

The most obvious would be: Ingredients = g.ToList().

Related

In LINQ, how can I do an .OrderBy() on data that came from many to many relationship?

I have already seen the answers to these questions In LINQ, how can I do an .OrderBy() on data that came from my .Include()?, ^ and ^, However, None is the answer to my question.
I have three entities: Letter, Person, LetterPerson as follows:
public class Letter
{
public int LetterId { get; set; }
public string Title { get; set; }
//MtoM
public ICollection<LetterPerson> LetterPersons { get; set; }
}
public class Person
{
public int PersonId { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
//MtoM
public ICollection<LetterPerson> LetterPersons { get; set; }
}
public class LetterPerson
{
public int LetterPersonId { get; set; }
[ForeignKey("Letter")]
public int LetterId { get; set; }
[ForeignKey("Person")]
public int PersonId { get; set; }
public DateTimeOffset? AssignDate { get; set; }=DateTimeOffset.Now;
public Letter Letter { get; set; }
public Person Person { get; set; }
}
The Letter entity has Many To Many relationship with the Person entity by the LetterPerson entity. Now, I'd like to get a list of the person according to a specific letter's id and order by on the LetterPerson's id.
I have something like the following query in mind:
var PersonRec = await _dbContext.Persons
.Include(u => u.LetterPersons)
.Where(u => u.LetterPersons.Any(i => i.LetterId == LetterId))
.OrderBy(u => u.LetterPersons.LetterPersonId)
//.Include(u => u.LetterPersons.OrderBy(f=>f.LetterPersonId))
//.Where(u => u.LetterPersons.OrderBy(f=>f.LetterPersonId).Any(i => i.LetterId == LetterId))
//.OrderBy(u => u.LetterPersons.FirstOrDefault().LetterPersonId)
.ProjectTo<PersonDTO>(_mapperConfiguration).ToListAsync();
The above commented codes are the attempts that I made, but still the desired result was not achieved. I need .OrderBy(u => u.LetterPersons.LetterPersonId) , However, it clearly gives a compile error.
Question:
How should I correct the OrderBy part?
Just note that I have to send the query as a Data Transfer Object (PersonDTO) that the same as Person entity except FirstName field.
I use EF6 in .Net6.
If the combination (LetterId, PersonId) is unique in the joining table (typical for many-to-many), then the one-to-many relation from Person to LetterPerson for specific LetterId value becomes one-to-one, hence you can use Select or SelectMany with filter to get the single LetterPerson entry, which then could be used for ordering.
For isntance, using LINQ query syntax (more natural for such type of queries):
var query =
(
from p in _dbContext.Persons
from lp in p.LetterPersons
where lp.LetterId == LetterId
orderby lp.LetterPersonId
select p
)
.ProjectTo<PersonDTO>(_mapperConfiguration);
var result = await query.ToListAsync();
Note that you don't need Include in order to access related data inside LINQ to Entities query. Also for projection queries Includes are ignored.

lambda expression to select single row from parent table where join table object value contains specific object

I apologize if this is a duplicate, but I searched and can not find my scenario.
I have a "parent" table called Tournaments and a Players table which houses participants of tournaments. This is a many to many relationship because I can have multiple tournaments and the same players (or more or less) can participate in each tournament.
I have created a join table for the purpose of allowing my many-to-many relationship.
my objects look like this:
TournamentEntity.cs
public class TournamentEntity : IInt32Identity
{
public int Id { get; set; }
...
public List<TournamentPlayers> Participants { get; set; }
}
TournamentPlayers.cs (this is my join entity)
public class TournamentPlayers
{
public int TournamentId { get; set; }
public TournamentEntity Tournament { get; set; }
public int PlayerId { get; set; }
public PlayerEntity Player { get; set; }
}
PlayerEntity.cs
public class PlayerEntity : IInt32Identity
{
public int Id { get; set; }
...
public List<TournamentPlayers> Participants { get; set; }
}
I would like to return a list and/or a single instance of TournamentEntity with a simple lambda expression from a single PlayerEntity passed into my method.
example method:
public async Task<TournamentEntity> GetTournamentByTournamentParticipant(PlayerEntity tournamentParticipant)
{
return await EntityDbSet
.Where(x => x.Participants.Contains) // this is where I'm a bit lost on how to link to "tournamentParticipant"
...
.Include(x => x.Participants)
.ThenInclude(b => b.Player)
.FirstOrDefaultAsync();
}
thanks in advance.
Have you tried this
return await EntityDbSet
.Where(x => x.Participants.Any(p => p.PlayerId == tournamentParticipant.Id))
.Include(x => x.Participants)
.ThenInclude(b => b.Player)
.FirstOrDefaultAsync();

How to combine Join with an aggregate function and group by

I am trying to get the average rating of all restaurants and return the names of all resteraunts associated with that id, I was able to write a sql statement to get the average of restaurants along with the names of the restaurants however I want to only return the name of the restaurant once.
Select t.Average, Name from [dbo].[Reviews] as rev
join [dbo].[Resteraunts] as rest
on rest.ResterauntId = rev.ResterauntId
inner join
(
SELECT [ResterauntId],
Avg(Rating) AS Average
FROM [dbo].[Reviews]
GROUP BY [ResterauntId]
)
as t on t.ResterauntId = rest.ResterauntId
resteraunt class
public int ResterauntId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public virtual ICollection<Review> Reviews { get; set; }
public virtual Review reviews{ get; set; }
Review class
public int ReviewId { get; set; }
public double Rating { get; set; }
[ForeignKey("ResterauntId")]
Resteraunt Resteraunt { get; set; }
public int ResterauntId { get; set; }
public DateTime DateOfReview { get; set; }
If possible I would like to have the answer converted to linq.
Resteurants.Select(r => new {
Average = r.Reviews.Average(rev => rev.Rating),
r.Name
})
This should give you a set of objects that have Average (the average of all reviews for that restaurant) and the Name of the restaurant.
This assumes that you have correctly setup the relationships so that Restaurant.Reviews only refers to the ones that match by ID.
If you don't have that relationship setup and you need to filter it yourself:
Resteurants.Select(r => new {
Average = Reviews.Where(rev => rev.ResteurantId == r.Id).Average(rev => rev.Rating),
r.Name
})
Firstly your models seems to have more aggregation than required, I have taken the liberty to trim it and remove extra fields, ideally all that you need a Relation ship between two models RestaurantId (Primary Key for Restaurant and Foreign Key (1:N) for Review)
public class Restaurant
{
public int RestaurantId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public virtual ICollection<Review> Reviews { get; set; }
}
public class Review
{
public int ReviewId { get; set; }
public double Rating { get; set; }
public int RestaurantId { get; set; }
public DateTime DateOfReview { get; set; }
}
If these are the models, then you just need List<Restaurant> restaurantList, since that internally contains the review collection, then all that you need is:
var result =
restaurantList.Select(x => new {
Name = x.Name,
Average = x.Reviews.Average(y => y.Rating)
}
);
In case collection aggregation is not there and you have separate ReviewList as follows: List<Review> reviewList, then do the following:
var result =
reviewList.GroupBy(x => x.RestaurantId, x => new {x.RestaurantId,x.Rating})
.Join(restaurantList, x => x.Key,y => y.RestaurantId,(x,y) => new {
Name = y.Name,
AvgRating = x.Average(s => s.Rating)
});
Also please note this will only List the Restaurants, which have atleast one review, since we are using InnerJoin, otherwise you need LeftOuterJoin using GroupJoin, for Restaurants with 0 Rating
I see your Restaurant class already has an ICollection<Review> that represents the reviews of the restaurant. This is probably made possible because you use Entity Framework or something similar.
Having such a collection makes the use of a join unnecessary:
var averageRestaurantReview = Restaurants
.Select(restaurant => new
.Where(restaurant => ....) // only if you don't want all restaurants
{
Name = restaurant.Name,
AverageReview = restaurants.Reviews
.Select(review => review.Rating)
.Average(),
});
Entity Framework will do the proper joins for you.
If you really want to use something join-like you'd need Enumerable.GroupJoin
var averageRestaurantReview = Restaurants
.GroupJoin(Reviews, // GroupJoin Restaurants and Reviews
restaurant => restaurant.Id, // From every restaurant take the Id
review => review.RestaurantId, // From every Review take the RestaurantId
.Select( (restaurant, reviews) => new // take the restaurant with all matching reviews
.Where(restaurant => ....) // only if you don't want all restaurants
{ // the rest is as before
Name = restaurant.Name,
AverageReview = reviews
.Select(review => review.Rating)
.Average(),
});

How to query a flatten sub collection in RavenDb? Index needed?

I am using RavenDb in C# web project. I have an object that I need to query its child collection with 1 row per child object and some of the root/parent object properties.
Note: This is not the actual design, just simplified for this question.
public class OrderLine
{
public string ProductName { get; set; }
public int Quantity { get; set; }
public DateTime? ShipDate { get; set; }
}
public class Order
{
public int OrderId { get; set; }
public string CustomerName { get; set; }
public DateTime OrderDate { get; set; }
public List<OrderLine> OrderLines { get; set; }
}
The order with the orderlines is one single document. ShipDate will be updated on each line because not all products are always in stock.
I need to be able to create a list of the last 10 products sent with the following columns:
OrderId
Customer
ProductName
ShipDate
This doesn't work because SelectMany is not supported:
var query = from helper in RavenSession.Query<Order>()
.SelectMany(l => l.OrderLines, (order, orderline) =>
new { order, orderline })
select new
{
helper.order.OrderId,
helper.order.CustomerName,
helper.orderline.ProductName,
helper.orderline.ShipDate
};
var result = query.Where(x => x.ShipDate.HasValue)
.OrderByDescending(x => x.ShipDate.Value).Take(10);
I believe the right thing to do isto create an Index that will flatten out the list but I haven't had any success. I don't believe a Map-Reduce situation will work because as I understand it will effectively does a group by which Reduces the number of documents to less rows (in the index). But in this case, I am trying to expand the number of documents to more rows (in the index).
I would rather not put each OrderLine in a separate document but I do not know what my options are.
Since you want to filter and sort by fields in the subclass, you'll need to make sure all the fields you want are indexed and stored.
public class ShippedItemsIndex
: AbstractIndexCreationTask<Order, ShippedItemsIndex.Result>
{
public class Result
{
public int OrderId { get; set; }
public string CustomerName { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public DateTime ShipDate { get; set; }
}
public ShippedItemsIndex()
{
Map = orders =>
from order in orders
from line in order.OrderLines
where line.ShipDate != null
select new
{
order.OrderId,
order.CustomerName,
line.ProductName,
line.Quantity,
line.ShipDate
};
StoreAllFields(FieldStorage.Yes);
}
}
Then you can project from the index into your results.
var query = session.Query<Order, ShippedItemsIndex>()
.ProjectFromIndexFieldsInto<ShippedItemsIndex.Result>()
.OrderByDescending(x => x.ShipDate)
.Take(10);
var results = query.ToList();
Here is a complete test demonstrating.

Linq SelectMany Usage

I unable to come up with a linq query for the following scenario.
public class Product
{
public virtual string ProductName { get; set; }
public virtual IList<SubProduct> SubProducts { get; set; }
}
public class SubProduct
{
public string SubProductName { get; set; }
public int SubProductTypeId { get; set; }
}
public class SubProductType
{
public int SubProductTypeId{ get; set; }
public string Description { get; set; }
}
var productList = List<Product>();
var subProductTypeLlist = List<SubProductType>();
I have a list of products and each product has list of SubProducts. I want to get the query to represent {ProductName, Description}. Please suggest how to write linq query.
Something like this should do the trick:
var result = productList
.SelectMany(p => p.SubProducts
.Select(sp => new { SubProduct = sp, ProductName = p.ProductName }))
.Select(sp =>
new { Description = subProductTypeList
.Single(spt => spt.SubProduct.SubProductTypeId == sp.SubProductTypeId).Description,
ProductName = sp.ProductName })
In the SelectMany, we first do a Select on the internal IEnumerable (IList implements IEnumerable) to convert each SubProduct object to an anonymous class holding the SubProduct object and the ProductName. The SelectMany then converts that to a flat list. We then use Select on that list to create a new anonymous class again, where this time, we grab the Description from subProductTypeList. The result is an IEnumerable of an anonymous class with the members Description and ProductName.

Categories

Resources