I have a parent/child hierarchy of which I want to insert a new parent into a DbContext and have it automatically persist the child objects. The relationship is a One-to-many, where each parent can at 0 or more columns.
But whenever I call DbContext.Save(parent) I receive a 'Conflicting changes detected. This may happen when trying to insert multiple entities with the same key.'. When I strip the parent of the child columns it saves fines, so I'm assuming this is related to the child objects not having their primary key set. How do I tell the EntityFramework to save my hierarchy properly?
My classes:
public class ExcelTemplate
{
public int Id { get; set; }
public string Name { get; set; }
public int FirstDataRow { get; set; }
public virtual ICollection<TemplateColumn> Columns { get; set; }
public ExcelTemplate()
{
Columns = new List<TemplateColumn>();
}
}
public class TemplateColumn
{
public int Id { get; set; }
public int Index { get; set; }
public int MetricTypeId { get; set; }
public virtual MetricType MetricType { get; set; }
public int TemplateId { get; set; }
public virtual ExcelTemplate Template { get; set; }
}
And the configurations:
public class ExcelTemplateConfiguration : EntityTypeConfiguration<ExcelTemplate>
{
public ExcelTemplateConfiguration()
{
HasKey(t => t.Id)
.Property(t => t.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Name)
.IsRequired();
HasMany(t => t.Columns)
.WithRequired(c => c.Template)
.HasForeignKey(c => c.TemplateId);
}
}
public class TemplateColumnConfiguration : EntityTypeConfiguration<TemplateColumn>
{
public TemplateColumnConfiguration()
{
HasKey(c => c.Id)
.Property(c => c.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(tpl => tpl.MetricType)
.WithRequiredDependent();
}
}
Do the ExcelTemplate.Id and TemplateColumn.Id have Identity specification? Are you setting the Id property properly?
Are you setting the ExcelTemplate property of the TemplateColumn objects to the ExcelTemplate parent?
Something like:
ExcelTemplate t = new ExcelTemplate();
//init template code...skip
TemplateColumn c = new TemplateColumn();
//init template column code...skip
c.ExcelTemplate = t;//this is the line I am asking if you are doing.
I have discovered the culprit through trial-and-error, because the problem persisted when I only tried to store a TemplateColumn by itself.
The problem was to remove this line of code from the TemplateColumn configuration and let the entity framework map by convention:
HasRequired(tpl => tpl.MetricType)
.WithRequiredDependent();
Any idea why this didn't work?
Related
I'm using EF Core 5.0 and I'm attempting to create a many-to-many relationship:
public class User
{
public int Id { get; set; }
public ICollection<UserItem> UserItems { get; set; }
}
public class Item
{
public int Id { get; set; }
public ICollection<UserItem> UserItems { get; set; }
}
public class UserItem
{
public int Id { get; set; }
public int UserId { get; set; }
public User User { get; set; }
public int ItemId { get; set; }
public Item Item { get; set; }
public int Quantity { get; set; }
}
As you can see, the UserItem class has an additional property called Quantity, I was wondering if this is the correct approach, and how would I map all this in the DBContext class?
Yes this is correct!
But I would recommend removing the Id property from UserItem and configure a composite key using -
modelBuilder.Entity<UserItem>()
.HasKey(userItem => new { userItem.UserId, userItem.ItemId });
in the DbContext so it is ensured that only one single UserItem for the same User/Item combination can exist (as I assume you want to use the Quantity property for representing "multiple" relationships between the same two items).
EF Core correctly sets up the relationships and foreign keys for you but you need to tell him which part of the relationship is the principal part.
If you want UserItem to be cascaded when the Item is deleted but restrict the deletion of the User when an UserItem is still referencing said User then add -
modelBuilder.Entity<UserItem>()
.HasOne(userItem => userItem.User)
.WithMany(user => user.UserItems)
.OnDelete(DeleteBehavior.Restrict);
If you want UserItem to be cascaded when the User is deleted but restrict the deletion of the Item when an UserItem is still referencing said Item then add -
modelBuilder.Entity<UserItem>()
.HasOne(userItem => userItem.Item)
.WithMany(item => item .UserItems)
.OnDelete(DeleteBehavior.Restrict);
And if you want to restrict the deletion of User and Item if they are referenced by a UserItem add both of them. Notice that you will need at least one of them because otherwise you will get an exception when adding the migration because you have multiple cascade paths for UserItem.
Everything is fine, maybe you will have add to your dbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserItem>(entity =>
{
entity.HasOne(d => d.User)
.WithMany(p => p.UserItems)
.HasForeignKey(d => d.UserId)
.OnDelete(DeleteBehavior.ClientSetNull)
entity.HasOne(d => d.Item)
.WithMany(p => p.UserItems)
.HasForeignKey(d => d.ItemId)
});
OnModelCreatingPartial(modelBuilder);
}
but if you use Net5 you are allowed to add a list of items:
public class User
{
public int Id { get; set; }
public ICollection<UserItem> UserItems { get; set; }
public ICollection<Items> Items { get; set; }
}
public class Item
{
public int Id { get; set; }
public ICollection<UserItem> UserItems { get; set; }
public ICollection<Users> Users { get; set; }
}
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'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 have a Many To Many relationship with some additional fields. But as there are Photos added to the many to many relationship which might apply to other relations I wanted to seperate it so I can change it by just altering the One to many relation. This is the model
public class Segment
{
public int SegmentId { get; set; }
public int ConnectionPointIdEnd { get; set; }
public string ConnectionName { get; set; }
public string ConnectionInformation { get; set; }
public string Image { get; set; }
public string Direction { get; set; }
public ICollection<ConnectionPointRoute> ConnectionPointRoutes { get; set; }
}
public class ConnectionPointRoute
{
public int ConnectionPointId { get; set; }
public int RouteId { get; set; }
public int SegmentId { get; set; }
public int Position { get; set; }
public ConnectionPoint ConnectionPoint { get; set; }
public Route Route { get; set; }
public Segment Segment { get; set; }
}
And the modelbuilder looks like this :
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ConnectionPointRoute>()
.HasKey(c => new { c.ConnectionPointId, c.RouteId, c.SegmentId });
modelBuilder.Entity<ConnectionPoint>()
.HasMany(c => c.ConnectionPointRoutes)
.WithRequired(x => x.ConnectionPoint)
.HasForeignKey(c => c.ConnectionPointId);
modelBuilder.Entity<Route>()
.HasMany(c => c.ConnectionPointRoutes)
.WithRequired(x => x.Route)
.HasForeignKey(c => c.RouteId);
modelBuilder.Entity<Segment>()
.HasMany(c => c.ConnectionPointRoutes)
.WithRequired(x => x.Segment)
.HasForeignKey(c => c.SegmentId);
}
And this all works well for getting the items, but for some reason it doesn't allow me to post a new Route for instance, it gets me the error:
"Multiplicity constraint violated. The role
'Segment_ConnectionPointRoutes_Source' of the relationship
'InBuildingNavigator.Data.Models.Segment_ConnectionPointRoutes' has
multiplicity 1 or 0..1."
Any thoughts?
Fixed this! I had an error in my Post code, I added the full child objects which doesn't make a whole lot of sense in my case.
Ask me if you want a more detailed fix!
Just two more things to this:
I would recommend you to use an extra object for the many-to-many relationship (if you don't already do this). This will give you more control over the table name and over selections you may want to do.
use the virtual keyword for your properties, which you do not need directly (for your collections) - this will allow ef to implement lazy loading on them.
I have the following entities:
[KnownType(typeof(Script))]
public class Application : IEntity
{
public long ID { get; set; }
public string Name { get; set; }
public long? StartScriptID { get; set; }
// Other properties...
// Navigation properties:
public virtual Script StartScript { get; set; } // new
public virtual List<Script> Scripts { get; set; }
}
[KnownType(typeof(Application))]
public class Script : IEntity
{
public long ID { get; set; }
public string Name { get; set; }
// Other properties...
// Navigation properties:
public virtual Application Application { get; set; }
}
I have no fluent configurations on the DbContext
So the model exists of Scripts which must be bound to an application. Applications can have a startScript defined. All this I already have working.
Now I am in need of a navigation property: Application.StartScript.
The question is how can I add the navigation property on Application without having to add a the equivalent navigation property on Script.
Thanks in advance!
EDIT: When I run Add-Migration the following migration code is generated:
public override void Up()
{
DropForeignKey("dbo.Scripts", "ApplicationID", "dbo.Applications");
AddColumn("dbo.Scripts", "Application_ID", c => c.Long());
CreateIndex("dbo.Applications", "StartScriptID");
CreateIndex("dbo.Scripts", "Application_ID");
AddForeignKey("dbo.Applications", "StartScriptID", "dbo.Scripts", "ID");
AddForeignKey("dbo.Scripts", "Application_ID", "dbo.Applications", "ID");
}
This creates a new column on Scripts which I don't need, as I already have the StartScriptID column.
EDIT: Update after #haim770 answer
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Application>().HasOptional(x => x.StartScript).WithOptionalDependent(x => x.Application);
}
public override void Up()
{
DropForeignKey("dbo.Scripts", "ApplicationID", "dbo.Applications");
DropIndex("dbo.Scripts", new[] { "ApplicationID" });
RenameColumn(table: "dbo.Applications", name: "ApplicationID", newName: "StartScript_ID");
AddColumn("dbo.Scripts", "Application_ID", c => c.Long());
CreateIndex("dbo.Applications", "StartScript_ID");
CreateIndex("dbo.Scripts", "Application_ID");
AddForeignKey("dbo.Scripts", "Application_ID", "dbo.Applications", "ID");
AddForeignKey("dbo.Applications", "StartScript_ID", "dbo.Scripts", "ID");
}
It doesn't understand that it should use the already existing StartScriptID. Is there a way to point it in the right direction?
EDIT: Wanted database structure:
Applications:
ID, PK, bigint, not null
Name, nvarchar(max), not null
StartScriptID, bigint, null
Scripts:
ID, PK, bigint, not null
Name, nvarchar(max), not null
ApplicationID, FK, bigint, not null
EDIT:
public class Application
{
[InverseProperty("StartScript")]
public long? StartScriptID { get; set; }
[ForeignKey("StartScriptID")]
public virtual Script StartScript { get; set; }
}
I was thinking there was no changes needed in the database, so I have tried adding migration with -IgnoreChanges. But then I got an EntityCommandExecutionException when querying for entities: "Invalid column name 'Application_ID'". So the Entity Framework needs some configuration for telling to use the StartScriptID property.
You can modify the mapping using the ForeignKeyAttribute and the InversePropertyAttribute.
If that doesn't work the way you want, you can always edit the migration yourself before applying it.
If you want to customize the column name, try the ColumnAttribute:
public class Application
{
[Column("DatabaseColumnName")]
public long? StartScriptID { get; set; }
[ForeignKey("StartScriptID")]
public virtual Script StartScript { get; set; }
}
A longer explanation can be found here: http://msdn.microsoft.com/de-de/data/gg193958.aspx
Try to use EntityTypeConfiguration It makes more easy to handle this kind of problem. Then:
public class Application
{
public long ID { get; set; }
public string Name { get; set; }
public long? StartScriptID { get; set; }
// Other properties...
// Navigation properties:
public virtual Script StartScript { get; set; } // new
public virtual ICollection<Script> Scripts { get; set; }
}
public class Script
{
public long ID { get; set; }
public string Name { get; set; }
public int ApplicationId { get; set; }
// Other properties...
// Navigation properties:
public virtual Application Application { get; set; }
}
public class ApplicationMap : EntityTypeConfiguration<Application>
{
public ApplicationMap()
{
this.HasKey(t => t.ID);
this.ToTable("TB_APLICATION", "aplication");
this.Property(t => t.ID).HasColumnName("id");
this.Property(t => t.Name).HasColumnName("name");
this.Property(t => t.StartScriptID).HasColumnName("startscript_id");
this.HasOptional(t => t.StartScript)
.WithMany()
.HasForeignKey(t => t.StartScriptID);
}
}
public class ScriptMap : EntityTypeConfiguration<Script>
{
public ScriptMap()
{
this.HasKey(t => t.ID);
this.ToTable("TB_APLICATION", "aplication");
this.Property(t => t.ID).HasColumnName("id");
this.Property(t => t.Name).HasColumnName("name");
this.HasRequired(t => t.Application)
.WithMany(w => w.Scripts)
.HasForeignKey(t => t.ApplicationId);
}
}
Don't forget to tell EF about the Mappings: (Call it inside OnModelCreating)
public static class MappingConfig
{
public static void ConfigureMap(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new ApplicationMap());
modelBuilder.Configurations.Add(new ScriptMap());
}
}