【问题标题】:Using Ardalis.Specification to query CosmosDb using Linq syntax使用 Ardalis.Specification 使用 Linq 语法查询 CosmosDb
【发布时间】:2021-08-25 08:44:08
【问题描述】:

我们有一个可行的解决方案,它使用规范模式通过纯文本 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 为空。

我们引用的一些来源

【问题讨论】:

  • 那么.....有什么问题?您需要解释实际问题。错误信息?输出错误?
  • it's not working 我知道这并没有多大帮助......这就像在黑暗中哭泣,看看是否有其他人尝试过这个问题。我让我的同事创建了一个简单的基本解决方案,我们将在准备演示时将其发布到 GitHub。
  • @Ruskin,你克服了这个问题吗?我目前面临同样的问题。请回答您的问题,如果您回答了,请接受。 ????

标签: c# linq azure-cosmosdb azure-cosmosdb-sqlapi ardalis-specification


【解决方案1】:

我们'得到了一些工作'......不优雅但可以完成工作。

Gotcha - 我和我的同事没有办法通过主实现来使用 SelectMany,这在从单独的集合中获取数组时是必需的,例如在 SQL 世界中:

select s.foo from c join s in c.someArray

这是有效的:

  • 创建了自己的子类关闭规范
  • 实现了 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);
        }
}

【讨论】:

  • 罗伯特告诉我你是否有更优雅的解决方案......祝你好运
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-01
  • 1970-01-01
  • 2013-08-09
  • 1970-01-01
相关资源
最近更新 更多