Friday, December 28, 2018

Custom SharePoint Integration for Dynamics 365 Online



Most of the customers prefer to use SharePoint as there document storage. CMost of the cases you may able to use the OOB SharePoint - Dynamics CRM integration.

https://community.dynamics.com/crm/b/magnetismsolutionscrmblog/archive/2017/12/19/setting-up-server-based-sharepoint-integration-for-dynamics-365-online

But sometimes you may need to extend the OOB SharePoint - Dynamics CRM integration to meet client's expectations.

To develop this kind of third party solutions, SharePoint client object model (CSOM) can be used.

So there are two methods to develop this kind of solution for Dynamics 365 Online.
  1. Use the share point Microsoft.SharePoint.Client.dll and Microsoft.SharePoint.Client.Runtime.dll and use the standerd methods.

    https://docs.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537013(v%3doffice.14)

    But in Dynamics 365 Online, there is a limitation that only one dll can be resisted and it won't be allowed to register other dlls for referencing.
    There you need to merge your custom solution dll with Microsoft.SharePoint.Client.dll and Microsoft.SharePoint.Client.Runtime.dll using a dll merging technique.

  2. Create a custom solution by calling the SharePoint REST Api with web requests.
    Following is a example of crating a folder in SharePont document library using REST Api. Same way you can create, delete, update records in SharePoint.
    There are some helper classes which are needed to execute following sample methods. You can find the helper classes here.
public class SharePointService
{
 private string _username;
 private string _password;
 private string _siteUrl;

 private SpoAuthUtility _spo;
 public SharePointService(string username, string password)
 {
  _username = username;
  _password = password;
 }

 /// <summary>
 /// For creating folder in sharepoint
 /// </summary>
 /// <param name="siteUrl">The site URL.</param>
 /// <param name="relativePath">The relative path.</param>
 public string CreateFolder(string siteUrl, string relativePath)
 {
  if (siteUrl != _siteUrl)
  {
   _siteUrl = siteUrl;
   Uri spSite = new Uri(siteUrl);

   _spo = SpoAuthUtility.Create(spSite, _username, WebUtility.HtmlEncode(_password), false);
  }

  string odataQuery = "_api/web/folders";

  byte[] content = ASCIIEncoding.ASCII.GetBytes(@"{ '__metadata': { 'type': 'SP.Folder' }, 'ServerRelativeUrl': '" + relativePath + "'}");

  string digest = _spo.GetRequestDigest();

  Uri url = new Uri(String.Format("{0}/{1}", _spo.SiteUrl.ToString().TrimEnd('/'), odataQuery));
  // Set X-RequestDigest
  var webRequest = (HttpWebRequest)HttpWebRequest.Create(url);
  webRequest.Headers.Add("X-RequestDigest", digest);

  // Send a json odata request to SPO rest services to fetch all list items for the list.
  byte[] result = HttpHelper.SendODataJsonRequest(
    url,
    "POST", // reading data from SP through the rest api usually uses the GET verb 
    content,
    webRequest,
    _spo // pass in the helper object that allows us to make authenticated calls to SPO rest services
    );

  string response = Encoding.UTF8.GetString(result, 0, result.Length);
  return response;
 }
}

If you need code samples to create/update/delete list items and documents, please let me know in comment section.

Saturday, November 10, 2018

"OwningBusinessUnit" field has been changed in CRM v9 for some entities.

When upgrading CRM v8.2 to CRM v9, most of the developers pay attention to the deprecations coming with CRM v9 and hope the plugin/workflow code will work fine with CRM v9. 

But there is a drastic change for "OwningBusinessUnit" field in following entities.
  • InvoiceDetail
  • OpportunityProduct
  • QuoteDetail
  • SalesOrderDetail

The data type of the "OwningBusinessUnit" field has been changed to Lookup type.(In v8, this was an Unique Identifier).

V9

V8

If you have used the field "OwningBusinessUnit" in above mentioned entities in your code, it is the time to update your code for CRM v9.




Monday, October 29, 2018

Dynamics CRM - Auto generate plugin steps.

I believe that you are experienced with the plugin registration and adding steps using plugin registration tool. But sometimes you may need to add/remove plugin steps programmatically.

Adding a plugin step means, creating a SdkMessageProcessingStep record in CRM.

Before creating a SdkMessageProcessingStep record you need to retrieve following.
  1. The message id.
    /// <summary>
    /// Gets the message identifier.
    /// </summary>
    /// <param name="context">The context.</param>
    /// <param name="sdkMessageName">Name of the SDK message.</param>
    /// <returns>
    /// The message id.
    /// </returns>
    /// <exception cref="InvalidPluginExecutionException">No sdk messages found with the sdk message name.</exception>
    private static Guid GetMessageId(ServiceContext context, string sdkMessageName)
    {
        using (context)
        {
            // Gets the sdk message
            var sdkMessage = context.SdkMessageSet.Where(s => s.Name == sdkMessageName).FirstOrDefault();
                    
            // Throws an error if no sdk messages found.
            if (sdkMessage == null)
            {
                throw new InvalidPluginExecutionException(string.Format("No sdk messages found with the sdk message name '{0}'.", sdkMessageName));
            }
    
            return sdkMessage.Id;
        }
    }
  2.  The plugin type id.
    /// <summary>
    /// Gets the plugin type identifier.
    /// </summary>
    /// <param name="context">The context.</param>
    /// <param name="assemblyName">Name of the assembly.</param>
    /// <param name="pluginTypeName">Name of the plugin type.</param>
    /// <returns>
    /// The plugin type id.
    /// </returns>
    /// <exception cref="InvalidPluginExecutionException">No plugin assemblies found with the assmbly name
    /// or
    /// No plugin types found with the plugin type and the assmbly name</exception>
    private static Guid GetPluginTypeId(ServiceContext context, string assemblyName, string pluginTypeName)
    {
        using (context)
        {
            // Gets the plugin assembly.
            var pluginAssembly = context.PluginAssemblySet.Where(p => p.Name == assemblyName).FirstOrDefault();
                    
            // Throws an error if no plugin assembly found with the name.
            if (pluginAssembly == null)
            {
                throw new InvalidPluginExecutionException(string.Format("No plugin assemblies found with the assmbly name '{0}'", assemblyName));
            }
    
            var assemblyId = pluginAssembly.Id;
                    
            // Gets the plugin type
            var pluginType = context.PluginTypeSet.Where(p => p.PluginAssemblyId.Id == assemblyId && p.TypeName == pluginTypeName).FirstOrDefault();
                    
            // Throws an error if no plugin types found.
            if (pluginType == null)
            {
                throw new InvalidPluginExecutionException(string.Format("No plugin types found with the plugin type '{0}' and the assmbly name '{1}'", pluginType.ToString(), assemblyName));
            }
    
            return pluginType.Id;
        }
    }
  3. The sdk message filter id.
    /// <summary>
    /// Gets the SDK message filter identifier.
    /// </summary>
    /// <param name="context">The context.</param>
    /// <param name="entityName">Name of the entity.</param>
    /// <returns>
    /// The message filter id.
    /// </returns>
    /// <exception cref="InvalidPluginExecutionException">No sdk message filters found for the entity.</exception>
    private static Guid GetSdkMessageFilterId(ServiceContext context, string entityName)
    {
        using (context)
        {
            // Gets the message id.
            Guid messageId = GetMessageId(context, ContextMessageName.Create);
                    
            // Get the message filter.
            var sdkMessageFilter = context.SdkMessageFilterSet.Where(s => s.PrimaryObjectTypeCode == entityName && s.SdkMessageId.Id == messageId).FirstOrDefault();
                    
            // Throws an error if no message filters found.
            if (sdkMessageFilter == null)
            {
                throw new InvalidPluginExecutionException( string.Format("No sdk message filters found for the entity '{0}'", entityName));
            }
    
            return sdkMessageFilter.Id;
        }
    }

Now we have the message id,  plugin type id and the sdk message filter id.

We can define the assembly name and the plugin type name as properties.
/// <summary>
/// The assembly name
/// </summary>
private static string AssemblyName = "PluginSample.Plugins";

/// <summary>
/// The plugin type name
/// </summary>
private static string PluginTypeName = "PluginSample.Plugins.TestHandler";


With all the above things in hand, now following method can be used to add plugin steps programatically.
/// <summary>
/// Adds the plugin step.
/// </summary>
/// <param name="service">The service.</param>
/// <param name="entityLogicalName">The entity logical name.</param>
/// <param name="contextMessageName">Name of the context message.("Create", "Update", "Delete", "Associate", "Disassociate")</param>
public static void AddPluginStep(IOrganizationService service, string entityLogicalName, string contextMessageName)
{ 
    using (var context = new ServiceContext(service))
    {
        var stepName = string.Format("PluginSample.Plugins.TestHandler: {0} of {1}", contextMessageName, entityLogicalName);

        // Gets the message name.
        var messageId = GetMessageId(context, contextMessageName);

        // Gets the plugin type id.
        var pluginTypeId = GetPluginTypeId(context, AssemblyName, PluginTypeName);

        // Gets the sdk message filter id.
        var sdkMessageFilterId = GetSdkMessageFilterId(context, entityLogicalName);

        // Check the plugin step is already regiseterd and if not register the new step.
        var sdkMessageProcessingStep = context.SdkMessageProcessingStepSet.Where(s => s.SdkMessageFilterId.Id == sdkMessageFilterId && s.EventHandler.Id == pluginTypeId).FirstOrDefault();
        if (sdkMessageProcessingStep == null || sdkMessageProcessingStep.Id == Guid.Empty)
        {
            // Creates the sdk message processing step.
            SdkMessageProcessingStep step = new SdkMessageProcessingStep
            {
                AsyncAutoDelete = false,
                Mode = new OptionSetValue((int)SdkMessageProcessingStepMode.Synchronous),
                Name = stepName,
                EventHandler = new EntityReference(PluginType.EntityLogicalName, pluginTypeId),
                Rank = 1,
                SdkMessageId = new EntityReference(SdkMessage.EntityLogicalName, messageId),
                Stage = new OptionSetValue((int)PipelineStage.PreOperation),
                SupportedDeployment = new OptionSetValue((int)SdkMessageProcessingStepSupportedDeployment.ServerOnly),
                SdkMessageFilterId = new EntityReference(SdkMessageFilter.EntityLogicalName, sdkMessageFilterId)
            };

            service.Create(step);
        }
    }
}

In the same manner we can remove the plugin step by deleting the SdkMessageProcessingStep record.
/// <summary>
/// Removes the plugin step.
/// </summary>
/// <param name="service">The service.</param>
/// <param name="entityLogicalName">The entity logical name.</param>
public static void RemovePluginStep(IOrganizationService service, string entityLogicalName)
{
    using (var context = new ServiceContext(service))
    {
        // Gets the plugin type id.
        var pluginTypeId = GetPluginTypeId(context, AssemblyName, PluginTypeName);

        // Gets the message filter id.
        var sdkMessageFilterId = GetSdkMessageFilterId(context, entityLogicalName);

        // Filter the message processing step.
        var sdkMessageProcessingStep = context.SdkMessageProcessingStepSet.Where(s => s.SdkMessageFilterId.Id == sdkMessageFilterId && s.EventHandler.Id == pluginTypeId).FirstOrDefault();

        if (sdkMessageProcessingStep != null)
        {
            // Delete sdk message processing step.
            service.Delete(SdkMessageProcessingStep.EntityLogicalName, sdkMessageProcessingStep.Id);
        }
    }
}

In above example I'm using the early bound classes. If you need late bound code examples, please let me know in comment section.


Friday, October 19, 2018

Deeper look in to IExecutionContext.Depth Property

In CRM customizations Depth is a very important property provided for context management, which most of the time CRM developers pay less attention to.

“Depth” property(Int32 ) is hold by the IPluginExecutionContext which holds contextual information passed into a plugin. It gets the current depth of execution in the call stack and used by the platform for infinite loop prevention.

How Microsoft explains the behavior of the Depth:
“Every time a running plug-in or Workflow issues a message request to the Web services that triggers another plug-in or Workflow to execute, the Depth property of the execution context is increased. If the depth property increments to its maximum value within the configured time limit, the platform considers this behavior an infinite loop and further plug-in or Workflow execution is aborted.”

In simple words: 
Depth is the number of times a plugin/custom workflow has been called in one transaction



See following 3 examples.
  1. If Contact updates the account it self in post update, it makes an infinite loop and the first time the depth is 1, then in the second time the depth is 2 and so on…
  2. If Contact is updated form an account update plugin, and it triggers a plugin in contact entity, then the first plugin(in account) depth is 1 and the second plugin(in contact) depth is 2
  3. If a workflow(WF1) triggers in contact update, and the workflow triggers another child workflow(WF2), first workflow(WF1) depth is one and the second workflow(WF2) depth is 2


The default value in depth property is 1 and the maximum depth is 8.  And, the time limit is one hour. This means that after one hour of the first plugin/workflow execution, it reset the depth property to 1 again.

Knowingly or unknowingly you may have experienced this when you schedule a waiting workflow recursively. If the waiting time is less than 8min your workflow files in 8th execution. Mistry behind this is this is the above reason.

But maximum depth is configurable by the Microsoft Dynamics 365 administrator using the PowerShell command Set-CrmSetting. (The setting is WorkflowSettings.MaxDepth.)

In your plugins you can check the depth or parent context to prevent loops.

public void Execute(IServiceProvider serviceProvider)
{
 var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
 
    // Checking either depth or parent context is enough, but I have used both in this example.
 if (context.Depth > 1 || context.ParentContext != null)
 {
  return;
 }

 // Logic goes here.
}

But if your plugin should execute in second level (Previous Eg2 contact update and Eg3 WF3 update), no point of checking the parent context and need to check only the depth. The reason is the ParentContext is not null form the second level(2,3,4..).

public void Execute(IServiceProvider serviceProvider)
{
 var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
 if (context.Depth > 2)
 {
  return;
 }

 // Logic goes here.
}

When you use the depth property to prevent loops, use it wisely. Otherwise you might face in to trouble by missing the necessary plugin/workflow executions.




Friday, October 12, 2018

Dynamics CRM 365 App for Outlook permission error

Your client may complain that he is getting the following error when he opens the Dynamics 365 App for Outlook.

"We are unable to show Dynamics 365 App for Outlook because current user role does not have required permissions. Please contact your administrator to have required security role assigned to the Dynamics 365 App for Outlook solution."

The reason behind this is the client's Dynamics 365 security role does not have access to the app module used by the Dynamics 365 App for Outlook.

The fix is really simple.
  1. Go to Settings > Security > Security Roles and select the relevant security role for the user.
  2. Grant Create, Write permission for "App" entity.

Time to pay more attention to changes in the security roles with new app features...

Friday, October 5, 2018

Create auto-number attributes - Dynamics 365 (online), version 9.x

With the Dynamics 365 (online), version 9.0 release, you can add an auto-number attribute for any entity. Still a UI is not provided by Microsoft and you can do this programmatically. 
This is not the same Autonumber that is available for some of the out of the box entities like case, invoice, order, etc in Settings > Administration section. Here you can add any string field as the auto number field.


Ref : https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/create-auto-number-attributes

Auto Number Manager for XrmToolBox, provides you this facility with a nice and user firendly User Interface.

Ref : https://www.xrmtoolbox.com/plugins/Rappen.XrmToolBox.AutoNumManager/

But I found following two drawbacks in this new feature.

  1. After enabling the auto number for the field, there is no way to disable this at the moment. (Eventhough there is a checkbox in "Auto Number Manager" to disable this, it is not working at the moment. Delete attribute is not for deleting number format, it is for deleting the field.)
  2. The auto number will be increased even in failed attempt to create a record.Eg : Consider last sequence no is 100000. If next attempt of creating record fails and the successful record creation after that will not have the sequence number as 100001 and it will have the value 100002.





Friday, September 28, 2018

Adxstudio portal for dynamics 365-v9


    If you need to upgrade your Dynamics CRM on-premise or Dynamics 365-v8 to Dynamics 365-v9 and still want to Adxstudio portal, you need to follow following steps,

  1. Download and install Adxstudio Portals 7.0.0026 (https://community.adxstudio.com/products/adxstudio-portals/releases/adxstudio-portals-7/download/)
    If you get an error while installing, please go to my previous article and follow the steps. (http://thusithasblog.blogspot.com/2018/04/issue-while-installing-adxstudio-portal.html)
  2. Open the Adxstudio installed folder location(C:\Program Files (x86)\Adxstudio\XrmPortals\7.0.0026) and copy MasterPortal  folder & AdxstudioPortals.sln file from Sample folder and paste in to your's.Copy Framework folder and replace yours.
    (
    If you are upgrading form Dynamics 365-V8, you can skip this step.)
  3. Now open the solution AdxstudioPortals.sln.
  4. Compile it and make sure you don't get any errors, you should not as long as you have kept the directory as above.
  5. Change target framework to 4.5.2
  6. Microsoft.Xrm.Sdk.Proxy and Microsoft.Crm.Sdk dlls from v9 sdk and paste them into the Framework folder.
  7. Open your web.config and change URL, username and password to point it to your new instance. And also websiteName to your website that you have created in Dynamics 365 - v9.
  8. Change targetFramework to 4.5.2 in web config.
  9. Add following dependentAssemblies to web config.
  10. <dependentAssembly>
     <assemblyIdentity name="Microsoft.Xrm.Sdk" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
     <bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0"/>
    </dependentAssembly>
    <dependentAssembly>
     <assemblyIdentity name="Microsoft.Crm.Sdk.Proxy" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
     <bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0"/>
    </dependentAssembly>

  11. Build the solution.
Cheers!!! Now you are good to go!

Monday, May 7, 2018

Bing Maps integration is disabled means the sections contain 'Map View' are hidden.

If you have disabled Bing Maps integration, then any section containing Bing Maps control will be hidden, regardless of any controls it may contain.

Eg:

Bing Maps integration is disabled in my environment. (Settings > Administration > System Settings > General

I have added Account number field to the section which contains Map View.

Since Bing Maps is disabled, it swallows the whole section.


Saturday, April 14, 2018

Issue while Installing Adxstudio Portal 7.0.0026 in Dynamics 365


When you are going to install  Adxstudio Portal 7.0.0026 in Dynamics 365, you may get the following error. It means that you are not allowed to update the Marketing List canmodifymobileclientreadonly attribute due to it is being restricted in AdxstudioPortalDependencies Solution.


Error :
[entity] List - The evaluation of the current component(name=Entity, id=efd3a52d-04ca-4d36-a54c-2a26a64f5571) in the current operation (Update) failed during managed property evaluation of condition: The evaluation of the current component(name=Entity, id=efd3a52d-04ca-4d36-a54c-2a26a64f5571) in the current operation (Update) failed during managed property evaluation of condition: Managed Property Name: canmodifymobileclientreadonly; Component Name: Entity; Attribute Name: canmodifymobileclientreadonly;

Solution :
  1. Go to Adxstudio Portal Installed Location. Let say: I have installed it in C:\Program Files (x86).
  2. Navigate to C:\Program Files (x86)\Adxstudio\XrmPortals\7.0.0026\Customizations\Components.
  3. Extract the AdxstudioPortalsDependencies.zip solution to some other location.
  4. Now open the Customization.xml file.
  5. Modified following values from 0 to 1 under Marketing List Entity.
    <IsVisibleInMobile>1</IsVisibleInMobile>
    <IsVisibleInMobileClient>1</IsVisibleInMobileClient>
    <IsReadOnlyInMobileClient>1</IsReadOnlyInMobileClient>

  6. Select all the folder and Zip it again to create the updated AdxstudioPortalsDependencies solution.
  7. Import the solution in Dynamics 365.
Now you can install the Portal (Basic, Community, Customer etc).



Monday, April 9, 2018

Level up for Dynamics CRM/365

I found a very useful Chrome extension :  Level up for Dynamics CRM

There are so many useful features which saves developers time.
You can visit following URL and see all the available featurs.

https://github.com/rajyraman/Levelup-for-Dynamics-CRM/blob/master/README.md

When you open a CRM, the extension enables for you and when you click it will show following options. Those options are described in the given link before.


If you have open a record form, it'll show additional options as below.


Thursday, April 5, 2018

Dynamics 365 Share and Un-Share Records Programmatically C#

Please refer to these codes, to Share Records, and Unshare Records.
/// <summary>
/// Shares the record.
/// </summary>
/// <param name="service">The service.</param>
/// <param name="tracingService">The tracing service.</param>
/// <param name="targetEntity">The target entity.</param>
/// <param name="targetUser">The target user.</param>
private void ShareRecord(IOrganizationService service, ITracingService tracingService, EntityReference targetEntity, EntityReference targetUser)
{
    //no delete access
    GrantAccessRequest grant = new GrantAccessRequest();
    grant.Target = targetEntity;

    PrincipalAccess principal = new PrincipalAccess();
    principal.Principal = targetUser;
    principal.AccessMask = AccessRights.ReadAccess | AccessRights.AppendAccess | AccessRights.WriteAccess | AccessRights.AppendToAccess | AccessRights.ShareAccess | AccessRights.AssignAccess;
    grant.PrincipalAccess = principal;

    try
    {
        service.Execute(grant);
    }
    catch (Exception ex)
    {
        tracingService.Trace("Exception: {0}", ex.ToString());
        throw ex;
    }
}

/// <summary>
/// Un shares the record.
/// </summary>
/// <param name="service">The service.</param>
/// <param name="tracingService">The tracing service.</param>
/// <param name="targetEntity">The target entity.</param>
/// <param name="targetUser">The target user.</param>
private void UnShareRecord(IOrganizationService service, ITracingService tracingService, EntityReference targetEntity, EntityReference targetUser)
{
    //no delete access
    ModifyAccessRequest modif = new ModifyAccessRequest(); ;
    modif.Target = targetEntity;

    PrincipalAccess principal = new PrincipalAccess();
    principal.Principal = targetUser;
    principal.AccessMask = AccessRights.None;
    modif.PrincipalAccess = principal;

    try
    {
        service.Execute(modif); ;
    }
    catch (Exception ex)
    {
        tracingService.Trace("Exception: {0}", ex.ToString());
        throw ex;
    }
}

Wednesday, April 4, 2018

Errors importing marketing list entity in Dynamics 365 v9

When you try to move your customization in  marketing list entity in Dynamics 365 v9, you may receive an error importing the customization.

By default "Read-only in mobile" is selected. Since "Enable for mobile" is not selected, "Read-only in mobileis not included in the exported solutions customization XML.

You can fix this issue by following  below steps. 
  1. Extract the solution and open the customization XML file
  2. Locate the List entity in the XML file
  3. Add the following tag in the XML file:<IsReadOnlyInMobileClient>1</IsReadOnlyInMobileClient>
  4. Zip the solution and import it again