예제 #1
0
        public static RulEngStore ProcessAllOperationsReducer(RulEngStore previousState, IOpReqProcessing prescription)
        {
            // Set up a temporary 'Processing' copy of the Store as our Unit of Work
            var newState = previousState.DeepClone();

            newState = newState.AllOperations(previousState, prescription);

            return(newState.DeepClone());
        }
예제 #2
0
        public static RulEngStore DeepClone(this ProcessingRulEngStore rvStore)
        {
            var newStore = new RulEngStore
            {
                Rules       = rvStore.Rules.ToImmutableHashSet(),
                RuleResults = rvStore.RuleResults.ToImmutableHashSet(),
                Operations  = rvStore.Operations.ToImmutableHashSet(),
                Requests    = rvStore.Requests.ToImmutableHashSet(),
                Values      = rvStore.Values.ToImmutableHashSet()
            };

            return(newStore);
        }
예제 #3
0
        public static ProcessingRulEngStore DeepClone(this RulEngStore rvStore)
        {
            var newStore = new ProcessingRulEngStore
            {
                Rules       = rvStore?.Rules.ToHashSet(),
                RuleResults = rvStore?.RuleResults.ToHashSet(),
                Operations  = rvStore?.Operations.ToHashSet(),
                Requests    = rvStore?.Requests.ToHashSet(),
                Values      = rvStore?.Values.ToHashSet()
            };

            return(newStore);
        }
예제 #4
0
        public static RulEngStore ProcessAllRulesReducer(RulEngStore previousState, IRuleProcessing prescription)
        {
            // Set up a temporary 'Processing' copy of the Store as our Unit of Work
            var newState = previousState.DeepClone();

            // First identify rules for entities that may (not) exist
            newState = newState.AllExists(prescription as ProcessExistsRule);
            // Then test for meaningful Values
            newState = newState.AllHasMeaningfulValue(prescription as ProcessHasMeaningfulValueRule);

            newState = newState.AllCompare(prescription as ProcessLessThanRule, RuleType.LessThan);
            newState = newState.AllCompare(prescription as ProcessEqualRule, RuleType.Equal);
            newState = newState.AllCompare(prescription as ProcessGreaterThanRule, RuleType.GreaterThan);
            newState = newState.AllCompare(prescription as ProcessRegexMatchRule, RuleType.RegularExpression);

            newState = newState.AllCollection(prescription as ProcessAndRule, RuleType.And);
            newState = newState.AllCollection(prescription as ProcessOrRule, RuleType.Or);
            newState = newState.AllCollection(prescription as ProcessXorRule, RuleType.Xor);

            return(newState.DeepClone());
        }
예제 #5
0
        public static RulEngStore ReduceStore(RulEngStore state, IAction action)
        {
            if (state == null)
            {
                state = new RulEngStore();
            }

            // This is where CQRS like behaviour hits
            // Actions that only target one of the 'tables' (e.g. CRUD) are passed the specific item impacted
            // Actions that target multiple 'tables' (e.g. Processing) are passed the whole structure

            if (action is ICrud crud)
            {
                return(new RulEngStore
                {
                    Rules = CrudReducers.CrudReducer(state.Rules, crud),
                    Operations = CrudReducers.CrudReducer(state.Operations, crud),
                    Requests = CrudReducers.CrudReducer(state.Requests, crud),
                    Values = CrudReducers.CrudReducer(state.Values, crud)
                });
            }

            if (!(action is IProcessing))
            {
                return(state);
            }

            switch (action)
            {
            case IRuleProcessing rulesAction:
                return(ProcessingReducers.ProcessAllRulesReducer(state, rulesAction));

            case IOpReqProcessing opReqAction:
                return(ProcessingReducers.ProcessAllOperationsReducer(state, opReqAction));
            }

            return(state);
        }
예제 #6
0
        private static ProcessingRulEngStore AllOperations(this ProcessingRulEngStore newState, RulEngStore previousState, IOpReqProcessing prescription)
        {
            // First identify RuleResults that have just been processed successfully
            var ruleResultIds = newState.RuleResults
                                .Where(v => v.Detail)
                                .Select(v => v.RuleResultId)
                                .ToList();

            var opType = prescription is OperationMxProcessing
                ? OperationType.CreateUpdate
                : prescription is OperationDxProcessing
                    ? OperationType.Delete
                    : prescription is OperationSxProcessing
                        ? OperationType.Search
                        : OperationType.Unknown; // When the prescription is for requests

            // Find all relevant operations/requests for the identified rule results and then filter down by execution date
            var possibleOps = newState.Operations
                              .Where(a => ruleResultIds.Contains(a.RuleResultId) && a.OperationType == opType).ToList();
            var operationprescriptionsToProcessList = (
                from op in possibleOps
                let rr = newState.RuleResults.First(r => r.RuleResultId == op.RuleResultId)
                         where rr.LastChanged > op.LastExecuted
                         select op)
                                                      .ToList();
            var requestprescriptionsToProcessList = (
                from rq in newState.Requests.Where(a => ruleResultIds.Contains(a.RuleResultId))
                let rr = newState.RuleResults.First(r => r.RuleResultId == rq.RuleResultId)
                         where rr.LastChanged > rq.LastExecuted
                         select rq)
                                                    .ToList();

            // Restrict the Operation/Request Prescriptions to process to those that are guaranteed not to fail
            // Select those for which there is no conflict
            var destinationEntities = new List <TypeKey>();

            foreach (var opPre in operationprescriptionsToProcessList)
            {
                destinationEntities.AddRange(opPre.Operands.Select(o => (TypeKey)o));
            }
            foreach (var rqPre in requestprescriptionsToProcessList)
            {
                destinationEntities.Add(rqPre);
            }

            var groupedDestinations = destinationEntities
                                      .GroupBy(de => new EntMatch {
                EntityId = de.EntityId, EntType = de.EntType
            })
                                      .Select(grp => new { grp.Key, Count = grp.Count() })
                                      .ToList();
            //var conflictDestinations = groupedDestinations
            //    .Where(grp => grp.Count > 1)
            //    .Select(grp => new TypeKey { EntityId = grp.Key.EntityId, EntTags = grp.Key.EntTags, EntType = grp.Key.EntType })
            //    .ToList();
            var acceptableDestinations = groupedDestinations
                                         .Where(grp => grp.Count == 1)
                                         .Select(grp => grp.Key)
                                         .ToList();

            if (prescription is OperationMxProcessing)
            {
                newState = OperationMxProcessing(newState, previousState, ruleResultIds, operationprescriptionsToProcessList, acceptableDestinations);
            }
            if (prescription is OperationDxProcessing)
            {
                newState = OperationDxProcessing(newState, ruleResultIds, operationprescriptionsToProcessList, acceptableDestinations);
            }
            if (prescription is OperationSxProcessing)
            {
                newState = OperationSxProcessing(newState, previousState, ruleResultIds, operationprescriptionsToProcessList, acceptableDestinations);
            }

            return(newState);
        }
예제 #7
0
        private static ProcessingRulEngStore OperationSxProcessing(this ProcessingRulEngStore newState, RulEngStore previousState,
                                                                   List <Guid> ruleResultIds, List <Operation> operationprescriptionsToProcessList,
                                                                   List <EntMatch> acceptableDestinations)
        {
            // Get all of the sources from the previous state
            var acceptableSourceIds = new List <Guid>();
            //foreach (var opPresProc in operationprescriptionsToProcessList)
            //{
            //    var opPresOperands = opPresProc.Operands;

            //    var matchFound = false;
            //    foreach (var opo in opPresOperands)
            //    {
            //        if (!acceptableDestinations
            //            .Any(ad => ad.EntType == opo.EntType && (ad.EntityId == opo.EntityId || opo.EntityId == Guid.Empty)))
            //        {
            //            continue;
            //        }

            //        matchFound = true;
            //        break;
            //    }

            //    if (!matchFound)
            //    {
            //        continue;
            //    }

            //    acceptableSourceIds.AddRange(opPresOperands.SelectMany(oo => oo.SourceValueIds));
            //}

            //var acceptableSources = previousState.Values
            //    .Where(v => acceptableSourceIds.Contains(v.EntityId))
            //    .ToList();

            var e = new Engine();

            foreach (var ruleResultIdToProcess in ruleResultIds)
            {
                // Get all of the operations relevant to the Rule
                var relevantOps = operationprescriptionsToProcessList
                                  .Where(o => o.RuleResultId == ruleResultIdToProcess)
                                  .ToList();

                if (!relevantOps.Any())
                {
                    // TODO: confirm if we should be doing this if there was nothing relevant to process
                    //newState.RuleResults.RemoveWhere(r => r.RuleResultId == ruleResultIdToProcess);
                    continue;
                }

                // Process the acceptable
                foreach (var relevantOp in relevantOps)
                {
                    // Note: A Search operation does not specify an output destination as it does not 'know' in advance how many results will be found
                    // However, it may specify a reduced set of Ids to search through.
                    // These will be provided in the SourceValueIds field of each relevantOp.Operand

                    //        var firstEnt = new EntMatch
                    //        {
                    //            EntityId = relevantOp.Operands[0].EntityId,
                    //            EntType = relevantOp.Operands[0].EntType
                    //        };

                    //        if (!acceptableDestinations.Any(ad =>
                    //            ad.EntType == firstEnt.EntType && ad.EntityId == firstEnt.EntityId))
                    //        {
                    //            continue;
                    //        }

                    //        var destEntsToProcess = relevantOp.Operands
                    //            .Select(de => new
                    //            {
                    //                de.EntityId,
                    //                EntType = Convert.ToInt32(de.EntType),
                    //                sourceValues = de.SourceValueIds
                    //                    .Select(sv => JObject.Parse($"{{\"Id\":\"{sv}\",\"Value\":{acceptableSources.FirstOrDefault(a => a.EntityId == sv)?.Detail.ToString(Formatting.None)}}}"))
                    //                    .ToArray()
                    //            })
                    //            .ToList();

                    switch (relevantOp.Operands[0].SourceEntType)
                    {
                    case EntityType.Rule:
                        e.SetValue("source", JsonConvert.SerializeObject(previousState.Rules.ToArray()));
                        break;

                    case EntityType.RuleResult:
                        e.SetValue("source", JsonConvert.SerializeObject(previousState.RuleResults.ToArray()));
                        break;

                    case EntityType.Operation:
                        e.SetValue("source", JsonConvert.SerializeObject(previousState.Operations.ToArray()));
                        break;

                    case EntityType.Request:
                        e.SetValue("source", JsonConvert.SerializeObject(previousState.Requests.ToArray()));
                        break;

                    case EntityType.Value:
                        e.SetValue("source", JsonConvert.SerializeObject(previousState.Values.ToArray()));
                        break;
                    }

                    // The result must be a list of guids for the entities that met the search criteria
                    var result = e
                                 .Execute(relevantOp.OperationTemplate)
                                 .GetCompletionValue();

                    var sourceGuids = result.ToString()
                                      .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                                      .Select(Guid.Parse)
                                      .ToList();

                    for (var ix = 0; ix < sourceGuids.Count; ix++)
                    {
                        var sourceEnt = new TypeKey
                        {
                            EntityId = sourceGuids[ix],
                            EntType  = relevantOp.Operands[0].SourceEntType,
                            EntTags  = relevantOp.EntTags
                        } as IEntity;
                        switch (relevantOp.Operands[0].EntType)
                        {
                        case EntityType.Rule:
                            // Create/Update a rule using destEnt.EntityId and result
                            var rl = newState.FromSearchOperationAddUpdateExistsRule(sourceEnt, relevantOp.EntTags, GuidHelpers.NewTimeUuid());
                            var rr = new RuleResult(rl);
                            newState.RuleResults.Add(rr);
                            break;

                        case EntityType.Operation:
                            // Create/Update an Operation using destEnt.EntityId and result
                            var op = newState.FromSearchOperationAddUpdateOperation(sourceEnt, relevantOp.EntTags, OperationType.Delete, "", GuidHelpers.NewTimeUuid());
                            //var rr = new RuleResult(rl);
                            //newState.RuleResults.Add(rr);

                            //newState.FromOperationResultAddUpdateOperation(result, destEnt.EntityId);
                            break;
                            //case EntityType.Request:
                            //    // Create/Update a Request using destEnt.EntityId and result
                            //    newState.FromOperationResultAddUpdateRequest(result, destEnt.EntityId);
                            //    break;
                            //case EntityType.Value:
                            //    // Create/Update a Value using destEnt.EntityId and result
                            //    newState.FromOperationResultAddUpdateValue(result, destEnt.EntityId);
                            //    break;
                            //            }
                        }

                        //var values = new List<string>();
                        //for (var i = 0; i < a.GetLength(); i++)
                        //{
                        //    values.Add(a.Get(i.ToString()).AsString());
                        //}
                        //return values;

                        Console.WriteLine(JsonConvert.SerializeObject(result));



                        //        foreach (var destEnt in destEntsToProcess)
                        //        {
                        //            var sourceVals = destEnt.sourceValues;
                        //            var isSubstOk = true;

                        //            foreach (Match match in regexToken.Matches(jTempl))
                        //            {
                        //                var token = match.Groups["Token"].Value;
                        //                var indexOk = int.TryParse(match.Groups["Index"].Value, out var index);

                        //                if (!indexOk)
                        //                {
                        //                    break;
                        //                }

                        //                if (sourceVals.Length < index)
                        //                {
                        //                    isSubstOk = false;
                        //                    break;
                        //                }

                        //                jCode = jCode.Replace(token, sourceVals[index]["Value"].ToString(Formatting.None));
                        //            }

                        //            if (!isSubstOk)
                        //            {
                        //                Console.WriteLine(jCode);
                        //                continue;
                        //            }

                        //            JToken result = null;
                        //            if (jCode.StartsWith("{"))
                        //            {
                        //                result = JObject.FromObject(e.Execute(jCode).GetCompletionValue().ToObject());
                        //            }
                        //            if (jCode.StartsWith("["))
                        //            {
                        //                result = JArray.FromObject(e.Execute(jCode).GetCompletionValue().ToObject());
                        //            }
                        //            //Console.WriteLine(result);
                        //            switch ((EntityType)destEnt.EntType)
                        //            {
                        //                case EntityType.Rule:
                        //                    // Create/Update a rule using destEnt.EntityId and result
                        //                    newState.FromOperationResultAddUpdateRule(result, destEnt.EntityId);
                        //                    break;
                        //                case EntityType.Operation:
                        //                    // Create/Update an Operation using destEnt.EntityId and result
                        //                    newState.FromOperationResultAddUpdateOperation(result, destEnt.EntityId);
                        //                    break;
                        //                case EntityType.Request:
                        //                    // Create/Update a Request using destEnt.EntityId and result
                        //                    newState.FromOperationResultAddUpdateRequest(result, destEnt.EntityId);
                        //                    break;
                        //                case EntityType.Value:
                        //                    // Create/Update a Value using destEnt.EntityId and result
                        //                    newState.FromOperationResultAddUpdateValue(result, destEnt.EntityId);
                        //                    break;
                        //            }

                        //            // Mark the operation as Executed
                        //            var actionDate = DateTime.UtcNow;

                        //            // Mark this Rule as executed
                        //            relevantOp.LastExecuted = actionDate;
                        //        }
                    }

                    // newState.RuleResults.RemoveWhere(r => r.RuleResultId == ruleResultIdToProcess);
                }
            }

            return(newState);
        }
예제 #8
0
        private static ProcessingRulEngStore OperationMxProcessing(this ProcessingRulEngStore newState, RulEngStore previousState,
                                                                   List <Guid> ruleResultIds, List <Operation> operationprescriptionsToProcessList,
                                                                   List <EntMatch> acceptableDestinations)
        {
            // Get all of the sources from the previous state
            var acceptableSourceIds = new List <Guid>();

            foreach (var opPresProc in operationprescriptionsToProcessList)
            {
                var opPresOperands = opPresProc.Operands;

                var matchFound = false;
                foreach (var opo in opPresOperands)
                {
                    if (!acceptableDestinations
                        .Any(ad => ad.EntType == opo.EntType && ad.EntityId == opo.EntityId))
                    {
                        continue;
                    }

                    matchFound = true;
                    break;
                }

                if (!matchFound)
                {
                    continue;
                }

                acceptableSourceIds.AddRange(opPresOperands.SelectMany(oo => oo.SourceEntityIds));
            }

            var acceptableSources = previousState.Values
                                    .Where(v => acceptableSourceIds.Contains(v.EntityId))
                                    .ToList();

            var e = new Engine();

            var regexToken = new Regex(@".*?(?<Token>\$\{(?<Index>\d+)\}).*?");

            foreach (var ruleResultIdToProcess in ruleResultIds)
            {
                // Get all of the operations relevant to the Rule
                var relevantOps = operationprescriptionsToProcessList
                                  .Where(o => o.RuleResultId == ruleResultIdToProcess)
                                  .ToList();

                if (!relevantOps.Any())
                {
                    // TODO: confirm if we should be doing this if there was nothing relevant to process
                    //newState.RuleResults.RemoveWhere(r => r.RuleResultId == ruleResultIdToProcess);
                    continue;
                }

                // Process the acceptable
                foreach (var relevantOp in relevantOps)
                {
                    // Ensure the first entity in the operation is an acceptable destination
                    EntMatch firstEnt = relevantOp.Operands[0];

                    if (!acceptableDestinations.Any(ad =>
                                                    ad.EntType == firstEnt.EntType && ad.EntityId == firstEnt.EntityId))
                    {
                        continue;
                    }

                    var destEntsToProcess = relevantOp.Operands
                                            .Select(de => new
                    {
                        de.EntityId,
                        EntType      = Convert.ToInt32(de.EntType),
                        sourceValues = de.SourceEntityIds
                                       .Select(sv => JObject.Parse($"{{\"Id\":\"{sv}\",\"Value\":{acceptableSources.FirstOrDefault(a => a.EntityId == sv)?.Detail.ToString(Formatting.None)}}}"))
                                       .ToArray()
                    })
                                            .ToList();

                    var jTempl = relevantOp.OperationTemplate;
                    var jCode  = jTempl;
                    foreach (var destEnt in destEntsToProcess)
                    {
                        var sourceVals = destEnt.sourceValues;
                        var isSubstOk  = true;

                        foreach (Match match in regexToken.Matches(jTempl))
                        {
                            var token   = match.Groups["Token"].Value;
                            var indexOk = int.TryParse(match.Groups["Index"].Value, out var index);

                            if (!indexOk)
                            {
                                break;
                            }

                            if (sourceVals.Length < index)
                            {
                                isSubstOk = false;
                                break;
                            }

                            jCode = jCode.Replace(token, sourceVals[index]["Value"].ToString(Formatting.None));
                        }

                        if (!isSubstOk)
                        {
                            Console.WriteLine(jCode);
                            continue;
                        }

                        JToken result = null;
                        if (jCode.StartsWith("{"))
                        {
                            result = JObject.FromObject(e.Execute(jCode).GetCompletionValue().ToObject());
                        }
                        if (jCode.StartsWith("["))
                        {
                            result = JArray.FromObject(e.Execute(jCode).GetCompletionValue().ToObject());
                        }
                        //Console.WriteLine(result);
                        switch ((EntityType)destEnt.EntType)
                        {
                        case EntityType.Rule:
                            // Create/Update a rule using destEnt.EntityId and result
                            newState.FromOperationResultAddUpdateRule(result, destEnt.EntityId);
                            break;

                        case EntityType.Operation:
                            // Create/Update an Operation using destEnt.EntityId and result
                            newState.FromOperationResultAddUpdateOperation(result, destEnt.EntityId);
                            break;

                        case EntityType.Request:
                            // Create/Update a Request using destEnt.EntityId and result
                            newState.FromOperationResultAddUpdateRequest(result, destEnt.EntityId);
                            break;

                        case EntityType.Value:
                            // Create/Update a Value using destEnt.EntityId and result
                            newState.FromOperationResultAddUpdateValue(result, destEnt.EntityId);
                            break;
                        }

                        // Mark the operation as Executed
                        var actionDate = DateTime.UtcNow;

                        // Mark this Rule as executed
                        relevantOp.LastExecuted = actionDate;
                    }
                }

                // newState.RuleResults.RemoveWhere(r => r.RuleResultId == ruleResultIdToProcess);
            }

            return(newState);
        }
예제 #9
0
        public static void Main()
        {
            Console.WriteLine("Hello Salesman!");

            // Travelling Salesman - Setup
            const int cityCount = 50;

            Console.WriteLine($"Start Setup for {cityCount} cities : {DateTime.UtcNow.ToString("yyyy-MMM-dd HH:mm:ss.ff")}");

            (var rules, var ruleResults, var values, var rulePrescriptions) = BuildTheCities(cityCount);

            // Build a Collection Rule, Result and Prescription for all of the above rules
            (var collectRule, var collectRuleResult, var collectRulePrescription) = ruleResults.And();

            // Add the Collection Rule, Result and Prescription
            rules.Add(collectRule);
            ruleResults.Add(collectRuleResult);
            rulePrescriptions.Add(collectRulePrescription);

            var operations             = new List <Operation>();
            var operationPrescriptions = new List <OperationMxProcessing>();

            var cityValues = values.Where(c => c.Detail["properties"]["cityNo"] != null).ToList();

            (var pointOperations, var pointOperationPrescriptions) = BuildTheGeoJsonOutput(collectRuleResult, cityValues);
            operations.Add(pointOperations);
            operationPrescriptions.Add(pointOperationPrescriptions);

            (var distOperations, var distOperationPrescriptions) = BuildTheCityDistances(collectRuleResult, cityValues);
            operations.AddRange(distOperations);
            operationPrescriptions.AddRange(distOperationPrescriptions);

            // Build the Rule Engine Store ready for processing
            var startingStore = new RulEngStore
            {
                Rules       = rules.ToImmutableHashSet(),
                RuleResults = ruleResults.ToImmutableHashSet(),
                Operations  = operations.ToImmutableHashSet(),
                Values      = values.ToImmutableHashSet()
            };

            RvStore = new Store <RulEngStore>(StoreReducers.ReduceStore, startingStore);
            File.WriteAllText("storeStart.json", RvStore.GetState().ToString());

            // Commence Processing
            RulEngStore changes;

            RvStore.Subscribe(state => changes = state);

            var pass = 0;

            TikTok(pass++, rulePrescriptions, operationPrescriptions);

            Console.WriteLine($"Add more to Store for {cityCount} cities : {DateTime.UtcNow.ToString("yyyy-MMM-dd HH:mm:ss.ff")}");
            // Build the exists rules for the values resulting from the distance operations
            (var distRules, var distRuleResults, var distRulePrescriptions) =
                distOperations.SelectMany(dp => dp.Operands).Exists(false);

            // Build the operations to convert the distance results to roads
            var storeValues = RvStore.GetState().Values.ToList();

            (var roadOperations, var roadOperationPrescriptions) = BuildTheCityRoads(distRules, storeValues);

            // Add the new Entities to the Store ready for processing
            RvStore.AddUpdate(distRules, distRuleResults, roadOperations, null);
            TikTok(pass++, distRulePrescriptions, roadOperationPrescriptions);

            Console.WriteLine($"Add more to Store for {cityCount} cities : {DateTime.UtcNow.ToString("yyyy-MMM-dd HH:mm:ss.ff")}");
            // Build the exists rules for the values resulting from the 'road' operations
            (var roadExistsRules, var roadExistsRuleResults, var roadExistsRulePrescriptions) =
                roadOperations.SelectMany(rp => rp.Operands).Exists(false);
            // TODO: Build operations to mark the roads between CityA and CityB (and vice versa) as 'accepted'
            // Build a Collection Rule, Result and Prescription for all of the 'road' rules
            (var collectRoadRule, var collectRoadRuleResult, var collectRoadRulePrescription) = roadExistsRuleResults.And();
            roadExistsRules.Add(collectRoadRule);
            roadExistsRuleResults.Add(collectRoadRuleResult);
            roadExistsRulePrescriptions.Add(collectRoadRulePrescription);

            // Join the new Roads together in one map
            var roadValues = RvStore.GetState().Values
                             .Where(c => c.Detail != null && c.Detail.Type == JTokenType.Object && c.Detail["properties"]?["roadId"] != null)
                             .ToList();

            (var lineOperation, var lineOperationPrescription) = BuildTheGeoJsonOutput(collectRoadRuleResult, roadValues);

            // Add the new Entities to the Store ready for processing
            RvStore
            .AddUpdate(roadExistsRules, roadExistsRuleResults, new[] { lineOperation }.ToList(), null);

            TikTok(pass++, roadExistsRulePrescriptions, new[] { lineOperationPrescription });

            // Searching or finding entities by arbitrary requirements is not currently supported
            // It could potentially be implemented with:
            // a new Rule (e.g. AnyMatch) that triggers
            // a special Operation (Match -> Generates Exists Rules as simple triggers)
            // regular Operation(s) to process the matching entities

            // So search for duplicate roads (A->B and B->A) by code external to the RuleEngine store
            //(var dupRoadExistsRules, var dupRoadExistsRuleResults, var dupRoadDeleteOperations,
            //        var dupRoadRulePrescriptions, var dupRoadOpPrescriptions) =
            //    DeleteTheDuplicateRoads(roadValues, roadExistsRules);

            //RvStore.AddUpdate(dupRoadExistsRules, dupRoadExistsRuleResults, dupRoadDeleteOperations, null);

            //TikTok(pass++, dupRoadRulePrescriptions, dupRoadOpPrescriptions);

            // Refresh the list of roads
            roadValues = RvStore.GetState().Values
                         .Where(c => c.Detail != null && c.Detail.Type == JTokenType.Object && c.Detail["properties"]?["roadId"] != null)
                         .ToList();
            roadExistsRules = RvStore.GetState().Rules
                              .Where(c => c.RuleType == RuleType.Exists &&
                                     roadValues.Select(v => v.ValueId).Contains(c.ReferenceValues.EntityIds[0].EntityId))
                              .ToList();
            roadExistsRuleResults = RvStore.GetState().RuleResults
                                    .Where(s => roadExistsRules.Select(c => c.RuleId).Contains(s.RuleId))
                                    .ToList();

            // recreate the prescriptions for road rules
            roadExistsRulePrescriptions = new List <IRuleProcessing>();
            foreach (var rv in roadExistsRules)
            {
                roadExistsRulePrescriptions.Add(rv.Exists());
            }
            // Rebuild the Collection Rule, Result and Prescription for all of the 'road' rules
            (collectRoadRule, collectRoadRuleResult, collectRoadRulePrescription) = roadExistsRuleResults.And(collectRoadRule, collectRoadRuleResult);
            roadExistsRules.Add(collectRoadRule);
            roadExistsRuleResults.Add(collectRoadRuleResult);
            roadExistsRulePrescriptions.Add(collectRoadRulePrescription);

            // Join the new Roads together in one map
            (lineOperation, lineOperationPrescription) = BuildTheGeoJsonOutput(collectRoadRuleResult, roadValues, lineOperation);

            // Add the new Entities to the Store ready for processing
            RvStore
            .AddUpdate(roadExistsRules, roadExistsRuleResults, new[] { lineOperation }.ToList(), null);

            TikTok(pass++, roadExistsRulePrescriptions, new[] { lineOperationPrescription });

            // Find the Duplicate roads to be deleted
            // First create a Search Operation to generate Exists Rules and RuleResults
            var opKeyDups = new OperandKey
            {
                EntityId = GuidHelpers.NewTimeUuid(),
                EntTags  = new List <string> {
                    "Duplicates"
                },
                EntType       = EntityType.Rule,
                SourceEntType = EntityType.Value
            };

            // The source data is always presented as a serialised JSON string
            var searchTemplate = "JSON.parse(source)"
                                 // Filter for values with a roadId property
                                 + ".filter(function(s){return s.Detail&&s.Detail.properties&&s.Detail.properties.roadId})"
                                 // Map the results to just the values Id and the roadId (a hash)
                                 + ".map(function(s){return {vId:s.ValueId,rId:s.Detail.properties.roadId};})"
                                 // Reduce to an array grouping by the roadId, the first valueId with that roadId will also be in the structure
                                 + ".reduce(function(a,c){var ix=0;"
                                 + "for(;ix<a.length;ix++){if(a[ix].el.rId===c.rId)break;}"
                                 + "if(ix<a.length){a[ix].t++;}else{a.push({el:c,t:1});}"
                                 + "return a;},[])"
                                 // Filter for roadIds that occur more than once
                                 + ".filter(function(s){return s.t>1})"
                                 // Get the valueId
                                 + ".map(function(s){return s.el.vId})"
                                 // Sort (makes it easier to follow what's going on)
                                 + ".sort(function(a,b){if(a<b)return -1;return(a>b)?1:0;})";

            var opRoadSearch             = collectRuleResult.SearchOperation(new[] { opKeyDups }, GuidHelpers.NewTimeUuid(), searchTemplate);
            var opRoadSearchPrescription = opRoadSearch.Search();

            RvStore.AddUpdate(null, null, opRoadSearch, null);

            TikTok(pass++, null, new[] { opRoadSearchPrescription });

            // Generate Prescriptions for all of the Rules and execute them
            var searchForDupPrescriptions = RvStore.GetState()
                                            .Rules
                                            .Where(r => r.EntTags != null && r.EntTags[0] == "Duplicates")
                                            .Select(r => r.Exists())
                                            .ToList();

            TikTok(pass++, searchForDupPrescriptions, null);

            // Grab one of the Rule Results from above that was successful and use it to trigger the next operation
            var dupRuleResults    = searchForDupPrescriptions.Select(fdp => fdp.Entities.RuleResultId).ToList();
            var dupRoadRuleResult = RvStore.GetState()
                                    .RuleResults
                                    .FirstOrDefault(r => r.EntTags != null && r.EntTags[0] == "Duplicates" && dupRuleResults.Contains(r.RuleResultId) && r.Detail);

            var opKeyDels = new OperandKey
            {
                EntityId = GuidHelpers.NewTimeUuid(),
                EntTags  = new List <string> {
                    "Duplicates"
                },
                EntType       = EntityType.Operation,
                SourceEntType = EntityType.RuleResult
            };

            // The source data is always presented as a serialised JSON string
            var searchDupsTemplate = "JSON.parse(source)"
                                     // Filter for Rule Results with a Duplicates tag
                                     + ".filter(function(s){return s.EntTags&&s.EntTags[0]==='Duplicates'})"
                                     // Map the results to just the ruleresult Id
                                     + ".map(function(s){return s.RuleResultId;})"
                                     // Sort (makes it easier to follow what's going on)
                                     + ".sort(function(a,b){if(a<b)return -1;return(a>b)?1:0;})";

            var opDelRoadSearch = dupRoadRuleResult.SearchOperation(new[] { opKeyDels }, GuidHelpers.NewTimeUuid(), searchDupsTemplate);

            opDelRoadSearch.OperationType = OperationType.Delete;
            var opDelRoadSearchPrescription = opDelRoadSearch.Search();

            RvStore.AddUpdate(null, null, opDelRoadSearch, null);

            TikTok(pass++, null, new[] { opDelRoadSearchPrescription });


            // We'll start by adding all the shortest ones as the first set of 'actual' roads
            // A minimum of two * (cityCount - 1) roads will be required
            ////var roadSet = values
            ////    .Where(r => r.Detail["properties"]["cityAId"] != null && (r.Detail["properties"]["usage"] == null || (string)r.Detail["properties"]["usage"] == "Not Set"))
            ////    .OrderBy(r => (double)r.Detail["properties"]["distance"]);
            ////var cityIds = new List<(Guid cityId, int Count)>();
            ////foreach (var road in roadSet)
            ////{
            ////    var roadGeoJson = (JObject)road.Detail;
            ////    var cityAId = (Guid)roadGeoJson["properties"]["cityAId"];
            ////    var cityBId = (Guid)roadGeoJson["properties"]["cityBId"];

            ////    // Test whether either city at the end of this road already have two roads
            ////    var cityHasRoads = cityIds.Where(ci => ci.cityId == cityAId || ci.cityId == cityBId).ToList();

            ////    var citiesFullyConnected = cityHasRoads.Count(chr => chr.Count >= 2);

            ////    Console.WriteLine($"{cityIds.Count} - {citiesFullyConnected}");

            ////    switch (citiesFullyConnected)
            ////    {
            ////        case 0:
            ////            // Road connects two cities and neither has a full set of connecting roads
            ////            // Do connection
            ////            roadGeoJson["properties"]["usage"] = "Accepted";
            ////            try
            ////            {
            ////                var a = cityIds.First(chr => chr.cityId == cityAId);
            ////                cityIds.Remove(a);
            ////                a.Count++;
            ////                cityIds.Add(a);
            ////            }
            ////            catch
            ////            {
            ////                cityIds.Add((cityAId, 1));
            ////            }
            ////            try
            ////            {
            ////                var b = cityIds.First(chr => chr.cityId == cityBId);
            ////                cityIds.Remove(b);
            ////                b.Count++;
            ////                cityIds.Add(b);
            ////            }
            ////            catch
            ////            {
            ////                cityIds.Add((cityBId, 1));
            ////            }
            ////            break;
            ////        case 1:
            ////            // Road connecting one fully connected city
            ////            if (cityHasRoads.Count == 1)
            ////            {
            ////                // And only one city - so create an empty connection record for the other
            ////                var ci = cityHasRoads.All(chr => chr.cityId == cityAId) ? (cityBId, 0) : (cityAId, 0);
            ////                cityIds.Add(ci);
            ////            }
            ////            break;

            ////        case 2:
            ////        default:
            ////            // Road connecting two already full connected cities
            ////            break;
            ////    }

            ////    if (cityIds.Count >= 10)
            ////    {
            ////        break;
            ////    }
            ////}

            ////var acceptedRoads = values.Where(v =>
            ////    v.Detail["properties"]["cityAId"] != null &&
            ////    v.Detail["properties"]["usage"] != null && (string)v.Detail["properties"]["usage"] == "Accepted")
            ////    .ToList();
            ////var salesmansJourney = new StringBuilder();
            ////salesmansJourney.Append("{\"type\":\"FeatureCollection\",\"features\":[");
            ////salesmansJourney.Append(string.Join(',', acceptedRoads.Select(v => v.Detail.ToString())));
            ////salesmansJourney.Append("]}");
            ////var jny = JObject.Parse(salesmansJourney.ToString());
            ////File.WriteAllText("Routes00.json", jny.ToString());

            //var citiesWithNoRoadsCount = cityIds.Count(ci => ci.Count == 0);

            // For each city with no connections
            // Determine its two closest connected neighbours and reject that road
            // and accept the two roads to and from this city to those two closest neighbours
            ////foreach (var ci in cityIds.Where(ci => ci.Count == 0))
            ////{
            ////    Console.WriteLine($"{ci.cityId}");

            ////    var closestNeighboursWithRoads = values.Where(v =>
            ////        v.Detail["properties"]["cityAId"] != null &&
            ////        ((Guid)v.Detail["properties"]["cityAId"] == ci.cityId || (Guid)v.Detail["properties"]["cityBId"] == ci.cityId) &&
            ////        v.Detail["properties"]["usage"] != null && (string)v.Detail["properties"]["usage"] == "Accepted")
            ////        .OrderBy(r => (double)r.Detail["properties"]["distance"])
            ////        .ToList();

            ////    var closestNeighbourGuids = closestNeighboursWithRoads.SelectMany(ar => new[]
            ////        {(Guid) ar.Detail["properties"]["cityAId"], (Guid) ar.Detail["properties"]["cityBId"]})
            ////        .GroupBy(cg => cg)
            ////        .Select(cg => cg.Key)
            ////        .ToList();

            ////    foreach (var cng in closestNeighbourGuids)
            ////    {
            ////        var notCng = closestNeighbourGuids.Where(cg => cng != cg).ToList();

            ////        var cnwr = closestNeighboursWithRoads.Where(v =>
            ////        v.Detail["properties"]["cityAId"] != null &&
            ////        ((Guid)v.Detail["properties"]["cityAId"] == ci.cityId || (Guid)v.Detail["properties"]["cityBId"] == ci.cityId) &&
            ////        (notCng.Contains((Guid)v.Detail["properties"]["cityAId"]) || notCng.Contains((Guid)v.Detail["properties"]["cityBId"])))
            ////        .ToList();

            ////        if (!cnwr.Any())
            ////        {
            ////            continue;
            ////        }
            ////        // Road to Reject
            ////        var r2R = cnwr.First();
            ////        values.First(r => r.EntityId == r2R.EntityId).Detail["properties"]["usage"] = "Rejected";

            ////        // Rejected road cities
            ////        var cnwor = new[] { (Guid)r2R.Detail["properties"]["cityAId"], (Guid)r2R.Detail["properties"]["cityBId"] };

            ////        // Roads to Accept
            ////        var r2A = closestNeighboursWithRoads.Where(v =>
            ////            v.Detail["properties"]["cityAId"] != null &&
            ////            ((Guid)v.Detail["properties"]["cityAId"] == ci.cityId || (Guid)v.Detail["properties"]["cityBId"] == ci.cityId) &&
            ////            (cnwor.Contains((Guid)v.Detail["properties"]["cityAId"]) || cnwor.Contains((Guid)v.Detail["properties"]["cityBId"])));
            ////        foreach (var rd in r2A)
            ////        {
            ////            values.First(r => r.EntityId == rd.EntityId).Detail["properties"]["usage"] = "Accepted";
            ////        }
            ////        break;
            ////    }
            ////}

            // Now we'll ensure that every city has at least two roads connecting it
            // First step is to group all of the cities and get a count for the number of roads to each one
            //var citiesWithRoads = acceptedRoads
            //    .SelectMany(ar => new[]
            //        {(Guid) ar.Detail["properties"]["cityAId"], (Guid) ar.Detail["properties"]["cityBId"]})
            //    .GroupBy(cg => cg)
            //    .Select(cg => new { cityId = cg.Key, Count = cg.Count() })
            //    .ToList();
            //citiesWithRoadsCount = citiesWithRoads.Count;

            ////        acceptedRoads = values.Where(v =>
            ////v.Detail["properties"]["cityAId"] != null &&
            ////v.Detail["properties"]["usage"] != null && (string)v.Detail["properties"]["usage"] == "Accepted")
            ////.ToList();
            ////        salesmansJourney = new StringBuilder();
            ////        salesmansJourney.Append("{\"type\":\"FeatureCollection\",\"features\":[");
            ////        salesmansJourney.Append(string.Join(',', acceptedRoads.Select(v => v.Detail.ToString())));
            ////        salesmansJourney.Append("]}");
            ////        jny = JObject.Parse(salesmansJourney.ToString());
            ////        File.WriteAllText("Routes01.json", jny.ToString());

            // Then there's a need to check for any cities with no roads at all connected to them (a possibility)
            // and add these to the same list with a count of zero for each one.
            //if (citiesWithRoadsCount < cityCount)
            //{
            //    var citiesWithNoRoads = values.Where(c =>
            //            c.Detail["properties"]["cityNo"] != null &&
            //            !citiesWithRoads.Select(cr => cr.cityId).Contains(c.EntityId))
            //        .Select(cn => new { cityId = cn.EntityId, Count = 0 })
            //        .ToList();

            //    citiesWithNoRoadsCount = citiesWithNoRoads.Count;

            //    citiesWithRoads.AddRange(citiesWithNoRoads);
            //}

            //do
            //{
            //    if (pass > 10)
            //    {
            //        break;
            //    }

            //    // Take this list and add the two closest roads for each city with less than two roads
            //    // and output the result.
            //    foreach (var cwr in citiesWithRoads.Where(cwr => cwr.Count < 2))
            //    {
            //        roadSet = values.Where(v =>
            //                v.Detail["properties"]["cityAId"] != null &&
            //                v.Detail["properties"]["usage"] != null &&
            //                (string) v.Detail["properties"]["usage"] == "Not Set" &&
            //                ((Guid) v.Detail["properties"]["cityAId"] == cwr.cityId ||
            //                 (Guid) v.Detail["properties"]["cityBId"] == cwr.cityId))
            //            .OrderBy(r => (double) r.Detail["properties"]["distance"])
            //            .Take(2 - cwr.Count);

            //        foreach (var road in roadSet)
            //        {
            //            var roadGeoJson = (JObject) road.Detail;

            //            roadGeoJson["properties"]["usage"] = "Accepted";
            //        }
            //    }

            //    acceptedRoads = values.Where(v =>
            //            v.Detail["properties"]["cityAId"] != null &&
            //            v.Detail["properties"]["usage"] != null &&
            //            (string) v.Detail["properties"]["usage"] == "Accepted")
            //        .ToList();
            //    salesmansJourney = new StringBuilder();
            //    salesmansJourney.Append("{\"type\":\"FeatureCollection\",\"features\":[");
            //    salesmansJourney.Append(string.Join(',', acceptedRoads.Select(v => v.Detail.ToString())));
            //    salesmansJourney.Append("]}");
            //    jny = JObject.Parse(salesmansJourney.ToString());
            //    File.WriteAllText($"routes{pass++}.json", jny.ToString());

            //    // Identify cities with too many roads
            //    var citiesWithTooManyRoads = acceptedRoads
            //        .SelectMany(ar => new[]
            //            {(Guid) ar.Detail["properties"]["cityAId"], (Guid) ar.Detail["properties"]["cityBId"]})
            //        .GroupBy(cg => cg)
            //        .Select(cg => new {cityId = cg.Key, Count = cg.Count()})
            //        .Where(cwr => cwr.Count > 2)
            //        .ToList();

            //    foreach (var cwr in citiesWithTooManyRoads)
            //    {
            //        var road = values.Where(v =>
            //                v.Detail["properties"]["cityAId"] != null &&
            //                v.Detail["properties"]["usage"] != null &&
            //                (string) v.Detail["properties"]["usage"] == "Accepted" &&
            //                ((Guid) v.Detail["properties"]["cityAId"] == cwr.cityId ||
            //                 (Guid) v.Detail["properties"]["cityBId"] == cwr.cityId))
            //            .OrderByDescending(r => (double) r.Detail["properties"]["distance"])
            //            .First();
            //            //.Take(cwr.Count - 2);

            //        //foreach (var road in roadSet)
            //        //{
            //            Guid otherCityId;
            //            otherCityId = (Guid) road.Detail["properties"]["cityAId"] == cwr.cityId
            //                ? (Guid) road.Detail["properties"]["cityBId"]
            //                : (Guid) road.Detail["properties"]["cityAId"];
            //            var otherCityHasTooManyRoads = values.Count(v =>
            //                                               v.Detail["properties"]["cityAId"] != null &&
            //                                               v.Detail["properties"]["usage"] != null &&
            //                                               (string) v.Detail["properties"]["usage"] == "Accepted" &&
            //                                               ((Guid) v.Detail["properties"]["cityAId"] == otherCityId ||
            //                                                (Guid) v.Detail["properties"]["cityBId"] == otherCityId)) >
            //                                           2;

            //            if (!otherCityHasTooManyRoads)
            //            {
            //                continue;
            //            }

            //            var roadGeoJson = (JObject) road.Detail;

            //            roadGeoJson["properties"]["usage"] = "Rejected";
            //        //}
            //    }

            //    acceptedRoads = values.Where(v =>
            //            v.Detail["properties"]["cityAId"] != null &&
            //            v.Detail["properties"]["usage"] != null &&
            //            (string) v.Detail["properties"]["usage"] == "Accepted")
            //        .ToList();
            //    salesmansJourney = new StringBuilder();
            //    salesmansJourney.Append("{\"type\":\"FeatureCollection\",\"features\":[");
            //    salesmansJourney.Append(string.Join(',', acceptedRoads.Select(v => v.Detail.ToString())));
            //    salesmansJourney.Append("]}");
            //    jny = JObject.Parse(salesmansJourney.ToString());
            //    File.WriteAllText($"routes{pass++}.json", jny.ToString());

            //    citiesWithRoads = acceptedRoads
            //        .SelectMany(ar => new[]
            //            {(Guid) ar.Detail["properties"]["cityAId"], (Guid) ar.Detail["properties"]["cityBId"]})
            //        .GroupBy(cg => cg)
            //        .Select(cg => new {cityId = cg.Key, Count = cg.Count()})
            //        .ToList();
            //    citiesWithRoadsCount = citiesWithRoads.Count(cwr => cwr.Count >= 2);

            //    // Then there's a need to check for any cities with no roads at all connected to them (a possibility)
            //    // and add these to the same list with a count of zero for each one.
            //    if (citiesWithRoadsCount < cityCount)
            //    {
            //        var citiesWithNoRoads = values.Where(c =>
            //                c.Detail["properties"]["cityNo"] != null &&
            //                !citiesWithRoads.Select(cr => cr.cityId).Contains(c.EntityId))
            //            .Select(cn => new {cityId = cn.EntityId, Count = 0})
            //            .ToList();

            //        citiesWithNoRoadsCount = citiesWithNoRoads.Count;
            //        citiesWithRoads.AddRange(citiesWithNoRoads);
            //    }
            //} while (citiesWithRoadsCount != cityCount || citiesWithNoRoadsCount != 0);
            //values.Add(new Value(jny));

            //var startingStore = new RulEngStore
            //{
            //    Values = values.ToImmutableHashSet()
            //};

            //RvStore = new Store<RulEngStore>(null, startingStore);



            //var requestJObj = JToken.Parse("{\"Q\": \"How can we help?\", \"AA\":[\"New Claim\", \"Existing Claim\"]}");
            //var requestObj = new Value(12);

            //(rule, ruleResult, rulePrescription) = requestObj.Exists();

            //rules.Add(rule);
            //ruleResults.Add(ruleResult);
            //rulePrescriptions.Add(rulePrescription);

            //(operation, operationPrescription) = ruleResult.Create<Value>(requestObj);

            //operations.Add(operation);
            //operationPrescriptions.Add(operationPrescription);
            //values.Add(requestObj);

            //requestObj = new Value(13);
            //(ruleResult, rulePrescription) = requestObj.Exists(rule);
            //ruleResults.Add(ruleResult);

            //(operation, operationPrescription) = ruleResult.Create<Value>(requestObj);

            //operations.Add(operation);
            //operationPrescriptions.Add(operationPrescription);
            //values.Add(requestObj);



            //var valIds = new List<Guid> {values[0].ValueId, values[1].ValueId};
            //(operation, value, operationPrescription) = ruleResult.Create<Value>(new [] { values[0].ValueId }.ToList());
            //operations.Add(operation);
            //values.Add(value);
            //operationPrescriptions.Add(operationPrescription);
            //(operation, value, operationPrescription) = ruleResult.Create<Value>(new[] { values[1].ValueId }.ToList());
            //operations.Add(operation);
            //values.Add(value);
            //operationPrescriptions.Add(operationPrescription);

            ////var procAllRules = new ProcessAllRulesPrescription();
            ////var procAllOperations = new ProcessAllOperationsPrescription();

            //RvStore = new Store<RulEngStore>(StoreReducers.ReduceStore);

            //RulEngStore changes;
            //RvStore.Subscribe(state => changes = state);

            //var act = RvStore.Dispatch(rulePrescription);
            //foreach (var prescription in operationPrescriptions)
            //{
            //    act = RvStore.Dispatch(prescription);
            //}

            //File.WriteAllText("storeBefore.json", RvStore.GetState().ToString());
            //act = rvStore.Dispatch(procAllRules);
            // File.WriteAllText("storeMiddle.json", rvStore.GetState().ToString());
            //act = rvStore.Dispatch(procAllOperations);
            //File.WriteAllText("storeAfter.json", RvStore.GetState().ToString());
            pass++;
        }