/// <summary>
        /// Processes plugin for a type type rolled up
        /// </summary>
        /// <param name="plugin"></param>
        public void ExecuteRollupPlugin(XrmEntityPlugin plugin)
        {
            if (plugin.IsMessage(PluginMessage.Create, PluginMessage.Update, PluginMessage.Delete) &&
                plugin.IsStage(PluginStage.PostEvent) &&
                plugin.IsMode(PluginMode.Synchronous))
            {
                var rollupsToProcess         = GetRollupsForRolledupType(plugin.TargetType).ToArray();
                var dictionaryForDifferences = new Dictionary <string, Dictionary <Guid, List <UpdateMeta> > >();

                Action <string, Guid, string, object, LookupRollup> addValueToApply = (type, id, field, val, rollup) =>
                {
                    if (!dictionaryForDifferences.ContainsKey(type))
                    {
                        dictionaryForDifferences.Add(type, new Dictionary <Guid, List <UpdateMeta> >());
                    }
                    if (!dictionaryForDifferences[type].ContainsKey(id))
                    {
                        dictionaryForDifferences[type].Add(id, new List <UpdateMeta>());
                    }
                    dictionaryForDifferences[type][id].Add(new UpdateMeta(field, rollup, val));
                };

                foreach (var rollup in rollupsToProcess)
                {
                    //capture required facts in the plugin context to process our ifs and elses
                    var metConditionsBefore  = XrmEntity.MeetsFilter(plugin.GetFieldFromPreImage, rollup.Filter);
                    var meetsConditionsAfter = plugin.MessageName == PluginMessage.Delete
                        ? false
                        : XrmEntity.MeetsFilter(plugin.GetField, rollup.Filter);
                    var linkedIdBefore = XrmEntity.GetLookupType(plugin.GetFieldFromPreImage(rollup.LookupName)) == rollup.RecordTypeWithRollup
                        ? plugin.GetLookupGuidPreImage(rollup.LookupName)
                        : null;
                    var linkedIdAfter = plugin.MessageName == PluginMessage.Delete || XrmEntity.GetLookupType(plugin.GetField(rollup.LookupName)) != rollup.RecordTypeWithRollup
                        ? null
                        : plugin.GetLookupGuid(rollup.LookupName);
                    var isValueChanging   = rollup.FieldRolledup != null && plugin.FieldChanging(rollup.FieldRolledup);
                    var isOrderByChanging = rollup.OrderByField != null && plugin.FieldChanging(rollup.OrderByField);

                    if (AllowsDifferenceChange(rollup.RollupType))
                    {
                        //this covers scenarios which require changing the value in a parent record
                        if (linkedIdBefore.HasValue && linkedIdBefore == linkedIdAfter)
                        {
                            //the same record linked before and after
                            if (metConditionsBefore && meetsConditionsAfter)
                            {
                                //if part of Rollup before and after
                                if (isValueChanging)
                                {
                                    //and the value is changing then apply difference
                                    if (rollup.RollupType == RollupType.Sum)
                                    {
                                        addValueToApply(rollup.RecordTypeWithRollup, linkedIdAfter.Value, rollup.RollupField, GetDifferenceToApply(plugin.GetFieldFromPreImage(rollup.FieldRolledup), plugin.GetField(rollup.FieldRolledup)), rollup);
                                    }
                                    else if (rollup.RollupType == RollupType.Count)
                                    {
                                        //for count only adjust if changing between null and not null
                                        if (plugin.GetFieldFromPreImage(rollup.FieldRolledup) == null)
                                        {
                                            addValueToApply(rollup.RecordTypeWithRollup, linkedIdAfter.Value, rollup.RollupField, 1, rollup);
                                        }
                                        else if (plugin.GetField(rollup.FieldRolledup) == null)
                                        {
                                            addValueToApply(rollup.RecordTypeWithRollup, linkedIdAfter.Value, rollup.RollupField, -1, rollup);
                                        }
                                    }
                                }
                            }
                            if (!metConditionsBefore && meetsConditionsAfter)
                            {
                                //if was not part of Rollup before but is now apply the entire value
                                if (rollup.RollupType == RollupType.Sum)
                                {
                                    addValueToApply(rollup.RecordTypeWithRollup, linkedIdAfter.Value, rollup.RollupField, plugin.GetField(rollup.FieldRolledup), rollup);
                                }
                                else if (rollup.RollupType == RollupType.Count)
                                {
                                    addValueToApply(rollup.RecordTypeWithRollup, linkedIdAfter.Value, rollup.RollupField, 1, rollup);
                                }
                            }
                            if (metConditionsBefore && !meetsConditionsAfter)
                            {
                                //if was part of Rollup before but not now apply the entire value negative
                                if (rollup.RollupType == RollupType.Sum)
                                {
                                    addValueToApply(rollup.RecordTypeWithRollup, linkedIdAfter.Value, rollup.RollupField, GetNegative(plugin.GetFieldFromPreImage(rollup.FieldRolledup)), rollup);
                                }
                                else if (rollup.RollupType == RollupType.Count)
                                {
                                    addValueToApply(rollup.RecordTypeWithRollup, linkedIdAfter.Value, rollup.RollupField, -1, rollup);
                                }
                            }
                        }
                        else
                        {
                            //different parent linked before and after
                            if (linkedIdBefore.HasValue && metConditionsBefore)
                            {
                                //if was part of previous linked records Rollup then negate the previous value
                                if (rollup.RollupType == RollupType.Sum)
                                {
                                    addValueToApply(rollup.RecordTypeWithRollup, linkedIdBefore.Value, rollup.RollupField, GetNegative(plugin.GetFieldFromPreImage(rollup.FieldRolledup)), rollup);
                                }
                                else if (rollup.RollupType == RollupType.Count)
                                {
                                    addValueToApply(rollup.RecordTypeWithRollup, linkedIdBefore.Value, rollup.RollupField, -1, rollup);
                                }
                            }
                            if (linkedIdAfter.HasValue && meetsConditionsAfter)
                            {
                                //if part of new linked records Rollup then apply the entire value
                                if (rollup.RollupType == RollupType.Sum)
                                {
                                    addValueToApply(rollup.RecordTypeWithRollup, linkedIdAfter.Value, rollup.RollupField, plugin.GetField(rollup.FieldRolledup), rollup);
                                }
                                else if (rollup.RollupType == RollupType.Count)
                                {
                                    addValueToApply(rollup.RecordTypeWithRollup, linkedIdAfter.Value, rollup.RollupField, 1, rollup);
                                }
                            }
                        }
                    }
                    else
                    {
                        //these ones just recalculate on the parent record(s)
                        var isDependencyChanging = false;
                        switch (plugin.MessageName)
                        {
                        case PluginMessage.Delete:
                        {
                            isDependencyChanging = linkedIdBefore.HasValue && metConditionsBefore;
                            break;
                        }

                        case PluginMessage.Update:
                        {
                            if (linkedIdBefore != linkedIdAfter || isValueChanging)
                            {
                                isDependencyChanging = true;
                            }
                            else if (isOrderByChanging)
                            {
                                isDependencyChanging = true;
                            }
                            else
                            {
                                isDependencyChanging = metConditionsBefore != meetsConditionsAfter;
                            }
                            break;
                        }

                        case PluginMessage.Create:
                        {
                            isDependencyChanging = linkedIdAfter.HasValue &&
                                                   (rollup.FieldRolledup == null || isValueChanging) &&
                                                   meetsConditionsAfter;
                            break;
                        }
                        }
                        if (isDependencyChanging)
                        {
                            var processPreReferenced  = false;
                            var processPostReferenced = false;
                            //If they aren't the same do both
                            if (!XrmEntity.FieldsEqual(linkedIdBefore, linkedIdAfter))
                            {
                                processPreReferenced  = true;
                                processPostReferenced = true;
                            }
                            //else just do the first not null one
                            else
                            {
                                if (linkedIdBefore.HasValue)
                                {
                                    processPreReferenced = true;
                                }
                                else
                                {
                                    processPostReferenced = true;
                                }
                            }
                            if (processPreReferenced && linkedIdBefore.HasValue)
                            {
                                addValueToApply(rollup.RecordTypeWithRollup, linkedIdBefore.Value, rollup.RollupField, null, rollup);
                            }
                            if (processPostReferenced && linkedIdAfter.HasValue)
                            {
                                addValueToApply(rollup.RecordTypeWithRollup, linkedIdAfter.Value, rollup.RollupField, null, rollup);
                            }
                        }
                    }
                }

                //apply all required changes to parents we captured
                //type -> ids -> fields . values
                foreach (var item in dictionaryForDifferences)
                {
                    var targetType = item.Key;
                    foreach (var idUpdates in item.Value)
                    {
                        var id = idUpdates.Key;
                        //lock the parent record then retreive it
                        plugin.XrmService.SetField(targetType, id, "modifiedon", DateTime.UtcNow);
                        var fieldsForUpdating = idUpdates.Value.Select(kv => kv.FieldName).ToArray();
                        var targetRecord      = plugin.XrmService.Retrieve(targetType, id, idUpdates.Value.Select(kv => kv.FieldName));
                        //update the fields
                        foreach (var fieldUpdate in idUpdates.Value)
                        {
                            if (AllowsDifferenceChange(fieldUpdate.Rollup.RollupType))
                            {
                                targetRecord.SetField(fieldUpdate.FieldName, XrmEntity.SumFields(new[] { fieldUpdate.DifferenceValue, targetRecord.GetField(fieldUpdate.FieldName) }));
                            }
                            else
                            {
                                targetRecord.SetField(fieldUpdate.FieldName, GetRollup(fieldUpdate.Rollup, id));
                            }
                        }
                        plugin.XrmService.Update(targetRecord, fieldsForUpdating);
                    }
                }
            }
        }
        private bool DoRefresh()
        {
            var target = XrmService.Retrieve(TargetType, TargetId);

            var fieldsToSet = new List <string>();

            if (!target.GetBoolean(Fields.jmcg_calculatedfield_.jmcg_isrecalculating))
            {
                target.SetField(Fields.jmcg_calculatedfield_.jmcg_isrecalculating, true);
                fieldsToSet.Add(Fields.jmcg_calculatedfield_.jmcg_isrecalculating);
            }
            if (!string.IsNullOrWhiteSpace(target.GetStringField(Fields.jmcg_calculatedfield_.jmcg_errorrecalculating)))
            {
                target.SetField(Fields.jmcg_calculatedfield_.jmcg_errorrecalculating, null);
                fieldsToSet.Add(Fields.jmcg_calculatedfield_.jmcg_errorrecalculating);
            }
            if (fieldsToSet.Any())
            {
                XrmService.Update(target, fieldsToSet.ToArray());
            }

            var isFinished          = false;
            var createDateThreshold = target.GetDateTimeField(Fields.jmcg_calculatedfield_.jmcg_lastrecalculationcreatedate) ?? new DateTime(1910, 1, 1);

            try
            {
                var config                     = CalculatedService.LoadCalculatedFieldConfig(target);
                var startedAt                  = DateTime.UtcNow;
                var fieldsToLoad               = new List <string>();
                var calculatedFieldFieldName   = target.GetStringField(Fields.jmcg_calculatedfield_.jmcg_field);
                var calciulatedFieldEntityName = target.GetStringField(Fields.jmcg_calculatedfield_.jmcg_entitytype);
                fieldsToLoad.Add(calculatedFieldFieldName);
                fieldsToLoad.Add("createdon");
                fieldsToLoad.AddRange(CalculatedService.GetDependencyFields(config, config.CalculatedFieldEntity.GetStringField(Fields.jmcg_calculatedfield_.jmcg_entitytype)));
                fieldsToLoad.AddRange(XrmEntity.GetFieldsInFilter(config.ApplyFilterExpression));

                while (true)
                {
                    if (IsSandboxIsolated && DateTime.UtcNow - startedAt > new TimeSpan(0, 0, MaxSandboxIsolationExecutionSeconds - 10))
                    {
                        break;
                    }
                    else
                    {
                        var processSetQuery = new QueryExpression(calciulatedFieldEntityName);
                        processSetQuery.ColumnSet = new ColumnSet(fieldsToLoad.Distinct().ToArray());
                        processSetQuery.AddOrder("createdon", OrderType.Ascending);
                        processSetQuery.Criteria.AddCondition(new ConditionExpression("createdon", ConditionOperator.GreaterEqual, createDateThreshold));

                        var processSet   = XrmService.RetrieveMultiple(processSetQuery).Entities.ToList();
                        var countThisSet = processSet.Count;

                        if (countThisSet == 0)
                        {
                            isFinished = true;
                        }
                        else
                        {
                            var countProcessed = 0;
                            ProcessWhileInSandboxLimit(startedAt, processSet.ToArray(), (entity) =>
                            {
                                try
                                {
                                    if (target.GetOptionSetValue(Fields.jmcg_calculatedfield_.jmcg_type) == OptionSets.CalculatedField.Type.Rollup)
                                    {
                                        var rollup        = CalculatedService.CreateRollup(config);
                                        var rollupService = new CalculatedRollupService(XrmService, new []
                                        {
                                            rollup
                                        });

                                        rollupService.RefreshRollup(entity.Id, rollup);
                                    }
                                    else
                                    {
                                        if (XrmEntity.MeetsFilter(entity.GetField, config.ApplyFilterExpression))
                                        {
                                            var oldValue = entity.GetField(calculatedFieldFieldName);
                                            var newValue = CalculatedService.GetNewValue(config, entity.GetField);
                                            if (!XrmEntity.FieldsEqual(oldValue, newValue))
                                            {
                                                entity.SetField(calculatedFieldFieldName, newValue);
                                                XrmService.Update(entity, calculatedFieldFieldName);
                                            }
                                        }
                                    }
                                    createDateThreshold = entity.GetDateTimeField("createdon") ?? throw new InvalidPluginExecutionException("Error empty createdon " + entity.Id);
                                    processSet.Remove(entity);
                                    countProcessed++;
                                }
                                catch (Exception ex)
                                {
                                    throw new InvalidPluginExecutionException($"Error refreshing ecountered for ID {entity.Id}. Refresh will be discontinued", ex);
                                }
                            });

                            if (countThisSet < 5000 &&
                                countProcessed == countThisSet)
                            {
                                isFinished = true;
                            }
                        }
                    }
                    if (isFinished)
                    {
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                target.SetField(Fields.jmcg_calculatedfield_.jmcg_errorrecalculating, ex.XrmDisplayString().Left(10000));
                target.SetField(Fields.jmcg_calculatedfield_.jmcg_lastrecalculationcreatedate, null);
                target.SetField(Fields.jmcg_calculatedfield_.jmcg_isrecalculating, false);
                XrmService.Update(target, Fields.jmcg_calculatedfield_.jmcg_errorrecalculating, Fields.jmcg_calculatedfield_.jmcg_lastrecalculationcreatedate, Fields.jmcg_calculatedfield_.jmcg_isrecalculating);
                throw ex;
            }

            if (isFinished)
            {
                target.SetField(Fields.jmcg_calculatedfield_.jmcg_lastrecalculationcreatedate, null);
                target.SetField(Fields.jmcg_calculatedfield_.jmcg_isrecalculating, false);
                XrmService.Update(target, Fields.jmcg_calculatedfield_.jmcg_lastrecalculationcreatedate, Fields.jmcg_calculatedfield_.jmcg_isrecalculating);
            }
            else
            {
                target.SetField(Fields.jmcg_calculatedfield_.jmcg_lastrecalculationcreatedate, createDateThreshold);
                XrmService.Update(target, Fields.jmcg_calculatedfield_.jmcg_lastrecalculationcreatedate);
            }

            return(isFinished);
        }