I am using current Version of EF core and got trouble to load the original values.
I've the following constellation:
public class User
{
public string Name { get; set; }
public ICollection<UserRole> UserRoles { get; set; }
}
public class UserRole
{
public string Name { get; set; }
public ICollection<User> Users { get; set; }
}
Now I add a new UserRole to the User - something like that
user.UserRoles.Add(new UserRole() {Name="Admin", User=user}
After that I want to "revert" this step and tried to use Reload
dbContext.Entry(user).Reload();
But still there is the new Role added to the Collection.
I also tried
dbContext.Entry(user).Collection(x=>x.UserRole).Load();
But this also not work.
Is there something I am doing generally wrong?
Thanks for you help!
You can reset the context using
dbContext.ChangeTracker.Entries()ToList().ForEach(e => e.State = EntityState.Detached);. If you are using EF Core 5.0, you can use ChangeTracker.Clear().
Note: As #AluanHaddad says in one of the comments on the question, this will revert all unsaved changes. You can use a linq Where expression to limit which objects have their states reset:
dbContext.ChangeTracker.Entries()
.Where(e => e.Entity is UserRole)
.Where(e => e => e.State = EntityState.Added)
.ToList()
.ForEach(e => e.State = EntityState.Detached);
Also, you could consider implementing the Unit of work pattern so that there is less risk of having unrelated changes a that will be reverted.
Related
I have two tables(Teacher and Course) in my database and EF mapping:
public partial class Teacher
{
public long Id { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
And I load a teacher like this:
public Teacher GetTeacher(long id)
{
using(var entities = new MyEntities())
{
var teacher = entities.Teachers.Where(t => t.Id == id).FirstOrDefault();
return teacher;
}
}
Now sometimes I want to load Courses list for my Teacher. So I changed implementation like this:
public Teacher GetTeacher(long id, bool loadCourses)
{
using(var entities = new MyEntities())
{
var teacherQuery = entities.Teachers.Where(t => t.Id == id);
if(loadCourses)
{
teacherQuery.Include(t => t.Courses);
}
return teacherQuery.FirstOrDefault();
}
}
And this worked fine. But after that, I decided to turn off LazyLoading for Courses property since I've decided to control this manually by loadCourses field.
So I've just removed virtual from Courses collection:
public ICollection<Course> Courses { get; set; }
This did help to turn off LazyLoading but Include stopped working and my Courses collection never loads.
So the question is: is it possible to do eager loading for Courses collection with LazyLoading disabled?
P.S. I don't actually use entity objects in my application but convert them to my Domain objects. So that's why I decided to use bool loadCourses field instead of actually using LazyLoading.
Also, I'd like to have one SELECT query(with JOIN, of course) sent to the database instead of two separate SELECTs.
Shortly after asking the question, I found an answer which is dead simple: there is a bug under if(loadCourses) that I was not assigning teacherQuery. Fixed code looks like this and works fine:
if(loadCourses)
{
teacherQuery = teacherQuery.Include(t => t.Courses);
}
Also would like to mention a useful link that #VidmantasBlazevicius provided. It contains answers on how virtual keyword affects your entities.
I'll try explain simply my Entity Framework model. I have a User object which has a collection of zero or more UserInterest objects. Each user interest object has only three properties, unique ID, User Id and Description.
Whenever the user updates the User object, it should also update the related UserInterest objects but because these are free form (ie not part of a list of allowed interests), I want the user to pass in a list of type "string" to the webmethod of the names of all their interests. The code would ideally then look at the users existing list of interests, remove any that were no longer relevant and add in new ones and leave the ones which already exist.
My object model definitions
[Table("User")]
public class DbUser {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public virtual IList<DbUserInterest> Interests { get; set; }
}
[Table("UserInterest")]
public class DbUserInterest : IEntityComparable<DbUserInterest>
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public virtual DbUser User { get; set; }
public int? UserId { get; set; }
}
The context Fluent mappings
modelBuilder.Entity<DbUser>()
.HasKey(u => u.UserId);
modelBuilder.Entity<DbUser>()
.Property(u => u.UserId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<DbUser>()
.HasMany(u => u.Interests)
.WithRequired(p => p.User)
.WillCascadeOnDelete(true);
modelBuilder.Entity<DbUserInterest>()
.HasKey(p => p.Id);
modelBuilder.Entity<DbUserInterest>()
.Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<DbUserInterest>()
.HasRequired(p => p.User)
.WithMany(u => u.Interests)
.WillCascadeOnDelete(true);
And lastly my webmethod code and repository method to do the update
public UpdateUserProfileDetailsResponse UpdateUserProfileDetails(UpdateUserProfileDetailsRequest request)
{
try
{
var dbItem = _userDataRepository.GetItem(request.Header.UserId);
dbItem.Interests.Clear();
foreach (var dbInterest in request.UserInterests)
dbItem.Interests.Add(new DbUserInterest { Name = dbInterest, UserId = dbItem.UserId});
_userDataRepository.UpdateItem(dbItem);
_userDataRepository.Save();
}
catch (Exception ex)
{
}
}
public override bool UpdateItem(DbUser item)
{
var dbItem = GetItem(item.UserId);
if (dbItem == null)
throw new DataRepositoryException("User not found to update", "UserDataRepository.UpdateItem");
var dbInterests = Work.Context.UserInterests.Where(b => b.UserId == item.UserId).ToList();
var interestsToRemove = (from interest in dbInterests let found = item.Interests.Any(p => p.IsSame(interest)) where !found select interest).ToList();
var interestsToAdd = (from interest in item.Interests let found = dbInterests.Any(p => p.IsSame(interest)) where !found select interest).ToList();
foreach (var interest in interestsToRemove)
Work.Context.UserInterests.Remove(interest);
foreach (var interest in interestsToAdd)
{
interest.UserId = item.UserId;
Work.Context.UserInterests.Add(interest);
}
Work.Context.Entry(dbItem).State = EntityState.Modified;
return Work.Context.Entry(dbItem).GetValidationResult().IsValid;
}
When I run this, at the Repository.Save() line I get the exception
Assert.IsTrue failed. An unexpected error occurred: An error occurred while updating the entries. See the inner exception for details.
But interestingly in the webmethod if I comment out the line dbItem.Interests.Clear(); it doesn't throw an error, although then of course you get duplicates or extra items as it thinks everything is a new interest to add. However removing this line is the only way I can get the code to not error
Before, I had the UserId property of the Interest object set to non nullable and then the error was slightly different, something about you cannot change the relationship of a foreign key entity that is non nullable, which is why I changed the property to nullable but still no go.
Any thoughts?
You can't just clear the collection and then try to rebuild it. EF doesn't work that way. The DbContext keeps track of all of the objects that were brought back from the database in its Change Tracker. Doing it your way will of course cause duplicates because EF sees that they're not in the Change Tracker at all so they must be brand new objects necessitating being added to the database.
You'll need to either do the add/remove logic in your UpdateUserProfileDetails method, or else you have to find a way to pass request.UserInterests into your UpdateItem method. Because you need to adjust the existing entities, not the ones found on the request (which EF thinks are new).
you could try in this way
remove
dbItem.Interests.Clear();
then
foreach (var dbInterest in request.UserInterests){
if(dbItem.Interests.Any()){
if (dbItem.Interests.Count(i=> i.Name==dbInterest && i.UserId==dbItem.UserId) == 0){
dbItem.Interests.Add(new DbUserInterest { Name = dbInterest, UserId = dbItem.UserId});
}
}
}
I'm struggling to understand why when I remove a child Settings object from MyUser.Settings and SAVE MyUser I get SQL errors like below:
Cannot insert the value NULL into column 'MyUserId', table '###.Settings'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
What I would expect to happen is that removing the item from the collection, then saving MyUser causes NHibernate to issue a DELETE command for the given child. However, what it does is UPDATE the relevant row for the Settings object, setting MyUserId to NULL - which isn't allowed as I'm using a Composite Key.
I've tried so many combinations of Inverse() and the various Cascade options but nothing seems to work. I should point out that Adding to the collection works perfectly when I save MyUser.
I'm totally baffled!
Below is pseudo code to try and explain my entities and mappings.
public class SettingType
{
public virtual int SettingTypeId { get; set; }
public virtual string Name { get; set; }
public virtual bool Active { get; set; }
}
public class Setting
{
public virtual MyUser MyUser { get; set; }
public virtual SettingType SettingType { get; set; }
public virtual DateTime Created { get; set; }
}
public class MyUser
{
public virtual int MyUserId { get; set; }
public virtual IList<Setting> Settings { get; set; }
public virtual string Email { get; set; }
public void AddSetting(SettingType settingType, DateTime now)
{
var existing = _settings.SingleOrDefault(s => s.SettingType.SettingTypeId == settingType.SettingTypeId);
if (existing != null)
{
existing.Updated = now;
}
else
{
var setting = new Setting
{
MyUser = this,
SettingType = settingType,
Created = now,
};
_settings.Add(setting);
}
}
public void RemoveSetting(SettingType settingType)
{
var existingPref = _settings.SingleOrDefault(s => s.SettingType.SettingTypeId == settingType.SettingTypeId);
if (existingPref != null)
{
_settings.Remove(existingPref);
}
}
private readonly IList<Setting> _settings = new List<Setting>();
}
And my mappings:
public class SettingTypeMap : IAutoMappingOverride<SettingType>
{
public void Override(AutoMapping<SettingType> mapping)
{
mapping.Table("SettingTypes");
mapping.Id(m => m.SettingTypeId).GeneratedBy.Identity();
mapping.Map(m => m.Name).Not.Nullable().Length(100);
mapping.Map(m => m.Active).Not.Nullable().Default("0");
}
}
public class SettingMap : IAutoMappingOverride<Setting>
{
public void Override(AutoMapping<Setting> mapping)
{
mapping.Table("Settings");
mapping.CompositeId()
.KeyReference(m => m.MyUser)
.KeyReference(m => m.SettingType);
mapping.Map(m => m.Created).Not.Nullable().Default("CURRENT_TIMESTAMP");
mapping.Map(m => m.Updated).Nullable();
}
}
public class MyUserMappingOverride : IAutoMappingOverride<MyUser>
{
public void Override(AutoMapping<MyUser> mapping)
{
mapping.Table("MyUsers");
mapping.Id(m => m.MyUserId).GeneratedBy.Identity();
mapping.Map(m => m.Email).Not.Nullable().Length(200);
mapping.HasMany(m => m.Settings).KeyColumn("MyUserId").Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
}
}
All using:
FluentNHibernate v1.3.0.733
NHibernate v3.3.1.4000
UPDATE: After a few suggestions I've tried to change the mapping for MyUser entity.
First to this:
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
This gives the error: Given key was not present in the dictionary
So tried to add second key column:
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.KeyColumn("SettingTypeId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
But this then causes odd behaviour when loading the Settings collection from the DB for a given MyUserId. Looking at the nh profiler I see a second SELECT ... FROM Settings but setting the SettingTypeId same as value for MyUserId.
Still totally baffled. Has cost me too much time so going to revert to adding a primary key id field to the Settings entity. Maybe you just can't do what I'm trying using NHibernate. In pure SQL this is simple.
You should use the Inverse mapping
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
This will allow NHibernate to ask the setting itself to be deleted. Otherwise, NHibernate firstly tries to delete the relation, and would try to delete the entity.
See: 6.4. One-To-Many Associations
Very Important Note: If the column of a
association is declared NOT NULL, NHibernate may cause constraint
violations when it creates or updates the association. To prevent this
problem, you must use a bidirectional association with the many valued
end (the set or bag) marked as inverse="true". See the discussion of
bidirectional associations later in this chapter.
I am using MVC.NET web api, EF with DB first, and I have lazy loading turned off on my context. EF is returning way too much data, even with LazyLoading turned off.
For example, I have Users with one Role. When I query for Users and Include Role, the Role.Users property is automatically filled with data since Users have been loaded into the context.
Why can't I get EF to give me JUST what I request? Or am I missing something big here?
public partial class User
{
public int UserID { get; set; }
public string Title { get; set; }
public string Email { get; set; }
public int RoleID { get; set; }
....
public virtual Role Role { get; set; }
}
public partial class Role
{
public int RoleID { get; set; }
public string RoleName { get; set; }
....
public virtual ICollection<User> Users { get; set; }
}
return db.Users.Include(u => u.Role);
// ^^ user.Role.Users is filled with 1000s of users
TL;DR - I want EF to never load data into navigation properties/collections unless I .Include() it directly. When serializing to JSON I want just what I ask for explicitly. It seems that even with lazy loading off, navigation properties that are already in the context (ie usually "circular references") will be loaded and returned.
The behaviour your are seeing is called Relationship Fixup and you cannot disable it.
If you are loading users with roles to serialize them and sent them to somewhere I guess that you don't want to track changes of entities in the context they have been loaded in. So, there is no need to attach them to the context and you can use:
return db.Users.Include(u => u.Role).AsNoTracking();
Or use a projection into an object specialized for serialization, as suggested by #STLRick.
You can select only what you need by using Select().
var users = _db.Users.Select(x => new
{
UserID = x.UserID,
Title = x.Title,
Email = x.Email,
RoleID = x.RoleID
}).AsEnumerable();
You are right that with lazy loading on, you will get back navigation properties because they are "touched" by the serializer which causes them to be loaded. Lazy loading should be off if you want the properties to come back as null. That said, it "seems" that once entities are loaded into the context (through other queries, for example), they will be processed by the serializer. So the answer is to tell the serializer not to return the navigation properties. The best way I've been able to find to do this is to use DTOs (Data Transfer Objects). This allows you to return exactly the data you want rather than your actual entities.
Your DTO might look something like this:
public partial class UserDto
{
public UserDto(user User)
{
UserID = user.UserID;
Title = user.Title;
//... and so on
}
public int UserID { get; set; }
public string Title { get; set; }
public string Email { get; set; }
public int RoleID { get; set; }
//exclude the Role navigation property from your DTO
}
...and then you could do something like this:
return db.Users.Include(u => u.Role).Select(user => new UserDto(user));
I don't want it to load anything besides what I tell it to include.
It looks like you need to use Explicit Loading. Basically, you could load the specific entities like this:
context.Include("Roles")
To my best knowledge that should not include related entities. Lazy loading should indeed be disabled and you could load navigational properties explicitly with Load.
First: Turn Lazy Loading on.
Second: If you want to filter down what you retrieve and return, then do a custom return object or something.
from u in db.Users
join r in db.Roles
on u.RoleID equals r.RoleID
select new { u.UserID, u.Title, u.Email, r.RoleName }
Or something like that. You will have a minimal return object and your object graph will be tiny.
I think i ran into a bug, it seems that EF is not handling references well after deleting and reinserting an entity. I've managed to reproduce it with the code below (assume all asserts pass except the one i talk about in the comments):
var database = new TestEntities();
// select and delete the info record
var info = database.Info.First(i => i.ID == 1);
Assert.AreEqual(1, info.MemberID);
// when i uncomment the line below the last Assert fails
// Assert.IsNotNull(info.Member);
database.Info.Remove(info);
// add it again and persist it to the database
database.Info.Add(new Info {
ID = 1,
MemberID = 1
});
database.SaveChanges();
// should not be null ? EDIT: i guess i understand this becoming null
Assert.IsNull(info.Member);
// and even here its still null
info = database.Info.First(i => i.ID == 1);
Assert.IsNull(info.Member);
Can anyone tell me whats going on here?
EDIT:
My entities are generated using database first and im using the DbContext/POCO generator.
public partial class Member
{
public Member()
{
this.Info = new HashSet<Info>();
}
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Info> Info { get; set; }
}
public partial class Info
{
public int ID { get; set; }
public int MemberID { get; set; }
public virtual Member Member { get; set; }
}
It turns out that it had nothing to do with deleting and reinserting, not really anyway. It was was so obvious ...
I was inserting using a POCO which is not eagerly loaded and does not have any lazy loading capabilities ...
The second time i queried for the same record i was expecting a proxy, but it seems that the POCO was cached by EF and that is what it returned meaning still no eager or lazy loading.
I can fix it by making sure EF doesn't retrieve the second query from cache, inserting using a proxy (var info = database.Info.Create()) or including member in the query (database.Info.Include(i => i.Member).First(i => i == 1)).
Given this
var info = database.Info.First(i => i.ID == 1);
Assert.AreEqual(1, info.MemberID);
Aren't you comparing info.ID to info.MemberID here? Isn't it possible that ID and MemberID actually are different?
Also, shouldn't you be using .SaveChanges() after
database.Info.Remove(info);
?
Also, info does not have .member available if none have been instantiated. Is there a correlating Member with MemberID equal to info.MemberId?