Entity framework conditional entity relation - c#

UPDATED
I have a Notification table as
public class Notification
{
public int SourceType { get; set; }
public int SourceID { get; set; }
....
// Relations
public virtual SourceA A { get; set; }
public virtual SourceB B { get; set; }
}
and there are two source tables
public class SourceA
{
public int Id { get; set; }
...
public virtual Notification {get; set;}
}
public class SourceB
{
public int Id { get; set; }
...
public virtual Notification {get; set;}
}
In modelbuilder
modelBuilder.Entity<SourceA>()
.HasOptional(c => c.Notification)
.WithRequired(x => x.A);
modelBuilder.Entity<SourceB>()
.HasOptional(c => c.Notification)
.WithRequired(x => x.B);
This is my new query
var myNotification = db.Notifications.Select(x => new Notification()
{
A= x.A,
B= x.B,
SourceID = x.SourceID,
}).ToList(); //error
I am getting this error The entity or complex type 'Notification' cannot be constructed in a LINQ to Entities query
The SourceTypes are A and B. how do i make entity relation in Notification according to the type of Source?
I used linq join on every query to join the related entites according to sourcetypes for now.
I am doing database first model.

You can do it creating a relation 0..1 to many
In SourceA and SourceB you have to add a property to reference to Notification Parent
public virtual Notification { get; set; }
And then create 0..1 to many Relation in you Entity Framework configuration
HasOptional(x => x.SourceA)
.WithRequired(s => s.Notification);
HasOptional(x => x.SourceB)
.WithRequired(s => s.Notification);
Query error
You can't do a Select with a new in a Query that is executed in Database, this only work in memory collections, to have the value of SourceID you can do something like
public int SourceID { get return SourceA = !null ? SourceA.ID : SourceB.ID; }

Related

Entity Framework Core: Many To Many relationship with some null values for some records

Let's say I have the following 3 classes - a Company that employs ConstructionWorkers and TruckDrivers. Let's say those people can be employed at many companies (as they work part time) - so it's a many to many relationship.
Some companies will only employ TruckDrivers, others will only employ ConstructionWorkers and others yet will employ both. This means in my CompanyEmployeeMapping table, the record could look like this:
CompanyEmployeeMapping Table:
+-------------+------------------+------------------------+
| CompanyId | TruckDriverId | ConstructionWorkerId |
+-------------+------------------+------------------------+
| 1 | 10 | NULL |
+-------------+------------------+------------------------+
(ie - Company only employs Truck Drivers)
This is how I set up my Class Structure + Fluent API:
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<CompanyEmployeeMapping> Employees { get; set; }
}
public class ConstructionWorker
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<CompanyEmployeeMapping> Companies { get; set; }
}
public class TruckDriver
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<CompanyEmployeeMapping> Companies { get; set; }
}
public class CompanyEmployeeMapping
{
public int CompanyId { get; set; }
public int TruckDriverId { get; set; }
public int ConstructionWorkerId { get; set; }
[JsonIgnore]
public Company LinkedCompany { get; set; }
[JsonIgnore]
public TruckDriver LinkedTruckDriver { get; set; }
[JsonIgnore]
public ConstructionWorker LinkedConstructionWorker { get; set; }
}
Fluent API setup:
builder.Entity<CompanyEmployeeMapping>()
.HasKey(x => new { x.CompanyId, x.TruckDriverId, x.ConstructionWorkerId });
builder.Entity<CompanyEmployeeMapping>()
.HasOne(c => c.LinkedCompany)
.WithMany(m => m.LinkedEmployees)
.HasForeignKey(x => x.CompanyId)
.HasPrincipalKey(x => x.Id);
builder.Entity<CompanyEmployeeMapping>()
.HasOne(c => c.LinkedTruckDriver)
.WithMany(m => m.LinkedCompanies)
.HasForeignKey(x => x.TruckDriverId)
.HasPrincipalKey(x => x.Id);
builder.Entity<CompanyEmployeeMapping>()
.HasOne(c => c.LinkedConstructionWorker)
.WithMany(m => m.LinkedCompanies)
.HasForeignKey(x => x.ConstructionWorkerId)
.HasPrincipalKey(x => x.Id);
This seems to work correctly for me - I get an object with companies, containing a list of TruckDrivers and a list of ConstructionWorkers. Each of those also have their corresponding companies.
QUESTION
When I try to add a new company, which only employs TruckDrivers I get an exception:
The value of CompanyEmployeeMapping.ConstructionWorkerId is unknown when attempting to save changes. This is because the property is also part of a foreign key for which the principal entity in the relationship is not known.
What am I doing wrong here?
It looks like each instance of the mapping entity will only map to either TruckDriver or ConstructionWorker.
Therefore, your TruckDriver and ConstructionWorker ids need to be nullable.
This will then create a new challenge that your composite key cannot contain a nullable property.
I think the way forward is to make those ints nullable and then create a new single property EmployeeMapping Id for the mapping entity key.

EF Core - many to many relationship use / access custom join table

I am trying to implement a many to many relationship.
The Models -
public class User
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<Book> OwnedBooks { get; set; }
}
public class Own
{
public int UserId { get; set; }
public int BookId { get; set; }
public User User { get; set; }
public Book Book { get; set; }
}
public class Book
{
[Key]
public int Id { get; set; }
public int AuthorId { get; set; }
public User Author { get; set; }
public List<User> OwnedBy { get; set; } //Not really needed, but without it I can't create the join table "Own"
[NotMapped]
public int UsersReached; //Get this via the "Own" table
}
The DbContext -
public class TestContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Book> Books { get; set; }
public DbSet<Own> Own { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer("Server=DESKTOP-BT4H8CA;Database=Test;Trusted_Connection=True");
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Book>().HasOne(x => x.Author);
builder.Entity<User>()
.HasMany(x => x.OwnedBooks)
.WithMany(x => x.OwnedBy)
.UsingEntity(x => x.ToTable("Own"));
builder.Entity<Own>()
.HasKey(x => new {x.BookId, x.UserId});
}
}
I am struggling with accessing the join table "Own". I need it to get the amount of each Book that is sold, without completely loading the users. That's why I don't want to use the auto generated one:
Cannot use table 'Own' for entity type 'BookUser (Dictionary<string, object>)' since it is being used for entity type 'Own' and potentially other entity types, but there is no linking relationship. Add a foreign key to 'BookUser (Dictionary<string, object>)' on the primary key properties and pointing to the primary key on another entity typed mapped to 'Own'.
Thanks in advance for your help!
You can actually use the auto-generated joining table and still get the count of each book sold, without completely loading the users.
With your current User and Book models, configure the relationships as -
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Book>()
.HasOne(p => p.Author)
.WithMany()
.HasForeignKey(p => p.AuthorId)
.OnDelete(DeleteBehavior.NoAction);
builder.Entity<User>()
.HasMany(p => p.OwnedBooks)
.WithMany(p => p.OwnedBy);
}
Then you can query the books with their count of sales as -
var books = dbCtx.Books
.Select(p => new Book
{
Id = p.Id,
AuthorId = p.AuthorId,
Author = p.Author,
UsersReached = p.OwnedBy.Count // this will not load User entities
})
.ToList();
EDIT:
You can use AutoMapper which can do the projection in .Select() method for you, like -
var dtos = _Mapper.ProjectTo<BookDTO>(dbCtx.Books).ToList();
For that, you'll need to -
create a DTO model with properties you want from the query result, like -
public class BookDTO
{
public int Id { get; set; }
public string Author { get; set; }
public int UsersReached { get; set; }
}
define a map from Book to BookDTO -
CreateMap<Book, BookDTO>()
.ForMember(d => d.Author, opt => opt.MapFrom(s => s.Author.Name))
.ForMember(d => d.UsersReached, opt => opt.MapFrom(s => s.OwnedBy.Count));
You can remove the [NotMapped] property UsersReached from the Book model.

EF Core: Invalid column name 'companyId1'

I have trouble understanding why a EF generated SELECT clause contains the primary key twice, the second one is postfixed with '1'.
exec sp_executesql N'SELECT [entity.WebAdminCompanyUser].[CompanyId], [entity.WebAdminCompanyUser].[AspNetUserId], [entity.WebAdminCompanyUser].[CompanyId1]
FROM [SafeProtect].[WebAdminCompanyUser] AS [entity.WebAdminCompanyUser]
INNER JOIN (
SELECT [entity1].[AspNetUserId]
FROM [SafeProtect].[WebAdminUser] AS [entity1]
WHERE ([entity1].[RowEnabled] = 1) AND EXISTS (
SELECT 1
FROM [SafeProtect].[WebAdminCompanyUser] AS [companyUser1]
WHERE ([companyUser1].[CompanyId] = #__companyId_0) AND ([entity1].[AspNetUserId] = [companyUser1].[AspNetUserId]))
) AS [t0] ON [entity.WebAdminCompanyUser].[AspNetUserId] = [t0].[AspNetUserId]
ORDER BY [t0].[AspNetUserId]',N'#__companyId_0 int',#__companyId_0=1
It fails with Invalid column name 'CompanyId1'.
Following are the entities and the corresponding configurations (fluent API):
WebAdminCompanyUser:
public partial class WebAdminCompanyUser : ITrackable, IMergeable
{
public WebAdminCompanyUser()
{
AdditionalInit();
}
public int CompanyId { get; set; }
public int AspNetUserId { get; set; }
public virtual Company Company { get; set; }
[NotMapped]
public TrackingState TrackingState { get; set; }
[NotMapped]
public ICollection<string> ModifiedProperties { get; set; }
[NotMapped]
public Guid EntityIdentifier { get; set; }
partial void AdditionalInit();
}
}
Configuration:
builder.Entity<WebAdminCompanyUser>(entity =>
{
entity.ToTable(name: "WebAdminCompanyUser", schema: SqlSchema.SafeProtect);
entity.HasKey("CompanyId", "AspNetUserId");
entity
.HasOne(d => d.Company)
.WithMany()
.HasForeignKey(d => d.CompanyId)
.IsRequired();
});
WebAdminUser:
public partial class WebAdminUser : IdentityUser<int>, IAuditInfo, IRowDisableableWithDateTime, ITrackable, IMergeable
{
public WebAdminUser()
{
WebAdminCompanyUser = new HashSet<WebAdminCompanyUser>();
WebAdminUserRole = new HashSet<WebAdminUserRole>();
WebAdminUserClaim = new HashSet<WebAdminUserClaim>();
WebAdminUserLogin = new HashSet<WebAdminUserLogin>();
AdditionalInit();
}
public string DisplayName { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime? ModifiedOn { get; set; }
public bool RowEnabled { get; set; }
public DateTime? DisabledOn { get; set; }
public virtual ICollection<WebAdminCompanyUser> WebAdminCompanyUser { get; set; }
public virtual ICollection<WebAdminUserRole> WebAdminUserRole { get; set; }
public virtual ICollection<WebAdminUserClaim> WebAdminUserClaim { get; set; }
public virtual ICollection<WebAdminUserLogin> WebAdminUserLogin { get; set; }
[NotMapped]
public TrackingState TrackingState { get; set; }
[NotMapped]
public ICollection<string> ModifiedProperties { get; set; }
[NotMapped]
public Guid EntityIdentifier { get; set; }
partial void AdditionalInit();
}
Configuration:
builder.Entity<WebAdminUser>(entity =>
{
entity.ToTable(name: "WebAdminUser", schema: SqlSchema.SafeProtect);
entity.Property(e => e.Id).HasColumnName("AspNetUserId");
// authorize multiple user name
entity.HasIndex((p) => new { p.UserName }).IsUnique(false);
entity
.HasMany(user => user.WebAdminUserClaim)
.WithOne()
.HasForeignKey(userClaims => userClaims.UserId)
.IsRequired();
entity
.HasMany(user => user.WebAdminUserLogin)
.WithOne()
.HasForeignKey(userLogin => userLogin.UserId)
.IsRequired();
entity
.HasMany(user => user.WebAdminUserRole)
.WithOne()
.HasForeignKey(userRole => userRole.UserId)
.IsRequired();
entity
.HasMany(user => user.WebAdminCompanyUser)
.WithOne()
.HasForeignKey(companyUser => companyUser.AspNetUserId)
.IsRequired();
});
EF query:
IQueryable<WebAdminUser> query =
from WebAdminUser user WebAdminUserRepository.All()
.Include(user => user.WebAdminUserRole)
.ThenInclude(userRole => userRole.AspNetRole)
.Include(user => user.WebAdminCompanyUser)
where user.WebAdminCompanyUser.Any(companyUser => companyUser.CompanyId == companyId)
select user;
return query.ToList();
Any help appreciated.
This usually happens when you have improperly mapped relationship by leaving some navigation property out of fluent configuration.
Remember that each navigation property (collection or reference) represents a relationship. If you fluently configure relationships and use HasOne / HasMany / WithOne / WithMany w/o passing the navigation property, you are telling EF that the relationship has no navigation property for the corresponding end. But if you actually do have navigation property, EF will map it to a separate relationship with default FK column name. If the default property/column name is already used, EF will append index to it until it gets unique.
In your case, the WebAdminUser class and configuration you've shown are irrelevant. The invalid column name CompanyId1 indicates that the problem is with Company class which you haven't shown, and the WithMany() call here
.HasOne(d => d.Company)
.WithMany() // <--
Most likely your Company class has collection navigation property to WebAdminCompanyUser, something like this (virtual and the name of the property doesn't matter):
public virtual ICollection<WebAdminCompanyUser> CompanyUsers { get; set; }
then you need to change the above .WithMany() call with something like
.WithMany(c => c.CompanyUsers)
and the problem will be solved.

EF: one to one relationship without generate new column using fluent API

every time I tried to add-migration and then it shows me the code like this:
public override void Up()
{
AddColumn("dbo.Orders", "OrderItems_Id", c => c.Int(nullable: false));
CreateIndex("dbo.Orders", "OrderItems_Id");
AddForeignKey("dbo.Orders", "OrderItems_Id", "dbo.OrderItems", "Id");
}
I seek the google and haven't found the solution,my entity belows,I want to set the OrderId of orderItems as a foreign key references by Orders.
public class OrderItems
{
public int Id { get; set; }
public int OrderId {get; set;}
public virtual Orders Order { get; set; }
}
public class Orders
{
public int Id { get; set; }
public virtual OrderItems Order { get; set; }
}
My Fluent API looks like belows:
public OrderItemsMapping()
{
this.HasKey(x => x.Id);
this.Property(x => x.GoodId).IsRequired();
this.Property(x => x.OrderId).IsRequired();
this.Property(x => x.Price).IsRequired();
this.Property(x => x.Quantity).IsRequired();
this.Property(x => x.Score).IsRequired();
this.HasOptional(x => x.Order)
.WithOptionalPrincipal(x => x.OrderItems)
.Map(configurationAction: new Action<ForeignKeyAssociationMappingConfiguration>(x => x.MapKey()));
}
another things I have to be mentioned is that x.MapKey("Id") will trigger error :Name had been set and it must be unique. thanks.
You have said that, there is one-to-one relationship between Order and OrderItem entities. If this is the case, then OrderId must be primary key in the OrderItem entity.
Also, your entity's names shouldbe named in singular rather than plural by convention.
As a result, your entities must look like:
public class OrderItem
{
public int OrderId { get; set;}
public virtual Order Order { get; set; }
}
public class Order
{
public int Id { get; set; }
public virtual OrderItem OrderItem { get; set; }
}
And OrderItem mapping:
// Primary Key
this.HasKey(m => m.OrderId);
...
this.HasRequired(m => m.Order)
.WithRequiredDependent(m => m.OrderItem);
...

query through five tables with nested Any operator

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);

Categories