Search code examples
c#asp.netentity-framework-core

How to update only fields present in a form for a model in EF Core?


To simplify the problem, in C# I have a model like this

public class Person
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Key]
    [MaxLength(128)]
    public string? Id { get; set; }

    [MaxLength(256)]
    public string PersonName { get; set; }

    public short PersonAge { get; set; }

// ommitted other properties

now, I have a form like this in ASP Core in the frontend

@model Person


<form action="/api/new-person">
    <input form="PersonName"/>
    <button type="submit"></button>
</form>

As you can see, I only have PersonName in my form, in my backend I only want to be able to update the Person object in my database for the value of PersonName because it is property that exists in the form, again, this is a simplified example because my object is massive and it changes just about all the time not to mention a lot of foreign keys, now, I don't want to painstakingly set each properties in the backend such as

var existingPersonFromDatabase = await _appDbContext.User.FirstOrDefaultAsync(x=>x.Id == formPerson.Id);

existingPersonFromDatabase.PersonName = formPerson.PersonName;

existingPersonFromDatabase.PersonAge = formPerson.PersonAge ;
// + settings 20 more properties one by one

so the question here is, how do i make it so that i only have to update the fields existing in the form?


Solution

  • I found the answer, for some reason this is not well documented by Microsoft and its hidden below the docs, I used TryUpdateModelAsync in the Controller,

    inside the controller you can have

    var existingObject = await _appDbContext.Objects.FirstAsync(x => x.Id == 1);
    
    // then we can populate this existing object with the one in the form
    await TryUpdateModelAsync(existingObject);
    

    below is my complete code

        [HttpPost]
        public async Task<IActionResult> PostNewEmployee(IFormCollection formCollection)
        {
            if (!ModelState.IsValid) // check model validity
            {
                var errors = ModelState
                    .Select(x => x.Value)
                    .Where(y => y is not null && y.Errors.Count > 0)
                    .ToList();
    
    
                return BadRequest(JsonConvert.SerializeObject(errors));
            }
    
            // if the form has an Id then this is an existing person
            if (formCollection.TryGetValue("Id", out var existingId))
            {
                var realId = existingId.ToString(); 
    
                var existingUser = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Id == realId); // get the current existing user
                if (existingUser is not null) // make sure the user exists
                {
    
                    await TryUpdateModelAsync(existingUser); // this will populate the current ``existingUser`` magically with the current model, it will only overwrite fields in the form, it will not touch properties that are NOT in the model
    
                    // update a foreign key
                    _applicationDbContext.Entry(existingUser.EmployeeAdditionalData).State = EntityState.Modified;
                    
                    await _applicationDbContext.SaveChangesAsync();
                    return Ok();
                }
            }
    
    

    What TryUpdateModelAsync does is it will populate an object with the current properties from the model, overwriting only properties from the model and ignoring properties not in both model or object. It also supports foreign or class properties in the model.