I have the following POCO Classes:
public class Client
{
public string ClientId { get; set; }
public string CompanyId { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
[ForeignKey("AccountId")]
public virtual ICollection<Note> Notes { get; set; }
}
public class Note
{
public decimal NoteId { get; set; }
public string AccountId { get; set; }
public string NoteValue { get; set; }
[ForeignKey("ClientId")]
public virtual Client Client { get; set; }
}
and the following mapping classes:
public ClientMap(string schema)
{
ToTable("CLIENTS", schema);
// Primary Key
HasKey(t => t.ClientId);
// Properties
Property(t => t.ClientId)
.HasColumnName("CLIENT_ID")
.IsRequired()
.HasMaxLength(16);
Property(t => t.CompanyId)
.HasColumnName("COMPANY_ID")
.IsRequired()
.HasMaxLength(16);
Property(t => t.LastName)
.HasColumnName("LAST_NAME")
.IsRequired()
.HasMaxLength(50);
Property(t => t.FirstName)
.HasColumnName("FIRST_NAME")
.IsRequired()
.HasMaxLength(50);
}
public NoteMap(string schema)
{
ToTable("NOTES", schema);
// Primary Key
HasKey(t => t.NoteId);
// Properties
Property(t => t.NoteId)
.HasColumnName("NOTE_ID")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.AccountId)
.HasColumnName("ACCOUNT_ID")
.IsRequired()
.HasMaxLength(16);
Property(t => t.NoteValue)
.HasColumnName("NOTE")
.HasMaxLength(4000);
}
In this model (Using Fluent API), there is a one to many relationship between clients and notes. ClientID is the PK in clients and NoteId is the PK in Notes. There are no foreign keys in the DB. ClientId maps to the AccountId in Notes.
I can not get this to work. When I run it, I can get most of the client data back, but when trying to navigate to a note, I get a Funcation Evaluation Timed out error when trying to look at Notes. I can not get the relationship between clients and notes to work. Where have I gone wrong? (I would like to do this using Fluent API)
Thanks
You are trying to create a typical one to many relationship. First, remove the data annotations that you are using in your model, you don't need them if you are going to use Fluent Api. Second, add the ClientId FK property in your Note entity
public class Note
{
public decimal NoteId { get; set; }
public string AccountId { get; set; }
public string NoteValue { get; set; }
//FK property
public int ClientId{get;set;}
public virtual Client Client { get; set; }
}
Then, in the constructor of your NoteMap class, add this Fluent Api configuration:
HasRequired(n=>n.Client).WithMany(c=>c.Notes).HasForeignKey(n=>n.ClientId);
Related
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.
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.
I have three class Student,Teacher and Document. Student and Teacher may have many Documents
public class BaseEntity
{
public int ItemId { get; set; }
public bool IsDeleted { get; set; }
}
//ParentType Student = 1
public class Student : BaseEntity
{
public string Name { get; set; }
public string Surname { get; set; }
public ICollection<Document> Documents { get; set; }
}
//ParentType Teacher = 2
public class Teacher : BaseEntity
{
public string Name { get; set; }
public string Surname { get; set; }
public ICollection<Document> Documents { get; set; }
}
public class Document
{
public int ParentId { get; set; } //Foreign Key
public int ParentTypeId { get; set; }
}
I use Entity Framework(Fluent API). For example, I create map for Student and I don't know how to configure Document in student with two Condition (where parentId = itemId and ParentType = 1)?
public class StudentMap : EntityTypeConfiguration<Student>
{
public StudentMap()
{
ToTable("Student", "dbo");
// Primary Key
HasKey(t => new {t.ItemId});
// Properties
Property(t => t.ItemId).HasColumnName("ItemId");
Property(t => t.IsDeleted)
.IsRequired()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
.HasColumnName("IsDeleted");
Property(t => t.Name)
.IsRequired()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
.HasColumnName("Name");
Property(t => t.Surname)
.IsRequired()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
.HasColumnName("Surname");
}
You can't have a conditional foreign key in EF just like I am pretty sure you can't have one in most DBMSs (like sql server). You have 2 options:
Omit that relationship from the schema and model and create your join(s) when you need them. You also have to keep this in mind when creating the entities that you have to manually set the key values.
You could create a nullable column per relationship in the document entity which is supported. You could add a check constraint in the database to ensure exactly one relationship key has a value (per record).
I'm trying to create a commenting system backed by Entity Framework Core where multiple entities of different type can have comments attached to them.
These are my entities. (In the real application there are about 7 in total with varying relationships but this is how it generally looks)
public class Comment : IEntityBase
{
public int Id { get; set; }
public int? FreezerId{ get; set; }
public Freezer Freezer { get; set; }
public int? BoxId{ get; set; }
public Box Box{ get; set; }
public string Author { get; set; }
public DateTime CreatedAt { get; set; }
public string Content { get; set; }
}
public class Freezer: IEntityBase
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public ICollection<Box> Boxes{ get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Box: IEntityBase
{
public int Id { get; set; }
public Freezer Freezer{get; set;}
public int FreezerId{get; set;}
public string Data{ get; set; }
public ICollection<Comment> Comments { get; set; }
}
I want the Comment entity to be attached to one Freezer or one Box, but not both at the same time.
I defined the relationship in the fluent API as the following:
builder.Entity<Box>(boxBuilder=>
{
boxBuilder.HasOne(box=> box.Freezer)
.WithMany(freezer => freezer.boxes)
.HasForeignKey(box => box.FreezerId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
boxBuilder.HasMany(box => box.Comments)
.WithOne(comment => comment.Box)
.HasForeignKey(comment => comment.BoxId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<Freezer>(freezerBuilder =>
{
freezerBuilder.HasMany(freezer=> freezer.Comments)
.WithOne(comment => comment.Freezer)
.HasForeignKey(comment => comment.FreezerId)
.OnDelete(DeleteBehavior.Cascade);
});
When I try to update the database to this model I get the following error:
System.Data.SqlClient.SqlException: Introducing FOREIGN KEY constraint 'FK_Comment_Boxes_BoxId' on table 'Comment' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
I think the error comes from the Box and the Freezer property in the Comment class not being optional which would make this a 1 to many relationship instead of a 0..1 to many relationship which is what I want.
With Entity Framework 6 I would just use the .HasOptional() method, but this doesn't exist in Entity Framework Core
I think one way to solve this would be to just subclass the Comment class and create a unique comment class for each entity that can be commented on and move the foreign key and reference property to that subclass instead.
But it feels like I shouldn't have to do it this way.
You have to disable the cascade delete(DeleteBehavior.Restrict) then it will works for you:
modelBuilder.Entity<Box>(boxBuilder =>
{
boxBuilder.HasOne(box => box.Freezer)
.WithMany(freezer => freezer.Boxes)
.HasForeignKey(box => box.FreezerId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
boxBuilder.HasMany(box => box.Comments)
.WithOne(comment => comment.Box)
.HasForeignKey(comment => comment.BoxId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<Freezer>(freezerBuilder =>
{
freezerBuilder.HasMany(freezer => freezer.Comments)
.WithOne(comment => comment.Freezer)
.HasForeignKey(comment => comment.FreezerId)
.OnDelete(DeleteBehavior.Restrict);
});
base.OnModelCreating(modelBuilder);
}
Usage:
using (var myConext = new MyDbContext())
{
myConext.Database.EnsureCreated();
myConext.Boxes.Add(new Box() {Freezer = new Freezer()});
myConext.SaveChanges();
}
I am trying to establish a foreign key relationship between following domain classes using Fluent API (Entity Framework v5):
public partial class User
{
public long UserID { get; set; }
public string UserName { get; set; }
}
public partial class AccountGroup : BaseEntity
{
public long AccountGroupID { get; set; }
public string Name { get; set; }
public long ModifiedBy { get; set; }
public virtual User User { get; set; }
}
Fluent API
builder.Entity<User>().HasKey(p => p.UserID); //Set User Id as primary key
builder.Entity<AccountGroup>().HasKey(x => x.AccountGroupID); //SetAccountGroupId as PK
I am not sure how to set a relationship between User.UserId and AccountGroup.ModifiedBy column using fluent API. I can do this by Data Annotation but I am looking for a solution using fluent api
Remove the ModifiedBy property from your entity:
public partial class AccountGroup
{
public long AccountGroupID { get; set; }
public string Name { get; set; }
public virtual User User { get; set; }
}
And then map the foreign key like so:
builder.Entity<AccountGroup>()
.HasRequired(x => x.User)
.WithMany()
.Map(m => m.MapKey("ModifiedBy"));
Use HasOptional instead if the foreign key should be nullable:
builder.Entity<AccountGroup>()
.HasOptional(x => x.User)
.WithMany()
.Map(m => m.MapKey("ModifiedBy"));
You also don't need to specify the primary keys like you are. Entity Framework will discover those by convention.