Querying many-To-many relationship with a list of parameters - entity-framework

I use EF core and i have the classes "User" and "Authority" which are connected with a many-to-many relationship ("Users" has a property "authorities" and "Authority" has a property "users"). The relationship is managed via middle class "UserAuthority".
I need to query all "Users" that have "Authorities" with certain names.
I tried this:
List<string> authorities = A list of authorities;
(from user in this.dbContext.user.Include("authorities.authority")
where authorities.Any(x => user.authorities.Any(y => y.authority.authority == x))
select new UserDto(user.id, user.firstname + " " + user.lastname)).ToList()
But the console says that LINQ cant translate
authorities.Any(x => user.authorities.Any(y => y.authority.authority == x))
and that it will be handled in memory.
What is the correct approach here?

Currently the only translatable in-memory collection method is Contains (for primitive type in-memory collection, translates to SQL IN(...)).
So instead of
authorities.Any(x => user.authorities.Any(y => y.authority.authority == x))
use
user.authorities.Any(ua => authorities.Contains(ua.authority.authority))

You wrote:
i have the classes "User" and "Authority" which are connected with a many-to-many relationship ("Users" has a property "authorities" and "Authority" has a property "users").
Something like this:
class User
{
public int Id {get; set;}
...
// every User has zero or more Authorities (many-to-many)
public virtual ICollection<Authority> Authorities {get; set;}
}
class Authority
{
public int Id {get; set;}
public string Name {get; set;}
...
// every Authority has zero or more Users (many-to-many)
public virtual ICollection<User> Users {get; set;}
}
I need to query all "Users" that have "Authorities" with certain names.
If I read this literally, you want all Users, that have at least one Authority that has a Name that is in the collection of certainNames. You want each User with ALL his Authorities, even those Authorities with names that are not in certainNames
It could also mean that you want all Users, each with only those of their Authorities that have a Name which is in certainNames, but only those Users that have at least one such Authority.
How about this:
IEnumerable<string> certainNames = ...
var UsersWithAuthoritiesThatAreInCertainNames = myDbContext.Users
.Where (user => user.Authorities.Select(authority => authority.Name)
.Intersect(certainNames)
.Any())
.Select(user => new
{
// select only the User properties you actually plan to use:
Id = user.Id,
Name = user.Name,
...
Authorities = user.Authorities.Select(authority => new
{
// again select only the Authority properties you plan to use:
Id = authority.Id,
Name = authority.Name,
...
})
.ToList(),
})
In words:
From the collection of all users, keep only those users, that have at least one authority with a name that is also in certainNames. From the remaining users, select several properties.
If you don't want ALL Authorities of the user, but only the ones that are in certain names:
var UsersWithOnlyTheirAuthoritiesThatAreInCertainNames = myDbContext.Users
.Select(user => new
{
// select only the User properties you actually plan to use:
Id = user.Id,
Name = user.Name,
...
Authorities = user.Authorities
.Where(authority => certainNames.Contains(authority.Name))
.Select(authority => new
{
// again select only the Authority properties you plan to use:
Id = authority.Id,
Name = authority.Name,
...
})
.ToList(),
})
// keep only the Users that have at least one such authority
.Where(selectedUser => selectedUser.Authorities.Any());
In words:
from the collection of Users, select some properties of every user, inclusive some properties of only those Authorities of the user that have a name that is also in certainNames. From the remaining sequence of selected users keep only those users that have at least one authority left

Related

Linq result return zero vs null

Is there a way I have have this linq statement not return nulls ? I would like it to returns 0 I have tried ?? 0 but that is not working
Customer = (from c in db.Customers
join cx in db.CustomerXrefs on c.CustomerId equals cx.CustomerId
join g in db.Groups on cx.GroupId equals g.GroupId
select new Customer
{
CustomerId = c.CustomerId,
Institution = (from ig in db.Groups
join icx in db.CustomerXrefs on ig.GroupId equals icx.GroupId
where icx.CustomerId == c.CustomerId && ig.GroupTypeId == 308
select new Institution()
{
Name = ig.Name == "In1 " ? "Institution 1" :
ig.Name == "In2" ? "Institution 2" :
ig.Name
}
).FirstOrDefault()
}).FirstOrDefault();
It is a bit difficult to distill from your question what you want to achieve. You give us some code, and tell us that the code doesn't do what you want, and ask us to give you the code that does give the requested result.
I hope I can extract your requirement from your code.
It seems that you have a table of Customers and a table of Groups. There seems to be a many-to-many relation between Customers and Groups: every Customer belongs to zero or more Groups, every Group has zero or more Customers that belong to this Group.
In relational databases, a many-to-many relation is implemented using a junction table. The junction table for the many-to-many between Customers and Groups is table CustomerXRefs
For every Customer in the table of Customers, you want its CustomerId, and a Property Institution.
The value of Institution is filled with a Name. This name is taken from the first Group of this Customer with GroupTypeId equal to 308.
There are two solutions for this:
Use GroupJoins to get the Customers with their Groups and extract the Institution from these groups
Use the capability of entity framework to translate the virtual ICollection<...> into the corrrect GroupJoins. Use the virtual properties, entity framework translates them into the proper GroupJoin.
Usually the latter solution is easier, so we'll start with that one first.
The classes
If you have followed the entity framework conventions, you'll have classes similar to the following:
class Customer
{
public int Id {get; set;}
... // other properties
// every Customer belongs to zero or more Groups (many-to-many)
public virtual ICollection<Group> Groups {get; set;}
}
class Group
{
public int Id {get; set;}
public int GroupTypeId {get; set;}
public string Name {get; set;}
... // other properties
// every Group has to zero or more Customers(many-to-many)
public virtual ICollection<Customer> Customers {get; set;}
}
This is enough for entity framework to detect your many-to-many relation. Without even mentioning it, entity framework will create a junction table for you. Only if you want different names of tables or properties, you'll need fluent API or attributes.
In entity framework the columns of the table are represented by non-virtual properties; the virtual properties represent the relations between the tables: one-to-many, many-to-many, ...
Query using the virtual ICollection
var customersWithTheirGroups = dbContext.Customers.Select(customer => new
{
CustomerId = customer.Id,
Institution = customer.Groups.Where(group => group.GroupTypeId == 308)
.Select(group => new Institution
{
name = (ig.Name == "In1") ? "Institution 1" :
(ig.Name == "In2") ? "Institution 2 :
ig.Name,
})
.FirstOrDefault(),
});
BTW: note the parentheses around (ig.Name == "In1"). I think that if Name equals "In1" that you want a name "Institution 1", etc.
Is it correct that you have a class Institution with only one property?
class Institution
{
public string Name {get; set;}
}
Although it is allowed, it is a bit awkward, why not just select the InstitutionName?
.Select(group => (ig.Name == "In1") ? "Institution 1" :
(ig.Name == "In2") ? "Institution 2 :
ig.Name)
.FirstOrDefault(),
Anyway, entity framework knows the relations between the tables and will create the correct groupjoins with the junction tables.
This solution seems very natural.
Solution using GroupJoins
If you want to do the GroupJoin yourself, you'll need to define the junction table:
class CustomerXrefs
{
// every Xref in the junction table belongs to exactly one Customer:
public int CustomerId {get; set;}
// every Xref in the junction table belongs to exactly one Group:
public int GroupId {get; set;}
}
To get each Customer with its Groups, do a GroupJoin of the XRefs with the Customers, followed by another GroupJoin of Groups and XRefs.
var customersWithTheirGroups = dbContext.Customers.GroupJoin(dbContext.CustomerXRefs,
customer => customer.Id, // from every Customer take the Id
xref => xref.CustomerId, // from every xref take the foreign key to Customer
// Parameter resultSelector:
// for every Customer and all its xrefs make one new object:
(customer, xrefsOfThisCustomer) => new
{
Customer = customer,
// To get the Groups of this customer, GroupJoin the Groups with the xrefs:
Groups = dbContext.Groups
// GroupJoin the Group with the xrefs of this Customer
.GroupJoin(xrefsOfThisCustomer,
group => group.Id, // for every Group take the Id,
xRef => xref.GroupId, // for every xref of this customer take the foreign key
// parameter resultSelector:
// for every Group, and all xrefs of this Group that are xrefs of the Customer
// make one new:
(group, xrefs) => group).ToList(),
});
Result: all Customers, each with their Groups.
Select the institution:
var result = customersWithTheirGroups.Select(joinResult => new
{
CustomerId = joinResult.Customer.Id,
Institution = joinResult.Groups
// Keep only the Groups with the desired GroupTypeId
.Where(group => group.GroupTypeId == 308)
// The Select is similar as the Select in the virtual ICollection method:
.Select(group => new Institution
{
name = (ig.Name == "In1") ? "Institution 1" :
(ig.Name == "In2") ? "Institution 2 :
ig.Name,
})
.FirstOrDefault(),
});
Customer is a class (reference type). Default value for a reference type is null. If you want to return some non null value (if there exists correct "default" for your use case) you will need to provide one, for example:
Customer = (...).FirstOrDefault() ?? new Customer();

Entity Framework group by left join query

The Result should look like this
I've started to use Entity Framework for one month so I am not familiar with linq queries. The query I wrote in SQL is:
SELECT
om0001.CUSTOMER, om0001.ITEM_CODE,
SUM(om0001.AMOUNT) AS AMOUNT,
SUM(ep0001.EXPORT_AMOUNT) AS EXPORT_AMOUNT
FROM
om0001
LEFT OUTER JOIN
ep0001 ON om0001.ID = ep0001.om0001_ID
GROUP BY
om0001.CUSTOMER, om0001.ITEM_CODE;
When I run this query in SQL, it runs well so I tried to convert it to linq queries.
What I made so far is
var testjoin = from om0001 in EF.om0001
join ep0001 in EF.ep0001 on om0001.ID equals ep0001.om0001_ID
into jointable
from z in jointable.DefaultIfEmpty()
group z by new {om0001.CUSTOMER, om0001.ITEM_CODE } into g
select new
{
CUSTOMER = g.Key.CUSTOMER,
ITEM_CODE = g.Key.ITEM_CODE,
om0001SUMamount = g.Sum(x => x.AMOUNT),
ep0001EXPORTsumAmount = g.Sum(y => y.EXPORT_AMOUNT)
};
The problem over this linq query is I can not get om0001SUMamount. I get only ep0001 column data. Please help
Obviously, I cant peek into your EF database, so I created some sample data (the 'item' class structures are implied):
var EF = new efClass {
om0001 = new List<om0001item> {
new om0001item { ID = 0, CUSTOMER = 0, ITEM_CODE = 0, AMOUNT = 10 },
new om0001item { ID = 1, CUSTOMER = 0, ITEM_CODE = 0, AMOUNT = 20 },
new om0001item { ID = 2, CUSTOMER = 1, ITEM_CODE = 1, AMOUNT = 30 },
new om0001item { ID = 3, CUSTOMER = 1, ITEM_CODE = 1, AMOUNT = 40 }
},
ep0001 = new List<ep0001item> {
new ep0001item { om0001_ID = 0, EXPORT_AMOUNT = -20 },
new ep0001item { om0001_ID = 1, EXPORT_AMOUNT = -20 }
}
};
With this data, I created a query that frankly feels inelegant and left me disappointed, but that's the nature of left joins in LINQ:
var testjoin = from om0001 in EF.om0001
join ep0001 in EF.ep0001 on om0001.ID equals ep0001.om0001_ID into jointable
select new { om0001, ep0001 = jointable.DefaultIfEmpty() } into combined
group combined by new {
combined.om0001.CUSTOMER,
combined.om0001.ITEM_CODE
} into g
select new {
CUSTOMER = g.Key.CUSTOMER,
ITEM_CODE = g.Key.ITEM_CODE,
om0001SUMamount = g.Sum(x => x.om0001.AMOUNT),
ep0001EXPORTsumAmount = g.Sum(x => x?.ep0001.Sum(y => y?.EXPORT_AMOUNT ?? 0))
};
Bottom line is that when you group by 'jointable', you've lost ep0001 references. So select both ep0001 and om0001 into a new 'combined' object, and then group based off of it.
When I created a javascript library (fluent-data) that had some LINQ-like functionality, I developed a lot of respect and compassion for the LINQ developers. Nevertheless, I don't know why they don't just create a left join operator to add so much more value to all the C# developers who use LINQ.
So you have a table with Oms (actually Om00001, but I'm not going to write all those 0001 over and over again), and a table with Eps, and you have a one-to-many relation between Oms and Eps: Every Om has zero or more Eps, and every Ep belongs to exactly one Om, namely the Om that foreign key EpId refers to.
If you have followed Entity Framework code first conventions you will have classes similar to the following:
class Om
{
public int Id {get; set;}
public string Customer {get; set;}
public string ItemCode {get; set;}
...
// Every Om has zero or more Eps (one-to-many)
public virtual ICollection<Ep> Eps {get; set;}
}
class Ep
{
public int Id {get; set;}
public int Amount {get; set;}
public int ExportAmount {get; set;}
...
// every Ep belongs to exactly one Om, using foreign key
public int OmId {get; set;}
public virtual Om Om {get; set;}
}
This is enough for entity framework to detect your one-to-many relationship. Because I followed the conventions, there is no need for Attributes, nor fluent API. If you want different table names, or columns, you need fluent API / attributes.
In entity framework the non-virtual properties represent the columns of your tables, the virtual properties represent the relations between the tables (one-to-many, many-to-many, ...)
Solution using GroupJoin
var result = dbContext.Oms.GroupJoin(dbContext.Eps,
om => om.Id, // from every Om take the primary key
ep => ep.OmId, // from every ep take the foreign key to the Om
(om, epsOfThisOm) => new // from every om and all eps having the correct foreign key
{ // make one new object
// Select only the properties that you plan to use:
Customer = om.Customer,
ItemCode = om.ItemCode,
Amount = epsOfThisOm.Sum(ep => ep.Amount);
ExportAmount = epsOfThisOm.Sum(ep => ep.ExportAmount);
});
Solution using the virtual ICollection
Instead of executing a GroupJoin, you could also use the virtual ICollection.
Requirement: from every Om, give me the Customer and the ItemCode, and the sum of all Amounts of its Eps, and the sum of all ExportAmounts of its Eps.
var result = dbContext.Oms.Select(om => new
{
Customer = om.Customer,
ItemCode = om.ItemCode,
Amount = om.Eps.Sum(ep => ep.Amount);
ExportAmount = om.Eps.Sum(ep => ep.ExportAmount);
});
This looks much neater, and it matches more directly your requirement. Entity framework knows the relations, and will do the correct GroupJoin for you.

Making a temporary ID for entities in EF before saving

If I have two tables, which have an Id, whish is an autogenerated int (seed), anyway I have a many to many relationship between these two tables which requires another table.
Now, I do a "dry run" to generate the items for the first two table before saving them, this works perfect. The problem is when I try to generate the items for the (many-many relationship) in the third table. Before saving the items all Ids in the first two tables will be set to 0, when adding items to the relation table I have no problems, the problems comes when saving the tables because the relationship table will have the Ids of 0.
Is there a way to overcome this problem? like assigning a temp value which will be automatically changed to the real Id in the relationship table before saving it ?
For the same reason, I've chosen not to use default Seed methods (AddOrUpdate) provided by EF, but I'm rather writing my own seed methods.
Now if I want to set up relationships, I'm not explicitly using ID's, but rather use navigational properties.
Imagine the scenario:
public class User
{
public int Id {get;set;}
public string Name {get;set;}
public virtual IList<Roles> Roles { get;set;}
}
public class Role
}
public int Id {get;set;}
public string Name {get;set;}
public virtual IList<User> Users {get;set;}
}
Doing this will seed both values for user and roles and relationships once you hit the save changes:
Role admin = new Role
{
Name = "Administrator"
};
Role basic = new Role
{
Name = "Basic"
};
User user = new User
{
Name = "John",
Roles = new List<Role>()
{
basic,
admin
}
}
db.Users.Add(user);
db.SaveChanges();

One-To-One relationship with fluent api. A Hacky way?

EF 4.3.1. I have defined User and Box entities. Each box may or may not be assigned to a user.
What I'd like to achieve is to have a OwnBox property in User class, and an Owner property in Box class.
in Database, I have defined OwnerId foreignkey in Boxes (Boxes.OwnerId has relation with Users.UserId).
To define the relationship with fluent api, I have defined the following classes:
public partial class User
{
public int UserId {get; set;}
public virtual Box OwnBox { get; set; }
}
public partial class Box
{
public int? OwnerId { get; set; }
public virtual User User { get; set; }
}
Then in my Mapping class for Box, I have defined the relations as follows:
this.HasOptional(t => t.User).WithOptionalDependent(d => d.OwnBox).
Map(m => m.MapKey("OwnerId")).WillCascadeOnDelete(true);
But by firing up the project, I got the error:
Schema specified is not valid. Errors: (56,6) : error 0019: Each
property name in a type must be unique. Property name 'OwnerId' was
already defined.
So I had to tell EF to forget about the OwnerId column first:
this.Ignore(t => t.OwnerId);
Now the project works fine. But I'm still doubtful if this is a good approach and will everything work fine on CRUD operations with foreign key associations.
First of all, this is not one-to-one relationship. In one-to-one relationship the foreign key must be a primary key.
I believe in your scenario the situation can happen:
User = { UserID = 2 }
Box1 = { UserID = 2 }
Box2 = { UserID = 2 }
Nothing stops you from doing that, but which box should be returned when you do that:
User.OwnBox, Box1 or Box2?
EF can deal with that using Independent Association. It will create foreign key, hidden from your POCO class. You can specify the name of the column using MapKey as you did. However, because you also created a property called OnwerID, just as the column used with MapKey, the EF has a problem as two properties are mapped to the same column.
When you use ignore, the POCO OwnerID property is ignored by EF so that fixes the problem of two properties, however, the OwnderID value never gets saved or read to the database. Because EF just ignores it.
Thanks for your question, I have learnt a lot thanks to this.

can't delete object that has a many-to-many relationship

these are my simplified entities:
public class User : Entity
{
public virtual ICollection<Role> Roles { get; set; }
}
public class Role : Entity
{
public virtual ICollection<User> Users { get; set; }
}
var user = dbContext.Set<User>().Find(id);
dbContext.Set<User>().Remove(user);
dbContext.SaveChanges(); // here i get error (can't delete because it's the
//referenced by join table roleUsers
the problems is that the join table references the user table and ef doesn't remove the records from the join table before deleting the user
I tried writing test cases and I noticed that:
if use the same context to add user with roles, save changes, remove and again save changes it works
but if I use 2 different contexts one for insert and another one for delete I get this error
You must first clear Roles collection (user's roles must be loaded) before you will be able to remove user.
If you want to just get this deleting just do what the error is saying.
as a part of your DELETE method, you should do this, in order.
1) Get User including its related roles you can user User.Include(r=>r.roles)
2) iterate through and delete the roles for the given user (make sure you use a toList() when you do this loop )
3) Delete the user
4) savechanges
user.Roles
.ToList()
.ForEach(role => user.Roles.remove(role));
context.Users.remove(user);
context.SaveChanges();