Search code examples
c#asp.net-corejson-patch

JSON Patch and "aggregate" DTOs


A somewhat contrived, but nonetheless important, example.

Assume the following case whereby UserDetails is an aggregate DTO (not sure of correct terminology, please educate me, but basically a model of collected information from different stores/services) which is used by a RESTful web service. It does not necessarily have the same property names as the objects it collects together.

public class UserDetails
{
    public int UserId { get;set; }
    public string GivenName { get; set; }
    public string Surname { get; set; }
    public int? UserGroupId { get;set; } // FK in a different database
}

Let our stores persist the following models:

public class User
{
    public int Id { get; set; }
    public string GivenName { get; set; }
    public string Surname { get; set; }
}


public class UserGroup
{
    public int UserId { get; set; }
    public int GroupId { get; set; }
}

Let the UserDetails object be populated thusly:

User user = _userService.GetUser(userId) ?? throw new Exception();
UserGroup userGroup = _userGroupService.GetUserGroup(user.Id);

UserDetails userDetails = new UserDetails {
    UserId = user.Id,
    GivenName = user.GivenName,
    Surname = user.Surname,
    UserGroupId = userGroup?.GroupId
};

That is, setting FirstName or Surname should delegate to the UserService, and UserGroupId to the GroupService.

This UserDetails object is used for GET and PUT, the logic here is pretty trivial, however a JSON Patch document for this object is sent for PATCH requests. This is apparently much more complicated.

How can we go about changing the user's group? The best ('best' being used very loosely) I came up with is this:

int userId;
JsonPatchDocument<UserDetails> patch;

// This likely works fine, because the properties in `UserDetails`
// are named the same as those in `User`
IEnumerable<string> userPaths = new List<string> {"/givenName", "/surname"};
if (patch.Operations.Any(x => userPaths.Contains(x.path))) {
    User user = _userService.GetUserByUserId(userId);
    patch.ApplyTo(user);
    _userService.SetUser(userId, user);
}

// Do specialised stuff for UserGroup
// Can't do ApplyTo() because `UserDetails.UserGroupId` is not named the same as `UserGroup.GroupId`
IEnumerable<Operation<UserDetails>> groupOps = patch.Operations.Where(x => x.path == "/userGroupId");
foreach (Operation<UserDetails> op in groupOps)
{
    switch (op.OperationType)
    {
        case OperationType.Add:
        case OperationType.Replace:
            _groupService.SetOrUpdateUserGroup(userId, (int?)(op.value));
            break;

        case OperationType.Remove:
            _groupService.RemoveUserGroup(userId);
            break;
    }
}

Which is pretty garishly awful. It's a lot of boilerplate, and relies on a magic string.

Without requesting a change in the Microsoft.AspNetCore.JsonPatch API, something like

JsonPatchDocument<UserDetails> tmpPatch = new JsonPatchDocument<UserDetails>();
tmpPatch.Add(x => x.GivenName, String.Empty);
tmpPatch.Add(x => x.Surname, String.Empty);
IEnumerable<string> userPaths = tmpPatch.Operations.Select(x => x.path);

Would at least get rid of the magic strings, but, imo, this just feels wrong!

JsonPatch seems pretty limited in this regard, seems more tailored towards systems where there is a 1:1 mapping between DAO (entities) and DTO (model).

Anyone got any good ideas? Can't be hard to beat the tripe I came up with!!


Solution

  • Json Merge Patch - RFC7396 would be more suited to this.