From ee7afe49d222e6803f8bb5a98b8dcdc4b5eac38f Mon Sep 17 00:00:00 2001 From: David Lebee Date: Thu, 15 Nov 2018 19:42:30 -0600 Subject: [PATCH] multi group fix. --- PoweredSoft.DynamicQuery.Cli/Program.cs | 6 +- PoweredSoft.DynamicQuery.Test/GroupTests.cs | 32 ++++++++ .../Mock/MockContext.cs | 1 + .../Mock/TestSeeders.cs | 43 +++++++++++ PoweredSoft.DynamicQuery.Test/Mock/Ticket.cs | 36 +++++++++ .../PoweredSoft.DynamicQuery.Test.csproj | 1 + PoweredSoft.DynamicQuery/QueryHandler.cs | 76 ++++++++++++++++++- 7 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 PoweredSoft.DynamicQuery.Test/Mock/Ticket.cs diff --git a/PoweredSoft.DynamicQuery.Cli/Program.cs b/PoweredSoft.DynamicQuery.Cli/Program.cs index f2a4694..c010b23 100644 --- a/PoweredSoft.DynamicQuery.Cli/Program.cs +++ b/PoweredSoft.DynamicQuery.Cli/Program.cs @@ -109,12 +109,10 @@ namespace PoweredSoft.DynamicQuery.Cli criteria.Groups = new List() { - new Group { Path = "LastName" } - //, new Group { Path = "Sexe" } + new Group { Path = "LastName" }, + new Group { Path = "Sexe" } }; - - criteria.Aggregates = new List() { new Aggregate { Type = AggregateType.Count }, diff --git a/PoweredSoft.DynamicQuery.Test/GroupTests.cs b/PoweredSoft.DynamicQuery.Test/GroupTests.cs index 919a364..3cbfd58 100644 --- a/PoweredSoft.DynamicQuery.Test/GroupTests.cs +++ b/PoweredSoft.DynamicQuery.Test/GroupTests.cs @@ -47,5 +47,37 @@ namespace PoweredSoft.DynamicQuery.Test } }); } + + [Fact] + public void GroupComplex() + { + MockContextFactory.SeedAndTestContextFor("GroupTests_Complex", TestSeeders.SeedTicketScenario, ctx => + { + var criteria = new QueryCriteria() + { + Groups = new List() + { + new Group { Path = "TicketType" }, + new Group { Path = "Priority" } + }, + Aggregates = new List() + { + new Aggregate { Type = AggregateType.Count } + } + }; + + var queryHandler = new QueryHandler(); + var result = queryHandler.Execute(ctx.Tickets, criteria); + + var firstGroup = result.Data[0] as IGroupQueryResult; + Assert.NotNull(firstGroup); + var secondGroup = result.Data[1] as IGroupQueryResult; + Assert.NotNull(secondGroup); + + var expected = ctx.Tickets.Select(t => t.TicketType).Distinct().Count(); + var c = result.Data.Cast().Select(t => t.GroupValue).Count(); + Assert.Equal(expected, c); + }); + } } } diff --git a/PoweredSoft.DynamicQuery.Test/Mock/MockContext.cs b/PoweredSoft.DynamicQuery.Test/Mock/MockContext.cs index 0edeb9b..172396c 100644 --- a/PoweredSoft.DynamicQuery.Test/Mock/MockContext.cs +++ b/PoweredSoft.DynamicQuery.Test/Mock/MockContext.cs @@ -11,6 +11,7 @@ namespace PoweredSoft.DynamicQuery.Test.Mock public virtual DbSet Items { get; set; } public virtual DbSet Orders { get; set; } public virtual DbSet OrderItems { get; set; } + public virtual DbSet Tickets { get; set; } public MockContext() { diff --git a/PoweredSoft.DynamicQuery.Test/Mock/TestSeeders.cs b/PoweredSoft.DynamicQuery.Test/Mock/TestSeeders.cs index 12c33d6..d7a52aa 100644 --- a/PoweredSoft.DynamicQuery.Test/Mock/TestSeeders.cs +++ b/PoweredSoft.DynamicQuery.Test/Mock/TestSeeders.cs @@ -92,5 +92,48 @@ namespace PoweredSoft.DynamicQuery.Test.Mock ctx.SaveChanges(); }); } + + internal static void SeedTicketScenario(string testName) + { + MockContextFactory.TestContextFor(testName, ctx => + { + var faker = new Bogus.Faker() + .RuleFor(t => t.TicketType, (f, u) => f.PickRandom("new", "open", "refused", "closed")) + .RuleFor(t => t.Title, (f, u) => f.Lorem.Sentence()) + .RuleFor(t => t.Details, (f, u) => f.Lorem.Paragraph()) + .RuleFor(t => t.IsHtml, (f, u) => false) + .RuleFor(t => t.TagList, (f, u) => string.Join(",", f.Commerce.Categories(3))) + .RuleFor(t => t.CreatedDate, (f, u) => f.Date.Recent(100)) + .RuleFor(t => t.Owner, (f, u) => f.Person.FullName) + .RuleFor(t => t.AssignedTo, (f, u) => f.Person.FullName) + .RuleFor(t => t.TicketStatus, (f, u) => f.PickRandom(1, 2, 3)) + .RuleFor(t => t.LastUpdateBy, (f, u) => f.Person.FullName) + .RuleFor(t => t.LastUpdateDate, (f, u) => f.Date.Soon(5)) + .RuleFor(t => t.Priority, (f, u) => f.PickRandom("low", "medium", "high", "critical")) + .RuleFor(t => t.AffectedCustomer, (f, u) => f.PickRandom(true, false)) + .RuleFor(t => t.Version, (f, u) => f.PickRandom("1.0.0", "1.1.0", "2.0.0")) + .RuleFor(t => t.ProjectId, (f, u) => f.Random.Number(100)) + .RuleFor(t => t.DueDate, (f, u) => f.Date.Soon(5)) + .RuleFor(t => t.EstimatedDuration, (f, u) => f.Random.Number(20)) + .RuleFor(t => t.ActualDuration, (f, u) => f.Random.Number(20)) + .RuleFor(t => t.TargetDate, (f, u) => f.Date.Soon(5)) + .RuleFor(t => t.ResolutionDate, (f, u) => f.Date.Soon(5)) + .RuleFor(t => t.Type, (f, u) => f.PickRandom(1, 2, 3)) + .RuleFor(t => t.ParentId, () => 0) + .RuleFor(t => t.PreferredLanguage, (f, u) => f.PickRandom("fr", "en", "es")) + ; + + var fakeModels = new List(); + for (var i = 0; i < 500; i++) + { + var t = faker.Generate(); + t.TicketId = i + 1; + fakeModels.Add(t); + } + + ctx.AddRange(fakeModels); + ctx.SaveChanges(); + }); + } } } diff --git a/PoweredSoft.DynamicQuery.Test/Mock/Ticket.cs b/PoweredSoft.DynamicQuery.Test/Mock/Ticket.cs new file mode 100644 index 0000000..e46c019 --- /dev/null +++ b/PoweredSoft.DynamicQuery.Test/Mock/Ticket.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace PoweredSoft.DynamicQuery.Test.Mock +{ + public class Ticket + { + public int TicketId { get; set; } + public string TicketType { get; set; } + public string Title { get; set; } + public string Details { get; set; } + public bool IsHtml { get; set; } + public string TagList { get; set; } + public DateTimeOffset CreatedDate { get; set; } + public string Owner { get; set; } + public string AssignedTo { get; set; } + public int TicketStatus { get; set; } + public DateTimeOffset CurrentStatusDate { get; set; } + public string CurrentStatusSetBy { get; set; } + public string LastUpdateBy { get; set; } + public DateTimeOffset LastUpdateDate { get; set; } + public string Priority { get; set; } + public bool AffectedCustomer { get; set; } + public string Version { get; set; } + public int ProjectId { get; set; } + public DateTimeOffset DueDate { get; set; } + public decimal EstimatedDuration { get; set; } + public decimal ActualDuration { get; set; } + public DateTimeOffset TargetDate { get; set; } + public DateTimeOffset ResolutionDate { get; set; } + public int Type { get; set; } + public int ParentId { get; set; } + public string PreferredLanguage { get; set; } + } +} diff --git a/PoweredSoft.DynamicQuery.Test/PoweredSoft.DynamicQuery.Test.csproj b/PoweredSoft.DynamicQuery.Test/PoweredSoft.DynamicQuery.Test.csproj index 84570c8..96b2f4d 100644 --- a/PoweredSoft.DynamicQuery.Test/PoweredSoft.DynamicQuery.Test.csproj +++ b/PoweredSoft.DynamicQuery.Test/PoweredSoft.DynamicQuery.Test.csproj @@ -7,6 +7,7 @@ + diff --git a/PoweredSoft.DynamicQuery/QueryHandler.cs b/PoweredSoft.DynamicQuery/QueryHandler.cs index eab2afe..57393d8 100644 --- a/PoweredSoft.DynamicQuery/QueryHandler.cs +++ b/PoweredSoft.DynamicQuery/QueryHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; @@ -53,8 +54,14 @@ namespace PoweredSoft.DynamicQuery sb.ToList("Records"); }); + // loop through the grouped records. var groupRecords = CurrentQueryable.ToDynamicClassList(); + + // now join them into logical collections + result.Data = RecursiveRegroup(groupRecords, aggregateResults, Criteria.Groups.First()); + +/* result.Data = groupRecords.Select((groupRecord, groupRecordIndex) => { var groupRecordResult = new GroupQueryResult(); @@ -105,12 +112,73 @@ namespace PoweredSoft.DynamicQuery }); return (object)groupRecordResult; - }).ToList(); + }).ToList();*/ result.Aggregates = CalculateTotalAggregate(queryableAfterFilters); return result; } + protected virtual List RecursiveRegroup(List groupRecords, List> aggregateResults, IGroup group, List parentGroupResults = null) + { + var groupIndex = Criteria.Groups.IndexOf(group); + var isLast = Criteria.Groups.Last() == group; + var groups = Criteria.Groups.Take(groupIndex + 1).ToList(); + var hasAggregates = Criteria.Aggregates.Any(); + + var ret = groupRecords + .GroupBy(gk => gk.GetDynamicPropertyValue($"Key_{groupIndex}")) + .Select(t => + { + var groupResult = new GroupQueryResult(); + + // group results. + + List groupResults; + if (parentGroupResults == null) + groupResults = new List { groupResult }; + else + groupResults = parentGroupResults.Union(new[] { groupResult }).ToList(); + + groupResult.GroupPath = group.Path; + groupResult.GroupValue = t.Key; + + if (hasAggregates) + { + var matchingAggregate = FindMatchingAggregateResult(aggregateResults, groups, groupResults); + if (matchingAggregate == null) + Debugger.Break(); + + groupResult.Aggregates = new List(); + Criteria.Aggregates.ForEach((a, ai) => + { + var key = $"Agg_{ai}"; + var aggregateResult = new AggregateResult + { + Path = a.Path, + Type = a.Type, + Value = matchingAggregate.GetDynamicPropertyValue(key) + }; + groupResult.Aggregates.Add(aggregateResult); + }); + } + + if (isLast) + { + var entities = t.SelectMany(t2 => t2.GetDynamicPropertyValue>("Records")).ToList(); + groupResult.Data = InterceptConvertTo(entities); + } + else + { + groupResult.Data = RecursiveRegroup(t.ToList(), aggregateResults, Criteria.Groups[groupIndex+1], groupResults); + } + + return groupResult; + }) + .AsEnumerable() + .ToList(); + return ret; + } + protected virtual List CalculateTotalAggregate(IQueryable queryableAfterFilters) { if (!Criteria.Aggregates.Any()) @@ -127,7 +195,7 @@ namespace PoweredSoft.DynamicQuery }); }); - var aggregateResult = selectExpression.ToDynamicClassList().First(); + var aggregateResult = selectExpression.ToDynamicClassList().FirstOrDefault(); var ret = new List(); Criteria.Aggregates.ForEach((a, index) => { @@ -135,13 +203,13 @@ namespace PoweredSoft.DynamicQuery { Path = a.Path, Type = a.Type, - Value = aggregateResult.GetDynamicPropertyValue($"Agg_{index}") + Value = aggregateResult?.GetDynamicPropertyValue($"Agg_{index}") }); }); return ret; } - private DynamicClass FindMatchingAggregateResult(List> aggregateResults, List groups, List groupResults) + private DynamicClass FindMatchingAggregateResult(List> aggregateResults, List groups, List groupResults) { var groupIndex = groupResults.Count - 1; var aggregateLevel = aggregateResults[groupIndex];