public void Validate(List <WorkflowIssue> issuesContainer, Action <WorkflowGatewayEntity, WorkflowGatewayDirection> changeDirection) { List <WorkflowIssue> issues = issuesContainer; if (Events.Count(a => a.Value.Type.IsStart()) == 0) { issues.AddError(null, WorkflowValidationMessage.SomeStartEventIsRequired.NiceToString()); } if (Workflow.MainEntityStrategies.Any(a => a == WorkflowMainEntityStrategy.SelectByUser || a == WorkflowMainEntityStrategy.Clone)) { if (Events.Count(a => a.Value.Type == WorkflowEventType.Start) == 0) { issues.AddError(null, WorkflowValidationMessage.NormalStartEventIsRequiredWhenThe0Are1Or2.NiceToString( Workflow.MainEntityStrategies.GetType().NiceName(), WorkflowMainEntityStrategy.SelectByUser.NiceToString(), WorkflowMainEntityStrategy.Clone.NiceToString())); } } if (Events.Count(a => a.Value.Type == WorkflowEventType.Start) > 1) { foreach (var e in Events.Where(a => a.Value.Type == WorkflowEventType.Start)) { issues.AddError(e.Value, WorkflowValidationMessage.MultipleStartEventsAreNotAllowed.NiceToString()); } } var finishEventCount = Events.Count(a => a.Value.Type.IsFinish()); if (finishEventCount == 0) { issues.AddError(null, WorkflowValidationMessage.FinishEventIsRequired.NiceToString()); } Events.Values.ToList().ForEach(e => { var fanIn = PreviousConnections(e).Count(); var fanOut = NextConnections(e).Count(); if (e.Type.IsStart()) { if (fanIn > 0) { issues.AddError(e, WorkflowValidationMessage._0HasInputs.NiceToString(e)); } if (fanOut == 0) { issues.AddError(e, WorkflowValidationMessage._0HasNoOutputs.NiceToString(e)); } if (fanOut > 1) { issues.AddError(e, WorkflowValidationMessage._0HasMultipleOutputs.NiceToString(e)); } if (fanOut == 1) { var nextConn = NextConnections(e).SingleEx(); if (e.Type == WorkflowEventType.Start && !(nextConn.To is WorkflowActivityEntity)) { issues.AddError(e, WorkflowValidationMessage.StartEventNextNodeShouldBeAnActivity.NiceToString()); } } } if (e.Type.IsFinish()) { if (fanIn == 0) { issues.AddError(e, WorkflowValidationMessage._0HasNoInputs.NiceToString(e)); } if (fanOut > 0) { issues.AddError(e, WorkflowValidationMessage._0HasOutputs.NiceToString(e)); } } if (e.Type.IsScheduledStart()) { var schedule = e.ScheduledTask(); if (schedule == null) { issues.AddError(e, WorkflowValidationMessage._0IsTimerStartAndSchedulerIsMandatory.NiceToString(e)); } var wet = e.WorkflowEventTask(); if (wet == null) { issues.AddError(e, WorkflowValidationMessage._0IsTimerStartAndTaskIsMandatory.NiceToString(e)); } else if (wet.TriggeredOn != TriggeredOn.Always) { if (wet.Condition?.Script == null || !wet.Condition.Script.Trim().HasText()) { issues.AddError(e, WorkflowValidationMessage._0IsConditionalStartAndTaskConditionIsMandatory.NiceToString(e)); } } } if (e.Type.IsTimer()) { var boundaryOutput = NextConnections(e).Only(); if (boundaryOutput == null || boundaryOutput.Type != ConnectionType.Normal) { if (e.Type == WorkflowEventType.IntermediateTimer) { issues.AddError(e, WorkflowValidationMessage.IntermediateTimer0ShouldHaveOneOutputOfType1.NiceToString(e, ConnectionType.Normal.NiceToString())); } else { var parentActivity = Activities.Values.Where(a => a.BoundaryTimers.Contains(e)).SingleEx(); issues.AddError(e, WorkflowValidationMessage.BoundaryTimer0OfActivity1ShouldHaveExactlyOneConnectionOfType2.NiceToString(e, parentActivity, ConnectionType.Normal.NiceToString())); } } if (e.Type == WorkflowEventType.IntermediateTimer && !e.Name.HasText()) { issues.AddError(e, WorkflowValidationMessage.IntermediateTimer0ShouldHaveName.NiceToString(e)); } } }); Gateways.Values.ToList().ForEach(g => { var fanIn = PreviousConnections(g).Count(); var fanOut = NextConnections(g).Count(); if (fanIn == 0) { issues.AddError(g, WorkflowValidationMessage._0HasNoInputs.NiceToString(g)); } if (fanOut == 0) { issues.AddError(g, WorkflowValidationMessage._0HasNoOutputs.NiceToString(g)); } if (fanIn == 1 && fanOut == 1) { issues.AddError(g, WorkflowValidationMessage._0HasJustOneInputAndOneOutput.NiceToString(g)); } var newDirection = fanOut == 1 ? WorkflowGatewayDirection.Join : WorkflowGatewayDirection.Split; if (g.Direction != newDirection) { changeDirection(g, newDirection); } if (g.Direction == WorkflowGatewayDirection.Split) { if (g.Type == WorkflowGatewayType.Exclusive || g.Type == WorkflowGatewayType.Inclusive) { if (NextConnections(g).Any(c => c.Type == ConnectionType.Decision)) { List <WorkflowActivityEntity> previousActivities = new List <WorkflowActivityEntity>(); PreviousGraph.DepthExploreConnections(g, (prev, conn, next) => { if (next is WorkflowActivityEntity a) { previousActivities.Add(a); return(false); } return(true); }); foreach (var act in previousActivities.Where(a => a.Type != WorkflowActivityType.Decision)) { issues.AddError(act, WorkflowValidationMessage.Activity0ShouldBeDecision.NiceToString(act)); } } } switch (g.Type) { case WorkflowGatewayType.Exclusive: if (NextConnections(g).OrderByDescending(a => a.Order).Skip(1).Any(c => c.Type == ConnectionType.Normal && c.Condition == null)) { issues.AddError(g, WorkflowValidationMessage.Gateway0ShouldHasConditionOrDecisionOnEachOutputExceptTheLast.NiceToString(g)); } break; case WorkflowGatewayType.Inclusive: if (NextConnections(g).Count(c => c.Type == ConnectionType.Normal && c.Condition == null) != 1) { issues.AddError(g, WorkflowValidationMessage.InclusiveGateway0ShouldHaveOneConnectionWithoutCondition.NiceToString(g)); } break; case WorkflowGatewayType.Parallel: if (NextConnections(g).Count() == 0) { issues.AddError(g, WorkflowValidationMessage.ParallelSplit0ShouldHaveAtLeastOneConnection.NiceToString(g)); } if (NextConnections(g).Any(a => a.Type != ConnectionType.Normal || a.Condition != null)) { issues.AddError(g, WorkflowValidationMessage.ParallelSplit0ShouldHaveOnlyNormalConnectionsWithoutConditions.NiceToString(g)); } break; default: break; } } }); var starts = Events.Values.Where(a => a.Type.IsStart()).ToList(); TrackId = starts.ToDictionary(a => (IWorkflowNodeEntity)a, a => 0); TrackCreatedBy = new Dictionary <int, IWorkflowNodeEntity> { { 0, null ! } }; Queue <IWorkflowNodeEntity> queue = new Queue <IWorkflowNodeEntity>(); queue.EnqueueRange(starts); while (queue.Count > 0) { IWorkflowNodeEntity node = queue.Dequeue(); var nextConns = NextConnections(node).ToList(); //Clone; if (node is WorkflowActivityEntity wa) { foreach (var bt in wa.BoundaryTimers.Where(a => a.Type == WorkflowEventType.BoundaryInterruptingTimer)) { TrackId[bt] = TrackId[wa]; queue.Enqueue(bt); } foreach (var bt in wa.BoundaryTimers.Where(a => a.Type == WorkflowEventType.BoundaryForkTimer)) { var newTrackId = TrackCreatedBy.Count + 1; TrackCreatedBy.Add(newTrackId, bt); TrackId[bt] = TrackId[wa]; queue.Enqueue(bt); } } foreach (var con in nextConns) { if (ContinueExplore(con)) { queue.Enqueue(con.To); } } } bool IsSplitActivity(WorkflowActivityEntity wa) { return(wa.BoundaryTimers.Any(bt => bt.Type == WorkflowEventType.BoundaryForkTimer)); } bool ContinueExplore(WorkflowConnectionEntity conn) { IWorkflowNodeEntity prev = conn.From; IWorkflowNodeEntity next = conn.To; var prevTrackId = TrackId.GetOrThrow(prev); int newTrackId; if (IsParallelGateway(prev, WorkflowGatewayDirection.Split)) { if (IsParallelGateway(next, WorkflowGatewayDirection.Join)) { newTrackId = prevTrackId; } else { newTrackId = TrackCreatedBy.Count + 1; TrackCreatedBy.Add(newTrackId, (WorkflowGatewayEntity)prev); } } else if (prev is WorkflowActivityEntity act && IsSplitActivity(act) || prev is WorkflowEventEntity we && we.Type == WorkflowEventType.BoundaryInterruptingTimer && IsSplitActivity(this.Activities.GetOrThrow(we.BoundaryOf !))) { if (IsParallelGateway(next, WorkflowGatewayDirection.Join)) { newTrackId = prevTrackId; } else { var activity = (prev as WorkflowActivityEntity) ?? this.Activities.GetOrThrow(((WorkflowEventEntity)prev).BoundaryOf !); var mainTrackId = NextConnections(activity) .Concat(activity.BoundaryTimers.Where(a => a.Type == WorkflowEventType.BoundaryInterruptingTimer).SelectMany(we => NextConnections(we))) .Select(c => TrackId.TryGetS(c.To)) .Where(c => c != null) .Distinct() .SingleOrDefaultEx(); if (mainTrackId.HasValue) { newTrackId = mainTrackId.Value; } else { newTrackId = TrackCreatedBy.Count + 1; TrackCreatedBy.Add(newTrackId, activity); } } }
public List <string> Validate(Action <WorkflowGatewayEntity, WorkflowGatewayDirection> changeDirection) { List <string> errors = new List <string>(); if (Events.Count(a => a.Value.Type.IsStart()) == 0) { errors.Add(WorkflowValidationMessage.SomeStartEventIsRequired.NiceToString()); } if (Workflow.MainEntityStrategy != WorkflowMainEntityStrategy.CreateNew) { if (Events.Count(a => a.Value.Type == WorkflowEventType.Start) == 0) { errors.Add(WorkflowValidationMessage.NormalStartEventIsRequiredWhenThe0Are1Or2.NiceToString( Workflow.MainEntityStrategy.GetType().NiceName(), WorkflowMainEntityStrategy.SelectByUser.NiceToString(), WorkflowMainEntityStrategy.Both.NiceToString())); } } if (Events.Count(a => a.Value.Type == WorkflowEventType.Start) > 1) { errors.Add(WorkflowValidationMessage.MultipleStartEventsAreNotAllowed.NiceToString()); } var finishEventCount = Events.Count(a => a.Value.Type.IsFinish()); if (finishEventCount == 0) { errors.Add(WorkflowValidationMessage.FinishEventIsRequired.NiceToString()); } Events.Values.ToList().ForEach(e => { var fanIn = PreviousGraph.RelatedTo(e).Count; var fanOut = NextGraph.RelatedTo(e).Count; if (e.Type.IsStart()) { if (fanIn > 0) { errors.Add(WorkflowValidationMessage._0HasInputs.NiceToString(e)); } if (fanOut == 0) { errors.Add(WorkflowValidationMessage._0HasNoOutputs.NiceToString(e)); } if (fanOut > 1) { errors.Add(WorkflowValidationMessage._0HasMultipleOutputs.NiceToString(e)); } if (fanOut == 1) { var nextConn = NextGraph.RelatedTo(e).Single().Value; if (e.Type == WorkflowEventType.Start && !(nextConn.To is WorkflowActivityEntity)) { errors.Add(WorkflowValidationMessage.StartEventNextNodeShouldBeAnActivity.NiceToString()); } } } if (e.Type.IsFinish()) { if (fanIn == 0) { errors.Add(WorkflowValidationMessage._0HasNoInputs.NiceToString(e)); } if (fanOut > 0) { errors.Add(WorkflowValidationMessage._0HasOutputs.NiceToString(e)); } } if (e.Type.IsTimerStart()) { var schedule = e.ScheduledTask(); if (schedule == null) { errors.Add(WorkflowValidationMessage._0IsTimerStartAndSchedulerIsMandatory.NiceToString(e)); } var wet = e.WorkflowEventTask(); if (wet == null) { errors.Add(WorkflowValidationMessage._0IsTimerStartAndTaskIsMandatory.NiceToString(e)); } else if (wet.TriggeredOn != TriggeredOn.Always) { if (wet.Condition?.Script == null || !wet.Condition.Script.Trim().HasText()) { errors.Add(WorkflowValidationMessage._0IsConditionalStartAndTaskConditionIsMandatory.NiceToString(e)); } } } }); Gateways.Values.ToList().ForEach(g => { var fanIn = PreviousGraph.RelatedTo(g).Count; var fanOut = NextGraph.RelatedTo(g).Count; if (fanIn == 0) { errors.Add(WorkflowValidationMessage._0HasNoInputs.NiceToString(g)); } if (fanOut == 0) { errors.Add(WorkflowValidationMessage._0HasNoOutputs.NiceToString(g)); } if (fanIn == 1 && fanOut == 1) { errors.Add(WorkflowValidationMessage._0HasJustOneInputAndOneOutput.NiceToString(g)); } var newDirection = fanOut == 1 ? WorkflowGatewayDirection.Join : WorkflowGatewayDirection.Split; if (g.Direction != newDirection) { changeDirection(g, newDirection); } if (g.Direction == WorkflowGatewayDirection.Split) { if (g.Type == WorkflowGatewayType.Exclusive || g.Type == WorkflowGatewayType.Inclusive) { if (NextGraph.RelatedTo(g).OrderByDescending(a => a.Value.Order).Any(c => c.Value.DecisonResult != null)) { List <WorkflowActivityEntity> previousActivities = new List <WorkflowActivityEntity>(); PreviousGraph.DepthExploreConnections(g, (prev, conn, next) => { if (next is WorkflowActivityEntity a) { previousActivities.Add(a); return(false); } return(true); }); foreach (var act in previousActivities.Where(a => a.Type != WorkflowActivityType.Decision)) { errors.Add(WorkflowValidationMessage.Activity0ShouldBeDecision.NiceToString(act)); } } } if (g.Type == WorkflowGatewayType.Exclusive && NextGraph.RelatedTo(g).OrderByDescending(a => a.Value.Order).Skip(1).Any(c => c.Value.DecisonResult == null && c.Value.Condition == null)) { errors.Add(WorkflowValidationMessage.Gateway0ShouldHasConditionOrDecisionOnEachOutputExceptTheLast.NiceToString(g)); } if (g.Type == WorkflowGatewayType.Inclusive && NextGraph.RelatedTo(g).Any(c => c.Value.DecisonResult == null && c.Value.Condition == null)) { errors.Add(WorkflowValidationMessage.Gateway0ShouldHasConditionOnEachOutput.NiceToString(g)); } } }); var starts = Events.Values.Where(a => a.Type.IsStart()).ToList(); TrackId = starts.ToDictionary(a => (IWorkflowNodeEntity)a, a => 0); TrackCreatedBy = new Dictionary <int, WorkflowGatewayEntity> { { 0, null } }; ParallelWorkflowPairs = new Dictionary <WorkflowGatewayEntity, WorkflowGatewayEntity>(); starts.ForEach(st => NextGraph.BreadthExploreConnections(st, (prev, conn, next) => { var prevTrackId = TrackId.GetOrThrow(prev); int newTrackId; if (IsParallelGateway(prev, WorkflowGatewayDirection.Split)) { if (IsParallelGateway(next, WorkflowGatewayDirection.Join)) { newTrackId = prevTrackId; } else { newTrackId = TrackCreatedBy.Count + 1; TrackCreatedBy.Add(newTrackId, (WorkflowGatewayEntity)prev); } } else { if (IsParallelGateway(next, WorkflowGatewayDirection.Join)) { var split = TrackCreatedBy.TryGetC(prevTrackId); if (split == null) { errors.Add(WorkflowValidationMessage._0CanNotBeConnectedToAParallelJoinBecauseHasNoPreviousParallelSplit.NiceToString(prev)); return(false); } ParallelWorkflowPairs[split] = (WorkflowGatewayEntity)next; newTrackId = TrackId.GetOrThrow(split); } else { newTrackId = prevTrackId; } } if (TrackId.ContainsKey(next)) { if (TrackId[next] != newTrackId) { errors.Add(WorkflowValidationMessage._0Track1CanNotBeConnectedTo2Track3InsteadOfTrack4.NiceToString(prev, prevTrackId, next, TrackId[next], newTrackId)); } return(false); } else { TrackId[next] = newTrackId; return(true); } }) ); Action <WorkflowActivityEntity, IWorkflowTransitionTo> ValidateTransition = (WorkflowActivityEntity wa, IWorkflowTransitionTo item) => { var activity0CanNotXTo1Because2 = (item is WorkflowJumpEmbedded || item is WorkflowScriptPartEmbedded) ? WorkflowValidationMessage.Activity0CanNotJumpTo1Because2 : WorkflowValidationMessage.Activity0CanNotTimeoutTo1Because2; var to = item.To is Lite <WorkflowActivityEntity>?(IWorkflowNodeEntity)Activities.TryGetC((Lite <WorkflowActivityEntity>)item.To) : item.To is Lite <WorkflowGatewayEntity>?(IWorkflowNodeEntity)Gateways.TryGetC((Lite <WorkflowGatewayEntity>)item.To) : item.To is Lite <WorkflowEventEntity>?(IWorkflowNodeEntity)Events.TryGetC((Lite <WorkflowEventEntity>)item.To) : null; if (to == null) { errors.Add(activity0CanNotXTo1Because2.NiceToString(wa, item.To, WorkflowValidationMessage.IsNotInWorkflow.NiceToString())); } if (to is WorkflowEventEntity && ((WorkflowEventEntity)to).Type.IsStart()) { errors.Add(activity0CanNotXTo1Because2.NiceToString(wa, item.To, WorkflowValidationMessage.IsStart.NiceToString())); } if (to is WorkflowActivityEntity && to == wa) { errors.Add(activity0CanNotXTo1Because2.NiceToString(wa, item.To, WorkflowValidationMessage.IsSelfJumping.NiceToString())); } if (TrackId.GetOrThrow(to) != TrackId.GetOrThrow(wa)) { errors.Add(activity0CanNotXTo1Because2.NiceToString(wa, item.To, WorkflowValidationMessage.IsInDifferentParallelTrack.NiceToString())); } }; foreach (var wa in Activities.Values) { var fanIn = PreviousGraph.RelatedTo(wa).Count; var fanOut = NextGraph.RelatedTo(wa).Count; if (fanIn == 0) { errors.Add(WorkflowValidationMessage._0HasNoInputs.NiceToString(wa)); } if (fanOut == 0) { errors.Add(WorkflowValidationMessage._0HasNoOutputs.NiceToString(wa)); } if (fanOut > 1) { errors.Add(WorkflowValidationMessage._0HasMultipleOutputs.NiceToString(wa)); } if (fanOut == 1 && wa.Type == WorkflowActivityType.Decision) { var nextConn = NextGraph.RelatedTo(wa).Single().Value; if (!(nextConn.To is WorkflowGatewayEntity) || ((WorkflowGatewayEntity)nextConn.To).Type == WorkflowGatewayType.Parallel) { errors.Add(WorkflowValidationMessage.Activity0WithDecisionTypeShouldGoToAnExclusiveOrInclusiveGateways.NiceToString(wa)); } } if (wa.Reject != null) { var prevs = PreviousGraph.IndirectlyRelatedTo(wa, kvp => !(kvp.Key is WorkflowActivityEntity)); if (prevs.Any(a => a is WorkflowEventEntity && ((WorkflowEventEntity)a).Type.IsStart())) { errors.Add(WorkflowValidationMessage.Activity0CanNotRejectToStart.NiceToString(wa)); } if (prevs.Any(a => IsParallelGateway(a))) { errors.Add(WorkflowValidationMessage.Activity0CanNotRejectToParallelGateway.NiceToString(wa)); } } if (wa.Timeout != null) { ValidateTransition(wa, wa.Timeout); } if (wa.Script != null) { ValidateTransition(wa, wa.Script); } foreach (var item in wa.Jumps) { ValidateTransition(wa, item); } } if (errors.HasItems()) { this.TrackCreatedBy = null; this.TrackId = null; this.ParallelWorkflowPairs = null; } return(errors); }