Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Null fields on a partial update mutation on GraphQL .NET

At work, we're using EFCore on our data layer and graphql-dotnet to manage APIs requests, I'm having a problem updating some of our big objects using GraphQL mutations. When the user sends a partial update on the model, we would like to update on our database only the fields that actually were changed by the mutation. The problem we're having is that as we directly map the input to the entity, wheather some field was purposefully passed as null, or the field was not specified on the mutation at all, we get the property value as null. This way we can't send the changes to the database otherwise we would incorrectly update a bunch of fields to null.

So, we need a way to identify which fields are sent in a mutation and only update those. In JS this is achieved by checking if the property value is undefined, if the value is null we know that it was passed as null on purpose.

Some workarounds we've been thinking were using reflection on a Dictionary to update only the specified fields. But we would need to spread reflection to every single mutation. Another solution was to have a isChanged property to every nullable property on our model and change ir on the refered property setter, but... cmon...

I'm providing some code as example of this situation bellow:

Human class:

public class Human
{
    public Id { get; set; }
    public string Name { get; set; }
    public string HomePlanet { get; set; }
}

GraphQL Type:

public class HumanType : ObjectGraphType<Human>
{
    public HumanType()
    {
        Name = "Human";
        Field(h => h.Id).Description("The id of the human.");
        Field(h => h.Name, nullable: true).Description("The name of the human.");
        Field(h => h.HomePlanet, nullable: true).Description("The home planet of the human.");
    }
}

Input Type:

public class HumanInputType : InputObjectGraphType
    {
        public HumanInputType()
        {
            Name = "HumanInput";
            Field<NonNullGraphType<StringGraphType>>("name");
            //The problematic field
            Field<StringGraphType>("homePlanet");
        }
    }

Human Mutation:

/// Example JSON request for an update mutation without HomePlanet 
/// {
///   "query": "mutation ($human:HumanInput!){ createHuman(human: $human) { id name } }",
///   "variables": {
///     "human": {
///       "name": "Boba Fett"
///     }
///   }
/// }
///
public class StarWarsMutation : ObjectGraphType<object>
{
    public StarWarsMutation(StarWarsRepository data)
    {
        Name = "Mutation";

        Field<HumanType>(
            "createOrUpdateHuman",
            arguments: new QueryArguments(
                new QueryArgument<NonNullGraphType<HumanInputType>> {Name = "human"}
            ),
            resolve: context =>
            {
                //After conversion human.HomePlanet is null. But it was not informed, we should keep what is on the database at the moment
                var human = context.GetArgument<Human>("human");
                //On EFCore the Update method is equivalent to an InsertOrUpdate method
                return data.Update(human);
            });
    }
}
like image 928
Andre Batista Avatar asked Dec 21 '18 14:12

Andre Batista


1 Answers

You could use JsonConvert.PopulateObject from the Newtonsoft Json library. On the mutation resolver instead of using GetArgument with my type, I'm using GetArgument<dynamic> and serializing it using JsonConvert.SerializeObject then by calling JsonConvert.PopulateObject I'm able to update only the fields that were informed.

public StarWarsMutation(StarWarsRepository data)
{
    Name = "Mutation";

    Field<HumanType>(
        "createOrUpdateHuman",
        arguments: new QueryArguments(
            new QueryArgument<NonNullGraphType<HumanInputType>> {Name = "human"}
        ),
        resolve: context =>
        {
            //After conversion human.HomePlanet is null. But it was not informed, we should keep what is on the database at the moment
            var human = context.GetArgument<dynamic>("human");
            var humanDb = data.GetHuman(human["id"]);
            var json = JsonConvert.SerializeObject(human);
            JsonConvert.PopulateObject(json, humanDb);
            //On EFCore the Update method is equivalent to an InsertOrUpdate method
            return data.Update(humanDb);
        });
}
like image 168
Leonardo Costa Avatar answered Sep 24 '22 03:09

Leonardo Costa