diff --git a/PoweredSoft.DynamicQuery.Cli/PoweredSoft.DynamicQuery.Cli.csproj b/PoweredSoft.DynamicQuery.Cli/PoweredSoft.DynamicQuery.Cli.csproj index 9aff51a..3ec5c61 100644 --- a/PoweredSoft.DynamicQuery.Cli/PoweredSoft.DynamicQuery.Cli.csproj +++ b/PoweredSoft.DynamicQuery.Cli/PoweredSoft.DynamicQuery.Cli.csproj @@ -5,6 +5,10 @@ netcoreapp2.1 + + + + diff --git a/PoweredSoft.DynamicQuery.Cli/Program.cs b/PoweredSoft.DynamicQuery.Cli/Program.cs index a3dc86d..6b91df8 100644 --- a/PoweredSoft.DynamicQuery.Cli/Program.cs +++ b/PoweredSoft.DynamicQuery.Cli/Program.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace PoweredSoft.DynamicQuery.Cli { @@ -73,26 +75,42 @@ namespace PoweredSoft.DynamicQuery.Cli }; var queryable = list.AsQueryable(); - var criteria = new QueryCriteria(); + criteria.Page = 1; + criteria.PageSize = 10; - criteria.Filters.Add(new SimpleFilter + criteria.Filters = new List { - Path = "LastName", - Value = "Lebee", - Type = FilterType.Equal, - }); - - criteria.Filters.Add(new SimpleFilter - { - Path = "FirstName", - Value = "David,Michaela", - Type = FilterType.Equal, - }); + new SimpleFilter() {Path = nameof(Person.LastName), Value = "Lebee", Type = FilterType.Equal}, + new CompositeFilter() + { + Type = FilterType.Composite, + And = true, + Filters = new List + { + new SimpleFilter() {Path = nameof(Person.FirstName), Value = "David", Type = FilterType.Equal}, + new SimpleFilter() {Path = nameof(Person.FirstName), Value = "Zohra", Type = FilterType.Equal}, + } + } + }; var handler = new QueryHandler(); handler.AddInterceptor(new PersonQueryInterceptor()); - handler.Execute(queryable, criteria); + var result = handler.Execute(queryable, criteria); + + var jsonSettings = new JsonSerializerSettings() + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }; + + jsonSettings.Converters.Add(new StringEnumConverter { AllowIntegerValues = false }); + + Console.WriteLine("Request:\n"); + Console.WriteLine(JsonConvert.SerializeObject(criteria, Formatting.Indented, jsonSettings)); + Console.WriteLine(""); + Console.WriteLine("Response:\n"); + Console.WriteLine(JsonConvert.SerializeObject(result, Formatting.Indented, jsonSettings)); + Console.ReadKey(); } } } diff --git a/PoweredSoft.DynamicQuery.Core/IBeforeQueryAlteredInterceptor.cs b/PoweredSoft.DynamicQuery.Core/IBeforeQueryAlteredInterceptor.cs deleted file mode 100644 index 7fadcc5..0000000 --- a/PoweredSoft.DynamicQuery.Core/IBeforeQueryAlteredInterceptor.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Linq; - -namespace PoweredSoft.DynamicQuery.Core -{ - public interface IBeforeQueryAlteredInterceptor : IQueryInterceptor - { - IQueryable InterceptQueryBeforeAltered(IQueryCriteria criteria, IQueryable queryable); - } - - public interface IBeforeQueryAlteredInterceptor : IQueryInterceptor - { - IQueryable InterceptQueryBeforeAltered(IQueryCriteria criteria, IQueryable queryable); - } -} diff --git a/PoweredSoft.DynamicQuery.Core/IBeforeQueryExecuteInterceptor.cs b/PoweredSoft.DynamicQuery.Core/IBeforeQueryExecuteInterceptor.cs deleted file mode 100644 index 6896da6..0000000 --- a/PoweredSoft.DynamicQuery.Core/IBeforeQueryExecuteInterceptor.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Linq; - -namespace PoweredSoft.DynamicQuery.Core -{ - public interface IBeforeQueryExecuteInterceptor : IQueryInterceptor - { - IQueryable InterceptBeforeQuery(IQueryCriteria criteria, IQueryable queryable); - } - - public interface IBeforeQueryExecuteInterceptor : IQueryInterceptor - { - IQueryable InterceptBeforeQuery(IQueryCriteria criteria, IQueryable queryable); - } -} diff --git a/PoweredSoft.DynamicQuery.Core/IBeforeQueryFilterInterceptor.cs b/PoweredSoft.DynamicQuery.Core/IBeforeQueryFilterInterceptor.cs new file mode 100644 index 0000000..a3d0d80 --- /dev/null +++ b/PoweredSoft.DynamicQuery.Core/IBeforeQueryFilterInterceptor.cs @@ -0,0 +1,14 @@ +using System.Linq; + +namespace PoweredSoft.DynamicQuery.Core +{ + public interface IBeforeQueryFilterInterceptor : IQueryInterceptor + { + IQueryable InterceptBeforeFiltering(IQueryCriteria criteria, IQueryable queryable); + } + + public interface IBeforeQueryFilterInterceptor : IQueryInterceptor + { + IQueryable InterceptBeforeFiltering(IQueryCriteria criteria, IQueryable queryable); + } +} diff --git a/PoweredSoft.DynamicQuery.Core/IIncludeStrategyInterceptor.cs b/PoweredSoft.DynamicQuery.Core/IIncludeStrategyInterceptor.cs new file mode 100644 index 0000000..0e0a240 --- /dev/null +++ b/PoweredSoft.DynamicQuery.Core/IIncludeStrategyInterceptor.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace PoweredSoft.DynamicQuery.Core +{ + public interface IIncludeStrategyInterceptor : IQueryInterceptor + { + IQueryable InterceptIncludeStrategy(IQueryCriteria criteria, IQueryable queryable); + } + + public interface IIncludeStrategyInterceptor : IQueryInterceptor + { + IQueryable InterceptIncludeStrategy(IQueryCriteria criteria, IQueryable queryable); + } +} diff --git a/PoweredSoft.DynamicQuery.Core/INoSortInterceptor.cs b/PoweredSoft.DynamicQuery.Core/INoSortInterceptor.cs new file mode 100644 index 0000000..6b79dda --- /dev/null +++ b/PoweredSoft.DynamicQuery.Core/INoSortInterceptor.cs @@ -0,0 +1,14 @@ +using System.Linq; + +namespace PoweredSoft.DynamicQuery.Core +{ + public interface INoSortInterceptor : IQueryInterceptor + { + IQueryable InterceptNoSort(IQueryable queryable); + } + + public interface INoSortInterceptor : IQueryInterceptor + { + IQueryable InterceptNoSort(IQueryable queryable); + } +} diff --git a/PoweredSoft.DynamicQuery.Core/IQueryHandler.cs b/PoweredSoft.DynamicQuery.Core/IQueryHandler.cs index 335a56a..ca6738a 100644 --- a/PoweredSoft.DynamicQuery.Core/IQueryHandler.cs +++ b/PoweredSoft.DynamicQuery.Core/IQueryHandler.cs @@ -5,10 +5,18 @@ using System.Threading.Tasks; namespace PoweredSoft.DynamicQuery.Core { - public interface IQueryHandler + public interface IInterceptableQueryHandler { - IQueryResult Execute(IQueryable queryable, IQueryCriteria criteria); - Task ExecuteAsync(IQueryable queryable, IQueryCriteria criteria); void AddInterceptor(IQueryInterceptor interceptor); } + + public interface IQueryHandler : IInterceptableQueryHandler + { + IQueryExecutionResult Execute(IQueryable queryable, IQueryCriteria criteria); + } + + public interface IAsyncQueryHandler : IInterceptableQueryHandler + { + Task ExecuteAsync(IQueryable queryable, IQueryCriteria criteria); + } } diff --git a/PoweredSoft.DynamicQuery.Core/IQueryInterceptor.cs b/PoweredSoft.DynamicQuery.Core/IQueryInterceptor.cs index 4c4db2f..a806bee 100644 --- a/PoweredSoft.DynamicQuery.Core/IQueryInterceptor.cs +++ b/PoweredSoft.DynamicQuery.Core/IQueryInterceptor.cs @@ -1,7 +1,11 @@ -namespace PoweredSoft.DynamicQuery.Core +using System.Linq; + +namespace PoweredSoft.DynamicQuery.Core { public interface IQueryInterceptor { } + + } diff --git a/PoweredSoft.DynamicQuery.Core/IQueryResult.cs b/PoweredSoft.DynamicQuery.Core/IQueryResult.cs index 5fda81f..d42fceb 100644 --- a/PoweredSoft.DynamicQuery.Core/IQueryResult.cs +++ b/PoweredSoft.DynamicQuery.Core/IQueryResult.cs @@ -13,17 +13,21 @@ namespace PoweredSoft.DynamicQuery.Core public interface IQueryResult { - long Count { get; } List Aggregates { get; } - } - - public interface IQueryResultSimple : IQueryResult - { List Data { get; } } - public interface IQueryResultGrouped : IQueryResult + public interface IGroupQueryResult : IQueryResult { - List Data { get; } + string GroupPath { get; set; } + object GroupValue { get; set; } } + + public interface IQueryExecutionResult : IQueryResult + { + long TotalRecords { get; set; } + long? NumberOfPages { get; set; } + } + + } diff --git a/PoweredSoft.DynamicQuery.Core/ISortInteceptor.cs b/PoweredSoft.DynamicQuery.Core/ISortInterceptor.cs similarity index 71% rename from PoweredSoft.DynamicQuery.Core/ISortInteceptor.cs rename to PoweredSoft.DynamicQuery.Core/ISortInterceptor.cs index 14c76a8..211d122 100644 --- a/PoweredSoft.DynamicQuery.Core/ISortInteceptor.cs +++ b/PoweredSoft.DynamicQuery.Core/ISortInterceptor.cs @@ -2,7 +2,7 @@ namespace PoweredSoft.DynamicQuery.Core { - public interface ISortInteceptor : IQueryInterceptor + public interface ISortInterceptor : IQueryInterceptor { IEnumerable InterceptSort(ISort sort); } diff --git a/PoweredSoft.DynamicQuery/QueryHandler.cs b/PoweredSoft.DynamicQuery/QueryHandler.cs index dad7842..8078891 100644 --- a/PoweredSoft.DynamicQuery/QueryHandler.cs +++ b/PoweredSoft.DynamicQuery/QueryHandler.cs @@ -1,142 +1,61 @@ -using PoweredSoft.DynamicQuery.Core; -using PoweredSoft.DynamicLinq; -using System; +using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Net; using System.Reflection; using System.Text; -using System.Threading.Tasks; -using PoweredSoft.DynamicLinq.Fluent; -using PoweredSoft.DynamicQuery.Extensions; +using PoweredSoft.DynamicLinq; +using PoweredSoft.DynamicQuery.Core; namespace PoweredSoft.DynamicQuery { - public class QueryHandler : IQueryHandler + public class QueryHandler : QueryHandlerBase, IQueryHandler { - protected List Interceptors { get; } = new List(); - protected IQueryCriteria Criteria { get; set; } - protected IQueryable QueryableAtStart { get; private set; } - protected IQueryable CurrentQueryable { get; set; } - protected Type QueryableUnderlyingType => QueryableAtStart.ElementType; - private MethodInfo ApplyInterceptorsAndCriteriaMethod { get; } = typeof(QueryHandler).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).First(t => t.Name == "ApplyInterceptorsAndCriteria" && t.IsGenericMethod); + internal MethodInfo ExecuteGeneric = typeof(QueryHandler).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).First(t => t.Name == "Execute" && t.IsGenericMethod); + internal IQueryExecutionResult ExecuteReflected() => (IQueryExecutionResult)ExecuteGeneric.MakeGenericMethod(QueryableUnderlyingType).Invoke(this, new object[]{}); - protected virtual void Reset(IQueryable queryable, IQueryCriteria criteria) + protected virtual IQueryExecutionResult Execute() { - Criteria = criteria ?? throw new ArgumentNullException("criteria"); - QueryableAtStart = queryable ?? throw new ArgumentNullException("queryable"); - CurrentQueryable = QueryableAtStart; - } - - public virtual void AddInterceptor(IQueryInterceptor interceptor) - { - if (interceptor == null) throw new ArgumentNullException("interceptor"); - - if (!Interceptors.Contains(interceptor)) - Interceptors.Add(interceptor); - } - - protected virtual void ApplyInterceptorsAndCriteria() - { - ApplySimpleBeforeAlterInterceptors(); - ApplyGenericBeforeAlterInterceptors(); + ApplyIncludeStrategyInterceptors(); + ApplyBeforeFilterInterceptors(); ApplyFilters(); + return HasGrouping ? ExecuteGrouping() : ExecuteNoGrouping(); + } - // create group levels. - if (Criteria.Groups.Count != 0) + protected virtual IQueryExecutionResult ExecuteGrouping() + { + throw new NotImplementedException(); + } + + protected virtual IQueryExecutionResult ExecuteNoGrouping() + { + var result = new QueryExecutionResult(); + + // total records. + result.TotalRecords = CurrentQueryable.LongCount(); + + // sorts and paging. + ApplyNoGroupingSorts(); + ApplyNoGroupingPaging(); + + // the data. + result.Data = CurrentQueryable.ToObjectList(); + + // if there is paging. + if (HasPaging) { - + if (result.TotalRecords < Criteria.PageSize) + result.NumberOfPages = 1; + else + result.NumberOfPages = result.TotalRecords / Criteria.PageSize + (result.TotalRecords % Criteria.PageSize != 0 ? 1 : 0); } - // TODO. + return result; } - protected virtual ConditionOperators? ResolveFromOrDefault(FilterType filterType) => - filterType.ConditionOperator(); - - protected virtual ConditionOperators ResolveFrom(FilterType filterType) - { - var ret = ResolveFromOrDefault(filterType); - if (ret == null) - throw new NotSupportedException($"{filterType} is not supported"); - - return ret.Value; - } - - protected virtual void ApplyFilters() - { - CurrentQueryable = CurrentQueryable.Query(whereBuilder => - { - Criteria.Filters.ForEach(filter => ApplyFilter(whereBuilder, filter)); - }); - } - - protected virtual void ApplyFilter(WhereBuilder whereBuilder, IFilter filter) - { - var transformedFilter = InterceptFilter(filter); - if (transformedFilter is ISimpleFilter) - ApplySimpleFilter(whereBuilder, transformedFilter as ISimpleFilter); - else if (transformedFilter is ICompositeFilter) - AppleCompositeFilter(whereBuilder, transformedFilter as ICompositeFilter); - else - throw new NotSupportedException(); - } - - protected virtual void AppleCompositeFilter(WhereBuilder whereBuilder, ICompositeFilter filter) - { - whereBuilder.SubQuery(subWhereBuilder => filter.Filters.ForEach(subFilter => ApplyFilter(subWhereBuilder, subFilter)), filter.And == true); - } - - protected virtual void ApplySimpleFilter(WhereBuilder whereBuilder, ISimpleFilter filter) - { - var resolvedConditionOperator = ResolveFrom(filter.Type); - whereBuilder.Compare(filter.Path, resolvedConditionOperator, filter.Value, and: filter.And == true); - } - - private IFilter InterceptFilter(IFilter filter) - { - var ret = Interceptors.Where(t => t is IFilterInterceptor) - .Cast() - .Aggregate(filter, (previousFilter, interceptor) => interceptor.InterceptFilter(previousFilter)); - - return ret; - } - - private void ApplyInterceptorsAndCriteria() - { - var genericMethod = ApplyInterceptorsAndCriteriaMethod.MakeGenericMethod(QueryableUnderlyingType); - genericMethod.Invoke(this, null); - } - - protected virtual void ApplyGenericBeforeAlterInterceptors() - { - CurrentQueryable = Interceptors - .Where(t => t is IBeforeQueryAlteredInterceptor) - .Cast>() - .Aggregate((IQueryable)CurrentQueryable, (prev, interceptor) => interceptor.InterceptQueryBeforeAltered(Criteria, prev)); - } - - protected virtual void ApplySimpleBeforeAlterInterceptors() - { - CurrentQueryable = Interceptors - .Where(t => t is IBeforeQueryAlteredInterceptor) - .Cast() - .Aggregate(CurrentQueryable, (prev, interceptor) => interceptor.InterceptQueryBeforeAltered(Criteria, prev)); - } - - public virtual IQueryResult Execute(IQueryable queryable, IQueryCriteria criteria) + public virtual IQueryExecutionResult Execute(IQueryable queryable, IQueryCriteria criteria) { Reset(queryable, criteria); - ApplyInterceptorsAndCriteria(); - var debug = CurrentQueryable.ToObjectList(); - return null; - } - - - public virtual Task ExecuteAsync(IQueryable queryable, IQueryCriteria criteria) - { - throw new NotImplementedException(); + return ExecuteReflected(); } } } diff --git a/PoweredSoft.DynamicQuery/QueryHandlerBase.cs b/PoweredSoft.DynamicQuery/QueryHandlerBase.cs new file mode 100644 index 0000000..d01d420 --- /dev/null +++ b/PoweredSoft.DynamicQuery/QueryHandlerBase.cs @@ -0,0 +1,167 @@ +using PoweredSoft.DynamicQuery.Core; +using PoweredSoft.DynamicLinq; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using PoweredSoft.DynamicLinq.Fluent; +using PoweredSoft.DynamicQuery.Extensions; + +namespace PoweredSoft.DynamicQuery +{ + public abstract class QueryHandlerBase : IInterceptableQueryHandler + { + protected List Interceptors { get; } = new List(); + protected IQueryCriteria Criteria { get; set; } + protected IQueryable QueryableAtStart { get; private set; } + protected IQueryable CurrentQueryable { get; set; } + protected Type QueryableUnderlyingType => QueryableAtStart.ElementType; + protected bool HasGrouping => Criteria.Groups?.Any() == true; + protected bool HasPaging => Criteria.PageSize.HasValue && Criteria.PageSize > 0; + + protected virtual void Reset(IQueryable queryable, IQueryCriteria criteria) + { + Criteria = criteria ?? throw new ArgumentNullException("criteria"); + QueryableAtStart = queryable ?? throw new ArgumentNullException("queryable"); + CurrentQueryable = QueryableAtStart; + } + + public virtual void AddInterceptor(IQueryInterceptor interceptor) + { + if (interceptor == null) throw new ArgumentNullException("interceptor"); + + if (!Interceptors.Contains(interceptor)) + Interceptors.Add(interceptor); + } + + protected virtual void ApplyNoGroupingPaging() + { + if (!HasPaging) + return; + + var q = (IQueryable) CurrentQueryable; + var skip = ((Criteria.Page ?? 1) - 1) * Criteria.PageSize.Value; + CurrentQueryable = q.Skip(skip).Take(Criteria.PageSize.Value); + } + + protected virtual void ApplyNoGroupingSorts() + { + if (Criteria.Sorts?.Any() != true) + { + ApplyNoSortInterceptor(); + return; + } + + Criteria.Sorts.ForEach(sort => + { + var transformedSort = InterceptSort(sort); + if (transformedSort.Count == 0) + return; + }); + } + + protected virtual List InterceptSort(ISort sort) + { + var ret = Interceptors + .Where(t => t is ISortInterceptor) + .Cast() + .SelectMany(interceptor => interceptor.InterceptSort(sort)); + + return ret.Distinct().ToList(); + } + + protected virtual void ApplyNoSortInterceptor() + { + CurrentQueryable = Interceptors.Where(t => t is INoSortInterceptor) + .Cast() + .Aggregate(CurrentQueryable, (prev, interceptor) => interceptor.InterceptNoSort(prev)); + + CurrentQueryable = Interceptors.Where(t => t is INoSortInterceptor) + .Cast>() + .Aggregate((IQueryable)CurrentQueryable, (prev, interceptor) => interceptor.InterceptNoSort(prev)); + } + + protected virtual ConditionOperators? ResolveFromOrDefault(FilterType filterType) => filterType.ConditionOperator(); + + protected virtual ConditionOperators ResolveFrom(FilterType filterType) + { + var ret = ResolveFromOrDefault(filterType); + if (ret == null) + throw new NotSupportedException($"{filterType} is not supported"); + + return ret.Value; + } + + protected virtual void ApplyFilters() + { + if (true != Criteria.Filters?.Any()) + return; + + CurrentQueryable = CurrentQueryable.Query(whereBuilder => + { + Criteria.Filters.ForEach(filter => ApplyFilter(whereBuilder, filter)); + }); + } + + protected virtual void ApplyFilter(WhereBuilder whereBuilder, IFilter filter) + { + var transformedFilter = InterceptFilter(filter); + if (transformedFilter is ISimpleFilter) + ApplySimpleFilter(whereBuilder, transformedFilter as ISimpleFilter); + else if (transformedFilter is ICompositeFilter) + AppleCompositeFilter(whereBuilder, transformedFilter as ICompositeFilter); + else + throw new NotSupportedException(); + } + + protected virtual void AppleCompositeFilter(WhereBuilder whereBuilder, ICompositeFilter filter) + { + whereBuilder.SubQuery(subWhereBuilder => filter.Filters.ForEach(subFilter => ApplyFilter(subWhereBuilder, subFilter)), filter.And == true); + } + + protected virtual void ApplySimpleFilter(WhereBuilder whereBuilder, ISimpleFilter filter) + { + var resolvedConditionOperator = ResolveFrom(filter.Type); + whereBuilder.Compare(filter.Path, resolvedConditionOperator, filter.Value, and: filter.And == true); + } + + protected virtual IFilter InterceptFilter(IFilter filter) + { + var ret = Interceptors.Where(t => t is IFilterInterceptor) + .Cast() + .Aggregate(filter, (previousFilter, interceptor) => interceptor.InterceptFilter(previousFilter)); + + return ret; + } + + protected virtual void ApplyIncludeStrategyInterceptors() + { + CurrentQueryable = Interceptors + .Where(t => t is IIncludeStrategyInterceptor) + .Cast() + .Aggregate(CurrentQueryable, (prev, interceptor) => interceptor.InterceptIncludeStrategy(Criteria, prev)); + + CurrentQueryable = Interceptors + .Where(t => t is IIncludeStrategyInterceptor) + .Cast>() + .Aggregate((IQueryable)CurrentQueryable, (prev, interceptor) => interceptor.InterceptIncludeStrategy(Criteria, prev)); + } + + protected virtual void ApplyBeforeFilterInterceptors() + { + CurrentQueryable = Interceptors + .Where(t => t is IBeforeQueryFilterInterceptor) + .Cast() + .Aggregate(CurrentQueryable, (prev, interceptor) => interceptor.InterceptBeforeFiltering(Criteria, prev)); + + CurrentQueryable = Interceptors + .Where(t => t is IBeforeQueryFilterInterceptor) + .Cast>() + .Aggregate((IQueryable)CurrentQueryable, (prev, interceptor) => interceptor.InterceptBeforeFiltering(Criteria, prev)); + } + } +} diff --git a/PoweredSoft.DynamicQuery/QueryResult.cs b/PoweredSoft.DynamicQuery/QueryResult.cs new file mode 100644 index 0000000..729ff92 --- /dev/null +++ b/PoweredSoft.DynamicQuery/QueryResult.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using PoweredSoft.DynamicQuery.Core; + +namespace PoweredSoft.DynamicQuery +{ + /// + /// Represents an aggregate result. + /// + public class AggregateResult : IAggregateResult + { + public string Path { get; set; } + public AggregateType Type { get; set; } + public object Value { get; set; } + } + + // part of a result. + public abstract class QueryResult : IQueryResult + { + public List Aggregates { get; set; } + public List Data { get; set; } + + public bool ShouldSerializeAggregates() => Aggregates != null; + } + + // not grouped. + public class QueryExecutionResult : QueryResult, IQueryExecutionResult + { + public long TotalRecords { get; set; } + public long? NumberOfPages { get; set; } + } + + // grouped. + public class GroupQueryResult : QueryResult, IGroupQueryResult + { + public string GroupPath { get; set; } + public object GroupValue { get; set; } + + public IEnumerable GroupItems => Data.Cast(); + public bool ShouldSerializeGroupItems() => false; + } + + public class GroupedQueryExecutionResult : GroupQueryResult, IQueryExecutionResult + { + public long TotalRecords { get; set; } + public long? NumberOfPages { get; set; } + } +}