0

While I am updating related records, I noticed that after updating, the primary key is reassigned. The latter is a long datatype (RecordId in the example below in PartnersRegistry) and I noticed that its value has changed after saving the changes. It looks like EF is deleting the old entity object and adding the new one instead of just updating the changes (await unitOfWork.CompleteAsync(); in the example below).

I have two questions:

  1. Is this a normal behavior?
  2. What is the best Primary Key to be used here to avoid seeding?

I am posting the relevant code:

Entity 1

[Table("PatientsRegistry")]
public class PatientRegistry
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Display(Name = "Record Id")]
    public long RecordId { get; set; }

    [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Display(Name = "Patient File Number")]
    public long PatientFileId { get; set; }
    public DateTimeOffset DateCreated { get; set; }
    [Timestamp]
    public byte[] RowVersion { get; set; }
    public virtual ICollection<PartnerRegistry> Partners { get; set; }
}

Related entity

[Table("PartnersRegistry")]
public class PartnerRegistry
{   
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long RecordId { get; set; }

    public long PatientFileId { get; set; }
    public long PartnerFileId { get; set; }
    [JsonIgnore]
    public virtual PatientRegistry PatientsRegistry { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime? EndDate { get; set; }    
}

In my controller

[HttpPut]
public async Task<IActionResult> UpdatePatient(long fileId, [FromBody] SavePatientsRegistryViewModel model)
{
    SuccessResponse response = new SuccessResponse();

    if (!ModelState.IsValid)
        return StatusCode(400, StaticHandlers.FormatErrorResponse(ModelState));

    PatientRegistry patient = await repository.GetPatient(fileId);
    if (patient == null)
    {
        ModelState.AddModelError(string.Empty, "The patient does not exist in the database");
        return StatusCode(404, StaticHandlers.FormatErrorResponse(ModelState));
    }
    model.PatientFileId = patient.PatientFileId;
    PatientRegistry modelPatientRegistryData = mapper.Map<SavePatientsRegistryViewModel, PatientRegistry>(model, patient);

    await unitOfWork.CompleteAsync();
    response.SuccessMessage = patient.FirstName + " " + patient.LastName + " has been updated successfully!";
    response.DataResponse = patient.PatientFileId;
    return StatusCode(200, response);
}

Based on the comments below, I am extending the relevant code.

These are my view models that are consumed by the controller:

public class SavePatientsRegistryViewModel
{
    public long PatientFileId { get; set; }
    public ICollection<SavePartnerRegistryViewModel> Partners { get; set; }
    public OnlineAccountViewModel OnlineAccount { get; set; }
    public SavePatientsRegistryViewModel()
    {
        Partners = new Collection<SavePartnerRegistryViewModel>();
    }
}

public class SavePartnerRegistryViewModel
{
    public string RecordId { get; set; }
    public long PatientFileId { get; set; }
    public long PartnerFileId { get; set; }
    public string StartDate { get; set; }
    public string EndDate { get; set; }
}

And my mapping profiles are:

CreateMap<SavePatientsRegistryViewModel, PatientRegistry>()
    .ForMember(pr => pr.RecordId, opt => opt.Ignore())
    .ForMember(pr => pr.PatientFileId, opt => opt.Ignore())
    .ForMember(pr => pr.UserId, opt => opt.Ignore())
    .ForMember(pr => pr.Partners, opt => opt.MapFrom(c => c.Partners))
    .ForMember(pr => pr.User, opt => opt.MapFrom(c => c.OnlineAccount));

CreateMap<SavePartnerRegistryViewModel, PartnerRegistry>()
    .ForMember(pr => pr.RecordId, opt => opt.Ignore())
    .ForMember(pr => pr.PatientFileId, opt => opt.Ignore())
    .ForMember(pr => pr.PatientFileId, opt => opt.MapFrom(s => s.PatientFileId))
    .ForMember(pr => pr.PartnerFileId, opt => opt.MapFrom(s => s.PartnerFileId))
    .ForMember(pr => pr.StartDate, opt => opt.MapFrom(s => s.StartDate))
    .ForMember(pr => pr.EndDate, opt => opt.MapFrom(s => s.EndDate));

Basically, what I am doing in my Update method in my controller is the following:

  1. I check if the patient exists by calling it from Dbcontext.

  2. I map the view model to the entity.

  3. After mapping, values are changed, I call await unitOfWork.CompleteAsync(); which is basically await context.SaveChangesAsync();

My problem is in the related table (PartnerRegistry). Upon saving changes, the column RecordId, which is a PK, gets a new value. Now, EF is not updating this column for sure and even if I try, I get an error saying that values cannot be inserted in this filed since its autogenerated!

The only way RecordId changes its value is if the whole record is inserted, or removed and re-inserted.

Example below.

Before Update:

{
    "patientFileId": 1111,
    "partners": [
        {
            "recordId": **5**,
            "patientFileId": 1111,
            "partnerFileId": 2222,
            "startDate": "1/1/90 12:00:00 AM",
            "endDate": "1/1/00 12:00:00 AM",
            "patientName": null
        }
    ]
}

After Update:

{
    "patientFileId": 1111,
    "partners": [
        {
            "recordId": **6**,
            "patientFileId": 1111,
            "partnerFileId": 2222,
            "startDate": "1/1/90 12:00:00 AM",
            "endDate": "1/1/00 12:00:00 AM",
            "patientName": null
        }
    ]
}

Update

After debugging, I found that the Partners collection is re-initialized after mapping!

PatientRegistry patient = await repository.GetPatient(fileId);

The Partners collection here is the same as in the context. However, after mapping, the whole ICollection get re-initialized for some reason. Specifically after:

PatientRegistry modelPatientRegistryData = mapper.Map<SavePatientsRegistryViewModel, PatientRegistry>(model, patient);

aaron
  • 39,695
  • 6
  • 46
  • 102
JSON
  • 1,583
  • 4
  • 31
  • 63
  • 1
    It is likely that the remapping did not actually update the existing Partner but rather created new Partner object. Since reference equality wouldn't match for it, EF Core consider that you removed old partner and added a new. Try mapping Partners in separate call to AutoMapper. – Smit May 01 '18 at 20:29
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/170188/discussion-between-smit-and-json). – Smit May 01 '18 at 22:57
  • 1
    Check out [AutoMapper/AutoMapper.Collection](https://github.com/AutoMapper/AutoMapper.Collection). – aaron May 02 '18 at 05:26

1 Answers1

0

So as @smit said, AutoMapper was creating a new collection after mapping instead of modifying the existing collection. a similar issue is described here,

My workaround is as follows,

I ignore the mapping for the collection and then manually evaluate the record action here, (insert/update/delete)

// CreateMap<SavePartnerRegistryViewModel, PartnerRegistry> (); is no longer needed.

        CreateMap<SavePatientsRegistryViewModel, PatientRegistry> ()
            .ForMember (d => d.RecordId, opt => opt.Ignore ())
            .ForMember (d => d.PatientFileId, opt => opt.Ignore ())
            .ForMember (d => d.UserId, opt => opt.Ignore ())
            .ForMember (d => d.Partners, opt => opt.Ignore ())
            .ForMember (d => d.User, opt => opt.MapFrom (s => s.OnlineAccount))
            .AfterMap ((s, d) => {
// check if context object is included in the model? if not prime for delete.
                foreach (PartnerRegistry partner in d.Partners.ToList ()) {
                    SavePartnerRegistryViewModel modelPartner = s.Partners.SingleOrDefault (c => c.PatientFileId == partner.PatientFileId && c.PartnerFileId == partner.PartnerFileId);
                    if (!s.Partners.Contains (modelPartner)) {
                        d.Partners.Remove (partner);
                    }
                }
// check if model object(s) is/are included in the context? 
                foreach (SavePartnerRegistryViewModel modelPartner in s.Partners) {
                    PartnerRegistry partner = d.Partners.SingleOrDefault (c => c.PatientFileId == modelPartner.PatientFileId && c.PartnerFileId == modelPartner.PartnerFileId);
// if yes, prime for update
                    if (d.Partners.Contains (partner)) {
                        partner.PartnerFileId = modelPartner.PartnerFileId;
                        partner.StartDate = DateTime.Parse (modelPartner.StartDate);
                        partner.EndDate = string.IsNullOrWhiteSpace (modelPartner.EndDate) ? (DateTime?) null : DateTime.Parse (modelPartner.EndDate);
                    } else {
// if Not, prime for insert
                        d.Partners.Add (
                            new PartnerRegistry {
                                PatientFileId = modelPartner.PatientFileId,
                                    PartnerFileId = modelPartner.PartnerFileId,
                                    StartDate = DateTime.Parse (modelPartner.StartDate),
                                    EndDate = string.IsNullOrWhiteSpace (modelPartner.EndDate) ? (DateTime?) null : DateTime.Parse (modelPartner.EndDate)
                            }
                        );
                    }
                }
            });
JSON
  • 1,583
  • 4
  • 31
  • 63