I have a very simple EF operation that fails: break the relationship between two entities as shown in the code below:
public async Task RemoveCurrentTeacherOfGroup(int groupId)
{
var group = await _dataContext.Groups.SingleAsync(g => g.Id == groupId);
group.Teacher = null;
await _dataContext.SaveChangesAsync();
}
The database was generated code-first. The entities are defined like this:
public class Teacher
{
public int Id { get; set; }
..
public virtual List<Group> Groups { get; set; }
}
public class Group
{
public int Id { get; set; }
..
public virtual Teacher Teacher { get; set; }
}
However, breaking the relationship doesn't work, Teacher keeps pointing to the same entity. When stepping with the debugger, I see that the Teacher property doesn't become null after .Teacher = null. I tried it with the synchronous alternative, which had the same effect:
public void RemoveCurrentTeacherOfGroup(int groupId)
{
var group = _dataContext.Groups.Single(g => g.Id == groupId);
group.Teacher = null;
_dataContext.SaveChanges();
}
If Teacher is not loaded you can't break the relationship. Either include it (eager-load) on the query:
_dataContext.Groups.Include(g => g.Teacher).Single(g => g.Id == groupId);
Or if lazy loading is enabled, access the property for reading before setting it to null:
var teacher = group.Teacher;
group.Teacher = null;
You see that "Teacher is not null after setting it to null" because the debugger is accessing the property for reading (lazy-loading it) after you have set it to null.
The value was already null before you hit the group.Teacher = null line since you hadn't previously loaded it (you can't however debug this, since accessing the property for reading would cause EF to actually load it if lazy loading is enabled). If you see the property value with the debugger before setting it to null, it'll work as expected and break the relationship, since Teacher would be loaded
Related
I am in the process of migrating to EF6 from Linq To Sql, and I have the autogenerated object
public partial class PCU
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public PCU()
{
this.PUs = new HashSet<PU>();
}
public int ID { get; set; }
public int FileNumberID { get; set; }
public Nullable<int> PartnerID { get; set; }
public virtual Company Company { get; set; }
public virtual File File { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<PU> PUs { get; set; }
}
where PartnerID is the Foreign key for company
when I call:
var company = dc.Set<PCU>().FirstOrDefault(c => c.FileNumber == fileNumber).Company;
I get a Null object, however if I call:
var company = dc.Set<PCU>().Where(c => c.FileNumber == fileNumber).Select(x => x.Company).First();
It returns the company object as expected. I have both LazyLoading and ProxyCreation enabled.
I understand I could use:
var company = dc.Set<PCU>().Include(x => x.Company).FirstOrDefault(c => c.FileNumber == fileNumber).Company;
however, as this is existing code, and I have the same problem for hundreds of different objects, this will mean massive amounts of changes. Is there an easier way to achieve this?
At first it indeed looks like:
dc.Set<PCU>().FirstOrDefault(c => c.FileNumber == fileNumber).Company
is similar to:
dc.Set<PCU>().Where(c => c.FileNumber == fileNumber).Select(x => x.Company).First()
but in case the foreign key 'Company' is null while using 'FirstOrDefault', returning 'Company' will obviously return null.
The second case, selects a valid 'Company' FK from the result set which was created by the 'Where' condition, and returns the first one from that set, this is why the 'Where' query returns a 'Company'.
If you don't wish to alter existing code, it seems to me that the best solution for you will be to actually see why you have null foreign keys in your database in the first place.
If it's the way its supposed to be (e.g., a null 'Company' entry could exist) then you'll have to take it into account in your queries, hence changing them to return only existing 'Company' entries.
Edit: I take it back, I missed the 'LazyLoading enabled' part 🤔
As a follow up, I believe the cause of the error is the name of the ForeignKey (PartnerID), and if it were named "CompanyID" it would work fine.
I have had to bite the bullet, and had to implement
var company = dc.Set<PCU>().Include(x => x.Company).FirstOrDefault(c => c.FileNumber == fileNumber).Company;
where neccesary. There does not seem to be another workaround, except for renaming the columns in my DB (which I can't do).
I have a situation where I'd like to eager load the organization property of a user being retrieved from the DB using LINQ-to-SQL (because of my using here attempts to get the property later fail because the context's connection is closed). The problem is that sometimes the user does not have an organization. In that case FetchUserByName returns null. Not what I want at all. Is there a way to get this to work the way I want, which is to return an instance of user whether they have an associated organization or not?
private static void EagerLoad(DataContext ctx)
{
var loadOpt = new DataLoadOptions();
loadOpt.LoadWith<User>(u => u.Organization);
ctx.LoadOptions = loadOpt;
}
public User FetchUserByName(string username)
{
User user = null;
using (var ctx = contextManager.GetSingleDataContext())
{
EagerLoad(ctx);
user = ctx.GetTable<User>().SingleOrDefault(u => u.UserName == username && u.IsActive);
}
return user;
}
Documenting what the fix was for others who may run into this.
LoadWith actually determines whether to use an INNER JOIN or an OUTER LEFT JOIN in the SQL created by LINQ-to-SQL by looking whether the foreign key in the data model class is nullable in the database. If it is nullable it uses the outer join.
[Column]
public int OrganizationId { get; set; } /*** Here is The Problem ***/
private EntityRef<Organization> _organization;
[Association(OtherKey = "OrganizationId", Storage = "_organization", ThisKey = "OrganizationId", IsForeignKey = true)]
public Organization Organization
{
get
{
return this._organization.Entity;
}
}
Changed the one line to:
public? int OrganizationId { get; set; }
And then everything worked as expected.
I'm having trouble with detecting changes of a navigation property:
My testing model looks like this:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public virtual Address Address { get; set; }
}
public class Address
{
public int Id { get; set; }
public string Name { get; set; }
}
I've created and saved an object of type Person with both Name and Address properties assigned. My problem is that if I fetch the Person object back from the database and I change the Address property (ex. to null) then EF doesn't detect the change!
My code is this:
using (var ctx = new EFContext())
{
Person p = ctx.People.First();
// p.Address IS NOT NULL!
p.Address = null;
var entry = ctx.Entry(p);
}
Why is entry.State Unchanged ?
Edit: If I call SaveChanges, the record is saved correctly (the Address become null)!
Edit 2: I've created the foreign key property as billy suggested. If I inspect the Person object in Visual Studio, the State is Modified. If I don't stop with the debugger inspecting the object's values, the state is Unchanged!
Edit 3: Loading the Person object using ctx.People.Include(x => x.Address).First(); solves the problem. Is there a way to avoid calling Include and continue to modify the Address property instead of the AddressId one?
First of all: You MUST follow #billy's advice to use Include. Your remark "p.Address IS NOT NULL!" is only true because you are watching p.Address in the debugger and thereby triggering lazy loading in the debugger, so the change of setting the address to null is detected. In release mode or when you don't inspect the properties in the debugger your code wouldn't work and no changes would be saved.
So, the answer to your Edit 3 is: No.
Second: var entry = ctx.Entry(p) only returns entity states and you didn't change an entity state but instead a relationship state, or more precisely you deleted a relationship. You can't inspect relationship states with the DbContext API but only with the ObjectContext API:
Person p = ctx.People.Include(x => x.Address).First();
p.Address = null;
var objCtx = ((IObjectContextAdapter)ctx).ObjectContext;
var objentr = objCtx.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted);
objentr will have an entry of type RelationshipEntry now:
EF will consider this relationship entry together with entity state entries when you call SaveChanges() and delete the relationship, i.e. set the Address foreign key column of the Person in the database to NULL.
About Edit 2: Changing a foreign key property (which is a scalar property in your model) is a change of the entity itself, so the entity state will be Modified in this case.
You need to include the Address nav. prop. in your query, otherwise EF won't consider changes to it when you save :
using (var ctx = new EFContext())
{
Person p = ctx.People.Include(x => x.Address).First();
//p.Address IS NOT NULL!
p.Address = null;
var entry = ctx.Entry(p);
}
You could also use foreign keys in your model, which I like very much :
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public virtual Address Address { get; set; }
public int? AddressId {get; set;}
}
...
using (var ctx = new EFContext())
{
Person p = ctx.People.First();
p.AddressId = null;
var entry = ctx.Entry(p);
}
In my application, before a reload is requested or the user leaves the item/view, I perform some checks to make sure there are no unsaved changes.
This is basically running off the currently accepted answer, but I wanted to provide an implementation and bring to the attention that you must call Context.ChangeTracker.DetectChanges() before the ObjectContext.ObjectStateManager can pick up relationship changes! I spent quite a bit of time debugging this, silly!
_EagleContext.ChangeTracker.DetectChanges();
var objectContext = ((IObjectContextAdapter)_EagleContext).ObjectContext;
var changedEntities = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified);
if (_EagleContext.ChangeTracker.Entries().Any(e => e.State == EntityState.Modified)
|| changedEntities.Count() != 0)
{
var dialogResult = MessageBox.Show("There are changes to save, are you sure you want to reload?", "Warning", MessageBoxButton.YesNo);
if (dialogResult == MessageBoxResult.No)
{
return;
}
}
// Continue with reloading...
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?
Consider this simple Model and ViewModel scenario:
public class SomeModel
{
public virtual Company company {get; set;}
public string name {get; set;}
public string address {get; set;}
//some other few tens of properties
}
public class SomeViewModel
{
public Company company {get; set;}
public string name {get; set;}
public string address {get; set;}
//some other few tens of properties
}
Problem that occurs is:
I have a edit page where company is not needed so I do not fetch it from database. Now when the form is submitted I do:
SomeModel destinationModel = someContext.SomeModel.Include("Company").Where( i => i.Id == id) // assume id is available from somewhere.
Then I do a
Company oldCompany = destinationModel.company; // save it before mapper assigns it null
Mapper.Map(sourceViewModel,destinationModel);
//After this piece of line my company in destinationModel will be null because sourceViewModel's company is null. Great!!
//so I assign old company to it
destinationModel.company = oldCompany;
context.Entry(destinationModel).State = EntityState.Modified;
context.SaveChanges();
And problem is even when I assign oldCompany to my company it is still null in database after savechanges.
Note:
If I change these lines:
destinationModel.company = oldCompany;
context.Entry(destinationModel).State = EntityState.Modified;
context.SaveChanges();
to these:
context.Entry(destinationModel).State = EntityState.Modified;
destinationModel.company = oldCompany;
context.Entry(destinationModel).State = EntityState.Modified;
context.SaveChanges();
Notice I change the state 2 times, it works fine. What can be the issue? Is this a ef 4.1 bug?
This is a sample console application to address the issue:
using System;
using System.Linq;
using System.Data.Entity;
using System.ComponentModel.DataAnnotations;
using AutoMapper;
namespace Slauma
{
public class SlaumaContext : DbContext
{
public DbSet<Company> Companies { get; set; }
public DbSet<MyModel> MyModels { get; set; }
public SlaumaContext()
{
this.Configuration.AutoDetectChangesEnabled = true;
this.Configuration.LazyLoadingEnabled = true;
}
}
public class MyModel
{
public int Id { get; set; }
public string Foo { get; set; }
[ForeignKey("CompanyId")]
public virtual Company Company { get; set; }
public int? CompanyId { get; set; }
}
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyViewModel
{
public string Foo { get; set; }
public Company Company { get; set; }
public int? CompanyId { get; set; }
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer<SlaumaContext>(new DropCreateDatabaseIfModelChanges<SlaumaContext>());
SlaumaContext slaumaContext = new SlaumaContext();
Company company = new Company { Name = "Microsoft" };
MyModel myModel = new MyModel { Company = company, Foo = "Foo"};
slaumaContext.Companies.Add(company);
slaumaContext.MyModels.Add(myModel);
slaumaContext.SaveChanges();
Mapper.CreateMap<MyModel, MyViewModel>();
Mapper.CreateMap<MyViewModel, MyModel>();
//fetch the company
MyModel dest = slaumaContext.MyModels.Include("Company").Where( c => c.Id == 1).First(); //hardcoded for demo
Company oldCompany = dest.Company;
//creating a viewmodel
MyViewModel source = new MyViewModel();
source.Company = null;
source.CompanyId = null;
source.Foo = "foo hoo";
Mapper.Map(source, dest); // company null in dest
//uncomment this line then only it will work else it won't is this bug?
//slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
dest.Company = oldCompany;
slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
slaumaContext.SaveChanges();
Console.ReadKey();
}
}
}
Automapper always updates every property from the source instance to the destination instance by default. So if you don't want your Company property overwritten then you have to explicitly configure this for your mapper:
Mapper.CreateMap<MyViewModel, MyModel>().ForMember(m => m.Company, c => c.UseDestinationValue());
So far nothing EF related. But if you are using this with EF you have to use your navigation property Company and the CompanyId consistently: you also need to use the destination value for CompanyId during mapping:
Mapper.CreateMap<MyViewModel, MyModel>().ForMember(m => m.CompanyId, c => c.UseDestinationValue());
EDIT: But the problem is not that your Company is null but after resetting it is still null in the DB. And this caused by the fact that if you are having an explicit Id property like 'CompanyId' you have to mantain it. So it is not enough to call destinationModel.company = oldCompany; you also need to call destinationModel.companyId = oldCompany.Id;
And because you retrieved your dest entity from the context it's already doing the change tracking for you therefore there is no need set EntityState.Modified.
EDIT: Your modified sample:
Mapper.CreateMap<MyModel, MyViewModel>();
Mapper.CreateMap<MyViewModel, MyModel>();
//fetch the company
MyModel dest = slaumaContext.MyModels.Include("Company").Where(c => c.Id == 18).First(); //hardcoded for demo
var oldCompany = dest.Company;
//creating a viewmodel
MyViewModel source = new MyViewModel();
source.Company = null;
source.CompanyId = null;
source.Foo = "fdsfdf";
Mapper.Map(source, dest); // company null in dest
dest.Company = oldCompany;
dest.CompanyId = oldCompany.Id;
slaumaContext.SaveChanges();
The second EDIT in #nemesv's answer or the finetuning of AutoMapper is the way to go in my opinion. You should accept his answer. I only add an explanation why your code doesn't work (but your code with setting the state twice does). First of all, the problem has nothing to do with AutoMapper, you will get the same behaviour when you set the properties manually.
The important thing to know is that setting the state ( Entry(dest).State = EntityState.Modified ) does not only set some internal flag in the context but the property setter for State calls actually some complex methods, especially it calls DbContext.ChangeTracker.DetectChanges() (if you don't disable AutoDetectChangesEnabled).
So, what happens in the first case:
// ...
Mapper.Map(source, dest);
dest.Company = oldCompany;
// at this point the state of dest EF knows about is still the state
// when you loaded the entity from the context because you are not working
// with change tracking proxies, so the values are at this point:
// dest.CompanyId = null <- this changed compared to original value
// dest.Company = company <- this did NOT change compared to original value
// The next line will call DetectChanges() internally: EF will compare the
// current property values of dest with the snapshot of the values it had
// when you loaded the entity
slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
// So what did EF detect:
// dest.Company didn't change, but dest.CompanyId did!
// So, it assumes that you have set the FK property to null and want
// to null out the relationship. As a consequence, EF also sets dest.Company
// to null at this point and later saves null to the DB
What happens in the second case:
// ...
Mapper.Map(source, dest);
// Again in the next line DetectChanges() is called, but now
// dest.Company is null. So EF will detect a change of the navigation property
// compared to the original state
slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
dest.Company = oldCompany;
// Now DetectChanges() will find that dest.Company has changed again
// compared to the last call of DetectChanges. As a consequence it will
// set dest.CompanyId to the correct value of dest.Company
slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
// dest.Company and dest.CompanyId will have the old values now
// and SaveChanges() doesn't null out the relationship
So, this is actually normal change tracking behaviour and not a bug in EF.
One thing I find disturbing is that you have a ViewModel which apparently has properties you are not using in the view. If your ViewModel wouldn't have Company and CompanyId all the trouble would disappear. (Or configure at least AutoMapper to not map these properties, as shown by #nemesv.)