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 update must include all "Required" fields.
- The update tries to set all non-provided values to their default.
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.
- Because Enity Framework does not understand the
Optional<>
generic types, every model will require 2 classes - The Update method needs to have conditional code for every field to be updated This is obviously not ideal.
Entity 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
- Mapping the
Optional<>
properties of the UpdateClass into aJsonPatchDocument<>
object - Using code weaving to generate a class with
Optional<>
versions of every property - Overriding EF Code first to handle
Optional<>
properties
I 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.