Tag Archives: create

Microsoft Dynamics 365: Server Side Bulk Records Operations

In Microsoft Dynamics 365 Customer Engagement server side automation, e.g. plugins and custom workflow activities, we can write code to create, retrieve, update and delete individual records. When we need to carry out these operations on multiple records, we can use a loop and query the database for each record. However, a much more efficient way to carry out bulk operations is to use the ExecuteMultipleRequest message (to Create, Update and Delete records in bulk), the focus on this article. Also, the application example demonstrates how to make use of these bulk operations methods as well as makes use of the RetrieveMultiple Query Expression (i.e. how to retrieve multiple records in single query) .

The bulk operations suite of methods below can be incorporated into a Dynamics 365 project and be called upon to carry out bulk operations. These methods provide an efficient way of carrying out operations on multiple records, thereby enhancing web pages response rates and user experience.

Bulk Create Records

public static void BulkCreateRecords(IOrganizationService service, EntityCollection entityCollection)
{            
	var multipleRequest = new ExecuteMultipleRequest()
	{                 
		Settings = new ExecuteMultipleSettings()
		{
			ContinueOnError = false,
			ReturnResponses = true
		},                
		Requests = new OrganizationRequestCollection()
	};

	foreach (Entity entity in entityCollection.Entities)
	{
		CreateRequest createRequest = new CreateRequest { Target = entity };
		multipleRequest.Requests.Add(createRequest);
	}

	ExecuteMultipleResponse multipleResponse = (ExecuteMultipleResponse)service.Execute(multipleRequest);
}

Bulk Update Records

public static void BulkUpdateRecords(IOrganizationService service, EntityCollection entityCollection)
{           
	var multipleRequest = new ExecuteMultipleRequest()
	{               
		Settings = new ExecuteMultipleSettings()
		{
			ContinueOnError = false,
			ReturnResponses = true
		},               
		Requests = new OrganizationRequestCollection()
	};
   
	foreach (Entity entity in entityCollection.Entities)
	{
		UpdateRequest updateRequest = new UpdateRequest { Target = entity };
		multipleRequest.Requests.Add(updateRequest);
	}

	ExecuteMultipleResponse multipleResponse = (ExecuteMultipleResponse)service.Execute(multipleRequest);
}

Bulk Delete Records

public static void BulkDeleteRecords(IOrganizationService service, EntityCollection entityCollection)
{            
	var multipleRequest = new ExecuteMultipleRequest()
	{ 
		Settings = new ExecuteMultipleSettings()
		{
			ContinueOnError = false,
			ReturnResponses = true
		},                
		Requests = new OrganizationRequestCollection()
	};
   
	foreach (Entity entity in entityCollection.Entities)
	{
		EntityReference entityRef = new EntityReference(entity.LogicalName, entity.Id);

		DeleteRequest deleteRequest = new DeleteRequest { Target = entityRef };
		multipleRequest.Requests.Add(deleteRequest);
	}

	ExecuteMultipleResponse multipleResponse = (ExecuteMultipleResponse)service.Execute(multipleRequest);
}

Example: Calling bulk operations methods and a demonstration of RetrieveMultiple

In the example below, we show how the methods provided above can be put to use to carry out bulk operations, by calling the bulk delete records method, as well as we make use of the RetrieveMultiple Query expression to retrieve multiple records in single database query (see the code below the comment “RetrieveMultiple Query Expression”).

About the plugin

The plugin example below is registered on the Update message Synchronously on the Account entity and PreOperation event pipeline stage of execution. The plugin is triggered by a custom Two Option field called Delete Tasks (logical name: “hos_deletetasks”) field. Therefore, when the user changes the value of the “hos_deletetasks” field from No/false to Yes/true, the plugin retrieves all the tasks that relate to that account and deletes them.

Other things to note

I have added the BulkCreateteRecords, BulkUpdateRecords and BulkDeleteRecords methods to my D365Helpers class, which contains my other reusable methods, so that they can used across multiple server-side automation classes.

using System;
using System.ServiceModel;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;

namespace ItsFascinating.Plugins.D365
{
    public class DeleteFollowUpTasks : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {                  
            if (serviceProvider == null)
            {
                throw new ArgumentNullException("localContext");
            }
            #region Plugin Setup Variables
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            #endregion
            #region Validation Prior Execution
            string pluginMessageUpdate = "update";           

            if (context.Stage != 20)
            {
                tracingService.Trace("Invalid Stage: Stage = {0}", context.Stage);
                return;
            }
            
            if (!context.MessageName.ToLower().Equals(pluginMessageUpdate))
            {
                tracingService.Trace("Invalid Message: MessageName = {0}", context.MessageName);
                return;
            }
            #endregion
           
            if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
            {
                Entity primaryEntity = context.InputParameters["Target"] as Entity;
                string accountLogicalName = "account";

                if (primaryEntity.LogicalName != accountLogicalName)
                {
                    tracingService.Trace("Invalid Entity: EntityName = {0}", primaryEntity.LogicalName);
                    return;
                }                    

                try
                {
                    if (primaryEntity.LogicalName.Equals(accountLogicalName))
                        DeleteTasks(service, primaryEntity, tracingService);                   
                }                
                catch (FaultException ex)
                {
                    throw new InvalidPluginExecutionException(string.Format("An error occurred in the DeleteFollowUpTasks plug-in. {0}", ex.Message));
                }
                catch (Exception ex)
                {
                    tracingService.Trace(string.Format("DeleteFollowUpTasks plugin: {0}", ex.ToString()));
                    throw new Exception(string.Format("An error occurred in the DeleteFollowUpTasks plug-in. {0}", ex.Message));
                }
            }
        }

        private static void DeleteTasks(IOrganizationService service, Entity primaryEntity, ITracingService tracingService)
        {
            object deleteTasksFieldObj = new object(); ;
            string deleteTasksLogicalName = "hos_deletetasks";
            bool deleteTasks;
            string taskLogicalName = "task";
            string subject = "subject";
            string description = "description";
            string accountRegardingFieldLogicalName = "regardingobjectid";
            bool containsDeleteTasks = primaryEntity.Attributes.TryGetValue(deleteTasksLogicalName, out deleteTasksFieldObj);

            if (containsDeleteTasks)
            {
                deleteTasks = (bool)deleteTasksFieldObj;

                if (deleteTasks)
                {
                    //RetrieveMultiple Query Expression
                    var tasksQuery = new QueryExpression(taskLogicalName);
                    tasksQuery.ColumnSet = new ColumnSet(subject, description);
                    tasksQuery.Criteria.AddCondition(accountRegardingFieldLogicalName, ConditionOperator.Equal, primaryEntity.Id);
                    var tasksResults = service.RetrieveMultiple(tasksQuery);

                    if (tasksResults.Entities.Count > 0)
                        D365Helpers.BulkDeleteRecords(service, tasksResults);
                }
            }
        }
    }
}

Microsoft Dynamics 365: Varying Form Behavior On Form-type and Save-mode

Microsoft Dynamics 365 offers the ability to vary the behavior of forms based on the form type and how the form is saved, using JavaScript. In this blog post, we cover how we can make use of this flexibility offered by the platform to deliver unique solutions for end users.

Summary of the key syntax

Here is a high-level summary of the main Microsoft Dynamics 365 JavaScript syntax covered in this blog post.

  • Getting the type of form:
    formContext.ui.getFormType();
  • Hide a tab on a form (where “TabName” represents the name of the tab you would like to hide):
    formContext.ui.tabs.get("TabName").setVisible(false);
  • Get the object with methods to manage the Save event:
    executionContext.getEventArgs();
  • Get the form Save Mode:
    executionContext.getEventArgs().getSaveMode();
  • Inhibit the default save behavior on a form
    executionContext.getEventArgs().preventDefault();
  • Check if the default save behavior was inhibited
    executionContext.getEventArgs().isDefaultPrevented();
  • Get the form context
    executionContext.getFormContext();

Form Type

To get the Form Type, you use the following JavaScript code:

formContext.ui.getFormType();

Here are the different Dynamics 365 form types and their respective JavaScript return values:

Form type Return Value
Undefined0
Create1
Update2
Read Only3
Disabled4
Bulk Edit6

Application Example: Varying form behavior on Form Type

In this application example, we will hide the Products and Services tab on the Create type form (with JavaScript Return Value = 1). However, it will be still be visible on all other applicable form types. This prevents users from adding information to the Products and Services tab before a Contact record has been created and saved. Therefore, the user must first create the Contact record (using the Create form type) and after the record has been created, the users will be presented with the Update form type (with JavaScript Return Value = 2) and will be able to access the Products and Services tab as well as its contents.

The Contact entity’s create form type will look similar to this (i.e. the Products and Services tab is hidden):
Contact - Create form type

In contrast, the Update form type still shows the Products and Services tab.
Contact - Update form type

Here is the code to executes the requirement above.

//Hide Products and Services tab on Create form 
function HideProductsServicesTab(executionContext) {   
  //Get form context   
  var formContext = executionContext.getFormContext();   
  //Get form type   
  var formType = formContext.ui.getFormType();   
  //If formtype is Create, hide Products and Services tab on form     
  if (formType == 1) {
    formContext.ui.tabs.get("productsandservices").setVisible(false);   
  }   
  //To see the form type return value in your browser console
  console.log("Form type = " + formType); 
}

Save Mode

To get the form’s Save Mode, you use the following JavaScript code:

executionContext.getEventArgs().getSaveMode();

Here are Dynamics 365’s different Save Modes and their respective JavaScript return values as well as the applicable entities.

Entity Save Mode Return Value
AllSave1
AllSave and Close2
AllDeactivate5
AllReactivate6
EmailSend7
LeadDisqualify15
LeadQualify16
User or Team owned entitiesAssign47
ActivitiesSave as Completed58
AllSave and New59
AllAuto Save70

Application Example: Varying form behavior on Save Mode

Here is an example of how you can vary the form’s behavior on the Save Mode.

//Inhibit auto save functionality on a form
function InhibitAutoSave(executionContext) {
  var eventArgs = executionContext.getEventArgs();
  //To see the Save Mode return value in your browser console
  console.log("Save Mode = " + eventArgs.getSaveMode());
  //If save mode is 'Save and Close' or 'Auto Save', inhibit default behavior i.e. save 
  if (eventArgs.getSaveMode() == 2 || eventArgs.getSaveMode() == 70) {
    eventArgs.preventDefault(); 
  } 
  //To see if the auto save behavior was prevented in your browser console
  console.log("Default behaviour prevented: " + eventArgs.isDefaultPrevented());
 }

If you have some unsaved changes on a form, the code above inhibits the regular form auto save behavior as well as inhibits the regular Save and Close auto save behavior, that you ordinarily get when you navigate away from from a form with unsaved changes. Instead of auto-saving (i.e. the default behavior), if you try to navigate away from a form with an unsaved changes, the JavaScript code above will block the auto save behavior and offer you the pop up notification below. If you choose “OK”, you will loose all your unsaved changes, and if you choose “Cancel”, you will remain the same form. With the default auto save behavior inhibited, users have to manually save the form, by clicking on the Save button or using the keyboard shortcut: “Control + S”.
Auto Save Inhibited: Do you want to loose your changes?

What if you want to inhibit the auto-save behavior throughout your Dynamics 365 organization (i.e. disable auto-save on all forms), instead of a specific form? It would be inefficient to implement the JavaScript code above on all forms across all your Dynamics 365 organization entities. An efficient way execute such a requirement (i.e. on all forms) is to:

  1. Go to Settings > Administration.
  2. Click on System Settings.
  3. Click on the General tab
  4. Set the Enable auto save on all forms option, to No.
  5. Click OK, in the footer of the window.

Disabling Auto Save in a Dynamics 365 Organization

Microsoft Dynamics 365: How to Remove Unwanted Special Characters from Text Fields

Microsoft Dynamics 365 has a flexible and extendable architecture, and can be integrated with other platforms. In integrations, it is common for data entered in Dynamics 365 to be shared with other platforms. Although Dynamics 365 text fields are flexible to save special characters, other platforms, that integrate with Dynamics 365, may not be as flexible. For example, when you integrate Dynamics 365 with SharePoint or One Drive, you may want to use the record’s name in Dynamics 365 to create the record’s document set or folder in SharePoint or One Drive . In such a case, Dynamics 365 fields being used by SharePoint or One Drive have to adhere to those platforms’ more restrictive special characters restrictions. The the invalid characters for OneDrive, OneDrive for Business on Office 365, and SharePoint Online are ” * : < > ? / \ | and for OneDrive for Business on SharePoint Server 2013 they are ~ . ” # % & * : < > ? / \ { | }.

The plugin in this post addresses this integration challenge by preventing users from saving restricted special characters in the text field(s) enabled text validation. When creating or updating a record in Dynamics 365, the plugin will check the text field(s), enabled for text validation, and strip out the characters that are not part of the permitted list characters.

To demonstrate this plugin’s functionality, we enabled text validation on the Account Name field for the Account entity. The validation rule applied by the plugin is: the Account Name field is only permitted to take in alphanumeric characters, space and 2 special characters i.e. underscore and hyphen. If a user enters any characters in the field, other than the permitted list of characters, they will be stripped out when they save the record. In the image below, a user entered both valid and invalid characters. Before Save:
Before Saving, user has entered invalid special characters

When saving the record, the plugin stripped out the invalid characters from the text field, leaving only the permitted characters (i.e. alphanumeric characters, space and 2 special characters i.e. underscore and hyphen). After Save:
After Saving, invalid special characters are removed

The Plugin

using System;
using System.Text.RegularExpressions;
using Microsoft.Xrm.Sdk;


namespace ItsFascinating.Plugins.D365
{
    /// 
    /// This plugin removes non-approved special characters from the Account Name field of the Account entity
    /// 
    public class AccountNameCharactersRemover : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            string pluginMessageCreate = "create";
            string pluginMessageUpdate = "update";
            Entity primaryEntity = new Entity();

            if (serviceProvider == null)
            {
                throw new ArgumentNullException("localContext");
            }

            #region Plugin Setup Variables
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            #endregion
            #region Validation Prior Execution
            //Exist if plugin if it's not called at stage 20 of the excution pipeline
            if (context.Stage != 20)
            {
                tracingService.Trace("Invalid Stage: Stage = {0}", context.Stage);
                return;
            }
           
            //Only execute plugin if plugin message is Create or Update 
            if (!(context.MessageName.ToLower().Equals(pluginMessageCreate) || context.MessageName.ToLower().Equals(pluginMessageUpdate)))
            {
                tracingService.Trace("Invalid Message: MessageName = {0}", context.MessageName);
                return;
            }
            #endregion

            #region Plugin Logic
            if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
            {
                try
                {
                    primaryEntity = context.InputParameters["Target"] as Entity;
                    
                    // Display Name: "Account Name" | Database Name: "name"
                    string accountNameFieldLogicalName = "name";
                    Object accountNameObj;
                    string accountName, updatedAccountName;
                    //Regex pattern for alphanumeric characters, space and 2 special characters i.e. underscore and hyphen                 
                    string permittedCharacters = "[^0-9a-zA-Z_ -]+";
                   
                    //Check if the specified attribute is contained in the internal dictionary before you you try to Get its value
                    if (primaryEntity.Attributes.TryGetValue(accountNameFieldLogicalName, out accountNameObj))
                    {
                        //Get value of that has been entered in the text field
                        accountName = Convert.ToString(accountNameObj);
                        //Remove remove unpermitted characters
                        updatedAccountName = Regex.Replace(accountName, permittedCharacters, "");
                        //Update the value of the text field, after removing the restricted characters
                        primaryEntity.Attributes[accountNameFieldLogicalName] = updatedAccountName;
                    }
                }
                catch (Exception ex)
                {
                    throw new InvalidPluginExecutionException(string.Format("Error occured in the PreUpdateAccount plugin: {0}", ex.Message));
                }
            }
            #endregion
        }
    }
}