query through five tables with nested Any operator - c#

I'm trying to create a single query to get all the users that belong to a certain announcement, but I cannot seem to get my query to work.
I'm getting the following error:
Unable to create a constant value of type 'RequestSupplierEntity'.
Only primitive types or enumeration types are supported in this
context.
I do not know exactly what the error message means, or what I should do/avoid to prevent it. Any help or insight into the problem, or even a working query, would be greatly appreciated.
It seems to me that a single T-SQL query should be possible using a EXIST subquery, I just don't know if entity framework is able to construct it in this case.
My query statement:
Users.Where(u => notification.Announcement.RequestSuppliers.Any(rs => rs.Supplier.OrganisationId == u.OrganisationId));
Users is a DBSet. notification is the entity instance I'm trying to find the associated users for. I have the feeling that the problem lies with using the NotificationEntity instance within the where method of the DBSet, but I don't see how I should do this otherwise.
The POCO entities relate as follows:
User has a Organisation
Supplier is a Organisation
RequestSupplier is a Supplier
RequestSupplier has a Announcement
Notification has a Announcement
So what I'm trying to do is to get the Users with OrganisationId's that belong to the Suppliers that are associated with the Announcement through the RequestSupplier.
The associated FluentAPI is:
modelBuilder.Entity<NotificationEntity>()
.HasOptional<AnnouncementEntity>(n => n.Announcement)
.WithMany(a => a.Notifications)
.HasForeignKey(n => n.AnnouncementId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<RequestSupplierEntity>()
.HasRequired<SupplierEntity>(rs => rs.Supplier)
.WithMany(s => s.RequestSuppliers)
.WillCascadeOnDelete(false);
modelBuilder.Entity<RequestSupplierEntity>()
.HasKey(rs => new { rs.SupplierId });
modelBuilder.Entity<RequestSupplierEntity>()
.HasRequired<AnnouncementEntity>(rs => rs.Announcement)
.WithMany(a => a.RequestSuppliers)
.HasForeignKey(rs => rs.AnnouncementId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<SupplierEntity>()
.HasRequired<OrganisationEntity>(s => s.Organisation)
.WithMany(o => o.Suppliers)
.HasForeignKey(s => s.OrganisationId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<UserEntity>()
.HasOptional<OrganisationEntity>(u => u.Organisation)
.WithMany(o => o.Users)
.WillCascadeOnDelete(true);
The notification I'm doing this query for always has an associated Announcement.
Entities:
[Table("Announcement")]
public class AnnouncementEntity
{
public int Id { get; set; }
public virtual ICollection<RequestSupplierEntity> RequestSuppliers { get; set; }
public virtual ICollection<NotificationEntity> Notifications { get; set; }
}
[Table("Notification")]
public class NotificationEntity
{
public int Id { get; set; }
public int? AnnouncementId { get; set; }
public virtual AnnouncementEntity Announcement { get; set; }
}
[Table("Organisation")]
public class OrganisationEntity
{
public int Id { get; set; }
public virtual ICollection<SupplierEntity> Suppliers { get; set; }
public virtual ICollection<UserEntity> Users { get; set; }
}
[Table("RequestSupplier")]
public class RequestSupplierEntity
{
public int SupplierId { get; set; }
public int AnnouncementId { get; set; }
public virtual SupplierEntity Supplier { get; set; }
public virtual AnnouncementEntity Announcement { get; set; }
}
[Table("Supplier")]
public class SupplierEntity
{
public int Id { get; set; }
public int OrganisationId { get; set; }
public virtual OrganisationEntity Organisation { get; set; }
public virtual ICollection<RequestSupplierEntity> RequestSuppliers { get; set; }
}
[Table("User")]
public class UserEntity
{
public int Id { get; set; }
public int? OrganisationId { get; set; }
public virtual OrganisationEntity Organisation { get; set; }
}

I'm not sure but try this
db.Users.Where(u => db.Notifications.Select(n=>n.Announcement).SelectMany(a=>a.RequestSuppliers).Any(rs => rs.Supplier.OrganisationId == u.OrganisationId));
I guess that you try to make a collection of collections of RequestSuppliers which is selected via Annoncement from of collection of Notification inside Where() method
in this case you have to use SelectMany
And in any case may use join for this
var users=(from u in db.Users
join s in db.Suppliers
on u.OrganisationId equals s.OrganisationId
join rs in db.RequestSuppliers
on s.Id equals rs.SupplierId
join a in db.Announcements
on rs.AnnouncementId equals a.Id
join n in db.Notifications
on a.Id equals n.AnnouncementId
select u);

Related

Find entities not in many-to-many relation

I might be looking at this the wrong way but I have a basic many-to-many code-first setup in EF Core 3.1 with Department <-> DepartmentDay <-> Day.
modelBuilder.Entity<DepartmentDay>(entity =>
{
entity.HasKey(dd => new { dd.DepartmentId, dd.DayId });
entity.HasOne(dp => dp.Day)
.WithMany(p => p.DepartmentDays)
.HasForeignKey(d => d.DayId);
entity.HasOne(dp => dp.Department)
.WithMany(p => p.DepartmentDays)
.HasForeignKey(d => d.DepartmentId);
});
First question: Is this relationship optional in the way that I can have days not connected to a department? I need this since this is related to opening hours and want to have generic days that affects all departments without having to make a specific connection to all departments. but as I stated in the beginning, I might look at this the wrong way.
Second question: If question one is true and valid setup, how do I get those days not connected to a department in a Linq-query?
What I have so far is (EDIT: changed allDays from Hashset to List.)
var allDays = await _context.Days.ToListAsync();
var allDepartmentDays = _context.DepartmentDays.Select(dd => dd.DayId).ToHashSet();
var genericDays = allDays.Where(d => !allDepartmentDays.Contains(d.Id));
Or is it better to use an raw query here for performance?
SELECT Id
FROM Day
WHERE Id NOT IN (SELECT DayId FROM DepartmentDay)
Edit 2: Including the whole data model
public class Department
{
public int Id { get; set; }
public int DepartmentNr { get; set; }
public string Service { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public string Postal { get; set; }
public string City { get; set; }
public string Url { get; set; }
public string MapUrl { get; set; }
public DateTime Created { get; set; }
public string CreatedBy { get; set; }
public DateTime? Updated { get; set; }
public string UpdatedBy { get; set; }
public ICollection<DepartmentPeriod> DepartmentPeriods { get; set; }
public ICollection<DepartmentDay> DepartmentDays { get; set; }
}
public class DepartmentDay
{
public int DepartmentId { get; set; }
public int DayId { get; set; }
public Department Department { get; set; }
public Day.Models.Day Day { get; set; }
}
public class Day
{
public int Id { get; set; }
public int PeriodId { get; set; }
public string Service { get; set; }
public string City { get; set; }
public DateTime? Date { get; set; }
public DayOfWeek? DayOfWeek { get; set; }
public DateTime? OpenTime { get; set; }
public DateTime? CloseTime { get; set; }
public bool IsClosed { get; set; }
public string Description { get; set; }
public DateTime Created { get; set; }
public string CreatedBy { get; set; }
public DateTime? Updated { get; set; }
public string UpdatedBy { get; set; }
public ICollection<DepartmentDay> DepartmentDays { get; set; }
public virtual Period.Models.Period Period { get; set; }
}
modelBuilder.Entity<Day>(entity =>
{
entity.HasOne(d => d.Period)
.WithMany(p => p.Days)
.HasForeignKey(d => d.PeriodId);
});
There is another relation not included in the initial question which kinds of answers my first question which is Department 1-M DepartmentPeriod M-1 Period 1-1 Day. So there will be days in the Day table that has no relation to DepartmentDay but only to Period and to neither, correct?
First question: Is it optional? Really a tough question without your data types, but suppose you have the navigation properties correctly in place, you only need to specify the navigation properties if they keys are not named {DataType}Id, so if you name them like that it is not required, otherwise you have to specify which foreign key fields to use and which are the keys. If you do not specify relations, entity framework will generate the tables for you for the many to many relations.
Second question:
In terms of performance the SQL query will always outperform if you are able to write a good query, the one you suggest or even
select a.id from [day] a left join departmentday b on a.id = b.dayid where b.dayid is null
could perform slightly better even so,
However in terms of testability the sql is a problem because we'd want to run an InMemory model of our entity dbcontext and then your sql cannot execute typically at least.
So the question is if you really require that extra performance and is is possible to write a linq query that efficiently enable the linq engine to write a similar query, by making it part of the same expression instead of two or more.
In Your case, I could be missing something in the data model presently not provided, but it seems like you could get what you want like this:
_context.Days.Include(d => d.DepartmentDays).Where(!d.DepartmentDays.Any());
update:
Looking at the model it seems there are some model work missing and that the projection You're looking for can be done much along the lines suggested.
Changes/ addition to model creating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Department>()
.HasKey(k => k.Id);
modelBuilder.Entity<DepartmentDay>()
.HasKey(k => new { k.DayId, k.Department });
modelBuilder.Entity<Day>()
.HasOne(d => d.Period)
.WithMany(p => p.Days)
.HasPrincipalKey(k => k.PeriodId)
.HasForeignKey(d => d.Id);
//Notice your departmentid is not alone a foreign key in the relationship table, as departments can have one row per day
modelBuilder.Entity<DepartmentDay>()
.HasOne(a => a.Department)
.WithMany(b => b.DepartmentDays)
.HasPrincipalKey(p => p.Id)
.HasForeignKey(f => new { f.DepartmentId, f.DayId });
base.OnModelCreating(modelBuilder);
}
and the in the context we can make such query using:
public List<Day> GetDaysWithoutAnyDepartmentDays()
{
return Days
.Include(i => i.DepartmentDays)
.Where(x => !x.DepartmentDays.Any()
).ToList();
}

EFCore, many-to-many, DB First, navigation properties not working

I am trying to map to an existing database. When I use the below code and pull a list of results for a given entity, I get back the results I expect.
However, when I attempt to add .Include(x => x.Book) to a simple list query against the UserBook table, my result set returns empty.
[Table("User")]
public class User
{
[Key]
public Guid UserId { get; set; }
public string UserName{ get; set; }
// Reference
public ICollection<UserBook> UserBooks { get; set; }
}
[Table("Book")]
public class Book
{
[Key]
public Guid BookId { get; set; }
public string BookName{ get; set; }
// Reference
public ICollection<UserBook> UserBooks { get; set; }
}
[Table("UserBook")]
public class UserBook
{
public Guid UserId { get; set; }
public Guid BookId { get; set; }
public int PermissionMask { get; set; }
public bool Deleted { get; set; }
// Ref
public User User { get; set; }
public Book Book { get; set; }
}
I'm following the instructions layed out here: http://www.learnentityframeworkcore.com/configuration/many-to-many-relationship-configuration
To better illustrate the issue, here are the queries I'm using:
// Returns 1,000+ rows.
_context.UserBooks.ToList();
// Returns 1 row
_context.UserBooks
.Where(x => x.UserId == "SomeGuid")
.ToList();
// Returns 0 rows
_context.UserBooks
.Include(x => x.Book)
.Where(x => x.UserId == "SomeGuid")
.ToList();
// Returns 0 rows
_context.UserBooks.Include(x => x.Book).ToList();
I think EF would complain that UserBook entity does not have keys defined, not sure how you got it to work. But to make the include work I believe you need to be explicit on the composite key. Try adding the following to your DbContext class:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<UserBook>(e => e.HasKey(c => new { c.UserId, c.BookId }));
}
Your mapping indicates a non-nullable foreign key (so EF will generate an inner join in the query). Try changing datatype on the foreign keys from Guid to Guid? and try again.

Entity Framework with Condition Relationship

Is it possible to have a relationship that is based on a condition in Entity Framework? My model looks something like this...
public class Document
{
public int Id { get; set; }
public string Name { get; set; }
public OwnerType OwnerType { get; set; }
public int OwnerId { get; set; }
public virtual Organization OrganizationOwner { get; set; }
public virtual User UserOwner { get; set; }
}
public enum OwnerType
{
Organization = 1,
User = 2
}
public class Organization
{
public int Id { get; set; }
public string Name { get; set; }
//[other properties specific to Organization]
public virtual List<Documents> Documents { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
//[other properties specific to User]
public virtual List<Documents> Documents { get; set; }
}
So, what I'd like to is set up a relationship so that the OrganizationOwner property of a Document instance automatically gets populated when the OwnerType == OwnerType.Organization, and the UserOwner property is populated when OwnerType == OwnerType.User.
Is this possible to set up this kind of relationship in EntityFramework - Code First? Something like this in the mapping...
EntityTypeConfiguration<Document>.HasOptional(d => d.OrganizationOwner)
.WithMany(o => o.Documents)
.HasForeignKey(d => d.OwnerId)
.Where(d => d.OwnerType == OwnerType.Organization);
EntityTypeConfiguration<Document>.HasOptional(d => d.UserOwner)
.WithMany(u => u.Documents)
.HasForeignKey(d => d.OwnerId)
.Where(d => d.OwnerType == OwnerType.User);
I would like to be able to leverage joins on the OrganizationOwner and UserOwner when setting up my Linq queries on the context so that I don't have to do a separate selects on those entities for each Document. Is this type of relationship supported or is there a better way to do this? Thanks.

How do you map many-to-many relationships?

How does one map many to many relationships?
One-to-one relationships are easy....
Assuming...
public class ProductDTO
{
public int Id { get; set; }
public string Name { get; set; }
public int SupplierId { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public double Cost { get; set; }
public Supplier Supplier { get; set; }
}
public class Supplier
{
public int Id { get; set; }
public string Name { get; set; }
public int Rating { get; set; }
public ICollection<Product> Products { get; set; }
}
Mapper.CreateMap<Product, ProductDTO>()
.ForMember(d => d.SupplierId, m => m.MapFrom(s => s.Supplier.Id));
Works assuming every product has only one supplier and a supplier can have many products. How do I map it if a product can also have many suppliers?
I changed supplier line in Products to
public ICollection<Supplier> Supplier { get; set; }
and in ProductDTO doing the same
public ICollection<int> SupplierId { get; set; }
How do I alter CreateMap since the Ids are now collections? Autocomplete no longer shows Id and all I get are functions.
I'm new to C# so I many be missing something obvious. Am I supposed to iterate in a for loop and map the ids one by one?
You may try to use:
Mapper.CreateMap<Product, ProductDTO>()
.ForMember(d => d.SupplierIds, m => m.MapFrom(p => p.Suppliers.Select(s => s.Id)));
Another option:
Mapper.CreateMap<Supplier, int>().ConvertUsing(s => s.Id);
Mapper.CreateMap<Product, ProductDTO>()
.ForMember(d => d.SupplierIds, m => m.MapFrom(p => p.Suppliers));
One more thing if you are this using DTO to pass data from Web/WCF service you may consider using
public ICollection<SupplierDTO> Supplier { get; set; }
instead if passing supplier ids only. In most cases it's better (and more effective) to pass more data in one call to the service than doing few calls.

How to write a an EF query involving a many to many relationship

I'm quite new to using the entity framework and I'm having trouble getting my head around how to write a query that uses a many to many relationship. I have 3 entities. Role, User and Securable. A Role can have multiple Securables, and a Securable can be assigned to many Roles. A Role can have multiple Users, and a User can have multiple Roles.
My Question is: How would I go about writing a query that gave me a distinct list of Securables for a given user ID?
Here is my model, where EF automatically creates the link tables for me.
public class SecurityContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<Securable> Securables { get; set; }
}
public class User
{
public Guid UserId { get; set; }
public string Forename { get; set; }
public string Surname { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public virtual ICollection<Role> Roles { get; set; }
}
public class Securable
{
public Guid SecurableId { get; set; }
public string Name { get; set; }
public virtual ICollection<Role> Roles { get;set;}
}
public class Role
{
public Guid RoleId { get; set; }
public string Name { get; set; }
public virtual ICollection<Securable> Securables { get; set; }
public virtual ICollection<User> Users { get; set; }
}
Untested, but off the top of my head it would be something like this:
var context = new DbContext();
var result = context.Securables
.Where(s => s.Roles
.Any(r => r.Users
.Any(u => u.UserId = userId)))
.Distinct();
Like this?
User user = ...;
IEnumerable<Securable> securablesForUser =
user.Roles.SelectMany(x => x.Securables).Distinct();
Update:-
After working on a project where this was genuinely a performance bottleneck, I investigated more deeply and found that the following LINQ query generates the best SQL (for our data):-
IEnumerable<Securable> securablesForUser =
context.Users.Where(x => x.UserId == userId)
.SelectMany(x => x.Roles)
.SelectMany(x => x.Securables)
.Distinct();
This will use an INNER JOIN in the translated SQL wheras:-
IEnumerable<Securable> securablesForUser = context.Securables.Where(
x => x.Roles.Any(y => y.Users.Any(z => z.UserId == userId))).Distinct();
uses WHERE EXISTS which in our benchmark was slower than querying twice.
As always, if you have performance concerns I recommend profiling. Results with your data may differ. If you don't care enough to profile, you don't care enough to optimise!

Categories