Why Entity Framework Core resets filled navigation property to null during AddRange only for first item in range? - entity-framework

I'm using the Asp Net Identity library and I've customized IdentityUser to have a custom navigation property called CompanyRole of type ApplicationRole here is rough structure bellow
public class ApplicationUser: IdentityUser
{
...
public ApplicationRole CompanyRole { get; set }
}
public class ApplicationRole : IdentityRole
{
public string Id{ get; set; }
public string Name { get; set; }
}
Fragment of ApplicationUser model configuration
builder
.HasOne(x => x.CompanyRole)
.WithOne()
.HasForeignKey<ApplicationUser>(au => au.CompanyRoleId)
.OnDelete(DeleteBehavior.Restrict);
So when I try to add multiple users with existing roles like this
var viewerRole = await _rolesService.GetViewerRole() // it queries role from dbcontex behind the scene, so it should be tracked
var usersToAdd = emails.Select(email => new ApplicationUser
{
FirstName = request.Name,
Email = email,
CompanyRole = viewerRole
}
)
_dbContext.Set<ApplicationUser>().AddRange(usersToAdd)
_dbContext.SaveChanges();
It complaints that cannot insert NULL in CompanyRoleId column, since it's a FK constraint.
The reason of this exception is that first user in a range gets CompanyRole as null, whereas others users are good. Why it's happening, since viewer role should have been tracked ?
I've tryed to play with Entities states, such as Added and Attach entity again - no luck
I've expected that all users are created with reference to existing ApplicationRole
BTW the workaround that worked, was if I split users adding one by one with slight changes and detach those immediately then it works, the drawback is that query per user...inefficient
var result = _dbContext.Set<ApplicationUser>().Add(user);
await _dbContext.SaveChangesAsync();
_dbContext.Entry(user).State = EntityState.Detached;

The issue is that you are defining the relationship between user and company role as One to One. This means any 1 company role can be assigned to only one user. So as it tries to associate the role to each user, it would de-associate it from the previous.
What it looks like you want is a Many-to-one, many users can hold the same role. Adjust your mapping to:
builder
.HasOne(x => x.CompanyRole)
.Withmany()
.HasForeignKey<ApplicationUser>(au => au.CompanyRoleId)
.OnDelete(DeleteBehavior.Restrict);
... and you should be sorted.

Related

Cannot insert explicit value for identity column in table when IDENTITY_INSERT is set to OFF - EF Core many-to-many (child) data

Following through Julie Lerman's Pluralsight course EF Core 6 Fundamentals I've created two classes in my own project (my own design, but identical to the course in terms of class structure/data hierarchy):
Class 1: Events - To hold information about an event being held (e.g. a training course), with a title and description (some fields removed for brevity):
public class EventItem
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int EventItemId { get; set; }
[Required(AllowEmptyStrings = false)]
public string EventTitle { get; set; }
public string? EventDescription { get; set; }
[Required]
public List<EventCategory> EventCategories { get; set; } = new();
}
Class 2: Event categories - Each event can be linked to one or more pre-existing (seeded) categories (e.g. kids, adult).
public class EventCategory
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int EventCategoryId { get; set; }
[Required]
public string EventCategoryName { get; set; }
public List<EventItem>? EventItems { get; set; }
}
In my Razor form to create the event, the user can select from multiple categories. Using EF Core I take the posted data (via a VM/DTO object) and construct the relevant parent/child entities. However upon saving to the database I get an exception as EF Core tries to re-create the categories when they already exist:
Cannot insert explicit value for identity column in table
'EventCategories' when IDENTITY_INSERT is set to OFF.
My code explicitly looks up the existing categories selected by the user, but the context tracker appears to still believe they need inserting, in addition to creating the many-to-many relationship.
I'd appreciate any input as to why this is happening please:
using (var dbcontext = DbFactory.CreateDbContext())
{
// Get selected categories from user's check box list
var selectedCategoryIds = _eventCagetories.Where(c => c.isSelected).Select(c => c.EventCategoryId).ToList();
// Create new Event
var newEventItem = new EventFinderDomain.Models.EventItem() {
EventTitle = _eventItemDTO.EventTitle,
EventDescription = _eventItemDTO.EventDescription,
EventUrl = _eventItemDTO.EventUrl,
TicketUrl = _eventItemDTO.TicketUrl
};
// Find categories from the database based on their ID value
var selectedEventCategories = dbcontext.EventCategories.Where(c => selectedCategoryIds.Contains(c.EventCategoryId)).ToList();
// Add the categories to the event
newEventItem.EventCategories!.AddRange(selectedEventCategories);
// Add the event to the change tracker
await dbcontext.EventItems.AddAsync(newEventItem); // <-- Created correctly with child list objects added
// Detect changes for debugging
dbcontext.ChangeTracker.DetectChanges();
var debugView = dbcontext.ChangeTracker.DebugView; // <-- Incorrectly shows newEventItem.Categories being added
// Save to database
await dbcontext.SaveChangesAsync(); // <-- Cannot insert explicit value for identity column
}
The Event entity appears to be correctly created in the debugger with its related child categories included:
The change tracker however incorrectly shows the selected categories being added again when they already exist:
After commenting out every line of code in the app and adding back in until it broke, it emerges the problem was elsewhere within Program.cs:
builder.Services.AddDbContextFactory<EventFinderContext>(
opt => opt.UseSqlServer(new SqlConnectionStringBuilder() {/*...*/}.ConnectionString)
.EnableSensitiveDataLogging()
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) // <-- THE CULPRIT
);
In the training video this method was described as a way of reducing overhead for disconnected apps. I had assumed that because of the disconnected nature of HTTP, this would be beneficial and that context would be re-established when creating the model's child data. This was incorrect on my part.
I should have used .AsNoTracking() only when retriving read-only data from my database. For example, loading in the child-data for a new model that wouldn't be modified directly, but used to create the many-to-many data (explicitly, for the category data option items only and not for the event data).

EF "The member with identity X does not exist in the metadata collection" (and NopCommerce)

First of all and as you can see in the title, I'm designing a plugin for nopCommerce. I believe that the problem is not related to it (but with Entity Framework) but I'm not really sure.
My plugin has access to data, and nopCommerce let's me create my own context, creating the required tables and relations.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new ParentMap());
modelBuilder.Configurations.Add(new ChildMap());
}
Conceptually, what I would like to have is:
ROOT TABLE ----(1:N)---- CHILDTABLE ----(1:1)---- EXISTING NOPCOMMERCE TABLE
And the tables and columns created in the DB seems correct to me. The mappings are pretty simple:
public ParentMap()
{
ToTable("TableA");
HasKey(m => m.Id);
Property(m => m.SomeProperty);
HasMany(m => m.Child).WithRequired(s => s.Parent).WillCascadeOnDelete();
}
public ChildMap()
{
ToTable("TableB");
HasKey(m => new { m.Id });
HasRequired(m => m.NopCommerceEntity);
HasRequired(m => m.Parent);
}
Till this point, everything seems to work pretty well, things get registered and the application starts. When I try to insert or update anything that has relation with the nopCommerce entity, I get the following exception:
"The member with identity 'Namespace.ChildObject_NopCommerceEntity' does not exist in the metadata collection.\r\nParameter name: identity".
I've been thinking that the problem could be related with the fact that I'm not adding other mappings in my OnModelCreating. I've tried to add all the related mappings there but I get the same exception.
It's a bit complicated. I'd be very greatful for your help & time :)
You are right this issue related to Entity Framework because EF does not support cross-project POCO relations. You have to remove any property that has an entity from NopEntities in your plugin and instead use an Id of that entity as int as below :
public int ProductId { get; set; }
instead of
public virtual Product Product { get; set; }
and in the mapping you should not specify the relationship between your entity and the Nop.
I think you can still use the following relation in your child class
public int ProductId { get; set; }
public virtual Product Product { get; set; }
but when you save or update the child class, instead of setting
child.Product = product
instead set the following
child.ProductId = product.Id
and also i think in your PluginObjectContext plugin class inherits from NopObjectContext.

Self-referencing many-to-many relationship EF code first

I work with ASP.NET MVC With Durandal/Breeze templates.
Let's say I have the following class:
public class Person
{
public int Id { get; set; }
public string Firstname { get; set; }
public string Lastname { get; set; }
public virtual List<Person> Friends { get; set; }
}
With the following EF Fluent API:
modelBuilder.Entity<Person>()
.HasMany(m => m.Friends)
.WithMany()
.Map(m => m.ToTable("Friends"));
The database is generated successfully.
The problem is when I perform a que
ry with Breeze (client side) I have no data for the Friends property.
var query = entityQuery.from('Person')
.where('id', '==', 123)
.expand("Friends");
When the query is executed I get as result the requested People entity with all the data except the Friends property is always an empty array. When I check the Json answer I see that also the data are transmitted. Even data for the Friends property. However they are not linked to the Friends property itself.
My question: what do I have to do to have my Friends property filled with values?
Thanks.
You must declare a foreign key in Person. Breeze requires the FK to correctly resolve associations.
Edit:
I just realized you are asking about a many-to-many relationship. (yeah, I should have read the post title...)
Breeze does not support many-to-many associations.
However, you could have two one-to-many relationships to work as a many-to-many. (i.e. many-to-one-to-many) In this case, you will need to define the linking table/entity and the foreign key as mentioned earlier. (see http://www.breezejs.com/documentation/navigation-properties)
Try this answer: *Note that this is incomplete because i do not see the other table that you are trying to m-2-m with Persons. ( You will only want to use Persons Table and the 2nd Table , NOT table=Friends.
db.Person
.Include(c => c.Friends)
.Where(c => c.Friends.Any(up => up.FriendVlaue == c.FirstName)) //c.from Persons
.Select(c => new
{
PersonID = c.ID,
PersonName = c.FirstName,
PersonCount = c.Person.Count()
})
{
From This answer
You should include Friends in the results. You can do this by adding Include("Friends")at Server Side API.
[HttpGet]
public IQueryable<Person> Persons()
{
return _contextProvider.Persons.Include("Friends");
}
If you don't want to return always the Friendsreference, you can create another method in the API such as PersonsWithFriends as suggested in here (Specialized query actions).

Why do I get a "relationship multiplicity violation" accessing a relation property?

I have the following classes in which I am trying to map the entity object to the view model:
public class ConsumerIndexItem: MappedViewModel<Consumer>
{
public string UserName { get; set; }
public string RoleDescription { get; set; }
public override void MapFromEntity(Consumer entity)
{
base.MapFromEntity(entity);
UserName = entity.User.UserName;
}
}
public class Consumer: AuditableEntity
{
public virtual User User { get; set; }
public virtual Role Role { get; set; }
}
public class IndexModel<TIndexItem, TEntity> : ViewModel where TEntity : new()
{
public IndexModel()
{
Items = new List<TIndexItem>();
}
public List<TIndexItem> Items { get; set; }
public virtual void MapFromEntityList(IEnumerable<TEntity> entityList)
{
Items = Mapper.Map<IEnumerable<TEntity>, List<TIndexItem>>(entityList);
}
}
public class ConsumerIndexModel: IndexModel<ConsumerIndexItem, Consumer>
However, if I drive the mapping with the following code:
var model = new ConsumerIndexModel();
var list = _repository.List().Where(c => c.Parent == null).ToList();
model.MapFromEntityList(list);
return View(model);
on the line UserName = entity.User.UserName; in ConsumerIndexItem I get the following exception:
A relationship multiplicity constraint violation occurred: An EntityReference can have no more than one related object, but the query returned more than one related object. This is a non-recoverable error.
If I execute ?entity.User.UserName in the Immediate Window I get the expected user name value. What could be wrong here?
Let me explain why I had this exception and you may be able to correlate it with your own situation. I had EF Code First model mapped to the existing database. There was one-to-many relationship between two of the entities. The child table had composite primary consisting of the Id and Date. However, I missed the second segment of the primary key in my fluent map:
this.HasKey(t => t.Id);
The strange part of that was that the model worked but was throwing an exception in certain cases and it was very hard to understand why. Apparently when EF was loading the parent of child entity there were more than one parent since the key had not only Id but Date as well. The resolution was to include the second part of the key:
this.HasKey(t => new { t.Id, t.Date });
The tool that helped me to pinpoint the problem was EF Power Tools, currently it is in Beta 3. The tool gives a context menu for the EF context class where one the item is View Entity Model DDL SQL. Although I could have found this just by checking the code, the tool is nice in showing how close the EF model matches the actual database.
I believe that you're getting this exception because for a some reasons the multiplicity of relationship is violated. In my case it was incorrect mapping, in your it may be something else, I can't tell by looking at your code.
I think the problem maybe that you suppose that every user has only one consumer while this is not correct regarding data.
I had the same problem and it was because the relationship was on-to-many and I made it one-to-one.

EF 4.1 Code First - duplicate entities in object graph causes exception

I am getting the following exception when attempting to save my entity:
"AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges."
I'm creating a 3 tiered application where the data access layer is using EF Code First, and where the client calls the middle tier using WCF. I am therefore unable able to let the context track the entity state when building up an entity on the client.
In some situations I am finding that the same entity is contained twice in the object graph. In this situation it fails when I try and set the entity state of the duplicate.
For example, I have the following entities:
Customer
Country
Curreny
From the client I create a new
instance of a Customer. I then make
a service call to get Country
instance and assign it to the Customer. The Country instance has
an associated Currency.
The user can then associate a
Currency with the customer. They
may well choose the same Currency
that's associated with the Country.
I make another service call to get
this. Thus at this stage we may
have two separate instances of the
same currency.
So what I end up with are two instance of the same entity in the object graph.
When then saving the entity (in my service) I need to tell EF that both Currency entities are not modified (if I don't do this I get duplicates). Problem is that I get the exception above.
On saving if I set the Currency instance on Country instance to null, it resolves the problem, but I feel like the code is becoming increasingly messy (due to this and other WCF related EF workarounds I'm having to put in place).
Are there any suggestions on how to resolve this in a nicer way?
Many thanks for any help in advance. Here's the code:
using System;
using System.Collections.Generic;
using System.Data.Entity.ModelConfiguration;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
namespace OneToManyWithDefault
{
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Country Country { get; set; }
public Currency Currency { get; set; }
public byte[] TimeStamp { get; set; }
}
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
public Currency Currency { get; set; }
public byte[] TimeStamp { get; set; }
}
public class Currency
{
public int Id { get; set; }
public string Symbol { get; set; }
public byte[] TimeStamp { get; set; }
}
public class MyContext
: DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Currency> Currency { get; set; }
public DbSet<Country> Country { get; set; }
public MyContext(string connectionString)
: base(connectionString)
{
Configuration.LazyLoadingEnabled = false;
Configuration.ProxyCreationEnabled = false;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new CustomerConfiguration());
modelBuilder.Configurations.Add(new CountryConfiguration());
modelBuilder.Configurations.Add(new CurrencyConfiguration());
base.OnModelCreating(modelBuilder);
}
}
public class CustomerConfiguration
: EntityTypeConfiguration<Customer>
{
public CustomerConfiguration()
: base()
{
HasKey(p => p.Id);
Property(p => p.Id)
.HasColumnName("Id")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
.IsRequired();
Property(p => p.TimeStamp)
.HasColumnName("TimeStamp")
.IsRowVersion();
ToTable("Customers");
}
}
public class CountryConfiguration
: EntityTypeConfiguration<Country>
{
public CountryConfiguration()
: base()
{
HasKey(p => p.Id);
Property(p => p.Id)
.HasColumnName("Id")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
.IsRequired();
Property(p => p.TimeStamp)
.HasColumnName("TimeStamp")
.IsRowVersion();
ToTable("Countries");
}
}
public class CurrencyConfiguration
: EntityTypeConfiguration<Currency>
{
public CurrencyConfiguration()
: base()
{
HasKey(p => p.Id);
Property(p => p.Id)
.HasColumnName("Id")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
.IsRequired();
Property(p => p.TimeStamp)
.HasColumnName("TimeStamp")
.IsRowVersion();
ToTable("Currencies");
}
}
class Program
{
private const string ConnectionString =
#"Server=.\sql2005;Database=DuplicateEntities;integrated security=SSPI;";
static void Main(string[] args)
{
// Seed the database
MyContext context1 = new MyContext(ConnectionString);
Currency currency = new Currency();
currency.Symbol = "GBP";
context1.Currency.Add(currency);
Currency currency2 = new Currency();
currency2.Symbol = "USD";
context1.Currency.Add(currency2);
Country country = new Country();
country.Name = "UK";
country.Currency = currency;
context1.Country.Add(country);
context1.SaveChanges();
// Now add a new customer
Customer customer = new Customer();
customer.Name = "Customer1";
// Assign a country to the customer
// Create a new context (to simulate making service calls over WCF)
MyContext context2 = new MyContext(ConnectionString);
var countries = from c in context2.Country.Include(c => c.Currency) where c.Name == "UK" select c;
customer.Country = countries.First();
// Assign a currency to the customer
// Again create a new context (to simulate making service calls over WCF)
MyContext context3 = new MyContext(ConnectionString);
customer.Currency = context3.Currency.First(e => e.Symbol == "GBP");
// Again create a new context (to simulate making service calls over WCF)
MyContext context4 = new MyContext(ConnectionString);
context4.Customers.Add(customer);
// Uncommenting the following line prevents the exception raised below
//customer.Country.Currency = null;
context4.Entry(customer.Country).State = System.Data.EntityState.Unchanged;
context4.Entry(customer.Currency).State = System.Data.EntityState.Unchanged;
// The following line will result in this exception:
// AcceptChanges cannot continue because the object's key values conflict with another
// object in the ObjectStateManager. Make sure that the key values are unique before
// calling AcceptChanges.
context4.Entry(customer.Country.Currency).State = System.Data.EntityState.Unchanged;
context4.SaveChanges();
Console.WriteLine("Done.");
Console.ReadLine();
}
}
}
I guess you get the exception only if customer.Currency and customer.Country.Currency refer to the same currency, i.e. have the same identity key. The problem is that those two currency objects come from different object contexts, therefore they are different objects (ReferenceEquals(customer.Currency, customer.Country.Currency) is false). When you attach both to your last context (by setting the State) the exception occurs because they are two different objects with the same key.
Looking at your code, perhaps the easiest option would be to check if the currency you want to assign to the customer is the same as the country's currency before you even load the currency, something like:
if (customer.Country.Currency.Symbol == "GBP")
customer.Currency = customer.Country.Currency;
// currencies refer now to same object, avoiding the exception
else
{
MyContext context3 = new MyContext(ConnectionString);
customer.Currency = context3.Currency.First(e => e.Symbol == "GBP");
}
(I assume here that Symbol is the key for currency or a least unique in the DB.) You would also avoid one service/DB call if the currencies are the same.
Other options would be: Don't include the currency in the country query, if you can. Your solution to set customer.Country.Currency to null (not bad at all). Make the references to the two currencies equal in the last context before you add the customer (if (customer.Country.Currency.Symbol == customer.Currency.Symbol) customer.Currency = customer.Country.Currency;). Reload the currencies in your last context and assign them to the customer.
But that's all not really a "nicer way" to solve the problem, only another way - in my opinion.
I think the issue is because you are setting the EntityState to Unchanged. The exception you are seeing only happens if the entity keys always exist AND the entity state is not Added.
See http://msdn.microsoft.com/en-us/library/bb896271.aspx
The last paragraph of Considerations for Attaching Objects is:
"An InvalidOperationException occurs when an object being attached has the same EntityKey as a different object already present in the object context. This error does not occur if an object in the context with same key but is in the Added state."
So the question is, why are you forcing the state to Unchanged instead of leaving it as added?
EDIT:
Edited after looking at your post again and your comment. Ultimately the problem is you are telling EF "Hey, add these Currency and Country objects with this Customer" but two of those objects already exist.
You can use the Attach instead of Add method, but the customer doesn't exist yet.
I suggest wrapping these calls in a transactionscope, calling SaveChanges right after creating the Customer, than using Attach rather then Add. If you get errors, you can roll back the transaction if necessary. I don't have a code sample handy, but does what I am saying make sense?
Something like:
using (TransactionScope scope = new TransactionScope())
{
// Now add a new customer
Customer customer = new Customer();
customer.Name = "Customer1";
context1.SaveChange();
// Assign a country to the customer
// Create a new context (to simulate making service calls over WCF)
MyContext context2 = new MyContext(ConnectionString);
var countries = from c in context2.Country.Include(c => c.Currency) where c.Name == "UK" select c;
customer.Country = countries.First();
// Assign a currency to the customer
// Again create a new context (to simulate making service calls over WCF)
MyContext context3 = new MyContext(ConnectionString);
customer.Currency = context3.Currency.First(e => e.Symbol == "GBP");
// Again create a new context (to simulate making service calls over WCF)
MyContext context4 = new MyContext(ConnectionString);
context4.Customers.Attach(customer);
// The following line will result in this exception:
// AcceptChanges cannot continue because the object's key values conflict with another
// object in the ObjectStateManager. Make sure that the key values are unique before
// calling AcceptChanges.
context4.SaveChanges();
scope.Complete();
}
I had this same problem in a Windows Service and solved it by creating and disposing the DBContext in every insert/update/get call. I was previously keeping the dbContext as a private variable in my repos and reusing it.
So far so good. YMMV. I can't say I understand exactly why it works - I haven't gone deep enough into Code First yet. The magic unicorn features are nice but I'm bordlerine about to throw it out and hand code the TSQL as the magic makes it hard to actually understand what is going on.