Skip to content
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 @@ -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;
Expand All @@ -17,9 +20,20 @@
/// </summary>
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;

/// <summary>
/// Initializes a new instance of the <see cref="CrmServiceAdapter"/> class.
/// </summary>
Expand All @@ -34,6 +48,24 @@ public CrmServiceAdapter(CrmServiceClient crmSvc, ILogger logger)
/// <inheritdoc/>
public Guid? CallerAADObjectId { get => this.crmSvc.CallerAADObjectId; set => this.crmSvc.CallerAADObjectId = value; }

private Policy CustomizationLockPolicy
{
get
{
this.customizationLockPolicy ??= Policy
.Handle<CustomizationLockException>()
.RetryForever(_ => this.WaitForSolutionHistoryRecordsToComplete())
.Wrap(
Policy
.Handle<SolutionConcurrencyException>()
.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;
}
}

/// <inheritdoc/>
public ExecuteMultipleResponse ExecuteMultiple(IEnumerable<OrganizationRequest> requests, bool continueOnError = true, bool returnResponses = true, int? timeout = null)
{
Expand Down Expand Up @@ -262,7 +294,7 @@ public string GetEntityTypeCode(string entityLogicalName)
}

/// <inheritdoc/>
public TResponse Execute<TResponse>(OrganizationRequest request, string username, bool fallbackToExistingUser = true)
public TResponse Execute<TResponse>(OrganizationRequest request, string username, bool fallbackToExistingUser = true, bool logErrors = true)
where TResponse : OrganizationResponse
{
if (request is null)
Expand Down Expand Up @@ -290,7 +322,7 @@ public TResponse Execute<TResponse>(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}");
}
Expand Down Expand Up @@ -342,31 +374,13 @@ public void WaitForSolutionHistoryRecordsToComplete()
}

/// <inheritdoc/>
[Obsolete("Please use ExecuteManySolutionHistoryOperation.", true)]
public IEnumerable<ExecuteMultipleResponseItem> ExecuteMultipleSolutionHistoryOperation(IEnumerable<OrganizationRequest> 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<CustomizationLockException>()
.RetryForever(_ => this.WaitForSolutionHistoryRecordsToComplete())
.Wrap(
Policy
.Handle<SolutionConcurrencyException>()
.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)
Expand All @@ -384,24 +398,26 @@ public IEnumerable<ExecuteMultipleResponseItem> 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.");
}
});

Expand All @@ -413,6 +429,53 @@ public IEnumerable<ExecuteMultipleResponseItem> ExecuteMultipleSolutionHistoryOp
return responseByRequest.Values;
}

/// <inheritdoc/>
public IDictionary<OrganizationRequest, OrganizationResponse> ExecuteManySolutionHistoryOperation(IEnumerable<OrganizationRequest> requests, string username, Action<OrganizationRequest, Exception> 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<OrganizationResponse>(r, username, false, false);
}
catch (FaultException<OrganizationServiceFault> 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<OrganizationServiceFault> 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;
}

/// <inheritdoc/>
public bool UpdateStateAndStatusForEntity(string entityLogicalName, Guid entityId, int statecode, int status) => this.crmSvc.UpdateStateAndStatusForEntity(entityLogicalName, entityId, statecode, status);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,11 @@ public interface ICrmServiceAdapter : IOrganizationService
/// <param name="request">The request.</param>
/// <param name="username">The user to impersonate.</param>
/// <param name="fallbackToExistingUser">Whether to fallback to the authenticated user if the action fails as the specified user.</param>
/// <param name="logErrors">Whether to log errors.</param>
/// <typeparam name="TResponse">The type of response.</typeparam>
/// <returns>The response.</returns>
/// <exception cref="ArgumentException">Thrown when the specified user doesn't exist and fallback is disabled.</exception>
public TResponse Execute<TResponse>(OrganizationRequest request, string username, bool fallbackToExistingUser = true)
public TResponse Execute<TResponse>(OrganizationRequest request, string username, bool fallbackToExistingUser = true, bool logErrors = true)
where TResponse : OrganizationResponse;

/// <summary>
Expand Down Expand Up @@ -134,5 +135,14 @@ public TResponse Execute<TResponse>(OrganizationRequest request, string username
/// <param name="timeout">Timeout in seconds.</param>
/// <returns>Returns an <see cref="ExecuteMultipleResponseItem"/>.</returns>
IEnumerable<ExecuteMultipleResponseItem> ExecuteMultipleSolutionHistoryOperation(IEnumerable<OrganizationRequest> requests, string username, int? timeout = null);

/// <summary>
/// Executes multiple requests individually and performs a check on the Solution History during the operation.
/// </summary>
/// <param name="requests">The collection of <see cref="OrganizationRequest"/> to execute.</param>
/// <param name="username">The user to impersonate.</param>
/// <param name="onError">An action to be called for each errored request.</param>
/// <returns>A dictionary of responses keyed by request.</returns>
IDictionary<OrganizationRequest, OrganizationResponse> ExecuteManySolutionHistoryOperation(IEnumerable<OrganizationRequest> requests, string username, Action<OrganizationRequest, Exception> onError = null);
}
}
7 changes: 6 additions & 1 deletion src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,14 @@ public static class Fields
public const string Name = "name";

/// <summary>
/// The name of the process.
/// The state code.
/// </summary>
public const string StateCode = "statecode";

/// <summary>
/// The status code.
/// </summary>
public const string StatusCode = "statuscode";
}
}

Expand Down
Loading