entity-framework-coreentity-framework-core-3.1

EF Core ignores owned entity column in entity update when the entity primary key has value conversion


I have my entity and owned entity configured with complex primary key using DDD approach as follows,

public class Key : ValueObject
{
    public string Value { get; }

    protected Key()
    {
    }

    private Key(string id) : this()
    {
        Value = id;
    }

    public static Result<Key> Create(string id)
    {
        return Result.Success(new Key(id));
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }

    public static implicit operator string(Key id)
    {
        return id.Value;
    }
}

public class FolderId : ValueObject
{
    public int Value { get; }

    protected FolderId()
    {
    }

    private FolderId(int value) : this()
    {
        Value = value;
    }

    public static Result<FolderId> Create(int id)
    {
        return Result.Success(new FolderId(id));
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }

    public static implicit operator int(FolderId id)
    {
        return id.Value;
    }
}

public class TextPositionY : ValueObject
{
    public int Value { get; }
    public double ConvertedValue { get; }

    protected TextPositionY()
    {
    }

    private TextPositionY(int value) : this()
    {
        Value = value;
    }

    private TextPositionY(double value) : this()
    {
        ConvertedValue = value;
    }

    public static Result<TextPositionY> Create(int position)
    {
        return Result.Success(new TextPositionY(position));
    }

    public static Result<TextPositionY> CreateConverted(int position)
    {
        return Result.Success(new TextPositionY((double)position));
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }

    public static implicit operator int(TextPositionY id)
    {
        return id.Value;
    }
}

public class PicturePositionX : ValueObject
{
    public int Value { get; }

    protected PicturePositionX()
    {
    }

    private PicturePositionX(int value) : this()
    {
        Value = value;
    }

    public static Result<PicturePositionX> Create(int position)
    {
        return Result.Success(new PicturePositionX(position));
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }

    public static implicit operator int(PicturePositionX id)
    {
        return id.Value;
    }
}

public class BaseEntity
{
    protected BaseEntity()
    {
    }

    public BaseEntity(Key key, FolderId folderId, TextEntity text, PictureEntity picture) : this()
    {
        Name = key;
        FolderId = folderId;
        Text = text;
        Picture = picture;
    }

    public Key Name { get; set; }
    public FolderId FolderId { get; private set; }
    public TextEntity Text { get; set; }
    public PictureEntity Picture { get; set; }

    public void Update(FolderId folderId, TextEntity text, PictureEntity picture)
    {
        FolderId = folderId;
        Text = text;
        Picture = picture;
        //Text.Update(text.PositionY);
        //Picture.Update(picture.PositionX);
    }
}

public class TextEntity
{
    protected TextEntity()
    {
    }

    public TextEntity(TextPositionY positionY) : this()
    {
        PositionY = positionY;
    }

    public TextPositionY PositionY { get; private set; }

    public void Update(TextPositionY positionY)
    {
        PositionY = positionY;
    }

}

public class PictureEntity
{
    protected PictureEntity()
    {
    }

    public PictureEntity(PicturePositionX positionX) : this()
    {
        PositionX = positionX;
    }

    public PicturePositionX PositionX { get; private set; }

    public void Update(PicturePositionX positionX)
    {
        PositionX = positionX;
    }

}

Here is my DBContext,

public class BlogContext : DbContext
{
    public DbSet<BaseEntity> OwnedEntityTest { get; set; }

    static ILoggerFactory ContextLoggerFactory
    => LoggerFactory.Create(b => b.AddConsole().AddFilter("", LogLevel.Information));

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer("Server=.\\;Database=PracticeDB;Trusted_Connection=true;")
            .EnableSensitiveDataLogging()
            .UseLoggerFactory(ContextLoggerFactory);

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<BaseEntity>(entity =>
        {
            entity.ToTable("OwnedEntityTest");

            entity.HasKey(e => e.Name);

            entity.Property(p => p.Name)
                .HasColumnName("Id")
                .HasConversion(p => p.Value, p => Key.Create(p).Value);

            entity.Property(p => p.FolderId)
                .HasConversion(p => p.Value, p => FolderId.Create(p).Value);

            entity.OwnsOne(p => p.Picture, p =>
            {
                p.WithOwner();

                p.Property(pp => pp.PositionX)
                    .HasColumnName("PicturePositionX")
                    .HasConversion(p => p.Value, p => PicturePositionX.Create(p).Value);
            });

            entity.OwnsOne(p => p.Text, p =>
            {
                p.WithOwner();

                p.Property(pp => pp.PositionY)
                    .HasColumnName("TextPositionY")
                    .HasConversion(p => p.Value, p => TextPositionY.CreateConverted(p).Value);
            });
        });
    }
}

when I try to update the entity as shown below, update happens only on entity column but not on owned entity column.

class Program
{
    static async Task Main(string[] args)
    {
        using var ctx = new BlogContext();
        //ctx.Database.EnsureDeleted();
        //ctx.Database.EnsureCreated();

        if (!ctx.OwnedEntityTest.Any())
        {
            var folderId = FolderId.Create(1);
            var picturePositionX = PicturePositionX.Create(1);
            var textPositionY = TextPositionY.Create(1);
            var key = Key.Create("1-1-1");

            var text = new TextEntity(textPositionY.Value);
            var picture = new PictureEntity(picturePositionX.Value);

            var ownedEntity = new BaseEntity(key.Value, folderId.Value, text, picture);

            ctx.OwnedEntityTest.Add(ownedEntity);
            await ctx.SaveChangesAsync();
        }
        else
        {
            var key = Key.Create("1-1-1");
            var ownedEntity = await ctx.OwnedEntityTest.FindAsync(key.Value);

            var updatedFolderId = FolderId.Create(1);
            var updatedPicturePositionX = PicturePositionX.Create(1);
            var updatedTextPositionY = TextPositionY.Create(0);

            var updatedText = new TextEntity(updatedTextPositionY.Value);
            var updatedPicture = new PictureEntity(updatedPicturePositionX.Value);

            ownedEntity.Update(updatedFolderId.Value, updatedText, updatedPicture);

            ctx.Set<BaseEntity>().Update(ownedEntity);
            await ctx.SaveChangesAsync();
        }
    }
}

Here is the query generated by EF Core:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (61ms) [Parameters=[@__p_0='1-1-1' (Size = 450)], CommandType='Text', CommandTimeout='30']
      SELECT TOP(1) [o].[Id], [o].[FolderId], [t].[Id], [t].[PicturePositionX], [t0].[Id], [t0].[TextPositionY]
      FROM [OwnedEntityTest] AS [o]
      LEFT JOIN (
          SELECT [o0].[Id], [o0].[PicturePositionX]
          FROM [OwnedEntityTest] AS [o0]
          WHERE [o0].[PicturePositionX] IS NOT NULL
      ) AS [t] ON [o].[Id] = [t].[Id]
      LEFT JOIN (
          SELECT [o1].[Id], [o1].[TextPositionY]
          FROM [OwnedEntityTest] AS [o1]
          WHERE [o1].[TextPositionY] IS NOT NULL
      ) AS [t0] ON [o].[Id] = [t0].[Id]
      WHERE [o].[Id] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[@p1='1-1-1' (Nullable = false) (Size = 450), @p0='1' (Nullable = true)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [OwnedEntityTest] SET [FolderId] = @p0
      WHERE [Id] = @p1;
      SELECT @@ROWCOUNT;

Table Schema:

CREATE TABLE [dbo].[OwnedEntityTest](
    [Id] [nvarchar](50) NOT NULL,
    [FolderId] [int] NOT NULL,
    [PicturePositionX] [int] NULL,
    [TextPositionY] [int] NULL,
 CONSTRAINT [PK_OwnedEntityTest] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

EF Core ignores the column in update. Please assist on what I'm missing.

EF Core Details

EF Core version: 3.1.9

Database provider: Microsoft.EntityFrameworkCore.SqlServer 3.1.9

Target framework: .NET CORE 3.1

Operating system: Windows 10

IDE: Visual Studio 2019 16.8


Solution

  • This no longer happens from EF Core 6. Here is the GitHub issue link for the same.