Seed entity with owned property
Asked Answered
B

6

37

I am trying to seed an user entity in my database. The User entity has an owend property EmailPermissions.

When I run the command

dotnet ef migrations add Initial;

I get the error

The seed entity for entity type 'User' cannot be added because it has the navigation 'EmailPermissions' set. To seed relationships you need to add the related entity seed to 'EmailPermissions' and specify the foreign key values {'UserId'}.

but since EmailPermissions is an owned entity I didn't give it an explicit UserId property, meaning I can't seed it separately in the database.

the entity

public sealed class User : IdentityUser
{
    public User()
    {
        EmailPermissions = new EmailPermissions();
    }

    /* [..] */

    public string TemporaryEmailBeforeChange { get; set; }
    public bool IsEmailAwaitingUpdate { get; set; }
    public EmailPermissions EmailPermissions { get; set; }
    public ICollection<Registeration> Registerations { get; set; }

    /* [..] */

}

[Owned]
public class EmailPermissions
{
    /* [..] */

    public bool Newsletter { get; set; }
    public bool PromotionalOffers { get; set; }
    public bool PrestationReminders { get; set; }
    public bool PrestationOffers { get; set; }
}

The seeding call

private void SeedUser(ModelBuilder builder)
{
    builder.Entity<User>().HasData(
        new User
        {
            Id = "37846734-172e-4149-8cec-6f43d1eb3f60",
            Email = "[email protected]",
            UserName = "[email protected]",
            PasswordHash = "AQAAAAEAACcQAAAAEIytBES+jqKH9jfuY3wzKyduDZruyHMGE6P+ODe1pSKM7BuGjd3AIe6RGRHrXidRsg==",
            SecurityStamp = "WR6VVAGISJYOZQ3W7LGB53EGNXCWB5MS",
            ConcurrencyStamp = "c470e139-5880-4002-8844-ed72ba7b4b80",
            EmailConfirmed = true
        });
}   

If I remove the instantiation of the EmailPermissions property from the constructor I get the following error instead

The entity of type 'User' is sharing the table 'AspNetUsers' with entities of type 'EmailPermissions', but there is no entity of this type with the same key value that has been marked as 'Added'.

How can I seed a user via the .HasData method when it has an owned property ?

Barbuto answered 14/6, 2018 at 16:58 Comment(1)
Follow this: github.com/dotnet/efcore/issues/10000Junkman
T
48

Currently this information is missing from the documentation (tracked by #710: Document how to seed owned types). It's explained by EF Core team (with example) in the #12004: Problem seeding data that contains owned type thread:

Owned types must be seeded with a HasData call after the OwnsOne call. Also, since owned types by convention have a primary key generated in shadow state, and since seed data requires keys to be defined, then this requires use of an anonymous type and setting the key.

which is basically what the exception message is telling you.

Following the advice, you should remove the instantiation of the EmailPermissions property from the constructor and add a seeding code like this:

builder.Entity<User>().OwnsOne(e => e.EmailPermissions).HasData(
    new
    {
        UserId = "37846734-172e-4149-8cec-6f43d1eb3f60",
        // other properties ...
    }
);

Quite annoying and error prone due to the need to know the shadow PK name and the usage of an anonymous type. As the same member mentioned

Note that this would become easier if navigations were supported for seeding, which is tracked by #10000: Data Seeding: Add support for navigations

Tithing answered 14/6, 2018 at 17:33 Comment(2)
It's been 13 months since your answer. I just got into the same issue. Am I doing something wrong or is the feature still not fixed?Proclus
@Konrad As you can see by following the last link (#10000), the last reply is "It's in the backlog, so it probably will be done after 2019."Tithing
M
14

Thank Ivan Stoev's answer. i add some more code to easy to imagine. this is code of seed data function base on example.

  • First adding data of User.
  • After that add data of owned object.
  • Data of owned object have to be anonymous because PK will request. This PK will not appear in database. Name should be entity name + Id

Example: Entity XXX => PK will be XXXId

private void SeedUser(ModelBuilder builder)
{
    builder.Entity<User>(b =>
    {
        b.HasData(new User
        {
            Id = "37846734-172e-4149-8cec-6f43d1eb3f60",
            Email = "[email protected]",
            UserName = "[email protected]",
            // more properties of User
        });
        b.OwnsOne(e => e.EmailPermissions).HasData(new 
        {
                UserId = "37846734-172e-4149-8cec-6f43d1eb3f60",
                Newsletter = true,
                PromotionalOffers = true,
                PrestationReminders = true,
                PrestationOffers = true
        });
    });
}
Mammoth answered 19/7, 2019 at 18:45 Comment(0)
R
1

If you want to avoid using an anonymous type to specify the shadow property keys, you can declare them explicitly in your model class and configure them with the Fluent API as keys. This way you don't have to guess the property names and it's less error-prone.

If the name supplied to the Property method matches the name of an existing property (a shadow property or one defined on the entity class), then the code will configure that existing property rather than introducing a new shadow property. Source

Rockoon answered 3/4, 2019 at 18:4 Comment(0)
B
0

In my scenario I wanted the owned-type property to be auto-initialed in the parent class:

public class User
{
  EmailPermissions _EmailPermissions;
  public EmailPermissions 
  {
    get => _EmailPermissions ??= new EmailPermissions();
    set => _EmailPermissions = value;
  }
}

When I tried to add seed data I got that nasty exception.

The solution was to pass the User as anonymous type in its HasData call.

Barca answered 2/12, 2019 at 16:37 Comment(0)
J
0

I had the same issue seeded my data at startup. Here is the link to the github issue.

Junkman answered 17/3, 2020 at 20:44 Comment(0)
S
0

In my case, I was using anonymous types, but the default class constructor was instantiating an instance of the non-nullable child class, which contained the property in question. The fix was to either not instantiate the child class in the default constructor, or use a different constructor that did not instantiate the child class.

Original

public class BaseDownload {

    [Key]
    public Guid BaseDownloadId { get; set; }
    public Guid DownloadCategoryId { get; set; }
    public virtual DownloadCategory DownloadCategory { get; set; }

    public BaseDownload()
    {
        this.BaseDownloadId = Guid.NewGuid();
        this.DownloadCategory = new DownloadCategory();
    }
}

followed by the seed data:

modelBuilder.Entity<BaseDownload>().HasData(
   new BaseDownload()
   {
       BaseDownloadId = Guid.Parse("9150ebd7-dd84-4c97-bf58-62f1c3611545"),
       DownloadCategoryId = Guid.Parse("46b087f9-5c71-401f-a5cf-021274463715"),
   }
);

Trying to seed the data gave the error "The seed entity for entity type 'BaseDownload' cannot be added because it has the navigation 'DownloadCategory' set. To seed relationships, add the entity seed to 'BaseDownload' and specify the foreign key values {'DownloadCategoryId'}.". An instance of the child class (DownloadCategory) definitely exists as it was created using the same set of seed data. So I only needed to use the ID property. Adding a new constructor that did not instantiate the child DownloadCategory() class like below resolve the error.

public BaseDownload(bool isSeedData)
{

}

and

modelBuilder.Entity<BaseDownload>().HasData(
   new BaseDownload(true)
   {
       BaseDownloadId = Guid.Parse("9150ebd7-dd84-4c97-bf58-62f1c3611545"),
       DownloadCategoryId = Guid.Parse("46b087f9-5c71-401f-a5cf-021274463715"),
   }
);
Stearoptene answered 18/10, 2022 at 0:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.