diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs index a9805b3..0d8c0e4 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs @@ -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; /// /// Deployment functionality relating to processes. @@ -114,75 +114,91 @@ private void SetStates(IEnumerable processes, IEnumerable proces this.ExecuteSetStateRequests(requests, user); } - private void ExecuteSetStateRequests(IEnumerable requests, string user = null) + private void ExecuteSetStateRequests(IDictionary requests, string user = null) { - // Due to unpredictable process dependencies we should retry failed requests until there are zero successful responses. - var remainingRequests = new List(requests); - IEnumerable successfulResponses; - IEnumerable failedResponses; + var remainingRequests = requests; + + IDictionary failedRequests; + IDictionary successfulRequests; + IDictionary> 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(); + successfulRequests = new Dictionary(); + errors = new Dictionary>(); - 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(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(req.Value, user, true); + + successfulRequests.Add(req.Key, req.Value); + } + catch (FaultException 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 GetSetStateRequests(IEnumerable processes, IEnumerable processesToDeactivate) + private IDictionary GetSetStateRequests(IEnumerable processes, IEnumerable processesToDeactivate) { - var requests = new List(); - - 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(Constants.Workflow.Fields.StateCode).Value) + if (stateCode.Value == p.GetAttributeValue(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 names) diff --git a/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs b/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs index dd997d1..8cbe86b 100644 --- a/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs +++ b/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs @@ -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; @@ -61,20 +60,10 @@ public void SetStatesBySolution_ProcessInComponentsToDeactivateList_DeactivatesP { var solutionProcesses = new List { GetProcess(Constants.Workflow.StateCodeActive) }; this.MockBySolutionProcesses(solutionProcesses); - this.MockExecuteMultipleResponse( - null, - svc => svc.ExecuteMultiple( - It.Is>( - reqs => reqs.Cast().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(), - It.IsAny(), - It.IsAny()), - true); + this.crmServiceAdapterMock.Setup( + crmSvc => crmSvc.Execute( + It.Is(u => u.Target.GetAttributeValue(Constants.Workflow.Fields.StateCode).Value == Constants.Workflow.StateCodeInactive))) + .Verifiable(); this.processDeploymentSvc.SetStatesBySolution( Solutions, @@ -95,14 +84,13 @@ public void SetStatesBySolution_WithUserParameter_ExecutesAsUser() GetProcess(Constants.Workflow.StateCodeInactive), }; this.MockBySolutionProcesses(solutionProcesses); - this.MockExecuteMultipleResponse( - null, - svc => svc.ExecuteMultiple( - It.IsAny>(), + + this.crmServiceAdapterMock.Setup( + crmSvc => crmSvc.Execute( + It.IsAny(), userToImpersonate, - It.IsAny(), - It.IsAny(), - It.IsAny())); + It.IsAny())) + .Verifiable(); this.processDeploymentSvc.SetStatesBySolution( Solutions, user: userToImpersonate); @@ -165,7 +153,10 @@ public void SetStates_ProcessInComponentsToActivateFound_ActivatesProcess() { var foundProcesses = new List { GetProcess(Constants.Workflow.StateCodeInactive) }; this.MockSetStatesProcesses(foundProcesses); - this.MockExecuteMultipleResponse(); + this.crmServiceAdapterMock.Setup( + crmSvc => crmSvc.Execute( + It.Is(u => u.Target.GetAttributeValue(Constants.Workflow.Fields.StateCode).Value == Constants.Workflow.StateCodeActive))) + .Verifiable(); this.processDeploymentSvc.SetStates(new List { @@ -180,20 +171,10 @@ public void SetStates_ProcessInComponentsToDeactivateFound_DectivatesProcess() { var foundProcesses = new List { GetProcess(Constants.Workflow.StateCodeActive) }; this.MockSetStatesProcesses(foundProcesses); - this.MockExecuteMultipleResponse( - null, - svc => svc.ExecuteMultiple( - It.Is>( - reqs => reqs.Cast().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(), - It.IsAny(), - It.IsAny()), - true); + this.crmServiceAdapterMock.Setup( + crmSvc => crmSvc.Execute( + It.Is(u => u.Target.GetAttributeValue(Constants.Workflow.Fields.StateCode).Value == Constants.Workflow.StateCodeInactive))) + .Verifiable(); this.processDeploymentSvc.SetStates(Enumerable.Empty(), new List { @@ -209,15 +190,12 @@ public void SetStates_WithUserParameter_ExecutesAsUser() var foundProcesses = new List { GetProcess(Constants.Workflow.StateCodeInactive) }; this.MockSetStatesProcesses(foundProcesses); var userToImpersonate = "licenseduser@domaincom"; - this.MockExecuteMultipleResponse( - null, - svc => svc.ExecuteMultiple( - It.IsAny>(), - userToImpersonate, - It.IsAny(), - It.IsAny(), - It.IsAny()), - true); + this.crmServiceAdapterMock.Setup( + crmSvc => crmSvc.Execute( + It.IsAny(), + userToImpersonate, + It.IsAny())) + .Verifiable(); this.processDeploymentSvc.SetStates( new List @@ -235,17 +213,56 @@ public void SetStates_WithError_LogsError() { var foundProcesses = new List { 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(new OrganizationServiceFault(), "Some error."); + this.crmServiceAdapterMock + .Setup(crmSvc => crmSvc.Execute(It.IsAny())) + .Throws(fault); + + this.processDeploymentSvc.SetStates( + new List { - { "Responses", new ExecuteMultipleResponseItemCollection() }, - { "IsFaulted", true }, + foundProcesses.First().GetAttributeValue(Constants.Workflow.Fields.Name), }, + Enumerable.Empty()); + + this.loggerMock.VerifyLog(l => l.LogError(It.Is(s => s.Contains(fault.Message)))); + } + + [Fact] + public void SetStates_WithError_RetriesWhenOtherRequestsAreSuccessful() + { + var foundProcesses = new List + { + 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(new OrganizationServiceFault()); + this.crmServiceAdapterMock + .SetupSequence(crmSvc => crmSvc.Execute(It.IsAny())) + .Throws(fault) + .Returns(new UpdateResponse()); + + this.processDeploymentSvc.SetStates( + foundProcesses.Select(p => p.GetAttributeValue(Constants.Workflow.Fields.Name)).ToList(), + Enumerable.Empty()); + + this.crmServiceAdapterMock.Verify(svc => svc.Execute(It.IsAny()), Times.Exactly(3)); + } + + [Fact] + public void SetStates_WithError_LogsErrorAfterThirdRetry() + { + var foundProcesses = new List { GetProcess(Constants.Workflow.StateCodeInactive) }; + this.MockSetStatesProcesses(foundProcesses); + + var faultException = new FaultException( + new OrganizationServiceFault { Message = "Some message." }); + this.crmServiceAdapterMock + .SetupSequence(crmSvc => crmSvc.Execute(It.IsAny())) + .Throws(faultException) + .Throws(faultException) + .Returns(new UpdateResponse()); this.processDeploymentSvc.SetStates( new List @@ -254,7 +271,7 @@ public void SetStates_WithError_LogsError() }, Enumerable.Empty()); - this.loggerMock.VerifyLog(l => l.LogError(It.Is(s => s.Contains(fault.Message)))); + this.loggerMock.VerifyLog(l => l.LogError(It.Is(s => s.Contains(faultException.Message)))); } private static Entity GetProcess(int stateCode) @@ -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) @@ -273,33 +290,6 @@ private static Entity GetProcess(int stateCode) }; } - private void MockExecuteMultipleResponse(ExecuteMultipleResponse response = null, Expression> expression = null, bool verifiable = false) - { - if (expression == null) - { - expression = svc => svc.ExecuteMultiple( - It.IsAny>(), - It.IsAny(), - It.IsAny(), - It.IsAny()); - } - - 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 processes) { this.crmServiceAdapterMock.Setup(