Home > Enterprise >  Updating a model with bind params is nulling out ones not in form
Updating a model with bind params is nulling out ones not in form

Time:10-01

I'm new to C# and dotnet core. I'm wondering why when editing a model using bind, it creates a new model and binds the attributes from the post. But if you do not place all your fields in the form post as hidden and the bind it will null them out?

Shouldn't it load a model and update the bind parameters and leave the ones alone?

for example if I'm updating a person and person has Id,Name,Age,updated,Created

Edit(int id,[Bind("Id,Name,Age") Person p]

When I go to _context.update(p) its nulling out updated and Created because they weren't bound.

WHY does it work like that?

How can I make it only update the bound parameters without nulling out the ones I don't need to load?

CodePudding user response:

What you pass in is a deserialized block of data that MVC is mapping into an entity definition. It doesn't auto-magically open a DbContext, load the entity, and overwrite values, it just creates an instance of the entity, and copies in the values across. Everything else would be left as defaults.

As a general rule I advise against ever passing entities from the client to the server to avoid confusion about what is being sent back. When performing an update, accept a view model with the applicable properties to update, and ideally the data model and view model should include a row version #. (I.e. Timestamp column in SQL Server that can be converted to/from Base64 string to send/compare in ViewModel)

From there, when performing an update, you fetch the entity by ID, compare the timestamp, then can leverage Automapper to handle copying data from the ViewModel into the Entity, or copy the values across manually.

That way, when your ViewModel comes back with the data to update:

using (var context = new AppDbContext())
{
    var entity = context.Persons.Single(x => x.Id == id);
    if (entity.TimeStampBase64 != viewModel.TimestampBase64)
    {
         // Handle fact that data has changed since the client last loaded it.
    }
    Mapper.Map(entity, viewModel);
    context.SaveChanges();
}

You could use the entity definition as-is and still load the existing entity and use Automapper to copy values from the transit entity class to the DbContext tracked one, however it's better to avoid confusing instances of entities between tracked "real" entity instances, and potentially incomplete untracked transit instances. Code will often have methods that accept entities as parameters to do things like validation, calculations, etc. and it can get confusing if later those methods assume they will get "real" entities vs. getting called somewhere that has only a transient DTO flavour of an entity.

It might seem simpler to take an entity in and just call DbContext.Update(entity) with it, however this leaves you with a number of issues including:

  1. You need to pass everything about the entity to the client so that the client can send it back to the server. This requires hidden inputs or serializing the entire entity in the page exposes more about your domain model to the browser. This increases the payload size to the client and back.
  2. Because you need to serialize everything, "quick fixes" like serializing the entire entity in a <script> block for later use can lead to lazy-load "surprises" as the serializer will attempt to touch all navigation properties.
  3. Passing an entity back to the server to perform an Update() means trusting the data coming back from the client. Browser debug tools can intercept a Form submit or Ajax POST and tamper with the data in the payload. This can open the door to unexpected tampering. DbContext.Update also results in an UPDATE statement that overwrites all columns whether anything changed or not, where change tracked entities will only build UPDATE statements to include values that actually changed only if anything actually changed.
  • Related