Create complex LINQ query - c#

For the part of database below I need find what team in season scored most goals per one match.
For that I used methods
public List<Tour> GetAllTours(Guid seasonGuid){...}
and
public List<SimpleMatch> GetMatches(Guid tour)
{
using (var db = new ConnectToDb())
{
if (!db.Matches.Any()) return new List<Match>();
var matches = db.Matches;
var matchesToReturn = new List<Match>();
foreach (var item in
matches
.Include(x => x.Home)
.Include(x => x.Guest)
.Include(x => x.Result)
.Include(x => x.Tour))
{
if (item.Tour.Id != tour)
matchesToReturn.Add(item);
}
return matchesToReturn;
}
}
and
public List<SimpleTeam> GetTeamMostGoalInSeason(List<Match> matches){...}
where SimpleTeam is a team with count goals, if teams with max count == many, used List
it's method not tiny, and I don't know how do this with LINQ query.

Not very efficient, but my idea would be something like this:
matches.SelectMany(match => new[] {
new TeamScore {
TeamId = match.HomeId,
Goals = match.Result.HomeTeamGoals
},
new TeamScore {
TeamId = match.GuestId,
Goals = match.Result.GuestTeamGoals
}
})
.GroupBy(score => score.Goals)
.OrderByDescending(group => group.Key)
.First()
.Select(score => score.TeamId);
where TeamScore is a simple struct.
This will return an IEnumerable with all the teams that scored the most goals (if it was the same number). You will probably have to change some of the property names or other details. I'm not familiar with LINQ to databases.
If you only want one, you could do First() but that would pick the first one and ignore others depending on the order. SingleOrDefault() would return null when it's a draw, if that's what you want.
EDIT: To get all matches in all tours, you would do something like this:
GetAllTours(...).SelectMany(tour => GetMatches(tour))
but since you're dealing with a database, you could just ask for all matches directly.

Related

C# Linq union multiple properties to one list

Basically I have an object with 2 different properties, both int and I want to get one list with all values from both properties. As of now I have a couple of linq queries to do this for me, but I am wondering if this could be simplified somehow -
var componentsWithDynamicApis = result
.Components
.Where(c => c.DynamicApiChoicesId.HasValue ||
c.DynamicApiSubmissionsId.HasValue);
var choiceApis = componentsWithDynamicApis
.Select(c => c.DynamicApiChoicesId.Value);
var submissionApis = componentsWithDynamicApis
.Select(c => c.DynamicApiSubmissionsId.Value);
var dynamicApiIds = choiceApis
.Union(submissionApis)
.Distinct();
Not every component will have both Choices and Submissions.
By simplify, I assume you want to combine into fewer statements. You can also simplify in terms of execution by reducing the number of times you iterate the collection (the current code does it 3 times).
One way is to use a generator function (assuming the type of items in your result.Components collection is Component):
IEnumerable<int> GetIds(IEnumerable<Component> components)
{
foreach (var component in components)
{
if (component.DynamicApiChoicesId.HasValue) yield return component.DynamicApiChoicesId.Value;
if (component.DynamicApiSubmissionsId.HasValue) yield return component.DynamicApiSubmissionsId.Value;
}
}
Another option is to use SelectMany. The trick there is to create a temporary enumerable holding the appropriate values of DynamicApiChoicesId and DynamicApiSubmissionsId. I can't think of a one-liner for this, but here is one option:
var dynamicApiIds = result
.Components
.SelectMany(c => {
var temp = new List<int>();
if (c.DynamicApiChoicesId.HasValue) temp.Add(c.DynamicApiChoicesId.Value);
if (c.DynamicApiSubmissionsId.HasValue) temp.Add(c.DynamicApiSubmissionsId.Value);
return temp;
})
.Distinct();
#Eldar's answer gave me an idea for an improvement on option #2:
var dynamicApiIds = result
.Components
.SelectMany(c => new[] { c.DynamicApiChoicesId, c.DynamicApiSubmissionsId })
.Where(c => c.HasValue)
.Select(c => c.Value)
.Distinct();
Similar to some of the other answers, but I think this covers all your bases with a very minimal amount of code.
var dynamicApiIds = result.Components
.SelectMany(c => new[] { c.DynamicApiChoicesId, c.DynamicApiSubmissionsId}) // combine
.OfType<int>() // remove nulls
.Distinct();
To map each element in the source list onto more than one element on the destination list, you can use SelectMany.
var combined = componentsWithDynamicApis
.SelectMany(x => new[] { x.DynamicApiChoicesId.Value, x.DynamicApiSubmissionsId.Value })
.Distinct();
I have not tested it but you can use SelectMany with filtering out the null values like below :
var componentsWithDynamicApis = result
.Components
.Select(r=> new [] {r.DynamicApiChoicesId,r.DynamicApiSubmissionsId})
.SelectMany(r=> r.Where(p=> p!=null).Cast<int>()).Distinct();

Using GroupBy to compute average or count based on the whole data until the corresponding date

I have the AssessmentItems DB object which contains the items about: Which user evaluated (EvaluatorId), which submission (SubmissionId), based on which rubric item (or criteria)(RubricItemId) and when (DateCreated).
I group by this object by RubricItemId and DateCreated to get compute some daily statistics based on each assessment criteria (or rubric item).
For example, I compute the AverageScore, which works fine and returns an output like: RubricItem: 1, Day: 15/01/2019, AverageScore: 3.2.
_context.AssessmentItems
.Include(ai => ai.RubricItem)
.Include(ai => ai.Assessment)
.Where(ai => ai.RubricItem.RubricId == rubricId && ai.Assessment.Submission.ReviewRoundId == reviewRoundId)
.Select(ai => new
{
ai.Id,
DateCreated = ai.DateCreated.ToShortDateString(),//.ToString(#"yyyy-MM-dd"),
ai.CurrentScore,
ai.RubricItemId,
ai.Assessment.SubmissionId,
ai.Assessment.EvaluatorId
})
.GroupBy(ai => new { ai.RubricItemId, ai.DateCreated })
.Select(g => new
{
g.Key.RubricItemId,
g.Key.DateCreated,
AverageScore = g.Average(ai => ai.CurrentScore),
NumberOfStudentsEvaluating = g.Select(ai => ai.EvaluatorId).Distinct().Count(),
}).ToList();
What I want to do is to compute the average until that day. I mean instead of calculating the average for the day, I want to get the average until that day (that is, I want to consider the assessment scores of the preceding days). The same why, when I compute NumberOfStudentsEvaluating, I want to indicate the total number of students participated in the evaluation until that day.
One approach to achieve this could be to iterate through the result object and compute these properties again:
foreach (var i in result)
{
i.AverageScore = result.Where(r => r.DateCreated <= i.DateCreated).Select(r => r.AverageScore).Average(),
}
But, this is quite costly. I wonder if it is possible to tweak the code a bit to achieve this, or should I start from scratch with another approach.
If you split the query into two halves, you can compute the average as you would like (I also computed the NumberOfStudentsEvaluating on the same criteria) but I am not sure if EF/EF Core will be able to translate to SQL:
var base1 = _context.AssessmentItems
.Include(ai => ai.RubricItem)
.Include(ai => ai.Assessment)
.Where(ai => ai.RubricItem.RubricId == rubricId && ai.Assessment.Submission.ReviewRoundId == reviewRoundId)
.Select(ai => new {
ai.Id,
ai.DateCreated,
ai.CurrentScore,
ai.RubricItemId,
ai.Assessment.SubmissionId,
ai.Assessment.EvaluatorId
})
.GroupBy(ai => ai.RubricItemId);
var ans1 = base1
.SelectMany(rig => rig.Select(ai => ai.DateCreated).Distinct().Select(DateCreated => new { RubricItemId = rig.Key, DateCreated, Items = rig.Where(b => b.DateCreated <= DateCreated) }))
.Select(g => new {
g.RubricItemId,
DateCreated = g.DateCreated.ToShortDateString(), //.ToString(#"yyyy-MM-dd"),
AverageScore = g.Items.Average(ai => ai.CurrentScore),
NumberOfStudentsEvaluating = g.Items.Select(ai => ai.EvaluatorId).Distinct().Count(),
}).ToList();

Rank (top 3) the User that most occur in N lists?

So, I have a List of N Images and each image I have a List that interacted. I need to Rank the user that most interact within Images (List).
Searching at stack i saw something about Intersect with LINQ, but is not what i need.
I am starting with development...so, i donĀ“t know too much about LINQ
If your image object has a property Users which is a List of the users that interacted with it, then you can use SelectMany on your list of images to take the lists of users for each image and flatten them into a single list:
listOfImages.SelectMany(image => image.Users)
This won't remove duplicates, so each user will appear once for each time they interacted with an image. You can then use GroupBy to group up 'duplicate' users into groups; the size of each group will be the number of times that user appears in the flattened list:
listOfImages.SelectMany(image => image.Users)
.GroupBy(user => user)
Then since you want the top three, use OrderByDescending to rank them by the number of times they appear, and Take(3) to then take the top three. Then you'll need to use Select to return a list of users, since GroupBy returns a generic type IGrouping. FirstOrDefault() returns the first item in each group, which is going to be a user, or null if the group is empty.
listOfImages.SelectMany(image => image.Users)
.GroupBy(user => user)
.OrderByDescending(group => group.Count())
.Take(3)
.Select(group => group.FirstOrDefault())
This should hopefully achieve what you want. I suggest you read up on LINQ so you understand how all its methods work - it's pretty useful once you know how to use it. Play around with it a bit so you get a feel for it - some methods like OrderBy and Take are pretty self-explanatory, others like SelectMany aren't quite so obvious.
Try something like this with linq will do the job
public class User
{
public User(int id)
{
Id = id;
}
public int Id { get; set; }
}
public class Image
{
public int Id { get; set; }
public List<User> Users { get; set; }
}
.......
public List<Tuple<int, int>> OrderByUser(IList<Image> cursos)
{
var data = cursos.SelectMany(x => x.Users
.Select(p => new { image = x, user = p }))
.GroupBy(x => x.user.Id)
.Select(x => new { userId = x.Key, total = x.Count() })
.OrderByDescending(x => x.total)
.Take(3);
data = data.OrderByDescending(x => x.total);
return data.Select(x=> new Tuple<int, int>(x.userId, x.total)).ToList();
}

Check if last Row Entity Framework through Select

Ok people, before bashing, this is a little bit different from what I've investigated.
I currently have the following code:
public async Task<ParticipantTournament> GetParticipantTournamentByDescending(int tournamentId, int participantId)
{
var response = await _dbRepositories.TournamentParticipantMatchRepository
.Where(x => x.TournamentId == tournamentId)
.OrderByDescending(y => y.TournamentMatch.RoundNumber)
.ThenByDescending(y => y.TournamentMatch.Id)
.Include(x => x.Tournament)
.Include(x => x.Participant1)
.Include(x => x.Participant2)
.Include(x => x.TournamentMatch)
.Select(z => new TournamentParticipantMatchLogicDto
{
IsLastMatch = OneTrue() // <==== Problem here,
TournamentParticipantMatch = z
}).Where(x => (x.TournamentParticipantMatch.Participant1Id == participantId || x.TournamentParticipantMatch.Participant2Id == participantId))
.ToListAsync();
return new ParticipantTournament
{
ParticipantMatches = response,
};
}
/**
* This may be something super dumb. But I couldnt' come up with something better.
* How do I detect the latest match?
* http://stackoverflow.com/questions/14687136/check-if-record-is-last-or-first-in-list-with-linq
*
* Well, after digging a little bit, I've found that this is not possible :o
* */
private bool OneTrue()
{
if (!IsLast) return false;
IsLast = false;
return true;
}
I am building a tournament platform. I need to know which is the last match so I can give the players 5 rounds instead of 3. Instead of creating a new column and filling it with false or true, I decided to filter it out. I thought that I could take advantage of LINQ's deferred execution:
I would Select the whole data set from the tournament.
I would then order it by descending and select the first row as the last one. (All the matches were inserted in order, so the biggest id is the last one)
Then filter out which are from the users and which are not.
Possible solutions I think it could work:
- Create a boolean column that will hold "true" value for the last matches.
- Using Specification Pattern (Which I don't know how to apply in this situation. Tried using Expressions and Func but couldn't map them correctly)
- Load all the Ids and select the last one of those Ids. Compare those Ids with the ones that all the users have. Unfortunately this would add an extra roundtrip to the database.
What should I do?
Thanks!
P.S: The OneTrue() method it does what it does, it returns true once, and then it returns false. (Didn't find anything else after a quick Google search)
Edit
For clarification purposes:
The tables show a simulation of the data I currently have. I only need to extract what the current user needs, so I don't need the other 2 rows (which you can see in table #2). Once I select those two rows I exclude the other ones, which could potentially have the last match, but by only selecting this 2 rows I will not know. I'm trying to save any redundancy by trying to query it from the first try. I know that the last match Id is the last of the tournament
So what I was trying to do, is to order them all by descending (because they are in order), and select the last one as the last match.
Could you not just query your subset of data after your return it with linq, like:
var temp = from e in _dbRepositories.TournamentParticipantMatchRepository
where (from f in _dbRepositories.TournamentParticipantMatchRepository
where f.TournamentId == tournamentId)
.Include(x => x.Tournament)
.Include(x => x.Participant1)
.Include(x => x.Participant2)
.Include(x => x.TournamentMatch)
.Select(z => new TournamentParticipantMatchLogicDto
{
IsLastMatch = false, // <==== Problem here,
TournamentParticipantMatch = z
}).Where(x => (x.TournamentParticipantMatch.Participant1Id == participantId || x.TournamentParticipantMatch.Participant2Id == participantId))
.ToListAsync();
int maxResult= temp.Max(t => t.TournamentParticipantMatch.Id);
var update= temp.SingleOrDefault(x => x.TournamentParticipantMatch.Id== maxResult);
if(update!= null)
update.IsLastMatch= true;
This is what I ended up doing. Please if you see any improvements let me know!
public async Task<ParticipantTournament> GetParticipantTournamentByDescending(int tournamentId, int participantId)
{
var lastMatchId = await _dbRepositories.TournamentParticipantMatchRepository
.Where(x => x.TournamentId == tournamentId)
.OrderByDescending(y => y.TournamentMatch.RoundNumber)
.ThenByDescending(y => y.TournamentMatch.Id)
.Select(x => x.Id).FirstOrDefaultAsync();
var response = await _dbRepositories.TournamentParticipantMatchRepository
.Where(x => x.TournamentId == tournamentId)
.Where(x => (x.Participant1Id == participantId || x.Participant2Id == participantId))
.Include(x => x.Tournament)
.Include(x => x.Participant1)
.Include(x => x.Participant2)
.Include(x => x.TournamentMatch)
.ToListAsync();
var logic = response.Select(z=> new TournamentParticipantMatchLogicDto
{
IsLastMatch = z.Id == lastMatchId,
TournamentParticipantMatch = z
})
;
return new ParticipantTournament
{
ParticipantMatches = logic,
};
}

Retain default order for Linq Contains

I want to retain the default order that comes from sql, after processing by Linq also.I know this question has been asked before. Here is a link Linq Where Contains ... Keep default order.
But still i couldn't apply it to my linq query correctly. could anyone pls help me with this? Thanks!
Here is the query
var x = db.ItemTemplates.Where(a => a.MainGroupId == mnId)
.Where(a => a.SubGruopId == sbId)
.FirstOrDefault();
var ids = new List<int> { x.Atribute1, x.Atribute2, x.Atribute3, x.Atribute4 };
var y = db.Atributes.Where(a => ids.Contains(a.AtributeId))
.Select(g => new
{
Name = g.AtributeName,
AtType = g.AtributeType,
Options = g.atributeDetails
.Where(w=>w.AtributeDetailId!=null)
.Select(z => new
{
Value=z.AtributeDetailId,
Text=z.AtDetailVal
})
});
Your assumption is wrong. SQL server is the one that is sending the results back in the order you are getting them. However, you can fix that:
var x = db.ItemTemplates.Where(a => a.MainGroupId == mnId)
.Where(a => a.SubGruopId == sbId)
.FirstOrDefault();
var ids = new List<int> { x.Atribute1, x.Atribute2, x.Atribute3, x.Atribute4 };
var y = db.Atributes.Where(a => ids.Contains(a.AtributeId))
.Select(g => new
{
Id = g.AtributeId,
Name = g.AtributeName,
AtType = g.AtributeType,
Options = g.atributeDetails
.Where(w=>w.AtributeDetailId!=null)
.Select(z => new
{
Value=z.AtributeDetailId,
Text=z.AtDetailVal
})
})
.ToList()
.OrderBy(z=>ids.IndexOf(z.Id));
Feel free to do another select after the orderby to create a new anonymous object without the Id if you absolutely need it to not contain the id.
PS. You might want to correct the spelling of Attribute, and you should be consistent in if you are going to prefix your property names, and how you do so. Your table prefixes everything with Atribute(sp?), and then when you go and cast into your anonymous object, you remove the prefix on all the properties except AtributeType, which you prefix with At. Pick one and stick with it, choose AtName, AtType, AtOptions or Name, Type, Options.

Categories