We 'got something working' ... not elegant but does the job.
Gotcha - there is no way my colleagues and I discovered of using SelectMany using main implementation, which is needed when getting arrays from separate collections e.g. in SQL world:
select s.foo from c join s in c.someArray
This is what worked:
- Created own sub-class off Specification
- Implemented hacky evaluator
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; }
}
}
Implementing that interface to get the SelectMany functionality down the stack:
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;
}
}
}
Should probably have changed the InMemorySpecificationEvaluator.Default
singleton to our own ... but current implementation works and getting on with things.
This then all gets stitched together in repository using bespoke evaluator-like thing:
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);
}
}
}
and use as so in your generic base repository:
IOrderedQueryable<TResult> queryable =
SpecificationEvaluator<T>.ApplySpecification(_container, specification);
using var iterator = queryable.ToFeedIterator<TResult>();
...
With a specification something like:
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);
}
}