Search code examples
c#authenticationentity-framework-corerepository-patternunit-of-work

How to update database of parent class and [Owned] attribute class type in Entity Framework Core 8.0.8


How to save the [Owned] attribute class data from parent class into the database.

This is the parent class:

[Index(nameof(EmailId),IsUnique =true)]
public class User : Entity, IUser
{
   [Required(ErrorMessage ="First Name is required."), MaxLength(20),MinLength(1)]
   [DisplayName("First Name")]
   public string FirstName { get; set; } = string.Empty;

   [DisplayName("Last Name")]
   [Required(ErrorMessage ="Last Name is required."), MaxLength(20),MinLength(1)]
   public string LastName { get; set; } = string.Empty;

   [DisplayName("Email Address")]
   [Required(ErrorMessage = "Email Id is required"), MaxLength(50), MinLength(5)]
   [DataType(DataType.EmailAddress)]
   public string EmailId { get; set; } = string.Empty;

   [JsonIgnore]
   [DisplayName("Password"),MaxLength(200), MinLength(8),DataType(DataType.Password)]
   public string? Password { get; set; }    

   [JsonIgnore]
   public List<RefreshToken> RefreshTokens { get; set; }
}

And this is the child class RefreshToken structure

[Owned]
public class RefreshToken
{
   [Key]
   [JsonIgnore]
   public Guid Id { get; set; }= Guid.NewGuid();
   public string Token { get; set; }
   public DateTime Expires { get; set; }
   public DateTime Created { get; set; }
   public string CreatedByIp { get; set; }
   public DateTime? Revoked { get; set; }
   public string RevokedByIp { get;set; }
   public string ReplacedByToken { get; set; }
   public string ReasonRevoked { get; set; }
   public Boolean IsExpired => DateTime.UtcNow >= Expires;
   public bool IsRevoked => Revoked != null;
   public bool IsActive => !IsRevoked && !IsExpired;
}

Here is the code - I'm trying to save a parent with refresh token values:

_unitOfWork.BeginTransaction();

User? _user = GetByEmailId(model.EmailId).FirstOrDefault();

// return null if user not found
if (_user == null) 
    throw new AppException("Username or password is incorrect");

if (_user.Password != model.Password.Hash())
    throw new AppException("Username or password is incorrect");

// authentication successful so generate jwt token
var token = _jwtUtils.GenerateJwtToken(_user);
var _refreshtoken = _jwtUtils.GenerateRefreshToken(ipAddress);

_user.RefreshTokens.Add(_refreshtoken);

// remove old refresh token
removeOldRefreshTokens(_user);

apiResponse = _unitOfWork.Repository<User>().Update(_user);
_unitOfWork.SaveChange();
_unitOfWork.CommitTransaction();

While saving I'm getting an error:

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: the database operation was expected to affect 1 row(s), but actually affected 0 row(s);

Refresh token is added in the collection of user table but while saving the user table I'm getting an error

enter image description here

Below is the code for removeOldRefreshToken and Update for your reference:

private void removeOldRefreshTokens(User user)
{
    // remove old inactive refresh tokens from user based on TTL in app settings
    user.RefreshTokens.RemoveAll(x =>
   !x.IsActive &&
   x.Created.AddDays(_appSettings.RefreshTokenTTL) <= DateTime.UtcNow);

}

//this code is written in generic repository
public ApiResponse Update(TEntity entity)
{        
    try
    {
        if (entity == null) { throw new ArgumentNullException("entity is null"); }
        _dbContext.Entry(entity).State = EntityState.Modified;
    
        _entity.Update(entity);
        //await _dbContext.SaveChangesAsync();    
    
        apiResponse.message = MessageType.Successful;
        apiResponse.status=true;
        apiResponse.userData = entity;
        return apiResponse;
 }
   catch (Exception ex)
   {
       apiResponse.message=MessageType.Unsuccessful;
       apiResponse.status=false;
       return apiResponse;
  }

}


Solution

  • The reason for that is pretty simple actually.

    Generally, when EF tries to update data in table (insert or update), it looks at the Key property (in our case it's Id as usual). When it is present, EF assumes that it's need to be updated (it begins tracking it in Modified state). And so, it will send update query to DB.

    In our case, you have defined:

    [Key]
    [JsonIgnore]
    public Guid Id { get; set; } = Guid.NewGuid();
    

    Meaning, anytime you create new entity of the class, it will have Id automatically initialized.

    Then, EF will see that and will try to update the entity.

    In your case, you are creating new entities to INSERT to table. But since the Id is autogenerated on the code, EF will try to UPDATE the entity.

    In return, EF will get that "0 rows were affected", while EF expected to UPDATE and affect 1 entity. Thus the error.

    To fix that, before sending new entity to DB, you can set Id to Guid.Empty or remove Id initialization from the RefreshToken class.