Architecture & DesignCreating a Custom Repository in SharePoint Online and Its Integration with Dynamics...

Creating a Custom Repository in SharePoint Online and Its Integration with Dynamics CRM Online

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

By Yugesh Kumar

As a user in Microsoft Dynamics CRM, we usually perform actions to create, Upload, View, and/or delete documents related to records. The preferred repository is a SharePoint location without the need to log in to a different site.

Microsoft has provided us with document management in CRM by using Server-based SharePoint Integration. It allows Dynamics CRM online and SharePoint Online to perform server-to-server connection.

Cust1
Figure 1: Enabling server-based SharePoint Integration

Configuring CRM Online and SharePoint Online with Server-based SharePoint Integration can be followed from the preceding link.

The problem, though, with this out-of-box feature is, for every record that is created in Dynamics CRM and (assuming a document library is configured for the entity); while navigating to documents, an alert shows up indicating the folder name that will be created automatically.

Cust2
Figure 2: Alert for automatic creation of folder in SharePoint

The repository name does not look very friendly and there is no control over its name during creation. It is very difficult to search the repository due to its very random name.

Cust3
Figure 3: An awkward repository name

The next question arises: What should be the name. then?

It should be a property on the entity that is unique. An auto number is a good example to start with. It can also be a combination of two fields that makes the name unique.

In this post, I am going to name the repository created as the Unique Auto Number ID created for the opportunity entity. Opportunity Numbers will read as “Neu-001,” “Neu-002,” and so forth.

Let us first understand what happens when the I click the Confirm button when prompted with “Create Folder” alert.

Cust4
Figure 4: Dynamics CRM Document Location record

A SharePoint Document Location record in CRM is created in the background with following details:

  • Parent Site or Location: This is the location that is created automatically when doing CRM-SharePoint Integration.
  • Relative URL: This is the field that stores the repository name in CRM. A repository with this name is created automatically in integrated SharePoint.
    In my example, I need to populate this text with our Auto number, say, Neu-001.
  • Regarding: This is the reference to the CRM record.

Let me now write a plugin to create a SharePoint Document Location when the opportunity is created.

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Text;
using System.Threading.Tasks;

namespace CRMSharePointWithCustomFolderName
{
   public class CreateSharePointDocumentLocation : IPlugin
   {
      public void Execute(IServiceProvider serviceProvider)
      {
         IPluginExecutionContext context = (IPluginExecutionContext)
            serviceProvider.GetService(typeof(IPluginExecutionContext));

         IOrganizationServiceFactory serviceFactory =
            (IOrganizationServiceFactory)serviceProvider.GetService
            (typeof(IOrganizationServiceFactory));
         IOrganizationService service =
            serviceFactory.CreateOrganizationService(context.UserId);

         if (context.InputParameters.Contains("Target") &&
             context.InputParameters["Target"] is Entity)
         {
            // Obtain the target entity from the input parameters.
            Entity entity = (Entity)context.InputParameters["Target"];

            // Verify that the target entity represents an opportunity.
            // If not, this plug-in was not registered correctly.
            if (entity.LogicalName.ToLower() != "opportunity")
               return;

            if (context.MessageName.ToLower() != "create")
               return;

            try
            {
               // Get the Opportunity Auto Number generated, like "Neu-001"
               var opportunity = service.Retrieve("opportunity",
                  entity.Id, new ColumnSet("neu_opportunitynumber"));
               if (opportunity.Attributes.Contains("neu_opportunitynumber"))
               {
                  string oppNumber =
                     (string)opportunity["neu_opportunitynumber"];
                  string documentname = "Document for ";
                  CreateSharePointDocLocation(service, oppNumber,
                     "opportunity", opportunity.Id, documentname);
               }
            }
            catch (FaultException<OrganizationServiceFault> ex)
            {
               throw new InvalidPluginExecutionException("An error
                  occurred in the CreateSharePointDocumentLocation
                  plug-in.", ex);
            }

            catch (Exception ex)
            {
               throw new InvalidPluginExecutionException("An error
                  occurred in the CreateSharePointDocumentLocation
                  plug-in.", ex);
            }
         }
      }

      /// <summary>
      /// Creates the share point document location.
      /// </summary>
      /// <param name="serviceProxy">The service proxy.</param>
      /// <param name="folderName">Name of the folder.</param>
      /// <param name="entityLogicalName">Name of the entity logical.</param>
      /// <param name="recordId">The record identifier.</param>
      /// <param name="documentname">The documentname.</param>
      /// <returns></returns>
      /// <exception cref="System.ArgumentException">Sharepoint document
      /// location record doesnt exist with relative url set to
      /// 'opportunity'</exception>
      public static Guid? CreateSharePointDocLocation(IOrganizationService serviceProxy,
         string folderName, string entityLogicalName, Guid recordId, string documentname)
      {
         Guid topOppLocId = RetrieveTopOppSharePointLocationId(serviceProxy);
         if (topOppLocId == Guid.Empty)
         {
            throw new ArgumentException("Sharepoint document location record
               doesn't exist with relative url set to 'opportunity'");
         }

         var query = new QueryExpression("sharepointdocumentlocation");
         query.Criteria.AddCondition(new ConditionExpression("regardingobjectid",
            ConditionOperator.Equal, recordId));
         var result = serviceProxy.RetrieveMultiple(query);

         if (result.Entities.Count > 0)
         {
            return null;
         }

         var sharePointDocumentLocation = new
            Entity("sharepointdocumentlocation");
         sharePointDocumentLocation["name"] = documentname +
            folderName; ;
         sharePointDocumentLocation["description"] = null;
         sharePointDocumentLocation["regardingobjectid"] =
            new EntityReference(entityLogicalName, recordId);

         sharePointDocumentLocation["parentsiteorlocation"] = new
            EntityReference("sharepointdocumentlocation", topOppLocId);
         sharePointDocumentLocation["relativeurl"] = folderName;

         // Create a SharePoint document location record named Documents on
         // Default Site 1.
         Guid dococLocId = serviceProxy.Create(sharePointDocumentLocation);

         return dococLocId;
      }

      /// <summary>
      /// Retrieves the top opp share point location identifier.
      /// </summary>
      /// <param name="orgService">The org service.</param>
      /// <returns></returns>
      public static Guid RetrieveTopOppSharePointLocationId
         (IOrganizationService orgService)
      {
         Guid oppDocLocId = Guid.Empty;

         string fetchXml = @"<fetch version='1.0'
               output-format='xml-platform' mapping='logical'
               distinct='false'>
            <entity name='sharepointdocumentlocation'>
               <attribute name='sharepointdocumentlocationid' />
                  <filter type='and'>
                     <condition attribute='relativeurl'
                        operator='eq' value='opportunity' />
                     <condition attribute='regardingobjectid'
                        operator='null' />
                  </filter>
            </entity>
         </fetch>";

         EntityCollection result = orgService.RetrieveMultiple
            (new FetchExpression(fetchXml));
         foreach (var docLoc in result.Entities)
         {
            oppDocLocId = (Guid)docLoc.Attributes
               ["sharepointdocumentlocationid"];
         }

         if (oppDocLocId != Guid.Empty)
         {
            return oppDocLocId;
         }

         return Guid.Empty;
      }
   }
}

Listing 1: Plugin on Create of Opportunity to Create SharePoint Document Location

The plugin is registered when the opportunity is created. Once an opportunity is created, the plugin fires and creates a SharePoint Document Location. Navigate to the document from the opportunity and here is what I see now.

Cust5
Figure 5: SharePoint Document Associated View

It looks like the CRM is trying to locate a folder in SharePoint. Because it does not find a respective SharePoint Location record and a matching relative URL, it is showing us a very friendly message to create/edit the folder name in SharePoint.

Additionally, if you notice, we do not get an alert to create a SharePoint folder at all.

So, it looks like, if we create a folder in SharePoint with matching “Relative URL” text, our problem is solved.

Now, I am going to create programmatically a folder with same Opportunity Number Text (the relative URL). This can be done by following two steps:

  1. Registering a Microsoft Azure Service Bus Solution Endpoint and a step on creation of an opportunity.
  2. Write a Windows Service to process the message posted in Service Bus and create Folder in SharePoint with desired Name.
Note: Having Windows Service does not seem to be a good idea because we are talking about CRM and SharePoint Online. Hence, creating an Azure role for this should be a fairly good idea. But then, the following code can always be used.

Cust6
Figure 6: Associating a SharePoint document

The Azure Message Processing and Folder Creation:

/// <summary>
   /// Processes the messages.
   /// </summary>
   public void ProcessMessages()
   {
      Guid recordId = Guid.Empty;
      ReceiveMode mode = this.queueClient.Mode;
      string[] result = new string[2];
      while (true)
      {
         Library.WriteErrorLog("Waiting for a message
            from the queue... ");
         BrokeredMessage message;
         try
         {
            message = this.queueClient.Receive();

            if (message != null)
            {
               try
               {
                  string keyRoot = "http://schemas.microsoft.com/
                     xrm/2011/Claims/";
                  string entityLogicalNameKey =
                     "EntityLogicalName";
                  string requestNameKey = "RequestName";
                  object entityLogicalNameValue;
                  object requestNameValue;
                  message.Properties.TryGetValue(keyRoot +
                     entityLogicalNameKey, out entityLogicalNameValue);
                  message.Properties.TryGetValue(keyRoot +
                     requestNameKey, out requestNameValue);

                  if (entityLogicalNameValue != null &&
                     requestNameValue != null)
                  {
                     if (entityLogicalNameValue.ToString() ==
                        "opportunity" && requestNameValue.ToString().
                        ToLower() == "create")
                     {
                        Library.WriteErrorLog("Got opportunity message with Create");
                        Utility.GetOpportunityId(message.
                           GetBody<RemoteExecutionContext>(), out recordId);
                        var oppNumber = Utility.GetOpportunityAutoNumber(recordId);
                        if (!string.IsNullOrEmpty(oppNumber))
                        {
                           SharePointUtility sputility = new SharePointUtility();
                           sputility.CreateSharePointFolder(oppNumber);
                        }
                     }
                     else
                     {
                        continue;
                     }
                  }
                  else
                  {
                     continue;
                  }

                  if (mode == ReceiveMode.PeekLock)
                  {
                     message.Complete();
                     Library.WriteErrorLog("Message processed and removed");
                  }
               }
               catch (System.Exception ex)
               {
                  if (mode == ReceiveMode.PeekLock)
                  {
                     message.Abandon();
                     Library.WriteErrorLog("Message was not cleared ");
                  }

                  Library.WriteErrorLog(ex.Message);
                  continue;
               }
            }
            else
            {
               break;
            }
         }
         catch (System.TimeoutException e)
         {
            Library.WriteErrorLog(e.Message);
            continue;
         }
         catch (System.ServiceModel.FaultException e)
         {
            Library.WriteErrorLog(e.Message);
            continue;
      }
   }
   recordId = Guid.Empty;
}

SharePoint Utility Class:

internal class SharePointUtility
   {
      /// <summary>
      /// Creates the share point folder.
      /// </summary>
      /// <param name="oppNumber">The opp number.</param>
      public void CreateSharePointFolder(string oppNumber)
      {
         string targetUrl = ConfigurationManager.
            AppSettings["TargetUrl"].ToString();
         using (ClientContext clientContext =
            NewClientContext(targetUrl, true))
         {
            Library.WriteErrorLog("Got the Context");
            List docList = clientContext.Web.Lists.
               GetByTitle("opportunity");
            clientContext.Load(docList);
            var folder = docList.RootFolder;
            folder = folder.Folders.Add(oppNumber);
            clientContext.Load(folder);
            clientContext.ExecuteQuery();
            string url = targetUrl + "/opportunity/" + oppNumber;
            Library.WriteErrorLog("Sharepoint Folder was created...");
         }
      }

      /// <summary>
      /// News the client context.
      /// </summary>
      /// <param name="siteUrl">The site
      /// URL.</param>
      /// <param name="isOnline">if set to <c>true</c>
      /// [is online].</param>
      /// <returns></returns>
      public ClientContext NewClientContext(string siteUrl,
         bool isOnline = false)
      {
         ClientContext context = new ClientContext(siteUrl);
         string userName = ConfigurationManager.AppSettings
            ["UserName"].ToString();
         string password = ConfigurationManager.AppSettings
            ["Password"].ToString();

         SecureString secPassword = new SecureString();
         foreach (char c in password.ToCharArray())
            secPassword.AppendChar(c);
         context.Credentials = new
            SharePointOnlineCredentials(userName, secPassword);

         return context;
      }
   }

Listing 2: Windows Service that Creates custom SharePoint repository

The following friendly repository name is created in SharePoint. This can be easily searched and is more readable.

Cust7
Figure 7: SharePoint has created a friendly repository name

Conclusion

Although the CRM out-of-box feature for SharePoint integration does not provide flexibility and control to name each record folder created in SharePoint, the same can be achieved by creating a SharePoint Location record in CRM and by creating a folder in SharePoint separately and then associating the two. Seamless integration of Microsoft Azure and Dynamics helps post the created record message on service bus and a scheduler to read the message from Azure Service Bus and create SharePoint folder based on entity record attribute(s).

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories