3

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();

        }
    }
}
Jaggu
  • 6,298
  • 16
  • 58
  • 96
  • Is there a foreign key property for the `Company` in your `SomeModel` and/or `SomeViewModel`, something like `CompanyId`? And why do you load the `Company` at all with `Include` when you know that it's `null` in your ViewModel? In my opinion you could remove the `Include`. – Slauma Oct 18 '11 at 12:06
  • Is the foreign key property nullable and also in `SomeViewModel`? What happens then after `Mapper.Map`, does it overwrite the FK value in the `destinationModel`? – Slauma Oct 18 '11 at 12:32
  • @Slauma: Yes you are pretty much correct. The foreign key is nullable. Yes after Mapper.Map the fk values are null but then I explicitly assign it a value but it has no effect. It "liked" null only. – Jaggu Oct 19 '11 at 04:11
  • Can you show what you are exactly doing with the FK properties in your code above? Somehow I have the feeling that there is the problem. – Slauma Oct 19 '11 at 10:47
  • @Slauma: I have created a simple demo application. Please read my edited question. You can paste the source code in console application and tell me why is this happening! – Jaggu Oct 19 '11 at 11:59

2 Answers2

3

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();
nemesv
  • 138,284
  • 16
  • 416
  • 359
  • Your solution is good but not exactly what I wanted since you want me to change my CreateMaps. My question still exist: Why does it take null even though I assign company? – Jaggu Oct 19 '11 at 12:58
  • Great answer nemesv. I really appreciate it! Now things got more clearer to me after Slauma's explanation and your answer. – Jaggu Oct 20 '11 at 04:20
  • @nemesv what if the property that becomes `null` is a collection? See my question here http://stackoverflow.com/q/41430679/613605. I'd greatly appreciate any help & advice. – J86 Jan 03 '17 at 01:28
2

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.)

Slauma
  • 175,098
  • 59
  • 401
  • 420
  • It's is not disturbing. Actually I have a shared ViewModel for my edit, delete and add operations. In Add operation I do need companyId and company but in edit and delete I do not. Is it a bad practise to create shared viewmodels between add, edit and delete? I didn't want to create class overloads for each add, edit and delete operations. So I tried to reuse my class. Is this bad? – Jaggu Oct 20 '11 at 04:14
  • Probably I should create it as a different question with a link to this question. – Jaggu Oct 20 '11 at 04:20
  • 1
    @Jaggu ViewModel's are usually tailored for the View they serve. For me I find that user requirements, business rules, or other nuances such as making something user friendly dictates that each view varies somewhat. Thus you might have a ViewModel for each View. This has at least been my own experience with MVC. – AaronLS Oct 27 '11 at 02:21