1

I'm having trouble while trying to delete a row with Entity Framework. It says I'm having Concurrency issues, but I don't see why that could be.

using (AppHost_DataHubEntities db = new AppHost_DataHubEntities())
{
    DateTime tooOld = (DateTime.Now - HISTORY_TIME_SPAN);
    var asd = db.FromDataHub.RemoveRange(db.FromDataHub.Where(x => x.DateTime < tooOld));
    db.SaveChanges();
}

Here, SaveChanges() throws the following error:

Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling opti...

As I understand this means that a row I was trying to delete was already modified. But how can this be, if this is a development database on my machine that only this application accesses?

Also I've looked at the state of the row. And it properly changes to 'deleted' after the RemoveRange() call.

Any ideas on this?

Happy to provide more info if needed.

Edit:

DbContext definition:

public partial class AppHost_DataHubEntities : DbContext
{
    public AppHost_DataHubEntities()
        : base("name=AppHost_DataHubEntities")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        throw new UnintentionalCodeFirstException();
    }

    public virtual DbSet<FromDataHub> FromDataHub { get; set; }
    public virtual DbSet<Point> Point { get; set; }
    public virtual DbSet<ToDataHub> ToDataHub { get; set; }
}

Screenshot of the ERD:

ERD

Project to reproduce:

  • SQL setup script in root directory.
  • SQL express was used.
  • .NET Version doesn't matter (tested with 4.5, 4.6 and 4.7)

filebin.net

Daniel
  • 25
  • 6
  • When you say "state of the row", do you mean state of the object in memory, or state of the row in the database? – Lasse V. Karlsen Nov 23 '20 at 09:14
  • 1
    I mean when I call db.Entry(xy).State. – Daniel Nov 23 '20 at 09:15
  • Where are you loading the values into your Entity object? You are creating a new instance of the db and unless the connection string is getting data from a database your object is empty. Check the db object to determine if you have any data. – jdweng Nov 23 '20 at 09:20
  • @jdweng I have values. When I put the .Where clause outside I see all values that need to be deleted. – Daniel Nov 23 '20 at 09:25
  • The savechanges needs to have four commands in the Entity Model : Select, Update, Delete, Insert. The Model is missing these methods. They should automatically be created in the Model. See : https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/fluent/cud-stored-procedures – jdweng Nov 23 '20 at 09:31
  • Can you show the DB context configuration for this entity? – Paddy Nov 23 '20 at 09:35
  • @jdweng I think what you mentioned only applies to the Code First approach? I have my schema directly from the DB, forgot to mention that. – Daniel Nov 23 '20 at 09:37
  • @Paddy added it to the question. – Daniel Nov 23 '20 at 09:37
  • Are any other entities in your context definition referencing these entities or referenced by these entities? Is the database set up for Cascade Delete on any dependencies of this object? It may be that in attempting to delete a FromDataHub, the DB is automatically cascading a delete that EF isn't expecting and it's trying to delete a related record manually. – Steve Py Nov 23 '20 at 09:42
  • Do you have a primary key set on this table? – Paddy Nov 23 '20 at 09:46
  • @StevePy Yes, FromDataHub has a foreign key to Point. But there is no UPDATE or DELETE rule configured. No table is depending on FromDataHub. There is only a dependency FromDataHub to Point. – Daniel Nov 23 '20 at 09:49
  • @Paddy The Primary key is not an ID, but a foreign key and a timestamp column combined. Might that be an issue? – Daniel Nov 23 '20 at 09:50
  • So running the exact code that you posted, with nothing else included, just executing the code you had pasted against your Database with your current mappings results in the error? – Steve Py Nov 23 '20 at 10:04
  • @StevePy correct! – Daniel Nov 23 '20 at 10:06
  • You always need the Update, Delete, and Insert commands when you update the database. Not needed if you are only doing select. Depending how you built you model these three methods may automatically be generated or you have to custom build the commands. The schema is the mapping (edmx file) that link the tables/columns in the database to the c# classes. It doesn't include the three cokmands. – jdweng Nov 23 '20 at 10:27
  • @jdweng I see. I even found that there are no functions linked to these three in the .edmx file. But how can it then be that I'm able to insert into this database with the same context. When I change it to delete it doesn't work anymore. – Daniel Nov 23 '20 at 10:30
  • See the answer with the check mark on following : https://stackoverflow.com/questions/6819813/solution-for-store-update-insert-or-delete-statement-affected-an-unexpected-n – jdweng Nov 23 '20 at 10:46
  • @jdweng This didn't work for me. My "context" has no refresh function on it. When I tried with `var context = ((IObjectContextAdapter)db).ObjectContext;` I found the refresh function, but there was no `context.Articles`. – Daniel Nov 23 '20 at 12:17

2 Answers2

1

A couple things to try, too much for a comment so:

Try deleting a single "old" item:

using (AppHost_DataHubEntities db = new AppHost_DataHubEntities())
{
    DateTime tooOld = (DateTime.Now - HISTORY_TIME_SPAN);
    var item = db.FromDataHub.Where(x => x.DateTime < tooOld).First();
    db.FromDataHub.Remove(item);
    db.SaveChanges();
}

Does it delete the expected row? Does it appear to change anything in the related Point?

Next, try a Range:

using (AppHost_DataHubEntities db = new AppHost_DataHubEntities())
{
    DateTime tooOld = (DateTime.Now - HISTORY_TIME_SPAN);
    var items = db.FromDataHub.Where(x => x.DateTime < tooOld).Take(3).ToList();
    db.FromDataHub.RemoveRange(items);
    db.SaveChanges();
}

Same checks for any errors or unexpected modifications? What "Point" records do each of these touch? The same one? different ones?

Next, check the data if there are any where all From records might be deleted, all items for a point that are older than the threshold, get that Point ID:

using (AppHost_DataHubEntities db = new AppHost_DataHubEntities())
{
    DateTime tooOld = (DateTime.Now - HISTORY_TIME_SPAN);
    var items = db.FromDataHub.Where(x => x.DateTime < tooOld && x.PointId == pointId)..ToList();
    db.FromDataHub.RemoveRange(items);
    db.SaveChanges();
}

Does this work?

Basically when facing issues like this you need to work out the lowest common denominator to see if there is a mapping issue or such that results in the error in all cases, or a particular data combination that trips it up in cases where that data might be identified for deletion.

If you cannot delete a single row, then something in the mapping is blocking it. If you can delete a single row, but a range is causing an issue, either across a single Point or different points then further drilling will be needed to determine why. If all three worked, you could try adding the .ToList() inside your DeleteRange call.

Normally bulk deletes for maintenance isn't something I'd do with EF, rather setting up a scheduled job to run after hours against the database directly. If I do want to do bulk deletes in EF I will typically break it up into blocks of work. for example:

List<FromDataHub> itemsToClean = new List<FromDataHub>();
using(var context = new AppHost_DataHubEntities())
{
    itemsToClean = context.FromDataHub
        .Where(x => x.DateTime < tooOld)
        .Select(x => new FromDataHub { PointId = x.PointId, DateTime = x.DateTime })
        .ToList();
}

const int batchSize = 500;
int batchCount = 1;
var batch = itemsToClean.Take(batchSize);
while(batch.Any())
{
    using(var context = new AppHost_DataHubEntities())
    {
        foreach(var item in batch)
        {
            context.FromDataHub.Attach(item);
            context.Entity(item).EntityState = EntityState.Deleted;
        }
        context.SaveChanges();
    }
    batch = itemsToClean.Skip(++batchCount).Take(batchSize);
}

That code is basically off the top of the head and it's late, but should give the idea. For potentially large entities I would consider a bounded context specific to this purpose where the defined "FromDataHub" entity registered for that Context consisted of just the ID columns. (in which case you could simplify this to read & delete in batches. What this example does instead is executes a query to just return the relevant ID colums (assumed to be PointId and the DateTime) and populating detached entities from it. Then in batches of 500 it opens a new DbContext, attaches that entity, sets its modified state to deleted, and then saves the batch. This is to guard against tracking a large # of entities within a DbContext instance in case this code needs to delete a lot of rows. Again, my first preference would be to use a scheduled maintenance job on the database server after hours, but it might give you some ideas to track down the issue.

Update: You will need to update your example to provide a complete and minimal reproducible example. I've set up a schema based on what you provided and was not able to reproduce the issue with EF:

Entities:

[Table("Points")]
public class Point
{
    [Key]
    public int PointId { get; set; }
    public string PointName { get; set; }
    public bool Collect { get; set; }

    public virtual ICollection<FromDataHub> FromDataHubs { get; set; } = new List<FromDataHub>();
    public virtual ICollection<ToDataHub> ToDataHubs { get; set; } = new List<ToDataHub>();
}

[Table("FromDataHub")]
public class FromDataHub
{
    [Key, Column(Order = 0), ForeignKey("Point")]
    public int PointId { get; set; }
    [Key, Column(Order = 1)]
    public DateTime DateTime { get; set; }
    [Key, Column(Order = 2)]
    public int Value { get; set; }

    public virtual Point Point { get; set; }
}

[Table("ToDataHub")]
public class ToDataHub
{
    [Key, Column(Order = 0), ForeignKey("Point")]
    public int PointId { get; set; }
    [Key, Column(Order = 1)]
    public DateTime DateTime { get; set; }

    public virtual Point Point { get; set; }
}

with mapping:

modelBuilder.Entity<Point>()
    .HasMany(x => x.FromDataHubs)
    .WithRequired(x => x.Point);

modelBuilder.Entity<Point>()
    .HasMany(x => x.ToDataHubs)
    .WithRequired(x => x.Point); 

... which was completely optional, it worked without the explicit mapping.

The test:

using (var context = new TestDbContext())
{
    DateTime date = DateTime.Today.AddMonths(-6);
    var itemsToDelete = context.FromDataHubs.Where(x => x.DateTime < date);
    context.FromDataHubs.RemoveRange(itemsToDelete);
    context.SaveChanges();
}

After seeding the tables with a couple Points and a handful of both From and To DataHub records ranging less than and greater than 6 months old, running this code successfully deleted the old records without any exception.

If your classes differ significantly from the above then please post the entity definitions and any mapping configuration you are currently using.

Update #2:

Ok, WOW, that is a mystery. What I have been able to uncover is that the issue seems to be particular to your Database for some reason. I modified my schema to match yours and modified your database to eliminate possible wierdness. I also copied your test data across to my database in case it was a data-related issue. The result was that my test against your DB continues to fail with the same exception, while your app against my DB succeeds.

Your schema:

USE [StackExample]
GO
/****** Object:  Table [dbo].[FromDataHub]    Script Date: 25/11/2020 8:50:04 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[FromDataHub](
    [PointId] [int] NOT NULL,
    [DateTime] [datetime] NOT NULL,
    [Value] [float] NOT NULL,
 CONSTRAINT [PK_FromDataHub] PRIMARY KEY CLUSTERED 
(
    [PointId] ASC,
    [DateTime] 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
/****** Object:  Table [dbo].[Point]    Script Date: 25/11/2020 8:50:04 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Point](
    [PointId] [int] IDENTITY(1,1) NOT NULL,
    [PointName] [varchar](255) NOT NULL,
    [Collect] [bit] NOT NULL,
 CONSTRAINT [PK_Point] PRIMARY KEY CLUSTERED 
(
    [PointId] 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
/****** Object:  Table [dbo].[ToDataHub]    Script Date: 25/11/2020 8:50:04 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[ToDataHub](
    [PointId] [int] NOT NULL,
    [Value] [float] NOT NULL,
 CONSTRAINT [PK_ToDataHub] PRIMARY KEY CLUSTERED 
(
    [PointId] 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].[FromDataHub]  WITH CHECK ADD  CONSTRAINT [FK_FromDataHub_Point] FOREIGN KEY([PointId])
REFERENCES [dbo].[Point] ([PointId])
GO
ALTER TABLE [dbo].[FromDataHub] CHECK CONSTRAINT [FK_FromDataHub_Point]
GO
ALTER TABLE [dbo].[ToDataHub]  WITH CHECK ADD  CONSTRAINT [FK_ToDataHub_Point] FOREIGN KEY([PointId])
REFERENCES [dbo].[Point] ([PointId])
GO
ALTER TABLE [dbo].[ToDataHub] CHECK CONSTRAINT [FK_ToDataHub_Point]
GO

My Schema

USE [Spikes]
GO
/****** Object:  Table [dbo].[FromDataHub]    Script Date: 25/11/2020 8:50:44 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[FromDataHub](
    [PointId] [int] NOT NULL,
    [DateTime] [datetime] NOT NULL,
    [Value] [float] NOT NULL,
 CONSTRAINT [PK_FromDataHub_1] PRIMARY KEY CLUSTERED 
(
    [PointId] ASC,
    [DateTime] 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
/****** Object:  Table [dbo].[Point]    Script Date: 25/11/2020 8:50:44 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Point](
    [PointId] [int] NOT NULL,
    [PointName] [nvarchar](20) NOT NULL,
    [Collect] [bit] NOT NULL,
 CONSTRAINT [PK_Points] PRIMARY KEY CLUSTERED 
(
    [PointId] 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
/****** Object:  Table [dbo].[ToDataHub]    Script Date: 25/11/2020 8:50:44 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[ToDataHub](
    [PointId] [int] NOT NULL,
    [Value] [float] NOT NULL,
 CONSTRAINT [PK_ToDataHub_1] PRIMARY KEY CLUSTERED 
(
    [PointId] 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].[Point] ADD  CONSTRAINT [DF_Points_Collect]  DEFAULT ((1)) FOR [Collect]
GO
ALTER TABLE [dbo].[FromDataHub]  WITH CHECK ADD  CONSTRAINT [FK_FromDataHub_Points] FOREIGN KEY([PointId])
REFERENCES [dbo].[Point] ([PointId])
GO
ALTER TABLE [dbo].[FromDataHub] CHECK CONSTRAINT [FK_FromDataHub_Points]
GO
ALTER TABLE [dbo].[ToDataHub]  WITH CHECK ADD  CONSTRAINT 
[FK_ToDataHub_Point] FOREIGN KEY([PointId])
REFERENCES [dbo].[Point] ([PointId])
GO
ALTER TABLE [dbo].[ToDataHub] CHECK CONSTRAINT [FK_ToDataHub_Point]
GO

Data in both was identical. The only schema difference I didn't change was the data type/size for the PointName. I also had a default constraint on the Collect value. The changes from your original Schema was basically to change the Point ID PK and FKs from "ID" and "Point" to "PointId" in both cases. Originally my schema didn't have the FKs declared.

One thing I will note is that I was not able to get your EDMX to work out of the box. I noticed that the entities it declared were lacking any Key or FK declarations for the entities. I had to add these manually and disable the CodeFirst assertion:

public AppHost_DataHubEntities : base("name=AppHost_DataHubEntities")
{
    Database.SetInitializer<AppHost_DataHubEntities>(null);
}

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    //throw new UnintentionalCodeFirstException();
    modelBuilder.Entity<Point>()
        .ToTable("Point")
        .HasKey(x => x.PointId)
        .HasMany(x => x.FromDataHub)            
        .WithRequired(x => x.Point)
        .HasForeignKey(x => x.PointId);
    //modelBuilder.Entity<Point>()
    //    .HasKey(x => x.ID)
    //    .HasOptional(x => x.ToDataHub)
    //    .WithRequired(x => x.Point1);

    modelBuilder.Entity<FromDataHub>()
        .ToTable("FromDataHub")
        .HasKey(x => new { x.PointId, x.DateTime });

    modelBuilder.Entity<ToDataHub>()
        .ToTable("ToDataHub")
        .HasKey(x => new { x.PointId });

    }

Test Code:

using (var context = new SoDbContext())
{
    DateTime tooOld = DateTime.Now - new TimeSpan(0, 15, 0);
    var items = context.FromDataHubs.Where(x => x.DateTime < tooOld).ToList();
    //db.FromDataHub.RemoveRange(items);
    context.FromDataHubs.Remove(items.First());
    context.SaveChanges();
}

I also set the initializer on the DbContext to null and deleted the Migrations, but the error persists on your test database. So if I point your app's config at your DB I get the error. If I point it at my "Spikes" DB then it runs without issue. Same goes for my previous unit tests.

About the only thing I can suggest would be to avoid the EDMX & Code First generation and create the desired schema manually, mapping EF entities over to it. There seems to be something subtle in the generated schema that strangely isn't apparent in the generated SQL for the tables, or at the DB level?

Steve Py
  • 26,149
  • 3
  • 25
  • 43
  • Thanks for the big answer. (: But sadly nothing of what you described worked for me. Currently I have only two points in the database. So around 50% of the FromDataHub entries are associated with point1 and 50% are with point2. .. I have no idea where the error could be. The only thing that I don't really understand is what @jdweng was saying with these insert, delete and update functions. – Daniel Nov 23 '20 at 10:52
  • I set up a database with the schema you outlined and was not able to reproduce the issue. I have updated the answer with the entities and test that I ran, but you will need to provide a minimal reproducible example with your exact entities & config because there was no issue deleting the historical records. – Steve Py Nov 23 '20 at 21:34
  • I posted a project folder in my question yesterday where you can (hopefully) reproduce the issue. I'm really wondering by now how this can be.. – Daniel Nov 24 '20 at 06:41
  • ok ,I will have a look tomorrow morning. It's possibly something odd with the EDMX, you might try manually configuring the bindings by declaring EntityTypeConfiguration classes and registering those on model creating, or using the modelBuilder. – Steve Py Nov 24 '20 at 12:09
  • Thanks! I've now converted the delete clauses to standard SQL statements.. But I'd still be interested in why this doesn't work. I'll look into the EDMX when I find time. – Daniel Nov 24 '20 at 13:33
  • I wish I had an answer for you, but it looks to be something particular to your database schema. I've updated the answer again ("Update 2") with details about what I tried but it's a stumper. I updated my schema to match yours and the test code works against my DB but not your DB. The schema scripts are essentially identical in terms of relationships etc. as far as I could determine. The CodeFirst migrations in your project were not happy after I built the DB so I suspect something weird happened with the DB creation or such. Odd to say the least. – Steve Py Nov 24 '20 at 23:29
  • Thanks a lot for your efforts. I'll try with rebuilding the database and using the code first approach. – Daniel Nov 26 '20 at 06:38
0

I've come across this before and this below information helped me.

I suggest that you follow your usage of the DbContext and see if changes are made to entities before the .RemoveRange is performed.

Since you are using a new AppHost_DataHubEntities every time (it seems), changes to the entites might have happened in another instance of AppHost_DataHubEntities in your code, before .RemoveRange is called.

Therefore the context in your example is not aware of changes that were made in another instance of AppHost_DataHubEntities.

what it's telling you is that between when you grabbed the data out of the database and when you saved your changes someone else has changed the data (Which meant when you went to save it 0 rows actually got updated). In SQL terms, their update query's where clause contains the original value of every field in the row, and if 0 rows are affected it knows something's gone wrong

Source: https://stackoverflow.com/a/1836304/5782981

Kristian
  • 125
  • 2
  • 17
  • Actually everything is done within the same DbContext. I just stripped anything unnecessary out of the example to make it as easy to understand as possible. There's nothing accessing my database except this one DbContext block. – Daniel Nov 23 '20 at 09:47
  • Chances are something you stripped out, if within the scope of this DbContext, is linked to the exception. It may not seem relevant, but often that's where the bug lies, in code that is assumedly not related. :) – Steve Py Nov 23 '20 at 10:01
  • @StevePy I understand, but the error also happens in this stripped out version. So that shouldn't be the issue, right? – Daniel Nov 23 '20 at 10:03
  • @Daniel if you can perhaps provide us with a project to test on, it would be convenient for helping more. – Kristian Nov 23 '20 at 10:57
  • @Kristian Added in Question (: – Daniel Nov 23 '20 at 12:16