C
C#•4d ago
Juicy

DbConcurrencyException, what am I missing?

Hi, getting this on the SaveChangesAsync and im not really sure what im doing wrong. DbConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. Inside UserService.cs:
public async Task SetVariable(Guid productId, Guid userId, string requestKey, string requestValue)
{
var product = await productRepository.GetByIdAsync(productId);
if (product is null)
{
throw new ProductNotFoundException(productId);
}

var user = await userRepository.GetByIdAsync(productId, userId);
if (user is null)
{
throw new UserNotFoundException(userId);
}

user.SetVariable(requestKey, requestValue);
userRepository.Update(user);
await unitOfWork.SaveChangesAsync();
}
public async Task SetVariable(Guid productId, Guid userId, string requestKey, string requestValue)
{
var product = await productRepository.GetByIdAsync(productId);
if (product is null)
{
throw new ProductNotFoundException(productId);
}

var user = await userRepository.GetByIdAsync(productId, userId);
if (user is null)
{
throw new UserNotFoundException(userId);
}

user.SetVariable(requestKey, requestValue);
userRepository.Update(user);
await unitOfWork.SaveChangesAsync();
}
Inside User.cs:
public ICollection<UserVariable> Variables { get; set; }

public void SetVariable(string key, string value)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);

var existingVariable = Variables.FirstOrDefault(v => v.Key.Equals(key, StringComparison.OrdinalIgnoreCase));

if (existingVariable != null)
{
existingVariable.UpdateValue(value);
}
else
{
var newVariable = UserVariable.Create(Guid.NewGuid(), Id, key, value);
Variables.Add(newVariable);
}
}
public ICollection<UserVariable> Variables { get; set; }

public void SetVariable(string key, string value)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);

var existingVariable = Variables.FirstOrDefault(v => v.Key.Equals(key, StringComparison.OrdinalIgnoreCase));

if (existingVariable != null)
{
existingVariable.UpdateValue(value);
}
else
{
var newVariable = UserVariable.Create(Guid.NewGuid(), Id, key, value);
Variables.Add(newVariable);
}
}
Inside UserRepository.cs:
public void Update(User user)
{
dbContext.Set<User>().Update(user);
}
public void Update(User user)
{
dbContext.Set<User>().Update(user);
}
32 Replies
Juicy
JuicyOP•4d ago
Also to mention, I do include Variables when getting the user and I also tried without the explicit Update call
Angius
Angius•4d ago
I generally avoid using .Update() And using repositories, but that's another matter See if using .ExecuteUpdateAsync() would fix the issue
Jimmacle
Jimmacle•4d ago
ah, the joys of wrapping EF in generic repositories
Juicy
JuicyOP•4d ago
😭
Jimmacle
Jimmacle•4d ago
which you shouldn't if you have a choice
Juicy
JuicyOP•4d ago
Why do "best practices" youtube channels teach this stuff
Jimmacle
Jimmacle•4d ago
¯\_(ツ)_/¯ lots of useless content out there
Juicy
JuicyOP•4d ago
anyway
dbContext.Set<User>().ExecuteUpdateAsync(
u => u.SetProperty(x => x.Variables, user.Variables));
dbContext.Set<User>().ExecuteUpdateAsync(
u => u.SetProperty(x => x.Variables, user.Variables));
is this how you use it? It doesnt seem to work
Jimmacle
Jimmacle•4d ago
that would set those variables on every single user in the table you didn't filter it beforehand
Juicy
JuicyOP•4d ago
ah
Jimmacle
Jimmacle•4d ago
i'm not actually sure how executeupdate works with navigation properties, i haven't tried it
Juicy
JuicyOP•4d ago
is there another possible way I could fix the issue?
Jimmacle
Jimmacle•4d ago
as far as options go, you can load, modify, then save the user and let the change tracker deal with it if you can tolerate the performance penalty of 2 round trips
Juicy
JuicyOP•4d ago
yeah this isnt very performance dependant thats kinda what I tried to do I guess loading the user, modifying it's Variables collection and then trying to save the user
Jimmacle
Jimmacle•4d ago
you don't need to use Update if the object is already being tracked by the dbcontext
Juicy
JuicyOP•4d ago
yeah that makes sense but UserVariable has its own table as well
Jimmacle
Jimmacle•4d ago
that's fine, the change tracker can deal with that
Juicy
JuicyOP•4d ago
But even without the Update its giving me the DbUpdateConcurrencyException It feels like it should work..?
[16:18:59 INF] Executed DbCommand (1ms) [Parameters=[@p3='?' (DbType = Guid), @p0='?' (Size = 64), @p1='?' (DbType = Guid), @p2='?' (Size = 512)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
UPDATE [UserVariables] SET [Key] = @p0, [UserId] = @p1, [Value] = @p2
OUTPUT 1
WHERE [Id] = @p3;
[16:18:59 INF] Executed DbCommand (1ms) [Parameters=[@p3='?' (DbType = Guid), @p0='?' (Size = 64), @p1='?' (DbType = Guid), @p2='?' (Size = 512)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
UPDATE [UserVariables] SET [Key] = @p0, [UserId] = @p1, [Value] = @p2
OUTPUT 1
WHERE [Id] = @p3;
Jimmacle
Jimmacle•4d ago
are you somehow changing the ID of the user (or variable) either in the entity or in the db in the middle of this happening?
Juicy
JuicyOP•4d ago
yes the variable is recieving a new ID
var newVariable = UserVariable.Create(Guid.NewGuid(), Id, key, value);
var newVariable = UserVariable.Create(Guid.NewGuid(), Id, key, value);
Jimmacle
Jimmacle•4d ago
so you need to insert, not update
Juicy
JuicyOP•4d ago
using System.ComponentModel.DataAnnotations;

namespace Lightning.Domain.Models;

public sealed class UserVariable : BaseEntity
{
private const int MaxKeyLength = 64;
private const int MaxValueLength = 256;
private UserVariable(Guid id, Guid userId, string key, string? value) : base(id)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key, nameof(key));
if (key.Length > MaxKeyLength)
{
throw new ArgumentException($"Key length exceeds {MaxKeyLength} characters.", nameof(key));
}
if (value?.Length > MaxValueLength)
{
throw new ArgumentException($"Value length exceeds {MaxValueLength} characters.", nameof(value));
}

UserId = userId;
Key = key;
Value = value;
}


[MaxLength(MaxKeyLength)] public string Key { get; private set; }
[MaxLength(MaxValueLength)] public string? Value { get; private set; }

public Guid UserId { get; private set; }
public User User { get; private set; } = null!;

public static UserVariable Create(Guid id, Guid userId, string key, string? value)
{
return new UserVariable(id, userId, key, value);
}

public void UpdateValue(string? newValue)
{
if (newValue?.Length > MaxValueLength)
{
throw new ArgumentException($"Value length exceeds {MaxValueLength} characters.", nameof(newValue));
}
Value = newValue;
}
}
using System.ComponentModel.DataAnnotations;

namespace Lightning.Domain.Models;

public sealed class UserVariable : BaseEntity
{
private const int MaxKeyLength = 64;
private const int MaxValueLength = 256;
private UserVariable(Guid id, Guid userId, string key, string? value) : base(id)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key, nameof(key));
if (key.Length > MaxKeyLength)
{
throw new ArgumentException($"Key length exceeds {MaxKeyLength} characters.", nameof(key));
}
if (value?.Length > MaxValueLength)
{
throw new ArgumentException($"Value length exceeds {MaxValueLength} characters.", nameof(value));
}

UserId = userId;
Key = key;
Value = value;
}


[MaxLength(MaxKeyLength)] public string Key { get; private set; }
[MaxLength(MaxValueLength)] public string? Value { get; private set; }

public Guid UserId { get; private set; }
public User User { get; private set; } = null!;

public static UserVariable Create(Guid id, Guid userId, string key, string? value)
{
return new UserVariable(id, userId, key, value);
}

public void UpdateValue(string? newValue)
{
if (newValue?.Length > MaxValueLength)
{
throw new ArgumentException($"Value length exceeds {MaxValueLength} characters.", nameof(newValue));
}
Value = newValue;
}
}
Jimmacle
Jimmacle•4d ago
if you change the ID then WHERE [Id] = @p3; is never going to find the old one
Juicy
JuicyOP•4d ago
ah but thats weird so I need to insert the userVariable to it's table before I add it to the user collection
Jimmacle
Jimmacle•4d ago
no, it basically means you're doing something in your EF model that is leading it to generate the wrong SQL for what actually needs to happen
Juicy
JuicyOP•4d ago
i see I honestly have no clue what it could be In the other cases I have repositories for every model that im working with so I just add it via reposistory before adding it to another model's collection but in this case I thought it would be unnecessary how can I check if its handled by change tracker btw?
Jimmacle
Jimmacle•4d ago
if you got the object from a query on the same dbcontext (that didn't use AsNoTracking), explicitly added it with Add, or is a member of an object that meets those conditions, it's handled by the change tracker
Juicy
JuicyOP•4d ago
ah yeah I just get it like this
public async Task<User?> GetByIdAsync(Guid productId, Guid id, CancellationToken cancellationToken = default)
{
return await dbContext.Set<User>()
.Include(u => u.Licenses)
.Include(u => u.Machine)
.Include(u => u.Variables)
.FirstOrDefaultAsync(u => u.ProductId == productId && u.Id == id, cancellationToken);
}
public async Task<User?> GetByIdAsync(Guid productId, Guid id, CancellationToken cancellationToken = default)
{
return await dbContext.Set<User>()
.Include(u => u.Licenses)
.Include(u => u.Machine)
.Include(u => u.Variables)
.FirstOrDefaultAsync(u => u.ProductId == productId && u.Id == id, cancellationToken);
}
Jimmacle
Jimmacle•4d ago
then all you should have to do is add to the user's variables collection and call savechanges, you don't even need to explicitly set the reference to the user
Juicy
JuicyOP•4d ago
damn
Jimmacle
Jimmacle•4d ago
the change tracker will figure that out if it sees a new item in the user's variables collection
Juicy
JuicyOP•4d ago
could it be the way i configured the table? i have no clue how to find the issue 😭 I tried to drop the database and have EF core re create it but still same Well I found the cause Value generation was turned on even though I set the Id property manually on creation so I had to add a .ValueGeneratedNever() alternatively had to manually insert by calling DbContext.Add but since I handle the id creations myself I just set the .ValueGeneratedNever and now it seems to work fine

Did you find this page helpful?