How to combine Join with an aggregate function and group by - c#

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(),
});

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.

writing LINQ query to pivot the result

I have developed a LINQ query. Now my requirement is to create pivot query from it. I am new to LINQ, I do not know how to proceed further. Please see the attached the attached result image.
public JsonResult SchoolNikashaRpt()
{
try
{
var temp = (from n in db.Nikashas
join s in db.Schools on n.SchoolId equals s.SchoolId
join k in db.Programs on n.ProgramId equals k.ProgramId
orderby n.SchoolId
select new RptSchoolsNikashaViewModel
{
SCHOOL_NAME = s.SCHOOL_NAME
,PROGRAM_NAME = k.PROGRAM_NAME
,MAPPED_AMOUNT = n.MAPPED_AMOUNT
}).ToList();
return Json(temp, JsonRequestBehavior.AllowGet);
}
catch (Exception)
{
throw;
}
}
Involved model classes are as follows::
public class NikashaModels
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int NIKASHAId { get; set; }
[Required]
public decimal MAPPED_AMOUNT { get; set; }
[ForeignKey("ProgramId")]
public ProgramModels Program { get; set; }
public int ProgramId { get; set; }
[ForeignKey("SchoolId")]
public SchoolModels School { get; set; }
public int SchoolId { get; set; }
}
public class SchoolModels
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int SchoolId { get; set; }
public string SCHOOL_NAME { get; set; }
}
public class ProgramModels
{
[Key]
public int ProgramId { get; set; }
public string PROGRAM_NAME { get; set; }
}
So you have Nikashas, Schools and Programs. There is a one-to-many relation: Every Nikasha has exactly one School, and exactly one Program, namely the School and Program that the foreign key refers to. In the other direction: Every School has zero or more Nikashas, every Program has zero or more Nikashas.
You want for every Nikasha, some properties, and some information of its School and its Program. For this you can use one of the overloads of Enumerable.Join, if more than two tables are involved it is easier to do this using Enumerab.Select
You didn't mention it, but because I see the word db, it seems to me that you are fetching the data from a DbContext, so your Enumerables are IQueryable<...>. This doesn't influence the answer very much:
IQueryable<Nikasha> nikashas = db.Nikashas
// only if you don't want all Nikashas:
.Where(nikasha => ...);
IQueryable<School> schools = ...
IQueryable<Program> programs = ...
var result = nikashas.Select(nikasha => new RptSchoolsNikashaViewModel
{
// Get the name of the one and only School of this nikasha
SchoolName = schools
// Keep only the School that the foreign key refers to:
.Where(school => school.Id == nikasha.SchoolId)
// Select the name of the School
.Select(school => school.SchoolName)
// and take the first element
.FirstOrDefault(),
// Do something similar with the program name:
ProgramName = programs.Where(program => program.Id == nikasha.ProgramId)
.Select(program => program.ProgramName)
.FirstOrDefault(),
MappedAmount = nikasha.MappedAmount,
});
In words: from every Nikasha, make one new RptSchoolsNikashaViewModel. For this RptSchoolsNikashaViewModel use the MappedAmount of the Nikasha. To get the SchoolName, take all Schools that have a primary key value equal to the foreign key in the Nikasha. From the remaining schools (probably only one), take the name of the School. Finally take the first item from the remaining school names. Do something similar for ProgramNames.

How can I link two classes, select and still get a list output using Entity Framework and LINQ?

I have two objects: PhraseCategory and Phrase. Here's the classes:
public class PhraseCategory
{
public System.Guid PhraseCategoryId { get; set; } // PhraseCategoryId
public int PhraseCategoryShortId { get; set; } // PhraseCategoryShortId (Primary key)
public int PhraseCategoryGroupId { get; set; } // PhraseCategoryGroupId
public string Name { get; set; } // Name (length: 20)
// Reverse navigation
public virtual System.Collections.Generic.ICollection<Phrase> Phrases { get; set; } // Phrase.FK_PhrasePhraseCategory
}
public class Phrase : AuditableTable
{
public System.Guid PhraseId { get; set; } // PhraseId (Primary key)
public string English { get; set; } // English
public int? CategoryId { get; set; } // CategoryId
// Foreign keys
public virtual PhraseCategory PhraseCategory { get; set; } // FK_PhrasePhraseCategory
}
Can someone tell me how I could join these so that I am able to select all the phrases with for example a PhraseCategoryGroupId of 25.
Here's what I have right now but it does not take into account my need to also be able to select the Phrases with a PhraseCategory that has a PhraseCategoryGroupId:
List<Phrase> phrases;
var query = db.Phrases.AsQueryable();
if (options.CreatedBy != 0) query = query
.Where(w => w.CreatedBy == options.CreatedBy);
phrases = await query
.AsNoTracking()
.ToListAsync();
return Ok(phrases);
Note that I would like to get just a flat output (hope that makes sense). What I mean is a list that contains just:
PhraseId, English and CategoryId
This should get you what you need:
phrases = phrases.Where( x => x.PhraseCategory.PhraseCategoryGroupId == 25 )
.Select( x => new
{
PhraseId = x.PhraseId,
English = x.English,
CategoryId = x.CategoryId
});
Please note that you can also create instances of another type instead of the anonymous type which I am creating in the above query.
Also, the PhraseCategory will be lazy loaded in the above query since you have lazy loading enabled on the property: it is virtual. If you have lazy loading disabled globally, then you will need to use the Include method in your query. Then your query will become:
phrases = phrases.Include(x => x.PhraseCategory)
.Where( x => x.PhraseCategory.PhraseCategoryGroupId == 25 )
.Select( x => new
{
PhraseId = x.PhraseId,
English = x.English,
CategoryId = x.CategoryId
});

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.

Assign aggregate result to the entity property not pulling all subquery rows

I have a Comment and Votes related to the comment.
[Table("QAComment")]
public class QaComment : IEntity
{
[Key, Column("QACommentID")]
public int Id { get; set; }
// ...
public virtual ICollection<QaCommentVote> Votes { get; set; }
[NotMapped]
public int OverallVote { get; set; }
}
[Table("QACommentVote")]
public class QaCommentVote : IEntity
{
[Key, Column("QACommentVoteID")]
public int Id { get; set; }
[ForeignKey("QAComment")]
public int QaCommentId { get; set; }
public int Value { get; set; }
public virtual QaComment QaComment { get; set; }
}
I need to get comments with the sum of their votes, not pulling all votes to the application.
The ways I can see to achive this:
1. Make a database view for Commment and calc votes sum in there.
Cons: dont wanna make extra-views
2. Via LINQ:
var comments =
Set<QaComment>()
.Select(c => new QaComment() {/* assign every property once again and calc OverallVote */});
Cons: don't like to assign allproperties once again.
Is there a better way devoid of that cons?
UPDATE
This is what I want as a result of LINQ:
SELECT
qac.*,
(SELECT SUM(v.Value)
FROM QACommentVote v
WHERE v.QACommentID = qac.QACommentID) as OverallVote
FROM QAComment qac
You can fetch QaComment and the sum you're looking for separately as anonymous type and merge them into one object using LINQ to Objects:
var comments
= Set<QaComment>()
.Select(c => new { c, sum = c.Votes.Sum(v => v.Value))
.AsEnumerable() // to make next query execute as LINQ to Objects query
.Select(x => { x.c.OverallVote = x.sum; return x.c; })
.ToList();
But to make point clear: I haven't tested that :)

Categories

Resources