4

我正在尝试使用 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. 此更新尝试将所有未提供的值设置为其默认值。

为了避免这个问题,我查看了Optional<>HotChocolate 提供的泛型类型。这需要定义一个新的“更新”类型,如下所示

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<>泛型类型,所以每个模型都需要 2 个类
  2. Update 方法需要对每个要更新的字段都有条件代码,这显然是不理想的。

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

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

我也考虑过方法

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

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

4

3 回答 3

3

我在使用 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;
}

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

于 2020-09-09T15:46:07.690 回答
0

这是我最终得到的解决方案。它也使用反射,但我认为可以使用一些 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 });
                    }
                }
            }
        }
    }
}
于 2021-02-03T14:42:08.453 回答
0

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

在这里,我正在使用 Mapster。

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

将此添加到您的中间件

MapsterConfig.Config();
于 2020-07-29T07:26:07.540 回答