I am trying to create an ASP.NET Core 3.1 application using Entity Framework Core and Hot Chocolate. The application needs to support creating, querying, updating and deleting objects through GraphQL. Some fields are required to have values.
Creating, Querying and Deleting objects is not a problem, however updating objects is more tricky. The issue that I am trying to resolve is that of partial updates.
The following model object is used by Entity Framework to create the database table through code first.
public class Warehouse
{
[Key]
public int Id { get; set; }
[Required]
public string Code { get; set; }
public string CompanyName { get; set; }
[Required]
public string WarehouseName { get; set; }
public string Telephone { get; set; }
public string VATNumber { get; set; }
}
I can create an record in the database with a mutation defined something like this:
public class WarehouseMutation : ObjectType
{
protected override void Configure(IObjectTypeDescriptor descriptor)
{
descriptor.Field("create")
.Argument("input", a => a.Type<InputObjectType<Warehouse>>())
.Type<ObjectType<Warehouse>>()
.Resolver(async context =>
{
var input = context.Argument<Warehouse>("input");
var provider = context.Service<IWarehouseStore>();
return await provider.CreateWarehouse(input);
});
}
}
At the moment, the objects are small, but they will have far more fields before the project is finished. I need to leaverage the power of GraphQL to only send data for those fields that have changed, however if I use the same InputObjectType for updates, I encounter 2 problems.
The avoid this issue I have looked at the Optional<>
generic type provided by HotChocolate.
This requires defining a new "Update" type like the following
public class WarehouseUpdate
{
public int Id { get; set; } // Must always be specified
public Optional<string> Code { get; set; }
public Optional<string> CompanyName { get; set; }
public Optional<string> WarehouseName { get; set; }
public Optional<string> Telephone { get; set; }
public Optional<string> VATNumber { get; set; }
}
Adding this to the mutation
descriptor.Field("update")
.Argument("input", a => a.Type<InputObjectType<WarehouseUpdate>>())
.Type<ObjectType<Warehouse>>()
.Resolver(async context =>
{
var input = context.Argument<WarehouseUpdate>("input");
var provider = context.Service<IWarehouseStore>();
return await provider.UpdateWarehouse(input);
});
The UpdateWarehouse method then needs to update only those fields that have been provided with a value.
public async Task<Warehouse> UpdateWarehouse(WarehouseUpdate input)
{
var item = await _context.Warehouses.FindAsync(input.Id);
if (item == null)
throw new KeyNotFoundException("No item exists with specified key");
if (input.Code.HasValue)
item.Code = input.Code;
if (input.WarehouseName.HasValue)
item.WarehouseName = input.WarehouseName;
if (input.CompanyName.HasValue)
item.CompanyName = input.CompanyName;
if (input.Telephone.HasValue)
item.Telephone = input.Telephone;
if (input.VATNumber.HasValue)
item.VATNumber = input.VATNumber;
await _context.SaveChangesAsync();
return item;
}
While this works, it does have a couple of major downsides.
Optional<>
generic types, every model will require 2 classesEntity Framework can be used along with the JsonPatchDocument<>
generic class. This allows partial updates to be applied to an entity without requiring custom code.
However I am struggling to find a way of combining this with the Hot Chocolate GraphQL implemention.
In order to make this work I am trying to create a custom InputObjectType that behaves as if the properties are defined using Optional<>
and maps to a CLR type of JsonPatchDocument<>
. This would work by creating custom mappings for every property in the model class with the help of reflection. I am finding however that some of the properties (IsOptional
) that define the way the framework processes the request are internal to the Hot Chocolate framework and cannot be accessed from the overridable methods in the custom class.
I have also considered ways of
Optional<>
properties of the UpdateClass into a JsonPatchDocument<>
objectOptional<>
versions of every propertyOptional<>
propertiesI am looking for any ideas as to how I can implement this using a generic approach and avoid needing to write 3 separate code blocks for each type - which need to be kept in sync with each other.
Relying on Optional<> provided by HotChocolate is probably not the best idea. Consider a case when a user has a field that is always supposed to be not null (password, login, etc.). Using Optional<> to patch that field, you will be forced to relax its type requirements in your update method input, allowing a null value. Of course, you could verify that later on in the execution stage, but your API becomes less strongly typed - now it's not enough to look at the type system to understand if field = null is allowed as a value for patching or not. So, if you want to use Optional<> without degrading API self-descriptiveness and consistency, you can do that only if all fields of all patch methods of the API don't allow null as a valid patch value. However, that's false in the vast majority of cases. Almost always, there's a situation in your API when you need to allow a user to reset some field to null.
mutation
{
updateUser(input: {
id: 1
phone: null
email: null
}) {
result
}
}
For example, in the above case, your API can allow the user to reset their phone number to null (when they have lost their mobile phone) but disallow the same for the email. But, despite that difference, for both fields the nullable type will be used. That's not the best design of the API.
According to the experience with our own API, we can conclude that using Optional<> for patching causes a mess in understanding the API. Almost all patch properties become nullable, even if that's not the case for the object they patch. It's worthwhile to mention, though, that issue with Optional<> is rooted not in the HotChocolate implementation but in the graphql spec, which defines optional and nullable fields with the very close logic:
Inputs (such as field arguments), are always optional by default. However a non-null input type is required. In addition to not accepting the value null, it also does not accept omission. For the sake of simplicity nullable types are always optional and non-null types are always required.
Probably it would be better if optionals and nulls were completely separated. For example, the spec could define an optional field as just the field that can be omitted (and nothing about whether it's nullable or not) and vice versa. That would allow making "cross-join" between [optional, non-optional] and [nullable, non-nullable]. In that way, we could get all possible combinations, and any one could have a practical use. For example, some fields could be optional, but if you set them, you must conform to their non-nullability. That would be optional non-nullable fields. Unfortunately, the spec doesn't allow us to get that functionality out-of-the-box, but it's quite easy to achieve that with the own solution.
In our production-ready API, consisting of dozens of mutations, instead of relying on Optional<>, we have just defined two patch types:
public class SetValueInput<TValue>
{
public TValue Value { get; set; }
}
public class SetNullableValueInput<T> where T : notnull
{
public T? Value { get; set; }
public static implicit operator SetValueInput<T?>?(SetNullableValueInput<T>? value) => value == null ? null : new() { Value = value.Value };
}
And all our input type patch fields are expressed through that types, for instance:
public class UpdateUserInput
{
int Id { get; set; }
public SetValueInput<string>? setEmail { get; set; }
public SetValueInput<decimal?>? setSalary { get; set; }
public SetNullableValueInput<string>? setPhone { get; set; }
}
Once the patch value is packed into setXXX object, we no longer need to distinguish nulls and optionals. Whether setXXX is null or not presented, it means the same: there's no patch for the field XXX.
Looking at our example input type, we clearly and without any type system relaxations, understand the following:
For p. 3 it's worthwhile to mention that there's no logical difference between SetNullableValueInput<string> and SetValueInput<string?>. But, technically, for nullable reference type T - the parameter of SetValueInput<T> generic, we have to define a separate class SetNullableValueInput<T> because, otherwise, the reflection misses the information about the nullability of that generic parameter. I.e. using SetValueInput<string?> we end up getting a non-nullable (instead of nullable) string type of Value generated by HotChocolate. Though there's no such problem for nullable value types - both SetValueInput<decimal> and SetValueInput<decimal?> will generate the correct nullability of decimal Value (non-nullable in the first case and nullable in the second) and, thus, can be used safely.
Continuing our example, we could have other scenarios over our entity "User" with some differences in the patch logic. Consider:
public class CreateUserInput
{
public SetValueInput<string>? setEmail { get; set; }
public SetValueInput<decimal?> setSalary { get; set; }
public SetValueInput<string> setPhone { get; set; }
}
Here, for the create user pipeline, we have:
And the last point about using the automatic patching of the entities - we don't do so, preferring "manual" updates:
if (input.setEmail != null)
user.Email = input.setEmail.Value;
But the solutions with the reflection proposed in other answers of this thread could be easily implemented for the SetInputValue model as well.