LINQ GroupBy Aggregation with AutoMapper - c#

Trying to get a query to work, but honestly not sure how (or if it's even possible) to go about it as everything I have tried hasn't worked.
Querying a total of 6 tables: Person, PersonVote, PersonCategory, Category, City, and FirstAdminDivision.
PersonVote is a user review table for people and contains a column called Vote that is a decimal accepting a value from 1-5 (5 being "best"). FirstAdminDivision would be synonymous with US states, like California. Person table has a column called CityId which is the foreign key to City. The other tables I believe are mostly self-explanatory so I won't comment unless needed.
My goal is create a query that returns a list of the "most popular" people which would be based on the average of all votes on the PersonVote table for a particular person. For instance, if a person has 3 votes and all 3 votes are "5" then they would be first in the list...don't really care about secondary ordering at this point...eg...like most votes in a tie would "win".
I have this working without AutoMapper, but I love AM's ability to do projection using the ProjectTo extension method as the code is very clean and readable and would prefer to use that approach if possible but haven't had any luck getting it to work.
Here is what I have that does work....so basically, I am trying to see if this is possible with ProjectTo instead of LINQ's Select method.
List<PersonModel> people = db.People
.GroupBy(x => x.PersonId)
.Select(x => new PersonModel
{
PersonId = x.FirstOrDefault().PersonId,
Name = x.FirstOrDefault().Name,
LocationDisplay = x.FirstOrDefault().City.Name + ", " + x.FirstOrDefault().City.FirstAdminDivision.Name,
AverageVote = x.FirstOrDefault().PersonVotes.Average(y => y.Vote),
Categories = x.FirstOrDefault().PersonCategories.Select(y => new CategoryModel
{
CategoryId = y.CategoryId,
Name = y.Category.Name
}).ToList()
})
.OrderByDescending(x => x.AverageVote)
.ToList();

By looking at your code sample I tried to determine what your models would be in order to setup an example. I only implemented using a few of the properties to show the functionality:
public class People
{
public int PeronId { get; set; }
public string Name { get; set; }
public City City { get; set; }
public IList<PersonVotes> PersonVoes { get; set; }
}
public class PersonVotes
{
public int Vote { get; set; }
}
public class City
{
public string Name { get; set; }
}
public class FirstAdminDivision
{
public string Name { get; set; }
}
public class PersonModel
{
public int PersonId { get; set; }
public string Name { get; set; }
public string LocationDisplay { get; set; }
public double AverageVote { get; set; }
}
To use the ProjectTo extension I then initialize AM through the static API:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<IEnumerable<People>, PersonModel>()
.ForMember(
model => model.LocationDisplay,
conf => conf.MapFrom(p => p.FirstOrDefault().City.Name))
.ForMember(
model => model.AverageVote,
conf => conf.MapFrom(p => p.FirstOrDefault().PersonVoes.Average(votes => votes.Vote)));
});
So given the following object:
var people = new List<People>()
{
new People
{
PeronId = 1,
City = new City
{
Name = "XXXX"
},
PersonVoes = new List<PersonVotes>
{
new PersonVotes
{
Vote = 4
},
new PersonVotes
{
Vote = 3
}
}
}
};
I would then a have query:
var result = people
.GroupBy(p => p.PeronId)
.Select(peoples => peoples)
.AsQueryable()
.ProjectTo<PersonModel>();
I'm just using in memory objects so that is why I convert to IQueryable to use the ProjectTo extension method in AM.
I'm hoping this was what you're looking for. Cheers,
UPDATED FOR LINQ TO ENTITIES QUERY:
var result = db.People
.GroupBy(p => p.PersonId)
.ProjectTo<PersonModel>(base.ConfigProvider) // AM requires me to pass Mapping Provider here.
.OrderByDescending(x => x.AverageVote)
.ToList();

Related

NHibernate QueryOver C#, selecting a specific property of a joined Entity to List field within a Dto

I was wondering how to do the following in QueryOver (NHibernate 4.0.4)
Say I have this set of classes
class Student
{
public virtual long Id { get; set; }
public virtual IList<Exam> Exams { get; set; }
}
class Exam
{
public virtual string Subject { get; set; }
public virtual int Score { get; set; }
//Many more unneeded properties
}
class StudentDto
{
public long Id { get; set; }
public IList<string> Subjects { get; set; }
public IList<int> Scores { get; set; }
}
How would I go about getting all students along with their Subjects and Scores without fetching the whole entities?
The functionality I am going for is:
foreach(var exam in student.Exams)
{
dto.Subjects.Add(exam.Subject);
dto.Scores.Add(exam.Score);
}
But hopefully using some of NHibernate's functionality like SelectList
Basically what I am trying to do is something along these lines:
StudentDto dto = null;
Student student = null;
Exam exam = null;
QueryOver<Student>(() => student)
.SelectList(list => list
.Select(st => st.Exams.Select(x => x.Subject)).WithAlias(() => dto.Subjects)
.Select(st => st.Exams.Select(x => x.Score)).WithAlias(() => dto.Scores)
)
I've tried the above, I get an exception that the object is out of scope
QueryOver<Student>(() => student)
.SelectList(list => list
.Select(() => student.Exams.Select(x => x.Subject)).WithAlias(() => dto.Subjects)
.Select(() => student.Exams.Select(x => x.Score)).WithAlias(() => dto.Scores)
)
I've tried this, throws a null reference exception, I've also tried the above with a ternary operator to check for whether or not the lists are null and pass an empty list if so, didn't work
EDIT
I would like to return a list of StudentDtos with every one of them containing a list of the respective student's Scores and Subjects.
So a structure somewhat resembling this
StudentDto
-> long ID
-> List<string> subjects -> Math
-> Physics
-> etc..
-> List<int> scores -> 100
-> 94
-> etc..
That's what JoinAlias is for. If the goal is to get students by some filter then this might look like this
Exam exam = null;
var data = session.QueryOver<Student>()
//.Where( whatever)
.JoinAlias(s => s.Exams, () => exam)
.Select(s => s.Id, s => exam.Subject, s => exam.Score)
.OrderBy(s => s.Id).Asc
.List<object[]>();
var studentDtos = new List<StudentDto>();
foreach (var item in data)
{
var id = (long)item[0];
StudentDto current = studentDtos.LastOrDefault();
if (studentDtos.Count == 0 || current.Id != id)
{
studentDtos.Add(current = new StudentDto { Id = id });
}
current.Subjects.Add((string)item[1]);
current.Scores.Add((int)item[2]);
}

How to improve the performance of a LINQ query that compares two lists?

The query below does what I want, but it is very slow when the two lists have many items (> 300 thousand).
Basically, it returns all people on list 2 who do not have documents in list 1.
personList1.Add(person1);
personList1.Add(person2);
personList2.Add(person2);
personList2.Add(person3);
var result = personList2
.Where(p2 => p2.documents
.Exists(d2 => !personList1
.Exists(p1 => p1.documents
.Contains(d2)
)
)
).ToList();
result.ForEach(r => Console.WriteLine(r.name));
//Should return person3 name
Classes
public class Person
{
public string name { get; set; }
public List<IdentificationDocument> documents { get; set; }
public Person()
{
documents = new List<IdentificationDocument>();
}
}
public class IdentificationDocument
{
public string number { get; set; }
}
Full code
https://dotnetfiddle.net/gS57gV
Anyone know how to improve query performance? Thank you!
Put all relevant data in a structure made for lookup first:
var lookup = new HashSet<string>(personList1.SelectMany(p => p.documents).Select(d => d.number));
var result = personList1.Where(p => !p.documents.Select(d => d.number).Any(lookup.Contains));

Prevent SELECT N+1 issue in Entity Framework query using Joins

I'm trying to query something from an indirectly related entity into a single-purpose view model. Here's a repro of my entities:
public class Team {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<Member> Members { get; set; }
}
public class Member {
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class Pet {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public Member Member { get; set; }
}
Each class is in a DbSet<T> in my database context.
This is the view model I want to construct from a query:
public class PetViewModel {
public string Name { get; set; }
public string TeamItIndirectlyBelongsTo { get; set; }
}
I do so with this query:
public PetViewModel[] QueryPetViewModel_1(string pattern) {
using (var context = new MyDbContext(connectionString)) {
return context.Pets
.Where(p => p.Name.Contains(pattern))
.ToArray()
.Select(p => new PetViewModel {
Name = p.Name,
TeamItIndirectlyBelongsTo = "TODO",
})
.ToArray();
}
}
But obviously there's still a "TODO" in there.
Gotcha: I can not change the entities at this moment, so I can't just include a List<Pet> property or a Team property on Member to help out. I want to fix things inside the query at the moment.
Here's my current solution:
public PetViewModel[] QueryPetViewModel_2(string pattern) {
using (var context = new MyDbContext(connectionString)) {
var petInfos = context.Pets
.Where(p => p.Name.Contains(pattern))
.Join(context.Members,
p => p.Member.Id,
m => m.Id,
(p, m) => new { Pet = p, Member = m }
)
.ToArray();
var result = new List<PetViewModel>();
foreach (var info in petInfos) {
var team = context.Teams
.SingleOrDefault(t => t.Members.Any(m => m.Id == info.Member.Id));
result.Add(new PetViewModel {
Name = info.Pet.Name,
TeamItIndirectlyBelongsTo = team?.Name,
});
}
return result.ToArray();
}
}
However, this has a "SELECT N+1" issue in there.
Is there a way to create just one EF query to get the desired result, without changing the entities?
PS. If you prefer a "plug and play" repro containing the above, see this gist.
You've made the things quite harder by not providing the necessary navigation properties, which as #Evk mentioned in the comments do not affect your database structure, but allow EF to supply the necessary joins when you write something like pet.Member.Team.Name (what you need here).
The additional problem with your model is that you don't have a navigation path neither from Team to Pet nor from Pet to Team since the "joining" entity Member has no navigation properties.
Still it's possible to get the information needed with a single query in some not so intuitive way by using the existing navigation properties and unusual join operator like this:
var result = (
from team in context.Teams
from member in team.Members
join pet in context.Pets on member.Id equals pet.Member.Id
where pet.Name.Contains(pattern)
select new PetViewModel
{
Name = pet.Name,
TeamItIndirectlyBelongsTo = team.Name
}).ToArray();

Adding related table fields to class via Entity Framework

Every building has multiple rooms. I would like to return data from a building that includes a room number from every room owned by said building via a class buildingView, which looks like this right now (with some pseudocode):
public class buildingView
{
public int id { get; set; }
public string name { get; set; }
...
public *something* roomNumbers *something*
}
To that end, I have the following query:
buildingView building = db.buildings.Select(b => new buildingView
{
id = b.id,
name = b.name,
...
roomNumbers = *I have no idea*
})
.Where(b => b.id == id)
.SingleOrDefault();
Room numbers are all strings, if that's relevant.
My questions:
How can I get my list of room numbers?
How can I improve my question's title? I lack the vocabulary right now to do better.
Is there a another way entirely?
Thanks!
P.S. If you are going to downvote, please say why. Otherwise how will I know how to improve my questions?
Assuming you have a class like ...
class Building
{
...
public ICollection<Room> Rooms { get; set; }
}
And that BuildingView is ...
public class BuildingView
{
public int Id { get; set; }
public string Name { get; set; }
...
public IEnumerable<string> RoomNumbers { get; set; }
}
... you can get the concatenated room numbers as follows:
var buildingViews = context.Buildings
.Select(b => new BuildingView
{
Id = b.Id,
Nems = b.Name,
RoomNumbers = b.Rooms
.Select(r => r.Number)
});
Side note: it's really recommended to use C# naming conventions, like PascalCase for class and property names.

How to select and consume a collection of value objects in an NHibernate QueryOver query

I have a simple model, consisting of a document that references one or more article using a reference object (this is because in the domain, we do not own the articles so we can only reference them).
I'm trying to write a query that lists the documents, printing the ID and a string consisting of a comma separated list of article numbers. For example:
ID ARTICLES
------------------
1 ACC, PE2,
2 ER0, AQ3, FEE
3 PE2
My problem is with selecting the comma separated list.
Here are the domain classes:
// The Entity class has an Id property.
public class Document : Entity
{
public virtual IEnumerable<ArticleReference> ArticleReferences { get; set; }
public virtual DateTime ReceiveDate { get; set; }
}
// The ValueObject does not have an Id property ofcourse.
public class ArticleReference : ValueObject
{
public virtual string ArticleNumber { get; set; }
public virtual string ArticleName { get; set; }
}
The article reference is a value object so it does not have an ID of its own.
This is the view model that represents an item in the result list:
public class DocumentListItemModel
{
public int Id { get; set; }
public string ArticleNumbers { get; set; }
public string ReceiveDate { get; set; }
}
And here's the query class I have come up with so far:
public class DocumentQuery
{
public IList<DocumentListItemModel> ExecuteQuery()
{
IntermediateModel model = null;
ArticleReference articleReferenceAlias = null;
return Session
.QueryOver<Document>()
.JoinAlias(n => n.ArticleReferences, () => articleReferenceAlias);
.SelectSubQuery(
QueryOver.Of<ArticleReference>(() => articleReferenceAlias)
// There is no way of matching references to documents from a domain
// point of view since the references are value objects and
// therefore don't have an ID.
.Where(n => ...)
.Select(q => articleReferenceAlias.Number))
.WithAlias(() => model.ArticleNumbers)
.TransformUsing(Transformers.AliasToBean<IntermediateModel>());
.Future<IntermediateModel>()
.ToList()
.Select(n =>
new DocumentListItemModel()
{
Id = n.Id,
ArticleNumbers = string.Join(", ", n.ArticleNumbers.OrderBy(p => p)),
ReceiveDate = n.ReceiveDate.ToString("d", CultureInfo.CurrentCulture)
})
.ToList();
}
private class IntermediateModel
{
public int Id { get; set; }
public IEnumerable<string> ArticleNumbers { get; set; }
public DateTime ReceiveDate { get; set; }
}
}
As you can see, I can't express the .Where statement because there is no way of matching references to documents from a domain point of view. The references are value objects and therefore don't have an ID.
The question is: how do I fix the query to properly select the list of article numbers so I can use it in my string.Join statement to make the comma separated string?
I think you are taking the definition of value object too literally. Assigning a surrogate identifier (identity column, Guid, etc.) to a value object does not make it any less of a value object. It's a value object because its equality is based its values, not its identity. This does not require that a value object cannot have an identity, and in practice it almost always has to.
Your application obviously has to be able to link a Document to a set of ArticleReferences and the best way to accomplish that is by adding an ID to ArticleReference.
I managed to solve the problem. This is what I ended up with:
public IList<DocumentListItemModel> ExecuteQuery()
{
ArticleReference articleReferenceAlias = null;
return Session
.QueryOver<Document>()
.JoinAlias(n => n.ArticleReferences, () => articleReferenceAlias,
JoinType.LeftOuterJoin)
.SelectList(list => list
.Select(n => n.Id)
.Select(n => articleReferenceAlias.Number))
.List<object[]>()
.Select(x => new
{
Id = (int)x[0],
ArticleNumber = (string)x[1]
})
.GroupBy(n => n.Id).Select(n =>
{
return new DocumentListItemModel
{
Id = n.First().Id,
ArticleNumbers = string.Join(", ", n.Select(p => p.ArticleNumber))
};
}).ToList();
}
}
I couldn't use the alias-to-bean transformer anymore because it cannot handle collection properties. That's why the solution has the GroupBy, it consolidates the rows, aggregating the article numbers into a string.

Categories

Resources