2

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

我们参考的一些来源

4

1 回答 1

0

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

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);
        }
}
于 2021-12-28T22:47:28.197 回答