Dmitriy
Dmitriy

Reputation: 939

Owned types problem in Entity Framework Core 3.0

I have strange behavior of EF Core 3.0 preview4. I have main class:

public enum ClientType
{
    Customer = 0,
    Produces = 1,
}

public class User : IdentityUser<Guid>
{
    public ClientType ClientType { get; set; }
    public SMSCodeInfo SMSCodeInfo { get; set; }
    public string Discriminator { get; set; }
}

In this code SMSCodeInfo is a class:

public class SMSCodeInfo
{ 
    public long Code { get; set; }
    public DateTime Expiration { get; set; }

    public SMSCodeInfo() { }
    public SMSCodeInfo(int days, int hours, int minutes) : this(DateTime.Now.AddDays(days).AddHours(hours).AddMinutes(minutes)) { }
    public SMSCodeInfo(DateTime Expiration)
    {
        this.Expiration = Expiration;
        Code = new Random().Next(100000, 999999);
    }        
}

I tried to add SMSCodeInfo as owned data into the User class by different methods: [Owned] attribute usage, adding code in OnModelCreating(ModelBuilder modelBuilder) method:

modelBuilder.Entity<User>().OwnsOne(o => o.SMSCodeInfo);

But each time I make a migration, I get 2 tables: AspNetUsers with an Identity information about users (I use Asp.net core identity in my project), and AspNetUsers1 table which consists of SMSCodeInfo members. EF Core generates this code for migration and I can't understand "why"!

        migrationBuilder.CreateTable(
            name: "AspNetUsers1",
            columns: table => new
            {
                UserId = table.Column<Guid>(nullable: false),
                Code = table.Column<long>(nullable: false),
                Expiration = table.Column<DateTime>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_AspNetUsers1", x => x.UserId);
                table.ForeignKey(
                    name: "FK_AspNetUsers1_AspNetUsers_UserId",
                    column: x => x.UserId,
                    principalTable: "AspNetUsers",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.Cascade);
            });

"Additional". I want to have owned data be added in the AspNetUser table, not in a separate table.

P.S. This problem arises for the classes, derived from IdentityUser<>. For other classes in my project code like this modelBuilder.Entity<TOwner>().OwnsOne(o => o.ToBeOwned); works correct and gives correct tables with injected owned types.

Upvotes: 3

Views: 4418

Answers (2)

Elmar
Elmar

Reputation: 332

Perhaps helpful for someone else too. Just landed on this page because of identical symptoms when using efcore v2.2.6. The microsoft documentation on the subject, suggests that adding the [Owned] attribute on the class should be sufficient to get "Table Sharing/Splitting" functionality out-of-the-box.

My setup:

[Owned]
public class Audit {
    public string By {get; set;}
    public DateTime At {get;set;}
}

public class Organisation {
    public Guid Id {get;set;}
    public Audit Created {get;set;}
    // other properties omitted 
}

My database schema

CREATE TABLE Organisations (
   Id char(36),   -- I'm using MySQL8
   Created_At DateTime NOT NULL,
   Created_By varchar(32) NOT NULL,
   PRIMARY KEY Id
)

When running my example I'm getting the following message:

InvalidOperationException: The entity type 'Audit' requires a primary key to be defined.

Which seems to indicate that efcore is attempting to locate the Audit data in a separate table. What I ended up doing was to add addional configuration in the DbContext class as mentioned by @Dmitry, the trick being to specify the table:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Organisation>().OwnsOne(org => org.Created, created => created.ToTable("Organisation"));
    }

I'm not sure if this is a bug, or is expected to work like that. But I find the documentation leaves room for interpretation. I've created the following extension method to eliminate the manual configuration of each entity-type:

    public static ModelBuilder FixOwnedAttributeImplementation(this ModelBuilder modelBuilder)
    {
        // loop over all entities
        foreach (var entity in modelBuilder.Model.GetEntityTypes())
        {
            // find properties that reference a type that is marked as [Owned]
            foreach (var property in entity.GetNavigations().Where(p => p.ClrType.GetCustomAttributes(typeof(OwnedAttribute), true).Any()))
            {
                // enforce table sharing
                modelBuilder.Entity(entity.Name).OwnsOne(property.ClrType, property.AsNavigation().Name).ToTable(entity.Relational().TableName);
            }
        }

        return modelBuilder;
    }

So you can only have to add:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.FixOwnedAttributeImplementation();
}

Upvotes: 1

TanvirArjel
TanvirArjel

Reputation: 32059

According to Owned Entity Types in EF Core, modelBuilder.Entity<User>().OwnsOne(o => o.SMSCodeInfo); should create the [Owned] entity columns in the Owner table but stangely it creating into a seprate table with a strange name AspNetUsers1.

If you want the [Owned] entity columns should be in a separate table then your configuration should be as follows:

modelBuilder.Entity<User>().OwnsOne(o => o.SMSCodeInfo , sm => 
{
   sm.ToTable("SMSCodeInfo");
});

It will generate as follows:

migrationBuilder.CreateTable(
            name: "SMSCodeInfo",
            columns: table => new
            {
                UserId = table.Column<Guid>(nullable: false),
                Code = table.Column<long>(nullable: false),
                Expiration = table.Column<DateTime>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_SMSCodeInfo", x => x.UserId);
                table.ForeignKey(
                    name: "FK_SMSCodeInfo_AspNetUsers_UserId",
                    column: x => x.UserId,
                    principalTable: "AspNetUsers",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.Cascade);
            });

Upvotes: 2

Related Questions