Can I use JsonPatchDocument for saving to an array?

79 views Asked by At

I have a model with a List<string>. I am writing an API endpoint which accepts a JsonPatchDocument and uses that to update the List. The endpoint then saves the changes to the database. Logging shows me that the Json representation of the model is indeed patched, but saving to the db does nothing.

This is my model:

public class Music
{
  public int Id {get; set;}
  public List<string> Types {get;set;}
}  

The database table does not allow List or arrays as a column type so I mapped it to a comma separated string like this:

modelBuilder.Entity<Music>().HasKey(x => x.Id);
modelBuilder.Entity<Music>()
      .Property(x => x.Types)
      .HasConversion(
                    x => string.Join(',', x),
                    x => x.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());

My endpoint:

[HttpPatch("{id}")]
public async Task<IActionResult> EditAsync([FromRoute] long id, [FromBody] JsonPatchDocument<Music> patchDoc)
{
    var persisted = await Db.Music.FindAsync(id);
    if (persisted is null) return NotFound();

    patchDoc.ApplyTo(persisted, ModelState);
    if (ModelState.TryGetValue(nameof(Music), out var modelStateEntry))
    {
       foreach (var error in modelStateEntry.Errors)
                Logger.LogError(error.ErrorMessage); 
       return BadRequest(modelStateEntry.Errors);
    }

    await Db.SaveChangesAsync();
    return new ObjectResult(persisted);
}

Logging shows me the original persisted:

{
 "id": 1,
"types" : [
      "One",
      "Two",
      "Three"
    ]
}

and after ApplyTo() has been called, the JSON has been correctly updated to:

{
 "id": 1,
"types" : [
      "One",
      "Two",
      "Test"
    ]
}

However the database entity remains unchanged:

SaveChanges completed for 'OperationsDbContext' with 0 entities written to the database.

2

There are 2 answers

0
Zvi On

I needed to add

Db.Music.Update(persisted);

just before the SaveChanges();

0
Steve Py On

While calling Update would likely resolve your issue, this would be marking all properties as modified. The underlying issue you are seeing is because when using a Conversion to/from a string into a List<string> property, the change tracker will not detect changes to the contents of the List<string> property in order to ensure the value is marked as updated.

To mark the Types property as modified, you could use:

db.Entry(persisted).Property(x => x.Types).IsModified = true;

The tricky bit would be ideally to only set this if the values of Types had actually changed. The above would persist the Types value every time regardless of whether it changed. For one property that may be worth doing compared to the expense of trying to determine if values had actually changed using a set comparison.

Since there are limitations to using type conversion, especially when using Linq Queries, alternatively I tend to just handle these as strings as far as the data domain is concerned, then use an unmapped delimited list accessor:

[Column("Types")]
public string RawTypes { get; set; } 

[NotMapped]
public IEnumerable<string> Types
{
    get => RawTypes.Split(',', StringSplitOptions.RemoveEmptyEntries);
    set => string.Join(',', value.Distinct());
}

This would work for simple cases, but if you want to be able to add/remove Types at runtime from the Types collection then I'd opt for a more DDD approach with AddType()/RemoveType()/ClearTypes() methods.

I.e:

public void AddType(string type)
{
    var types = Types.ToList();
    if (types.Contains(type)) return;

    types.Add(type);
    Types = types;
}

Normally I don't expose a Setter on the parsed IEnumerable, just replacing the string RawType value interally and guarding that setter as Protected.

Either way, using the conversion or the [NotMapped] accessor, there is a limitation that the "set" of types cannot be used as a set within things like query expressions. For instance you cannot query Music by a specify Type. It's generally better to use properly normalized relationships that can be queried.