diff --git a/v9.2/Client/Enums.cs b/v9.2/Client/Enums.cs new file mode 100644 index 0000000..dacf6a6 --- /dev/null +++ b/v9.2/Client/Enums.cs @@ -0,0 +1,16 @@ +namespace Microsoft.Pfe.Xrm +{ + public enum CrmOnlineRegion + { + NA, + EMEA, + APAC + } + + public enum XrmServiceType + { + Organization, + OrganizationWeb, + OrganizationData + } +} diff --git a/v9.2/Client/ILocalResults.cs b/v9.2/Client/ILocalResults.cs new file mode 100644 index 0000000..fa55ccc --- /dev/null +++ b/v9.2/Client/ILocalResults.cs @@ -0,0 +1,31 @@ +/*================================================================================================================================ + + This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. + + THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + + We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object + code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software + product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the + Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims + or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code. + + =================================================================================================================================*/ +namespace Microsoft.Pfe.Xrm +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.ServiceModel; + using System.Text; + + using Microsoft.Xrm.Sdk; + + public interface ILocalResults + where TFailure : IParallelOperationFailure + { + IList Results { get; } + IList Failures { get; } + } +} diff --git a/v9.2/Client/ParallelOperationContext.cs b/v9.2/Client/ParallelOperationContext.cs new file mode 100644 index 0000000..9106782 --- /dev/null +++ b/v9.2/Client/ParallelOperationContext.cs @@ -0,0 +1,90 @@ +/*================================================================================================================================ + + This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. + + THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + + We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object + code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software + product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the + Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims + or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code. + + =================================================================================================================================*/ +namespace Microsoft.Pfe.Xrm +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.ServiceModel; + using System.ServiceModel.Description; + using System.Text; + using Microsoft.PowerPlatform.Dataverse.Client; + using Microsoft.Xrm.Sdk; + using Microsoft.Xrm.Sdk.Client; + + /// + /// A parallel operation context object that maintains a reference to a Organization.svc channel + /// + /// The expected response type to collect + /// + /// ASSUMPTION: The local reference temporarily points to a threadlocal instance shared across partitions, thus we do not dispose from the context directly + /// + internal sealed class ParallelOrganizationOperationContext : ParallelOperationContext> + { + public ParallelOrganizationOperationContext() { } + + public ParallelOrganizationOperationContext(ServiceClient proxy) + : base(proxy) { } + } + + /// + /// A context object can be passed between iterations of a parallelized process partition + /// Maintains a reference to a ThreadLocal.Value and + /// implements ILocalResults for collecting partitioned results in parallel operations + /// + /// The expected response type to collect + internal class ParallelOperationContext : ILocalResults + where TFailure : IParallelOperationFailure + { + protected ParallelOperationContext() { } + + public ParallelOperationContext(TLocal local) { this.Local = local; } + + public TLocal Local { get; set; } + + #region ILocalResults Members + + private IList results; + public IList Results + { + get + { + if (this.results == null) + { + this.results = new List(); + } + + return this.results; + } + } + + private IList failures; + + public IList Failures + { + get + { + if (this.failures == null) + { + this.failures = new List(); + } + + return this.failures; + } + } + + #endregion + } +} diff --git a/v9.2/Client/ParallelOperationFailure.cs b/v9.2/Client/ParallelOperationFailure.cs new file mode 100644 index 0000000..d3d4d57 --- /dev/null +++ b/v9.2/Client/ParallelOperationFailure.cs @@ -0,0 +1,55 @@ +/*================================================================================================================================ + + This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. + + THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + + We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object + code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software + product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the + Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims + or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code. + + =================================================================================================================================*/ +namespace Microsoft.Pfe.Xrm +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.ServiceModel; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Xrm.Sdk; + + + /// + /// Represents an operation failure during parallel execution + /// + /// The originating request type + internal class ParallelOrganizationOperationFailure : ParallelOperationFailure + { + public ParallelOrganizationOperationFailure(TRequest request, FaultException fault) + : base(request, fault) { } + } + + /// + /// Represents a service operation failure during parallel execution + /// + /// The originating request type + /// The fault type representing the failure event + internal class ParallelOperationFailure : IParallelOperationFailure + where TFault : BaseServiceFault + { + public ParallelOperationFailure(TRequest request, FaultException exception) + { + this.Request = request; + this.Exception = exception; + } + + public TRequest Request { get; set; } + public FaultException Exception { get; set; } + } + + public interface IParallelOperationFailure { } +} diff --git a/v9.2/Client/ParallelServiceProxy.cs b/v9.2/Client/ParallelServiceProxy.cs new file mode 100644 index 0000000..3fa0a35 --- /dev/null +++ b/v9.2/Client/ParallelServiceProxy.cs @@ -0,0 +1,884 @@ +/*================================================================================================================================ + + This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. + + THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + + We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object + code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software + product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the + Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims + or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code. + + =================================================================================================================================*/ +namespace Microsoft.Pfe.Xrm +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using System.ServiceModel; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Pfe.Xrm.Diagnostics; + using Microsoft.PowerPlatform.Dataverse.Client; + using Microsoft.Xrm.Sdk; + using Microsoft.Xrm.Sdk.Client; + using Microsoft.Xrm.Sdk.Messages; + using Microsoft.Xrm.Sdk.Query; + + /// + /// Class for executing concurrent operations + /// + /// + /// During parallel operations, instances are aligned with individual data partitions + /// using a thread local class variable to avoid service channel thread-safety issues. + /// + public class ParallelOrganizationServiceProxy : ParallelServiceProxy + { + + #region Constructor(s) + + public ParallelOrganizationServiceProxy(OrganizationServiceManager serviceManager) + : base(serviceManager) { } + + public ParallelOrganizationServiceProxy(OrganizationServiceManager serviceManager, int maxDegreeOfParallelism) + : base(serviceManager, maxDegreeOfParallelism) { } + + #endregion + + #region Properties + + #endregion + + #region Multi-threaded IOrganizationService Operation Methods + + #region IOrganizationService.Create() + + /// + /// Performs data parallelism on a keyed collection of type to execute .Create() requests concurrently + /// + /// The keyed collection target entities to be created + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A keyed collection of unique identifiers for each created + /// callers should catch to handle exceptions raised by individual requests + /// + /// Only returning generated unique identifier because it's assumed that requesting process will maintain a reference + /// between key and the instance submitted as the Create target to which the unique identifier can be correlated + /// + public IDictionary Create(IDictionary targets, Action, FaultException> errorHandler = null) + { + return this.Create(targets, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a keyed collection of type to execute .Create() requests concurrently + /// + /// The keyed collection target entities to be created + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A keyed collection of unique identifiers for each created + /// callers should catch to handle exceptions raised by individual requests + /// + /// Only returning generated unique identifier because it's assumed that requesting process will maintain a reference + /// between key and the instance submitted as the Create target to which the unique identifier can be correlated + /// + public IDictionary Create(IDictionary targets, OrganizationServiceProxyOptions options, Action, FaultException> errorHandler = null) + { + return this.ExecuteOperationWithResponse, KeyValuePair>(targets, options, + (target, context) => + { + Guid id = context.Local.Create(target.Value); //Hydrate target with response Id + + //Collect the result from each iteration in this partition + context.Results.Add(new KeyValuePair(target.Key, id)); + }, + errorHandler) + .ToDictionary(t => t.Key, t => t.Value); + } + + /// + /// Performs data parallelism on a collection of type to execute .Create() requests concurrently + /// + /// The target entities to be created + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// The collection of created records, hydrated with the response Id + /// callers should catch to handle exceptions raised by individual requests + /// + /// By returning the original collection of target entities with Ids, this allows for concurrent creation of multiple entity types + /// and ability to cross-reference submitted data with the plaftorm generated Id. Note that subsequent Update requests should + /// always instantiate a new instance and assign the Id. + /// + public IEnumerable Create(IEnumerable targets, Action> errorHandler = null) + { + return this.Create(targets, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a collection of type to execute .Create() requests concurrently + /// + /// The target entities to be created + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// The collection of created records, hydrated with the response Id + /// callers should catch to handle exceptions raised by individual requests + /// + /// By returning the original collection of target entities with Ids, this allows for concurrent creation of multiple entity types + /// and ability to cross-reference submitted data with the plaftorm generated Id. Note that subsequent Update requests should + /// always instantiate a new instance and assign the Id. + /// + public IEnumerable Create(IEnumerable targets, OrganizationServiceProxyOptions options, Action> errorHandler = null) + { + return this.ExecuteOperationWithResponse(targets, options, + (target, context) => + { + target.Id = context.Local.Create(target); //Hydrate target with response Id + + //Collect the result from each iteration in this partition + context.Results.Add(target); + }, + errorHandler); + } + + #endregion + + #region IOrganizationService.Update() + + /// + /// Performs data parallelism on a list of type to execute .Update() requests concurrently + /// + /// The target entities to be updated + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// callers should catch to handle exceptions raised by individual requests + public void Update(IEnumerable targets, Action> errorHandler = null) + { + this.Update(targets, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a list of type to execute .Update() requests concurrently + /// + /// The target entities to be updated + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// callers should catch to handle exceptions raised by individual requests + public void Update(IEnumerable targets, OrganizationServiceProxyOptions options, Action> errorHandler = null) + { + this.ExecuteOperation(targets, options, + (target, proxy) => + { + proxy.Update(target); + }, + errorHandler); + } + + #endregion + + #region IOrganizationService.Delete() + + /// + /// Performs data parallelism on a list of type to execute .Delete() requests concurrently + /// + /// The target entities to be updated + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// callers should catch to handle exceptions raised by individual requests + public void Delete(IEnumerable targets, Action> errorHandler = null) + { + this.Delete(targets, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a list of type to execute .Delete() requests concurrently + /// + /// The target entities to be deleted + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// callers should catch to handle exceptions raised by individual requests + public void Delete(IEnumerable targets, OrganizationServiceProxyOptions options, Action> errorHandler = null) + { + this.ExecuteOperation(targets, options, + (target, proxy) => + { + proxy.Delete(target.LogicalName, target.Id); + }, + errorHandler); + } + + #endregion + + #region IOrganizationService.Associate() + + /// + /// Performs data parallelism on a list of type to execute .Associate() requests concurrently + /// + /// The collection defining the entity, relationship, and entities to associate + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// callers should catch to handle exceptions raised by individual requests + public void Associate(IEnumerable requests, Action> errorHandler = null) + { + this.Associate(requests, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a list of type to execute .Associate() requests concurrently + /// + /// The collection defining the entity, relationship, and entities to associate + /// The configurable options for the parallel OrganizationServiceProxy requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// callers should catch to handle exceptions raised by individual requests + public void Associate(IEnumerable requests, OrganizationServiceProxyOptions options, Action> errorHandler = null) + { + this.ExecuteOperation(requests, options, + (request, proxy) => + { + proxy.Associate(request.Target.LogicalName, request.Target.Id, request.Relationship, request.RelatedEntities); + }, + errorHandler); + } + + #endregion + + #region IOrganizationService.Disassociate() + + /// + /// Performs data parallelism on a list of type to execute .Disassociate() requests concurrently + /// + /// The collection defining the entity, relationship, and entities to disassociate + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// callers should catch to handle exceptions raised by individual requests + public void Disassociate(IEnumerable requests, Action> errorHandler = null) + { + this.Disassociate(requests, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a list of type to execute .Disassociate() requests concurrently + /// + /// The collection defining the entity, relationship, and entities to disassociate + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// callers should catch to handle exceptions raised by individual requests + public void Disassociate(IEnumerable requests, OrganizationServiceProxyOptions options, Action> errorHandler = null) + { + this.ExecuteOperation(requests, options, + (request, proxy) => + { + proxy.Disassociate(request.Target.LogicalName, request.Target.Id, request.Relationship, request.RelatedEntities); + }, + errorHandler); + } + + #endregion + + #region IOrganizationService.Retrieve() + + /// + /// Performs data parallelism on a collection of type to execute .Retrieve() requests concurrently + /// + /// The collection defining the entities to be retrieved + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A collection of type containing the retrieved entities + /// + /// IMPORTANT!! RetrieveMultiple is the favored approach for retrieving multiple entities of the same type + /// This approach should only be used if trying to retrieve multiple individual records of varying entity types. + /// + /// callers should catch to handle exceptions raised by individual requests + public IEnumerable Retrieve(IEnumerable requests, Action> errorHandler = null) + { + return this.Retrieve(requests, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a collection of type to execute .Retrieve() requests concurrently + /// + /// The collection defining the entities to be retrieved + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A collection of type containing the retrieved entities + /// + /// IMPORTANT!! RetrieveMultiple is the favored approach for retrieving multiple entities of the same type + /// This approach should only be used if trying to retrieve multiple individual records of varying entity types. + /// + /// callers should catch to handle exceptions raised by individual requests + public IEnumerable Retrieve(IEnumerable requests, OrganizationServiceProxyOptions options, Action> errorHandler = null) + { + return this.ExecuteOperationWithResponse(requests, options, + (request, context) => + { + var entity = context.Local.Retrieve(request.Target.LogicalName, request.Target.Id, request.ColumnSet); + + //Collect the result from each iteration in this partition + context.Results.Add(entity); + }, + errorHandler); + } + + #endregion + + #region IOrganizationService.RetrieveMultiple() + + /// + /// Performs data parallelism on a keyed collection of values to execute .RetrieveMultiple() requests concurrently + /// + /// The keyed collection of queries ( or ) + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A keyed collection of values which represent the results of each query + /// + /// Assumes that only the first page of results is desired (shouldRetrieveAllPages: false) + /// Assumes default proxy options should be used (options: new ()). + /// + /// IMPORTANT!! This approach should only be used if multiple queries for varying entity types are required or the result set can't be expressed in a single query. In the latter case, + /// leverage NoLock=true where possible to reduce database contention. + /// + /// callers should catch to handle exceptions raised by individual requests + public IDictionary RetrieveMultiple(IDictionary queries, Action, FaultException> errorHandler = null) + { + return this.RetrieveMultiple(queries, false, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a keyed collection of values to execute .RetrieveMultiple() requests concurrently + /// + /// The keyed collection of queries ( or ) + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A keyed collection of values which represent the results of each query + /// + /// Assumes that only the first page of results is desired (shouldRetrieveAllPages: false) + /// + /// IMPORTANT!! This approach should only be used if multiple queries for varying entity types are required or the result set can't be expressed in a single query. In the latter case, + /// leverage NoLock=true where possible to reduce database contention. + /// + /// callers should catch to handle exceptions raised by individual requests + public IDictionary RetrieveMultiple(IDictionary queries, OrganizationServiceProxyOptions options, Action, FaultException> errorHandler = null) + { + return this.RetrieveMultiple(queries, false, options, errorHandler); + } + + /// + /// Performs data parallelism on a keyed collection of values to execute .RetrieveMultiple() requests concurrently + /// + /// The keyed collection of queries ( or ) + /// True = iterative requests will be performed to retrieve all pages, otherwise only the first results page will be returned for each query + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A keyed collection of values which represent the results of each query + /// + /// Assumes default proxy options should be used (options: new ()). + /// + /// IMPORTANT!! This approach should only be used if multiple queries for varying entity types are required or the result set can't be expressed in a single query. In the latter case, + /// leverage NoLock=true where possible to reduce database contention. + /// + /// callers should catch to handle exceptions raised by individual requests + public IDictionary RetrieveMultiple(IDictionary queries, bool shouldRetrieveAllPages, Action, FaultException> errorHandler = null) + { + return this.RetrieveMultiple(queries, shouldRetrieveAllPages, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a keyed collection of values to execute .RetrieveMultiple() requests concurrently + /// + /// The keyed collection of queries ( or ) + /// True = iterative requests will be performed to retrieve all pages, otherwise only the first results page will be returned for each query + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A keyed collection of values which represent the results of each query + /// + /// IMPORTANT!! This approach should only be used if multiple queries for varying entity types are required or the result set can't be expressed in a single query. In the latter case, + /// leverage NoLock=true where possible to reduce database contention. + /// + /// callers should catch to handle exceptions raised by individual requests + public IDictionary RetrieveMultiple(IDictionary queries, bool shouldRetrieveAllPages, OrganizationServiceProxyOptions options, Action, FaultException> errorHandler = null) + { + return this.ExecuteOperationWithResponse, KeyValuePair>(queries, options, + (query, context) => + { + var result = context.Local.RetrieveMultiple(query.Value, shouldRetrieveAllPages); + + context.Results.Add(new KeyValuePair(query.Key, result)); + }, + errorHandler) + .ToDictionary(r => r.Key, r => r.Value); + } + + #endregion + + #region IOrganizationService.Execute() + + /// + /// Performs data parallelism on a keyed collection of type to execute .Execute() requests concurrently + /// + /// The keyed collection of requests to be executed + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A keyed collection of type containing the responses to each executed request + /// callers should catch to handle exceptions raised by individual requests + public IDictionary Execute(IDictionary requests, Action, FaultException> errorHandler = null) + { + return this.Execute(requests, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a keyed collection of type to execute .Execute() requests concurrently + /// + /// The keyed collection of requests to be executed + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A keyed collection of type containing the responses to each executed request + /// callers should catch to handle exceptions raised by individual requests + public IDictionary Execute(IDictionary requests, OrganizationServiceProxyOptions options, Action, FaultException> errorHandler = null) + { + return this.ExecuteOperationWithResponse, KeyValuePair>(requests, options, + (request, context) => + { + var response = context.Local.Execute(request.Value); + + //Collect the result from each iteration in this partition + context.Results.Add(new KeyValuePair(request.Key, response)); + }, + errorHandler) + .ToDictionary(r => r.Key, r => r.Value); + } + + /// + /// Performs data parallelism on a keyed collection of type TRequest to execute .Execute() requests concurrently + /// + /// The request type that derives from + /// The response type that derives from + /// The keyed collection of requests to be executed + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A keyed collection of type TResponse containing the responses to each executed request + /// Callers should catch to handle exceptions raised by individual requests + public IDictionary Execute(IDictionary requests, Action, FaultException> errorHandler = null) + where TRequest : OrganizationRequest + where TResponse : OrganizationResponse + { + return this.Execute(requests, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a keyed collection of type TRequest to execute .Execute() requests concurrently + /// + /// The request type that derives from + /// The response type that derives from + /// The keyed collection of requests to be executed + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A keyed collection of type TResponse containing the responses to each executed request + /// Callers should catch to handle exceptions raised by individual requests + public IDictionary Execute(IDictionary requests, OrganizationServiceProxyOptions options, Action, FaultException> errorHandler = null) + where TRequest : OrganizationRequest + where TResponse : OrganizationResponse + { + return this.ExecuteOperationWithResponse, KeyValuePair>(requests, options, + (request, context) => + { + var response = (TResponse)context.Local.Execute(request.Value); + + context.Results.Add(new KeyValuePair(request.Key, response)); + }, + errorHandler) + .ToDictionary(r => r.Key, r => r.Value); + } + + /// + /// Performs data parallelism on a collection of type to execute .Execute() requests concurrently + /// + /// The requests to be executed + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A collection of type containing the responses to each executed request + /// Callers should catch to handle exceptions raised by individual requests + public IEnumerable Execute(IEnumerable requests, Action> errorHandler = null) + { + return this.Execute(requests, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a collection of type to execute .Execute() requests concurrently + /// + /// The requests to be executed + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A collection of type containing the responses to each executed request + /// Callers should catch to handle exceptions raised by individual requests + public IEnumerable Execute(IEnumerable requests, OrganizationServiceProxyOptions options, Action> errorHandler = null) + { + return this.ExecuteOperationWithResponse(requests, options, + (request, context) => + { + var response = context.Local.Execute(request); + + //Collect the result from each iteration in this partition + context.Results.Add(response); + }, + errorHandler); + } + + /// + /// Performs data parallelism on a collection of type TRequest to execute .Execute() requests concurrently + /// + /// The request type that derives from + /// The response type that derives from + /// The requests to be executed + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A collection of type TResponse containing the responses to each executed request + /// Callers should catch to handle exceptions raised by individual requests + public IEnumerable Execute(IEnumerable requests, Action> errorHandler = null) + where TRequest : OrganizationRequest + where TResponse : OrganizationResponse + { + return this.Execute(requests, new OrganizationServiceProxyOptions(), errorHandler); + } + + /// + /// Performs data parallelism on a collection of type TRequest to execute .Execute() requests concurrently + /// + /// The request type that derives from + /// The response type that derives from + /// The requests to be executed + /// The configurable options for the parallel requests + /// An optional error handling operation. Handler will be passed the request that failed along with the corresponding + /// A collection of type TResponse containing the responses to each executed request + /// Callers should catch to handle exceptions raised by individual requests + public IEnumerable Execute(IEnumerable requests, OrganizationServiceProxyOptions options, Action> errorHandler = null) + where TRequest : OrganizationRequest + where TResponse : OrganizationResponse + { + return this.ExecuteOperationWithResponse(requests, options, + (request, context) => + { + var response = (TResponse)context.Local.Execute(request); + + context.Results.Add(response); + }, + errorHandler); + } + + #endregion + + #endregion + + #region Core Parallel Execution Methods & + + /// + /// Core implementation of the parallel pattern for service operations that do not return a response (i.e. Update/Delete/Associate/Disassociate) + /// + /// The type being submitted in the service operation request + /// The collection of requests to be submitted + /// The configurable options for the requests + /// The specific operation being executed + /// The error handling operation. Handler will be passed the request that failed along with the corresponding + private void ExecuteOperation(IEnumerable requests, OrganizationServiceProxyOptions options, + Action operation, Action> errorHandler) + { + var allFailures = new ConcurrentBag>(); + + // Inline method for initializing a new organization service channel + Func proxyInit = () => + { + var proxy = this.ServiceManager.GetProxy(); + proxy.SetProxyOptions(options); + + return proxy; + }; + + using (var threadLocalProxy = new ThreadLocal(proxyInit, true)) + { + try + { + Parallel.ForEach>(requests, + new ParallelOptions() { MaxDegreeOfParallelism = this.MaxDegreeOfParallelism }, + () => { + return new ParallelOrganizationOperationContext(); + }, + (request, loopState, index, context) => + { + try + { + XrmCoreEventSource.Log.ParallelCoreOperationStart("ExecuteOperation"); + operation(request, threadLocalProxy.Value); + XrmCoreEventSource.Log.ParallelCoreOperationCompleted("ExecuteOperation"); + } + catch (FaultException fault) + { + XrmCoreEventSource.Log.LogError(fault.ToErrorMessageString().ToString()); + // Track faults locally + if (errorHandler != null) + { + context.Failures.Add(new ParallelOrganizationOperationFailure(request, fault)); + } + else + { + throw; + } + } + catch (Exception exception) + when (exception is System.TimeoutException || exception is System.Net.WebException) + { + XrmCoreEventSource.Log.LogError(exception.ToErrorMessageString().ToString()); + + var errorDetails = new ErrorDetailCollection(); + + foreach (KeyValuePair dataElement in exception.Data) + { + errorDetails.Add(dataElement); + } + + var orgFaultMock = new FaultException(new OrganizationServiceFault() + { + ErrorCode = exception.HResult, + ErrorDetails = errorDetails + }); + + // Track faults locally + if (errorHandler != null) + { + context.Failures.Add(new ParallelOrganizationOperationFailure(request, orgFaultMock)); + } + else + { + throw; + } + } + return context; + }, + (context) => + { + // Join faults together + Array.ForEach(context.Failures.ToArray(), f => allFailures.Add(f)); + }); + } + finally + { + Array.ForEach(threadLocalProxy.Values.ToArray(), p => p.Dispose()); + } + } + + // Handle faults + if (errorHandler != null) + { + foreach (var failure in allFailures) + { + errorHandler(failure.Request, failure.Exception); + } + } + } + + /// + /// Core implementation of the parallel pattern for service operations that should collect responses to each request + /// + /// The type being submitted in the service operation request + /// The response type collected from each request and returned + /// The collection of requests to be submitted + /// The configurable options for the OrganizationServiceProxy requests + /// The specific operation being executed + /// The error handling operation. Handler will be passed the request that failed along with the corresponding + /// A collection of specified response types from service operation requests + /// + /// IMPORTANT!! When defining the core operation, be sure to add responses you wish to collect via proxy.Results.Add(TResponse item); + /// + private IEnumerable ExecuteOperationWithResponse(IEnumerable requests, OrganizationServiceProxyOptions options, + Action> coreOperation, Action> errorHandler) + { + var allResponses = new ConcurrentBag(); + var allFailures = new ConcurrentBag>(); + + // Inline method for initializing a new organization service channel + Func proxyInit = () => + { + var proxy = this.ServiceManager.GetProxy(); + proxy.SetProxyOptions(options); + + return proxy; + }; + + using (var threadLocalProxy = new ThreadLocal(proxyInit, true)) + { + try + { + Parallel.ForEach>( + requests, + new ParallelOptions() { MaxDegreeOfParallelism = this.MaxDegreeOfParallelism }, + () => { + return new ParallelOrganizationOperationContext(threadLocalProxy.Value); + }, + (request, loopState, index, context) => + { + try + { + XrmCoreEventSource.Log.ParallelCoreOperationStart("ExecuteOperationWithResponse"); + coreOperation(request, context); + XrmCoreEventSource.Log.ParallelCoreOperationCompleted("ExecuteOperationWithResponse"); + } + catch (FaultException fault) + { + XrmCoreEventSource.Log.LogError(fault.ToErrorMessageString().ToString()); + // Track faults locally + if (errorHandler != null) + { + context.Failures.Add(new ParallelOrganizationOperationFailure(request, fault)); + } + else + { + throw; + } + } + catch (Exception exception) + when (exception is System.TimeoutException || exception is System.Net.WebException) + { + XrmCoreEventSource.Log.LogError(exception.ToErrorMessageString().ToString()); + var errorDetails = new ErrorDetailCollection(); + + foreach (KeyValuePair dataElement in exception.Data) + { + errorDetails.Add(dataElement); + } + + var orgFaultMock = new FaultException(new OrganizationServiceFault() + { + ErrorCode = exception.HResult, + ErrorDetails = errorDetails + }); + + // Track faults locally + if (errorHandler != null) + { + context.Failures.Add(new ParallelOrganizationOperationFailure(request, orgFaultMock)); + } + else + { + throw; + } + } + + return context; + }, + (context) => + { + // Join results and faults together + Array.ForEach(context.Results.ToArray(), r => allResponses.Add(r)); + Array.ForEach(context.Failures.ToArray(), f => allFailures.Add(f)); + + // Remove temporary reference to ThreadLocal proxy + context.Local = null; + }); + } + finally + { + Array.ForEach(threadLocalProxy.Values.ToArray(), p => p.Dispose()); + } + } + + // Handle faults + if (errorHandler != null) + { + foreach(var failure in allFailures) + { + errorHandler(failure.Request, failure.Exception); + } + } + + return allResponses; + } + + #endregion + } + + /// + /// Base class for executing concurrent requests for common operations + /// + /// The type of service manager Organization + public abstract class ParallelServiceProxy : ParallelServiceProxy + where T : XrmServiceManagerBase + { + protected static object syncRoot = new Object(); + + #region Constructor(s) + + private ParallelServiceProxy() { throw new NotImplementedException(); } + + protected ParallelServiceProxy(T serviceManager) + : this(serviceManager, ParallelServiceProxy.MaxDegreeOfParallelismDefault) { } + + protected ParallelServiceProxy(T serviceManager, int maxDegreeOfParallelism) + { + this.ServiceManager = serviceManager; + this.MaxDegreeOfParallelism = maxDegreeOfParallelism; + } + + protected ParallelServiceProxy(T serviceManager, int maxDegreeOfParallelism, int ThrottleRetryCountOverride, TimeSpan ThrottleRetryDelayOverride) + { + this.ServiceManager = serviceManager; + this.MaxDegreeOfParallelism = maxDegreeOfParallelism; + } + #endregion + + #region Fields + + private int maxDegreeOfParallelism; + + #endregion + + #region Properties + + protected T ServiceManager { get; set; } + + + /// + /// Override the default max degree of concurrency for the ParallelServiceProxy operations + /// + public int MaxDegreeOfParallelism + { + get + { + return this.maxDegreeOfParallelism; + } + set + { + ValidateDegreeOfParallelism(value); + + this.maxDegreeOfParallelism = value; + } + } + + public bool IsCrmServiceClientReady + { + get + { + if (ServiceManager != null && + ServiceManager is OrganizationServiceManager ) + { + + if ((ServiceManager as OrganizationServiceManager).IsCrmServiceClient) + { + return (ServiceManager as OrganizationServiceManager).IsCrmServiceClientReady; + } + } + return false; + } + } + + #endregion + + #region Methods + + /// + /// Ensures a valid max degree of parallelism argument before initiating the parallel process + /// + /// The max degree of parallelism + /// An exception will be thrown if value is less than -1 or equal to 0. + protected void ValidateDegreeOfParallelism(int maxDegree) + { + if (maxDegree < -1 + || maxDegree == 0) + throw new ArgumentOutOfRangeException(string.Format("The provided MaxDegreeOfParallelism={0} is not valid. Argument must be -1 or greater than 0.", maxDegree)); + } + + #endregion + } + + public abstract class ParallelServiceProxy + { + public const int MaxDegreeOfParallelismDefault = -1; + } +} diff --git a/v9.2/Client/ServiceProxyOptions.cs b/v9.2/Client/ServiceProxyOptions.cs new file mode 100644 index 0000000..cc83528 --- /dev/null +++ b/v9.2/Client/ServiceProxyOptions.cs @@ -0,0 +1,55 @@ +/*================================================================================================================================ + + This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. + + THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + + We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object + code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software + product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the + Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims + or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code. + + =================================================================================================================================*/ +namespace Microsoft.Pfe.Xrm +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + using Microsoft.Xrm.Sdk; + using Microsoft.Xrm.Sdk.Client; + + /// + /// Class that contains configurable options for requests + /// + public class OrganizationServiceProxyOptions : ServiceProxyOptions + { + [Obsolete("When creating the original service client object, pass the proxy types in to enable proxy types", true)] + public bool ShouldEnableProxyTypes { get; set; } + public Guid CallerId { get; set; } + } + + /// + /// Base class that contains configurable options for requests + /// + public class ServiceProxyOptions + { + public static TimeSpan DefaultProxyTimeout = new TimeSpan(0,2,0); + + /// + /// Construct a with default Timeout + /// + public ServiceProxyOptions() + { + this.Timeout = ServiceProxyOptions.DefaultProxyTimeout; + } + + /// + /// The timeout for the channel + /// + public TimeSpan Timeout { get; set; } + } +} \ No newline at end of file diff --git a/v9.2/Client/XrmServiceManager.cs b/v9.2/Client/XrmServiceManager.cs new file mode 100644 index 0000000..d8302f9 --- /dev/null +++ b/v9.2/Client/XrmServiceManager.cs @@ -0,0 +1,342 @@ +/*================================================================================================================================ + + This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. + + THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + + We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object + code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software + product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the + Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims + or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code. + + =================================================================================================================================*/ +namespace Microsoft.Pfe.Xrm +{ + using System; + using System.Collections.Generic; + using System.Net; + using System.ServiceModel; + using System.ServiceModel.Description; + using System.Text; + + using Microsoft.Xrm; + using Microsoft.Xrm.Sdk; + using Microsoft.Xrm.Sdk.Client; + + using Microsoft.Pfe.Xrm.Diagnostics; + using System.Reflection; + using System.Data; + using System.Diagnostics; + using Microsoft.Rest; + using System.Security.Cryptography.X509Certificates; + using Microsoft.PowerPlatform.Dataverse.Client; + using System.IO; + + + /// + /// Wrapper class for managing the service configuration of the Dynamics CRM Organization.svc endpoint + /// + public class OrganizationServiceManager : XrmServiceManager + { + #region Constructor(s) + + /// + /// Setup base / Preinitialized XRM client. + /// + /// + public OrganizationServiceManager(ServiceClient crmServiceClientObject) + : base(crmServiceClientObject) { } + + /// + /// Establishes an configuration at Uri location using supplied identity details + /// + /// The service endpoint location + /// The username of the identity to authenticate + /// The password of the identity to authenticate + /// Authorized AppId + /// Redirect uri for the appId provided Redirect + public OrganizationServiceManager(Uri serviceUri, string username, string password, string applicationId = null, Uri redirectUri = null) + : base(serviceUri, username, password, applicationId, redirectUri) { } + + /// + /// Establishes an configuration at Uri location using supplied identity details + /// + /// The service endpoint location + /// The username of the identity to authenticate + /// The password of the identity to authenticate + public OrganizationServiceManager(Uri serviceUri, string username, string password) + : base(serviceUri, username, password) { } + + #endregion + + #region Fields + + private ParallelOrganizationServiceProxy parallelProxy; + + #endregion + + #region Properties + + public ParallelOrganizationServiceProxy ParallelProxy + { + get + { + if (this.parallelProxy == null) + { + this.parallelProxy = new ParallelOrganizationServiceProxy(this); + } + + return this.parallelProxy; + } + } + + #endregion + } + + /// + /// Generic class for establishing and managing a service configuration for Dynamics CRM endpoints + /// + /// Set type to request respective service proxy instances. + /// Set a proxy return type to type based on TService type. + /// + /// Provides a means to reuse thread-safe service configurations and security tokens to open multiple client service proxies (channels) + /// + public abstract class XrmServiceManager : XrmServiceManagerBase + where TService : class + where TProxy : class //ServiceProxy + { + #region Constructor(s) + + /// + /// Default constructor + /// + private XrmServiceManager() + { + throw new NotImplementedException("Default constructor not implemented"); + } + + protected XrmServiceManager(ServiceClient serviceClient) + { + if (serviceClient != null && serviceClient.IsReady == true) + { + this.ServiceClient = serviceClient; + } + else + { + XrmCoreEventSource.Log.LogFailureLine($"The provided serviceClient is 'Not Ready' or null. The reason provided by CrmServiceClient is: {ServiceClient.LastError}"); + + throw new Exception($"The provided serviceClient is 'Not Ready' or null. The reason provided by CrmServiceClient is: {ServiceClient.LastError}", ServiceClient.LastException); + } + } + + /// + /// Establishes a service configuration of type TService at location using supplied identity details + /// + /// The service endpoint location (ie: https://environment.crm.dynamics.com/) + /// The username of the identity to authenticate + /// The password of the identity to authenticate + /// The password of the identity to authenticate + /// The password of the identity to authenticate + protected XrmServiceManager(Uri environmentUri, string username, string password, string applicationId = null, Uri redirectUri = null) + { + if (applicationId == null || redirectUri == null) + { + applicationId = "2ad88395-b77d-4561-9441-d0e40824f9bc"; + redirectUri = new Uri("app://5d3e90d6-aa8e-48a8-8f2c-58b45cc67315"); + } + + var connectionstring = $"Username={username};Password={password};Url={environmentUri.AbsoluteUri};AuthType=OAuth;ClientId={applicationId};redirecturi={redirectUri};SkipDiscovery=True"; + + this.ServiceClient = new ServiceClient(connectionstring); + } + + #endregion + + #region Private Properties + + /// + /// Copy of the CrmService Client that has been initialized + /// + private ServiceClient ServiceClient { get; set; } + + /// + /// Is TService an IOrganizationService type + /// + private bool IsOrganizationService + { + get + { + return typeof(TService).Equals(typeof(IOrganizationService)); + } + } + + #endregion + + #region Public Properties + /// + /// Current endpoint address + /// + public Uri ServiceUri + { + get + { + return this.ServiceClient.ConnectedOrgUriActual; + } + } + + /// + /// The of the targeted endpoint + /// + public Microsoft.PowerPlatform.Dataverse.Client.AuthenticationType AuthenticationType + { + get + { + return this.ServiceClient.ActiveAuthenticationType; + } + } + + /// + /// Override the default (3) maximum retries when throttling events are encountered + /// + public int MaxRetryCount + { + get + { + return this.ServiceClient.MaxRetryCount; + } + set + { + this.ServiceClient.MaxRetryCount = value; + } + } + + /// + /// Override the default timespan to wait when throttling events are encountered + /// + public TimeSpan RetryPauseTime + { + get + { + return this.ServiceClient.RetryPauseTime; + } + set + { + this.ServiceClient.RetryPauseTime = value; + } + } + + /// + /// True if targeted endpoint's authentication provider type is LiveId or OnlineFederation, otherwise False + /// + public bool IsCrmOnline + { + get + { + return this.AuthenticationType == Microsoft.PowerPlatform.Dataverse.Client.AuthenticationType.ExternalTokenManagement + || this.AuthenticationType == Microsoft.PowerPlatform.Dataverse.Client.AuthenticationType.Certificate + || this.AuthenticationType == Microsoft.PowerPlatform.Dataverse.Client.AuthenticationType.OAuth; + } + } + + public bool IsCrmServiceClient + { + get { return ServiceClient != null; } + } + + public FileVersionInfo AdalVersion + { + get; + private set; + } + + public bool IsCrmServiceClientReady + { + get + { if (ServiceClient != null) return ServiceClient.IsReady; else return false; } + } + #endregion + + #region Methods + + protected Microsoft.PowerPlatform.Dataverse.Client.ServiceClient GetProxy() + { + if (this.ServiceClient.ActiveAuthenticationType != Microsoft.PowerPlatform.Dataverse.Client.AuthenticationType.ExternalTokenManagement) + { + //This should no longer be needed as DataverseClient is ported over to MSAL, but keeping it here for review/testing. + //Added check for file exist before getting the FileVersion. + if (AdalVersion == null) + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.RelativeSearchPath ?? "") + @"\Microsoft.IdentityModel.Clients.ActiveDirectory.dll"; + + if (File.Exists(path)) + { + FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(path); + AdalVersion = fvi; + } + } + + if (AdalVersion != null + && (AdalVersion.FileMajorPart != 2 && AdalVersion.FileMajorPart != 3)) + { + XrmCoreEventSource.Log.LogError($"ADAL Version {AdalVersion.FileVersion} is not matching the expected versions of 2.x or 3.x. Certain functions may not work as expected if you're not using the AuthOverrideHook."); + } + } + + if (this.IsCrmServiceClient) + { + if (this.ServiceClient.IsReady) + { + if (this.ServiceClient.ActiveAuthenticationType == Microsoft.PowerPlatform.Dataverse.Client.AuthenticationType.OAuth + || this.ServiceClient.ActiveAuthenticationType == Microsoft.PowerPlatform.Dataverse.Client.AuthenticationType.Certificate + || this.ServiceClient.ActiveAuthenticationType == Microsoft.PowerPlatform.Dataverse.Client.AuthenticationType.ClientSecret + || this.ServiceClient.ActiveAuthenticationType == Microsoft.PowerPlatform.Dataverse.Client.AuthenticationType.ExternalTokenManagement) + { + //cloning + var svcClientClone = this.ServiceClient.Clone(); + var sessionTrackingGuid = Guid.NewGuid(); + XrmCoreEventSource.Log.ServiceClientCloneRequested(sessionTrackingGuid.ToString()); + svcClientClone.SessionTrackingId = sessionTrackingGuid; + + //cloned objects don't inherit the same settings, fixing that in code for now + svcClientClone.RetryPauseTime = this.ServiceClient.RetryPauseTime; + svcClientClone.MaxRetryCount = this.ServiceClient.MaxRetryCount; + svcClientClone.UseWebApi = this.ServiceClient.UseWebApi; + + return svcClientClone; + } + else + { + XrmCoreEventSource.Log.LogFailureLine($"You must have successfully created a connection to CRM using OAuth or Certificate Auth before it can be cloned."); + throw new Exception("You must have successfully created a connection to CRM using OAuth or Certificate Auth before it can be cloned."); + } + } + } + + XrmCoreEventSource.Log.LogFailureLine($"CrmServiceClient is 'Not Ready'. The only reason we can find is: {this.ServiceClient.LastError}"); + throw new Exception($"CrmServiceClient is 'Not Ready'. The only reason we can find is: {this.ServiceClient.LastError}", this.ServiceClient.LastException); + } + + /// + /// Sets up a new proxy connection of type TProxy using + /// + /// An instance of a managed token TProxy + /// i.e. + /// + /// The proxy represents a client service channel to a service endpoint. + /// Proxy connections should be disposed of properly before they fall out of scope to free up the allocated service channel. + /// + public ServiceClient GetProxy() + { + return this.GetProxy(); + } + + #endregion + } + + /// + /// Base class for XrmServiceManager + /// + public abstract class XrmServiceManagerBase { } +} \ No newline at end of file diff --git a/v9.2/Client/XrmServiceUriFactory.cs b/v9.2/Client/XrmServiceUriFactory.cs new file mode 100644 index 0000000..a1d2c17 --- /dev/null +++ b/v9.2/Client/XrmServiceUriFactory.cs @@ -0,0 +1,180 @@ +namespace Microsoft.Pfe.Xrm +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// + /// Factory class used to create instances targeting Dynamics CRM endpoints + /// + public static class XrmServiceUriFactory + { + public const string OrganizationServicePath = @"/XRMServices/2011/Organization.svc"; + public const string OrganizationDataServicePath = @"/XRMServices/2011/OrganizationData.svc"; + public const string OrganizationWebServicePath = @"/XRMServices/2011/Organization.svc/Web"; + public const string OrganizationServiceOnlineNAUriFormat = "https://{0}.api.crm.dynamics.com"; + public const string OrganizationServiceOnlineEMEAUriFormat = "https://{0}.api.crm4.dynamics.com"; + public const string OrganizationServiceOnlineAPACUriFormat = "https://{0}.api.crm5.dynamics.com"; + + /// + /// Creates an Organization.svc instance based on the specified location + /// + /// The scheme, host, port # (if applicable), and organization representing location of Organization.svc + /// A new instance of for the Organization.svc + /// + /// EXAMPLES: https://hostname:5555/organization, https://organization.hostname:5555, https://hostname/organization, https://organization.hostname + /// The organization name, if detected in the path, is preserved in the resulting + /// The Organization.svc endpoint path is not necessary as this will be appended to all 's based on the type specified + /// + public static Uri CreateOrganizationServiceUri(string location) + { + return XrmServiceUriFactory.CreateServiceUri(location, XrmServiceType.Organization); + } + + /// + /// Creates an OrganizationData.svc instance based on the specified location + /// + /// The scheme, host, port # (if applicable), and organization representing location of Organization.svc + /// A new instance of for the OrganizationData.svc + /// + /// EXAMPLES: https://hostname:5555/organization, https://organization.hostname:5555, https://hostname/organization, https://organization.hostname + /// The organization name, if detected in the path, is preserved in the resulting + /// The OrganizationData.svc endpoint path is not necessary as this will be appended to all 's based on the type specified + /// + public static Uri CreateOrganizationDataServiceUri(string location) + { + return XrmServiceUriFactory.CreateServiceUri(location, XrmServiceType.OrganizationData); + } + + /// + /// Creates an Organization.svc/Web instance based on the specified location + /// + /// The scheme, host, port # (if applicable), and organization representing location of Organization.svc + /// A new instance of for the Organization.svc/Web + /// + /// EXAMPLES: https://hostname:5555/organization, https://organization.hostname:5555, https://hostname/organization, https://organization.hostname + /// The organization name, if detected in the path, is preserved in the resulting + /// The Organization.svc/Web endpoint path is not necessary as this will be appended to all 's based on the type specified + /// + public static Uri CreateOrganizationWebServiceUri(string location) + { + return XrmServiceUriFactory.CreateServiceUri(location, XrmServiceType.OrganizationWeb); + } + + /// + /// Creates an Organization.svc instance targeting CRM Online for the specified organization/region pair + /// + /// The organization name + /// The region where the organization is located + /// A new instance of for the Organization.svc + public static Uri CreateOnlineOrganizationServiceUri(string organizationName, CrmOnlineRegion region = CrmOnlineRegion.NA) + { + string location = XrmServiceUriFactory.CreateOnlineOrganizationServiceLocation(organizationName, region); + + return XrmServiceUriFactory.CreateOrganizationServiceUri(location); + } + + /// + /// Creates an OrganizationData.svc instance targeting CRM Online for the specified organization/region pair + /// + /// The organization name + /// The region where the organization is located + /// A new instance of for the OrganizationData.svc + public static Uri CreateOnlineOrganizationDataServiceUri(string organizationName, CrmOnlineRegion region = CrmOnlineRegion.NA) + { + string location = XrmServiceUriFactory.CreateOnlineOrganizationServiceLocation(organizationName, region); + + return XrmServiceUriFactory.CreateOrganizationDataServiceUri(location); + } + + /// + /// Creates an Organization.svc/Web instance targeting CRM Online for the specified organization/region pair + /// + /// The organization name + /// The region where the organization is located + /// A new instance of for the Organization.svc/Web + public static Uri CreateOnlineOrganizationWebServiceUri(string organizationName, CrmOnlineRegion region = CrmOnlineRegion.NA) + { + string location = XrmServiceUriFactory.CreateOnlineOrganizationServiceLocation(organizationName, region); + + return XrmServiceUriFactory.CreateOrganizationWebServiceUri(location); + } + + /// + /// Creates a that targets the specified location and XRM service endpoint type + /// + /// The location of the Dynamics CRM endpoint - This should include scheme, host, port, and organization name (if applicable) + /// The Dynamics CRM endpoint type + /// A targeting the specified Dynamics CRM service endpoint + /// + /// EXAMPLES: https://hostname:4443/organization, https://hostname/organization, https://organization.hostname, https://hostname + /// For Organization.svc types, the organization name, if detected in the path, is preserved in the resulting + /// The Dynamics CRM service endpoint path is not necessary as this will be appended to all 's based on the type specified + /// + private static Uri CreateServiceUri(string location, XrmServiceType serviceType) + { + Uri providedUri = null; + + //Try to create an absolute Uri from the provided string location + if (Uri.TryCreate(location, UriKind.Absolute, out providedUri)) + { + //Get the root Uri including scheme + delimeter and authority + string providedAuthority = providedUri.GetLeftPart(UriPartial.Authority); + + var uriBuilder = new UriBuilder(providedAuthority); + var pathBuilder = new StringBuilder(128); + + //If we detect something other than XRMServices in the second path segment, assume it's the organization name and preserve it for non-discovery locations. + if (providedUri.Segments.Length > 1 + && !providedUri.Segments[1].Equals(@"XRMServices/", StringComparison.OrdinalIgnoreCase)) + { + pathBuilder.Append(providedUri.Segments[0]); + pathBuilder.Append(providedUri.Segments[1].TrimEnd('/')); + } + + //Append path to the XRM service endpoint location based on endpoint type specified + switch (serviceType) + { + case XrmServiceType.Organization: + pathBuilder.Append(XrmServiceUriFactory.OrganizationServicePath); + break; + + case XrmServiceType.OrganizationData: + pathBuilder.Append(XrmServiceUriFactory.OrganizationDataServicePath); + break; + + case XrmServiceType.OrganizationWeb: + pathBuilder.Append(XrmServiceUriFactory.OrganizationWebServicePath); + break; + } + + uriBuilder.Path = pathBuilder.ToString(); + + return uriBuilder.Uri; + } + + return null; + } + + /// + /// Creates a string representing the CRM Online Organization.svc location based on the specified region + /// + /// The organization name being targeted + /// The applicable CRM Online region + /// The formatted Organization.svc location for the organization/region pair + private static string CreateOnlineOrganizationServiceLocation(string organizationName, CrmOnlineRegion region) + { + switch (region) + { + case CrmOnlineRegion.NA: + default: + return String.Format(XrmServiceUriFactory.OrganizationServiceOnlineNAUriFormat, organizationName); + case CrmOnlineRegion.EMEA: + return String.Format(XrmServiceUriFactory.OrganizationServiceOnlineEMEAUriFormat, organizationName); + case CrmOnlineRegion.APAC: + return String.Format(XrmServiceUriFactory.OrganizationServiceOnlineAPACUriFormat, organizationName); + } + } + } +} diff --git a/v9.2/Diagnostics/ExtensionMethods.cs b/v9.2/Diagnostics/ExtensionMethods.cs new file mode 100644 index 0000000..b9a696f --- /dev/null +++ b/v9.2/Diagnostics/ExtensionMethods.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.ServiceModel; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.Xrm.Sdk; + +namespace Microsoft.Pfe.Xrm.Diagnostics +{ + internal static class DiagnosticExtensionMethods + { + private const string InnerPrefix = "Inner "; + private const string StackTraceHeader = "Stack Trace: "; + + /// + /// Appends exception message and stack trace using StringBuilder, recurses inner exceptions + /// + /// The current exception + /// The current message builder + /// True = current exception is an inner exception + /// The provided StringBuilder instance. If arg is null, a new instance is created. + public static StringBuilder ToErrorMessageString(this Exception ex, StringBuilder builder = null, bool isInnerException = false) + { + if (builder == null) + builder = new StringBuilder(); + + if (isInnerException) + { + builder.Append(InnerPrefix); + } + + // Handle aggregate exception scenario + AggregateException ae = ex as AggregateException; + if (ae != null) + { + builder.AppendFormat("AggregateException: {0}", ae.Message); + + ae.Handle(e => + { + // Append message for each exception in the aggregateexception + e.ToErrorMessageString(builder); + + // Assume handled here... + return true; + }); + + return builder; + } + + // Build using fault exception details + FaultException orgFault = ex as FaultException; + if (orgFault != null) + { + builder.AppendFormat("FaultException: {0}", ex.Message).AppendLine(); + + return orgFault.Detail.ToFaultMessageString(builder); + } + + // Build for all other exception types + builder.AppendFormat("{0}: {1}", ex.GetType().Name, ex.Message).AppendLine(); + builder.AppendLine(StackTraceHeader); + builder.AppendLine(ex.StackTrace); + + // Recurse building inner exception message string + if (ex.InnerException != null) + { + ex.InnerException.ToErrorMessageString(builder, true); + } + + return builder; + } + + /// + /// Appends fault details using StringBuilder, recurses inner faults + /// + /// The current fault encountered + /// The current message builder + /// True = current fault is an inner fault + /// The provided StringBuilder instance. If arg is null, a new instance is created. + public static StringBuilder ToFaultMessageString(this OrganizationServiceFault fault, StringBuilder builder = null, bool isInnerFault = false) + { + if (builder == null) + builder = new StringBuilder(); + + if (isInnerFault) + { + builder.Append(InnerPrefix); + } + + // Append current fault + builder.AppendFormat("OrganizationServiceFault: {0}", fault.Message).AppendLine(); + builder.AppendLine(StackTraceHeader); + builder.AppendLine(fault.TraceText); + + // Recurse building the inner fault message string + if (fault.InnerFault != null) + { + fault.InnerFault.ToFaultMessageString(builder, true); + } + + return builder; + } + } +} + diff --git a/v9.2/Diagnostics/XrmCoreEventSource.Authentication.cs b/v9.2/Diagnostics/XrmCoreEventSource.Authentication.cs new file mode 100644 index 0000000..ffd1417 --- /dev/null +++ b/v9.2/Diagnostics/XrmCoreEventSource.Authentication.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Pfe.Xrm.Diagnostics +{ + public partial class XrmCoreEventSource + { + [Event(XrmCoreEventSourceEventIds.ServiceClientCloneRequested, + Message = "Cloned Connection SessionTrackingId: {0}", + Level = EventLevel.Verbose, + Keywords = Keywords.Authentication, + Task = Tasks.SecurityToken, + Opcode = EventOpcode.Info)] + internal void ServiceClientCloneRequested(string sessionTrackingId) + { + this.WriteEvent(XrmCoreEventSourceEventIds.ServiceClientCloneRequested, sessionTrackingId); + } + } +} diff --git a/v9.2/Diagnostics/XrmCoreEventSource.Parallel.cs b/v9.2/Diagnostics/XrmCoreEventSource.Parallel.cs new file mode 100644 index 0000000..c19ac1c --- /dev/null +++ b/v9.2/Diagnostics/XrmCoreEventSource.Parallel.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Pfe.Xrm.Diagnostics +{ + public partial class XrmCoreEventSource : EventSource + { + [Event(XrmCoreEventSourceEventIds.ThrottleEventGeneric, + Message = "ThrottleEvent: {0}", + Level = EventLevel.Warning, + Keywords = Keywords.Parallel, + Task = Tasks.ExceptionHandler, + Opcode = EventOpcode.Info)] + internal void ThrottleEventGeneric(string throttleDetails) + { + this.WriteEvent(XrmCoreEventSourceEventIds.ThrottleEventGeneric, throttleDetails); + } + + [Event(XrmCoreEventSourceEventIds.ParallelCoreOperationCompleted, + Message = "{0} ParallelCoreOperationCompleted", + Level = EventLevel.Verbose, + Keywords = Keywords.Parallel, + Task = Tasks.OrganizationRequest, + Opcode = EventOpcode.Info)] + internal void ParallelCoreOperationCompleted(string details) + { + this.WriteEvent(XrmCoreEventSourceEventIds.ParallelCoreOperationCompleted, details); + } + + [Event(XrmCoreEventSourceEventIds.ParallelCoreOperationStart, + Message = "{0} ParallelCoreOperationStart", + Level = EventLevel.Verbose, + Keywords = Keywords.Parallel, + Task = Tasks.OrganizationRequest, + Opcode = EventOpcode.Info)] + internal void ParallelCoreOperationStart(string details) + { + this.WriteEvent(XrmCoreEventSourceEventIds.ParallelCoreOperationStart, details); + } + } +} diff --git a/v9.2/Diagnostics/XrmCoreEventSource.Query.cs b/v9.2/Diagnostics/XrmCoreEventSource.Query.cs new file mode 100644 index 0000000..117812a --- /dev/null +++ b/v9.2/Diagnostics/XrmCoreEventSource.Query.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Pfe.Xrm.Diagnostics +{ + public partial class XrmCoreEventSource + { + + } +} diff --git a/v9.2/Diagnostics/XrmCoreEventSource.Service.cs b/v9.2/Diagnostics/XrmCoreEventSource.Service.cs new file mode 100644 index 0000000..117812a --- /dev/null +++ b/v9.2/Diagnostics/XrmCoreEventSource.Service.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Pfe.Xrm.Diagnostics +{ + public partial class XrmCoreEventSource + { + + } +} diff --git a/v9.2/Diagnostics/XrmCoreEventSource.cs b/v9.2/Diagnostics/XrmCoreEventSource.cs new file mode 100644 index 0000000..65d226d --- /dev/null +++ b/v9.2/Diagnostics/XrmCoreEventSource.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Pfe.Xrm.Diagnostics +{ + [EventSource(Name = "Microsoft-Dynamics365-XrmCore")] + public partial class XrmCoreEventSource : EventSource + { + private XrmCoreEventSource() :base(true) { } + private static readonly Lazy _instance = new Lazy(() => new XrmCoreEventSource(), true); + public static XrmCoreEventSource Log { get { return _instance.Value; } } + + public class Keywords + { + public const EventKeywords Authentication = (EventKeywords)1; + public const EventKeywords Service = (EventKeywords)2; + public const EventKeywords Parallel = (EventKeywords)4; + public const EventKeywords Query = (EventKeywords)8; + public const EventKeywords Cryptography = (EventKeywords)16; + } + + public class Tasks + { + public const EventTask Configuration = (EventTask)1; + public const EventTask Channel = (EventTask)2; + public const EventTask SecurityToken = (EventTask)3; + public const EventTask DiscoveryRequest = (EventTask)4; + public const EventTask OrganizationRequest = (EventTask)5; + public const EventTask Create = (EventTask)6; + public const EventTask Update = (EventTask)7; + public const EventTask Delete = (EventTask)8; + public const EventTask Retrieve = (EventTask)9; + public const EventTask RetrieveMultiple = (EventTask)10; + public const EventTask Associate = (EventTask)11; + public const EventTask Disassociate = (EventTask)12; + public const EventTask Execute = (EventTask)13; + public const EventTask ExceptionHandler = (EventTask)14; + } + + [Event(XrmCoreEventSourceEventIds.Failure, + Message = "Crital Failure: {0}", + Level = EventLevel.Critical)] + internal void LogFailureLine(string message) + { + WriteEvent(XrmCoreEventSourceEventIds.Failure, message); + } + + [Event(XrmCoreEventSourceEventIds.GenericFailure, + Message = "Request Failure encountered: {0}", + Level = EventLevel.Error)] + public void LogError(string Message) + { + WriteEvent(XrmCoreEventSourceEventIds.GenericFailure, Message); + } + + [Event(XrmCoreEventSourceEventIds.GenericWarning, + Message = "{0}", + Level = EventLevel.Warning, + Opcode = EventOpcode.Info)] + public void LogWarning(string Message) + { + WriteEvent(XrmCoreEventSourceEventIds.GenericWarning, Message); + } + + [Event(XrmCoreEventSourceEventIds.GenericInformational, + Message = "{0}", + Level = EventLevel.Informational, + Opcode = EventOpcode.Info)] + public void LogInformation(string Message) + { + WriteEvent(XrmCoreEventSourceEventIds.GenericInformational, Message); + } + } +} diff --git a/v9.2/Diagnostics/XrmCoreEventSourceEventIds.cs b/v9.2/Diagnostics/XrmCoreEventSourceEventIds.cs new file mode 100644 index 0000000..192e806 --- /dev/null +++ b/v9.2/Diagnostics/XrmCoreEventSourceEventIds.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Pfe.Xrm.Diagnostics +{ + public class XrmCoreEventSourceEventIds + { + public const int Failure = 1; + public const int ParallelOperationFailure = 2; + + public const int ServiceConfigurationInitialized = 400; + public const int ProxyChannelOpened = 410; + + public const int SecurityTokenRequested = 500; + public const int SecurityTokenRequestFailure = 501; + public const int SecurityTokenRefreshRequired = 510; + public const int SecurityTokenRefreshFailure = 511; + + public const int ServiceClientCloneRequested = 601; + + public const int ParallelCoreOperationStart = 650; + public const int ParallelCoreOperationCompleted = 660; + public const int ParallelProxyInit = 670; + + + public const int ThrottleEventGeneric = 700; + public const int ThrottleRetryAfter = 701; + + public const int GenericInformational = 1000; + public const int GenericWarning = 2000; + public const int GenericFailure = 3000; + } +} diff --git a/v9.2/Extensions/BatchRequestExtensions.cs b/v9.2/Extensions/BatchRequestExtensions.cs new file mode 100644 index 0000000..e76d39b --- /dev/null +++ b/v9.2/Extensions/BatchRequestExtensions.cs @@ -0,0 +1,237 @@ +/*================================================================================================================================ + + This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. + + THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + + We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object + code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software + product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the + Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims + or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code. + + =================================================================================================================================*/ + +namespace Microsoft.Pfe.Xrm +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + using Microsoft.Xrm.Sdk; + using Microsoft.Xrm.Sdk.Client; + using Microsoft.Xrm.Sdk.Messages; + using Microsoft.Crm.Sdk.Messages; + + public static class BatchRequestExtensions + { + public const int maxBatchSize = 1000; + public const bool continueSettingDefault = true; + public const bool returnSettingDefault = true; + + + /// + /// Converts a collection of type to batches of + /// + /// The collection of entities to partition into batches as + /// The size of each batch + /// A keyed collection of s representing the request batches + /// + /// Uses default settings of ContinueOnError = True, ReturnResposnes = True + /// + public static IDictionary AsCreateBatches(this IEnumerable entities, int batchSize) + { + return entities.AsCreateBatches(batchSize, continueSettingDefault, returnSettingDefault); + } + + /// + /// Converts a collection of type to batches of + /// + /// The collection of entities to partition into batches as + /// The size of each batch + /// True if each should continue processing when an is encountered + /// True if an should be returned after processing is complete + /// A keyed collection of type representing the request batches + public static IDictionary AsCreateBatches(this IEnumerable entities, int batchSize, bool continueOnError, bool returnResponses) + { + var requests = new List(entities.Count()); + + foreach (Entity entity in entities) + { + var request = new CreateRequest() + { + Target = entity + }; + + requests.Add(request); + } + + return requests.AsBatches(batchSize, continueOnError, returnResponses); + } + + /// + /// Converts a collection of type to batches of + /// + /// The collection of entities to partition into batches as + /// The size of each batch + /// A keyed collection of s representing the request batches + /// + /// Uses default settings of ContinueOnError = True, ReturnResposnes = True + /// + public static IDictionary AsUpdateBatches(this IEnumerable entities, int batchSize) + { + return entities.AsUpdateBatches(batchSize, continueSettingDefault, returnSettingDefault); + } + + /// + /// Converts a collection of type to batches of + /// + /// The collection of entities to partition into batches as + /// The size of each batch + /// True if each should continue processing when an is encountered + /// True if an should be returned after processing is complete + /// A keyed collection of type representing the request batches + public static IDictionary AsUpdateBatches(this IEnumerable entities, int batchSize, bool continueOnError, bool returnResponses) + { + var requests = new List(entities.Count()); + + foreach (Entity entity in entities) + { + var request = new UpdateRequest() + { + Target = entity + }; + + requests.Add(request); + } + + return requests.AsBatches(batchSize, continueOnError, returnResponses); + } + + /// + /// Converts a collection of type to batches of + /// + /// The collection of entities to partition into batches as + /// The size of each batch + /// A keyed collection of s representing the request batches + /// + /// Uses default settings of ContinueOnError = True, ReturnResposnes = True + /// + public static IDictionary AsDeleteBatches(this IEnumerable entityReferences, int batchSize) + { + return entityReferences.AsDeleteBatches(batchSize, continueSettingDefault, returnSettingDefault); + } + + /// + /// Converts a collection of type to batches of + /// + /// The collection of entities to partition into batches as + /// The size of each batch + /// True if each should continue processing when an is encountered + /// True if an should be returned after processing is complete + /// A keyed collection of type representing the request batches + public static IDictionary AsDeleteBatches(this IEnumerable entityReferences, int batchSize, bool continueOnError, bool returnResponses) + { + var requests = new List(entityReferences.Count()); + + foreach (EntityReference entityRef in entityReferences) + { + var request = new DeleteRequest() + { + Target = entityRef + }; + + requests.Add(request); + } + + return requests.AsBatches(batchSize, continueOnError, returnResponses); + } + + /// + /// Converts a collection of type to batches + /// + /// The typeof + /// The collection of requests to partition into batches + /// The size of each batch + /// A keyed collection of type representing the request batches + /// + /// Uses default settings of ContinueOnError = True, ReturnResposnes = True + /// + public static IDictionary AsBatches(this IEnumerable requests, int batchSize) + where T : OrganizationRequest + { + return requests.AsBatches(batchSize, continueSettingDefault, returnSettingDefault); + } + + /// + /// Converts a collection of type to batches + /// + /// The typeof + /// The collection of requests to partition into batches + /// The size of each batch + /// True if each should continue processing when an is encountered + /// True if an should be returned after processing is complete + /// A keyed collection of type representing the request batches + public static IDictionary AsBatches(this IEnumerable requests, int batchSize, bool continueOnError, bool returnResponses) + where T : OrganizationRequest + { + return requests.AsBatches(batchSize, new ExecuteMultipleSettings() { ContinueOnError = continueOnError, ReturnResponses = returnResponses }); + } + + /// + /// Converts a collection of type to batches + /// + /// The typeof + /// The collection of requests to partition into batches + /// The size of each batch + /// The desired settings + /// A keyed collection of type representing the request batches + public static IDictionary AsBatches(this IEnumerable requests, int batchSize, ExecuteMultipleSettings batchSettings) + where T : OrganizationRequest + { + if (batchSize <= 0) + throw new ArgumentException("Batch size must be greater than 0", "batchSize"); + if (batchSize > maxBatchSize) + throw new ArgumentException(String.Format("Batch size of {0} exceeds max batch size of 1000", batchSize), "batchSize"); + if (batchSettings == null) + throw new ArgumentNullException("batchSettings"); + + // Index each request + var indexedRequests = requests.Select((r, i) => new { Index = i, Value = r }); + + // Partition the indexed requests by batch size + var partitions = indexedRequests.GroupBy(ir => ir.Index / batchSize); + + // Convert each partition to an ExecuteMultilpleRequest batch + IEnumerable batches = partitions.Select(p => p.Select(ir => ir.Value).AsBatch(batchSettings)); + + // Index each batch + var indexedBatches = batches.Select((b, i) => new { Index = i, Value = b }); + + // Return indexed batches as dictionary + return indexedBatches.ToDictionary(ib => ib.Index.ToString(), ib => ib.Value); + } + + /// + /// Converts a collection of type to a single instance + /// + /// The typeof + /// The collection of requests representing the batch + /// The desired settings + /// A single instance + public static ExecuteMultipleRequest AsBatch(this IEnumerable requests, ExecuteMultipleSettings batchSettings) + where T : OrganizationRequest + { + var batch = new OrganizationRequestCollection(); + batch.AddRange(requests); + + return new ExecuteMultipleRequest() + { + Requests = batch, + Settings = batchSettings + }; + } + } +} diff --git a/v9.2/Extensions/QueryExtensions.cs b/v9.2/Extensions/QueryExtensions.cs new file mode 100644 index 0000000..22348b0 --- /dev/null +++ b/v9.2/Extensions/QueryExtensions.cs @@ -0,0 +1,247 @@ +/*================================================================================================================================ + + This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. + + THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + + We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object + code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software + product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the + Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims + or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code. + + =================================================================================================================================*/ +namespace Microsoft.Pfe.Xrm +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using System.Xml; + using System.Xml.Linq; + + using Microsoft.Xrm.Sdk; + using Microsoft.Xrm.Sdk.Query; + + public static class QueryExtensions + { + internal const int DefaultPageSize = 5000; + + /// + /// Parses a FetchXML query as an instance of System.Xml.Linq.XElement + /// + /// The expression of a FetchXML query + /// A System.Xml.Linq.XElement representing the query + public static XElement ToXml(this FetchExpression fe) + { + return XElement.Parse(fe.Query, LoadOptions.PreserveWhitespace); + } + + /// + /// Gets the page size specified in the 'count' attribute of a FetchXML query + /// + /// The expression of a FetchXML query + /// The fetch count as an integer value + /// + /// Returns default page size of 5000 + /// + public static int GetPageSize(this FetchExpression fe) + { + return fe.GetPageSize(DefaultPageSize); + } + + /// + /// Gets the page size specified in the 'count' attribute of a FetchXML query + /// + /// The expression of a FetchXML query + /// The default page size to return if not found in the FetchXML query + /// The fetch count as an integer value + public static int GetPageSize(this FetchExpression fe, int defaultPageSize) + { + XElement fetchXml = fe.ToXml(); + + return fetchXml.GetFetchXmlPageSize(defaultPageSize); + } + + /// + /// Gets the page size specified in the 'count' attribute of a FetchXML query + /// + /// The XElement representing the fetch query + /// The default page size to return if not found in the FetchXML query + /// The fetch count as an integer value + public static int GetFetchXmlPageSize(this XElement fetchXml, int defaultPageSize) + { + int pageSize = defaultPageSize; + XAttribute countAttribute = fetchXml.Attribute("count"); + + if (countAttribute != null) + { + Int32.TryParse(countAttribute.Value, out pageSize); + } + + return pageSize; + } + + /// + /// Gets the top count specified in the 'top' attribute of a FetchXML query + /// + /// The XElement representing the fetch query + /// The fetch top count as an integer value, returns '0' if top count not specified + public static int GetFetchXmlTopCount(this XElement fetchXml) + { + int topCount = 0; + XAttribute topAttribute = fetchXml.Attribute("top"); + + if (topAttribute != null) + { + Int32.TryParse(topAttribute.Value, out topCount); + } + + return topCount; + } + + /// + /// Gets the page number specificed in the 'page' attribute of a FetchXML query + /// + /// The XElement representing the fetch query + /// The fetch page number as an integer value, returns '1' if page not specified + public static int GetFetchXmlPageNumber(this XElement fetchXml) + { + int pageNumber = 1; + XAttribute pageAttribute = fetchXml.Attribute("page"); + + if (pageAttribute != null) + { + Int32.TryParse(pageAttribute.Value, out pageNumber); + } + + return pageNumber; + } + + /// + /// Gets the paging cookie specified in the 'paging-cookie' attribute of a FetchXML query + /// + /// The XElement representing the fetch query + /// The fetch paging cookie string value, returns emtpty string if not specified + public static string GetFetchXmlPageCookie(this XElement fetchXml) + { + XAttribute cookieAttribute = fetchXml.Attribute("paging-cookie"); + + if (cookieAttribute != null) + { + return cookieAttribute.Value; + } + + return String.Empty; + } + + /// + /// Sets the paging info in a FetchXML query + /// + /// The expression of a FetchXML query + /// The paging cookie string to set in the FetchXML query + /// The page number to set in the FetchXML query + /// + /// If top count is greater than 0, skips page setup and assumes query should only return TOP(X) results + /// + public static void SetPage(this FetchExpression fe, string pagingCookie, int pageNumber) + { + XElement fetchXml = fe.ToXml(); + int count = fetchXml.GetFetchXmlPageSize(DefaultPageSize); + + fetchXml.SetFetchXmlPage(pagingCookie, pageNumber, count); + + fe.Query = fetchXml.ToString(); + } + + /// + /// Sets the paging info in a FetchXML query + /// + /// The expression of a FetchXML query + /// The paging cookie string to set in the FetchXML query + /// The page number to set in the FetchXML query + /// The page size (count) to set in the FetchXML query + /// + /// If top count is greater than 0, skips page setup and assumes query should only return TOP(X) results + /// + public static void SetPage(this FetchExpression fe, string pagingCookie, int pageNumber, int count) + { + XElement fetchXml = fe.ToXml(); + fetchXml.SetFetchXmlPage(pagingCookie, pageNumber, count); + + fe.Query = fetchXml.ToString(); + } + + /// + /// Sets the paging info in a FetchXML query + /// + /// The XElement representing the FetchXML query + /// The paging cookie to set in the FetchXML query + /// The page number to set in the FetchXML query + /// The page size (count) to set in the FetchXML query + /// + /// If top count is greater than 0, skips page setup and assumes query should only return TOP(X) results + /// + public static void SetFetchXmlPage(this XElement fetchXml, string pagingCookie, int pageNumber, int count) + { + if (fetchXml.GetFetchXmlTopCount() <= 0) + { + if (!String.IsNullOrWhiteSpace(pagingCookie)) + { + fetchXml.SetAttributeValue("paging-cookie", pagingCookie); + } + + fetchXml.SetAttributeValue("page", pageNumber); + fetchXml.SetAttributeValue("count", count); + } + } + + + /// + /// Copies the details from one EntityCollection page to another + /// + /// The copy target + /// The copy source + public static void CopyFrom(this EntityCollection target, EntityCollection source) + { + if (source != null) + { + target.EntityName = source.EntityName; + target.MinActiveRowVersion = source.MinActiveRowVersion; + target.MoreRecords = source.MoreRecords; + target.PagingCookie = source.PagingCookie; + target.TotalRecordCount = source.TotalRecordCount; + target.TotalRecordCountLimitExceeded = source.TotalRecordCountLimitExceeded; + } + } + + /// + /// Extracts the page number from the paging cookie returned with the EntityCollection result + /// + /// The current result + /// The page attribute of the paging cookie as an integer, otherwise '1' if cookie is null or empty + public static int PageNumber(this EntityCollection results) + { + int pageNumber = 1; + + if (!String.IsNullOrWhiteSpace(results.PagingCookie)) + { + XElement cookie = XElement.Parse(results.PagingCookie, LoadOptions.PreserveWhitespace); + + if (cookie != null) + { + XAttribute page = cookie.Attribute("page"); + + if (page != null) + { + Int32.TryParse(page.Value, out pageNumber); + } + } + } + + return pageNumber; + } + } +} diff --git a/v9.2/Extensions/SecurityExtensions.cs b/v9.2/Extensions/SecurityExtensions.cs new file mode 100644 index 0000000..aa020d8 --- /dev/null +++ b/v9.2/Extensions/SecurityExtensions.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Text; + + +namespace Microsoft.Pfe.Xrm +{ + public static class SecurityExtensions + { + //TODO: Replace this value with your own entropy key + private static byte[] additionalEntropy = Encoding.Unicode.GetBytes("replacewithyourentropykey"); + + /// + /// Converts a SecureString object to a plain-text string value + /// + /// The SecureString value + /// A plain-text string version of the SecureString value + /// + /// Allocs unmanaged memory in process of converting to string. + /// Calls Marshal.ZeroFreeGlobalAllocUnicode to free unmanaged memory space for the ptr struct in finally { } + /// + public static string ToUnsecureString(this SecureString value) + { + if (value == null) + throw new ArgumentNullException("value", "Cannot convert null value.ToUnsecureString()"); + + var valuePtr = IntPtr.Zero; + + try + { + valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value); + + return Marshal.PtrToStringUni(valuePtr); + } + finally + { + Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); + } + } + + /// + /// Encrypts a SecureString value and returns it as a base64 string + /// + /// The SecureString value + /// An encrypted string value + /// + /// Leverages DPAPI and assumes CurrentUser data protection scope + /// Assumes that SecureString should not be disposed + /// + /// + /// This method calls ProtectedData.Protect(). Callers of this method should handle potential cryptographic exceptions. + /// + public static string ToEncryptedString(this SecureString value) + { + if (value == null) + throw new ArgumentNullException("value", "Cannot encrypt a null SecureString value"); + + var encryptedValue = ProtectedData.Protect(Encoding.Unicode.GetBytes(value.ToUnsecureString()), SecurityExtensions.additionalEntropy, DataProtectionScope.CurrentUser); + + return Convert.ToBase64String(encryptedValue); + } + + /// + /// Converts a plain-text string value to a SecureString object + /// + /// The string value + /// A SecureString representation of the string value + public static SecureString ToSecureString(this string value) + { + if (String.IsNullOrEmpty(value)) + throw new ArgumentNullException("value", "Cannot convert null value.ToSecureString()"); + + var secureValue = new SecureString(); + + value.ToCharArray() + .ToList() + .ForEach(c => + { + secureValue.AppendChar(c); + }); + + secureValue.MakeReadOnly(); + + return secureValue; + } + + /// + /// Decrypts an encrypted string value and returns it as a SecureString + /// + /// The base64 encoded encrypted string value + /// The decrypted string value wrapped in a SecureString + /// + /// Leverages DPAPI and assumes CurrentUser data protection scope + /// + /// + /// This method calls ProtectedData.Unprotect(). Callers of this method should handle potential cryptographic exceptions. + /// + public static SecureString ToDecryptedSecureString(this string value) + { + if (String.IsNullOrEmpty(value)) + throw new ArgumentNullException("value", "Cannot decrypt a null (or empty) String value"); + + var decryptedValue = ProtectedData.Unprotect(Convert.FromBase64String(value), SecurityExtensions.additionalEntropy, DataProtectionScope.CurrentUser); + + return Encoding.Unicode.GetString(decryptedValue).ToSecureString(); + } + } +} diff --git a/v9.2/Extensions/ServiceExtensions.cs b/v9.2/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000..682a5a8 --- /dev/null +++ b/v9.2/Extensions/ServiceExtensions.cs @@ -0,0 +1,413 @@ +/*================================================================================================================================ + + This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. + + THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + + We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object + code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software + product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the + Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims + or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code. + + =================================================================================================================================*/ +namespace Microsoft.Pfe.Xrm +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Xml.Linq; + + + using Microsoft.Pfe.Xrm.Diagnostics; + using Microsoft.Xrm.Sdk; + using Microsoft.Xrm.Sdk.Client; + using Microsoft.Xrm.Sdk.Query; + using System.ServiceModel; + using Microsoft.PowerPlatform.Dataverse.Client; + + public static class ServiceProxyExtensions + { + /// + /// Ensures credentials are structured properly for the authentication type + /// + public static void EnsureCredentials(this ServiceProxy proxy) + where TService : class + { + if (proxy.ClientCredentials == null) + { + return; + } + + switch (proxy.ServiceConfiguration.AuthenticationType) + { + case AuthenticationProviderType.ActiveDirectory: + proxy.ClientCredentials.UserName.UserName = null; + proxy.ClientCredentials.UserName.Password = null; + break; + case AuthenticationProviderType.Federation: + case AuthenticationProviderType.OnlineFederation: + case AuthenticationProviderType.LiveId: + proxy.ClientCredentials.Windows.ClientCredential = null; + break; + default: + return; + } + } + + /* + /// + /// Ensures the security token for non-AD scenarios is valid and renews if it near expiration or expired + /// + public static void EnsureValidToken(this ServiceProxy proxy) + where TService : class + { + if (proxy.ServiceConfiguration.AuthenticationType != AuthenticationProviderType.ActiveDirectory + && proxy.SecurityTokenResponse != null) + { + DateTime validTo = proxy.SecurityTokenResponse.Token.ValidTo; + + if (DateTime.UtcNow.AddMinutes(15) >= validTo) + { + //XrmCoreEventSource.Log.SecurityTokenRefreshRequired(validTo.ToString("u"), true); + + try + { + proxy.Authenticate(); + } + catch (Exception ex) + { + StringBuilder messageBuilder = ex.ToErrorMessageString(); + + //XrmCoreEventSource.Log.SecurityTokenRefreshFailure(validTo.ToString("u"), messageBuilder.ToString()); + + // Rethrow if current token is lost during authentication attempt + // or if current token has expired + if (proxy.SecurityTokenResponse == null + || DateTime.UtcNow >= validTo) + { + throw; + } + } + } + } + } + */ + } + + public static class OrganizationServiceExtensions + { + /// + /// Helper method for assigning common proxy-specific settings for impersonation, early-bound types, and channel timeout + /// + /// The service proxy + /// The options to configure on the service proxy + //public static void SetProxyOptions(this OrganizationServiceProxy proxy, OrganizationServiceProxyOptions options) + //{ + // if (!options.CallerId.Equals(Guid.Empty)) + // proxy.CallerId = options.CallerId; + + // if (options.ShouldEnableProxyTypes) + // proxy.EnableProxyTypes(); + + // if (!options.Timeout.Equals(TimeSpan.Zero) + // && options.Timeout > TimeSpan.Zero) + // proxy.Timeout = options.Timeout; + //} + + /// + /// Helper method for assigning common proxy-specific settings for impersonation, early-bound types, and channel timeout + /// + /// The service proxy + /// The options to configure on the service proxy + public static void SetProxyOptions(this ServiceClient proxy, OrganizationServiceProxyOptions options) + { + if (!options.CallerId.Equals(Guid.Empty)) + { + proxy.CallerId = options.CallerId; + } + + if (!options.Timeout.Equals(TimeSpan.Zero) && options.Timeout > TimeSpan.Zero) + { + //proxy.OrganizationWebProxyClient.ChannelFactory.Endpoint.Binding.OpenTimeout = options.Timeout; + //proxy.OrganizationWebProxyClient.ChannelFactory.Endpoint.Binding.CloseTimeout = options.Timeout; + //proxy.OrganizationWebProxyClient.ChannelFactory.Endpoint.Binding.ReceiveTimeout = options.Timeout; + //proxy.OrganizationWebProxyClient.ChannelFactory.Endpoint.Binding.SendTimeout = options.Timeout; + } + + } + + /// + /// Performs an iterative series of RetrieveMultiple requests in order to obtain all pages of results + /// + /// The current IOrganizationService instance + /// The query to be executed + /// True = perform iterative paged query requests, otherwise return first page only + /// An upper limit on the maximum number of entity records that should be retrieved as the query results - useful when the total size of result set is unknown and size may cause OutOfMemoryException + /// An operation to perform on each page of results as it's retrieved + /// An EntityCollection containing the results of the query. Details reflect the last page retrieved (e.g. MoreRecords, PagingCookie, etc.) + /// + /// CRM limits query response to paged result sets of 5,000. This method encapsulates the logic for performing subsequent + /// query requests so that all results can be retrieved. + /// + /// If retrieving all pages, max result count is essentially unbound - Int64.MaxValue: 9,223,372,036,854,775,807 + /// + public static EntityCollection RetrieveMultiple(this IOrganizationService service, QueryBase query, bool shouldRetrieveAllPages, Action pagedOperation = null) + { + return service.RetrieveMultiple(query, shouldRetrieveAllPages, Int64.MaxValue, pagedOperation); + } + + /// + /// Perform an iterative series of RetrieveMultiple requests in order to obtain all results up to the provided maximum result count + /// + /// The current IOrganizationService instance + /// The QueryExpression query to be executed + /// An upper limit on the maximum number of entity records that should be retrieved as the query results - useful when the total size of result set is unknown and size may cause OutOfMemoryException + /// An operation to perform on each page of results as it's retrieved + /// An EntityCollection containing the results of the query. Details reflect the last page retrieved (e.g. MoreRecords, PagingCookie, etc.) + /// + /// CRM limits query response to paged result sets of 5,000. This method encapsulates the logic for performing subsequent + /// query requests so that all results can be retrieved. + /// + /// Inherently retrieves all pages up to the max result count. If max result count is less than initial page size, then page size is adjusted down to honor the max result count + /// + public static EntityCollection RetrieveMultiple(this IOrganizationService service, QueryBase query, int maxResultCount, Action pagedOperation = null) + { + return service.RetrieveMultiple(query, true, maxResultCount, pagedOperation); + } + + /// + /// Performs an iterative series of RetrieveMultiple requests in order to obtain all pages of results up to the provided maximum result count + /// + /// The current IOrganizationService instance + /// The QueryExpression query to be executed + /// True = perform iterative paged query requests, otherwise return first page only + /// An upper limit on the maximum number of entity records that should be retrieved as the query results - useful when the total size of result set is unknown and size may cause OutOfMemoryException + /// An operation to perform on each page of results as it's retrieved + /// An EntityCollection containing the results of the query. Details reflect the last page retrieved (e.g. MoreRecords, PagingCookie, etc.) + /// + /// CRM limits query response to paged result sets of 5,000. This method encapsulates the logic for performing subsequent + /// query requests so that all results can be retrieved. + /// + private static EntityCollection RetrieveMultiple(this IOrganizationService service, QueryBase query, bool shouldRetrieveAllPages, long maxResultCount, Action pagedOperation) + { + if (query == null) + throw new ArgumentNullException("query", "Must supply a query for the RetrieveMultiple request"); + if (maxResultCount <= 0) + throw new ArgumentException("maxResultCount", "Max entity result count must be a value greater than zero."); + + var qe = query as QueryExpression; + + if (qe != null) + { + return service.RetrieveMultiple(qe, shouldRetrieveAllPages, maxResultCount, pagedOperation); + } + else + { + var fe = query as FetchExpression; + + if (fe != null) + { + return service.RetrieveMultiple(fe, shouldRetrieveAllPages, maxResultCount, pagedOperation); + } + } + + throw new ArgumentException("This method only handles FetchExpression and QueryExpression types.", "query"); + } + + /// + /// Performs an iterative series of RetrieveMultiple requests using QueryExpression in order to obtain all pages of results up to the provided maximum result count + /// + /// The current IOrganizationService instance + /// The QueryExpression query to be executed + /// True = perform iterative paged query requests, otherwise return first page only + /// An upper limit on the maximum number of entity records that should be retrieved as the query results - useful when the total size of result set is unknown and size may cause OutOfMemoryException + /// An operation to perform on each page of results as it's retrieved + /// An EntityCollection containing the results of the query. Details reflect the last page retrieved (e.g. MoreRecords, PagingCookie, etc.) + /// + /// CRM limits query response to paged result sets of 5,000. This method encapsulates the logic for performing subsequent + /// query requests so that all results can be retrieved. + /// + private static EntityCollection RetrieveMultiple(this IOrganizationService service, QueryExpression query, bool shouldRetrieveAllPages, long maxResultCount, Action pagedOperation) + { + // Establish page info (only if TopCount not specified) + if (query.TopCount == null) + { + if (query.PageInfo == null) + { + // Default to first page + query.PageInfo = new PagingInfo() + { + Count = QueryExtensions.DefaultPageSize, + PageNumber = 1, + PagingCookie = null, + ReturnTotalRecordCount = false + }; + } + else if (query.PageInfo.PageNumber <= 1 + || query.PageInfo.PagingCookie == null) + { + // Reset to first page + query.PageInfo.PageNumber = 1; + query.PageInfo.PagingCookie = null; + } + + // Limit initial page size to max result if less than current page size. No risk of conversion overflow. + if (query.PageInfo.Count > maxResultCount) + { + query.PageInfo.Count = Convert.ToInt32(maxResultCount); + } + } + + // Track local long value to avoid expensive IEnumerable.LongCount() method calls + long totalResultCount = 0; + var allResults = new EntityCollection(); + + while (true) + { + // Retrieve the page + EntityCollection page = service.RetrieveMultiple(query); + + // Capture the page + if (totalResultCount == 0) + { + // First page + allResults = page; + } + else + { + allResults.Entities.AddRange(page.Entities); + } + + // Invoke the paged operation if non-null + pagedOperation?.Invoke(page); + + // Update the count of pages retrieved and processed + totalResultCount = totalResultCount + page.Entities.Count; + + // Determine if we should retrieve the next page + if (shouldRetrieveAllPages + && totalResultCount < maxResultCount + && page.MoreRecords) + { + // Setup for next page + query.PageInfo.PageNumber++; + query.PageInfo.PagingCookie = page.PagingCookie; + + long remainder = maxResultCount - totalResultCount; + + // If max result count is not divisible by page size, then final page may be less than the current page size and should be sized to remainder. + // No risk of coversion overflow. + if (query.PageInfo.Count > remainder) + { + query.PageInfo.Count = Convert.ToInt32(remainder); + } + } + else + { + allResults.CopyFrom(page); + break; + } + } + + return allResults; + } + + /// + /// Performs an iterative series of RetrieveMultiple requests using FetchExpression in order to obtain all pages of results up to the provided maximum result count + /// + /// The current IOrganizationService instance + /// The FetchExpression query to be executed + /// True = perform iterative paged query requests, otherwise return first page only + /// An upper limit on the maximum number of entity records that should be retrieved as the query results - useful when the total size of result set is unknown and size may cause OutOfMemoryException + /// An operation to perform on each page of results as it's retrieved + /// An EntityCollection containing the results of the query. Details reflect the last page retrieved (e.g. MoreRecords, PagingCookie, etc.) + /// + /// CRM limits query response to paged result sets of 5,000. This method encapsulates the logic for performing subsequent + /// query requests so that all results can be retrieved. + /// + private static EntityCollection RetrieveMultiple(this IOrganizationService service, FetchExpression fetch, bool shouldRetrieveAllPages, long maxResultCount, Action pagedOperation) + { + XElement fetchXml = fetch.ToXml(); + int pageNumber = fetchXml.GetFetchXmlPageNumber(); + string pageCookie = fetchXml.GetFetchXmlPageCookie(); + int pageSize = fetchXml.GetFetchXmlPageSize(QueryExtensions.DefaultPageSize); + + // Establish the first page based on lesser of initial/default page size or max result count (will be skipped if top count > 0) + if (pageSize > maxResultCount) + { + pageSize = Convert.ToInt32(maxResultCount); + } + + if (pageNumber <= 1 + || String.IsNullOrWhiteSpace(pageCookie)) + { + // Ensure start with first page + fetchXml.SetFetchXmlPage(null, 1, pageSize); + } + else + { + // Start with specified page + fetchXml.SetFetchXmlPage(pageCookie, pageNumber, pageSize); + } + + fetch.Query = fetchXml.ToString(); + + // Track local long value to avoid expensive IEnumerable.LongCount() method calls + long totalResultCount = 0; + var allResults = new EntityCollection(); + + while (true) + { + // Retrieve the page + EntityCollection page = service.RetrieveMultiple(fetch); + + // Capture the page + if (totalResultCount == 0) + { + // First page + allResults = page; + } + else + { + allResults.Entities.AddRange(page.Entities); + } + + // Invoke the paged operation if non-null + pagedOperation?.Invoke(page); + + // Update the count of pages retrieved and processed + totalResultCount = totalResultCount + page.Entities.Count; + + // Determine if we should retrieve the next page + if (shouldRetrieveAllPages + && totalResultCount < maxResultCount + && page.MoreRecords) + { + // Setup for next page + pageNumber++; + + long remainder = maxResultCount - totalResultCount; + + // If max result count is not divisible by page size, then final page may be less than the current page size and should be sized to remainder. + // No risk of coversion overflow. + if (pageSize > remainder) + { + pageSize = Convert.ToInt32(remainder); + } + + fetch.SetPage(page.PagingCookie, pageNumber, pageSize); + } + else + { + allResults.CopyFrom(page); + break; + } + } + + return allResults; + } + } +} \ No newline at end of file diff --git a/v9.2/Microsoft.Pfe.Xrm.Core v9.2.csproj b/v9.2/Microsoft.Pfe.Xrm.Core v9.2.csproj new file mode 100644 index 0000000..de2bbb0 --- /dev/null +++ b/v9.2/Microsoft.Pfe.Xrm.Core v9.2.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + false + Microsoft.Pfe.Xrm.Core + Microsoft.Pfe.Xrm.Core + Microsoft Corporation + 9.0.0.0 + false + https://github.com/seanmcne/XrmCoreLibrary + 2.0.0.0 + + + + + + + + + + + diff --git a/v9.2/Microsoft.Pfe.Xrm.Core v9.2.sln b/v9.2/Microsoft.Pfe.Xrm.Core v9.2.sln new file mode 100644 index 0000000..e29b089 --- /dev/null +++ b/v9.2/Microsoft.Pfe.Xrm.Core v9.2.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31729.503 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Pfe.Xrm.Core v9.2", "Microsoft.Pfe.Xrm.Core v9.2.csproj", "{98353AD8-4872-4145-A4A1-6859370DCBAD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {98353AD8-4872-4145-A4A1-6859370DCBAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98353AD8-4872-4145-A4A1-6859370DCBAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98353AD8-4872-4145-A4A1-6859370DCBAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98353AD8-4872-4145-A4A1-6859370DCBAD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {021A75D3-CC19-4EE3-8FB4-6784BA343343} + EndGlobalSection +EndGlobal diff --git a/v9.2/ProjectUrl.txt b/v9.2/ProjectUrl.txt new file mode 100644 index 0000000..00bd1e0 --- /dev/null +++ b/v9.2/ProjectUrl.txt @@ -0,0 +1 @@ +https://github.com/seanmcne/XrmCoreLibrary \ No newline at end of file