Gabriel Castillo Prada
Gabriel Castillo Prada

Reputation: 4663

EF Core - Error when adding a related entity

I get an error when I try to update a related entity of an entity that I already got from database. For illustration purposes I have these entites:

    class Car
{
   int Id ..;
   string Name ..;
   virtual ICollection<TireCar> tires ...;
}

class TireCar
{
  int Id ..;
  int TireId ..;
  int CarId..;
  int Size..;
  virtual TireBrand tire;
  virtual Car car;
}

class TireBrand
{
  int Id;
  string Name ..;
}

So, I'm trying to make a Patch method that allows me to update the Car data and also adds, updates or deletes the tires. The problem happens when I get the Car entity and after that I add a Tire. Something like that:

    void UpdateCar()
    {
        var car = carService.Get(...);

        ...

        carService.AddTire(new TireCar{ CarId = car.Id, TireId = 1 });

        ...
    }

I'm using the Repository pattern with DI, so the context is the same. The error that is throwing is:

System.InvalidOperationException: The association between entity types 'Car' and 'TireCar' has been severed but the foreign key for this relationship cannot be set to null. If the dependent entity should be deleted, then setup the relationship to use cascade deletes.'

I tried two things that worked but I think is not the solution:

Why that happend if I'm updating another tables? What Can I do to solve this?

Upvotes: 12

Views: 32645

Answers (2)

Ogglas
Ogglas

Reputation: 69928

From EF Core documentation:

By convention, cascade delete will be set to Cascade for required relationships and ClientSetNull for optional relationships. Cascade means dependent entities are also deleted. ClientSetNull means that dependent entities that are not loaded into memory will remain unchanged and must be manually deleted, or updated to point to a valid principal entity. For entities that are loaded into memory, EF Core will attempt to set the foreign key properties to null.

Example many to many with working Cascade using join entity type configuration and NuGet Microsoft.EntityFrameworkCore.SqlServer:

internal class MyContext : DbContext
{
    public MyContext(DbContextOptions<MyContext> options)
        : base(options)
    {
    }

    public DbSet<Post> Posts { get; set; }
    public DbSet<Tag> Tags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .HasMany(p => p.Tags)
            .WithMany(p => p.Posts)
            .UsingEntity<PostTag>(
                j => j
                    .HasOne(pt => pt.Tag)
                    .WithMany(t => t.PostTags)
                    .HasForeignKey(pt => pt.TagId),
                j => j
                    .HasOne(pt => pt.Post)
                    .WithMany(p => p.PostTags)
                    .HasForeignKey(pt => pt.PostId),
                j =>
                {
                    j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
                    j.HasKey(t => new { t.PostId, t.TagId });
                });
    }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public ICollection<Tag> Tags { get; set; }
    public List<PostTag> PostTags { get; set; }
}

public class Tag
{
    public string TagId { get; set; }

    public ICollection<Post> Posts { get; set; }
    public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
    public DateTime PublicationDate { get; set; }

    public int PostId { get; set; }
    public Post Post { get; set; }

    public string TagId { get; set; }
    public Tag Tag { get; set; }
}

https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=data-annotations%2Cfluent-api-simple-key%2Csimple-key#join-entity-type-configuration

If you introduce more paths you might end up getting the following exception on Update-Database using Microsoft.EntityFrameworkCore.Tools or a similar command.

Microsoft.Data.SqlClient.SqlException (0x80131904): Introducing FOREIGN KEY constraint '' on table '' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints. Could not create constraint or index. See previous errors.

A solution could be to simply add DeleteBehavior.Restrict to the relation but you will then have a similar error again when deleting a parent entity unless you clear every relation yourself.

Example:

The association between entity types '' and '' has been severed, but the relationship is either marked as required or is implicitly required because the foreign key is not nullable. If the dependent/child entity should be deleted when a required relationship is severed, configure the relationship to use cascade deletes. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values

You can solve this by specifying DeleteBehavior.ClientCascade instead which will allow EF to perform cascade deletes on loaded entities.

internal class MyContext : DbContext
{
    public MyContext(DbContextOptions<MyContext> options)
        : base(options)
    {
    }

    public DbSet<Post> Posts { get; set; }
    public DbSet<Tag> Tags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .HasMany(p => p.Tags)
            .WithMany(p => p.Posts)
            .UsingEntity<PostTag>(
                j => j
                    .HasOne(pt => pt.Tag)
                    .WithMany(t => t.PostTags)
                    .HasForeignKey(pt => pt.TagId)
                    .OnDelete(DeleteBehavior.Cascade),
                j => j
                    .HasOne(pt => pt.Post)
                    .WithMany(p => p.PostTags)
                    .HasForeignKey(pt => pt.PostId)
                    .OnDelete(DeleteBehavior.ClientCascade),
                j =>
                {
                    j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
                    j.HasKey(t => new { t.PostId, t.TagId });
                });
    }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public ICollection<Tag> Tags { get; set; }
    public List<PostTag> PostTags { get; set; }
}

public class Tag
{
    public string TagId { get; set; }

    public ICollection<Post> Posts { get; set; }
    public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
    public DateTime PublicationDate { get; set; }

    public int PostId { get; set; }
    public Post Post { get; set; }

    public string TagId { get; set; }
    public Tag Tag { get; set; }
}

https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.deletebehavior?view=efcore-5.0

Upvotes: 1

jorgonor
jorgonor

Reputation: 1719

Your issue is actually a configuration issue, not an issue with EF Core. Read carefully what the error says:

The association between entity types 'Car' and 'TireCar' has been severed but the foreign key for this relationship cannot be set to null. If the dependent entity should be deleted, then setup the relationship to use cascade deletes.

Entity Framework Core has by default (.NET Core 2.0 forward) a SET NULL policy when a dependent entity becomes orphan. If we look carefully at your TireCar model, you are not setting the CarId property as nullable, so the column cannot be set to null.

You have two different solutions to fix this. If you want the TireCar entity to get deleted when you remove it from your Car entity, then setup cascade deletes for this relationship (You can also change the EF Core default policy). This can be setup in your DbContext via FluentApi.

class MyContext : DbContext
{
    // Your DbSets

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TireCar>()
            .HasOne(tc => tc.car)
            .WithMany(car => car.tires)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

Another solution is to let the TireCar entity have null values in the CarId column, simply changing your entity model like this.

class TireCar
{
    public int Id  { get; set; }
    public int TireId { get; set; }
    public int? CarId { get; set; }
    public int Size { get; set; }
    public virtual TireBrand tire { get; set; }
    public virtual Car car { get; set; }
}

Upvotes: 13

Related Questions