好的,我已经尝试了 Jason Freitas 的建议,结果太复杂而无法在评论中解决,所以我添加了一个答案。
tl; dr:您可以做到这一点,尽管解决方案围绕IDataServiceUpdateProvider
and展开IUpdatable
。(Jason 建议IDataServiceQueryProvider
,这似乎没有帮助。)但是,问题在于DataService<T>
它并不是真正设计为支持 upsert,也不是用于更新的接口,所以虽然你可以让它工作,但解决方案是hack(而且不是很好),我怀疑将来可能会导致问题。
长版:
我已经在实现IUpdatable
,这是支持更新、插入和删除所需的。IDataServiceQueryProvider
不添加任何与更新支持相关的内容,因此IUpdatable
是关键。我最初错误地认为IUpdatable.GetResource
即使请求的项目不存在也安排返回项目会弄乱查询行为。但是当然,针对的查询DataService<T>
不会通过IUpdatable
,因此无论请求什么,都可以让该方法返回一个对象。
这样做非常复杂,而且事实证明这还不够。这是代码:
public object GetResource(IQueryable query, string fullTypeName)
{
var item = query.Cast<object>().SingleOrDefault();
if (item == null && fullTypeName != null)
{
var ctor = Type.GetType(fullTypeName).GetConstructor(Type.EmptyTypes);
if (ctor != null)
{
item = ctor.Invoke(null);
PopulatePutStandin(query.Expression, item);
}
}
return item;
}
private void PopulatePutStandin(Expression expression, object item)
{
var call = expression as MethodCallExpression;
if (call != null && call.Method.Name == "Where" && call.Method.DeclaringType == typeof(Queryable))
{
foreach (Expression arg in call.Arguments)
{
var ux = arg as UnaryExpression;
if (ux != null)
{
var op = ux.Operand as LambdaExpression;
if (op != null)
{
var bx = op.Body as BinaryExpression;
if (bx != null && bx.Method.Name == "op_Equality")
{
var left = bx.Left as MemberExpression;
var right = bx.Right as ConstantExpression;
if (left != null && right != null)
{
var prop = left.Member as PropertyInfo;
if (prop != null)
{
prop.SetValue(item, right.Value);
}
}
}
}
}
else
{
PopulatePutStandin(arg, item);
}
}
}
}
该GetResource
方法是IUpdatable
接口的一部分,DataService<T>
当它接收到 PUT 时,它是用来尝试查找现有资源的方法。如您所见,如果对象还不存在,它只是构造一个新实例。然而,仅仅让新对象保持默认状态是不够的。PartitionKey
和RowKey
必须匹配传入 PUT 请求的值。而且您不会直接被告知这些值 - 它们嵌入在查询中。
因此,我编写了该PopulatePutStandin
方法以将这些值从查询中提取出来。它遍历表示查询的调用表达式链,查找Where
调用。(这不处理任何其他 LINQ 运算符,但对于更新/插入,您不应该看到任何更复杂的东西。)对于每个Where
测试特定属性是否具有特定值的子句,我的代码将该属性设置为新对象的值。在实践中,这将最终只设置PartitionKey
andRowKey
因为这些是唯一Where
存在于 upsert 的子句,但是编写不查找任何特定属性的代码更容易。
这有点不稳定,因为它假设每个属性都将通过自己的Where
子句处理。理论上,没有什么可以停止DataService<T>
使用一个Where
包含对PartitionKey
and的单个表达式测试的子句RowKey
。所以理论上它可以使用:
src.Where(e => e.PartitionKey == "123").Where(e => e.RowKey == "456")
或者
src.Where(e => e.PartitionKey == "123" && e.RowKey == "456")
两者应该具有相同的效果。它碰巧使用了前者,我的代码依赖于它,但我没有找到任何DataService<T>
承诺以任何特定形式提供查询的文档。所以我们对DataService<T>
. 一个更健壮的实现会想要处理任何一种形式,尽管这感觉很麻烦——理论上我们可以被问到很多方法。目前尚不清楚是否有一种完全通用且安全的方法来制作与我们可能在此处收到的任何查询匹配的新对象。
但是,这是在测试中运行的代码,所以我们会在开发时检测到这种问题,所以我想这是可以接受的。
然而,尽管这使我们能够为 PUT 生成合适的目标,但事实证明这还不够。我们得到DataServiceException
以下错误:
由于实体类型“Mm.Web.Tests.Fakes.AzureTableStorage.FakeUserPermission”具有一个或多个 etag 属性,因此必须为该类型的 DELETE/PUT 操作指定 If-Match HTTP 标头。
内部DataService<T>
已经决定 PUT 请求必须包含一个 ETag 如果它有任何意义,因为你将如何确定你正在编辑你要编辑的实体?这对于更新是有意义的,但对于插入显然没有意义。所以这对upsert不友好。
我尝试在客户端包含一个 etag:
var permission = new TableEntity(userId, claimId) { ETag = "*" };
await _myTable.ExecuteAsync(TableOperation.InsertOrReplace(permission));
但是,Azure 表存储客户端显然足够聪明,知道 etag 对 upsert 没有意义。(如果你知道 etag,你知道这绝对是一个更新,所以你不应该使用 upsert。)所以它实际上并没有传递那个 ETag。
但是,您可以解决此问题。IUpdatable
您可以实现,而不仅仅是实现IDataServiceUpdateProvider
,它派生自IUpdatable
并添加一个成员:
public void SetConcurrencyValues(
object resourceCookie,
bool? checkForEquality,
IEnumerable<KeyValuePair<string, object>> concurrencyValues)
{
}
如果你实现了这个接口,你基本上就是在告诉DataService<T>
你想自己处理 ETags。因为我很高兴没有 ETag,所以什么也不做也没关系。只需提供此接口的空实现即可禁用默认的 ETag 处理,并且我们不再遇到异常。所以它似乎有点工作。
一个问题是,在您生成假替身时,没有明确的方法来区分 PUT 或 DELETE。GetResource
没有被告知手术是什么。或者至少,不是直接的。碰巧的是,我发现该fullTypeName
论点恰好是null
针对 DELETE 的,但文档并没有承诺这一点。所以我觉得我在依赖一个无证的巧合。
而这反过来又像是一个潜在问题的症状:这里涉及的接口并不是为了支持 upsert 而设计的。因此,尽管有可能让它类似于“工作”的东西,但它总是会有点令人不满意。
所以我怀疑唯一令人满意的解决方案是在 HTTP 级别伪造它,因为这是支持我试图伪造的语义的唯一方法。