I using Entity Framework Core, and I have a table:
public class BlogComment
{
public int Id { get; set; }
public BlogPost Post { get; set; }
[StringLength(100)]
public string AuthorName { get; set; }
[StringLength(254)]
public string AuthorEmail { get; set; }
public bool SendMailOnReply { get; set; }
[StringLength(2000)]
public string Content { get; set; }
public DateTime CreatedTime { get; set; }
public int? ReplyToId { get; set; }
public BlogComment ReplyTo { get; set; }
}
From this, EFC generates the following table:
CREATE TABLE [dbo].[BlogComment] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[AuthorEmail] NVARCHAR (254) NULL,
[AuthorName] NVARCHAR (100) NULL,
[Content] NVARCHAR (2000) NULL,
[CreatedTime] DATETIME2 (7) NOT NULL,
[PostId] INT NULL,
[ReplyToId] INT NULL,
[SendMailOnReply] BIT NOT NULL,
CONSTRAINT [PK_BlogComment] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_BlogComment_BlogPost_PostId] FOREIGN KEY ([PostId]) REFERENCES [dbo].[BlogPost] ([Id]),
CONSTRAINT [FK_BlogComment_BlogComment_ReplyToId] FOREIGN KEY ([ReplyToId]) REFERENCES [dbo].[BlogComment] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_BlogComment_PostId]
ON [dbo].[BlogComment]([PostId] ASC);
GO
CREATE UNIQUE NONCLUSTERED INDEX [IX_BlogComment_ReplyToId]
ON [dbo].[BlogComment]([ReplyToId] ASC) WHERE ([ReplyToId] IS NOT NULL);
Some comments are send as a reply to another, but not all. When the original comment is deleted, the reply becomes a normal comment. So, following this tutorial, the configuration looks is this:
modelBuilder.Entity<BlogComment>()
.HasOne(p => p.ReplyTo)
.WithOne()
.HasForeignKey<BlogComment>(c => c.ReplyToId)
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
The delete method is pretty simple:
var comment = await context.BlogComment.Include(c => c.ReplyTo).SingleAsync(m => m.Id == id);
context.BlogComment.Remove(comment);
await context.SaveChangesAsync();
But I can't run it, I get an error:
System.Data.SqlClient.SqlException: The DELETE statement conflicted with the SAME TABLE REFERENCE constraint "FK_BlogComment_BlogComment_ReplyToId".
How can I fix this?
To wrap up the conversation in the comments:
First, the self reference is a 1:n association:
modelBuilder.Entity<BlogComment>()
.HasOne(p => p.ReplyTo)
.WithMany(c => c.Replies)
.HasForeignKey(c => c.ReplyToId)
.IsRequired(false)
.OnDelete(<we'll get to that>);
So, just for convenience, BlogComment now also has a property
public ICollection<BlogComment> Replies { get; set; }
However, I can't create the table using
.OnDelete(DeleteBehavior.SetNull);
It gives me
Introducing FOREIGN KEY constraint 'FK_BlogComments_BlogComments_ReplyToId' on table 'BlogComments' may cause cycles or multiple cascade paths.
This is a Sql Server restriction we just have to accept, no way to evade it. The only way to get the desired cascade behavior is
.OnDelete(DeleteBehavior.ClientSetNull);
Which is:
For entities being tracked by the DbContext, the values of foreign key properties in dependent entities are set to null. This helps keep the graph of entities in a consistent state while they are being tracked, such that a fully consistent graph can then be written to the database. (...) This is the default for optional relationships.
I.e.: the client executes SQL to nullify the foreign key values. The child records should be tracked though. To remove a BlogComment parent the delete action should look like:
using (var db = new MyContext(connectionString))
{
var c1 = db.BlogComments
.Include(c => c.Replies) // Children should be included
.SingleOrDefault(c => c.Id == 1);
db.BlogComments.Remove(c1);
db.SaveChanges();
}
As you see, you don't have to set ReplyToId = null, that's something EF takes care of.
For me, I had to Include() the entities I needed to be "dealt with" when I deleted an entity. EF cant manage things it is not currently tracking.
var breedToDelete = context.Breed
.Include(x => x.Cats)
.Single(x => x.Id == testBreedId);
context.Breed.Remove(breedToDelete);
context.SaveChanges();
I could get it working by manually setting ReplyTo to null. I'm still looking for a better solution, or an explanation why is it needed. Isn't it what OnDelete(DeleteBehavior.SetNull) supposed to do?
var comment = await context.BlogComment.Include(c => c.ReplyTo).SingleAsync(m => m.Id == id);
var reply = await context.BlogComment.SingleOrDefaultAsync(m => m.ReplyToId == id);
if (reply != null)
{
reply.ReplyTo = null;
reply.ReplyToId = null;
context.Entry(reply).State = EntityState.Modified;
}
context.BlogComment.Remove(comment);
Related
I have the following classes generated from an edmx model:
public partial class A
{
public int Id { get; set; }
public virtual B B { get; set; }
}
public partial class B
{
public int Id { get; set; }
public virtual A A { get; set; }
}
The existing db doesn't use the EF default which expects A.Id to be the primary key of table B:
CREATE TABLE [dbo].[B] (
[Id] INT IDENTITY (1, 1) NOT NULL,
PRIMARY KEY CLUSTERED ([Id] ASC)
);
CREATE TABLE [dbo].[A] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[BId] INT NULL,
CONSTRAINT [fk] FOREIGN KEY ([BId]) REFERENCES [dbo].[B] ([Id])
);
With an edmx model, I can explicitly configure the multiplicity of each end, but I haven't found how to get the equivalent model using the fluent-api. When I do something like the following and generate a new db, the foreign key gets placed in table A instead of table B.
modelBuilder.Entity<A>().HasOptional(a => a.B).WithRequired(b => b.A);
I'm guessing I need to use a convention, but so far I've been unable to get the desired output.
UPDATE:
The closest solution I've found so far is to use the following which generates the correct SQL in the db:
modelBuilder.Entity<A>()
.HasOptional(a => a.B)
.WithOptionalDependent(b => b.A)
.Map(c => c.MapKey("BId"));
However, it's conceptually modeled as a 0..1:0..1 relationship and I haven't found how to set a CASCADE delete rule that deletes B when A is deleted.
I wasn't able to find a direct solution, but using the following code seems to meet my requirements of preserving the existing schema and creating a conceptual model that has the same multiplicities & delete behaviors as my original edmx model.
I'd still be interested in any solutions that don't require updating the conceptual model during the post-processing IStoreModelConvention.
{
var overridesConvention = new OverrideAssociationsConvention();
modelBuilder.Conventions.Add(overridesConvention);
modelBuilder.Conventions.Add(new OverrideMultiplictyConvention(overridesConvention));
}
private class OverrideAssociationsConvention : IConceptualModelConvention<AssociationType>
{
...
public List<AssociationEndMember> MultiplicityOverrides { get; } = new List<AssociationEndMember>();
public void Apply(AssociationType item, DbModel model)
{
if (multiplicityOverrides.Contains(item.Name))
{
// Defer actually updating the multiplicity until the store model is generated
// so that foreign keys are placed in the desired tables.
MultiplicityOverrides.Add(item.AssociationEndMembers.Last());
}
if (cascadeOverrides.Contains(item.Name))
{
item.AssociationEndMembers.Last().DeleteBehavior = OperationAction.Cascade;
}
}
}
private class OverrideMultiplictyConvention : IStoreModelConvention<EdmModel>
{
private readonly OverrideAssociationsConvention overrides;
public OverrideMultiplictyConvention(OverrideAssociationsConvention overrides)
{
this.overrides = overrides;
}
public void Apply(EdmModel item, DbModel model)
{
overrides.MultiplicityOverrides.ForEach(o => o.RelationshipMultiplicity = RelationshipMultiplicity.One);
}
}
I have a Gig Model as follows:
public class Gig
{
public int Id { get; set; }
[Required]
public ApplicationUser Artist { get; set; }
[Required]
public string ArtistId { get; set; }
public DateTime DateTime { get; set; }
[Required]
[StringLength(255)]
public string Venue { get; set; }
[Required]
public Genre Genre { get; set; }
[Required]
public byte GenreId { get; set; }
}
In EF6, I was able to Eager Load Artist and Genre using the following code
var gigs = _context.Attendances
.Where(a => a.AttendeeId == userId)
.Select(a => a.Gig)
.Include(a => a.Artist)
.Include(a => a.Genre)
.ToList();
But with EF Core, the Artist info or the Genre info is not getting loaded. SQL Profiler shows that there is no INNER JOIN being called on the projection tables.
SELECT [a.Gig].[Id], [a.Gig].[ArtistId], [a.Gig].[DateTime], [a.Gig].[GenreId], [a.Gig].[Venue]
FROM [Attendances] AS [a]
INNER JOIN [Gigs] AS [a.Gig] ON [a].[GigId] = [a.Gig].[Id]
WHERE [a].[AttendeeId] = #__userId_0',N'#__userId_0 nvarchar(450)',#__userId_0=N'469d8515-9a04-46af-9276-09c6fead9e10'
Can someone help me re-write the query for EF Core please to include the projection tables?
UPDATE:
added link to db schema scripts here. posting just the gigs table here:
CREATE TABLE [dbo].[Gigs](
[Id] [int] IDENTITY(1,1) NOT NULL,
[ArtistId] [nvarchar](450) NOT NULL,
[DateTime] [datetime2](7) NOT NULL,
[GenreId] [tinyint] NOT NULL,
[Venue] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_Gigs] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Attendances] WITH CHECK ADD CONSTRAINT [FK_Attendances_AspNetUsers_AttendeeId] FOREIGN KEY([AttendeeId])
REFERENCES [dbo].[AspNetUsers] ([Id])
GO
ALTER TABLE [dbo].[Attendances] CHECK CONSTRAINT [FK_Attendances_AspNetUsers_AttendeeId]
GO
ALTER TABLE [dbo].[Attendances] WITH CHECK ADD CONSTRAINT [FK_Attendances_Gigs_GigId] FOREIGN KEY([GigId])
REFERENCES [dbo].[Gigs] ([Id])
GO
ALTER TABLE [dbo].[Attendances] CHECK CONSTRAINT [FK_Attendances_Gigs_GigId]
GO
ALTER TABLE [dbo].[Gigs] WITH CHECK ADD CONSTRAINT [FK_Gigs_AspNetUsers_ArtistId] FOREIGN KEY([ArtistId])
REFERENCES [dbo].[AspNetUsers] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[Gigs] CHECK CONSTRAINT [FK_Gigs_AspNetUsers_ArtistId]
GO
ALTER TABLE [dbo].[Gigs] WITH CHECK ADD CONSTRAINT [FK_Gigs_Genres_GenreId] FOREIGN KEY([GenreId])
REFERENCES [dbo].[Genres] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[Gigs] CHECK CONSTRAINT [FK_Gigs_Genres_GenreId]
GO
If you turn on EF Core Logging, you'll see inside the log something like this:
The Include operation for navigation: 'a.Gig.Artist' was ignored because the target navigation is not reachable in the final query results.
and similar for a.Gig.Genre.
Looks like EF Core at this time cannot handle includes for such queries (that don't start from the resulting entity). The only workaround I can propose is to rewrite the query like this:
var gigs = _context.Gigs
.Where(g => g.Attendances.Any(a => a.AttendeeId == userId))
.Include(g => g.Artist)
.Include(g => g.Genre)
.ToList();
or this (translates to better SQL, although the SQL execution plan could be the same):
var gigs = (from g in _context.Gigs
from a in g.Attendances
where a.AttendeeId == userId
select g)
.Include(g => g.Artist)
.Include(g => g.Genre)
.ToList();
Using Entity Framework Code First, I'm seeing very strange behavior when inserting a row with a composite key. The composite key consists of a guid ID field and a guid foreign key field, creating an "identifying relationship". The strange behavior is that regardless of what I set the ID and foreign key field to, the generated SQL sets them both to the foreign key value.
My classes look like this:
public class Parent {
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child {
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; }
public Guid ParentId { get; set; }
}
In my DbContext file I have:
modelBuilder.Entity<Child>().HasKey(c => new { c.Id, c.ParentId });
Doing something like:
var parent = new Parent() { Id = Guid.NewGuid() };
var child = new Child() { Id = Guid.NewGuid(), ParentId = parent.Id };
parent.Children.Add(child);
You'd think the SQL executed would insert a new child with differing Id and ParentId values. But instead, what I'm seeing is:
// Assume parent is already in the DB, with ID of '1b1a6ecd-00ad-4265-ac0d-9a50bd30e247'
INSERT [dbo].[Child]
([Id],
[ParentId])
VALUES ('1b1a6ecd-00ad-4265-ac0d-9a50bd30e247' /* #0 */,
'1b1a6ecd-00ad-4265-ac0d-9a50bd30e247' /* #1 */)
Why is the SQL using the ParentId value for both fields? This doesn't make sense at all.
UPDATE
Unless I totally misunderstand something fundamental to EF, I think this must be a bug. I've uploaded a tiny reproducible project to http://1drv.ms/1kX2oVC
It uses EF 6.1 and .NET 4.5. I'm hoping some EF expert can chime in here and confirm this is a bug, or that I'm doing something fundamentally wrong.
With this set up Entity Framework isn't able to properly infer the associations. You have two options how to fix it:
Add modelBuilder.Entity<Parent>().HasMany(x => x.Children).WithRequired().HasForeignKey(x => x.ParentId); into your OnModelCreating.
or
Add public Parent Parent { get; set; } into your Child entity.
I have a table which has an optional FK to another table and want to change that FK to a required relationship.
I have Automatic Migrations enabled and enabled destructive changes for this update. All entities in the database also have this key populated.
I changed this:
modelBuilder.Entity<Blog>().HasOptional(b => b.AuthorSecurable).WithMany().Map(b => b.MapKey("AuthorSecurableId"));
to:
modelBuilder.Entity<Blog>().HasRequired(b => b.AuthorSecurable).WithMany().Map(b => b.MapKey("AuthorSecurableId"));
and got the following error:
'FK_dbo.Blogs_dbo.Securables_AuthorSecurableId' is not a constraint.
Could not drop constraint. See previous errors.
There are no previous errors I could see (no inner exception ect.)
This post says you can get around this error with the following:
ALTER TABLE [dbo].[Blogs] NOCHECK CONSTRAINT [FK_dbo.Blogs_dbo.Securables_AuthorSecurable_Id]
so i did:
public override void Up()
{
Sql("ALTER TABLE [dbo].[Blogs] NOCHECK CONSTRAINT [FK_dbo.Blogs_dbo.Securables_AuthorSecurable_Id]");
DropForeignKey("dbo.Blogs", "AuthorSecurableId", "dbo.Securables");
DropIndex("dbo.Blogs", new[] { "AuthorSecurableId" });
AlterColumn("dbo.Blogs", "AuthorSecurableId", c => c.Int(nullable: false));
AddForeignKey("dbo.Blogs", "AuthorSecurableId", "dbo.Securables", "Id", cascadeDelete: true);
CreateIndex("dbo.Blogs", "AuthorSecurableId");
}
But still got the same error
EDIT:
the full code is avaliable here and a minimal models are below:
public class Blog
{
public int Id { get; set; }
public Securable AuthorSecurable { get; set; }
}
public class Securable
{
public int Id { get; set; }
}
I'm pretty sure it's something regarding hidden conventions, but I always get an error when trying to map a many-to-many relation to an existing database.
Here is the simplest example:
[Table("ALRole", SchemaName = "AL")]
public class Role
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<User> Users { get; set; }
}
[Table("ALUser", SchemaName = "AL")]
public class User
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Role> Roles { get; set; }
}
I got the usual three tables in the db: the first two are obvious, and the third is created with this script:
CREATE TABLE AL.ALUsersRoles
(
RoleID int NOT NULL,
UserID int NOT NULL,
CONSTRAINT PK_ALUserRole PRIMARY KEY(RoleID, UserID),
CONSTRAINT FK_ALUserRole_RoleID FOREIGN KEY(RoleID) REFERENCES AL.ALRole(ID),
CONSTRAINT FK_ALUserRole_UserID FOREIGN KEY(UserID) REFERENCES AL.ALUser(ID)
)
Now I try to map the many-to-many relation, with code like this:
// ...I'm in the EntityTypeConfiguration-derived class (User)
HasMany(u => u.Roles)
.WithMany(r => r.Users)
.Map(m =>
{
m.MapLeftKey(u => u.ID, "UserID");
m.MapRightKey(r => r.ID, "RoleID");
ToTable("ALUsersRoles", "AL");
});
I tried all the possibile combinations and variations in this code, but I always get the error:
{"Invalid column name 'Name'.\r\nInvalid ...and so on...
So I think it must be the table that is not created correctly.
Any ideas?
Thanks in advance
Andrea
P.S.: I stripped down some of my code, so maybe there can be some small typo...
well, this works for me same as OP.
//many-to-many between *Users -> Web_User_Rol <- Web_Rol*
modelBuilder.Entity<Users>()
.HasMany(u => u.Web_Rols).WithMany(r => r.Users)
.Map(t=>t.MapLeftKey("user_id")
.MapRightKey("roleID")
.ToTable("Web_User_Rol"));
There is nothing wrong with your object model or fluent API code. I've used them and they perfectly created the desired schema without any exception. I think your problem comes from another entity (perhaps one with a "Name" property) unrelated to what you've shown here. To find that, drop (or rename) your existing database and let Code First create one for you and then compare the 2 databases and see what is different.