Build bidimensional table based on many-to-many EF relationship - c#

I want to build a bi-dimensional table based on a Driver/Race/Points relationships.
Models:
public class Race
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public DateTime RaceStart { get; set; }
public Circuit Circuit { get; set; }
public virtual ICollection<DriverRacePoint> DriverRacePoints { get; set; }
}
public class Driver
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public int Standing { get; set; }
public virtual ICollection<DriverRacePoint> DriverRacePoints { get; set; }
}
public class DriverRacePoint
{
[Key, Column("Driver_Id", Order = 0)]
public int Driver_Id { get; set; }
[Key, Column("Race_Id", Order = 1)]
public int Race_Id { get; set; }
[ForeignKey("Driver_Id")]
public virtual Driver Driver { get; set; }
[ForeignKey("Race_Id")]
public virtual Race Race { get; set; }
public string Points { get; set; }
}
The result view:
| Race1 | Race2 | Race3 | Race4 |
Driver1 | points | points | points | points | total points
Driver2 | points | points | points | points | total points
Driver3 | points | points | points | points | total points
The lines order is not by the total but by the Driver.Standing
How do I build the ViewModel from the DbContext to get the result view?
Here is an example of the result I need:
http://en.espnf1.com/f1/motorsport/season/138549.html?template=standings

I don't understand why the Points property in the DriverRacePoint entity is string, but if you change that property for a numeric type, for example int, you could do this to obtain the info that you need:
using (var db = new YourDbContext())
{
var query = (from driver in db.Drivers
join drp in db.DriverRacePoints on driver.Id equals drp.Driver_Id
join race in db.Races on drp.Race_Id equals race.Id
where race.RaceStart.Year==2014 //races for an specific year
group new {driver,drp, race} by driver.Id into g
select new {DriverId=g.Key,// The driver Id
RacesWithPoints=g.Select(a=>new {a.race,a.drp.Points}),// All the races ( and the points X race) where the driver participates
TotalPoints=g.Sum(a=>a.drp.Points)// Total of points
}).OrderByDescending(e=>e.TotalPoints);
//use the data that you get from this query
}
Update 1
If you need the driver info then you can modify the query this way:
var query = (from driver in db.Drivers
join drp in db.DriverRacePoints on driver.Id equals drp.Driver_Id
join race in db.Races on drp.Race_Id equals race.Id
where race.RaceStart.Year==2014 //races for an specific year
group new {driver,drp, race} by driver.Id into g
select new { DriverName = g.FirstOrDefault().driver.Name,
DriverUrlSlug = g.FirstOrDefault().driver.UrlSlug,
// All the races ( and the points X race) where the driver participates
RacesWithPoints = g.Select(a => new StandingRacePointsVM { RacePoints = a.drp.Points == null ? -1 : a.drp.Points }).ToList(),
TotalPoints = g.Sum(a => a.drp.Points),
Standing = g.FirstOrDefault().driver.Standing
}).OrderBy(d => d.Standing).ToList();
For the list of races in the header, how do suggest that I build it?
Well, the problem is you don't know if some driver was involved in all the races. So I think you need to do another query for that:
var query2 = from race in db.Races
where race.RaceStart.Year == 2014
select new {race.Id, race.Name};
Update 2
I think this query is better for you but I can't test it because I don't have your model and data. This query will get you all the drivers, and for each driver you will get the list of all the races, including where he was not involved:
var query3 = (from driver in db.Drivers
join drp in db.DriverRacePoints on driver.Id equals drp.Driver_Id
from race in db.Races
where race.RaceStart.Year == 2014
select new { driver,drp, race })
.GroupBy(e => e.driver.Id)
.Select(g => new
{
DriverName = g.FirstOrDefault().driver.Name,
RacesWithPoints = g.Select(a => new { a.race.Name, Points = a.drp.Race_Id == a.race.Id ? 0 : a.drp.Points }), // All the races (if the driver was involved => you have the Points value, otherwise, the value is 0 )
TotalPoints = g.Sum(a => a.drp.Points)// Total of points
}).OrderByDescending(e=>e.TotalPoints);
Now to create the header, you can choose the first driver an iterate foreach race using the Name.

Related

Entity Left Join to Replace SQL

I am accustomed to using SQL left joins to get a list of all available options and determine what items are currently selected. My table structure would look like this
Table MainRecord -- recordId | mainRecordInfo
Table SelectedOptions -- recordId | optionId
Table AvailableOptions -- optionId | optionValue
and my query would look like this
SELECT optionValue, IIF(optionId IS NULL, 0, 1) AS selected
FROM AvailableOptions AS a
LEFT JOIN SelectedOptions AS s ON s.optionId = a.optionId AND a.recordId = 'TheCurrentRecord'
I am trying to replace this with Entity Framework, so I need help with both a model and a query -- they both need corrected.
public class MainRecord
{
public int recordId { get; set; }
public string mainRecordInfo { get; set; }
[ForeignKey("recordId")]
public List<SelectedOptions> selectedOptions { get; set; }
}
public class SelectedOptions
{
public int recordId { get; set; }
public int optionId { get; set; }
}
public class AvailableOptions
{
public int optionId { get; set; }
public string optionValue { get; set; }
}
Query
IQueryable<AvailableOptions> options = from o in context.AvailableOptions select o;
I can get a list of AvailableOptions, but how can I get a list and know which ones are selected?
If the number of selections and available options is small enough, you can do this in memory:
var selected = options.Join(record.selectedOptions, ao => ao.optionId, so => so.optionId, (a, s) => new { Available = a, Selected = s });
selected will now be a list of objects with Available and Selected as properties and will only contain those that matched in optionId value.
If you only wish to get a pure list of AvailableOptions that match, simply chain a Select to the join:
var selected = options.Join(record.selectedOptions, ao => ao.optionId, so => so.optionId, (a, s) => new { Available = a, Selected = s })
.Select(o => o.Available);
Not a complete answer, but it is really good to understand the navigational properties that you get from the model. Here is a query that most likely isn't exactly what you want but that demonstrate it
from ao in _db.AvailableOptions
where ao.recordId == "TheCurrentRecord" && ao.SelectedOptions.OptionId == 1
select new
MyPoxo {ao.SelectedOptions.Value ?? 0};
so instead of just having o you navigate through the joins that gets specified by the FKs. In this example I would assum AvailableOptions would have a linke to SelectedOptions.

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

Entity Framework : How to groupjoin? (Query users within location with additional requirements)

Scenario:
A user can assign himself multiple categories (ex. "fishing", "stackoverflowfan", etc..)
User A wants to befriend(connection) a similar user B.
There are plenty of similar user B's, so user A wants to narrow it down based on location & activity of user B.
Also user A obviously doesn't want someone he or she already befriended or blocked.
How would I implement this in Entity Framework (Core)?
I've got it to work for just finding based on location and activity date and I also think filtering on connections(befriending) and blocked users, but filtering it on those categories got me scratching my head a bit.
The result that I picture in my head:
If there would be x-amount of users in the system it should get:
1) closest users in x-radius (ex. 10 km/miles)
2) based on those closest users, order it based on similar categories
3) cool, we have users with similar categories, now let's see who of those users was recently online, but categories are more important and maybe also a order by closest location? or is that too much?
UserId | Location | ActivityDate | Categories
----------------------------------------------------------------
21 | point( lon lat) within radius | 2017-26-01 | {1,2,12,3,5} (5 common categories)
12 | point( lon lat) within radius | 2017-26-06 | {1,2,12} (3 common categories)
13 | point( lon lat) within radius | 2017-26-07 | {1,2} (2 common categories)
44 | point( lon lat) within radius | 2017-26-07 | {} (0 common categories)
25 | point( lon lat) within radius | 2017-26-06 | {} (0 common categories)
33 | point( lon lat) within radius | 2017-26-05 | {} (0 common categories)
Extra: While we are on a streak, I want activitydate (so last login) not just ordered but for example maximum 2 weeks old, because no use in befriending a user who hasn't been online in decades, no matter how many similar categories they have.
Here is a simple example of the models:
User Model:
public class User
{
public int Id { get; set; }
public DateTime ActivityDate { get; set; }
public Location Location { get; set; }
//User can have multiple connections
public ICollection<Connection> Connections { get; set; }
//User can have multiple categories
public ICollection<UserCategory> UserCategory { get; set; }
}
Category Model:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
//Category can have multiple users
public ICollection<UserCategory> UserCategory { get; set; }
}
Intersection Table UserCategory (ManyToMany - User with Categories):
public class UserCategory
{
public int UserId { get; set; }
public User User { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
Intersection Table Connection (ManyToMany - User with User):
public class Connection
{
public int UserId { get; set; }
public User User { get; set; }
public int UserConnectionId { get; set; }
public User UserConnection { get; set; }
public bool IsBlocked { get; set; }
}
This is what I have so far:
return await myDBContext.User.AsNoTracking().FromSql(
#"DECLARE #from geography = geography::STPointFromText([Location])', 4326);
DECLARE #to geography = geography::STPointFromText('POINT(' + CAST({0} AS VARCHAR(20)) +' ' + CAST({1} AS VARCHAR(20)) +')', 4326);
SELECT *
FROM dbo.User
WHERE #from.STDistance(#to) <= #p0",
requestor.Location.Longitude,
requestor.Location.Latitude,
requestor.Appsetting.Radius)
.Where(x => !x.Connections.Any(y => y.UserId == requestor.Id))
.GroupJoin(/*do I use groupjoin?...*/).OrderBy(/*...*/).ThenBy(x => x.ActivityDate) //<=========
.Skip(skip)
.Take(take)
.ToListAsync();
P.S: I'm working in Entity Framework Core (so DBGeography is out of the question)
Any push in the right direction is very much appriciated!
Thank you!
You don't need a GroupJoin because you can use the existing navigation properties to let EF generate the required joins in SQL. The gist of this query is that a user's categories is intersected with another user's categories:
var users = myDBContext.User
.AsNoTracking().FromSql(...)
.Where(x => !x.Connections.Any(y => y.UserId == requestor.Id));
var result = (from u1 in users
from u2 in u1.Connections.Select(c => c.UserConnection)
let commonCategories = u1.UserCategory.Select(uc1 => uc1.CategoryId)
.Intersect(u2.UserCategory.Select(uc2 => uc2.CategoryId))
orderby commonCategories.Count(), u1.ActivityDate
select new
{
u2.Id,
u2.Location,
u2.ActivityDate,
CommonCategories = commonCategories.Select(cc => cc.Id)
})
.AsEnumerable()
.Select(x => new
{
x.Id,
x.Location,
x.ActivityDate,
Categories = string.Join(",", x.CommonCategories)
});
The AsEnumerable() statement is put in-between to continue the query execution as LINQ-to-objects, because LINQ-to-entities doesn't support string.Join.
Note: I'm not sure if you'd want the first user's Id in there too. This query produces a list of connected users per user in users, so without the first user's Id you can't tell to which user each row belongs. It's not necessary if you execute this query for one user.

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

LINQ MVC ViewModel: Multiple joins to the same table with optional field

Given the following database structure
Category
ID
CategoryNameResID
ParentCategory(Optional)
Resources
ID
Text
Lang
And given a ViewModel
public class CategoryViewModel
{
public int ID { get; set; }
public int CategoryNameResID { get; set; }
public string CategoryName { get; set; }
public int ParentCategory { get; set; }
public string ParentCategoryName { get; set; }
}
I want to get the list of all Categories with ParentCategoryName included
What I did so far is:
var categories = (from cat in db.Categories
join res in db.Resources on cat.CategoryNameResID equal res.ID
select new CategoryViewModel{
ID = cat.ID,
CategoryNameResID = cat.CategoryNameResID,
CategoryName = res.Text,
ParentCategory = cat.ParentCategory,
ParentCategoryName = (from p in db.Resources
where p.ID == cat.ParentCategory
select p.Text)
}).ToList();
I can't figure out how to get the ParentCategoryName without having to iterate again, which is definitely wrong.
Try this:
(from cat in cats
join res in resources on cat.ResId equals res.Id let categoryName = res.Text
join cat1 in cats on cat.ParentId equals cat1.Id into parentJoin
from pj in parentJoin.DefaultIfEmpty() let parentCatResId =pj==null?0: pj.ResId
join res1 in resources on parentCatResId equals res1.Id into resJoin
from res2 in resJoin.DefaultIfEmpty() let parentName = (res2==null?string.Empty:res2.Text)
select new CategoryVM
{
Id = cat.Id,
ResId = cat.ResId,
CatName = categoryName,
ParentId = cat.ParentId,
ParentName = parentName
}).ToList();
Say you have the following data with your tables
dbo.Categories
ID CategoryNameResID ParentCategory
1 1 NULL
2 2 NULL
3 3 1
4 4 NULL
5 5 4
6 6 4
7 7 4
dbo.Resources
ID Text Lang
1 Standard en-GB
2 Custom en-GB
3 Standard Oversize en-GB
4 Loose en-GB
5 Loose 2F Set en-GB
6 Loose (4” Scale) en-GB
7 Loose (6” Scale) en-GB
The following LINQ statements will output the desired results:
public class CategoryViewModel
{
public int ID { get; set; }
public int CategoryNameResID { get; set; }
public string CategoryName { get; set; }
public int? ParentCategory { get; set; }
public string ParentCategoryName { get; set; }
}
var categories = (from cat in Categories
join res in Resources on cat.CategoryNameResID equals res.ID let categoryName = res.Text
select new CategoryViewModel
{
ID = cat.ID,
CategoryNameResID = cat.CategoryNameResID,
CategoryName = categoryName,
ParentCategory = cat.ParentCategory,
ParentCategoryName = Resources.FirstOrDefault(r => r.ID == cat.ParentCategory).Text
}).ToList();
foreach(var c in categories)
{
Console.WriteLine(c.CategoryName + " - " + c.ParentCategoryName);
}
// Prints
Standard -
Custom -
Standard Oversize - Standard
Loose -
Loose 2F Set - Loose
Loose (4” Scale) - Loose
Loose (6” Scale) - Loose

Categories

Resources