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
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;
}
}
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.