Gicu Mironica
Gicu Mironica

Reputation: 635

EF Core - Concurrent saving of the same entity creates multiple

The Problem
My concept is that when a user creates a post with some tags, the server first checks if the tag name already exists, if it does, it's counter is incremented, otherwise a new tag is created.

The problem comes when multiple users create a post at the same time, with a new tag, lets say new_tag, then multiple tags of the same name get persisted in the database instead of 1 tag with counter = # of users who used this tag

As you can see for each user, a new tag record is created in the database:

--------------------------------
|  id  |  tagName  |  counter  |
|------|-----------|-----------|
|   1  |  new_tag  |    1      |
|   2  |  new_tag  |    1      |
|   3  |  new_tag  |    1      |
|   4  |  new_tag  |    1      |
--------------------------------

What I expect:

--------------------------------
|  id  |  tagName  |  counter  |
|------|-----------|-----------|
|   1  |  new_tag  |    4      |
--------------------------------

This code shows how I implemented the persistence:


PostRepository

public async Task<bool> AddAsync(Post entity)
        {
            await AddNewTagsAsync(entity);
            _context.Attach(entity.Event);
            await _context.AddAsync(entity);
            await _context.Database.BeginTransactionAsync();
                var result = await _context.SaveChangesAsync();
            _context.Database.CommitTransaction();
                return result > 0;
        }

 public async Task AddNewTagsAsync(Post post)
        {
            // store tags name in lower case
            if ((post.PostTags == null) || (post.PostTags.Count==0))
                return;
            post.PostTags.ForEach(pt => pt.Tag.TagName = pt.Tag.TagName.ToLower());

            for(var i =0; i<post.PostTags.Count; i++)
            {
                var postTag = post.PostTags[i];

                // here lays the main problem, when many concurrent users check for tag existence 
                // all get null and new tag will be created, workaround needed!
                var existingTag = await _context.Tags.SingleOrDefaultAsync(x => x.TagName == postTag.Tag.TagName);

                // if tag exists, increment counter
                if (existingTag != null)
                {
                    existingTag.Counter++;
                    postTag.Tag = existingTag;
                    continue;
                }

               // else the new Tag object will be peristed   
            }
        }

This is a piece of my ER Diagram:

This

Just to mention, it works as expected if one user creates the tag first, then others are just incrementing the counter and use the same tag

Upvotes: 3

Views: 637

Answers (1)

Brent Ellingson
Brent Ellingson

Reputation: 356

You're looking for an atomic UPSERT statement (a combined UPDATE or INSERT).

UPSERTS are not supported by EF Core. See: https://github.com/dotnet/efcore/issues/4526

However, if you're willing to forgo change tracking, you can directly make a SQL merge statement, like this:

    MERGE INTO dbo.Tags AS target  
        USING (VALUES ({TagName})) AS source (TagName)  
        ON target.TagName = source.TagName  
    WHEN MATCHED THEN  
        UPDATE SET Counter = Counter + 1  
    WHEN NOT MATCHED BY TARGET THEN  
        INSERT (TagName, Counter) VALUES (TagName, 1);

You might call it something like this:

public async Task AddNewTagsAsync(Post post)
{
    foreach (var tag in post.PostTags)
    {
        await _context.Database.ExececuteInterpolatedAsync($@"
            MERGE INTO dbo.Tags AS target  
                USING (VALUES ({tag.TagName})) AS source (TagName)  
                ON target.TagName = source.TagName  
            WHEN MATCHED THEN  
                UPDATE SET Counter = Counter + 1  
            WHEN NOT MATCHED BY TARGET THEN  
                INSERT (TagName, Counter) VALUES (TagName, 1)");
    } 
}

Upvotes: 1

Related Questions