Getting error related to entity configuration when configuring a ValueObject in EF Core

68 views Asked by At

When I am trying to add a migration, I get the following error:

Unable to determine the relationship represented by navigation 'User.Email' of type 'Email'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

I don't want to use the [NotMapped] attribute, because I would like to create a column in my Users table. The existing answers I found on the internet were connected to many-many relationship or having collections of entities. I would like to configure the Email as a 'single' owned entity by the user, as far as I know using OwnsOne is the way to configure a value object.

Before the switching the Email to a value object, the add migration worked. Thank you for the help and let me know if you need more information.

The User class:

public class User : AggregateRoot, IAuditableEntity
{
    private User(Guid id, Email email, UserName userName) : base(id)
    {
        Email = email;
        UserName = userName;
    }

    private User()
    {
    }

    public ICollection<RolePermission> RolePermissions { get; set; }

    public UserName UserName { get; private set; }
    public Email Email { get; private set; }
    public DateTime CreatedOnUtc { get; set; }
    public DateTime? ModifiedOnUtc { get; set; }

    public static User Create(Guid id, Email email, UserName userName)
    {
        var user = new User(id, email, userName);
        return user;
    }

    public void ChangeName(UserName userName)
    {
        UserName = userName;
    }
}

The Email class (which is a value object):

public sealed class Email : ValueObject
{
    public const int MaxLength = 255;

    private Email(string value) => Value = value;

    private Email()
    {  
    }

    public static Result<Email> Create(string email) =>
        Result.Create(email)
            .Ensure(
                e => !string.IsNullOrWhiteSpace(e),
                DomainErrors.Email.Empty)
            .Ensure(
                e => e.Length <= MaxLength,
                DomainErrors.Email.TooLong)
            .Ensure(
                e => e.Split('@').Length == 2,
                DomainErrors.Email.InvalidFormat)
            .Map(e => new Email(e));

    public string Value { get; private set; }

    public override IEnumerable<object> GetAtomicValues()
    {
        yield return Value;
    }
}

The DbContext:

public class ApplicationDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Permission> Role { get; set; }

    public DbSet<Role> Roles { get; set; }
    public ApplicationDbContext(DbContextOptions options)
    : base(options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(DbContext).Assembly);
    }
}

And finally the UserConfiguration:

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.ToTable(TableNames.Users);
        builder.HasKey(x => x.Id);
        builder.OwnsOne(x => x.Email);

        builder
            .Property(x => x.Email)
            .HasConversion(x => x.Value, v => Email.Create(v).Value);

        builder
            .Property(x => x.UserName)
            .HasConversion(x => x.Value, v => UserName.Create(v).Value)
            .HasMaxLength(100);

        //builder.HasIndex(c => c.Email).IsUnique();
    }
}

I tried to use [ComplexType] for the Email entity, it didn't make the migration work.

2

There are 2 answers

2
TTomi On

It turned out that for some reason none of the configurations were applied when I called the ApplyConfigurationsFromAssembly method providing the assembly containing the dbcontext. So the the reason for this is still not clear. After applying the configurations one by one, it fixed the issue:

public class ApplicationDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Permission> Role { get; set; }

    public DbSet<Role> Roles { get; set; }
    public ApplicationDbContext(DbContextOptions options)
    : base(options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new UserConfiguration());
        modelBuilder.ApplyConfiguration(new RoleConfiguration());
        modelBuilder.ApplyConfiguration(new RolePermissionConfiguration());
    }
}
1
Panagiotis Kanavos On

Assuming there's no typo, the problem is :

modelBuilder.ApplyConfigurationsFromAssembly(typeof(DbContext).Assembly);

This is telling EF to look for application type configurations in the assembly where the base class DbContext is defined, Microsoft.EntityFrameworkCore.dll.

At the very least, the code should be :

modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext ).Assembly);

or, better yet :

modelBuilder.ApplyConfigurationsFromAssembly(typeof(UserConfiguration ).Assembly);

The second option would work even if the context and configurations were defined in different assemblies