Eric Patrick
Eric Patrick

Reputation: 2247

.NetCore MVC deserialization

In a .netcore application, I would like to offer the following (simplified):

// Create a new record, assume it returns an ID=1
https://site/MyController/Save?FirstName=John&LastName=Doe&Status=Active

// Update the record without full state
PUT https://site/MyController/1
{
  'DOB': '1/1/1970',
  'Status': null
}

I would like to translate this second call to:

UPDATE MyModel SET DOB = '1/1/1970' AND Status=NULL WHERE Id = 1

I can certainly code my Create method in MyController to parse the request (querystring/form/body) for the submitted values, and create my SQL accordingly.

However, I'd prefer to follow MVC conventions and leverage the binding that MVC offers out of the box:

public async Task<MyModel> Save(string id, [FromBody]MyModel instance)
{
  await _MyRepository.UpdateAsync(id, message);
  return message;
}

The problem here is that instance will look like this:

{
  'FirstName': null,
  'LastName': null,
  'DOB': '1/1/1970',
  'Status': null
}

At which point I cannot determine which fields should be NULLed in the Db, and which should be left alone.

I've implemented a wrapper class that:

This would change my method signature a bit, but not impose a burden on developers:

public async Task<MyModel> Save(string id, [FromBody]MyWrapper<MyModel> wrapper
{
  await _MyRepository.UpdateAsync(id, wrapper.Instance, wrapper.DirtyProperties);
  return wrapper.Instance;
}

My two questions are:

  1. Am I re-inventing an established pattern?
  2. Can I intercept the MVC deserialzation (in an elegant manner)?

Upvotes: 4

Views: 598

Answers (2)

Set
Set

Reputation: 49789

You may look into custom model binding.

  • create own model binder: class that implements IModelBinder interface:

    /// <summary>
    /// Defines an interface for model binders.
    /// </summary>
    public interface IModelBinder
    {
       /// <summary>
       /// Attempts to bind a model.
       /// </summary>
       /// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
       /// <returns>
       /// <para>
       /// A <see cref="Task"/> which will complete when the model binding process completes.
       /// </para>
       /// <para>
       /// If model binding was successful, the <see cref="ModelBindingContext.Result"/> should have
       /// <see cref="ModelBindingResult.IsModelSet"/> set to <c>true</c>.
       /// </para>
       /// <para>
       /// A model binder that completes successfully should set <see cref="ModelBindingContext.Result"/> to
       /// a value returned from <see cref="ModelBindingResult.Success"/>. 
       /// </para>
       /// </returns>
       Task BindModelAsync(ModelBindingContext bindingContext);
     }
    
  • register your binder:

    services.AddMvc().Services.Configure<MvcOptions>(options => {
        options.ModelBinders.Insert(0, new YourCustomModelBinder());
    });
    

MVC github repo and "Custom Model Binding" article may help:

Upvotes: 1

randcd
randcd

Reputation: 2293

The PUT verb requires the whole entity, but you can send an HTTP PATCH with a delta. There is little official documentation on exactly HOW this is done, but I did find this link that lays out how to accomplish this with a JSONPatchDocument that does basically what your intercepting class does.

Upvotes: 0

Related Questions