Сущность Framework 4.1 кода в первую очередь и автоматическая выдача карт - PullRequest
3 голосов
/ 18 октября 2011

Рассмотрим простой сценарий Model и ViewModel:

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
}

Возникла следующая проблема:

У меня есть страница редактирования, где компания не нужна, поэтому я не выбираю ее из базы данных.Теперь, когда форма отправлена, я делаю:

SomeModel destinationModel = someContext.SomeModel.Include("Company").Where( i => i.Id == id) // assume id is available from somewhere.

Затем я делаю

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

И проблема в том, что даже когда я назначаю oldCompany своей компании, в базе данных после сохранения остается пустое значение,

Примечание:

Если я изменю эти строки:

destinationModel.company = oldCompany;

context.Entry(destinationModel).State = EntityState.Modified;

context.SaveChanges();

на эти:

context.Entry(destinationModel).State = EntityState.Modified;

destinationModel.company = oldCompany;

context.Entry(destinationModel).State = EntityState.Modified;

context.SaveChanges();

Обратите внимание, что я изменяюсостояние 2 раза, работает нормально.В чем может быть проблема?Это ошибка ef 4.1?

Это пример консольного приложения для решения проблемы:

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

        }
    }
}

Ответы [ 2 ]

3 голосов
/ 19 октября 2011

Automapper всегда обновляет каждое свойство от исходного экземпляра до конечного экземпляра по умолчанию.Поэтому, если вы не хотите, чтобы свойство вашей компании было перезаписано, вы должны явно настроить его для своего маппера:

Mapper.CreateMap<MyViewModel, MyModel>().ForMember(m => m.Company, c => c.UseDestinationValue());

Пока что ничего не связано с EF.Но если вы используете это с EF, вы должны последовательно использовать свойство навигации Company и CompanyId: вам также нужно использовать значение назначения для CompanyId во время отображения:

Mapper.CreateMap<MyViewModel, MyModel>().ForMember(m => m.CompanyId, c => c.UseDestinationValue());

EDIT : Но проблема не в том, что ваша компания имеет нулевое значение, а после сброса она по-прежнему равна нулю в БД.И это связано с тем, что если у вас есть явное свойство Id, например, «CompanyId», вы должны поддерживать его.Таким образом, недостаточно вызвать destinationModel.company = oldCompany;, вам также нужно вызвать destinationModel.companyId = oldCompany.Id;

И поскольку вы извлекаете свою сущность dest из контекста, она уже выполняет отслеживание изменений для вас, поэтому нет необходимости устанавливать EntityState.Modified.

РЕДАКТИРОВАТЬ : Ваш измененный образец:

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();
2 голосов
/ 19 октября 2011

Второй EDIT в ответе @ nemesv или настройка AutoMapper - это, на мой взгляд, путь.Вы должны принять его ответ.Я только добавляю объяснение, почему ваш код не работает (но ваш код с установкой состояния дважды работает).Прежде всего, проблема не имеет ничего общего с AutoMapper , вы получите то же поведение, когда будете устанавливать свойства вручную.

Важно знать, что установка состояния (Entry(dest).State = EntityState.Modified) не только устанавливает некоторый внутренний флаг в контексте, но установщик свойства для State вызывает на самом деле некоторые сложные методы, особенно это вызывает DbContext.ChangeTracker.DetectChanges() (если вы не отключите AutoDetectChangesEnabled).

Итак, что происходит в первом случае:

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

Что происходит во втором случае:

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

Итак, это на самом деле нормальное поведение отслеживания изменений, а неошибка в EF.

Одна вещь, которая меня беспокоит, это то, что у вас есть ViewModel, которая, очевидно, имеет свойства, которые вы не используете в представлении.Если ваша ViewModel не будет иметь Company и CompanyId, все проблемы исчезнут.(Или настройте хотя бы AutoMapper, чтобы не отображать эти свойства, как показано @nemesv.)

...