Getting a Dictionary from a Query with GroupBy - c#

Given two persisted entities
public class Header
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual List<Detail> Details { get; set; }
}
and
public class Detail
{
public virtual int Id { get; set; }
public virtual Header MyHeader { get; set; }
public virtual string Text { get; set; }
public virtual object LotsOfPropertiesIDontNeed { get; set; }
}
I want to populate a new object
public class MiniHeader
{
public string Name { get; set; }
public Dictionary<int, string> DetailTexts { get; set; }
}
with only the name from the Header, and with a dictionary relating detail IDs to the associated texts. Note that Detail also has LotsOfPropertiesIDontNeed, which I would prefer not to pull
across the wire or even request from SQL Server.
With the code
IEnumerable<MiniHeader> mini =
ctx.Details.Include(d => d.MyHeader)
.GroupBy(d => d.MyHeader)
.Select(g => new MiniHeader()
{
Name = g.Key.Name,
DetailTexts = g.ToDictionary(d => d.Id, d => d.Text)
});
I get the expected
LINQ to Entities does not recognize the method 'System.Collections.Generic.Dictionary`2[System.Int32,System.String]
since .ToDictionary cannot execute on the database side. I can make it work like this:
IEnumerable<MiniHeader> mini =
ctx.Details.Include(d => d.MyHeader)
.GroupBy(d => d.MyHeader)
.AsEnumerable()
.Select(g => new MiniHeader()
{
Name = g.Key.Name,
DetailTexts = g.ToDictionary(d => d.Id, d => d.Text)
});
but I presume that LotsOfPropertiesIDontNeed will be requested of SQL Server and pulled across the wire.
Is there a way to make this work, without pulling the unnecessary fields?

You can project your results to an anonymous type and then apply AsEnumerable and later project to your class like:
IEnumerable<MiniHeader> mini =
ctx.Details.Include(d => d.MyHeader)
.GroupBy(d => d.MyHeader)
.Select(g => new
{
Name = g.Key.Name,
Details = g.Select(i => new { i.Id, i.Text }),
})
.AsEnumerable()
.Select(e => new MiniHeader()
{
Name = e.Name,
DetailTexts = e.Details.ToDictionary(d => d.Id, d => d.Text)
});
This will let you get only those field that you need and later you can use ToDictionary on an in-memory collection.

Related

Simplify querying SQL in Entity Framework Core

I have the following code:
public async Task<IEnumerable<Submission>> SelectSubmissionsAsync(string submitterId, IEnumerable<Group> groups)
{
var submissions = new List<Submission>();
var apps = context.Apps
.Select(a => new
{
Id = a.Id,
Member = a.MemberHistories.OrderByDescending(ash => ash.MemberChangeDate).FirstOrDefault().Member,
Owner = a.OwnerHistories.OrderByDescending(oh => oh.OwnerChangeDate).FirstOrDefault().Owner
})
.ToDictionary(x => x.Id, x => x.Member + x.Owner);
var subs = context.Submissions.ToList();
foreach (var sub in subs)
{
if (apps.ContainsKey((Guid)sub.AppId))
{
var value = apps[(Guid)sub.AppId];
var check = value.Contains(submitterId, StringComparison.InvariantCultureIgnoreCase) || groups.Any(g => value.Contains(g.Id, StringComparison.InvariantCultureIgnoreCase));
if (check)
submissions.Add(sub);
}
}
}
public class Submission
{
public Guid Id { get; set; }
public Application App { get; set; }
public Guid? AppId { get; set; }
}
public class App
{
public Guid Id { get; set; }
public string Identifier { get; set; }
public ICollection<MemberHistory> MemberHistories { get; set;}
public ICollection<OwnerHistory> OwnerHistories { get; set;}
}
Is there a way to simplify this code (avoid for loop for example)?
Ideally you should be able to construct a single query looking something like this:
var appInfo = context.Apps
.Select(a => new
{
Id = a.Id,
Member = a.MemberHistories.OrderByDescending(ash => ash.MemberChangeDate).FirstOrDefault().Member,
Owner = a.OwnerHistories.OrderByDescending(oh => oh.OwnerChangeDate).FirstOrDefault().Owner
})
.Where(appCriteria)
;
var submissions = context.Submissions
.Where(s => appInfo.Any(app => s.AppId == app.Id))
.ToList();
That will allow your app to build a single SQL command that filters the apps down to just the ones you want before bringing them back from the database.
Building checkCriteria will be complicated, because that's going to be based on the "OR"/Union of several criteria. You'll probably want to build a collection of those criteria, and then combine them using a strategy similar to what I've defined here. If you start with a collection of values including submitterId and groupIds, each criteria would be something like s => s.Member == val || s.Owner == val.
In order to create these expressions, you'll probably need to declare a class to represent the type that you're currently using an anonymous type for, so you have a name to associate with the generic arguments on your Expression types.

Why am I unable to do a count after a GroupBy with LINQ

I have the following classes:
public class LogViews
{
public string DateYYMMDD { get; set; }
public string Mode { get; set; }
public int LearnViews { get; set; }
public int PracticeViews { get; set; }
public int QuizViews { get; set; }
}
public class Views2Model
{
public string DateYYMMDD { get; set; }
public int Devices { get; set; }
public int? LearnViews { get; set; }
public int? PracticeViews { get; set; }
public int? QuizViews { get; set; }
}
What I would like to do is to get a count when grouped by the date:
var ViewCount = CreateExcelFile.ListToDataTable(Views
.GroupBy(x => x.DateYYMMDD)
.OrderBy(g => g.Key)
.Select(g => new Views2Model
{
DateYYMMDD = $"20{g.Key.Substring(0, 2)}/{g.Key.Substring(2, 2)}/{g.Key.Substring(4, 2)}",
Devices = g.Count(),
LearnViews = g.LearnViews.Count(),
PracticeViews = g.PracticeViews.Count(),
QuizViews = g.QuizViews.Count(),
})
.ToList());
But I am getting errors for g.LearnViews, g.PracticeViews and g.QuizViews
GetViews.cs(36,36): Error CS1061: 'IGrouping<string, LogViews>' does not contain a definition for 'LearnViews' and no accessible extension method 'LearnViews' accepting a first argument of type 'IGrouping<string, LogViews>' could be found (are you missing a using directive or an assembly reference?) (CS1061) (Cosmos)
Can anyone give advice on what I am doing wrong?
You're treating a group as if it's a single record. In your select g represents a group. A single group object has a common Key, but it contains many "View" items. Therefore you need to Sum the individual record values for LearnViews, etc.
var ViewCount = CreateExcelFile.ListToDataTable(Views
.GroupBy(x => x.DateYYMMDD)
.OrderBy(g => g.Key)
.Select(g => new Views2Model
{
DateYYMMDD = $"20{g.Key.Substring(0, 2)}/{g.Key.Substring(2, 2)}/{g.Key.Substring(4, 2)}",
Devices = g.Count(),
LearnViews = g.Sum(gi => gi.LearnViews),
PracticeViews = g.Sum(gi => gi.PracticeViews),
QuizViews = g.Sum(gi => gi.QuizViews),
})
.ToList());
Maybe you've messed up using succesive LINQ function. g is not LogViews, but it's IEnumerable<LogViews>. As I see in your code, You used g.Count(), but there is no Count function defined in LogViews, right?
You can only do a Count() on an IEnumerable object.
LearnViews is an int.
What you want to do is : LearnViews = g.Sum(v => v.LearnViews)

Include collection in Entity Framework Core

For example, I have those entities:
public class Book
{
[Key]
public string BookId { get; set; }
public List<BookPage> Pages { get; set; }
public string Text { get; set; }
}
public class BookPage
{
[Key]
public string BookPageId { get; set; }
public PageTitle PageTitle { get; set; }
public int Number { get; set; }
}
public class PageTitle
{
[Key]
public string PageTitleId { get; set; }
public string Title { get; set; }
}
How should I load all PageTitles, if I know only the BookId?
Here it is how I'm trying to do this:
using (var dbContext = new BookContext())
{
var bookPages = dbContext
.Book
.Include(x => x.Pages)
.ThenInclude(x => x.Select(y => y.PageTitle))
.SingleOrDefault(x => x.BookId == "some example id")
.Pages
.Select(x => x.PageTitle)
.ToList();
}
But the problem is, that it throws exception
ArgumentException: The properties expression 'x => {from Pages y
in x select [y].PageTitle}' is not valid. The expression should represent
a property access: 't => t.MyProperty'. When specifying multiple
properties use an anonymous type: 't => new { t.MyProperty1,
t.MyProperty2 }'. Parameter name: propertyAccessExpression
What's wrong, what exactly should I do?
Try accessing PageTitle directly in ThenInclude:
using (var dbContext = new BookContext())
{
var bookPages = dbContext
.Book
.Include(x => x.Pages)
.ThenInclude(y => y.PageTitle)
.SingleOrDefault(x => x.BookId == "some example id")
.Select(x => x.Pages)
.Select(x => x.PageTitle)
.ToList();
}

Query nested class and return the all root document in MongoDB via C# driver 2.1

I have a collection called 'Projects'.
In every Project I have subDocuments called Structures that included another subDocuments called StructureProperties.
I wish to get the Property by the 'userId' and also it's ROOT using the C# driver 2.1 (userId - found in every Properties subDocuments).
my Project class look like that:
public interface IGeneralProject
{
[BsonId]
ObjectId Id { get; set; }
string Name { get; set; }
List<GeneralStructure> GeneralStructures { get; set; }
}
[BsonKnownTypes(typeof(ProjectOne), typeof(ProjectTwo))]
public class GeneralProject : IGeneralProject
{
[BsonId]
public ObjectId Id { get; set; }
public string Name { get; set; }
public List<GeneralStructure> GeneralStructures { get; set; }
}
Structure class:
public interface IGeneralStrucute
{
ObjectId StructureId { get; set; }
string Name { get; set; }
List<GeneralStructureProperty> StructureProperties { get; set; }
}
[BsonKnownTypes(typeof(Structure), typeof(Houseware))]
public class GeneralStructure : IGeneralStrucute
{
public ObjectId StructureId { get; set; }
public string Name { get; set; }
public List<GeneralStructureProperty> StructureProperties { get; set; }
}
and the last one StructureProperty:
public interface IGeneralStructureProperty
{
string Name { get; set; }
ObjectId UserId { get; set; }
}
[BsonKnownTypes(typeof(Propery), typeof(Office))]
public class GeneralStructureProperty
{
public string Name { get; set; }
public ObjectId UserId { get; set; }
}
I have tried LINQ to query that but got stuck...
here some of my attempts:
1.
return (from project in projectCollection.AsQueryable()
from generalStructure in project.GeneralStructures
from generalStructureProperty in generalStructure.StructureProperties
where generalStructureProperty.UserId == ObjectId.Parse(userId)
select new GeneralProject()
{
Id = project.Id, Name = project.Name, GeneralStructures = new List<GeneralStructure>()
{
new GeneralStructure()
{
StructureId = generalStructure.StructureId, Name = generalStructure.Name, StructureProperties = new List<GeneralStructureProperty>()
{
new GeneralStructureProperty()
{
Name = generalStructureProperty.Name, UserId = generalStructureProperty.UserId
}
}
}
}
}).ToList();
2.
var query = from project in projectCollection.AsQueryable()
from structure in project.GeneralStructures
from structureProperty in structure.StructureProperties
where structureProperty.UserId == ObjectId.Parse(userId)
select // where I got stuck...
3.
var query =
projectCollection.AsQueryable()
.Select(project => project)
.Where(
project =>
project.GeneralStructures.Any(
structure =>
structure.StructureProperties.Any(
property => property.UserId == ObjectId.Parse(userId)))).ToList();
It very important to say that I could make that query and get good results, but I have been got the all Project document with *all of is subDocuments** instead of getting the Project with the specific Structure and the specific StructurePropery ('ROOT' of the node).
Do you want to return only the root document GeneralProject (as you mentioned in the subject)? You can try this:
var collection = mongoContext.GetCollection<GeneralProject>(); //your implementation here
var result = await collection
.Find(x => x.GeneralStructures.Any(y => y.StructureProperties.Any(z => z.UserId == {user id here})))
.FirstOrDefaultAsync();
Or if you want to return GeneralProject and single GeneralStructureProperty with user id, you can try project it dynamically:
var result = await collection
.Find(x => x.GeneralStructures.Any(y => y.StructureProperties.Any(z => z.UserId == {user id here})))
.Project(
p => new
{
GeneralStructureProperty = p.GeneralStructures.FirstOrDefault(x => x.StructureProperties.Any(z => z.UserId == {user id here})),
GeneralProject = p
})
.FirstOrDefaultAsync();
I didn't test it on real database and I suppose, these operations could be complicated for large amounts of data in db. But it based on your db architecture.
EDIT:
According to your comments:
var result = await collection
.Find(x => x.GeneralStructures.Any(y => y.StructureProperties.Any(z => z.UserId == ObjectId.Parse(userId))))
.Project(
p => new
{
Id = p.Id,
Name = p.Name,
Structure = p.GeneralStructures.FirstOrDefault(x => x.StructureProperties.Any(z => z.UserId == ObjectId.Parse(userId)))
})
.FirstOrDefaultAsync();
result.Structure.StructureProperties = result.Structure.StructureProperties.Where(x => x.UserId == ObjectId.Parse(userId)).ToList();
Now you should retrieve project id, project name and structure with properties (with expected userId). Adjust my suggestion to your needs.

Order by Collection entity property in ASP.NET MVC 3 (entity framework)

I have two entities like:
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Lastname { get; set; }
public virtual ICollection<EmployeeEducation> EducationList { get; set; }
}
and
public class EmployeeEducation
{
public int Id { get; set; }
public int EmployeeId { get; set; }
public int Type { get; set; }
[ForeignKey("EmployeeId")]
public virtual Employee Employee { get; set; }
}
My question is, how can I get a specific employee and this employee's education list ordered by Type property?
I have tried:
Employee employee = _work.EmployeeRepository.GetSet()
.SelectMany(e => e.EducationList, (e,d) => new { e, d })
.OrderBy(x => x.d.Type)
.Select(x => x.e)
.FirstOrDefault(e => e.Id == id);
But it does't seem to be sorting. What is the correct way to do this?
Thanks for everyone...
You do SelectMany(), but never use the produced EducationList part, becuase you do .Select(x => x.e). But couldn't life be simpler? After all, you only get 1 employee, why not sort its EducationList as soon as you need it, after having Included it, if necessary:
Employee employee = _work.EmployeeRepository.GetSet().Include("EducationList")
.FirstOrDefault(e => e.Id == id);
Depending if u are using POCO or not u should either use CreateSourceQuery() or Query()
In the case of POCO something like:
Employee employee = _work.EmployeeRepository.GetSet()
.SelectMany(e => e.EducationList, (e,d) => new { e, d })
.Query()
.OrderBy(x => x.d.Type)
.Select(x => x.e)
.FirstOrDefault(e => e.Id == id);

Categories

Resources