writing LINQ query to pivot the result - c#

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.

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.

How can I get the count of a list in an Entity Framework model without including/loading the entire collection?

I have a model in Entity Framework Core that goes something like this:
public class Anime
{
public int EpisodeCount { get { return Episodes.Count() } }
public virtual ICollection<Episode> Episodes { get; set; }
}
I'm having the issue of EpisodeCount being 0. The solution currently is to run a .Include(x => x.Episodes) within my EF query, but that loads the entire collection of episodes where it's not needed. This also increases my HTTP request time, from 100ms to 700ms which is just not good.
I'm not willing to sacrifice time for simple details, so is there a solution where I can have EF only query the COUNT of the episodes, without loading the entire collection in?
I was suggested to do this
var animeList = context.Anime.ToPagedList(1, 20);
animeList.ForEach(x => x.EpisodeCount = x.Episodes.Count());
return Json(animeList);
but this also returns 0 in EpisodeCount, so it's not a feasible solution.
You need to project the desired data into a special class (a.k.a. ViewModel, DTO etc.). Unfortunately (or not?), in order to avoid N + 1 queries the projection must not only include the count, but all other fields as well.
For instance:
Model:
public class Anime
{
public int Id { get; set; }
public string Name { get; set; }
// other properties...
public virtual ICollection<Episode> Episodes { get; set; }
}
ViewModel / DTO:
public class AnimeInfo
{
public int Id { get; set; }
public string Name { get; set; }
// other properties...
public int EpisodeCount { get; set; }
}
Then the following code:
var animeList = db.Anime.Select(a => new AnimeInfo
{
Id = a.Id,
Name = a.Name,
EpisodeCount = a.Episodes.Count()
})
.ToList();
produces the following single SQL query:
SELECT [a].[Id], [a].[Name], (
SELECT COUNT(*)
FROM [Episode] AS [e]
WHERE [a].[Id] = [e].[AnimeId]
) AS [EpisodeCount]
FROM [Anime] AS [a]

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

Creating LINQ statement against an EF Context with no relationships

I cannot wrap my head around how to write a linq query against my EF context to get what I want.
1) What I have
Database with no foreign keys assigned, and a reverse engineered code first entity framework project. I tried manually adding virtual classes so EF might create implied foreign keys in the DBcontext, but I get errors on my .Include statements still.
Without the include the only thing I can think of is to use left joins, but I haven't gotten it down yet. In the end there will be 21 tables I have to get data from, but the following table outline encapsulates the majority of issues i'm facing.
Sample data structure:
Table Human: HumanId, LastFoodEatenId, FavoriteFoodId, CurrentlyDesiredFoodId
Table Food: FoodId, FoodName, FoodStuff
Table Toys: HumanOwnerId, ToyId, ToyName
Table Pets: HumanOwnerId, PetId, PetName, PetType
Table PetSurgery: PetId, SurgeryId, SurgeryPerformed
2) What I want
Given a HumanID, I want a compsite class or something like it from a single query.
Public Class QueryResult
{
public Human human {get;set;}
public Food LastEatenFood {get;set;}
public Food FavoriteFood {get;set;}
public Food CurrentlyDesiredFood {get;set;}
public IEnumerable<Toy> Toys {get;set;}
public IEnumerable<Pet> Pets {get;set;} //Includes surgeries if any
}
Is it even possible to write a single query to get this kind of information in a single db hit? I'd be fine is someone simply confirmed it is't possible. Then I can at least request we add relationships to our database.
Thanks in advance,
You can use linq to query multiple, non-related tables.
I'm going to assume a LOT about your entities, but here we go...
int humanId = 1234;
using (var context = new MyContext())
{
var human = (from h in context.Humans
join lf in context.Foods on h.LastFoodEatenId equals lf.foodId into lfg
from lf in lfg.DefaultIfEmpty() // left join
join ff in context.Foods on h.FavoriteFoodId equals lf.foodId into ffg
from ff in ffg.DefaultIfEmpty() // left join
join cf in context.Foods on h.CurrentlyDesiredFoodId equals lf.foodId into cfg
from cf in cfg.DefaultIfEmpty() // left join
join p in context.Pets on h.humanId equals p.humanId into pg // group
join t in context.Toys on h.humanId equals t.humanId into tg // group
where h.humanId = humanId
select new QueryResult { human = h, LastEatenFood = lf, FavoriteFood = ff, CurrentlyDesiredFood = cf, Toys = tg, Pets = pg }
).SingleOrDefault();
}
Note: I'm doing this from memory without a syntax checker, so ymmv. Adding surgeries should be possible as well, but may require a subquery.
I tried manually adding virtual classes
I assume you mean virtual collections. You can define relationships in a "code-first" model if they are not in the database. The only condition is that foreign keys must refer to properties that EF knows as primary keys. So you should be able to do LINQ queries using navigation properties in stead of these verbose joins by a model like this (reduced to the essentials):
class Human
{
public int HumanId { get; set; }
public int LastFoodEatenId { get; set; }
public virtual Food LastEatenFood { get; set; }
public int FavoriteFoodId { get; set; }
public virtual Food FavoriteFood { get; set; }
public int CurrentlyDesiredFoodId { get; set; }
public virtual Food CurrentlyDesiredFood { get; set; }
public virtual ICollection<Toy> Toys { get; set; }
public virtual ICollection<Pet> Pets { get; set; }
}
class Food
{
public int FoodId { get; set; }
}
class Pet
{
public int PetId { get; set; }
public int HumanOwnerId { get; set; }
}
class Toy
{
public int ToyId { get; set; }
public int HumanOwnerId { get; set; }
}
And a mapping:
class HumanMapping : EntityTypeConfiguration<Human>
{
public HumanMapping()
{
HasOptional(h => h.LastEatenFood).WithMany()
.HasForeignKey(h => h.LastFoodEatenId);
HasOptional(h => h.FavoriteFood).WithMany()
.HasForeignKey(h => h.FavoriteFoodId);
HasOptional(h => h.CurrentlyDesiredFood).WithMany()
.HasForeignKey(h => h.CurrentlyDesiredFoodId);
HasMany(h => h.Toys).WithOptional().HasForeignKey(t => t.HumanOwnerId);
HasMany(h => h.Pets).WithOptional().HasForeignKey(t => t.HumanOwnerId);
}
}
EF will infer the primary keys by name conventions.
Now you will be able to execute a LINQ statement like:
context.Humans.Where(h => h.HumanId == id)
.Include(h => h.LastEatenFood)
.Include(h => h.FavoriteFood)
.Include(h => h.CurrentlyDesiredFood)
.Include(h => h.Toys)
.Include(h => h.Pets)
From your description I understand that PetSurgery should be a junction class between Pet and another class (Surgery?). Anyway, I think you will manage creating the correct mappings, seeing this example.

Violation of PRIMARY KEY constraint: Cannot insert duplicate key in object

When I want to persist a complex model, I get this error. I think I know where it comes from, but I don't know how to solve it. I'm importing a few feeds and create objects automatically, including children (many-to-many).
{"Violation of PRIMARY KEY constraint 'PK_dbo.Parent'. Cannot insert
duplicate key in object 'dbo.Parent'. The duplicate key value is
(291).\r\nThe statement has been terminated."}
The error speaks for itself, but how to prevent it? :)
The code that triggers it
var parser = new SchoolFeedReader();
var update = parser.GetAll();
var students = Mapper.Map<List<StudentDTO>, List<Student>>(update);
using (var db = new SchoolContext())
{
// I'm updating every night, so clean out the database before import
db.Database.ExecuteSqlCommand("DELETE FROM Student");
db.Database.ExecuteSqlCommand("DELETE FROM Parent");
db.Database.ExecuteSqlCommand("DELETE FROM Subject");
db.Database.ExecuteSqlCommand("DELETE FROM StudentParent");
db.Database.ExecuteSqlCommand("DELETE FROM StudentSubject");
students.ForEach(s => db.Students.Add(s));
db.SaveChanges(); // Triggers the Exception
}
The TL;DR
For a schoolproject I need to import 3 XML Feeds into the database.
Students.xml
Parents.xml
Subjects.xml
In Students.xml I encountered a design flaw: a fixed number (3) of possible Parents.
<student>
<StudentId>100</StudentId>
<Name>John Doe</Name>
<Location>Main Street</Location>
<Parent1>1002</Parent1>
<Parent2>1002</Parent2>
<Parent3/>
</student>
(... more students)
In Parents.xml, things are more straightforward.
<parent>
<ParentId>1102</ParentId>
<Name>Dad Doe</Name>
<Email>dad#doe.com</Email>
</parent>
(... more parents)
And Subjects.xml is also very simple.
<subject>
<StudentId>100</StudentId>
<Name>English</Name>
</subject>
(... more subjects)
The Models
So I created 3 models, including the DTOs.
public class Student
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public long StudentId { get; set; }
public string Name { get; set; }
public string Location { get; set; }
[InverseProperty("Students")]
public virtual ICollection<Parent> Parents { get; set; }
public virtual ICollection<Subject> Subjects { get; set; }
}
public class StudentDTO
{
public long StudentId { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public List<ParentDTO> Parents { get; set; }
public List<SubjectDTO> Subjects { get; set; }
}
public class Parent
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public long ParentId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
[InverseProperty("Parents")]
public virtual ICollection<Student> Students { get; set; }
}
public class ParentDTO
{
public long ParentId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public List<StudentDTO> Students { get; set; }
public ParentDTO()
{
Students = new List<StudentDTO>();
}
}
public class Subject
{
public long SubjectId { get; set; }
public string Name { get; set; }
public virtual List<Student> Students { get; set; }
}
public class SubjectDTO
{
public string Name { get; set; }
public List<StudentDTO> Students { get; set; }
public SubjectDTO()
{
Students = new List<StudentDTO>();
}
}
From XML to DTOs
The Importer class has this giant LINQ query to get everything I need in one big swoop.
var query = from student in _xStudents.Descendants("Student")
select new StudentDTO
{
StudentId = (long)student.Element("StudentId"),
Name = (String)student.Element("Name"),
Subjects = (
from subject in _xSubjects.Descendants("Subject").DefaultIfEmpty()
where (String)student.Element("StudentId") == (String)subject.Element("StudentId")
select new SubjectDTO
{
Name = (String)subject.Element("Name")
}
).ToList(),
Parents = (
from parent in _xParents.Descendants("Parent").DefaultIfEmpty()
group parent by (String)parent.Element("ParentId") into pg
where (String)student.Element("Parent1") == (String)pg.FirstOrDefault().Element("ParentId") ||
(String)student.Element("Parent2") == (String)pg.FirstOrDefault().Element("ParentId") ||
(String)student.Element("Parent3") == (String)pg.FirstOrDefault().Element("ParentId")
select new ParentDTO
{
ParentId = (long)pg.FirstOrDefault().Element("ParentId"),
Name = (String)pg.FirstOrDefault().Element("Name")
}
).ToList()
};
That works fine, some students get 2 parents, some get 1, so my data looks good.
The Problem
I have these AutoMappers in my Global.asax.cs:
Mapper.CreateMap<StudentDTO, Student>()
.ForMember(dto => dto.Parents, opt => opt.MapFrom(x => x.Parents))
.ForMember(dto => dto.Subjects, opt => opt.MapFrom(x => x.Subjects));
Mapper.CreateMap<ParentDTO, Parent>();
Mapper.CreateMap<SubjectDTO, Subject>();
But when I start the import I get errors on my db.SaveChanges(). It complains about a duplicate ForeignKey on the Parent model. So I'm thinking:
it's a Many-to-Many relationship, so if John Doe's sister, Jane Doe, tries to insert the same Dad Doe, then it crashes
So How can I make sure that the entire set of Mapped Business Objects only have 1 reference to each entity; how to delete the duplicate daddy's and mommy's? I probably want to do this also for Subject.
If two or more student in _xStudents.Descendants("Student") reference the same parent (by id), you then create two or more ParentDTOs with the same id, so you are then trying to insert the same Primary Key twice within your Importer class.
If you simply pre-process your _xParents, to transform them into a new list of ParentDTO, which is unique by ParentId you can then use that in your var query to get a reference to the single ParentDTO instance that refers to the given ParentId PK.
This code sample doesn't change your code much so that you can easily relate it to your original. Note, however, that you can probably optimise this, and you will also have the same problem with your SubjectDTO list as well if you are using SubjectDTO.Name to be unique (as you should be, I guess).
var parents = (from parent in _xParents.Descendants("Parent").DefaultIfEmpty()
group parent by (String)parent.Element("ParentId") into pg
select new ParentDTO
{
ParentId = (long)pg.FirstOrDefault().Element("ParentId"),
Name = (String)pg.FirstOrDefault().Element("Name")
// you might want to not use ToList here and let parents be an IEnumerable instead
}).ToList();
var query = from student in _xStudents.Descendants("Student")
select new StudentDTO
{
StudentId = (long)student.Element("StudentId"),
Name = (String)student.Element("Name"),
Subjects = (
from subject in _xSubjects.Descendants("Subject").DefaultIfEmpty()
where (String)student.Element("StudentId") == (String)subject.Element("StudentId")
select new SubjectDTO
{
Name = (String)subject.Element("Name")
}
).ToList(),
Parents = (
from parent in parents
// Calling ToString each time is not fantastic
where (String)student.Element("Parent1") == parent.ParentId.ToString() ||
(String)student.Element("Parent2") == parent.ParentId.ToString() ||
(String)student.Element("Parent3") == parent.ParentId.ToString()
select parent
).ToList()
};
The real problem is in mapping. Mapper adds that same parent two times, and hence its new entity, it is in Added state. Later dbContext treats it like new record, and tries insert.
I see three options:
Replace StudentDTO.ParentDTO with StudentDTO.IDParentDTO
Add StudentDTO.IDParentDTO and ignore StudentDTO.ParentDTO in mapping
Play with mapping. There is a bunch of features but you just need to find them. Check this question
I received this error with a User-Defined Table Type. When building data relationships, I sometimes pull the same record multiple times. If appropriate, turn ON IGNORE_DUP_KEY when declaring your PRIMARY KEY.
Microsoft index_option (w / IGNORE_DUP_KEY)
Example:
CREATE TYPE [dbo].[udt_Promotion] AS TABLE(
[PromotionID] [int] NOT NULL PRIMARY KEY CLUSTERED WITH (IGNORE_DUP_KEY = ON),
...
)

Categories

Resources