// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. namespace WixToolset.Core.WindowsInstaller.Bind { using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using WixToolset.Data; using WixToolset.Data.Tuples; using WixToolset.Data.WindowsInstaller; using WixToolset.Extensibility.Services; internal class SequenceActionsCommand { public SequenceActionsCommand(IntermediateSection section) { this.Section = section; this.RelativeActionsForActions = new Dictionary(); this.StandardActionsById = WindowsInstallerStandard.StandardActions().ToDictionary(a => a.Id.Id); } private IntermediateSection Section { get; } private Dictionary RelativeActionsForActions { get; } private Dictionary StandardActionsById { get; } public IMessaging Messaging { private get; set; } /// /// Set sequence numbers for all the actions and create tuples in the output object. /// public void Execute() { var requiredActionTuples = new Dictionary(); // Get the standard actions required based on tuples in the section. var overridableActionTuples = this.GetRequiredStandardActions(); // Index all the action tuples and look for collisions. foreach (var actionTuple in this.Section.Tuples.OfType()) { if (actionTuple.Overridable) // overridable action { if (overridableActionTuples.TryGetValue(actionTuple.Id.Id, out var collidingActionTuple)) { this.Messaging.Write(ErrorMessages.OverridableActionCollision(actionTuple.SourceLineNumbers, actionTuple.SequenceTable.ToString(), actionTuple.Action)); if (null != collidingActionTuple.SourceLineNumbers) { this.Messaging.Write(ErrorMessages.OverridableActionCollision2(collidingActionTuple.SourceLineNumbers)); } } else { overridableActionTuples.Add(actionTuple.Id.Id, actionTuple); } } else // unsequenced or sequenced action. { // Unsequenced action (allowed for certain standard actions). if (null == actionTuple.Before && null == actionTuple.After && !actionTuple.Sequence.HasValue) { if (this.StandardActionsById.TryGetValue(actionTuple.Id.Id, out var standardAction)) { // Populate the sequence from the standard action actionTuple.Sequence = standardAction.Sequence; } else // not a supported unscheduled action. { throw new InvalidOperationException("Found an action with no Sequence, Before, or After column set."); } } if (requiredActionTuples.TryGetValue(actionTuple.Id.Id, out var collidingActionTuple)) { this.Messaging.Write(ErrorMessages.ActionCollision(actionTuple.SourceLineNumbers, actionTuple.SequenceTable.ToString(), actionTuple.Action)); if (null != collidingActionTuple.SourceLineNumbers) { this.Messaging.Write(ErrorMessages.ActionCollision2(collidingActionTuple.SourceLineNumbers)); } } else { requiredActionTuples.Add(actionTuple.Id.Id, actionTuple); } } } // Add the overridable action tuples that are not overridden to the required action tuples. foreach (var actionTuple in overridableActionTuples.Values) { if (!requiredActionTuples.ContainsKey(actionTuple.Id.Id)) { requiredActionTuples.Add(actionTuple.Id.Id, actionTuple); } } // Suppress the required actions that are overridable. foreach (var suppressActionTuple in this.Section.Tuples.OfType()) { var key = suppressActionTuple.Id.Id; // If there is an overridable tuple to suppress; suppress it. There is no warning if there // is no action to suppress because the action may be suppressed from a merge module in // the binder. if (requiredActionTuples.TryGetValue(key, out var requiredActionTuple)) { if (requiredActionTuple.Overridable) { this.Messaging.Write(WarningMessages.SuppressAction(suppressActionTuple.SourceLineNumbers, suppressActionTuple.Action, suppressActionTuple.SequenceTable.ToString())); if (null != requiredActionTuple.SourceLineNumbers) { this.Messaging.Write(WarningMessages.SuppressAction2(requiredActionTuple.SourceLineNumbers)); } requiredActionTuples.Remove(key); } else // suppressing a non-overridable action tuple { this.Messaging.Write(ErrorMessages.SuppressNonoverridableAction(suppressActionTuple.SourceLineNumbers, suppressActionTuple.SequenceTable.ToString(), suppressActionTuple.Action)); if (null != requiredActionTuple.SourceLineNumbers) { this.Messaging.Write(ErrorMessages.SuppressNonoverridableAction2(requiredActionTuple.SourceLineNumbers)); } } } } // Build up dependency trees of the relatively scheduled actions. // Use ToList() to create a copy of the required action tuples so that new tuples can // be added while enumerating. foreach (var actionTuple in requiredActionTuples.Values.ToList()) { if (!actionTuple.Sequence.HasValue) { // check for standard actions that don't have a sequence number in a merge module if (SectionType.Module == this.Section.Type && WindowsInstallerStandard.IsStandardAction(actionTuple.Action)) { this.Messaging.Write(ErrorMessages.StandardActionRelativelyScheduledInModule(actionTuple.SourceLineNumbers, actionTuple.SequenceTable.ToString(), actionTuple.Action)); } this.SequenceActionTuple(actionTuple, requiredActionTuples); } else if (SectionType.Module == this.Section.Type && 0 < actionTuple.Sequence && !WindowsInstallerStandard.IsStandardAction(actionTuple.Action)) // check for custom actions and dialogs that have a sequence number { this.Messaging.Write(ErrorMessages.CustomActionSequencedInModule(actionTuple.SourceLineNumbers, actionTuple.SequenceTable.ToString(), actionTuple.Action)); } } // Look for standard actions with sequence restrictions that aren't necessarily scheduled based // on the presence of a particular table. if (requiredActionTuples.ContainsKey("InstallExecuteSequence/DuplicateFiles") && !requiredActionTuples.ContainsKey("InstallExecuteSequence/InstallFiles")) { var standardAction = this.StandardActionsById["InstallExecuteSequence/InstallFiles"]; requiredActionTuples.Add(standardAction.Id.Id, standardAction); } // Schedule actions. List scheduledActionTuples; if (SectionType.Module == this.Section.Type) { scheduledActionTuples = requiredActionTuples.Values.ToList(); } else { scheduledActionTuples = this.ScheduleActions(requiredActionTuples); } // Remove all existing WixActionTuples from the section then add the // scheduled actions back to the section. Note: we add the indices in // reverse order to make it easy to remove them from the list later. var removeIndices = new List(); for (var i = this.Section.Tuples.Count - 1; i >= 0; --i) { var tuple = this.Section.Tuples[i]; if (tuple.Definition.Type == TupleDefinitionType.WixAction) { removeIndices.Add(i); } } foreach (var removeIndex in removeIndices) { this.Section.Tuples.RemoveAt(removeIndex); } foreach (var action in scheduledActionTuples) { this.Section.Tuples.Add(action); } } private Dictionary GetRequiredStandardActions() { var overridableActionTuples = new Dictionary(); var requiredActionIds = this.GetRequiredActionIds(); foreach (var actionId in requiredActionIds) { var standardAction = this.StandardActionsById[actionId]; overridableActionTuples.Add(standardAction.Id.Id, standardAction); } return overridableActionTuples; } private List ScheduleActions(Dictionary requiredActionTuples) { var scheduledActionTuples = new List(); // Process each sequence table individually. foreach (SequenceTable sequenceTable in Enum.GetValues(typeof(SequenceTable))) { // Create a collection of just the action tuples in this sequence var sequenceActionTuples = requiredActionTuples.Values.Where(a => a.SequenceTable == sequenceTable).ToList(); // Schedule the absolutely scheduled actions (by sorting them by their sequence numbers). var absoluteActionTuples = new List(); foreach (var actionTuple in sequenceActionTuples) { if (actionTuple.Sequence.HasValue) { // Look for sequence number collisions foreach (var sequenceScheduledActionTuple in absoluteActionTuples) { if (sequenceScheduledActionTuple.Sequence == actionTuple.Sequence) { this.Messaging.Write(WarningMessages.ActionSequenceCollision(actionTuple.SourceLineNumbers, actionTuple.SequenceTable.ToString(), actionTuple.Action, sequenceScheduledActionTuple.Action, actionTuple.Sequence ?? 0)); if (null != sequenceScheduledActionTuple.SourceLineNumbers) { this.Messaging.Write(WarningMessages.ActionSequenceCollision2(sequenceScheduledActionTuple.SourceLineNumbers)); } } } absoluteActionTuples.Add(actionTuple); } } absoluteActionTuples.Sort((x, y) => (x.Sequence ?? 0).CompareTo(y.Sequence ?? 0)); // Schedule the relatively scheduled actions (by resolving the dependency trees). var previousUsedSequence = 0; var relativeActionTuples = new List(); for (int j = 0; j < absoluteActionTuples.Count; j++) { var absoluteActionTuple = absoluteActionTuples[j]; // Get all the relatively scheduled action tuples occuring before and after this absolutely scheduled action tuple. var relativeActions = this.GetAllRelativeActionsForSequenceType(sequenceTable, absoluteActionTuple); // Check for relatively scheduled actions occuring before/after a special action // (those actions with a negative sequence number). if (absoluteActionTuple.Sequence < 0 && (relativeActions.PreviousActions.Any() || relativeActions.NextActions.Any())) { // Create errors for all the before actions. foreach (var actionTuple in relativeActions.PreviousActions) { this.Messaging.Write(ErrorMessages.ActionScheduledRelativeToTerminationAction(actionTuple.SourceLineNumbers, actionTuple.SequenceTable.ToString(), actionTuple.Action, absoluteActionTuple.Action)); } // Create errors for all the after actions. foreach (var actionTuple in relativeActions.NextActions) { this.Messaging.Write(ErrorMessages.ActionScheduledRelativeToTerminationAction(actionTuple.SourceLineNumbers, actionTuple.SequenceTable.ToString(), actionTuple.Action, absoluteActionTuple.Action)); } // If there is source line information for the absolutely scheduled action display it if (absoluteActionTuple.SourceLineNumbers != null) { this.Messaging.Write(ErrorMessages.ActionScheduledRelativeToTerminationAction2(absoluteActionTuple.SourceLineNumbers)); } continue; } // Schedule the action tuples before this one. var unusedSequence = absoluteActionTuple.Sequence - 1; for (var i = relativeActions.PreviousActions.Count - 1; i >= 0; i--) { var relativeActionTuple = relativeActions.PreviousActions[i]; // look for collisions if (unusedSequence == previousUsedSequence) { this.Messaging.Write(ErrorMessages.NoUniqueActionSequenceNumber(relativeActionTuple.SourceLineNumbers, relativeActionTuple.SequenceTable.ToString(), relativeActionTuple.Action, absoluteActionTuple.Action)); if (absoluteActionTuple.SourceLineNumbers != null) { this.Messaging.Write(ErrorMessages.NoUniqueActionSequenceNumber2(absoluteActionTuple.SourceLineNumbers)); } unusedSequence++; } relativeActionTuple.Sequence = unusedSequence; relativeActionTuples.Add(relativeActionTuple); unusedSequence--; } // Determine the next used action sequence number. var nextUsedSequence = Int16.MaxValue + 1; if (absoluteActionTuples.Count > j + 1) { nextUsedSequence = absoluteActionTuples[j + 1].Sequence ?? 0; } // Schedule the action tuples after this one. unusedSequence = absoluteActionTuple.Sequence + 1; for (var i = 0; i < relativeActions.NextActions.Count; i++) { var relativeActionTuple = relativeActions.NextActions[i]; if (unusedSequence == nextUsedSequence) { this.Messaging.Write(ErrorMessages.NoUniqueActionSequenceNumber(relativeActionTuple.SourceLineNumbers, relativeActionTuple.SequenceTable.ToString(), relativeActionTuple.Action, absoluteActionTuple.Action)); if (absoluteActionTuple.SourceLineNumbers != null) { this.Messaging.Write(ErrorMessages.NoUniqueActionSequenceNumber2(absoluteActionTuple.SourceLineNumbers)); } unusedSequence--; } relativeActionTuple.Sequence = unusedSequence; relativeActionTuples.Add(relativeActionTuple); unusedSequence++; } // keep track of this sequence number as the previous used sequence number for the next iteration previousUsedSequence = absoluteActionTuple.Sequence ?? 0; } // add the absolutely and relatively scheduled actions to the list of scheduled actions scheduledActionTuples.AddRange(absoluteActionTuples); scheduledActionTuples.AddRange(relativeActionTuples); } return scheduledActionTuples; } private IEnumerable GetRequiredActionIds() { var set = new HashSet(); // gather the required actions for the output type if (SectionType.Product == this.Section.Type) { // AdminExecuteSequence table set.Add("AdminExecuteSequence/CostFinalize"); set.Add("AdminExecuteSequence/CostInitialize"); set.Add("AdminExecuteSequence/FileCost"); set.Add("AdminExecuteSequence/InstallAdminPackage"); set.Add("AdminExecuteSequence/InstallFiles"); set.Add("AdminExecuteSequence/InstallFinalize"); set.Add("AdminExecuteSequence/InstallInitialize"); set.Add("AdminExecuteSequence/InstallValidate"); // AdminUISequence table set.Add("AdminUISequence/CostFinalize"); set.Add("AdminUISequence/CostInitialize"); set.Add("AdminUISequence/ExecuteAction"); set.Add("AdminUISequence/FileCost"); // AdvtExecuteSequence table set.Add("AdvtExecuteSequence/CostFinalize"); set.Add("AdvtExecuteSequence/CostInitialize"); set.Add("AdvtExecuteSequence/InstallInitialize"); set.Add("AdvtExecuteSequence/InstallFinalize"); set.Add("AdvtExecuteSequence/InstallValidate"); set.Add("AdvtExecuteSequence/PublishFeatures"); set.Add("AdvtExecuteSequence/PublishProduct"); // InstallExecuteSequence table set.Add("InstallExecuteSequence/CostFinalize"); set.Add("InstallExecuteSequence/CostInitialize"); set.Add("InstallExecuteSequence/FileCost"); set.Add("InstallExecuteSequence/InstallFinalize"); set.Add("InstallExecuteSequence/InstallInitialize"); set.Add("InstallExecuteSequence/InstallValidate"); set.Add("InstallExecuteSequence/ProcessComponents"); set.Add("InstallExecuteSequence/PublishFeatures"); set.Add("InstallExecuteSequence/PublishProduct"); set.Add("InstallExecuteSequence/RegisterProduct"); set.Add("InstallExecuteSequence/RegisterUser"); set.Add("InstallExecuteSequence/UnpublishFeatures"); set.Add("InstallExecuteSequence/ValidateProductID"); // InstallUISequence table set.Add("InstallUISequence/CostFinalize"); set.Add("InstallUISequence/CostInitialize"); set.Add("InstallUISequence/ExecuteAction"); set.Add("InstallUISequence/FileCost"); set.Add("InstallUISequence/ValidateProductID"); } // Gather the required actions for each tuple type. foreach (var tupleType in this.Section.Tuples.Select(t => t.Definition.Type).Distinct()) { switch (tupleType) { case TupleDefinitionType.AppSearch: set.Add("InstallExecuteSequence/AppSearch"); set.Add("InstallUISequence/AppSearch"); break; case TupleDefinitionType.CCPSearch: set.Add("InstallExecuteSequence/AppSearch"); set.Add("InstallExecuteSequence/CCPSearch"); set.Add("InstallExecuteSequence/RMCCPSearch"); set.Add("InstallUISequence/AppSearch"); set.Add("InstallUISequence/CCPSearch"); set.Add("InstallUISequence/RMCCPSearch"); break; case TupleDefinitionType.Class: set.Add("AdvtExecuteSequence/RegisterClassInfo"); set.Add("InstallExecuteSequence/RegisterClassInfo"); set.Add("InstallExecuteSequence/UnregisterClassInfo"); break; case TupleDefinitionType.Complus: set.Add("InstallExecuteSequence/RegisterComPlus"); set.Add("InstallExecuteSequence/UnregisterComPlus"); break; case TupleDefinitionType.CreateFolder: set.Add("InstallExecuteSequence/CreateFolders"); set.Add("InstallExecuteSequence/RemoveFolders"); break; case TupleDefinitionType.DuplicateFile: set.Add("InstallExecuteSequence/DuplicateFiles"); set.Add("InstallExecuteSequence/RemoveDuplicateFiles"); break; case TupleDefinitionType.Environment: set.Add("InstallExecuteSequence/WriteEnvironmentStrings"); set.Add("InstallExecuteSequence/RemoveEnvironmentStrings"); break; case TupleDefinitionType.Extension: set.Add("AdvtExecuteSequence/RegisterExtensionInfo"); set.Add("InstallExecuteSequence/RegisterExtensionInfo"); set.Add("InstallExecuteSequence/UnregisterExtensionInfo"); break; case TupleDefinitionType.File: set.Add("InstallExecuteSequence/InstallFiles"); set.Add("InstallExecuteSequence/RemoveFiles"); var foundFont = false; var foundSelfReg = false; var foundBindPath = false; foreach (var file in this.Section.Tuples.OfType()) { if (!foundFont && !String.IsNullOrEmpty(file.FontTitle)) { set.Add("InstallExecuteSequence/RegisterFonts"); set.Add("InstallExecuteSequence/UnregisterFonts"); foundFont = true; } if (!foundSelfReg && file.SelfRegCost.HasValue) { set.Add("InstallExecuteSequence/SelfRegModules"); set.Add("InstallExecuteSequence/SelfUnregModules"); foundSelfReg = true; } if (!foundBindPath && !String.IsNullOrEmpty(file.BindPath)) { set.Add("InstallExecuteSequence/BindImage"); foundBindPath = true; } } break; case TupleDefinitionType.IniFile: case TupleDefinitionType.RemoveIniFile: set.Add("InstallExecuteSequence/WriteIniValues"); set.Add("InstallExecuteSequence/RemoveIniValues"); break; case TupleDefinitionType.IsolatedComponent: set.Add("InstallExecuteSequence/IsolateComponents"); break; case TupleDefinitionType.LaunchCondition: set.Add("InstallExecuteSequence/LaunchConditions"); set.Add("InstallUISequence/LaunchConditions"); break; case TupleDefinitionType.MIME: set.Add("AdvtExecuteSequence/RegisterMIMEInfo"); set.Add("InstallExecuteSequence/RegisterMIMEInfo"); set.Add("InstallExecuteSequence/UnregisterMIMEInfo"); break; case TupleDefinitionType.MoveFile: set.Add("InstallExecuteSequence/MoveFiles"); break; case TupleDefinitionType.Assembly: set.Add("AdvtExecuteSequence/MsiPublishAssemblies"); set.Add("InstallExecuteSequence/MsiPublishAssemblies"); set.Add("InstallExecuteSequence/MsiUnpublishAssemblies"); break; case TupleDefinitionType.MsiServiceConfig: case TupleDefinitionType.MsiServiceConfigFailureActions: set.Add("InstallExecuteSequence/MsiConfigureServices"); break; case TupleDefinitionType.ODBCDataSource: case TupleDefinitionType.ODBCTranslator: case TupleDefinitionType.ODBCDriver: set.Add("InstallExecuteSequence/SetODBCFolders"); set.Add("InstallExecuteSequence/InstallODBC"); set.Add("InstallExecuteSequence/RemoveODBC"); break; case TupleDefinitionType.ProgId: set.Add("AdvtExecuteSequence/RegisterProgIdInfo"); set.Add("InstallExecuteSequence/RegisterProgIdInfo"); set.Add("InstallExecuteSequence/UnregisterProgIdInfo"); break; case TupleDefinitionType.PublishComponent: set.Add("AdvtExecuteSequence/PublishComponents"); set.Add("InstallExecuteSequence/PublishComponents"); set.Add("InstallExecuteSequence/UnpublishComponents"); break; case TupleDefinitionType.Registry: case TupleDefinitionType.RemoveRegistry: set.Add("InstallExecuteSequence/WriteRegistryValues"); set.Add("InstallExecuteSequence/RemoveRegistryValues"); break; case TupleDefinitionType.RemoveFile: set.Add("InstallExecuteSequence/RemoveFiles"); break; case TupleDefinitionType.ServiceControl: set.Add("InstallExecuteSequence/StartServices"); set.Add("InstallExecuteSequence/StopServices"); set.Add("InstallExecuteSequence/DeleteServices"); break; case TupleDefinitionType.ServiceInstall: set.Add("InstallExecuteSequence/InstallServices"); break; case TupleDefinitionType.Shortcut: set.Add("AdvtExecuteSequence/CreateShortcuts"); set.Add("InstallExecuteSequence/CreateShortcuts"); set.Add("InstallExecuteSequence/RemoveShortcuts"); break; case TupleDefinitionType.TypeLib: set.Add("InstallExecuteSequence/RegisterTypeLibraries"); set.Add("InstallExecuteSequence/UnregisterTypeLibraries"); break; case TupleDefinitionType.Upgrade: set.Add("InstallExecuteSequence/FindRelatedProducts"); set.Add("InstallUISequence/FindRelatedProducts"); // Only add the MigrateFeatureStates action if MigrateFeature attribute is set on // at least one UpgradeVersion element. if (this.Section.Tuples.OfType().Any(t => t.MigrateFeatures)) { set.Add("InstallExecuteSequence/MigrateFeatureStates"); set.Add("InstallUISequence/MigrateFeatureStates"); } break; } } return set; } private IEnumerable GetActions(SequenceTable sequence, string[] actionNames) { foreach (var action in WindowsInstallerStandard.StandardActions()) { if (action.SequenceTable == sequence && actionNames.Contains(action.Action)) { yield return action; } } } /// /// Sequence an action before or after a standard action. /// /// The action tuple to be sequenced. /// Collection of actions which must be included. private void SequenceActionTuple(WixActionTuple actionTuple, Dictionary requiredActionTuples) { var after = false; if (actionTuple.After != null) { after = true; } else if (actionTuple.Before == null) { throw new InvalidOperationException("Found an action with no Sequence, Before, or After column set."); } var parentActionName = (after ? actionTuple.After : actionTuple.Before); var parentActionKey = actionTuple.SequenceTable.ToString() + "/" + parentActionName; if (!requiredActionTuples.TryGetValue(parentActionKey, out var parentActionTuple)) { // If the missing parent action is a standard action (with a suggested sequence number), add it. if (this.StandardActionsById.TryGetValue(parentActionKey, out parentActionTuple)) { // Create a clone to avoid modifying the static copy of the object. // TODO: consider this: parentActionTuple = parentActionTuple.Clone(); requiredActionTuples.Add(parentActionTuple.Id.Id, parentActionTuple); } else { throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, "Found an action with a non-existent {0} action: {1}.", (after ? "After" : "Before"), parentActionName)); } } else if (actionTuple == parentActionTuple || this.ContainsChildActionTuple(actionTuple, parentActionTuple)) // cycle detected { throw new WixException(ErrorMessages.ActionCircularDependency(actionTuple.SourceLineNumbers, actionTuple.SequenceTable.ToString(), actionTuple.Action, parentActionTuple.Action)); } // Add this action to the appropriate list of dependent action tuples. var relativeActions = this.GetRelativeActions(parentActionTuple); var relatedTuples = (after ? relativeActions.NextActions : relativeActions.PreviousActions); relatedTuples.Add(actionTuple); } private bool ContainsChildActionTuple(WixActionTuple childTuple, WixActionTuple parentTuple) { var result = false; if (this.RelativeActionsForActions.TryGetValue(childTuple.Id.Id, out var relativeActions)) { result = relativeActions.NextActions.Any(a => a.SequenceTable == parentTuple.SequenceTable && a.Id.Id == parentTuple.Id.Id) || relativeActions.PreviousActions.Any(a => a.SequenceTable == parentTuple.SequenceTable && a.Id.Id == parentTuple.Id.Id); } return result; } private RelativeActions GetRelativeActions(WixActionTuple action) { if (!this.RelativeActionsForActions.TryGetValue(action.Id.Id, out var relativeActions)) { relativeActions = new RelativeActions(); this.RelativeActionsForActions.Add(action.Id.Id, relativeActions); } return relativeActions; } private RelativeActions GetAllRelativeActionsForSequenceType(SequenceTable sequenceType, WixActionTuple action) { var relativeActions = new RelativeActions(); if (this.RelativeActionsForActions.TryGetValue(action.Id.Id, out var actionRelatives)) { this.RecurseRelativeActionsForSequenceType(sequenceType, actionRelatives.PreviousActions, relativeActions.PreviousActions); this.RecurseRelativeActionsForSequenceType(sequenceType, actionRelatives.NextActions, relativeActions.NextActions); } return relativeActions; } private void RecurseRelativeActionsForSequenceType(SequenceTable sequenceType, List actions, List visitedActions) { foreach (var action in actions.Where(a => a.SequenceTable == sequenceType)) { if (this.RelativeActionsForActions.TryGetValue(action.Id.Id, out var actionRelatives)) { this.RecurseRelativeActionsForSequenceType(sequenceType, actionRelatives.PreviousActions, visitedActions); } visitedActions.Add(action); if (actionRelatives != null) { this.RecurseRelativeActionsForSequenceType(sequenceType, actionRelatives.NextActions, visitedActions); } } } private class RelativeActions { public List PreviousActions { get; } = new List(); public List NextActions { get; } = new List(); } } }