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.)
Related
I'm learning how to use Entity Framework, having a NHibernate background.
There are plenty of tutorials on the net, but I didn't find something for my specific case.
I don't want to delete the Category record, I only want to delete the relation!
I have following poco's:
public class TrainingCourse
{
[Key]
public int CourseId { get; set; }
public string CourseName { get; set; }
public virtual ICollection<TrainingContent> Content { get; set; } = new List<TrainingContent>();
}
public class TrainingContent
{
[Key]
public int ContentId { get; set; }
public string ContentName { get; set; }
public int? CategoryId { get; set; }
[ForeignKey("CategoryId")]
public Category Category { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
}
In the database I have following 'data' inserted
var category = new Category { CategoryName = "Category 1" };
var course = new TrainingCourse { CourseName = "Course 1" };
context.TrainingCourses.Add(course);
var content = new TrainingContent { ContentName = "Content 1", Category = category };
context.TrainingContents.Add(content);
course.Content.Add(content);
I now want to delete the relation from TrainingContent to Category: in database terms speaking, I want to set my foreignkey CategoryId in the table TrainingContent to null.
When doing this in one context, it is working, my foreign key is NULL after the save:
using (var context = new ClientContext())
{
_course = context.TrainingCourses.Include(c => c.Content.Select(cat => cat.Category)).FirstOrDefault(n => n.CourseName == "Course 1");
_course.Content.ToList()[0].ContentName = "Content 1 changed";
_course.Content.ToList()[0].Category = null;
context.SaveChanges();
}
But in the real world, we are working with disconnected entities.
I simulate this with following code
using (var context = new ClientContext())
{
_course = context.TrainingCourses.Include(c => c.Content.Select(cat => cat.Category)).FirstOrDefault(n => n.CourseName == "Course 1");
}
_course.Content.ToList()[0].ContentName = "Content 1 changed";
_course.Content.ToList()[0].Category = null;
using (var context = new ClientContext())
{
context.Entry(_course.Content.ToList()[0]).State = EntityState.Modified;
context.SaveChanges();
}
This is not working, the content name is changed, but my foreign key is still defined.
If I set the navigation property to NULL and the FK to NULL, it is working:
using (var context = new ClientContext())
{
_course = context.TrainingCourses.Include(c => c.Content.Select(cat => cat.Category)).FirstOrDefault(n => n.CourseName == "Course 1");
}
_course.Content.ToList()[0].ContentName = "Course 1 changed";
_course.Content.ToList()[0].Category = null;
_course.Content.ToList()[0].CategoryId = null;
using (var context = new ClientContext())
{
context.Entry(_course.Content.ToList()[0]).State = EntityState.Modified;
context.SaveChanges();
}
Why this behaviour? I'm new to EF and EF Core, so maybe there is a simple explication?
I tested this originally with with Entity Framework Core, but same behaviour.
Your problem is that with a navigation property of null, EF will not check referential integrity (this will happen on SQL side).
However, this is necessary most of the time. Consider you want to attach a content to another category, you might want to do:
var content=context.Contents.FirstOrDefault();
content.CategoryId=2;
context.Entry(content).State=EntityState.Modified;
context.SaveChanges();
this looks fine, doesn't it? However, notice you never actually loaded (lazily or explicitly) the corresponding Category object - it is still null.
So, in this example and in yours the code itself looks fine, but the FK and the navigation property do not match. However, since the navigation property does not have to be correct (unless tracked by the context, since then it is loaded and EF knows its correct), EF will expect the FK to be correct and therefore save this value into the database.
Notice that this issue will not happen in a few cases: The first obviously is when EF was able to track the category in its ChangeTracker - then it will know you actually want to consider the category navigation property, not necessarily the FK.
The second is when your FK is not part of your object. Since then EF has to create a FK, however you cannot set it yourself. Then EF has to set the FK corresponding to the navigation property. (notice that this will not work with null. null means either the value is not loaded or the value is nothing (along with some other meanings) and EF cannot determine which one is correct).
Also, keep in mind that in disconnected scenario EF will check the referential integrity of the whole object tree upon Attaching the root of that tree - if any FK's do not match the navigation property, EF will throw an Exception (with the exception of null because of reasons above).
And: when you work in disconnected scenario, be aware that when any parent entries are in Added state, all child objects have to be attached (atm I do not know if they also have to be Modified/New), since otherwise EF cannot fix the child object's FK's and throw an exception.
I have two classes: Owner and Dog. In the Dog class there is a Navigation Property defined like this:
public class Dog {
//Other properties
public int OwnerId { get; set; }
[ForeignKey("OwnerId")]
public virtual Owner Owner{ get; set; }
}
I also have the following (example) method
public void SetOwnerOfFirstDog(int ownerId)
{
//var owner = context.Owners.First(e => e.Id == ownerId);
var dog = context.Dogs.First();
dog.OwnerId = ownerId;
context.SaveChanges();
}
This all works but when I later use the same db context and look up the same dog entity it's Owner property is null. (When I use another db context it is not null.)
However, if I uncomment the first line in the SetOwnerOfFirstDog method the Owner property will be correctly set. But of course this is an extra query to the database and I'd like to avoid it.
So my question:
How can I make sure that the Owner property is correctly filled in when I look it up after I have set the Dog's Owner by Id.
I am using lazy loading.
You can use Include to load related entities
public void SetOwnerOfFirstDog(int ownerId)
{
var dog = context.Dogs.Include(x => x.Owner).First();
dog.OwnerId = ownerId;
context.SaveChanges();
}
I have sets of entities all of them are derived from abstract class
public abstract class NamedEntity : INamedEntity
{
#region Public Properties
public string Description { get; set; }
public string Id { get; set; }
public string Name { get; set; }
#endregion
}
When I persist all entities I want to use Name field as a key, so I override DocumentKeyGenerator and provide such implementation:
store.Conventions.DocumentKeyGenerator = entity =>
{
var namedEntity = entity as NamedEntity;
if (namedEntity != null)
{
return string.Format("{0}/{1}", store.Conventions.GetTypeTagName(entity.GetType()), namedEntity.Name);
}
return string.Format("{0}/", store.Conventions.GetTypeTagName(entity.GetType()));
};
It works fine when I persist the list of entities for the first time, but if I want to persist them again I get an exception
PUT attempted on document 'xxxxx' using a non current etag
I just started using RavenDB, so I cannot understand what I am doing wrong?
Just a guess, but it's probably not with your key generation, but how you are storing them.
On first usage you probably have something like:
var myEntity = new MyEntity(...);
session.Store(myEntity);
...
session.SaveChanges();
That part is fine, but on subsequent usage, you should not be doing the same thing. Instead, it should be more like this:
var myEntity = session.Load<MyEntity>("myentities/foobar");
myEntity.Something = 123;
...
session.SaveChanges();
Note there is no call to .Store() when making changes. This is because the entity is "tracked" by the session, and all changes to it are automatically persisted when you call .SaveChanges()
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?