Improving Dynamics CRM connection performance with Singleton pattern

Calling Dynamics CRM services is slow process.The authentication process adds the most overhead in establishing a connection to CRM. The call needs to go through multiple steps in order to be authenticated and authorized to access the data in Dynamics CRM.

If you have an application that needs to perform CRUD operation outside of CRM you can leave the connection open to Dynamics CRM with Singleton pattern.

Singleton pattern:

public class XrmConnectionProvider
    {
        private static IOrganizationService instance;
        private static object _lockObject = new object();

		private static IConfigurationService _configurationService;
        
		private XrmConnectionProvider() { }
		
        public static IOrganizationService CRMService
        {
            get
            {
                try
                {
                    lock (_lockObject)
                    {
                        if (instance == null)
                        {
							var container = IoC.Initialize();
							_configurationService = container.GetInstance<IConfigurationService>();


                            instance = Connect();
                        }
                        return instance;
                    }
                }
                catch (Exception ex)
                {
					throw new Exception("Unable to connect to CRM", ex);
                }

            }
        }
        private static IOrganizationService Connect()
        {
		     //user name and password stored in a config file
			var config = _configurationService.Get<XrmClientConfiguration>();

			Uri dInfo = new Uri(config.XrmUri);
			ClientCredentials clientcred = new ClientCredentials();
			clientcred.UserName.UserName = config.XrmClientCredUserName;
			clientcred.UserName.Password = config.XrmClientCredPassword;


			#region on-premise/online

			DiscoveryServiceProxy dsp = new DiscoveryServiceProxy(dInfo, null, clientcred, null);
            dsp.Authenticate();
            RetrieveOrganizationsRequest rosreq = new Microsoft.Xrm.Sdk.Discovery.RetrieveOrganizationsRequest();
            RetrieveOrganizationsResponse r = (RetrieveOrganizationsResponse)dsp.Execute(rosreq);

            //get the OrganizationService
            OrganizationServiceProxy _serviceproxy = new OrganizationServiceProxy(new Uri(config.XrmOrgServiceProxy), null, clientcred, null);
            //In order to use the generated types when dealing with the organization service, you have to add the ProxyTypesBehavior to the endpoint Behaviors collection. This instructs the OrganizationServiceProxy to look in the assembly for classes with certain attributes to use. The generated classes are already attributed with these attributes. Simply, this makes all interactions with the organization service to be done using the generated typed classes for each entity instead of the generic Entity class we used earlier.
            _serviceproxy.ServiceConfiguration.CurrentServiceEndpoint.EndpointBehaviors.Add(new ProxyTypesBehavior());
            //Do not forget to include _serviceproxy.EnableProxyTypes();. Without this line,you will be unable to use early binding.
            _serviceproxy.EnableProxyTypes();
            IOrganizationService service = (IOrganizationService)_serviceproxy;

            #endregion
            return service;
        }
    }

Now in your code you can call below class and it will only generate one instance instead of calling it for each request :

 var CRMService= XrmConnectionProvider.Connect();

Here is the result of URL based test on Visual studio online after applying above approach :

The test ran for duration of 120 seconds with 25 users load hitting my Web API application (hosted on azure) retrieving Dynamics CRM contacts.

Before:

url-based-test-dynamics-crm-before

After:

url-based-test-dynamics-crm-after

 

The only caveat to this method is that you need to monitor your token expiry depending on your implementation method and renew it before expiry.
You can read about it on this blog post.
But before that you need to increase the life time of token if you’ve configured your CRM for IFD.  By default the ADFS token last for 60 minutes. By using Windows PowerShell, you can change the TokenLifetime property for the relying party objects that you created from 60 minutes to a longer period, such as 480 minutes (8 hours).

From MSDN best practices:

Monitor your WCF security token (Token) and refresh it before it expires so that you do not lose the token and have to start over with authentication. To check the token, create a custom class that inherits from the OrganizationServiceProxy or DiscoveryServiceProxy class and that implements the business logic to check the token. Or wrap the proxy classes in a new class. Another technique is to explicitly check the token before each call to the web service. Example code that demonstrates these techniques can be found in the ManagedTokenDiscoveryServiceProxy, ManagedTokenOrganizationServiceProxy, and AutoRefreshSecurityToken classes in the Helper code: ServerConnection class topic.

Adding a Dynamics CRM Status Reason with custom values

The default status reasons for Dynamics CRM custom entities are 1 for active and 2 for inactive.

dynamics-crm-status-reason

It’s not possible to add Status Reason (statuscode) with values less than 551,870,000 through MS Dynamics CRM user interface and added new values are read only and incremental. If the default status reason is somehow deleted or you want to create a status reason with specific values, you can add it using CRM API’s.

Here is an example of adding an active status reason in CRM with value of 1:

(in my case the default active status reason was deleted)

         var response= ((InsertStatusValueResponse)ServiceContext.Execute
             (
                new InsertStatusValueRequest
                 {
                     AttributeLogicalName = "statuscode",
                     EntityLogicalName = YourEntity.EntityLogicalName,
                     Label = new Microsoft.Xrm.Sdk.Label("Active", 1033),
                     StateCode = 0, //Status: 0 active & 1 inactive 
                     Value = 1 
                 }
             )).NewOptionValue;

Retrieve Multiple Dynamics CRM Records with XrmServiceToolkit

In order to install XrmServiceToolkit check this blog post.

In this library, I found RetrieveMultiple function really helpful and I use it for different of purposes like to create a custom numbering system in CRM records, for instance since you can retrieve multiple record, you can loop through all your record and get all the fields in those.

here is the signature of the function :

     retrieveMultipleRecords = function (type, options, successCallback, errorCallback, onComplete, async)

One important thing is that successCallback only returns one page of records (50) at the time so you should loop through the results and push the records into an array outside of the function.

Here are Some Sample retrieveMultipleRecords calls:

1. This sample retrieves all accounts with all fields in the account form and push them in accountArray :

      var accountArray = new Array();

        XrmServiceToolkit.Rest.RetrieveMultiple(
                     "AccountSet",
                     "",// if you leave it empty, it retrieves all fields
                     function (results) {
                         if (results.length >= 1)
                             for (var i = 0; i < results.length; i++) {
                                 accountArray.push(results[i]);
                             }
                     },
                     function (error) {
                         alert(error.message);
                     },
                     function onComplete() {
                         //alert(" records should have been retrieved.");
                     },
                     false
                 );

     alert(accountArray.length); //this should show the number of account records in CRM

2. This sample retrieves only AccountID and AcountNumber when “new_AccountingApproved”(custom field) check box is checked:

   var accountArray = new Array();

        XrmServiceToolkit.Rest.RetrieveMultiple(
                     "AccountSet",
                     "$select=AccountId, AccountNumber &$filter=new_AccountingApproved eq true",
                     function (results) {
                         if (results.length >= 1)
                           //accountArray = results;  //don't do this! instead use push to array. for instance if you have 58 accounts in CRM, accountArray will only get 8 of them, because successCallback will run twice.
                             for (var i = 0; i < results.length; i++) {
                                 accountArray.push(results[i]);
                             }
                     },
                     function (error) {
                         alert(error.message);
                     },
                     function onComplete() {
                         //alert(" records should have been retrieved.");
                     },
                     false
                 );

In case you are interested ,Here is RetrieveMultipleRecords function definition from Source code of the project:

var retrieveMultipleRecords = function (type, options, successCallback, errorCallback, onComplete, async) {
        ///<summary>
        /// Sends synchronous/asynchronous request to retrieve records.
        ///</summary>
        ///<param name="type" type="String">
        /// The Schema Name of the Entity type record to retrieve.
        /// For an Account record, use "Account"
        ///</param>
        stringParameterCheck(type, "XrmServiceToolkit.REST.retrieveMultipleRecords requires the type parameter is a string.");
        ///<param name="options" type="String">
        /// A String representing the OData System Query Options to control the data returned
        ///</param>
        if (options != null)
            stringParameterCheck(options, "XrmServiceToolkit.REST.retrieveMultipleRecords requires the options parameter is a string.");
        ///<param name="successCallback" type="Function">
        /// The function that will be passed through and be called for each page of records returned.
        /// Each page is 50 records. If you expect that more than one page of records will be returned,
        /// this function should loop through the results and push the records into an array outside of the function.
        /// Use the OnComplete event handler to know when all the records have been processed.
        /// </param>
        callbackParameterCheck(successCallback, "XrmServiceToolkit.REST.retrieveMultipleRecords requires the successCallback parameter is a function.");
        ///<param name="errorCallback" type="Function">
        /// The function that will be passed through and be called by a failed response.
        /// This function must accept an Error object as a parameter.
        /// </param>
        callbackParameterCheck(errorCallback, "XrmServiceToolkit.REST.retrieveMultipleRecords requires the errorCallback parameter is a function.");
        ///<param name="OnComplete" type="Function">
        /// The function that will be called when all the requested records have been returned.
        /// No parameters are passed to this function.
        /// </param>
        callbackParameterCheck(onComplete, "XrmServiceToolkit.REST.retrieveMultipleRecords requires the OnComplete parameter is a function.");
        ///<param name="async" type="Boolean">
        /// A Boolean representing if the method should run asynchronously or synchronously
        /// true means asynchronously. false means synchronously
        /// </param>
        booleanParameterCheck(async, "XrmServiceToolkit.REST.retrieveMultipleRecords requires the async parameter is a boolean.");

Resolve Connectivity Issue to Microsoft Dynamics GP Web Service in Dynamics Connector

If you are using Dynamics Connector to integrate GP and CRM, you need GP web services installed, and defined and in Adapter setting of dynamics connector. The error that I’ve seen a lot during Test Setting for GP web services in Connector is:

Dynamics Connector error

Dynamics Connector GP web service error

 

To resolve it make sure:

  1.  You have entered correct credentials and URL.

GP web service URL format should be like:

http://YourServer:48620/Dynamics/GPService/GPService

(YourServer is the server GP web services installed on and default port is 48620)

If you want to test the credential to see you have appropriate access, you can run IE as username defined in your setting and browse to this address:

http://YourServer:48620/Dynamics/GPService

The result should be like this screen:

GPWebService

 

To verify that the Microsoft Dynamics GP Web services run correctly, follow this  link.

  1. If you still can’t run the test setting successfully it might be user role in Dynamics Security Console. The user must have “Microsoft Dynamics Integration-All Companies” and “Microsoft Dynamics Integration-Integrated Company”

In order to grant those role follow these steps:

  • Log in to GP server and run “Dynamics Security Console” from administrative tools.
  • Under “Microsoft Dynamics GP Web Services” go to “Role Assignment”

dynamicsSecurityConsole

  • Click on Add on the right side of console and make sure you add the user with “Microsoft Dynamics Integration-All Companies” and “Microsoft Dynamics Integration-Integrated Company” Roles.

hopefully you will get this result in the end:

result

 

 

 

Merging Entities in Dynamics CRM using Webservices and MergeRequest

If you have lots of duplicated accounts in you system that has lots of records like Orders or Invoices under them, you can use MergeRequest to merge those accounts.

The good thing about merge is that it will handle all relationships for you and you don’t have to worry about related order or invoices ,etc. you also define what fields you want to copy from duplicated record to main record.

Here is a sample code for merging all duplicated accounts in CRM:

I retrieve all accounts in CRM where their account number contains “-0” and then based on that criteria retrieved the duplicated.

If you have more than 5000 records , the Retrieve Multiple will only return first 5000 records so it’s better to use “ConditionOperator” to restrict the number of accounts to be retrieved in your QueryExpression. I find it really useful and fast. Use  “ConditionOperator.Like“…

private void btnMerge_Click(object sender, EventArgs e)
        {
            //connecting to the service and getting the credentials from app.confi
            ClientCredentials Credentials = new ClientCredentials();
            ClientCredentials oClientCredential = new ClientCredentials();
            oClientCredential.UserName.UserName = System.Configuration.ConfigurationManager.AppSettings["userName"];
            oClientCredential.UserName.Password = System.Configuration.ConfigurationManager.AppSettings["password"];

            Uri OrganizationUri = new Uri(System.Configuration.ConfigurationManager.AppSettings["CRMURI"]);
            Uri HomeRealmUri = null;

            using (OrganizationServiceProxy serviceProxy = new OrganizationServiceProxy(OrganizationUri, HomeRealmUri, oClientCredential, null))
            {
                try
                {
                    IOrganizationService service = (IOrganizationService)serviceProxy;

                    QueryExpression query = new QueryExpression();

                    query.EntityName = "account";

                    query.ColumnSet = new ColumnSet() { AllColumns = true };

                    query.Criteria = new FilterExpression();

                    query.Criteria.FilterOperator = LogicalOperator.And;

                    query.Criteria.Conditions.Add

                    (
                     new ConditionExpression("statuscode", ConditionOperator.Equal, 1)//all the active accounts
                    );

                    query.Criteria.Conditions.Add(new ConditionExpression("accountnumber", ConditionOperator.Like, "%-0%"));

                    EntityCollection entities = service.RetrieveMultiple(query);

                    //loop through the list of retrieved entities and find the duplicate that match the criteria 

                    foreach (var item in entities.Entities)
                    {
                        string accountNumber = item.GetAttributeValue<string>("accountnumber").ToString();
                        string mainAccountNumber = item.GetAttributeValue<string>("dtm_mainaccountnumber").ToString();
                        string accountName = item.GetAttributeValue<string>("name").ToString();

                        if (accountNumber.Contains("-0"))
                        {
                            int start = accountNumber.IndexOf('-');
                            string strAdressCode = accountNumber.Substring(start + 1, accountNumber.Length - start - 1);
                            int number;
                            if (Int32.TryParse(strAdressCode, out number))
                            {
                                EntityReference target = new EntityReference();
                                target.Id = item.Id;
                                target.LogicalName = "account";

                                string newAccountNumber = accountNumber.Substring(0, start + 1) + number;

                                QueryExpression innerQuery = new QueryExpression();
                                innerQuery.EntityName = "account";
                                innerQuery.ColumnSet = new ColumnSet() { AllColumns = true };
                                innerQuery.Criteria = new FilterExpression();
                                innerQuery.Criteria.FilterOperator = LogicalOperator.And;
                                innerQuery.Criteria.Conditions.Add(new ConditionExpression("statuscode", ConditionOperator.Equal, 1));
                                innerQuery.Criteria.Conditions.Add(new ConditionExpression("dtm_mainaccountnumber", ConditionOperator.Equal, mainAccountNumber));
                                //innerQuery.Criteria.Conditions.Add(new ConditionExpression("name", ConditionOperator.Equal, accountName));
                                innerQuery.Criteria.Conditions.Add(new ConditionExpression("accountnumber", ConditionOperator.Equal, newAccountNumber));

                                EntityCollection innerEntities = service.RetrieveMultiple(innerQuery);

                                if (innerEntities.Entities.Count >= 1)
                                {

                                    // Create the request for Merge
                                    MergeRequest merge = new MergeRequest();
                                    merge.SubordinateId = innerEntities.Entities[0].Id;
                                    merge.Target = target;
                                    merge.PerformParentingChecks = false;
                                    
                                    // Create another account to hold new data to merge into the entity.
                                    // If you use the subordinate account object, its data will be merged.
                                    Entity updateContent = new Entity("account");
                                    updateContent["dtm_ismerged"] = "merged";

                                    // Set the content you want updated on the merged account
                                    merge.UpdateContent = updateContent;

                                    service.Execute(merge);
                                }
                            }
                            continue;
                        }
                    }
                }
                catch (Exception ex)
                {
                    System.Windows.Forms.MessageBox.Show(ex.ToString());
                    string fileloc = "C:\\myLog.txt";

                    if (File.Exists(fileloc))
                    {
                        using (StreamWriter sw = new StreamWriter(fileloc))
                        {
                            sw.Write(ex.ToString());
                        }
                    }

                }
            }
        }