c# - EF Core - Owned type is null even though the change tracker has its entry loaded - Stack Overflow

I'm trying to configure an aggregate root User that have a nullable value object ChangePasswordCod

I'm trying to configure an aggregate root User that have a nullable value object ChangePasswordCode. When I call _dbContext.Users.FirstOrDefaultAsync in the repository, I receive a User object with a null ChangePasswordCode even though the data exists in the database.

Here are the codes:

public class User : AggregateRoot<UserId>
{
    private readonly List<Ban> _bans = [];

    public Name Name { get; private set; }

    public Email Email { get; private set; }

    public Password Password { get; private set; }

    public DepartmentId DepartmentId { get; private set; }

    public Roles Roles { get; private set; }

    public IReadOnlyList<Ban> Bans => _bans.AsReadOnly();

    public ChangePasswordCode? ChangePasswordCode { get; private set; }

#pragma warning disable CS8618
    private User() { }
#pragma warning restore CS8618

    private User(
        UserId id,
        Name name,
        Email email,
        Password password,
        DepartmentId departmentId,
        Roles roles) : base(id)
    {
        Name = name;
        Email = email;
        Password = password;
        DepartmentId = departmentId;
        Roles = roles;
    }
    ...
}
public class ChangePasswordCode : ValueObject
{
    public string Value { get; private set; }

    public DateTime ExpiryTime { get; private set; }

    public ChangePasswordCode(string value, DateTime expiryTime)
    {
        Value = value;
        ExpiryTime = expiryTime;
    }
    ...
}
internal class UserConfigurations : IEntityTypeConfiguration<User>
{
    ...
    private void ConfigureUsersTable(EntityTypeBuilder<User> builder)
    {
        ...

        builder.OwnsOne(u => u.ChangePasswordCode);
        ...

Here is the DbContextSnapshop:

b.OwnsOne("Domain.UserAggregate.ValueObjects.ChangePasswordCode", "ChangePasswordCode", b1 =>
    {
        b1.Property<Guid>("UserId")
            .HasColumnType("uuid");

        b1.Property<DateTime>("ExpiryTime")
            .HasColumnType("timestamp with time zone");

        b1.Property<string>("Value")
            .IsRequired()
            .HasColumnType("text");

        b1.HasKey("UserId");

        b1.ToTable("Users");

        b1.WithOwner()
            .HasForeignKey("UserId");
    });

b.Navigation("ChangePasswordCode");

Here is where the issue happens:

internal class UserRepository : IUserRepository
{
    private readonly IdearDbContext _context;

    public UserRepository(IdearDbContext context)
    {
        _context = context;
    }

    public async Task<User?> GetByIdAsync(UserId id, CancellationToken cancellationToken = default)
    {
        var user = await _context.Users
            .FirstOrDefaultAsync(u => u.Id == id, cancellationToken);

        return user; // user.ChangePasswordCode is null here
    }

I have tried checking the change tracker debug view to see if the data is actually retrieved correctly and the debug view is:

User {Id: Domain.UserAggregate.ValueObjects.UserId} Unchanged
    Id: 'Domain.UserAggregate.ValueObjects.UserId' PK
    DepartmentId: 'Domain.DepartmentAggregate.ValueObjects.DepartmentId' FK
    Email: 'Domain.UserAggregate.ValueObjects.Email'
    Name: 'Domain.Shared.ValueObjects.Name'
    Password: 'Domain.UserAggregate.ValueObjects.Password'
    Roles: 'Admin'
  Bans: []
  ChangePasswordCode: <null>
ChangePasswordCode {UserId: Domain.UserAggregate.ValueObjects.UserId} Unchanged
    UserId: 'Domain.UserAggregate.ValueObjects.UserId' PK FK
    ExpiryTime: '11/20/2024 9:31:31 AM'
    Value: 'B20BB3'

It's weird that the ChangePasswordCode is actually loaded and tracked, but for some reason, it's not attached to the User entry.

I have found a similar question here, and tried out the suggestion but it's still null. I have also tried switching to non-nullable ChangePasswordCode property, it does make the field required in the database, but it's still null when mapped to User object.

I'm trying to configure an aggregate root User that have a nullable value object ChangePasswordCode. When I call _dbContext.Users.FirstOrDefaultAsync in the repository, I receive a User object with a null ChangePasswordCode even though the data exists in the database.

Here are the codes:

public class User : AggregateRoot<UserId>
{
    private readonly List<Ban> _bans = [];

    public Name Name { get; private set; }

    public Email Email { get; private set; }

    public Password Password { get; private set; }

    public DepartmentId DepartmentId { get; private set; }

    public Roles Roles { get; private set; }

    public IReadOnlyList<Ban> Bans => _bans.AsReadOnly();

    public ChangePasswordCode? ChangePasswordCode { get; private set; }

#pragma warning disable CS8618
    private User() { }
#pragma warning restore CS8618

    private User(
        UserId id,
        Name name,
        Email email,
        Password password,
        DepartmentId departmentId,
        Roles roles) : base(id)
    {
        Name = name;
        Email = email;
        Password = password;
        DepartmentId = departmentId;
        Roles = roles;
    }
    ...
}
public class ChangePasswordCode : ValueObject
{
    public string Value { get; private set; }

    public DateTime ExpiryTime { get; private set; }

    public ChangePasswordCode(string value, DateTime expiryTime)
    {
        Value = value;
        ExpiryTime = expiryTime;
    }
    ...
}
internal class UserConfigurations : IEntityTypeConfiguration<User>
{
    ...
    private void ConfigureUsersTable(EntityTypeBuilder<User> builder)
    {
        ...

        builder.OwnsOne(u => u.ChangePasswordCode);
        ...

Here is the DbContextSnapshop:

b.OwnsOne("Domain.UserAggregate.ValueObjects.ChangePasswordCode", "ChangePasswordCode", b1 =>
    {
        b1.Property<Guid>("UserId")
            .HasColumnType("uuid");

        b1.Property<DateTime>("ExpiryTime")
            .HasColumnType("timestamp with time zone");

        b1.Property<string>("Value")
            .IsRequired()
            .HasColumnType("text");

        b1.HasKey("UserId");

        b1.ToTable("Users");

        b1.WithOwner()
            .HasForeignKey("UserId");
    });

b.Navigation("ChangePasswordCode");

Here is where the issue happens:

internal class UserRepository : IUserRepository
{
    private readonly IdearDbContext _context;

    public UserRepository(IdearDbContext context)
    {
        _context = context;
    }

    public async Task<User?> GetByIdAsync(UserId id, CancellationToken cancellationToken = default)
    {
        var user = await _context.Users
            .FirstOrDefaultAsync(u => u.Id == id, cancellationToken);

        return user; // user.ChangePasswordCode is null here
    }

I have tried checking the change tracker debug view to see if the data is actually retrieved correctly and the debug view is:

User {Id: Domain.UserAggregate.ValueObjects.UserId} Unchanged
    Id: 'Domain.UserAggregate.ValueObjects.UserId' PK
    DepartmentId: 'Domain.DepartmentAggregate.ValueObjects.DepartmentId' FK
    Email: 'Domain.UserAggregate.ValueObjects.Email'
    Name: 'Domain.Shared.ValueObjects.Name'
    Password: 'Domain.UserAggregate.ValueObjects.Password'
    Roles: 'Admin'
  Bans: []
  ChangePasswordCode: <null>
ChangePasswordCode {UserId: Domain.UserAggregate.ValueObjects.UserId} Unchanged
    UserId: 'Domain.UserAggregate.ValueObjects.UserId' PK FK
    ExpiryTime: '11/20/2024 9:31:31 AM'
    Value: 'B20BB3'

It's weird that the ChangePasswordCode is actually loaded and tracked, but for some reason, it's not attached to the User entry.

I have found a similar question here, and tried out the suggestion but it's still null. I have also tried switching to non-nullable ChangePasswordCode property, it does make the field required in the database, but it's still null when mapped to User object.

Share Improve this question asked Nov 20, 2024 at 15:33 Huynh Loc LeHuynh Loc Le 12 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

TL;DR - I implement the GetHashCode method wrong for ValueObject.

After creating a different project to test out the User and ChangePasswordCode, I find out that the problem lies in the UserId value object as it works fine when I use Guid as the primary key. At that time, I suspect that the change tracker may compare the 2 UserId in User and ChangePasswordCode entries and somehow doesn't match the 2. So the problem could be in the Equals and GetHashCode method that I implement for ValueObject.

By placing breakpoints in debug mode, I find out that the change tracker indeed calls the GetHashCode first before the Equals and it returns 2 different hash codes for the same UserId. I fix the GetHashCode method and it works now.

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1742348720a4427074.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信