Composite primary key as shadow property in many-to-many relation - c#

I'm using .NET Core end Entity Framework core to build many-to-many relation between two entities. I've built join entity to fullfil the relation and based primary key on shadow properties like this :
Entity User :
public class User
{
[Key]
public int IDUser { get; set; }
[Required]
public string Forename { get; set; }
public List<UserGroup> UsersGroups { get; set; }
}
Entity Group :
public class Group
{
[Key]
public int IDGroup { get; set; }
[Required]
public string GroupName { get; set; }
public List<UserGroup> UsersGroups { get; set; }
}
Entity UserGroup :
public class UserGroup
{
public Group Group { get; set; }
public User User { get; set; }
}
DBcontext class :
public class DBContext : DbContext
{
public DBContext(DbContextOptions<DBContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// shadow property - primary/foreign key
modelBuilder.Entity<UserGroup>()
.Property<int>("IDUser");
// shadow property - primary/foreign key
modelBuilder.Entity<UserGroup>()
.Property<int>("IDGroup");
// composite primary key based on shadow properties
modelBuilder.Entity<UserGroup>()
.HasKey( new string[]{ "IDUser", "IDGroup" });
modelBuilder.Entity<UserGroup>()
.HasOne(ug => ug.Group)
.WithMany(g => g.UsersGroups)
.HasForeignKey(???); //what to do here ?
modelBuilder.Entity<UserGroup>()
.HasOne(ug => ug.User)
.WithMany(u => u.UsersGroups)
.HasForeignKey(???); // what to do here ?
base.OnModelCreating(modelBuilder);
}
public DbSet<Group> Groups { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<UserGroup> UserGroups { get; set; }
}
Now. How can I properly establish Foreign Key on UserGroup entity based on my shadow composite primary key ? I would like this shadow primary key to be foreign key simultaneously. I don't know how to refer to this shadow primary key now in order to make foreign key. I marked where I don't know what to do with question markes.

.HasForeignKey() declares Foreign Key Properties on your entity.
If you don't want Foreign Key Properties on your linking Entity (and you should have them), just omit the .HasForeignKey declaration and EF will use map the FK columns by convention.
eg
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// shadow property - primary/foreign key
modelBuilder.Entity<UserGroup>()
.Property<int>("IDUser");
// shadow property - primary/foreign key
modelBuilder.Entity<UserGroup>()
.Property<int>("IDGroup");
// composite primary key based on shadow properties
modelBuilder.Entity<UserGroup>()
.HasKey(new string[] { "IDUser", "IDGroup" });
modelBuilder.Entity<UserGroup>()
.HasOne(ug => ug.Group)
.WithMany(g => g.UsersGroups);
//.HasForeignKey(???); //what to do here ?
modelBuilder.Entity<UserGroup>()
.HasOne(ug => ug.User)
.WithMany(u => u.UsersGroups);
//.HasForeignKey(???); // what to do here ?
base.OnModelCreating(modelBuilder);
}
Generates
CREATE TABLE [UserGroups] (
[IDUser] int NOT NULL,
[IDGroup] int NOT NULL,
CONSTRAINT [PK_UserGroups] PRIMARY KEY ([IDUser], [IDGroup]),
CONSTRAINT [FK_UserGroups_Groups_IDGroup] FOREIGN KEY ([IDGroup]) REFERENCES [Groups] ([IDGroup]) ON DELETE CASCADE,
CONSTRAINT [FK_UserGroups_Users_IDUser] FOREIGN KEY ([IDUser]) REFERENCES [Users] ([IDUser]) ON DELETE CASCADE
);

You are trying to create a many-to-many join table without defining any scalar properties and you are utilizing shadow property to configure the join table. For EF fluent API, where you have to refer to a shadow property you need to use string based methods. Due to lack of backing CLR property lambda expression doesn't work.
In your case, "what to do here" part is just use the string name of the property.
e.g.
modelBuilder.Entity<UserGroup>()
.HasOne(ug => ug.Group)
.WithMany(g => g.UsersGroups)
.HasForeignKey("IDGroup"); //what to do here ?
Same for the other relationship. This is general mechanism when you want to configure a shadow property as your foreign key property. Furthermore, configuring the shadow property in HasForeignKey does not require you to define the shadow property in advance, since EF can infer the type based on the properties on principal side of relationship. Though for HasKey you still need to declare the shadow properties since EF has no knowledge of types. (As you have done in your example)
EF Core also has convention to figure out FK property. One of the convention is to use property as FK if it has same name as the principal property. In your special case as above, since your principal side primary key property is named as IDGroup which is same as foreign key you are trying to configure, EF will use that by convention automatically. That means you can ignore configuring your relationship (as #David suggested). Also since EF discovers relationships based on navigations you can remove following piece of code fully from your application and it will create the same model.
modelBuilder.Entity<UserGroup>()
.HasOne(ug => ug.Group)
.WithMany(g => g.UsersGroups);
//.HasForeignKey(???); //what to do here ?
modelBuilder.Entity<UserGroup>()
.HasOne(ug => ug.User)
.WithMany(u => u.UsersGroups);
//.HasForeignKey(???); // what to do here ?

Related

The property is part of a key and so cannot be modified or marked as modified (ForeignKey is not a key)

I have two entities with a relationship without a key, when I want to edit the personnel code field in Personnel, this error is displayed to me.
'The property 'Personnel.PersonnelCode' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key, first delete the dependent and invoke 'SaveChanges', and then associate the dependent with the new principal.'
The EnrolNumberPersonnelNum table also does not have a record that has a relationship with the changed personnel record in my table
personnel class
public class Personnel : BaseEntity, IBaseEntityTypeConfiguration<Personnel>
{
public Personnel()
{
EnrolNumberPersonnelNums = new HashSet<EnrolNumberPersonnelNum>();
}
[Key]
public int PersonnelId { get; set; }
public string PersonnelCode { get; set; }
public virtual ICollection<EnrolNumberPersonnelNum> EnrolNumberPersonnelNums { get; set; }
public void Map(EntityTypeBuilder<Personnel> builder)
{
builder.ToTable("Personnel", "HR");
builder.Property(e => e.PersonnelCode).HasMaxLength(50);
}
}
EnrolNumberPersonnelNum class
public class EnrolNumberPersonnelNum : BaseEntity, IBaseEntityTypeConfiguration<EnrolNumberPersonnelNum>
{
public EnrolNumberPersonnelNum()
{
}
[Key]
public int EnrolNumberPersonnelNumId { get; set; }
public string PersonnelCode { get; set; }
public virtual Personnel Personnel { get; set; }
public void Map(EntityTypeBuilder<EnrolNumberPersonnelNum> builder)
{
builder.ToTable("EnrolNumberPersonnelNum", "HR");
builder.Property(e => e.PersonnelCode)
.HasMaxLength(50);
builder.HasOne(c => c.Personnel).WithMany(c => c.EnrolNumberPersonnelNums)
.HasForeignKey(c => c.PersonnelCode).HasPrincipalKey(c => c.PersonnelCode)
.OnDelete(DeleteBehavior.ClientNoAction)
.HasConstraintName($"FK_{nameof(EnrolNumberPersonnelNum)}_{nameof(Personnel)}_{nameof(PersonnelCode)}");
}
}
Error message when I want to edit personnel code in personnel entity
and EnrolNumberPersonnelNum table is empty .
HasPrincipalKey(c => c.PersonnelCode) here
builder.HasOne(c => c.Personnel).WithMany(c => c.EnrolNumberPersonnelNums)
.HasForeignKey(c => c.PersonnelCode).HasPrincipalKey(c => c.PersonnelCode)
.OnDelete(DeleteBehavior.ClientNoAction)
.HasConstraintName($"FK_{nameof(EnrolNumberPersonnelNum)}_{nameof(Personnel)}_{nameof(PersonnelCode)}");
marks the property PersonnelCode of the Personnel entity as alternate key:
An alternate key serves as an alternate unique identifier for each entity instance in addition to the primary key; it can be used as the target of a relationship. When using a relational database this maps to the concept of a unique index/constraint on the alternate key column(s) and one or more foreign key constraints that reference the column(s).
And in EF Core all type of keys (primary and alternate) are read only, i.e. EF Core does not allow updating them (are required and can be provided only for new entities). This is basically what the error message is telling you at the beginning
The property 'Personnel.PersonnelCode' is part of a key and so cannot be modified or marked as modified.
With that being said, there is no solution so far for such type of model, if you need that property editable. The suggestion in the error message
To change the principal of an existing entity with an identifying foreign key, first delete the dependent and invoke 'SaveChanges', and then associate the dependent with the new principal.
doesn't seem viable, since obviously you cannot delete dependents (EnrolNumberPersonnelNums in you case) and then associate them with a new parent (since they are deleted). Eventually you can try loading them in memory, then call RemoveRange, modify the parent key, then SaveChanges, then update PersonnelCode of the cached children instances, call AddRange followed by SaveChanges. Looks unreliable/error prone, but worth trying.
If you are allowed to modify the database, better remove the PersonnelCode from EnrolNumberPersonnelNum and create and use regular FK int PersonnelId bound to the PK of the Personnel. This way you can remove the alternate key (and add just unique constraint if needed) as mentioned in the documentation
Tip
If you just want to enforce uniqueness on a column, define a unique index rather than an alternate key (see Indexes). In EF, alternate keys are read-only and provide additional semantics over unique indexes because they can be used as the target of a foreign key.
That will allow updating the PersonnelCode of Personel as any other property.

.NET EF Core context querying Postgresql tries to query unwanted shadow FK column

After changing entity, adding or removing properties, a "random" column was added to query and it's causing problems.
Npgsql.PostgresException (0x80004005): 42703: column u.UserOldId does not exist
I am querying this entity
[Table("user_visuals", Schema = "present")]
public class UserVisual
{
#region Columns
[Key]
[Column("user_visual_id")]
public long Id { get; set; }
[Column("user_id")]
public long UserId { get; set; }
[Column("visual_id")]
public long VisualId { get; set; }
[Column("render_position")]
public int RenderPosition { get; set; }
#endregion
#region Relations
public Visual Visual { get; set; } = default!;
//public UserOld User { get; set; } = default!;
#endregion
}
After some investigation I've found out that context somehow contains shadow FK and tries to query it though it doesn't exist
Seems like context has this kind of information about it
EntityType: UserVisual
Properties:
Id (long) Required PK AfterSave:Throw ValueGenerated.OnAdd
Annotations:
Npgsql:ValueGenerationStrategy: IdentityByDefaultColumn
Relational:ColumnName: user_visual_id
RenderPosition (int) Required
Annotations:
Npgsql:ValueGenerationStrategy: None
Relational:ColumnName: render_position
UserId (long) Required FK Index
Annotations:
Npgsql:ValueGenerationStrategy: None
Relational:ColumnName: user_id
UserOldId (no field, long?) Shadow FK Index
Annotations:
Npgsql:ValueGenerationStrategy: None
VisualId (long) Required FK Index
Annotations:
Npgsql:ValueGenerationStrategy: None
Relational:ColumnName: visual_id
Navigations:
Visual (Visual) ToPrincipal Visual Inverse: UserVisuals
Keys:
Id PK
Foreign keys:
UserVisual {'UserId'} -> User {'Id'} ToDependent: Visuals Cascade
UserVisual {'UserOldId'} -> UserOld {'Id'} ToDependent: Visuals ClientSetNull
UserVisual {'VisualId'} -> Visual {'Id'} ToDependent: UserVisuals ToPrincipal: Visual Cascade
Indexes:
UserId
UserOldId
VisualId
Annotations:
DiscriminatorProperty:
Relational:FunctionName:
Relational:Schema: present
Relational:SqlQuery:
Relational:TableName: user_visuals
Relational:ViewName:
Relational:ViewSchema:
Context configuration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<UserIdentity>()
.HasKey(x => x.Id);
modelBuilder.Entity<UserRole>()
.HasKey(x => x.Id);
modelBuilder.Entity<IdentityUserRole<long>>().HasKey(p => new { p.UserId, p.RoleId });
modelBuilder.Entity<UserVisual>().HasOne<Visual>(x => x.Visual).WithMany(x => x.UserVisuals).HasForeignKey(x => x.VisualId);
modelBuilder.Entity<UserDevice>().HasOne<Device>(x => x.Device).WithMany(x => x.UserDevices).HasForeignKey(x => x.DeviceId);
modelBuilder.Entity<Device>().HasOne<DeviceType>(x => x.DeviceType).WithMany(x => x.Devices).HasForeignKey(x => x.TypeId);
}
Out of ideas, what should I do?
Phantom shadow FKs usually indicate some relationship misconfiguration via fluent API, or like in this case, relationship created by EF Core default conventions.
Note that Single navigation property in one of the entities is enough to imply relationship in the model. So even if you removed the reference navigation property from the dependent entity (UserOld User in UserVisual), if the principal type (UserOld in this case) is still included in the model, and has collection navigation property referring to the dependent entity type (ICollection<UserVisual or similar), EF Core still considers a relationship with conventional shadow FK.
So, always check the usage (find all references) of a entity class inside other entities navigation properties, and remove the ones which are not intended to create relationship. Also (even though it's not the case here) the correct pairing of Has / With` calls to not leave existing navigation property out of the fluent configuration, thus creating additional conventional relationship.

Creating a one to many with an association table in Entity Framework

I have an association table called SportTeams:
public class SportTeam
{
int SportId;
int TeamId;
Sport Sport;
Team Team
}
public class Sport
{
ICollection<SportTeam> SportTeams;
}
public class Team
{
ICollection<SportTeam> SportTeams;
}
I mark this as an association table via fluent api
modelBuilder.Entity<SportTeam>().HasKey(q => new { q.SportId,q.TeamId }); // set the primary key of the table
modelBuilder.Entity<SportTeam>().HasRequired(s => s.Team).WithMany(t => t.SportTeams).HasForeignKey(s => s.TeamId);
modelBuilder.Entity<SportTeam>().HasRequired(s => s.Sport).WithMany(s => s.SportTeams).HasForeignKey(s => s.SportId);
Now I need to create a one to many with the association table SportTeams. Let's call that table Matches.
public class Matches
{
int Id;
int SportTeamId;
SportTeam SportTeam;
}
public class SportTeam
{
int SportId ;
int TeamId;
Sport Sport;
Team Team;
ICollection<Match> Matches;
}
I go back to the fluent api to make the changes for this one to many.
I say
modelBuilder.Entity<SportTeam>().HasMany(st => st.Matches).WithRequired(matches => matches.SportTeam).HasForeignKey(m => m.SportTeamId).WillCascadeOnDelete(false);
I get an error:
The number of properties in the Dependent and Principal Roles in a relationship constraint must be identical.
I believe this error states that my sportTeam PK is a composite key and in my HasForeignKey section I just specify one FK to connect with.
How should I go about this situation?
You're configuring the relashionship between SportTeam and Matches incorrectly.
So you say that SportTeam entity can have many Matches then it logic that Matches entity to have a foreign key that reference the SportTeam entity.
But if your look at your SportTeam entity configuration, you say it has a composite keys as primary key (SportId, TeamId).
You get this error:
The number of properties in the Dependent and Principal Roles in a
relationship constraint must be identical.
Because if you have composite key as primary key then the foreign key that refers to the primary key of SportTeam should also have both the properties implied in the composite key.
So to solve this, your Matches entity should look like this:
public class Matches
{
public int Id { get; set; }
// these two properties below represent the foreign key that refers to SportTeam entity.
public int SportId { get; set; }
public int TeamId { get; set; }
public SportTeam SportTeam { get; set; };
}
In the OnModelCreating method you should have this line:
modelBuilder.Entity<SportTeam>()
.HasMany(st => st.Matches)
.WithRequired(matches => matches.SportTeam)
.HasForeignKey(m => new { m.SportId, m.TeamId }) // <-- the composite foreign keys.
.WillCascadeOnDelete(false);
Instead of:
modelBuilder.Entity<SportTeam>()
.HasMany(st => st.Matches)
.WithRequired(matches => matches.SportTeam)
.HasForeignKey(m => m.SportTeamId)
.WillCascadeOnDelete(false);
Side Note 1:
I always avoid using composite foreign key. If I end up with things like you have in your sample I just put a primary key property Id in SportTeam entity and make the couple of properties SportId and TeamId with unique constraint in database.
Side Note 2:
I don't know if you code like this in your real project but please use properties and make them public if necessary.

Entity Framework Self Referencing Using Non-Primary Key Column

I have an employee table that self references to determine organization structure. I'm having some trouble trying to set this up using Code-First (POCO) fluently.
An employee record has both a "Position" field and a "ReportsTo" field and neither of the columns are the primary key (employee.id).
An employee with a "ReportsTo" value of "08294" , is an employee of a direct report of an employee with "Position" value of "08294".
Can anyone offer up some info on how to set this up using EF code first, fluently...is it possible?
I tried the code below and am getting error:
Employee_Employees_Source_Employee_Employees_Target: : The types of
all properties in the Dependent Role of a referential constraint must
be the same as the corresponding property types in the Principal Role.
The type of property 'ReportsTo' on entity 'Employee' does not match
the type of property 'Id' on entity 'Employee' in the referential
constraint 'Employee_Employees'.
Employee.cs
public class Employee
{
public int Id { get; set; } //pk
public string Position { get; set; } // i.e. 06895
public string ReportsTo{ get; set; } // i.e. 08294
public virtual Employee Supervisor { get; set; }
public virtual ICollection<Employee> Employees { get; set; }
}
DbContext
modelBuilder.Entity<Employee>()
.HasMany(e => e.Employees)
.WithOptional(e => e.Supervisor)
.HasForeignKey(e => e.ReportsTo);
I think more than anything, I would like to keep the POCO free of EF "stuff" and be able to do something like:
employee.IsSupervisor(); // based on child employee count.
The issue is in the relationship configuration. If you want to configure your one to many relation without using a FK, you could do this:
modelBuilder.Entity<Employee>()
.HasMany(e => e.Employees)
.WithOptional(e => e.Supervisor);
Now if you want to use a FK property, then add this property to your model class:
public class Employee
{
//...
public int SupervisorId { get; set; }
}
And map your relationship this way:
modelBuilder.Entity<Employee>()
.HasMany(e => e.Employees)
.WithOptional(e => e.Supervisor)
.HasForeignKey(e => e.SupervisorId);
To resolve your issue related with ReportTo and Position properties,I think you should handle that logic in your code. If you want to know if an Employee is a supervisor based on the count of Employees property, you could use a NotMapped property:
public class Employee
{
[NotMapped]
public bool IsSupervisor
{
get
{
return Employess.Count>0
}
}
}
You can do the same using Fluent Api:
modelBuilder.Entity<Employee>().Ignore(e => e.IsSupervisor);
PS: Remember initialize Employees in your class'constructor.
The error you get is because it is trying to map a PK of int type to a FK of string type. User int for all of your key fields.
Then, you need to declare your OnModelBuilding like this:
modelBuilder.Entity<Employee>()
.HasOptional(e => e.Supervisor)
.WithMany()
.HasForeignKey(s => s.ReportsTo);
To get something like IsSupervisor() you can take advantage of partial classes. Create another class file which is a public partial class Employee (and modify your original one to be partial), then in your new file you will add a property that does whatever you want, and decorate it with [NotMapped] attribute. Yours will probably look something like public bool IsSupervisor {get { return (Employees == null) ? false : true; } set {} } The new partial class is where you can do whatever you want for the POCO without changing the EF class (make sure you use [NotMapped] though).

Entity Framework skipping Identity column Concurrency Check

I am working with Entity Framework 5 code-first and I have a situation where I have an entity which has an Identity column that is not part of the primary key. When I add a new record and invoke SaveChanges on the context, I get a ConcurrencyException. If I change the primary key on the entity map to SCHED_ID or remove SCHED_ID from the entity map altogether, SaveChanges completes without issue. I suspect that since SCHED_ID is defined as an int, it defaults to a value of zero and Entity Framework thinks it has changed when the database actually assigns it a value (thus causing a ConcurrencyException). Am I correct in my assumption? How can I work around this without changing the key on the entity map to SCHED_ID?
The entity is defined as follows:
public partial class Sched
{
public int SCHED_ID { get; set; }
public System.DateTime DATE_QUEUED { get; set; }
public string STATUS_CODE { get; set; }
}
public class SchedMap : EntityTypeConfiguration<Sched>
{
public SchedMap()
{
// Primary Key
this.HasKey(t => new { t.STATUS_CODE, t.DATE_QUEUED });
//Properties
this.Property(t => t.SCHED_ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
.IsConcurrencyToken(false);
// Table & Column Mappings
this.ToTable("Sched");
this.Property(t => t.SCHED_ID).HasColumnName("SCHED_ID");
this.Property(t => t.STATUS_CODE).HasColumnName("STATUS_CODE");
this.Property(t => t.DATE_QUEUED).HasColumnName("DATE_QUEUED");
}
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new SchedMap());
}

Categories

Resources