Search code examples
c#reflectionentity-framework-coreautomappernavigation-properties

Updating navigational properties sets them to null in EF Core


I have a situation when I'm attempting to update navigational properties in Entity Framework Core and I'm getting some unusual behaviour where it sets the value to null which then removes the record from the database.

Simplified example:

public class Hla : BaseEntity
{
    public string AField1 { get; set; }
    public virtual Patient Patient { get; set; }
    public int PatientId { get; set; }
}

public class Patient : BaseEntity
{
    public string Test { get; set; }
    public virtual Hla Hla { get; set; }
}

public class PatientRepository : Repository, IPatientRepository
{
    private readonly SearchAndMatchContext _context;

    public PatientRepository(SearchAndMatchContext context) : base(context)
    {
        _context = context;
    }

    public async Task<Patient> RetrieveEntity(int id)
    {
        return await _context.Patients
            .Include(t=>t.Hla)
            .FirstOrDefaultAsync(t => t.Id == id);
    }

    public  void UpdateEntity(Patient entity)
    {
        _context.Patients.Update(entity);
    }
}

The code that's causing the issue:

private static Hla Map(Hla original, Hla update)
{
    var  fieldsToSkip = new string[]{"Id"};
    
    PropertyInfo[] fields = update.GetType().GetProperties();

    foreach (var field in fields)
    {
        if (fieldsToSkip.Contains(field.Name))
        {
            continue;
        }

        var newValue = field.GetValue(update);
        field.SetValue(original, newValue);
    }

    return original;
}

var patient = await _patientRepository.RetrieveEntity(1);

Map(patient.Hla, new Hla
    {
        AField1 = "dfgdfg",
        PatientId = patient.Id
    });

patientRepository.UpdateEntity(patient);
await _patientRepository.UnitOfWork.SaveChangesAsync();

As soon as the SaveChangesAsync is called, the Hla is being set to null - output from the immediate window:

enter image description here

Whereas, if I have a simple "direct" mapper, it seems to work perfectly.

More simple mapper:

private static void Map(Hla current, Hla update)
{
    current.AField1 = update.AField1;
}

Then I get the immediate window output as:

enter image description here

I'm a little bit confused about why this happens, it seems to make no sense to me.

This also happens if I use an automapper etc.

What am I missing?


Solution

  • The method Map(Hla original, Hla update) copy all properties (except Hla.Id), this include the properties Hla.Patient and Hla.PatientId. But the copy from Hla update hasn't patient :

    var patient = await _patientRepository.RetrieveEntity(1);
    
    Map(patient.Hla, new Hla
        {
            AField1 = "dfgdfg",
            PatientId = patient.Id
            //No patient set
        });
    
    

    Then Map set in the orignal Patient <- null. Next EF detect the modification, update in DB and apply in all entities include the Patient.

    The solution is to exclude from the copy the properties Patient and PatientId:

    private static Hla Map(Hla original, Hla update)
    {
        var  fieldsToSkip = new string[]{"Id", "PatientId", "Patient"};
    
        PropertyInfo[] fields = update.GetType().GetProperties();
        foreach (var field in fields)
        {
            if (fieldsToSkip.Contains(field.Name))
            {
                continue;
            }
            var newValue = field.GetValue(update);
            field.SetValue(original, newValue);
        }
        return original;
    }
    

    EDIT : Other solution is to set the property Patient in copy from instance :

    Map(patient.Hla, new Hla
        {
            AField1 = "dfgdfg",
            PatientId = patient.Id,
            Patient = patient
        });