Select common value in navigation table using LINQ lambda expression - c#

I have a table called InvestigatorGroup and a table called InvestigatorGroupUsers which is used to see what groups have what users. I am trying to get the common investigator group between two users
My query is as follows:
public InvestigatorGroup GetCommonGroup(string userId, string investigatorUserId)
{
using (GameDbContext entityContext = new GameDbContext())
{
string[] ids = new[] { userId, investigatorUserId };
return entityContext.InvestigatorGroups
.Where(i => i.IsTrashed == false)
.Include(i => i.InvestigatorGroupUsers)
.Where(i => i.InvestigatorGroupUsers.Any(e => ids.Contains(e.UserId)))
.OrderByDescending(i => i.InvestigatorGroupId)
.GroupBy(i => i.InvestigatorGroupId)
.Where(i => i.Count() > 1)
.SelectMany(group => group).FirstOrDefault();
}
}
The entity InvestigatorGroup is as follows:
public class InvestigatorGroup : IIdentifiableEntity
{
public InvestigatorGroup()
{
this.InvestigatorGroupGames = new HashSet<InvestigatorGroupGame>();
this.InvestigatorGroupUsers = new HashSet<InvestigatorGroupUser>();
}
// Primary key
public int InvestigatorGroupId { get; set; }
public string InvestigatorGroupName { get; set; }
public bool HasGameAssignment { get; set; }
public string GroupRoleName { get; set; }
public bool IsTrashed { get; set; }
// Navigation property
public virtual ICollection<InvestigatorGroupUser> InvestigatorGroupUsers { get; private set; }
public virtual ICollection<InvestigatorGroupGame> InvestigatorGroupGames { get; private set; }
public int EntityId
{
get { return InvestigatorGroupId; }
set { InvestigatorGroupId = value; }
}
}
The problem is that it keeps returning a value of 0. It doesn't see the shared group with a count of 2 between the two users.
I did a test to return the groups (I removed the count>1 condition) and it returned all the groups for both users not only the one they have in common
I believe the issue is with this line: .Where(i => i.InvestigatorGroupUsers.Any(e => ids.Contains(e.UserId)))
Thanks for the help!

I've resolved this by changing my query so that it searches for the rows containing one of the UserId's. Then it queries through those selected rows and selects the ones containing the other UserId (InvestigatorUserId). This way only the rows containing both are returned
My new code is as follows:
public InvestigatorGroup GetCommonGroup(string userId, string investigatorUserId)
{
using (GameDbContext entityContext = new GameDbContext())
{
IEnumerable<InvestigatorGroup> userGroups = entityContext.InvestigatorGroups
.Where(i => i.IsTrashed == false)
.Include(i => i.InvestigatorGroupUsers)
.Where(i => i.InvestigatorGroupUsers.Any(e => e.UserId.Contains(userId)))
.OrderByDescending(i => i.InvestigatorGroupId);
return userGroups.Where(i => i.InvestigatorGroupUsers.Any(e => e.UserId.Contains(investigatorUserId))).FirstOrDefault();
}
}

Related

Selecting TreeView values based on a many to many relationships in a razor pages model

I have a Razor pages project that is trying to populate a Kendo TreeView (or any other TreeView) from a Database created with a Data Model.
The page I am working on contains apps that are in use, and the tree is reading a self referencing Organisations list so that we know what apps each organisation or department etc has access to.
I'm working on the Edit page in a razor app, so ~Pages\Apps\Edit.cshtml and ~Pages\Apps\Edit.cshtml.cs, with associated model pages as shown below.
These are the models involved, ignore RoleApps for this issue:
namespace FliveRetry.Models
{
public class Org
{
public int ID { get; set; }
public string OrgName { get; set; }
public int? ParentID { get; set; }
public bool? HasChildren { get; set; }
}
}
And
namespace FliveRetry.Models
{
public class App
{
public int ID { get; set; }
public string AppName { get; set; }
public string AppDescription { get; set; }
public int? DisplayOrder { get; set; }
public bool? Archived { get; set; }
public DateTime? Saved { get; set; }
public int? SavedBy { get; set; }
public ICollection<OrgAppJoin> OrgAppJoins { get; set; }
public ICollection<RoleAppJoin> RoleAppJoins { get; set; }
}
}
and the index model:
{
public class AppIndexData
{
public IEnumerable<App> Apps { get; set; }
public IEnumerable<Role> Roles { get; set; }
public IEnumerable<Org> Orgs { get; set; }
public IEnumerable<RoleAppJoin> RoleAppJoins { get; set; }
public IEnumerable<OrgAppJoin> OrgAppJoins { get; set; }
}
public class AssignedAppOrgData
{
public int OrgID { get; set; }
public string Org { get; set; }
public int? ParentID { get; set; }
public bool Assigned { get; set; }
public bool? HasChildren { get; set; }
}
public class SelectedAppOrgs
{
public int OrgID { get; set; }
}
public class SelectedAppOrgNames
{
public string OrgName { get; set; }
}
I have a page model to populate selected items into lists called selectedOrgs or selectedOrgNames that I can use in the view.
public class AppSelectPageModel : PageModel
{
//Load list for Selecting Orgs for Apps
public List<AssignedAppOrgData> AssignedAppOrgDataList;
public List<SelectedAppOrgs> selectedOrgs;
public List<SelectedAppOrgNames> selectedOrgNames;
public void PopulateAssignedAppOrgData(FliveRetryContext context, App app)
{
var allOrgs = context.Org;
var appOrgs = new HashSet<int>(
app.OrgAppJoins.Select(c => c.OrgID));
AssignedAppOrgDataList = new List<AssignedAppOrgData>();
selectedOrgs = new List<SelectedAppOrgs>();
selectedOrgNames = new List<SelectedAppOrgNames>();
foreach (var org in allOrgs)
{
AssignedAppOrgDataList.Add(new AssignedAppOrgData
{
OrgID = org.ID,
Org = org.OrgName,
Assigned = appOrgs.Contains(org.ID)
});
if (appOrgs.Contains(org.ID))
{
selectedOrgs.Add(new SelectedAppOrgs
{
OrgID = org.ID
});
selectedOrgNames.Add(new SelectedAppOrgNames
{
OrgName = org.OrgName
});
}
}
}
public void UpdateAppOrgs(FliveRetryContext context,
string[] selectedOrgs, App appToUpdate)
{
if (selectedOrgs == null)
{
appToUpdate.OrgAppJoins = new List<OrgAppJoin>();
return;
}
var selectedOrgsHS = new HashSet<string>(selectedOrgs);
var appOrgs = new HashSet<int>
(appToUpdate.OrgAppJoins.Select(c => c.Org.ID));
foreach (var org in context.Org)
{
if (selectedOrgsHS.Contains(org.OrgName.ToString()))
{
if (!appOrgs.Contains(org.ID))
{
appToUpdate.OrgAppJoins.Add(
new OrgAppJoin
{
AppID = appToUpdate.ID,
OrgID = org.ID
});
}
}
else
{
if (appOrgs.Contains(org.ID))
{
OrgAppJoin orgToRemove
= appToUpdate
.OrgAppJoins
.SingleOrDefault(i => i.OrgID == org.ID);
context.Remove(orgToRemove);
}
}
}
}
I then process them in OnGetAsync in Edit.cshtml.cs:
public async Task<IActionResult> OnGetAsync(int? id)
{
this.TreeData = GetOrgTreeData();
if (id == null)
{
return NotFound();
}
App = await _context.App
.Include(i => i.OrgAppJoins).ThenInclude(i => i.Org)
.Include(i => i.RoleAppJoins).ThenInclude(i => i.Role)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (App == null)
{
return NotFound();
}
PopulateAssignedAppRoleData(_context, App);
PopulateAssignedAppOrgData(_context, App);
return Page();
}
and OnPostAsync
public async Task<IActionResult> OnPostAsync(int? id, string[] selectedOrgs, string[] selectedRoles)
{
if (!ModelState.IsValid)
{
return Page();
}
var appToUpdate = await _context.App
.Include(i => i.OrgAppJoins).ThenInclude(i => i.Org)
.Include(i => i.RoleAppJoins).ThenInclude(i => i.Role)
.FirstOrDefaultAsync(s => s.ID == id);
if (await TryUpdateModelAsync<App>(
appToUpdate,
"app", // Prefix for form value.
c => c.AppName, c => c.AppDescription, c => c.DisplayOrder))
{
UpdateAppOrgs(_context, selectedOrgs, appToUpdate);
UpdateAppRoles(_context, selectedRoles, appToUpdate);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
UpdateAppOrgs(_context, selectedOrgs, appToUpdate);
UpdateAppRoles(_context, selectedRoles, appToUpdate);
PopulateAssignedAppOrgData(_context, App);
PopulateAssignedAppRoleData(_context, App);
return Page();
}
This works fine when using multiselect dropdowns and reads and writes correctly to the many to many join tables.
I am using kendo controls at the moment, but I'm happy to use generic if I can find a solution to my problem.
I need to have a treeview for the Org model to display as a multi select, so I have two examples I am trying to get working, that behave differently, the DropDownTreeview is not essential but in some pages it will be handy, the TreeView is essential.
The first is the TreeView:
#(Html.Kendo().TreeView()
.Name("selectedOrgNames")
.DataTextField("OrgName")
.Checkboxes(checkboxes => checkboxes
.Name("ischecked")
.CheckChildren(true))
.HtmlAttributes(new { style = "width:100%" })
.DataSource(d => d
.Read(read =>
read.Url("/Apps/Edit?handler=Read")
)
)
)
The second is the DropDownTreeview:
#(Html.Kendo().DropDownTree()
.Placeholder("Select ...")
.Name("selectedOrgs")
.DataTextField("OrgName")
.DataValueField("ID")
.Checkboxes(checkboxes => checkboxes
.Name("ischecked")
.CheckChildren(true))
.AutoClose(false)
.Value(Model.selectedOrgNames)
.HtmlAttributes(new { style = "width:100%" })
.DataSource(d => d
.Read(read =>
read.Url("/Apps/Edit?handler=Read")
)
)
)
Both examples read the datasource from here in edit.cshtml.cs:
public IActionResult OnGetRead(int? id)
{
var result = from e in _context.Org
where id.HasValue ? e.ParentID == id : e.ParentID == null
select new
{
id = e.ID,
hasChildren = (from q in _context.Org
where (q.ParentID == e.ID)
select q
).Count() > 0,
OrgName = e.OrgName,
selected = (from s in _context.OrgAppJoin
where (s.OrgID == e.ID) && (s.AppID == 2)// <--this works, this doesn't--> (s.AppID == app.ID)
select s
).Count() > 0,
ischecked = (from s in _context.OrgAppJoin
where (s.OrgID == e.ID) && (s.AppID == 2)// <--this doesn't work, this doesn't either-->
(s.AppID == app.ID)
select s
).Count() > 0
};
return new JsonResult(result);
}
My first issue is probably very simple, I'm new to this platform: I can't seem to find a way to get the value of the AppID from the page into the OnGetRead Module ( i have hard coded s.AppID == 2 as an test example to see if it works) I have tried all sorts of variables and other methods.
The id passed into OnPostAsync and OnGetAsync is the id of the App, but the id passed into the OnGetRead is the id of the Org, which is correct and works, but how do I use the AppID from the page to replace the number two in this line? where (s.OrgID == e.ID) && (s.AppID == 2)?
My second issue is getting the checkboxes to read and write.
The DropDownTree above writes to database correctly, but doesn't read and populate checkboxes.
The Treeview doesn't populate checkboxes or write to the database, however it DOES read the selected value from the join table for app number 2 (or any other number I manually insert) in OnGetRead, and displays different font colours etc for the correct items as selected, but not as checked (aschecked may not even be a valid call theere, but I can't find a reference for that).
I feel like I'm close but I have tried for so long to get this to work to no avail, including to and fro with telerik who have tried help but they don't really help much with modelling to your own code, just with static data, and they seem to be light on for a Razor app knowledgebase and keep giving examples of controllers with hard coded data.
Please advise if I need to split this into two questions or more but any help will be much appreciated
Thanks

Filtering "include" entities in EF Core

I have the following models
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
public List<PersonRole> PersonRoles { get; set; }
}
public class RoleInDuty
{
public int roleInDutyId { get; set; }
public string Name { get; set; }
public int typeOfDutyId { get; set; }
public TypeOfDuty typeOfDuty { get; set; }
public List<PersonRole> PersonRoles { get; set; }
}
public class PersonRole
{
public int PersonId { get; set; }
public Person Person { get; set; }
public int RoleInDutyId { get; set; }
public RoleInDuty RoleInDuty { get; set; }
}
And now I can load all people with all their roles using the following code:
var people = _context.Persons
.Include(p => p.PersonRoles)
.ThenInclude(e => e.RoleInDuty).ToList();
But I wantn't load all data to List, I need load PersonRole according entered typeOfDutyId.
I am trying to solve this with the following code
people = _context.Persons
.Include(p => p.PersonRoles
.Where(t=>t.RoleInDuty.typeOfDutyId == Id)).ToList();
But VS throw error
InvalidOperationException: The Include property lambda expression 'p
=> {from PersonRole t in p.PersonRoles where ([t].RoleInDuty.typeOfDutyId == __typeOfDuty_typeOfDutyId_0) select
[t]}' is invalid. The expression should represent a property access:
't => t.MyProperty'. To target navigations declared on derived types,
specify an explicitly typed lambda parameter of the target type, E.g.
'(Derived d) => d.MyProperty'. For more information on including
related data, see http://go.microsoft.com/fwlink/?LinkID=746393.
As I understand I can't access property RoleInDuty.typeOfDutyId because i'm not include it yet.
I solved this problem with the following code
people = _context.Persons
.Include(p => p.PersonRoles)
.ThenInclude(e=>e.RoleInDuty).ToList();
foreach (Person p in people)
{
p.PersonRoles = p.PersonRoles
.Where(e => e.RoleInDuty.typeOfDutyId == Id)
.ToList();
}
Finally, filter in Include with ef core 5. Details in MSDN doc: https://learn.microsoft.com/en-us/ef/core/querying/related-data#filtered-include
Var people = _context.Persons
.Include(p => p.PersonRoles
.Where(t=>t.RoleInDuty.typeOfDutyId == Id))
.ToList();
var blogs = context.Blogs
.Include(e => e.Posts.OrderByDescending(post => post.Title).Take(5)))
.ToList();
devnull show the next How to filter "Include" entities in entity framework?, and there the same problem, I read it, and find the answer. Solve my problem can with the next:
var temp = _context.Persons.Select(s => new
{
Person = s,
PersonRoles= s.PersonRoles
.Where(p => p.RoleInDuty.typeOfDutyId == this.typeOfDuty.typeOfDutyId)
.ToList()
}).ToList();

NHibernate code mapping bag join two tables

I have the following definition for a Transaction (as in purchase details) object :
public class Transaction : MappingObject
{
public virtual int Id { get; set; }
public virtual IList<TransactionProduct> Products { get; set; }
}
public class TransactionMap : ClassMapping<Transaction>
{
public TransactionMap()
{
Table("TRANSACTIONS_TBL");
Id(x => x.Id, m =>
{
m.Column("ID");
m.Generator(Generators.Identity);
});
Bag(x => x.Products, m =>
{
m.Inverse(true);
m.Table("TRANSACTION_PRODUCTS_TBL");
m.Key(k => k.Column("TRANSACTION_ID"));
m.Lazy(CollectionLazy.NoLazy);
},
relation => relation.OneToMany(mapper => mapper.Class(typeof(TransactionProduct))));
}
}
And TransactionProduct is defined like this :
public class TransactionProduct : MappingObject
{
public virtual int TransactionId { get; set; }
public virtual int ProductId { get; set; }
public virtual int Quantity { get; set; }
public override bool Equals(object obj)
{
var t = obj as TransactionProduct;
if (t == null)
return false;
if (TransactionId == t.TransactionId && ProductId == t.ProductId)
return true;
return false;
}
public override int GetHashCode()
{
return (TransactionId + "|" + ProductId).GetHashCode();
}
}
public class TransactionProductMap : ClassMapping<TransactionProduct>
{
public TransactionProductMap()
{
Table("TRANSACTION_PRODUCTS_TBL");
ComposedId(map =>
{
map.Property(x => x.TransactionId, m => m.Column("TRANSACTION_ID"));
map.Property(x => x.ProductId, m => m.Column("PRODUCT_ID"));
});
Property(x => x.Quantity, m => m.Column("QUANTITY"));
}
}
Now, I want to select a transaction and populate the Products array in a single select (I know I can select the transaction then the products but It's bad practice)
So I'm using this :
using (var session = CommonDAL.GetSession())
{
Transaction transactionAlias = null;
TransactionProduct transactionProductAlias = null;
return session.QueryOver(() => transactionAlias).
JoinAlias(() => transactionAlias.Products, () => transactionProductAlias).
Where(() => transactionAlias.Id == transactionProductAlias.TransactionId).List().ToList();
}
This work's quite well but the problem is that if I have a transaction with 2 products, I get 2 transaction objects with 2 products inside them, same goes for if I have a transaction with 4 products, I get 4 transaction objects with 4 products. The transaction objects are good, but the problem is the duplicates.
I can probably solve it with Distinct() but again, I want best practice
I solved it using .TransformUsing(Transformers.DistinctRootEntity) after the Where(...)

Why does calling .Include() after a .Join() not work?

I have a DB structure which is not ideal, but I have coded it into EF like this:
[Table("Item")]
public class Item
{
[Key] public int Id { get; set; }
public int CategoryId { get; set; }
public int ItemTypeId { get; set; } // An ItemTypeId of 1 means this row refers to an Article
public int ItemId { get; set; } // this refers to the Article primary key
}
[Table("Article")]
public class Article
{
[Key] public int Id { get; set; }
...
public virtual ICollection<SubArticle> SubArticles { get; set; }
}
[Table("SubArticle")]
public class SubArticle
{
...
public int ArticleId { get; set; }
}
modelBuilder.Entity<Article>().Collection(_ => _.SubArticles).InverseReference(_ => _.Article).ForeignKey(_ => _.ArticleId);
What I want to do is get all articles (with the corresponding sub-articles) that belong to a specific category. I have this query which is working:
var result = await Context.Set<Item>()
.Where(i => i.CategoryId == 200)
.Where(i => i.ItemTypeId == 1)
.Join(
Context.Set<Article>().Include(a => a.SubArticles),
i => i.ItemId,
a => a.Id,
(i,a) => a)
.ToListAsync();
result.SubArticles.First(); // works
My question is why does this query not work:
var result = await Context.Set<Item>()
.Where(i => i.CategoryId == 200)
.Where(i => i.ItemTypeId == 1)
.Join(
Context.Set<Article>(),
i => i.ItemId,
a => a.Id,
(i,a) => a)
.Include(a => a.SubArticles)
.ToListAsync();
result.SubArticles.First(); // error: result.SubArticles is null

LINQ to Entities, Where Any In

How to write 'Where Any In' in LINQ to Entity?
Here is my model :
class Chair
{
public int Id { get; set; }
public int TableId { get; set; }
public Table Table { get; set; }
}
class Table
{
public int Id { get; set; }
public ICollection<Chair> Chairs { get; set; }
public ICollection<Category> Categories { get; set; }
public Table()
{
Chairs = new List<Chair>();
Categories = new List<Category>();
}
}
class Category
{
public int Id { get; set; }
public ICollection<Table> Tables { get; set; }
}
I also got a simple list of Category :
List<Category> myCategories = new List<Category>(c,d,e);
I want to get only that Chairs that belongs to Table that got one of the Category from myCategories List. Thats what im trying to do :
var result =
ctx.Chairs.Where(x => x.Table.Categories.Any(y => myCategories.Any(z => z.Id == y.Id))).ToList();
I think its ok but what i get is error :
"Unable to create a constant value of type 'ConsoleApplication1.Category'. Only primitive types or enumeration types are supported in this context"
Try to compare with in-memory categories Ids collection, instead of categories collection.
var myCategoriesIds = myCategories.Select(c => c.Id).ToArray();
var result =
context.Chairs
.Where(
x => x.Table.Categories.Any(
y => myCategoriesIds.Contains(y.Id)))
.ToList();
this is because ctx.Chairs is a collection that is in database, you should retrieve that collection first in order to compare it with in-memory data:
var result = ctx
.Chairs
.AsEnumerable() // retrieve data
.Where(x =>
x.Table.Categories.Any(y =>
myCategories.Any(z => z.Id == y.Id)))
.ToList();
EDIT: that wouldn't be the correct thing to do if you have a lot of entities on database, what you can do is to split it into two queries:
var tables = ctx.Tables
.Where(x =>
x.Categories.Any(y =>
myCategories.Any(z => z.Id == y.Id)));
var result = ctx.Chairs
.Where(x =>
tables.Any(t=> t.Id == x.TableId))
.ToList();
You can select Ids from myCategories and use it last statement.
var CategoryIds = myCategories.Select(ct => ct.Id);
var result = ctx.Chairs.Where(x => x.Table.Categories.Any(y => CategoryIds.Any(z => z == y.Id))).ToList();

Categories