0
\$\begingroup\$

I'm trying to "optimize" the command below. In other words, to make it suit my needs using best practices. I found a lot of posts regarding the "upsert" operation and there were a lot of solutions and in the end, I'm not sure which one is best.

The term upsert is a combination of the words “update” and “insert.” In the context of relational databases, an upsert is a database operation that will update an existing row if a specified value already exists in a table, and insert a new row if the specified value doesn't already exist.

There are only two tables (the database structure can be find below), and there are different event types for which we have these three booleans. Administrators can change these global notification settings on demand. If the record for a specific event type id doesn't exist, it should load it at runtime. Since these settings can be updated concurrently by many administrators at the same time, I think I should handle DbUpdateConcurrencyException https://github.com/petercwq/EFCorePractice/blob/master/EFCorePractice/DbContextConcurrencyExtension.cs. What do you think?

I'm not so good at EF Core, so I would like to know your opinion. I also had a look at https://stackoverflow.com/questions/5557829/update-row-if-it-exists-else-insert-logic-with-entity-framework and I feel like instead of using a stored procedure or an atomic transaction, I could use Update which cuts off the round trips to the database? I mean I wouldn't need to do .FindAsync and then insert/update.

This is something that should be done at the database engine level, in a stored procedure. Otherwise, you'll have to wrap the detect/update/insert in a transaction.

public sealed class UpsertGlobalNotificationSettingsCommand : IRequest
{
    public Guid EventTypeId { get; set; }
    public bool InternalNotificationsEnabled { get; set; }
    public bool EmailNotificationsEnabled { get; set; }
    public bool SmsNotificationsEnabled { get; set; }
}

public sealed class UpsertGlobalNotificationSettingsCommandHandler : IRequestHandler<UpsertGlobalNotificationSettingsCommand>
{
    private readonly AppDbContext _dbContext;

    public UpsertGlobalNotificationSettingsCommandHandler(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task Handle(UpsertGlobalNotificationSettingsCommand request, CancellationToken cancellationToken)
    {
        var existingSettings = await _dbContext.GlobalNotificationSettings.FindAsync(new object[] { request.EventTypeId }, cancellationToken);

        if (existingSettings == null)
        {
            var entity = new GlobalNotificationSettings
            {
                EventTypeId = request.EventTypeId,
                InternalNotificationsEnabled = request.InternalNotificationsEnabled,
                EmailNotificationsEnabled = request.EmailNotificationsEnabled,
                SmsNotificationsEnabled = request.SmsNotificationsEnabled
            };
            
            _dbContext.GlobalNotificationSettings.Add(entity);
        }
        else
        {
            existingSettings.InternalNotificationsEnabled = request.InternalNotificationsEnabled;
            existingSettings.EmailNotificationsEnabled = request.EmailNotificationsEnabled;
            existingSettings.SmsNotificationsEnabled = request.SmsNotificationsEnabled;
        }

        await _dbContext.SaveChangesAsync(cancellationToken);
    }
}

Database

public class EventType
{
    public Guid Id { get; set; }
    public string Name { get; set; } = default!;

    public ICollection<GlobalNotificationSettings> GlobalNotificationSettings { get; set; } = default!;
}

public class GlobalNotificationSettings
{
    public Guid EventTypeId { get; set; }
    public bool InternalNotificationsEnabled { get; set; }
    public bool EmailNotificationsEnabled { get; set; }
    public bool SmsNotificationsEnabled { get; set; }
    
    public EventType EventType { get; set; } = default!;
}

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

    public DbSet<EventType> EventTypes => Set<EventType>();
    public DbSet<GlobalNotificationSettings> GlobalNotificationSettings => Set<GlobalNotificationSettings>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<GlobalNotificationSettings>(entity =>
        {
            entity.HasKey(gns => gns.EventTypeId);

            entity.HasOne(et => et.EventType)
                .WithMany(gns => gns.GlobalNotificationSettings)
                .HasForeignKey(gns => gns.EventTypeId)
                .OnDelete(DeleteBehavior.Cascade);
        });
    }
}
```
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Since the existence check and the conditional insert/update happens in two different components that's why your system has a race condition. \$\endgroup\$ 17 hours ago
  • 1
    \$\begingroup\$ It might happen that the query returns nothing, so you decide to insert, but before you could submit the operation to the db, a concurrent request had been committed to the database so now your insert is not valid anymore. \$\endgroup\$ 16 hours ago

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.