使用 Ardalis.Specification 使用 Linq 语法查询 CosmosDb
Using Ardalis.Specification to query CosmosDb using Linq syntax
我们有一个可行的解决方案,它使用规范模式通过纯文本 SQL 语句访问 CosmosDb。
我们正在尝试使用最新版本的 Ardalis.Specification (5.1.0) 来做同样的事情,但是使用 LINQ 在我们的 sql.
中提供类型安全
对于集合 foo
我们有一个规范:
using System.Linq;
using Ardalis.Specification;
using Example.Sample.Core.Entities;
namespace Example.Sample.Core.Specifications
{
public class FooFromIdSpecification : Specification<Foo>
{
public FooFromIdSpecification(string id)
{
Query.Where(x => x.Id == id);
}
}
}
我们遇到问题的地方是在基本通用存储库中...获取代码以根据规范生成 sql:
public async IAsyncEnumerable<T> GetItemsAsyncEnumerable(ISpecification<T> specification)
{
# This is the line that is not working
var foo = (IQueryable<T>)specification.Evaluate(_container.GetItemLinqQueryable<T>());
using var iterator = foo.ToFeedIterator<T>();
while (iterator.HasMoreResults)
{
var response = await iterator.ReadNextAsync();
foreach (var item in response)
{
yield return item;
}
}
}
让评估器工作时碰壁。可能遗漏了一些明显的东西。
问题
上面的代码在调用时没有命中任何 try-catch 块,但 foo
为空。
我们参考了一些来源
我们 'got something working' ... 不优雅,但能胜任。
Gotcha - 我和我的同事无法通过主要实现发现使用 SelectMany,这在从单独的集合中获取数组时是必需的,例如在 SQL 世界中:
select s.foo from c join s in c.someArray
这是有效的方法:
- 创建了自己的子class 规格
- 实施了 hacky 评估器
using Ardalis.Specification;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Whatever.Namespace.You.Want
{
public interface ICosmosDbSpecification<T, TMapped> : ISpecification<T, TMapped>
{
Expression<Func<T, IEnumerable<TMapped>>>? ManySelector { get; }
}
}
实现该接口以在堆栈中获取 SelectMany 功能:
using Ardalis.Specification;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Whatever.Namespace.You.Want
{
public abstract class CosmosDbSpecification<T, TMapped> : Specification<T, TMapped>, ICosmosDbSpecification<T, TMapped>
{
protected new virtual ICosmosDbSpecificationBuilder<T, TMapped> Query { get; }
public Expression<Func<T, IEnumerable<TMapped>>>? ManySelector { get; internal set; }
protected CosmosDbSpecification()
: this(InMemorySpecificationEvaluator.Default)
{
}
protected CosmosDbSpecification(IInMemorySpecificationEvaluator inMemorySpecificationEvaluator)
: base(inMemorySpecificationEvaluator)
{
this.Query = new CosmosDbSpecificationBuilder<T, TMapped>(this);
}
}
public interface ICosmosDbSpecificationBuilder<T, TResult> : ISpecificationBuilder<T, TResult>
{
new CosmosDbSpecification<T, TResult> Specification { get; }
}
public class CosmosDbSpecificationBuilder<T, TResult> : SpecificationBuilder<T, TResult>, ICosmosDbSpecificationBuilder<T, TResult>
{
public new CosmosDbSpecification<T, TResult> Specification { get; }
public CosmosDbSpecificationBuilder(CosmosDbSpecification<T, TResult> specification)
: base(specification)
{
this.Specification = specification;
}
}
public static class CosmosDbSpecificationBuilderExtensions
{
/// <summary>
/// Allows CosmosDb SelectMany methods. WARNING can only have Select OR SelectMany ... using both may throw
/// </summary>
public static ICosmosDbSpecificationBuilder<T, TResult> SelectMany<T, TResult>(
this ICosmosDbSpecificationBuilder<T, TResult> specificationBuilder,
Expression<Func<T, IEnumerable<TResult>>> manySelector)
{
specificationBuilder.Specification.ManySelector = manySelector;
return specificationBuilder;
}
}
}
可能应该将 InMemorySpecificationEvaluator.Default
单例更改为我们自己的单例...但当前的实现有效并继续进行。
然后使用定制的评估器之类的东西将所有这些都缝合在存储库中:
using Ardalis.Specification;
using Microsoft.Azure.Cosmos;
using System;
using System.Linq;
using System.Linq.Expressions;
namespace Whatever.Namespace.You.Want
{
public static class SpecificationEvaluator <T>
{
public static IOrderedQueryable<TResult> ApplySpecification<TResult>(Container container, ISpecification<T, TResult> specification)
{
var queryable = container.GetItemLinqQueryable<T>(
true, default, default,
new CosmosLinqSerializerOptions { PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase });
foreach (var criteria in specification.WhereExpressions)
{
queryable = (IOrderedQueryable<T>)queryable.Where(criteria);
}
if (specification.OrderExpressions != null)
{
if (specification.OrderExpressions.Where(x => x.OrderType == OrderTypeEnum.OrderBy ||
x.OrderType == OrderTypeEnum.OrderByDescending).Count() > 1)
{
throw new DuplicateOrderChainException();
}
IOrderedQueryable<T> orderedQuery = null;
foreach (var orderExpression in specification.OrderExpressions)
{
if (orderExpression.OrderType == OrderTypeEnum.OrderBy)
{
orderedQuery = Queryable.OrderBy((dynamic)queryable, (dynamic)RemoveConvert(orderExpression.KeySelector));
}
else if (orderExpression.OrderType == OrderTypeEnum.OrderByDescending)
{
orderedQuery = Queryable.OrderByDescending((dynamic)queryable, (dynamic)RemoveConvert(orderExpression.KeySelector));
}
else if (orderExpression.OrderType == OrderTypeEnum.ThenBy)
{
orderedQuery = Queryable.ThenBy((dynamic)orderedQuery, (dynamic)RemoveConvert(orderExpression.KeySelector));
}
else if (orderExpression.OrderType == OrderTypeEnum.ThenByDescending)
{
orderedQuery = Queryable.ThenByDescending((dynamic)orderedQuery, (dynamic)RemoveConvert(orderExpression.KeySelector));
}
}
if (orderedQuery != null)
{
queryable = orderedQuery;
}
}
if (specification.Skip != null && specification.Skip != 0)
{
queryable = (IOrderedQueryable<T>)queryable.Skip(specification.Skip.Value);
}
if (specification.Take != null)
{
queryable = (IOrderedQueryable<T>)queryable.Take(specification.Take.Value);
}
if (typeof(ICosmosDbSpecification<T, TResult>).IsAssignableFrom(specification.GetType()))
{
var selectMany = ((ICosmosDbSpecification<T, TResult>)specification).ManySelector;
if (selectMany != null)
{
if (specification.Selector != null)
{
throw new ApplicationException("Cannot set both Selector and ManySelector on same specification");
}
if (specification.Take != null || specification.Skip != null)
{
// until figured out how to implement this on final solution instead of inner root request (gives not supported error in sdk)
throw new ApplicationException("Select many does not support take or skip ...");
}
return (IOrderedQueryable<TResult>)queryable.SelectMany(selectMany);
}
}
return (IOrderedQueryable<TResult>)queryable.Select(specification.Selector);
}
private static LambdaExpression RemoveConvert(LambdaExpression source)
{
var body = source.Body;
while (body.NodeType == ExpressionType.Convert)
body = ((UnaryExpression)body).Operand;
return Expression.Lambda(body, source.Parameters);
}
}
}
并在您的通用基础存储库中如此使用:
IOrderedQueryable<TResult> queryable =
SpecificationEvaluator<T>.ApplySpecification(_container, specification);
using var iterator = queryable.ToFeedIterator<TResult>();
...
规格类似于:
public class GetDetailSpecification : CosmosDbSpecification<TypeOfData, TypeOfOutput>
{
public GetFooBarSpecification(YourParameterisedFilterObject filter)
{
if (filter == null) throw new ArgumentNullException(nameof(filter));
Query.Select(x => new TypeOfOutput { Foo = x.Bar });
Query.Where(x => x.Id == filter.Id && x.PartitionKey == filter.PartitionKeyValue);
}
}
我们有一个可行的解决方案,它使用规范模式通过纯文本 SQL 语句访问 CosmosDb。
我们正在尝试使用最新版本的 Ardalis.Specification (5.1.0) 来做同样的事情,但是使用 LINQ 在我们的 sql.
中提供类型安全对于集合 foo
我们有一个规范:
using System.Linq;
using Ardalis.Specification;
using Example.Sample.Core.Entities;
namespace Example.Sample.Core.Specifications
{
public class FooFromIdSpecification : Specification<Foo>
{
public FooFromIdSpecification(string id)
{
Query.Where(x => x.Id == id);
}
}
}
我们遇到问题的地方是在基本通用存储库中...获取代码以根据规范生成 sql:
public async IAsyncEnumerable<T> GetItemsAsyncEnumerable(ISpecification<T> specification)
{
# This is the line that is not working
var foo = (IQueryable<T>)specification.Evaluate(_container.GetItemLinqQueryable<T>());
using var iterator = foo.ToFeedIterator<T>();
while (iterator.HasMoreResults)
{
var response = await iterator.ReadNextAsync();
foreach (var item in response)
{
yield return item;
}
}
}
让评估器工作时碰壁。可能遗漏了一些明显的东西。
问题
上面的代码在调用时没有命中任何 try-catch 块,但 foo
为空。
我们参考了一些来源
我们 'got something working' ... 不优雅,但能胜任。
Gotcha - 我和我的同事无法通过主要实现发现使用 SelectMany,这在从单独的集合中获取数组时是必需的,例如在 SQL 世界中:
select s.foo from c join s in c.someArray
这是有效的方法:
- 创建了自己的子class 规格
- 实施了 hacky 评估器
using Ardalis.Specification;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Whatever.Namespace.You.Want
{
public interface ICosmosDbSpecification<T, TMapped> : ISpecification<T, TMapped>
{
Expression<Func<T, IEnumerable<TMapped>>>? ManySelector { get; }
}
}
实现该接口以在堆栈中获取 SelectMany 功能:
using Ardalis.Specification;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Whatever.Namespace.You.Want
{
public abstract class CosmosDbSpecification<T, TMapped> : Specification<T, TMapped>, ICosmosDbSpecification<T, TMapped>
{
protected new virtual ICosmosDbSpecificationBuilder<T, TMapped> Query { get; }
public Expression<Func<T, IEnumerable<TMapped>>>? ManySelector { get; internal set; }
protected CosmosDbSpecification()
: this(InMemorySpecificationEvaluator.Default)
{
}
protected CosmosDbSpecification(IInMemorySpecificationEvaluator inMemorySpecificationEvaluator)
: base(inMemorySpecificationEvaluator)
{
this.Query = new CosmosDbSpecificationBuilder<T, TMapped>(this);
}
}
public interface ICosmosDbSpecificationBuilder<T, TResult> : ISpecificationBuilder<T, TResult>
{
new CosmosDbSpecification<T, TResult> Specification { get; }
}
public class CosmosDbSpecificationBuilder<T, TResult> : SpecificationBuilder<T, TResult>, ICosmosDbSpecificationBuilder<T, TResult>
{
public new CosmosDbSpecification<T, TResult> Specification { get; }
public CosmosDbSpecificationBuilder(CosmosDbSpecification<T, TResult> specification)
: base(specification)
{
this.Specification = specification;
}
}
public static class CosmosDbSpecificationBuilderExtensions
{
/// <summary>
/// Allows CosmosDb SelectMany methods. WARNING can only have Select OR SelectMany ... using both may throw
/// </summary>
public static ICosmosDbSpecificationBuilder<T, TResult> SelectMany<T, TResult>(
this ICosmosDbSpecificationBuilder<T, TResult> specificationBuilder,
Expression<Func<T, IEnumerable<TResult>>> manySelector)
{
specificationBuilder.Specification.ManySelector = manySelector;
return specificationBuilder;
}
}
}
可能应该将 InMemorySpecificationEvaluator.Default
单例更改为我们自己的单例...但当前的实现有效并继续进行。
然后使用定制的评估器之类的东西将所有这些都缝合在存储库中:
using Ardalis.Specification;
using Microsoft.Azure.Cosmos;
using System;
using System.Linq;
using System.Linq.Expressions;
namespace Whatever.Namespace.You.Want
{
public static class SpecificationEvaluator <T>
{
public static IOrderedQueryable<TResult> ApplySpecification<TResult>(Container container, ISpecification<T, TResult> specification)
{
var queryable = container.GetItemLinqQueryable<T>(
true, default, default,
new CosmosLinqSerializerOptions { PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase });
foreach (var criteria in specification.WhereExpressions)
{
queryable = (IOrderedQueryable<T>)queryable.Where(criteria);
}
if (specification.OrderExpressions != null)
{
if (specification.OrderExpressions.Where(x => x.OrderType == OrderTypeEnum.OrderBy ||
x.OrderType == OrderTypeEnum.OrderByDescending).Count() > 1)
{
throw new DuplicateOrderChainException();
}
IOrderedQueryable<T> orderedQuery = null;
foreach (var orderExpression in specification.OrderExpressions)
{
if (orderExpression.OrderType == OrderTypeEnum.OrderBy)
{
orderedQuery = Queryable.OrderBy((dynamic)queryable, (dynamic)RemoveConvert(orderExpression.KeySelector));
}
else if (orderExpression.OrderType == OrderTypeEnum.OrderByDescending)
{
orderedQuery = Queryable.OrderByDescending((dynamic)queryable, (dynamic)RemoveConvert(orderExpression.KeySelector));
}
else if (orderExpression.OrderType == OrderTypeEnum.ThenBy)
{
orderedQuery = Queryable.ThenBy((dynamic)orderedQuery, (dynamic)RemoveConvert(orderExpression.KeySelector));
}
else if (orderExpression.OrderType == OrderTypeEnum.ThenByDescending)
{
orderedQuery = Queryable.ThenByDescending((dynamic)orderedQuery, (dynamic)RemoveConvert(orderExpression.KeySelector));
}
}
if (orderedQuery != null)
{
queryable = orderedQuery;
}
}
if (specification.Skip != null && specification.Skip != 0)
{
queryable = (IOrderedQueryable<T>)queryable.Skip(specification.Skip.Value);
}
if (specification.Take != null)
{
queryable = (IOrderedQueryable<T>)queryable.Take(specification.Take.Value);
}
if (typeof(ICosmosDbSpecification<T, TResult>).IsAssignableFrom(specification.GetType()))
{
var selectMany = ((ICosmosDbSpecification<T, TResult>)specification).ManySelector;
if (selectMany != null)
{
if (specification.Selector != null)
{
throw new ApplicationException("Cannot set both Selector and ManySelector on same specification");
}
if (specification.Take != null || specification.Skip != null)
{
// until figured out how to implement this on final solution instead of inner root request (gives not supported error in sdk)
throw new ApplicationException("Select many does not support take or skip ...");
}
return (IOrderedQueryable<TResult>)queryable.SelectMany(selectMany);
}
}
return (IOrderedQueryable<TResult>)queryable.Select(specification.Selector);
}
private static LambdaExpression RemoveConvert(LambdaExpression source)
{
var body = source.Body;
while (body.NodeType == ExpressionType.Convert)
body = ((UnaryExpression)body).Operand;
return Expression.Lambda(body, source.Parameters);
}
}
}
并在您的通用基础存储库中如此使用:
IOrderedQueryable<TResult> queryable =
SpecificationEvaluator<T>.ApplySpecification(_container, specification);
using var iterator = queryable.ToFeedIterator<TResult>();
...
规格类似于:
public class GetDetailSpecification : CosmosDbSpecification<TypeOfData, TypeOfOutput>
{
public GetFooBarSpecification(YourParameterisedFilterObject filter)
{
if (filter == null) throw new ArgumentNullException(nameof(filter));
Query.Select(x => new TypeOfOutput { Foo = x.Bar });
Query.Where(x => x.Id == filter.Id && x.PartitionKey == filter.PartitionKeyValue);
}
}