Seed entity with owned property - c#

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 = "foo#foo.foo",
UserName = "foo#foo.foo",
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 ?

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

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 = "foo#foo.foo",
UserName = "foo#foo.foo",
// 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
});
});
}

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

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.

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

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"),
}
);

Related

Can't update child entities in my controller with entity framework core

Having these two entities, I fetch them, map them to viwemodels/dtos before I pass them to the UI.
I also had to ignore reference loop handling, in my startup.cs file, to map them to DTO's correctly.
public class Matter
{
public int Id { get; set; }
public ICollection<MatterExposure> Exposures { get; set; }
// other properties
}
public class MatterExposure
{
public int Id { get; set; }
public Matter Matter { get; set; }
// other properties
}
When I save the form (which includes a table of 'MatterExposure's) in the UI I pass everything back to the controller to be saved. INFO - not saving child entities 'MatterExposure' yet in below controller call and it works fine!
[HttpPut("{id}")]
public async Task<IActionResult> UpdateData(string id, MatterForClaimDetailedDto generalMatterDto)
{
var user = await _userManager.GetUserAsync(HttpContext.User);
var matter = await _autoRepo.GetMatter(id);
// fill some matter data and add a child then save and it works fine
if (await _autoRepo.SaveAll())
return NoContent();
}
public class MatterForClaimDetailedDto
{
public int Id { get; set; }
public GeneralMatterDto MatterData { get; set; }
public ICollection<MatterExposure> Exposures { get; set; }
// other properties
}
Now I want to add the update of MatterExposure entities, as I could have made changes to them in the UI. So I try to use UpdateRange like this
[HttpPut("{id}")]
public async Task<IActionResult> UpdateData(string id, MatterForClaimDetailedDto generalMatterDto)
{
var user = await _userManager.GetUserAsync(HttpContext.User);
var matter = await _autoRepo.GetMatter(id);
matter.EditedDate = DateTime.Now;
matter.FirstName = generalMatterDto.FirstName;
matter.LastName = generalMatterDto.LastName;
_autoRepo.UpdateRange<List<MatterExposure>>(generalMatterDto.Exposures.ToList());
await _autoRepo.SaveAll()
}
public void UpdateRange<T>(T entity) where T : class
{
_autoContext.UpdateRange(entity);
}
But on calling UpdateRange I get this exception message:
"The entity type 'List MatterExposure' was not found. Ensure that the entity type has been added to the model."
In my context I have this:
public DbSet<MatterExposure> MatterExposure { get; set; }
I then tried below with no luck
public DbSet<List<MatterExposure>> MatterExposure { get; set; }
I thought I would try updating each individual 'MatterExposure' entity to see if that would change anything. So I tried removing the UpdateRange call and tried with individual 'MatterExposure' entities
foreach(var exposure in generalMatterDto.Exposures) {
_autoRepo.Update<MatterExposure>(exposure);
}
// in my repo I have this with different things I tried
public void Update<T>(T entity) where T : class
{
// _autoContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
//_autoContext.Entry(entity).State = EntityState.Detached;
_autoContext.Update(entity);
// _autoContext.ChangeTracker.
}
On the first loop through each 'MatterExposure' Update call to the repo I get this exception
"The instance of entity type 'MatterExposure' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values."
After the exception above I tried I put the loop at the top of the controller method to see if the other entity stuff was interfering.
// at top of controler method before the other entity actions are performed
foreach(var exposure in generalMatterDto.Exposures) {
_autoRepo.Update<MatterExposure>(exposure);
}
And moving the for loop to the top of the controller, runs through the 1st iteration but then fails on the second giving me the same error message again
"The instance of entity type 'MatterExposure' cannot be tracked because
another instance with the same key value for {'Id'} is already being tracked.
When attaching existing entities, ensure that only one entity instance with a given key value is attached.
Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values."
QUESTION - am I not updating the child entities correctly or is it something else?

"The owned entity type requires to be referenced from another entity type via a navigation"

I have an entity in my application called Person. There are two types of users, Student and Professor, that inherit from Person.
Every Person has a settings property:
public abstract class Person
{
public Guid UserId { get; set; }
public string Name { get; set; }
public PersonSettings Settings { get; set; }
}
public class Student : Person
{
}
public class Professor : Person
{
}
My PersonSettings class is just a couple of properties. It isn't an entity to be stored in the database, so I marked it as Owned:
[Owned]
public class PersonSettings
{
public bool NotificationsEnabled { get; set; }
public int GymPassId { get; set; }
}
These are stored in the database as json, which I'm using EF Core conversion values in my Person entity configuration to serialize and deserialize it:
builder.Property(p => p.Settings).HasConversion(
s => JsonConvert.SerializeObject(s, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
s => JsonConvert.DeserializeObject<PersonSettings>(s, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));
But when I try to run my application and do a database migration, I'm getting an error saying
The owned entity type 'PersonSettings' requires to be referenced from another entity type via a navigation. Add a navigation to an entity type that points at 'PersonSettings'.
What am I supposed to do here? I couldn't find anything on the error message. Not sure if it has anything to do with Person being an abstract class.
I also can't repro, but you don't need an Owned Type here.
Using Owned Types is an alternative to using JSON serialization of a non-scalar property. When using Owned Types the type is stored along with the Entity that references it. So as Owned Type EF would create the Person table with separate columns for Settings_NotificationEnabled, and Settings_GymPassId.
So you can simply remove the OwnedAttribute, and ensure that you don't declare it to be an Entity with a property of type DbSet<PersonSettings> in your DbContext.
As to which to pick, I would generally use an Owned Type for this scenario, so you could query the database by the individual PersonSettings properties.
Using JSON conversion of a non-scalar property is useful for the case where you have a collection, because EF Core does not currently support collections of Owned Types.
I had the same problem and what solved it for me was to follow the documentation here.
Basically what you want to do is add an OwnsOne in your entity configuration for Person:
builder.OwnsOne(p => p.PersonSettings, ps => {
//if need be add additional settings here for NotificationsEnabled and GymPassId
});
That will create a "link" between Person and PersonSettings which the migration generator uses to construct the script.
Same error here, and the issue was that I had a reference in the owned class to an entity:
public class OwnedClass // owned, NOT a DbSet
{
// ... other properties
public Guid? CalendarId { get; set; }
[ForeignKey(nameof(CalendarId))]
public Calendar FactoryCalendar { get; set; } // this is a DbSet
{

EF6 entity with DatabaseGeneratedOption.Identity Guid Id force insert my Id value

I am trying to use EF to export/import the existing database of a DbContext. In this context, there are several entities with Guid Id properties with DatabaseGeneratedOption.Identity defined by the ModelBuilder. When I re-import the entities, I want to use the Id value from the serialized object, but it always generates a new Id value when I save the changes. Is there any way to force EF to use my Id value in this case? I know DatabaseGeneratedOption.None will allow me to do it, but then I will always be responsible for generating the Id. I know there segmentation issues of the index that occur without using sequential Guids, so I do not want to do this.
Am I out of luck or has anyone found a trick?
Update: we have decided to simply change all Guid Id from DatabaseGeneratedOption.Identity to DatabaseGenerationOption.None and provide the Id ourselves. Although this leads to index fragmentation, we do not expect this to be a problem with the smaller size of our tables.
You can achieve what you want by defining two contexts that derive from a base context. One context defines its keys with DatabaseGeneratedOption.Identity, the other one with DatabaseGeneratedOption.None. The first one will be your regular application's context.
This is possible by virtue of Guid primary keys not being real identity columns. They're just columns with a default constraint, so they can be inserted without a value, or with a value without having to set identity_insert on.
To demonstrate that this works I used a very simple class:
public class Planet
{
public Guid ID { get; set; }
public string Name { get; set; }
}
The base context:
public abstract class BaseContext : DbContext
{
private readonly DatabaseGeneratedOption _databaseGeneratedOption;
protected BaseContext(string conString, DatabaseGeneratedOption databaseGeneratedOption)
: base(conString)
{
this._databaseGeneratedOption = databaseGeneratedOption;
}
public DbSet<Planet> Planets { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Planet>().HasKey(p => p.ID);
modelBuilder.Entity<Planet>().Property(p => p.ID)
.HasDatabaseGeneratedOption(this._databaseGeneratedOption);
base.OnModelCreating(modelBuilder);
}
}
The context subclasses:
public class GenerateKeyContext : BaseContext
{
public GenerateKeyContext(string conString)
: base(conString, DatabaseGeneratedOption.Identity)
{ }
}
public class InsertKeyContext : BaseContext
{
public InsertKeyContext(string conString)
: base(conString, DatabaseGeneratedOption.None)
{ }
}
I first run some code to create and seed the source database:
var db1 = #"Server=(localDB)\MSSQLLocalDB;Integrated Security=true;Database=GuidGen";
var db2 = #"Server=(localDB)\MSSQLLocalDB;Integrated Security=true;Database=GuidInsert";
// Set initializers:
// 1. just for testing.
Database.SetInitializer(new DropCreateDatabaseAlways<GenerateKeyContext>());
// 2. prevent model check.
Database.SetInitializer<InsertKeyContext>(null);
using (var context = new GenerateKeyContext(db1))
{
var earth = new Planet { Name = "Earth", };
var mars = new Planet { Name = "Mars", };
context.Planets.Add(earth);
context.Planets.Add(mars);
context.SaveChanges();
}
And a target database:
using (var context = new GenerateKeyContext(db2))
{
context.Database.Initialize(true);
}
Finally this is the code that does the actual job:
var planets = new List<UserQuery.Planet>();
using (var context = new GenerateKeyContext(db1))
{
planets = context.Planets.AsNoTracking().ToList();
}
using (var context = new InsertKeyContext(db2))
{
context.Planets.AddRange(planets);
context.SaveChanges();
}
Now in both databases you'll see two records with identical key values.
You might wonder: why can't I use one context class, and construct it either with or without the Identity option? That's because EF builds the EDM model only once for a context type and stores it in the AppDomain. So the option you use first would determine which model EF will use for your context class.

Why can't I add new data to my database with Entity Framework Code First?

For the first time ever I'm using GUIDs for PK values in my POCO classes which has presented a rather irritating problem being that I can't seem to get data added to my tables.
Here's an example addition:
public partial class EntityType
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string Type { get; set; }
public bool Deleted { get; set; }
}
Here's my migration configuration seed method:
internal sealed class Configuration : DbMigrationsConfiguration<AC.WebGUI.Models.OrtundDataModel>
{
protected override void Seed(AC.WebGUI.Models.OrtundDataModel context)
{
// Prepopulate Entity Types.
if (!context.EntityTypes.Any())
{
context.EntityTypes.AddOrUpdate(
new EntityType { Id = Guid.NewGuid(), Type = "Supplier" },
new EntityType { Id = Guid.NewGuid(), Type = "Distributor" },
new EntityType { Id = Guid.NewGuid(), Type = "Staff" },
new EntityType { Id = Guid.NewGuid(), Type = "Customer" },
new EntityType { Id = Guid.NewGuid(), Type = "Pharmacy" }
);
context.SaveChanges();
}
}
Note that in both classes, I'm specifying a Guid.NewGuid() value for my Id property.
This, however, has not got me around the error:
System.Data.SqlClient.SqlException: Cannot insert the value NULL into column 'Id', table 'Ortund.dbo.EntityTypes'; column does not allow nulls. INSERT fails.
Obviously calling Guid.NewGuid() can't be a null value so I'm very unsure of how to proceed at this point. I figured that if I added it to the constructor on the class that defines the table, I could remove it from the actual implementation of that class, however, whether I take it out of the constructor or out of the implementation, the error persists.
How can I get these Guids added to my table data without any errors? What am I missing?
Edit - Addressing duplicate question
Since my question has been marked as a duplicate of this question which deals with using the specified data annotation to make EF automatically generate the GUID value for the PK column on the table, I thought I'd just explain how mine is different.
The idea is the same, true enough, but the implementation has diverged from what is discussed in the other question so I've arrived at a totally different solution for my own project which involved removal of this data annotation whereas the solutions proposed in the other question keep it and make other changes to the data model.
Figured I'd answer this before it gets closed.
Per comments, simply removing the DatabaseGenerated(DatabaseGeneratedOption.Identity)] annotation from the PK property fixed the problem though I did keep the assignment in the constructor.

EF Seed ignoring the DatabaseGeneratedOption.Computed attribute

I have a problem with the attribute DatabaseGenerate(DatabaseGeneratedOption.Computed), it seems to be the case that it is not respected by the AddOrUpdate() call within the Seed method.
I have made a simple project, to illustrate my issue:
public class EFModel : DbContext
{
public EFModel()
: base("name=EFModel")
{
}
public virtual DbSet<MyEntity> MyEntities { get; set; }
}
public class MyEntity
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
[StringLength(50)]
public string Name { get; set; }
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
[StringLength(50)]
public string DefaultName { get; set; }
}
The Seed call
protected override void Seed(DummyEF.EFModel context)
{
context.MyEntities.AddOrUpdate(new MyEntity { Id = 10, Name = "Samual", DefaultName = "Sam" });
context.MyEntities.AddOrUpdate(new MyEntity { Id = 11, Name = "David" });
}
When I run the Update-Database command, with the first Seed row (that is with Samual, and specifying a value for the default name) it works fine. When I run it with the second line (that is with Dadid, and without specifying a value for the DefaultName) it fails:
Validation failed for one or more entities. See
'EntityValidationErrors' property for more details.
The table it self is valid, and has a default constraint, so normal insert into via SQL works.
It just seems to be the case, that the Seed is ignoring the fact that
the entity property is marked with the
DatabaseGeneratedOption.Computed attribute.
Any idea why this is ignored?
I am using EF 6 code first.
When you use DatabaseGeneratedOption.Computed you are telling EF not to add or update the information in the database because the database will generate it as is common for timestamps or computed SQL columns, so it makes no sense to try and seed that column. You are getting the validation error because you have it marked required.
The conclusion must be that the current version of EF 6 does not cater for the combination of the attriburtes Required and DatabaseGenerated(DatabaseGeneratedOption.Computed)
So if one wants to set a column to not be nullable, yet avoid EF from validating it before the save to the database, one cannot use the Required attribute.
I guess that the Fluent API or modification of the migration code needs to be used in stead.

Categories