Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: activation of flows dependent on actions #79

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using Capgemini.PowerApps.PackageDeployerTemplate.Adapters;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;
using Polly;

/// <summary>
/// Deployment functionality relating to processes.
Expand Down Expand Up @@ -114,75 +114,91 @@ private void SetStates(IEnumerable<Entity> processes, IEnumerable<string> proces
this.ExecuteSetStateRequests(requests, user);
}

private void ExecuteSetStateRequests(IEnumerable<OrganizationRequest> requests, string user = null)
private void ExecuteSetStateRequests(IDictionary<Entity, UpdateRequest> requests, string user = null)
{
// Due to unpredictable process dependencies we should retry failed requests until there are zero successful responses.
var remainingRequests = new List<OrganizationRequest>(requests);
IEnumerable<ExecuteMultipleResponseItem> successfulResponses;
IEnumerable<ExecuteMultipleResponseItem> failedResponses;
var remainingRequests = requests;

IDictionary<Entity, UpdateRequest> failedRequests;
IDictionary<Entity, UpdateRequest> successfulRequests;
IDictionary<Entity, FaultException<OrganizationServiceFault>> errors;

do
{
var timeout = 120 + (remainingRequests.Count * 10);
var executeMultipleRes = string.IsNullOrEmpty(user) ?
this.crmSvc.ExecuteMultiple(remainingRequests, true, true, timeout) : this.crmSvc.ExecuteMultiple(remainingRequests, user, true, true, timeout);

successfulResponses = executeMultipleRes.Responses
.Where(r => r.Fault == null)
.ToList();
failedResponses = executeMultipleRes.Responses
.Except(successfulResponses)
.ToList();
remainingRequests = failedResponses
.Select(r => remainingRequests[r.RequestIndex])
.ToList();
}
while (successfulResponses.Any() && remainingRequests.Count > 0);
failedRequests = new Dictionary<Entity, UpdateRequest>();
successfulRequests = new Dictionary<Entity, UpdateRequest>();
errors = new Dictionary<Entity, FaultException<OrganizationServiceFault>>();

if (!successfulResponses.Any() && remainingRequests.Any())
{
foreach (var failedResponse in failedResponses)
foreach (var req in remainingRequests)
{
var failedRequest = (SetStateRequest)remainingRequests[failedResponse.RequestIndex];
this.logger.LogError($"Failed to set state for process {failedRequest.EntityMoniker.Name} with the following error: {failedResponse.Fault.Message}.");
if (req.Value == null)
{
this.logger.LogInformation($"Process {req.Key[Constants.Workflow.Fields.Name]} already has desired state. Skipping.");
continue;
}

if (req.Value.Target.GetAttributeValue<OptionSetValue>(Constants.Workflow.Fields.StateCode).Value == 1)
{
this.logger.LogInformation($"Activating {req.Key[Constants.Workflow.Fields.Name]}.");
}
else
{
this.logger.LogInformation($"Deactivating {req.Key[Constants.Workflow.Fields.Name]}.");
}

try
{
var response = string.IsNullOrEmpty(user) ?
(UpdateResponse)this.crmSvc.Execute(req.Value) : this.crmSvc.Execute<UpdateResponse>(req.Value, user, true);

successfulRequests.Add(req.Key, req.Value);
}
catch (FaultException<OrganizationServiceFault> ex)
{
failedRequests.Add(req.Key, req.Value);
errors.Add(req.Key, ex);
}
}

remainingRequests = failedRequests;
}
while (remainingRequests.Count > 0 && successfulRequests.Any());

foreach (var error in errors)
{
this.logger.LogError($"Failed to set state for process {error.Key[Constants.Workflow.Fields.Name]} with the following error: {error.Value.Message}.");
}
}

private List<OrganizationRequest> GetSetStateRequests(IEnumerable<Entity> processes, IEnumerable<string> processesToDeactivate)
private IDictionary<Entity, UpdateRequest> GetSetStateRequests(IEnumerable<Entity> processes, IEnumerable<string> processesToDeactivate)
{
var requests = new List<OrganizationRequest>();

foreach (var deployedProcess in processes)
return processes.ToDictionary(p => p, p =>
{
var stateCode = new OptionSetValue(Constants.Workflow.StateCodeActive);
var statusCode = new OptionSetValue(Constants.Workflow.StatusCodeActive);

if (processesToDeactivate != null && processesToDeactivate.Contains(deployedProcess[Constants.Workflow.Fields.Name]))
if (processesToDeactivate != null && processesToDeactivate.Contains(p[Constants.Workflow.Fields.Name]))
{
stateCode.Value = Constants.Workflow.StateCodeInactive;
statusCode.Value = Constants.Workflow.StatusCodeInactive;
}

if (stateCode.Value == deployedProcess.GetAttributeValue<OptionSetValue>(Constants.Workflow.Fields.StateCode).Value)
if (stateCode.Value == p.GetAttributeValue<OptionSetValue>(Constants.Workflow.Fields.StateCode).Value)
{
this.logger.LogInformation($"Process {deployedProcess[Constants.Workflow.Fields.Name]} already has desired state. Skipping.");
continue;
return null;
}

this.logger.LogInformation($"Setting process status for {deployedProcess[Constants.Workflow.Fields.Name]} with statecode {stateCode.Value} and statuscode {statusCode.Value}");

// SetStateRequest is supposedly deprecated but UpdateRequest doesn't work for deactivating active flows
requests.Add(
new SetStateRequest
return new UpdateRequest
{
Target = new Entity(p.LogicalName, p.Id)
{
EntityMoniker = deployedProcess.ToEntityReference(),
State = stateCode,
Status = statusCode,
});
}

return requests;
Attributes =
{
{ "statecode", stateCode },
{ "statuscode", statusCode },
},
},
};
});
}

private EntityCollection RetrieveProcesses(IEnumerable<string> names)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.ServiceModel;
using Capgemini.PowerApps.PackageDeployerTemplate.Adapters;
using Capgemini.PowerApps.PackageDeployerTemplate.Services;
using FluentAssertions;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
Expand Down Expand Up @@ -61,20 +60,10 @@ public void SetStatesBySolution_ProcessInComponentsToDeactivateList_DeactivatesP
{
var solutionProcesses = new List<Entity> { GetProcess(Constants.Workflow.StateCodeActive) };
this.MockBySolutionProcesses(solutionProcesses);
this.MockExecuteMultipleResponse(
null,
svc => svc.ExecuteMultiple(
It.Is<IEnumerable<OrganizationRequest>>(
reqs => reqs.Cast<SetStateRequest>().Any(
req =>
req.EntityMoniker.LogicalName == Constants.Workflow.LogicalName &&
req.EntityMoniker.Id == solutionProcesses.First().Id &&
req.State.Value == Constants.Workflow.StateCodeInactive &&
req.Status.Value == Constants.Workflow.StatusCodeInactive)),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<int?>()),
true);
this.crmServiceAdapterMock.Setup(
crmSvc => crmSvc.Execute(
It.Is<UpdateRequest>(u => u.Target.GetAttributeValue<OptionSetValue>(Constants.Workflow.Fields.StateCode).Value == Constants.Workflow.StateCodeInactive)))
.Verifiable();

this.processDeploymentSvc.SetStatesBySolution(
Solutions,
Expand All @@ -95,14 +84,13 @@ public void SetStatesBySolution_WithUserParameter_ExecutesAsUser()
GetProcess(Constants.Workflow.StateCodeInactive),
};
this.MockBySolutionProcesses(solutionProcesses);
this.MockExecuteMultipleResponse(
null,
svc => svc.ExecuteMultiple(
It.IsAny<IEnumerable<OrganizationRequest>>(),

this.crmServiceAdapterMock.Setup(
crmSvc => crmSvc.Execute<UpdateResponse>(
It.IsAny<OrganizationRequest>(),
userToImpersonate,
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<int?>()));
It.IsAny<bool>()))
.Verifiable();

this.processDeploymentSvc.SetStatesBySolution(
Solutions, user: userToImpersonate);
Expand Down Expand Up @@ -165,7 +153,10 @@ public void SetStates_ProcessInComponentsToActivateFound_ActivatesProcess()
{
var foundProcesses = new List<Entity> { GetProcess(Constants.Workflow.StateCodeInactive) };
this.MockSetStatesProcesses(foundProcesses);
this.MockExecuteMultipleResponse();
this.crmServiceAdapterMock.Setup(
crmSvc => crmSvc.Execute(
It.Is<UpdateRequest>(u => u.Target.GetAttributeValue<OptionSetValue>(Constants.Workflow.Fields.StateCode).Value == Constants.Workflow.StateCodeActive)))
.Verifiable();

this.processDeploymentSvc.SetStates(new List<string>
{
Expand All @@ -180,20 +171,10 @@ public void SetStates_ProcessInComponentsToDeactivateFound_DectivatesProcess()
{
var foundProcesses = new List<Entity> { GetProcess(Constants.Workflow.StateCodeActive) };
this.MockSetStatesProcesses(foundProcesses);
this.MockExecuteMultipleResponse(
null,
svc => svc.ExecuteMultiple(
It.Is<IEnumerable<OrganizationRequest>>(
reqs => reqs.Cast<SetStateRequest>().Any(
req =>
req.EntityMoniker.LogicalName == Constants.Workflow.LogicalName &&
req.EntityMoniker.Id == foundProcesses.First().Id &&
req.State.Value == Constants.Workflow.StateCodeInactive &&
req.Status.Value == Constants.Workflow.StatusCodeInactive)),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<int?>()),
true);
this.crmServiceAdapterMock.Setup(
crmSvc => crmSvc.Execute(
It.Is<UpdateRequest>(u => u.Target.GetAttributeValue<OptionSetValue>(Constants.Workflow.Fields.StateCode).Value == Constants.Workflow.StateCodeInactive)))
.Verifiable();

this.processDeploymentSvc.SetStates(Enumerable.Empty<string>(), new List<string>
{
Expand All @@ -209,15 +190,12 @@ public void SetStates_WithUserParameter_ExecutesAsUser()
var foundProcesses = new List<Entity> { GetProcess(Constants.Workflow.StateCodeInactive) };
this.MockSetStatesProcesses(foundProcesses);
var userToImpersonate = "licenseduser@domaincom";
this.MockExecuteMultipleResponse(
null,
svc => svc.ExecuteMultiple(
It.IsAny<IEnumerable<OrganizationRequest>>(),
userToImpersonate,
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<int?>()),
true);
this.crmServiceAdapterMock.Setup(
crmSvc => crmSvc.Execute<UpdateResponse>(
It.IsAny<OrganizationRequest>(),
userToImpersonate,
It.IsAny<bool>()))
.Verifiable();

this.processDeploymentSvc.SetStates(
new List<string>
Expand All @@ -235,17 +213,56 @@ public void SetStates_WithError_LogsError()
{
var foundProcesses = new List<Entity> { GetProcess(Constants.Workflow.StateCodeInactive) };
this.MockSetStatesProcesses(foundProcesses);
var fault = new OrganizationServiceFault { Message = "Some error." };
var response = new ExecuteMultipleResponse
{
Results = new ParameterCollection
var fault = new FaultException<OrganizationServiceFault>(new OrganizationServiceFault(), "Some error.");
this.crmServiceAdapterMock
.Setup(crmSvc => crmSvc.Execute(It.IsAny<UpdateRequest>()))
.Throws(fault);

this.processDeploymentSvc.SetStates(
new List<string>
{
{ "Responses", new ExecuteMultipleResponseItemCollection() },
{ "IsFaulted", true },
foundProcesses.First().GetAttributeValue<string>(Constants.Workflow.Fields.Name),
},
Enumerable.Empty<string>());

this.loggerMock.VerifyLog(l => l.LogError(It.Is<string>(s => s.Contains(fault.Message))));
}

[Fact]
public void SetStates_WithError_RetriesWhenOtherRequestsAreSuccessful()
{
var foundProcesses = new List<Entity>
{
GetProcess(Constants.Workflow.StateCodeInactive),
GetProcess(Constants.Workflow.StateCodeInactive),
};
response.Responses.Add(new ExecuteMultipleResponseItem { Fault = fault });
this.MockExecuteMultipleResponse(response);
this.MockSetStatesProcesses(foundProcesses);
var fault = new FaultException<OrganizationServiceFault>(new OrganizationServiceFault());
this.crmServiceAdapterMock
.SetupSequence(crmSvc => crmSvc.Execute(It.IsAny<UpdateRequest>()))
.Throws(fault)
.Returns(new UpdateResponse());

this.processDeploymentSvc.SetStates(
foundProcesses.Select(p => p.GetAttributeValue<string>(Constants.Workflow.Fields.Name)).ToList(),
Enumerable.Empty<string>());

this.crmServiceAdapterMock.Verify(svc => svc.Execute(It.IsAny<UpdateRequest>()), Times.Exactly(3));
}

[Fact]
public void SetStates_WithError_LogsErrorAfterThirdRetry()
{
var foundProcesses = new List<Entity> { GetProcess(Constants.Workflow.StateCodeInactive) };
this.MockSetStatesProcesses(foundProcesses);

var faultException = new FaultException<OrganizationServiceFault>(
new OrganizationServiceFault { Message = "Some message." });
this.crmServiceAdapterMock
.SetupSequence(crmSvc => crmSvc.Execute(It.IsAny<UpdateRequest>()))
.Throws(faultException)
.Throws(faultException)
.Returns(new UpdateResponse());

this.processDeploymentSvc.SetStates(
new List<string>
Expand All @@ -254,7 +271,7 @@ public void SetStates_WithError_LogsError()
},
Enumerable.Empty<string>());

this.loggerMock.VerifyLog(l => l.LogError(It.Is<string>(s => s.Contains(fault.Message))));
this.loggerMock.VerifyLog(l => l.LogError(It.Is<string>(s => s.Contains(faultException.Message))));
}

private static Entity GetProcess(int stateCode)
Expand All @@ -264,7 +281,7 @@ private static Entity GetProcess(int stateCode)
Attributes =
{
{
Constants.Workflow.Fields.Name, "Process"
Constants.Workflow.Fields.Name, $"Process {Guid.NewGuid()}"
},
{
Constants.Workflow.Fields.StateCode, new OptionSetValue(stateCode)
Expand All @@ -273,33 +290,6 @@ private static Entity GetProcess(int stateCode)
};
}

private void MockExecuteMultipleResponse(ExecuteMultipleResponse response = null, Expression<Func<ICrmServiceAdapter, ExecuteMultipleResponse>> expression = null, bool verifiable = false)
{
if (expression == null)
{
expression = svc => svc.ExecuteMultiple(
It.IsAny<IEnumerable<OrganizationRequest>>(),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<int?>());
}

if (response == null)
{
response = new ExecuteMultipleResponse();
response.Results["Responses"] = new ExecuteMultipleResponseItemCollection();
}

var returnResult = this.crmServiceAdapterMock
.Setup(expression)
.Returns(response);

if (verifiable)
{
returnResult.Verifiable();
}
}

private void MockSetStatesProcesses(IList<Entity> processes)
{
this.crmServiceAdapterMock.Setup(
Expand Down