diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/CrmServiceAdapter.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/CrmServiceAdapter.cs index 4709fc2..2fa9f3f 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/CrmServiceAdapter.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/CrmServiceAdapter.cs @@ -4,7 +4,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; + using System.ServiceModel; + using System.Web.Configuration; using Capgemini.PowerApps.PackageDeployerTemplate.Exceptions; + using DocumentFormat.OpenXml.Office2016.Excel; using Microsoft.Extensions.Logging; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; @@ -17,9 +20,20 @@ /// public class CrmServiceAdapter : ICrmServiceAdapter, IDisposable { + private static readonly int[] CustomizationLockErrorCodes = new int[] + { + Constants.ErrorCodes.CustomizationLockExBlockingUnknown, + Constants.ErrorCodes.CustomizationLockExBothKnownDifferent, + Constants.ErrorCodes.CustomizationLockExBothKnownSame, + Constants.ErrorCodes.CustomizationLockExBlockedUnknown, + Constants.ErrorCodes.CustomizationLockExBothUnknown, + }; + private readonly CrmServiceClient crmSvc; private readonly ILogger logger; + private Policy customizationLockPolicy; + /// /// Initializes a new instance of the class. /// @@ -34,6 +48,24 @@ public CrmServiceAdapter(CrmServiceClient crmSvc, ILogger logger) /// public Guid? CallerAADObjectId { get => this.crmSvc.CallerAADObjectId; set => this.crmSvc.CallerAADObjectId = value; } + private Policy CustomizationLockPolicy + { + get + { + this.customizationLockPolicy ??= Policy + .Handle() + .RetryForever(_ => this.WaitForSolutionHistoryRecordsToComplete()) + .Wrap( + Policy + .Handle() + .WaitAndRetryForever( + sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(30), + onRetry: (_, timeSpan) => this.logger.LogInformation("A solution concurrency issue has occured. Waiting for {0} seconds before retrying.", timeSpan.TotalSeconds))); + + return this.customizationLockPolicy; + } + } + /// public ExecuteMultipleResponse ExecuteMultiple(IEnumerable requests, bool continueOnError = true, bool returnResponses = true, int? timeout = null) { @@ -262,7 +294,7 @@ public string GetEntityTypeCode(string entityLogicalName) } /// - public TResponse Execute(OrganizationRequest request, string username, bool fallbackToExistingUser = true) + public TResponse Execute(OrganizationRequest request, string username, bool fallbackToExistingUser = true, bool logErrors = true) where TResponse : OrganizationResponse { if (request is null) @@ -290,7 +322,7 @@ public TResponse Execute(OrganizationRequest request, string username { this.logger.LogWarning($"Failed to execute {request.RequestName} as {username} as the user was not found."); } - else + else if (logErrors) { this.logger.LogWarning(ex, $"Failed to execute {request.RequestName} as {username}. {ex.Message}"); } @@ -342,31 +374,13 @@ public void WaitForSolutionHistoryRecordsToComplete() } /// + [Obsolete("Please use ExecuteManySolutionHistoryOperation.", true)] public IEnumerable ExecuteMultipleSolutionHistoryOperation(IEnumerable requests, string username, int? timeout = null) { - var customizationLockErrorCodes = new int[] - { - Constants.ErrorCodes.CustomizationLockExBlockingUnknown, - Constants.ErrorCodes.CustomizationLockExBothKnownDifferent, - Constants.ErrorCodes.CustomizationLockExBothKnownSame, - Constants.ErrorCodes.CustomizationLockExBlockedUnknown, - Constants.ErrorCodes.CustomizationLockExBothUnknown, - }; - var firstIndexByRequest = requests.ToDictionary(request => request, request => default(int?)); var responseByRequest = requests.ToDictionary(request => request, request => (ExecuteMultipleResponseItem)null); - var retryPolicy = Policy - .Handle() - .RetryForever(_ => this.WaitForSolutionHistoryRecordsToComplete()) - .Wrap( - Policy - .Handle() - .WaitAndRetryForever( - sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(30), - onRetry: (_, timeSpan) => this.logger.LogInformation("A solution concurrency issue has occured. Waiting for {0} seconds before retrying.", timeSpan.TotalSeconds))); - - retryPolicy.Execute(() => + this.CustomizationLockPolicy.Execute(() => { var res = string.IsNullOrEmpty(username) ? this.ExecuteMultiple(requests, true, true, timeout) @@ -384,24 +398,26 @@ public IEnumerable ExecuteMultipleSolutionHistoryOp responseByRequest[request] = response; } - if (res.IsFaulted) + if (!res.IsFaulted) { - requests = res.Responses - .Where(r => r.Fault != null) - .Select(r => requests.ElementAt(r.RequestIndex)) - .ToList(); + return; + } - var solutionConcurrencyErrors = res.Responses.Where(r => r.Fault?.ErrorCode == Constants.ErrorCodes.SolutionConcurrencyFailure); - if (solutionConcurrencyErrors.Any()) - { - throw new SolutionConcurrencyException($"{solutionConcurrencyErrors.Count()} requests failed due to solution concurrency errors."); - } + requests = res.Responses + .Where(r => r.Fault != null) + .Select(r => requests.ElementAt(r.RequestIndex)) + .ToList(); - var customizationLockErrors = res.Responses.Where(r => r.Fault != null && customizationLockErrorCodes.Contains(r.Fault.ErrorCode)); - if (customizationLockErrors.Any()) - { - throw new CustomizationLockException($"{customizationLockErrors.Count()} requests failed due to customization lock errors."); - } + var solutionConcurrencyErrors = res.Responses.Where(r => r.Fault?.ErrorCode == Constants.ErrorCodes.SolutionConcurrencyFailure); + if (solutionConcurrencyErrors.Any()) + { + throw new SolutionConcurrencyException($"{solutionConcurrencyErrors.Count()} requests failed due to solution concurrency errors."); + } + + var customizationLockErrors = res.Responses.Where(r => r.Fault != null && CustomizationLockErrorCodes.Contains(r.Fault.ErrorCode)); + if (customizationLockErrors.Any()) + { + throw new CustomizationLockException($"{customizationLockErrors.Count()} requests failed due to customization lock errors."); } }); @@ -413,6 +429,53 @@ public IEnumerable ExecuteMultipleSolutionHistoryOp return responseByRequest.Values; } + /// + public IDictionary ExecuteManySolutionHistoryOperation(IEnumerable requests, string username, Action onError = null) + { + // Errors are expected and caught and handled. Uncaught errors are handled and logged via the onError callback. + var previousTraceLevel = TraceControlSettings.TraceLevel; + TraceControlSettings.TraceLevel = System.Diagnostics.SourceLevels.Off; + + var results = requests.ToDictionary(r => r, r => + { + try + { + return this.CustomizationLockPolicy.Execute(() => + { + try + { + if (string.IsNullOrEmpty(username)) + { + return this.crmSvc.Execute(r); + } + + return this.Execute(r, username, false, false); + } + catch (FaultException ex) when (ex.Detail.ErrorCode == Constants.ErrorCodes.SolutionConcurrencyFailure) + { + // Policy will handle this exception. + throw new SolutionConcurrencyException($"Request failed due to solution concurrency errors."); + } + catch (FaultException ex) when (CustomizationLockErrorCodes.Contains(ex.Detail.ErrorCode)) + { + // Policy will handle this exception. + throw new CustomizationLockException($"Request failed due to customization lock errors."); + } + }); + } + catch (Exception ex) + { + onError(r, ex); + } + + return null; + }); + + TraceControlSettings.TraceLevel = previousTraceLevel; + + return results; + } + /// public bool UpdateStateAndStatusForEntity(string entityLogicalName, Guid entityId, int statecode, int status) => this.crmSvc.UpdateStateAndStatusForEntity(entityLogicalName, entityId, statecode, status); diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/ICrmServiceAdapter.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/ICrmServiceAdapter.cs index 0b9b1df..9b0320b 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/ICrmServiceAdapter.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/ICrmServiceAdapter.cs @@ -90,10 +90,11 @@ public interface ICrmServiceAdapter : IOrganizationService /// The request. /// The user to impersonate. /// Whether to fallback to the authenticated user if the action fails as the specified user. + /// Whether to log errors. /// The type of response. /// The response. /// Thrown when the specified user doesn't exist and fallback is disabled. - public TResponse Execute(OrganizationRequest request, string username, bool fallbackToExistingUser = true) + public TResponse Execute(OrganizationRequest request, string username, bool fallbackToExistingUser = true, bool logErrors = true) where TResponse : OrganizationResponse; /// @@ -134,5 +135,14 @@ public TResponse Execute(OrganizationRequest request, string username /// Timeout in seconds. /// Returns an . IEnumerable ExecuteMultipleSolutionHistoryOperation(IEnumerable requests, string username, int? timeout = null); + + /// + /// Executes multiple requests individually and performs a check on the Solution History during the operation. + /// + /// The collection of to execute. + /// The user to impersonate. + /// An action to be called for each errored request. + /// A dictionary of responses keyed by request. + IDictionary ExecuteManySolutionHistoryOperation(IEnumerable requests, string username, Action onError = null); } } diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs index 9fffdbe..cc208e0 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs @@ -107,9 +107,14 @@ public static class Fields public const string Name = "name"; /// - /// The name of the process. + /// The state code. /// public const string StateCode = "statecode"; + + /// + /// The status code. + /// + public const string StatusCode = "statuscode"; } } diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs index 24254a1..6651532 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs @@ -3,10 +3,11 @@ 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; /// @@ -103,78 +104,95 @@ private void SetStates(IEnumerable processes, IEnumerable proces this.logger.LogInformation($"Activating processes as {user}."); } - var requests = this.GetSetStateRequests(processes, processesToDeactivate); + var requests = this.GetRequestByProcess(processes, processesToDeactivate); if (!requests.Any()) { return; } - this.ExecuteSetStateRequests(requests, user); + this.ExecuteUpdateRequests(requests, user); } - private void ExecuteSetStateRequests(IEnumerable requests, string user = null) + private void ExecuteUpdateRequests(IDictionary requestsByProcess, 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, failedResponses; + var nameMap = requestsByProcess.Keys + .ToDictionary(e => e.Id, e => e.GetAttributeValue(Constants.Workflow.Fields.Name)); + var remainingRequests = requestsByProcess.Values.Where(r => r != null); + if (!remainingRequests.Any()) + { + return; + } + + this.logger.LogInformation($"Updating the states of {remainingRequests.Count()} processes."); + + var iteration = 1; + var iterationSuccessfulRequestCount = 0; + var errorMessages = new List(); do { - var timeout = 120 + (remainingRequests.Count * 10); - var executeMultipleResponses = this.crmSvc - .ExecuteMultipleSolutionHistoryOperation(remainingRequests, user, timeout); + errorMessages = new List(); + var responses = this.crmSvc.ExecuteManySolutionHistoryOperation( + remainingRequests, + user, + (r, ex) => + { + errorMessages.Add($"Failed to update status of process {nameMap[((UpdateRequest)r).Target.Id]} with the following error: {((FaultException)ex).Detail.Message}"); + }); + + remainingRequests = responses + .Where(kvp => kvp.Value is null) + .Select(kvp => kvp.Key) + .Cast(); - successfulResponses = executeMultipleResponses.Where(r => r.Fault == null); - failedResponses = executeMultipleResponses.Except(successfulResponses); - remainingRequests = failedResponses.Select(r => remainingRequests[r.RequestIndex]).ToList(); + iterationSuccessfulRequestCount = responses.Values.Where(v => v != null).Count(); + this.logger.LogInformation($"Successfully updated the state of {iterationSuccessfulRequestCount} processes in iteration {iteration}."); + iteration++; } - while (successfulResponses.Any() && remainingRequests.Any()); + while (remainingRequests.Any() && iterationSuccessfulRequestCount > 0); - if (remainingRequests.Any()) + foreach (var errorMessage in errorMessages) { - foreach (var failedResponse in failedResponses) - { - 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}."); - } + this.logger.LogError(errorMessage); } } - private List GetSetStateRequests(IEnumerable processes, IEnumerable processesToDeactivate) + private IDictionary GetRequestByProcess(IEnumerable processes, IEnumerable processesToDeactivate) { - var requests = new List(); - - foreach (var deployedProcess in processes) - { - var stateCode = new OptionSetValue(Constants.Workflow.StateCodeActive); - var statusCode = new OptionSetValue(Constants.Workflow.StatusCodeActive); - - if (processesToDeactivate != null && processesToDeactivate.Contains(deployedProcess[Constants.Workflow.Fields.Name])) + return processes.ToDictionary( + p => p, + p => { - stateCode.Value = Constants.Workflow.StateCodeInactive; - statusCode.Value = Constants.Workflow.StatusCodeInactive; - } + var stateCode = new OptionSetValue(Constants.Workflow.StateCodeActive); + var statusCode = new OptionSetValue(Constants.Workflow.StatusCodeActive); - if (stateCode.Value == deployedProcess.GetAttributeValue(Constants.Workflow.Fields.StateCode).Value) - { - this.logger.LogInformation($"Process {deployedProcess[Constants.Workflow.Fields.Name]} already has desired state. Skipping."); - continue; - } - - this.logger.LogInformation($"Setting process status for {deployedProcess[Constants.Workflow.Fields.Name]} with statecode {stateCode.Value} and statuscode {statusCode.Value}"); + if (processesToDeactivate != null && processesToDeactivate.Contains(p[Constants.Workflow.Fields.Name])) + { + stateCode.Value = Constants.Workflow.StateCodeInactive; + statusCode.Value = Constants.Workflow.StatusCodeInactive; + } - // SetStateRequest is supposedly deprecated but UpdateRequest doesn't work for deactivating active flows - requests.Add( - new SetStateRequest + if (stateCode.Value == p.GetAttributeValue(Constants.Workflow.Fields.StateCode).Value) { - EntityMoniker = deployedProcess.ToEntityReference(), - State = stateCode, - Status = statusCode, - }); - } + this.logger.LogInformation($"Process {p[Constants.Workflow.Fields.Name]} will be skipped. Already has desired state."); + return null; + } - return requests; + this.logger.LogInformation($"Process {p[Constants.Workflow.Fields.Name]} will be {(stateCode.Value == Constants.Workflow.StateCodeActive ? "activated" : "deactivated")}."); + + return new UpdateRequest + { + Target = new Entity(Constants.Workflow.LogicalName, p.Id) + { + Attributes = + { + [Constants.Workflow.Fields.StateCode] = stateCode, + [Constants.Workflow.Fields.StatusCode] = statusCode, + }, + }, + }; + }); } private EntityCollection RetrieveProcesses(IEnumerable names) diff --git a/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ConnectionReferenceDeploymentServiceTests.cs b/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ConnectionReferenceDeploymentServiceTests.cs index 495c302..5872fd3 100644 --- a/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ConnectionReferenceDeploymentServiceTests.cs +++ b/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ConnectionReferenceDeploymentServiceTests.cs @@ -79,7 +79,7 @@ public void ConnectConnectionReferences_WithConnectionOwner_UpdatesAsConnectionO this.connectionReferenceSvc.ConnectConnectionReferences(connectionMap, connectionOwner); - this.crmSvc.Verify(svc => svc.Execute(It.IsAny(), connectionOwner, true)); + this.crmSvc.Verify(svc => svc.Execute(It.IsAny(), connectionOwner, It.IsAny(), It.IsAny())); } [Fact] @@ -119,8 +119,12 @@ public void ConnectConnectionReferences_WithErrorUpdating_Continues() private void MockUpdateConnectionReferencesResponse(ExecuteMultipleResponse response) { - this.crmSvc.Setup(svc => svc.Execute(It.IsAny())).Returns(response); - this.crmSvc.Setup(svc => svc.Execute(It.IsAny(), It.IsAny(), true)).Returns(response); + this.crmSvc + .Setup(svc => svc.Execute(It.IsAny())) + .Returns(response); + this.crmSvc + .Setup(svc => svc.Execute(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(response); } private EntityCollection MockConnectionReferencesForConnectionMap(Dictionary connectionMap) diff --git a/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs b/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs index d7c22bc..ac8a070 100644 --- a/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs +++ b/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs @@ -4,10 +4,10 @@ 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,18 +61,18 @@ public void SetStatesBySolution_ProcessInComponentsToDeactivateList_DeactivatesP { var solutionProcesses = new List { GetProcess(Constants.Workflow.StateCodeActive) }; this.MockBySolutionProcesses(solutionProcesses); - this.MockExecuteMultipleSolutionHistoryOperationResponse( + this.MockExecuteManySolutionHistoryOperationResponse( null, - svc => svc.ExecuteMultipleSolutionHistoryOperation( + svc => svc.ExecuteManySolutionHistoryOperation( It.Is>( - reqs => reqs.Cast().Any( + 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)), + req.Target.LogicalName == Constants.Workflow.LogicalName && + req.Target.Id == solutionProcesses.First().Id && + req.Target.GetAttributeValue(Constants.Workflow.Fields.StateCode).Value == Constants.Workflow.StateCodeInactive && + req.Target.GetAttributeValue(Constants.Workflow.Fields.StatusCode).Value == Constants.Workflow.StatusCodeInactive)), It.IsAny(), - It.IsAny()), + It.IsAny>()), true); this.processDeploymentSvc.SetStatesBySolution( @@ -94,12 +94,12 @@ public void SetStatesBySolution_WithUserParameter_ExecutesAsUser() GetProcess(Constants.Workflow.StateCodeInactive), }; this.MockBySolutionProcesses(solutionProcesses); - this.MockExecuteMultipleSolutionHistoryOperationResponse( + this.MockExecuteManySolutionHistoryOperationResponse( null, - svc => svc.ExecuteMultipleSolutionHistoryOperation( + svc => svc.ExecuteManySolutionHistoryOperation( It.IsAny>(), userToImpersonate, - It.IsAny())); + It.IsAny>())); this.processDeploymentSvc.SetStatesBySolution( Solutions, user: userToImpersonate); @@ -162,7 +162,7 @@ public void SetStates_ProcessInComponentsToActivateFound_ActivatesProcess() { var foundProcesses = new List { GetProcess(Constants.Workflow.StateCodeInactive) }; this.MockSetStatesProcesses(foundProcesses); - this.MockExecuteMultipleSolutionHistoryOperationResponse(); + this.MockExecuteManySolutionHistoryOperationResponse(); this.processDeploymentSvc.SetStates(new List { @@ -177,18 +177,18 @@ public void SetStates_ProcessInComponentsToDeactivateFound_DectivatesProcess() { var foundProcesses = new List { GetProcess(Constants.Workflow.StateCodeActive) }; this.MockSetStatesProcesses(foundProcesses); - this.MockExecuteMultipleSolutionHistoryOperationResponse( + this.MockExecuteManySolutionHistoryOperationResponse( null, - svc => svc.ExecuteMultipleSolutionHistoryOperation( + svc => svc.ExecuteManySolutionHistoryOperation( It.Is>( - reqs => reqs.Cast().Any( + 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)), + req.Target.LogicalName == Constants.Workflow.LogicalName && + req.Target.Id == foundProcesses.First().Id && + req.Target.GetAttributeValue(Constants.Workflow.Fields.StateCode).Value == Constants.Workflow.StateCodeInactive && + req.Target.GetAttributeValue(Constants.Workflow.Fields.StatusCode).Value == Constants.Workflow.StatusCodeInactive)), It.IsAny(), - It.IsAny()), + It.IsAny>()), true); this.processDeploymentSvc.SetStates(Enumerable.Empty(), new List @@ -205,12 +205,12 @@ public void SetStates_WithUserParameter_ExecutesAsUser() var foundProcesses = new List { GetProcess(Constants.Workflow.StateCodeInactive) }; this.MockSetStatesProcesses(foundProcesses); var userToImpersonate = "licenseduser@domaincom"; - this.MockExecuteMultipleSolutionHistoryOperationResponse( + this.MockExecuteManySolutionHistoryOperationResponse( null, - svc => svc.ExecuteMultipleSolutionHistoryOperation( + svc => svc.ExecuteManySolutionHistoryOperation( It.IsAny>(), userToImpersonate, - It.IsAny()), + It.IsAny>()), true); this.processDeploymentSvc.SetStates( @@ -230,16 +230,21 @@ 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 - { - { "Responses", new ExecuteMultipleResponseItemCollection() }, - { "IsFaulted", true }, - }, - }; - response.Responses.Add(new ExecuteMultipleResponseItem { Fault = fault }); - this.MockExecuteMultipleSolutionHistoryOperationResponse(response.Responses); + this.crmServiceAdapterMock + .Setup(svc => svc.ExecuteManySolutionHistoryOperation( + It.IsAny>(), + It.IsAny(), + It.IsAny>())) + .Callback, string, Action>( + (requests, username, onError) => + { + foreach (var request in requests) + { + onError(request, new FaultException(fault)); + } + }) + .Returns, string, Action>( + (requests, username, onError) => requests.ToDictionary(r => r, r => (OrganizationResponse)null)); this.processDeploymentSvc.SetStates( new List @@ -292,33 +297,27 @@ private void MockBySolutionProcesses(IList processes) .Returns(new EntityCollection(processes)); } - private void MockExecuteMultipleSolutionHistoryOperationResponse( - IEnumerable responses = null, - Expression>> expression = null, + private void MockExecuteManySolutionHistoryOperationResponse( + IDictionary response = null, + Expression>> expression = null, bool verifiable = false) { if (expression == null) { - expression = svc => svc.ExecuteMultipleSolutionHistoryOperation( + expression = svc => svc.ExecuteManySolutionHistoryOperation( It.IsAny>(), It.IsAny(), - It.IsAny()); + It.IsAny>()); } - if (responses == null) + if (response == null) { - var executeMultipleResponse = new ExecuteMultipleResponse(); - executeMultipleResponse.Results["Responses"] = new ExecuteMultipleResponseItemCollection() - { - new ExecuteMultipleResponseItem() { RequestIndex = 0 }, - new ExecuteMultipleResponseItem() { RequestIndex = 1 }, - }; - responses = executeMultipleResponse.Responses; + response = new Dictionary(); } var returnResult = this.crmServiceAdapterMock .Setup(expression) - .Returns(responses); + .Returns(response); if (verifiable) {