Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuring facets of owned entity types are missing in migrations #9144

Closed
lurumad opened this issue Jul 11, 2017 · 1 comment
Closed

Configuring facets of owned entity types are missing in migrations #9144

lurumad opened this issue Jul 11, 2017 · 1 comment
Labels
closed-no-further-action The issue is closed and no further action is planned.

Comments

@lurumad
Copy link

lurumad commented Jul 11, 2017

Hi,

I'm playing with complex types in EF Core 2.0 Preview 2 trying to avoid use primitive obsession in my models. Below I show you the code:

class MyDbContext : DbContext
{
    public DbSet<Order> Orders { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=.;Database=EFCore2;User Id=sa;Password=Plainconcepts01!");

        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ZipCode>()
            .Property(c => c.Value).HasField("_value");

        modelBuilder.Entity<Order>()
            .OwnsOne(
                c => c.BillingAddress,
                cb => cb.OwnsOne(c => c.ZipCode));
        modelBuilder.Entity<Order>()
            .OwnsOne(
                c => c.ShippingAddress,
                cb => cb.OwnsOne(c => c.ZipCode));

        base.OnModelCreating(modelBuilder);
    }
}

class Order
{
    public int Id { get; set; }
    public StreetAddress BillingAddress { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

class StreetAddress
{
    public ZipCode ZipCode { get; set; }
}

class ZipCode : ValueObject<ZipCode>
{
    private int _value;
    public int Value => _value;

    public ZipCode(int value)
    {
        if (value < 1 || value > 52999)
        {
            throw new Exception("Invalid zipcode");
        }
        _value = value;
    }

    public static implicit operator ZipCode(int value)
    {
        return new ZipCode(value);
    }

    public static explicit operator int(ZipCode zipCode)
    {
        return zipCode._value;
    }
}

abstract class ValueObject<T> : IEquatable<T> where T : ValueObject<T>
{
    protected bool Equals(ValueObject<T> other)
    {
        var type = GetType();
        var otherType = other.GetType();
        var fields = GetFields();
        foreach (var field in fields)
        {
            var value1 = field.GetValue(type);
            var value2 = field.GetValue(otherType);

            if (value1 == null)
            {
                if (value2 != null)
                {
                    return false;
                }
            }
            else if (!value1.Equals(value2))
            {
                return false;
            }
        }
        return true;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((ValueObject<T>) obj);
    }

    public override int GetHashCode()
    {
        var fields = GetFields();
        var hash = 12;
        foreach (var field in fields)
        {
            var value = field.GetValue(this);
            if (value != null)
            {
                hash += hash * 7 + value.GetHashCode();
            }
        }
        return hash;
    }

    private IEnumerable<FieldInfo> GetFields()
    {
        var type = GetType();
        var fields = new List<FieldInfo>();

        while (type != typeof(object))
        {
            fields.AddRange(type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public));
            type = type.BaseType;
        }

        return fields;
    }

    public bool Equals(T other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        if (other.GetType() != GetType()) return false;
        return Equals((ValueObject<T>)other);
    }
}

When I try to create an initial migration, some weird is happen because my address and zipcodes is nor present in the migration:

public partial class Initial : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Orders",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Orders", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Orders");
    }
}

If I change the ZipCode entity to use a public property instead of a backing field (For me the perfect approach should be a private property without expose it), EF creates the migration but if a set some value to the property _value it's not storing in db:

public partial class Initial : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Orders",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                BillingAddress_ZipCode_Value = table.Column<int>(type: "int", nullable: false),
                ShippingAddress_ZipCode_Value = table.Column<int>(type: "int", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Orders", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Orders");
    }
}

It's possible to achieve this? What I'm doing wrong?

Regards!

@smitpatel
Copy link
Contributor

The way you are configuring ZipCode as entity type first and then owned entity type, it should throw exception (tracked in #9148).

With that issue in mind, first you are adding EntityType ZipCode and configuring Value to use backing field. When you use OwnsOne with ZipCode it will remove the EntityType and add OwnedEntityType. This causes loss of all information on EntityType. So whatever configuration you have done on ZipCode are gone. Therefore when you use OwnsOne, Value is not added to the class (non-settable property) hence not generated in migrations.
Since each owned entity is independent even though having same CLR type, you can & need to configure each of them separately. (#9117 allows a way to deal with all owned entity types together. In future when it is implemented, user can use it to configure all facets of given owned type by filtering on CLR type)
Correct fluent API configuration would be

modelBuilder.Entity<Order>()
    .OwnsOne(
        c => c.BillingAddress,
        cb => cb.OwnsOne(
            c => c.ZipCode,
            z => z.Property(c => c.Value).HasField("_value")));
modelBuilder.Entity<Order>()
    .OwnsOne(
        c => c.ShippingAddress,
        cb => cb.OwnsOne(
            c => c.ZipCode,
            z => z.Property(c => c.Value).HasField("_value")));

Which generates migration as expected

public partial class Init : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Orders",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                BillingAddress_ZipCode_Value = table.Column<int>(type: "int", nullable: false),
                ShippingAddress_ZipCode_Value = table.Column<int>(type: "int", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Orders", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Orders");
    }
}

@smitpatel smitpatel added the closed-no-further-action The issue is closed and no further action is planned. label Jul 11, 2017
@smitpatel smitpatel changed the title Owned entity types (Value objects) Configuring facets of owned entity types are missing in migrations Jul 11, 2017
@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-no-further-action The issue is closed and no further action is planned.
Projects
None yet
Development

No branches or pull requests

3 participants