【问题标题】:How can I create a GraphQL partial update with HotChocolate and EFCore如何使用 Hot Chocolate 和 EF Core 创建 GraphQL 部分更新
【发布时间】:2020-07-18 17:59:59
【问题描述】:

我正在尝试使用 Entity Framework Core 和 Hot Chocolate 创建一个 ASP.NET Core 3.1 应用程序。 应用程序需要支持通过 GraphQL 创建、查询、更新和删除对象。 有些字段需要有值。

创建、查询和删除对象不是问题,但是更新对象就比较麻烦了。 我要解决的问题是部分更新。

Entity Framework 使用下面的模型对象先通过代码创建数据库表。

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; }
}

我可以在数据库中创建一条记录,其中定义如下:

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);
            });
    }
}

目前,对象很小,但在项目完成之前它们将拥有更多的领域。我需要放弃 GraphQL 的强大功能,只为那些已更改的字段发送数据,但是如果我使用相同的 InputObjectType 进行更新,我会遇到 2 个问题。

  1. 更新必须包含所有“必填”字段。
  2. 更新尝试将所有未提供的值设置为默认值。

避免这个问题我看过 HotChocolate 提供的Optional&lt;&gt; 泛型类型。 这需要定义一个新的“更新”类型,如下所示

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; }
}

将其添加到突变中

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);
            });

UpdateWarehouse 方法只需要更新那些已被提供值的字段。

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;
}

虽然这可行,但它确实有几个主要缺点。

  1. 由于 Enity Framework 不理解 Optional&lt;&gt; 泛型类型,每个模型都需要 2 个类
  2. Update 方法需要为要更新的每个字段提供条件代码 这显然不理想。

实体框架可以与JsonPatchDocument&lt;&gt; 泛型类一起使用。这允许将部分更新应用于实体而无需自定义代码。 但是,我正在努力寻找一种将其与 Hot Chocolate GraphQL 实现相结合的方法。

为了完成这项工作,我正在尝试创建一个自定义 InputObjectType,它的行为就像使用 Optional&lt;&gt; 定义属性并映射到 JsonPatchDocument&lt;&gt; 的 CLR 类型一样。这将通过在反射的帮助下为模型类中的每个属性创建自定义映射来工作。但是,我发现定义框架处理请求方式的某些属性 (IsOptional) 是 Hot Chocolate 框架的内部属性,无法从自定义类中的可覆盖方法访问。

我也考虑过

  • 将 UpdateClass 的 Optional&lt;&gt; 属性映射到 JsonPatchDocument&lt;&gt; 对象中
  • 使用代码编织生成具有每个属性的Optional&lt;&gt; 版本的类
  • 首先覆盖 EF 代码以处理 Optional&lt;&gt; 属性

我正在寻找有关如何使用通用方法实现此功能的任何想法,并避免需要为每种类型编写 3 个单独的代码块 - 这些代码块需要彼此保持同步。

【问题讨论】:

  • 我们在 graphql-dotnet 中使用 AutoMapper 解决了这个问题,您可以在其中将参数作为字典获取,然后将其映射到实体。这样,未传入的属性将不在字典中,因此不会映射,并且传入值为 null 的属性设置为 null。但是,我无法弄清楚如何从 hotchoclate 中的 IResolverContext 作为字典获取参数。

标签: c# graphql entity-framework-core json-patch hotchocolate


【解决方案1】:

我在使用 Hot Chocolate 时遇到了同样的问题,并且将巨大的表(其中一个有 129 列)映射到对象。为每个表的每个可选属性编写 if 检查会非常痛苦,因此,在下面编写了一个通用的辅助方法以使其更容易:

/// <summary>
/// Checks which of the optional properties were passed and only sets those on the db Entity. Also, handles the case where explicit null
/// value was passed in an optional/normal property and such property would be set to the default value of the property's type on the db entity
/// Recommendation: Validate the dbEntityObject afterwards before saving to db
/// </summary>
/// <param name="inputTypeObject">The input object received in the mutation which has Optional properties as well as normal properties</param>
/// <param name="dbEntityObject">The database entity object to update</param>
public void PartialUpdateDbEntityFromGraphQLInputType(object inputTypeObject, object dbEntityObject)
{
    var inputObjectProperties = inputTypeObject.GetType().GetProperties();
    var dbEntityPropertiesMap = dbEntityObject.GetType().GetProperties().ToDictionary(x => x.Name);
    foreach (var inputObjectProperty in inputObjectProperties)
    {
        //For Optional Properties
        if (inputObjectProperty.PropertyType.Name == "Optional`1")
        {
            dynamic hasValue = inputObjectProperty.PropertyType.GetProperty("HasValue").GetValue(inputObjectProperty.GetValue(inputTypeObject));
            if (hasValue == true)
            {
                var value = inputObjectProperty.PropertyType.GetProperty("Value").GetValue(inputObjectProperty.GetValue(inputTypeObject));
                //If the field was passed as null deliberately to set null in the column, setting it to the default value of the db type in this case.
                if (value == null)
                {
                    dbEntityPropertiesMap[inputObjectProperty.Name].SetValue(dbEntityObject, default);
                }
                else
                {
                    dbEntityPropertiesMap[inputObjectProperty.Name].SetValue(dbEntityObject, value);
                }
            }
        }
        //For normal required Properties
        else
        {
            var value = inputObjectProperty.GetValue(inputTypeObject);
            //If the field was passed as null deliberately to set null in the column, setting it to the default value of the db type in this case.
            if (value == null)
            {
                dbEntityPropertiesMap[inputObjectProperty.Name].SetValue(dbEntityObject, default);
            }
            else
            {
                dbEntityPropertiesMap[inputObjectProperty.Name].SetValue(dbEntityObject, 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");

    PartialUpdateDbEntityFromGraphQLInputType(input, item);

    await _context.SaveChangesAsync();

    return item;
}

希望这会有所帮助。如果有,请将其标记为答案。

【讨论】:

  • 我最终得到了一些非常相似的东西,但类型更强。见下文。
【解决方案2】:

您可以使用 Automapper 或 Mapster 来忽略空值。因此,如果您的模型中有空值,它不会替换现有值。

在这里,我正在使用 Mapster。

public class MapsterConfig
{
    public static void Config()
    {
        TypeAdapterConfig<WarehouseUpdate , Warehouse>
               .ForType()
               .IgnoreNullValues(true);
     }
}

将此添加到您的中间件

MapsterConfig.Config();

【讨论】:

  • 不幸的是,这种方法并不能解决根本问题。其中一项要求是某些字段在创建时可能没有 值,但在更新字段时可以省略它们。此外,某些字段必须能够用 显式填充。到目前为止,在 Hot Chocolate 正确支持这一点之前,我一直在使用的解决方案是自定义通用 InputType 与 TypeBuilder 相结合,以创建具有镜像基本类型的 Optional 类型属性的类。
【解决方案3】:

这是我最终得到的解决方案。 它也使用反射,但我认为可以使用一些 JIT 编译来优化它。

public void ApplyTo(TModel objectToApplyTo)
{
    var targetProperties = typeof(TModel).GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public).ToDictionary(p => p.Name);
    var updateProperties = GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);

    // OK this is going to use reflection - bad boy - but lets see if we can get it to work
    // TODO: Sub types
    foreach (var prop in updateProperties)
    {
        Type? propertyType = prop?.PropertyType;
        if (propertyType is { }
            && propertyType.IsGenericType
            && propertyType.GetGenericTypeDefinition() == typeof(Optional<>))
        {
            var hasValueProp = propertyType.GetProperty("HasValue");
            var valueProp = propertyType.GetProperty("Value");
            var value = prop?.GetValue(this);
            if (valueProp !=null && (bool)(hasValueProp?.GetValue(value) ?? false))
            {
                if (targetProperties.ContainsKey(prop?.Name ?? string.Empty))
                {
                    var targetProperty = targetProperties[prop.Name];
                    if (targetProperty.PropertyType.IsValueType || targetProperty.PropertyType == typeof(string) ||
                            targetProperty.PropertyType.IsArray || (targetProperty.PropertyType.IsGenericType && targetProperty.PropertyType.GetGenericTypeDefinition() == typeof(IList<>)))
                        targetProperty.SetValue(objectToApplyTo, valueProp?.GetValue(value));
                    else
                    {
                        var targetValue = targetProperty.GetValue(objectToApplyTo);
                        if (targetValue == null)
                        {
                            targetValue = Activator.CreateInstance(targetProperty.PropertyType);
                            targetProperty.SetValue(objectToApplyTo, targetValue);
                        }

                        var innerType = propertyType.GetGenericArguments().First();
                        var mi = innerType.GetMethod(nameof(ApplyTo));
                        mi?.Invoke(valueProp?.GetValue(value), new[] { targetValue });
                    }
                }
            }
        }
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-01-19
    • 2021-01-07
    • 2022-08-19
    • 2021-09-30
    • 2022-11-14
    • 2021-12-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多