Alb*_*ban 8 c# json-patch entity-framework-core graphql hotchocolate
我正在尝试使用 Entity Framework Core 和 Hot Chocolate 创建 ASP.NET Core 3.1 应用程序。应用程序需要支持通过 GraphQL 创建、查询、更新和删除对象。有些字段需要有值。
创建、查询和删除对象不是问题,但更新对象则比较棘手。我试图解决的问题是部分更新的问题。
实体框架使用以下模型对象首先通过代码创建数据库表。
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; }
}
Run Code Online (Sandbox Code Playgroud)
我可以在数据库中创建一条记录,其中的突变定义如下:
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);
});
}
}
Run Code Online (Sandbox Code Playgroud)
目前,这些对象很小,但在项目完成之前它们将拥有更多的字段。我需要利用 GraphQL 的强大功能来仅发送已更改字段的数据,但是如果我使用相同的 InputObjectType 进行更新,则会遇到两个问题。
为了避免这个问题,我查看了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; }
}
Run Code Online (Sandbox Code Playgroud)
将其添加到突变中
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);
});
Run Code Online (Sandbox Code Playgroud)
然后,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;
}
Run Code Online (Sandbox Code Playgroud)
虽然这有效,但它确实有几个主要缺点。
Optional<>泛型类型,因此每个模型都需要 2 个类实体框架可以与通用类一起使用JsonPatchDocument<>。这允许将部分更新应用于实体,而无需自定义代码。然而,我正在努力寻找一种将其与 Hot Chocolate GraphQL 实现相结合的方法。
为了完成这项工作,我尝试创建一个自定义的 InputObjectType,其行为就好像属性是使用定义的Optional<>并映射到JsonPatchDocument<>. 这可以通过在反射的帮助下为模型类中的每个属性创建自定义映射来实现。然而,我发现IsOptional定义框架处理请求的方式的一些属性 () 是 Hot Chocolate 框架的内部属性,无法从自定义类中的可重写方法进行访问。
我也考虑过方法
Optional<>将UpdateClass 的属性映射到JsonPatchDocument<>对象中Optional<>每个属性版本的类Optional<>属性我正在寻找关于如何使用通用方法实现这一点的任何想法,并避免需要为每种类型编写 3 个单独的代码块 - 这些代码块需要保持彼此同步。
依赖 HotChocolate 提供的Optional<>可能不是最好的主意。考虑这样一种情况:用户有一个始终不为空的字段(密码、登录名等)。使用Optional<>来修补该字段,您将被迫放宽更新方法输入中的类型要求,允许空值。当然,您可以稍后在执行阶段验证这一点,但是您的 API 的类型变得不那么强 - 现在仅查看类型系统来了解是否允许 field = null 作为修补值是不够的。因此,如果您想使用Optional<>而不降低API的自描述性和一致性,只有当API的所有补丁方法的所有字段都不允许将null作为有效补丁值时,您才可以这样做。然而,在绝大多数情况下,这是错误的。几乎总是,您的 API 中会出现这样的情况:您需要允许用户将某些字段重置为 null。
mutation
{
updateUser(input: {
id: 1
phone: null
email: null
}) {
result
}
}
Run Code Online (Sandbox Code Playgroud)
例如,在上述情况下,您的 API 可以允许用户将其电话号码重置为空(当他们丢失手机时),但不允许对电子邮件进行相同的操作。但是,尽管存在这种差异,这两个字段都将使用可空类型。这不是 API 的最佳设计。
根据我们自己的API的经验,我们可以得出结论,使用Optional<>进行修补会导致对API的理解混乱。几乎所有补丁属性都可以为空,即使它们补丁的对象并非如此。不过,值得一提的是,Optional<> 的问题并非源于 HotChocolate 实现,而是源于 graphql 规范,该规范使用非常接近的逻辑定义了可选字段和可为空字段:
默认情况下,输入(例如字段参数)始终是可选的。但是,需要非空输入类型。除了不接受 null 值之外,它也不接受省略。为了简单起见,可空类型始终是可选的,非空类型始终是必需的。
如果可选值和空值完全分开,可能会更好。例如,规范可以将可选字段定义为可以省略的字段(而不考虑它是否可为空),反之亦然。这将允许在[可选,非可选]和[可空,不可空]之间进行“交叉连接” 。这样,我们就可以得到所有可能的组合,并且任何一种都可以有实际用途。例如,某些字段可能是可选的,但如果设置它们,则必须遵守它们的非空性。那将是可选的不可为空的字段。不幸的是,规范不允许我们立即获得该功能,但使用自己的解决方案很容易实现该功能。
在我们的生产就绪 API 中,由数十个突变组成,我们没有依赖Optional<>,而是定义了两种补丁类型:
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 };
}
Run Code Online (Sandbox Code Playgroud)
我们所有的输入类型补丁字段都是通过该类型来表达的,例如:
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; }
}
Run Code Online (Sandbox Code Playgroud)
一旦 patch 值被打包到 setXXX 对象中,我们就不再需要区分 null 和可选值。无论setXXX是否为null,其含义都是一样的:XXX字段没有补丁。
看看我们的示例输入类型,我们清楚地且没有任何类型系统放松地理解以下内容:
对于 p。3 值得一提的是,SetNullableValueInput<string> 和 SetValueInput<string?> 之间没有逻辑差异。但是,从技术上讲,对于可空引用类型 T(SetValueInput<T> 泛型参数),我们必须定义一个单独的类SetNullableValueInput<T>,因为否则反射会错过有关该泛型参数可空性的信息。即使用 SetValueInput<string?> 我们最终得到由 HotChocolate 生成的不可为空(而不是可为空)字符串类型的值。尽管可空值类型不存在这样的问题 - SetValueInput<decimal> 和 SetValueInput<decimal?> 都将生成十进制值的正确可空性(在第一种情况下不可为空,在第二种情况下可空),因此可以使用安全。
继续我们的示例,我们可以在实体“用户”上有其他场景,但补丁逻辑存在一些差异。考虑:
public class CreateUserInput
{
public SetValueInput<string>? setEmail { get; set; }
public SetValueInput<decimal?> setSalary { get; set; }
public SetValueInput<string> setPhone { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
在这里,对于创建用户管道,我们有:
关于使用实体自动修补的最后一点 - 我们不这样做,更喜欢“手动”更新:
if (input.setEmail != null)
user.Email = input.setEmail.Value;
Run Code Online (Sandbox Code Playgroud)
但是,在该线程的其他答案中提出的具有反射的解决方案也可以轻松地针对 SetInputValue 模型实现。
| 归档时间: |
|
| 查看次数: |
5534 次 |
| 最近记录: |