What would cause the Entity Framework to save an u

2019-01-09 19:07发布

I'm running into an interesting bug in my ASP.NET MVC 3 application using Entity Framework 4.1 Code First. I have three classes/tables that are joined in sequence. There's an Invitation that has a reference to a Project which then has a reference to Company.

When I load a company and save it everything is fine. Same for projects. However when the invitation gets edited and saved, it wipes out the fields in the company. They're all just blank!

When I edit the project, I need to show some info from the company, so I'm explicitly loading that with .Include(x => x.Company). When I edit the invitation though, I don't need the company so I haven't bothered including it.

I'd think that if the object wasn't ever loaded, then there shouldn't be any reason for EF to flag it as edited, right?

Update: After much debugging via commenting out lines of code I've narrowed it down some.

The actual object being cleared was a Contact object that's referenced by the company. And it wasn't really being cleared so much as a new contact was created in the constructor (so it wouldn't be null for new Companies.)

So I guess that changes my question: Is there a way to have a referenced property set to a default value without breaking EF?

public class InvitationController
{
    [HttpPost]
    public RedirectToRouteResult AcceptInvitation(int id, int companyId, int projectId, Invitation invitation)
    {
        // This line triggered the problem by loading a company, without 
        // eagerly loading the contacts.
        CheckAuthorizationEdit(companyId, CommunicationService.GetById(id));

        var dbResponse = InvitationService.GetPreviousResponse(companyId, projectId);
        dbResponse.WillBid = invitation.WillBid;
        InvitationService.Save(dbResponse);

        return RedirectToAction("Response", new { id, companyId } );
    }

    private void CheckAuthorizationEdit(int companyId, Communication communication)
    {
        var companyIds = communication.DistributionList.Companies.Select(c => c.Id).ToList();
        //CheckAuthorization(companyIds);
    }
}

public class InvitationService
{
    public Invitation GetPreviousResponse(int companyId, int projectId)
    {
        return (from invitation in _db.Invitations
                where invitation.ProjectId == projectId && invitation.SenderCompanyId == companyId
                select invitation).SingleOrDefault();
    }

    public void Save(Invitation invitation)
    {
        _db.SaveChanges();
    }
}

public class Invitation
{
    public int Id { get; set; }
    public int ProjectId { get; set; }
    [ForeignKey("ProjectId")]
    public virtual Project Project { get; set; }
    // ...
}


public class Project
{
    public int Id { get; set; }
    public int CompanyId { get; set; }
    [ForeignKey("CompanyId")]
    public virtual Company Company { get; set; }
    // ...
}

public class Company
{
    public Company()
    {
        MainContact = new Contact();
    }

    public int Id { get; set; }
    public virtual Contact MainContact { get; set; }
    // ...
}

public class Contact
{
    public int Id { get; set; }
    public string AddressLine1 { get; set; }
    // ...
}

1条回答
放我归山
2楼-- · 2019-01-09 19:24

If I understand right you have something like this:

public class Company
{
    public Company()
    {
        MainContact = new Contact();
    }

    public int Id { get; set; }
    public virtual Contact MainContact { get; set; }
}

A simple code like this...

var company = context.Companies.Find(1);
context.SaveChanges();

...will indeed create a new empty contact in the database.

The main conclusion I would draw is: Don't instantiate reference navigation properties in the constructor! (Instantiating navigation collections is OK, I think, as long as you leave their content empty. Also instantiating properties of complex types in the constructor is fine because they are not other entities.)

If you want to make sure to create a new contact with a new company, perhaps a static factory method in the Company class is the better option:

public static Company CreateNewCompany()
{
    return new Company { MainContact = new Contact() };
}

This would also work:

var company = context.Companies.Find(1);
context.Entry(company.MainContact).State = EntityState.Detached;
context.SaveChanges();

But such a procedure looks really ridiculous.

Edit:

It's by the way automatic change detection which causes the behaviour. This code...

context.Configuration.AutoDetectChangesEnabled = false;
var company = context.Companies.Find(1);
context.SaveChanges();

...doesn't create a new contact. It's the change detection working internally in SaveChanges Find which thinks to identify the MainContact in company as a new entity and puts it into Added state into the context.

查看更多
登录 后发表回答