Search code examples
c#entity-frameworkauditaspnetboilerplatechange-tracking

Why are CreationTime and CreatorUserId getting set on UpdateAsync?


I am calling UpdateAsync, but it's updating the CreationTime and CreatorUserId columns even if I'm not passing in these values. It should update only the required columns.

{
  "testCode": "string",
  "testName": "string",
  "testDesc": "string",
  "id": 1
}
public async Task UpdateTest(TestDetailsDTO input)
{
    var classobj = ObjectMapper.Map<Test>(input);
    await UpdateAsync(classobj);
}
public class TestDetailsDTO : FullAuditedEntityDto
{
    public string TestCode { get; set; }
    public string TestName { get; set; }
    public string TestDesc { get; set; }
}

Parameter input gets CreationTime in UpdateTest service method.

public class Test : FullAuditedEntity
{
    public const int NVarcharLength20 = 20;
    public const int NVarcharLength30 = 30;
    public const int NVarcharLength50 = 50;

    [Required]
    [MaxLength(NVarcharLength20)]
    public virtual string TestCode { get; set; }

    [Required]
    [MaxLength(NVarcharLength30)]
    public virtual string TestName { get; set; }

    [MaxLength(NVarcharLength50)]
    public virtual string TestDesc { get; set; }
}

Solution

  • Current flow:

    1. ObjectMapper.Map<Test>(input) creates a new Test object.
    2. CreationTime and CreatorUserId are default(DateTime) and default(long?).
    3. ABP sets those values.

    Correct flow:

    1. Get classobj from database.
    2. Restore CreationTime and CreatorUserId.
    3. Map input to classobj.

    var classobj = repository.GetEntity(input.id); // 1
    input.CreationTime = classobj.CreationTime;    // 2
    input.CreatorUserId = classobj.CreatorUserId;  // 2
    ObjectMapper.Map(input, classobj);             // 3
    

    Better design:

    • Don't inherit FullAuditedEntityDto for input → skip step 2.

    It's working, any other way around? Because it's calling extra GetEntity method.

    The other way is to attach. The trade-off is that you have to map explicitly, not ObjectMapper.Map.

    // Create new stub with correct id and attach to context.
    var classobj = new Test { Id = input.Id };
    repository.As<EfRepositoryBase<MyDbContext, Test>>().Table.Attach(classobj);
    
    // Now the entity is being tracked by EF, update required properties.
    classobj.TestCode = input.TestCode;
    classobj.TestName = input.TestName;
    classobj.TestDesc = input.TestDesc;
    
    // EF knows only to update the properties specified above.
    _unitOfWorkManager.Current.SaveChanges();