Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The UPDATE statement conflicted with the FOREIGN KEY constraint in EF Core

We have 3 model classes:

  • Host
  • TournamentBatch
  • TournamentBatchItem

Host has many TournamentBatch. TournamentBatch has many TournamentBatchItem. In the TournamentBatch table will have FK Host.

We did override for SaveChangesAsync in ApplicationDbContext to allow soft-delete as following:

public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
    {
        OnBeforeSaving();

        return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

    private void OnBeforeSaving()
    {

        if (_httpContextAccessor.HttpContext != null)
        {
            var userName = _httpContextAccessor.HttpContext.User.Identity.Name;
            var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);


            // Added
            var added = ChangeTracker.Entries().Where(v => v.State == EntityState.Added && typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

            added.ForEach(entry =>
            {
                ((IBaseEntity)entry.Entity).DateCreated = DateTime.UtcNow;
                ((IBaseEntity)entry.Entity).CreatedBy = userId;

                ((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
                ((IBaseEntity)entry.Entity).LastModifiedBy = userId;
            });

            // Modified
            var modified = ChangeTracker.Entries().Where(v => v.State == EntityState.Modified &&
            typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

            modified.ForEach(entry =>
            {
                ((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
                ((IBaseEntity)entry.Entity).LastModifiedBy = userId;
            });

            // Deleted
            var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
           typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

            // var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted).ToList();

            deleted.ForEach(entry =>
            {
                ((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
                ((IBaseEntity)entry.Entity).DeletedBy = userId;
            });

            foreach (var entry in ChangeTracker.Entries()
                                    .Where(e => e.State == EntityState.Deleted &&
                                    e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        entry.CurrentValues["IsDeleted"] = false;
                        break;

                    case EntityState.Deleted:
                        entry.State = EntityState.Modified;
                        entry.CurrentValues["IsDeleted"] = true;
                        break;
                }
            }
        }
        else
        {
            // DbInitializer kicks in
        }
    }

In our model:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace AthlosifyWebArchery.Models
{
  public class TournamentBatch : IBaseEntity
  {
    [Key]
    public Guid TournamentBatchID { get; set; }

    public Guid HostID { get; set; }

    public string Name { get; set; }

    public string BatchFilePath { get; set; }

    [Display(Name = "Batch File Size (bytes)")]
    [DisplayFormat(DataFormatString = "{0:N1}")]
    public long BatchFileSize { get; set; }

    [Display(Name = "Uploaded (UTC)")]
    [DisplayFormat(DataFormatString = "{0:F}")]
    public DateTime DateUploaded { get; set; }

    public DateTime DateCreated { get; set; }

    public string CreatedBy { get; set; }

    public DateTime LastDateModified { get; set; }

    public string LastModifiedBy { get; set; }

    public DateTime? DateDeleted { get; set; }

    public string DeletedBy { get; set; }

    public bool IsDeleted { get; set; }

    public Host Host { get; set; }

    public ICollection<TournamentBatchItem> TournamentBatchItems { get; set; }

    [Timestamp]
    public byte[] RowVersion { get; set; }

    [ForeignKey("CreatedBy")]
    public ApplicationUser ApplicationCreatedUser { get; set; }

    [ForeignKey("LastModifiedBy")]
    public ApplicationUser ApplicationLastModifiedUser { get; set; }


}

}

In our Razorpage, we have a page to delete TournamentBatch including TournamentBatchItem by doing this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Data;
using AthlosifyWebArchery.Models;
using Microsoft.Extensions.Logging;

namespace AthlosifyWebArchery.Pages.Administrators.TournamentBatches
{
  public class DeleteModel : PageModel
   {
    private readonly AthlosifyWebArchery.Data.ApplicationDbContext _context;


    private readonly ILogger _logger;


    public DeleteModel(AthlosifyWebArchery.Data.ApplicationDbContext context,
                        ILogger<DeleteModel> logger)
    {
        _context = context;
        _logger = logger;
    }

    [BindProperty]
    public TournamentBatch TournamentBatch { get; set; }

    public IList<TournamentBatchItem> tournamentBatchItems { get; set; }

    public string ConcurrencyErrorMessage { get; set; }

    public async Task<IActionResult> OnGetAsync(Guid? id, bool? concurrencyError)
    {
        if (id == null)
        {
            return NotFound();
        }

        TournamentBatch = await _context.TournamentBatch
                                    .AsNoTracking() //Addded
                                    .FirstOrDefaultAsync(m => m.TournamentBatchID == id);



        if (TournamentBatch == null)
        {
            return NotFound();
        }

        if (concurrencyError.GetValueOrDefault())
        {
            ConcurrencyErrorMessage = "The record you attempted to delete "
              + "was modified by another user after you selected delete. "
              + "The delete operation was canceled and the current values in the "
              + "database have been displayed. If you still want to delete this "
              + "record, click the Delete button again.";
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(Guid? id)
    {
        try
        {
            //var tournamentBatchItems = await _context.TournamentBatchItem.Where(m => m.TournamentBatchID == id).ToListAsync();
            //_context.TournamentBatchItem.RemoveRange(tournamentBatchItems);
            //await _context.SaveChangesAsync();


            if (await _context.TournamentBatch.AnyAsync(
                m => m.TournamentBatchID == id))
            {
                // Department.rowVersion value is from when the entity
                // was fetched. If it doesn't match the DB, a
                // DbUpdateConcurrencyException exception is thrown.
                _context.TournamentBatch.Remove(TournamentBatch);
                _logger.LogInformation($"TournamentBatch.BeforeSaveChangesAsync ... ");
                await _context.SaveChangesAsync();
                _logger.LogInformation($"DbInitializer.AfterSaveChangesAsync ... ");
            }
            return RedirectToPage("./Index");
        }
        catch(DbUpdateException)
        {
            return RedirectToPage("./Delete",
                new { concurrencyError = true, id = id });

        }
        //catch (DbUpdateConcurrencyException)
        //{
        //    return RedirectToPage("./Delete",
        //        new { concurrencyError = true, id = id });
        //}
    }
}

}

... and we have the following error which is a bit odd.

System.Data.SqlClient.SqlException (0x80131904): The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_TournamentBatch_Host_HostID". The conflict occurred in database "aspnet-AthlosifyWebArchery-53bc9b9d-9d6a-45d4-8429-2a2761773502", table "dbo.Host", column 'HostID'. The statement has been terminated.

Any ideas?

Things we did:

  • If we removed OnBeforeSaving(); from the SaveChangesAsyc()method, the code is deleting (hard-delete) successfully the TournamentBatch as well as TournamentBatchItem.

  • If we included OnBeforeSaving(); from the SaveChangesAsyc()method AND tested with deleting Host and TournamentBatchItem (Not TournamentBatch), the code is deleting (soft-delete) successfully.

It seems it has something to do with the relationship between Host and TournamentBatch

Environment:

  • .Net Core 2.1
  • Ms SQL Server
like image 556
dcpartners Avatar asked Dec 07 '18 23:12

dcpartners


People also ask

What is a FOREIGN KEY constraint conflict?

FIX: A conflict with the foreign key constraint occurs when you update the case of the column values in the primary key table or you pad column values in the primary key table in SQL Server 2005

Is it possible to manually update primary and foreign keys in EF?

When you update anything regarding primary or foreign keys in EF, more often than not, an error is thrown. It is possible to fix this manually. However the thing I personally do is to drop the entire database, add a migration and update the DB.

How do I remove a FOREIGN KEY constraint from a table?

Right-click the constraint and select Modify. In the grid under Table Designer, click Enforce Foreign Key Constraint and select No from the drop-down menu. Click Close. Kindly note this is unsupported change what we are doing and we are not sure what complications it can cause.

Why is my primary key being referenced by a foreign key?

This error is encountered when the primary key of a table is updated but it is referenced by a foreign key from another table and the update specific is set to No action. The No action is the default option. If this is your case and No action is set on the update operation you can change the foreign-key definition to Cascade.


2 Answers

Reason

I guess the reason is you're having your TournamentBatch bind from client side .

Let's review the OnPostAsync() method:

public async Task<IActionResult> OnPostAsync(Guid? id)
{
    try
    {
        if (await _context.TournamentBatch.AnyAsync(
            m => m.TournamentBatchID == id))
        {
            _context.TournamentBatch.Remove(TournamentBatch);
            _logger.LogInformation($"TournamentBatch.BeforeSaveChangesAsync ... ");
            await _context.SaveChangesAsync();
            _logger.LogInformation($"DbInitializer.AfterSaveChangesAsync ... ");
        }
        return RedirectToPage("./Index");
    }
    // ....
}

Here the TournamentBatch is a property of PageModel:

    [BindProperty]
    public Models.TournamentBatch TournamentBatch{ get; set; }

Note you didn't retrieve it from the database according to the id, and you just remove it by _context.TournamentBatch.Remove(TournamentBatch); directly.

In other words, the other properties of TournamentBatch will be set by ModelBinding. Let's say if you submit only the Id, all the other property will be the default value. For example, Host will be null and the HostID will be the default 00000000-0000-0000-0000-000000000000. So when you save changes, the EF Core will update the model as below :

UPDATE [TournamentBatch]
SET [HostID] = '00000000-0000-0000-0000-000000000000' , 
    [IsDeleted] = 1 ,
    # ... other fields
WHERE [TournamentBatchID] = 'A6F5002A-60CA-4B45-D343-08D660167B06'

Because there's no Host record whose id equals 00000000-0000-0000-0000-000000000000, the database will complains :

The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_TournamentBatch_Host_HostID". The conflict occurred in database "App-93a194ca-9622-487c-94cf-bcbe648c6556", table "dbo.Host", column 'Id'. The statement has been terminated.

How to fix

Instead of binding the TournamentBatch from client side, you need retrieve the TournamentBatch from server by TournamentBatch = await _context.TournamentBatch.FindAsync(id); . Thus you will have all the properties set correctly so that EF will update the field correctly :

    try
    {
        //var tournamentBatchItems = await _context.TournamentBatchItem.Where(m => m.TournamentBatchID == id).ToListAsync();
        //_context.TournamentBatchItem.RemoveRange(tournamentBatchItems);
        //await _context.SaveChangesAsync();
        TournamentBatch = await _context.TournamentBatch.FindAsync(id);

        if (TournamentBatch != null)
        {
            // Department.rowVersion value is from when the entity
            // was fetched. If it doesn't match the DB, a
            // DbUpdateConcurrencyException exception is thrown.
            _context.TournamentBatch.Remove(TournamentBatch);
            _logger.LogInformation($"TournamentBatch.BeforeSaveChangesAsync ... ");
            await _context.SaveChangesAsync();
            _logger.LogInformation($"DbInitializer.AfterSaveChangesAsync ... ");
        }
        return RedirectToPage("./Index");
    }
    // ...
like image 166
itminus Avatar answered Oct 23 '22 03:10

itminus


Can you try the following and change how you implemeted the soft-delete.

Change the code below in your ApplicationDBContext OnBeforeSaving method

foreach (var entry in ChangeTracker.Entries()
                                    .Where(e => e.State == EntityState.Deleted &&
                                    e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
    switch (entry.State)
    {
        case EntityState.Added:
            entry.CurrentValues["IsDeleted"] = false;
            break;

        case EntityState.Deleted:
            entry.State = EntityState.Modified;
            entry.CurrentValues["IsDeleted"] = true;
            break;
    }
}

---- TO -----

foreach (var entry in ChangeTracker.Entries()
                                    .Where(e => e.State == EntityState.Deleted &&
                                    e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
    SoftDelete(entry);
}

SoftDelete method:

private void SoftDelete(DbEntityEntry entry)
{
    Type entryEntityType = entry.Entity.GetType();

    string tableName = GetTableName(entryEntityType);
    string primaryKeyName = GetPrimaryKeyName(entryEntityType);

    string sql =
        string.Format(
            "UPDATE {0} SET IsDeleted = true WHERE {1} = @id",
                tableName, primaryKeyName);

    Database.ExecuteSqlCommand(
        sql,
        new SqlParameter("@id", entry.OriginalValues[primaryKeyName]));

    // prevent hard delete            
    entry.State = EntityState.Detached;
}

This method will execute sql query over each removed entity:

UPDATE TournamentBatch SET IsDeleted = true WHERE TournamentBatchID = 123

To make it versatile and compatible with any entity (not just TournamentBatch) we need to know two additional properties, Table name and Primary Key name

There are two functions inside of SoftDelete method for this purpose: GetTableName and GetPrimaryKeyName. I have defined them in separate file and marked class as partial. So be sure to make your context class partial in order for things to work. Here is GetTableName and GetPrimaryKeyName with caching mechanism:

public partial class ApplicationDBContext
{
    private static Dictionary<Type, EntitySetBase> _mappingCache =
        new Dictionary<Type, EntitySetBase>();

    private string GetTableName(Type type)
    {
        EntitySetBase es = GetEntitySet(type);

        return string.Format("[{0}].[{1}]",
            es.MetadataProperties["Schema"].Value,
            es.MetadataProperties["Table"].Value);
    }

    private string GetPrimaryKeyName(Type type)
    {
        EntitySetBase es = GetEntitySet(type);

        return es.ElementType.KeyMembers[0].Name;
    }

    private EntitySetBase GetEntitySet(Type type)
    {
        if (!_mappingCache.ContainsKey(type))
        {
            ObjectContext octx = ((IObjectContextAdapter)this).ObjectContext;

            string typeName = ObjectContext.GetObjectType(type).Name;

            var es = octx.MetadataWorkspace
                            .GetItemCollection(DataSpace.SSpace)
                            .GetItems<EntityContainer>()
                            .SelectMany(c => c.BaseEntitySets
                                            .Where(e => e.Name == typeName))
                            .FirstOrDefault();

            if (es == null)
                throw new ArgumentException("Entity type not found in GetTableName", typeName);

            _mappingCache.Add(type, es);
        }

        return _mappingCache[type];
    }
}
like image 40
Tarik Tutuncu Avatar answered Oct 23 '22 04:10

Tarik Tutuncu