Simplify querying SQL in Entity Framework Core - c#

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.

Related

How to create a where condition to an object inside an object in c# linq?

Let's say I have a class like:
public class TrainingPlan
{
public int Id { get; set; }
public int ProjectId { get; set; }
public string TrainingPlanName { get; set; }
public List<Training> TrainingList { get; set; }
public bool IsDeleted { get; set; }
}
And a Training object inside it:
public class TrainingViewModel : AuditViewModel
{
public int Id { get; set; }
public int ProjectId { get; set; }
public int TrainingPlanId { get; set; }
public bool IsDeleted { get; set; }
public ProjectViewModel ProjectObject { get; set; }
public TrainingPlanViewModel TrainingPlanObject { get; set; }
}
I could write something like this in order to retrieve TrainingPlans where IsDeleted = false and also retrieve the Training Objects attached to it.
var result = _trainingPlanRepository.FindBy(t => t.ProjectId == projectId && t.IsDeleted == false).ToList();
But how do I set a condition for the Training objects where IsDeleted = false also?
You could use Any() like below. Assumption Your t has a List<Training> which you want to query as well to get the non-deleted ones.
var result = _trainingPlanRepository.
Where(t => t.ProjectId == projectId && !t.IsDeleted &&
t.TrainingList.Any(x => !x.IsDeleted)).ToList();
var trainingPlans = _trainingPlanRepository
.Where(t => t.ProjectId == projectId && t.IsDeleted == false)
.ToList();
Now, In trainingPlans variable, All related training objects exist in it, so we want to filter it bases on IsDelted Property. so you can use below code:
foreach (var item in trainingPlans)
{
item.trainingList = item.trainingList.Where(t => !t.IsDelete).ToList();
}
good luck.
I guess there is a one-to-many relation (possibly many-to-many) between TrainingPlan and Training: every TrainingPlan has zero or more Tranings, and every Training belongs to exactly one TrainingPlan, namely the Training that the foreign key TrainingId (or something similar) refers to.
So somewhere deep inside your repository, you'll have two IQueryable sequences:
IQueryable<Training> trainings = ...
IQueryable<TrainingPlan> trainingPlans = ...
You want to query all trainingplans that are not deleted and that have a ProjectId equal to projectId, together with all its not deleted TrainingPlans.
Using method syntax this is fairly straightforward:
var result = trainingPlans
// keep only the training plans you want to keep:
.Where(trainingPlan => trainingPlan.ProjectId == projectId && !trainingPlan.IsDeleted)
// GroupJoin each trainingPlan with its trainings:
.GroupJoin(trainings,
trainingPlan => trainingPlan.Id, // from each training plan take the Id
training => training.TrainingPlanId, // from each training take the foreign key
(trainingPlan, matchingTrainings) => new // take the training plan with all its matching trainings
{ // to make a new
// Select the trainingplan properties you plan to use
Id = trainingPlan.Id,
Name = trainingPlan.Name,
...
// Keep only the non-deleted trainings
Trainings = matchingTrainings.Where(training => !training.IsDeleted)
.Select(training => new
{
// Select only the training properties that you plan to use:
Id = training.Id,
Date = training.Date,
...
// not needed, you know the value:
// TrainingPlanId = training.TrainingPlanId,
})
.ToList(),
});
Another method would be to remove the non-deleted trainings before the GroupJoin:
var validTrainingPlans = trainingPlans.Where(...);
var validTrainings = trainings.Where(training => !training.IsDeleted);
var result = validTrainingPlans.GroupJoin(validTrainings,
... etc

Get Table A record if Table B has a match in a search

I have two tables
CREATE TABLE RetailGroup(
Id int IDENTITY(1,1),
GroupName nvarchar(50),
)
CREATE TABLE RetailStore(
Id int IDENTITY(1,1),
StoreName nvarchar(100),
RetailGroupId int
)
Where RetailGroupId in RetailStore is referencing RetailGroup ID. I am trying to create a search function where I can search for both RetailGroup and RetailsStores. If I get a matching RetailStore I want to return the Group it is tied to and the matching Store record. If I get a matching Group, I want the group record but no retail stores.
I tried to do it the following way:
public List<RetailGroup> SearchGroupsAndStores(string value)
{
return _context.RetailGroups.Where(group => group.GroupName.Contains(value)).Include(group => group.RetailStores.Where(store => store.StoreName.Contains(value))).ToList();
}
But this is wrong because include should not be used for selection.
Here is my entity framework model for groups
public class RetailGroup
{
[Key]
public int Id { set; get; }
[MaxLength(50)]
public String GroupName { set; get; }
//Relations
public ICollection<RetailStore> RetailStores { set; get; }
}
And here is the one for the store
public class RetailStore
{
[Key]
public int Id { get; set; }
[MaxLength(100)]
public string StoreName { get; set; }
[ForeignKey("RetailGroup")]
public int RetailGroupId { get; set; }
//Relations
public RetailGroup RetailGroup { get; set; }
public ICollection<EGPLicense> Licenses { get; set; }
}
How do I create my LINQ to get the results I am looking for ?
The query returning the desired result with projection is not hard:
var dbQuery = _context.RetailGroups
.Select(rg => new
{
Group = rg,
Stores = rg.RetailStores.Where(rs => rs.StoreName.Contains(value))
})
.Where(r => r.Group.GroupName.Contains(value) || r.Stores.Any());
The problem is that you want the result to be contained in the entity class, and EF6 neither supports projecting to entity types nor filtered includes.
To overcome these limitations, you can switch to LINQ to Objects context by using AsEnumerable() method, which at that point will effectively execute the database query, and then use delegate block to extract the entity instance from the anonymous type projection, bind the filtered collection to it and return it:
var result = dbQuery
.AsEnumerable()
.Select(r =>
{
r.Group.RetailStores = r.Stores.ToList();
return r.Group;
})
.ToList();
Try using an OR condition to filter both the group name and the store name.
return _context.RetailGroups
.Where(group => group.GroupName.Contains(value) || group.RetailStores.Any(store => store.StoreName.Contains(value)))
.Include(group => group.RetailStores.Where(store => store.StoreName.Contains(value)))
.ToList();
Another option would be doing 2 searches, one for the groups and other for the stores. The problem here would be geting a unique set of groups from both results.
List<RetailGroup> retailGroups = new List<RetailGroup>();
var groupSearchResults = _context.RetailGroups
.Where(group => group.GroupName.Contains(value))
.Include(group => group.RetailStores.Where(store => store.StoreName.Contains(value)))
.ToList();
var storeSearchResults = _context.RetailStores
.Where(store => store.StoreName.Contains(value))
.Select(store => store.RetailGroup)
.ToList();
retailGroups.AddRange(groupSearchResults);
retailGroups.AddRange(storeSearchResults);
// Remove duplicates by ID
retailGroups = retailGroups
.GroupBy(group => group.Id)
.Select(group => group.First());
Either use OR condition and have the search in one statement :
public List<RetailGroup> SearchGroupsAndStores(string value)
{
return _context.RetailGroups
.Where(rg => rg.GroupName.Contains(value) || rg.RetailStores.Any(rs => rs.StoreName.Contains(value)))
.Include(rg => rg.RetailStores.Where(rs => rs.StoreName.Contains(value)).ToList())
.ToList();
}
Or you can split the search, first look for RetailGroups then search RetailStores and return their RetailGroup :
public List<RetailGroup> SearchGroupsAndStores(string value)
{
List<RetailGroup> searchResults = new List<RetailGroup>();
searchResults.AddRange(_context.RetailGroups.Where(rg => rg.GroupName.Contains(value)).ToList());
searchResults.AddRange(_context.RetailStores.Where(rs => rs.StoreName.Contains(value)).Select(rs => rs.RetailGroup).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.

Get Distinct List using LINQ

I want to translate this query in LINQ format:
select m.MenuName,m.ParentID from Menu m where Id in(
select distinct m.ParentID from Menu m inner join MenuRole mr on mr.MenuID=m.Id)
This is what I have tried
var _employee = _db.Employees.AsEnumerable().Where(e => e.Id == Int32.Parse(Session["LoggedUserId"].ToString()))
.FirstOrDefault();
var _dashboardVM = new DashboardVM
{
MenuParentList = _employee.Designation.Role.MenuRoles
.Select(x => new SMS.Models.ViewModel.DashboardVM.MenuParent
{
MenuParentID=x.Menu.ParentID ,
MenuParentName=x.Menu.MenuName
})
.Distinct().ToList()
};
I am getting all list instead of distinct List
Dashboard VM
public class DashboardVM
{
public class MenuParent
{
public int? MenuParentID { get; set; }
public string MenuParentName { get; set; }
}
public List<MenuParent> MenuParentList { get; set; }
public List<Menu> MenuList { get; set; }
public User User { get; set; }
}
The Distinct() method checks reference equality for reference types. This means it is looking for literally the same object duplicated, not different objects which contain the same values.
Can you try the following? You may need to tweek as I have no testing environment:
MenuParentList = _employee.Designation.Role.MenuRoles.GroupBy ( r => r.Menu.ParentID + r.Menu.MenuName ).
.Select (y => y.First ())
.Select(x => new SMS.Models.ViewModel.DashboardVM.MenuParent
{
MenuParentID=x.Menu.ParentID ,
MenuParentName=x.Menu.MenuName
}).ToList();

Getting a Dictionary from a Query with GroupBy

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.

Categories

Resources