Explanation
In this article we will explain our way to integrate Dynamics Portal with SharePoint.
As you all definitely know, SharePoint is widely used for documents and files management, and there are plenty of ready solutions for SharePoint and Dynamics CRM integration (like PowerAttachment). But unfortunately, OOB Dynamics Portal has no options to integrate with SharePoint, and that’s what we offer in case you plan to do it.
This task was divided into 2 subtasks:
- How to transfer files from Dynamics Portal to SharePoint.To be able to do that, you need to allow to attach files in Portal parameters (Portals -> Entity Form) and configure PowerAttachment (or you can write your own plugin to transfer files from CRM to SharePoint)
- How to transfer files from SharePoint to Dynamics Portal.
To do that, you have to create a new custom pagesharepointfoldercontent with custom page template. This new page will process file retrieval requests using CRM fetch queries to sharepointfile entity. The data is returned in JSON format.{% fetchxml sharepointfetch %}
On custom page template, the query was implemented using LIQUID fetchxml query.
{% fetchxml sharepointfetch%} <fetch> <entity name="uds_sharepointfile"> <attribute name="uds_sharepointresponse" /> <filter type="and"> <condition attribute="uds_targetid" operator="eq" value="{{request.params['id']}}" /> <condition attribute="uds_userid" operator="eq" value="{{user.id}}" /> <condition attribute="uds_name" operator="eq" value="{{now}}" /> </filter> </entity> </fetch> {% endfetchxml %} {% assign entities = sharepointfetch.results.entities %} {% assign object = entities.first %} {{ object.uds_sharepointfilesjson }}
We added a new entity to Dynamics CRM – uds_sharepointfile, and developed plugin on RetrieveMultiple event.
Do not forget to add Entity Permissions for this new entity.
Parameter uds_targetid contains Guid entries.
Parameter uds_userid can be used for checking the user access rights for the files he requests.
Parameter condition attribute="uds_name" operator="eq" value="{{now }}" allows to avoid query cashing.
In the plugin the FetchXML is parsed, and its parameters are extracted.
If the query contains the conditions of this type condition attribute="uds_targetid" operator="eq" value="{Guid}",after that, the received guid is used, the destination folder in SharePoint is defined for this entry by sharepointdocumentlocation entity.
public void Execute(IServiceProvider serviceProvider) { var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); var service = serviceFactory.CreateOrganizationService(context.UserId); //string query = ""; Guid targetId = Guid.Empty; Guid userId = Guid.Empty; string fileRelativeUrlBase64 = String.Empty; string result = String.Empty; switch (context.MessageName) { case "RetrieveMultiple": if (context.InputParameters["Query"] is FetchExpression currentQuery) { try { XDocument parsedQuery = XDocument.Parse(currentQuery.Query); var attribute = parsedQuery .Descendants("attribute") .Where(e => e.Attribute("name").Value == "uds_jsoncontent") .Select(e => e) .FirstOrDefault(); if (attribute != null) { string targetIdStringValue = parsedQuery .Descendants("condition") .Where(e => e.Attribute("attribute") != null && e.Attribute("operator") != null && e.Attribute("value") != null && String.Equals(e.Attribute("attribute").Value, "uds_targetid_string", StringComparison.InvariantCultureIgnoreCase) && String.Equals(e.Attribute("operator").Value, "eq", StringComparison.InvariantCultureIgnoreCase)) .Select(e => e.Attribute("value").Value) .FirstOrDefault(); if (!String.IsNullOrWhiteSpace(targetIdStringValue)) { try { targetId = new Guid(targetIdStringValue); } catch (Exception ex) { result = $"Error: {ex.Message}"; } } string userIdStringValue = parsedQuery .Descendants("condition") .Where(e => e.Attribute("attribute") != null && e.Attribute("operator") != null && e.Attribute("value") != null && String.Equals(e.Attribute("attribute").Value, "uds_userid", StringComparison.InvariantCultureIgnoreCase) && String.Equals(e.Attribute("operator").Value, "eq", StringComparison.InvariantCultureIgnoreCase)) .Select(e => e.Attribute("value").Value) .FirstOrDefault(); if (!String.IsNullOrWhiteSpace(userIdStringValue)) { try { userId = new Guid(userIdStringValue); } catch (Exception ex) { result = $"Error: {ex.Message}"; } } fileRelativeUrlBase64 = parsedQuery .Descendants("condition") .Where(e => e.Attribute("attribute") != null && e.Attribute("operator") != null && e.Attribute("value") != null && String.Equals(e.Attribute("attribute").Value, "uds_filepath", StringComparison.InvariantCultureIgnoreCase) && String.Equals(e.Attribute("operator").Value, "eq", StringComparison.InvariantCultureIgnoreCase)) .Select(e => e.Attribute("value").Value) .FirstOrDefault(); } } catch (Exception ex) { result = $"Error: {ex.Message}"; } } break; } SharePointService sharepointService = new SharePointService(); if (targetId != Guid.Empty) { DocumentLocationService locService = new DocumentLocationService(service); try { var folders = locService.GetSharePointLocation(targetId); result = sharepointService.GetAllSharePointFilesJSON(folders); } catch (Exception ex) { result = $"Error: {ex.Message}"; } } else if (!String.IsNullOrEmpty(fileRelativeUrlBase64)) { result = sharepointService.GetSharePointFileAsBase64(fileRelativeUrlBase64); } if (!String.IsNullOrWhiteSpace(result)) { Entity sharepointResponse = new Entity("uds_sharepointfile") { Id = targetId, ["uds_jsoncontent"] = result }; if (context.OutputParameters["BusinessEntityCollection"] is EntityCollection outputEntities) { outputEntities.Entities.Clear(); outputEntities.Entities.Add(sharepointResponse); } } } public Dictionary<string, string> GetSharePointLocation(Guid targetId) { Dictionary<string, string> pathDictionary = new Dictionary<string, string>(); var query = new QueryExpression("sharepointdocumentlocation") { ColumnSet = new ColumnSet("relativeurl", "parentsiteorlocation"), Criteria = new FilterExpression(LogicalOperator.Or) { Conditions = { new ConditionExpression("regardingobjectid", ConditionOperator.Equal, targetId) } } }; var results = _service.RetrieveMultiple(query).Entities.Select(x => x.ToEntity()); foreach (var location in results) { if (!String.IsNullOrWhiteSpace(location.RelativeUrl) && !pathDictionary.Keys.Contains(location.RelativeUrl)) { pathDictionary.Add(location.RelativeUrl, location.RelativeUrl); } } return pathDictionary; }
Than the service, receive data about every catalogue content. At this time, only file preview is returned (not the whole file), which is received with SharePoint API.
public string GetAllSharePointFilesJson(Dictionary<string, string> folders) { var sharepointFolders = GetAllSharePointFiles(folders); DataContractJsonSerializer serializer = null; using (MemoryStream memoryStream = new MemoryStream()) { serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(List<SharePointFolderModel>)); serializer.WriteObject(memoryStream, sharepointFolders); jsonResult = Encoding.UTF8.GetString(memoryStream.ToArray()); } return jsonResult; } private List<SharePointFolderModel> GetAllSharePointFiles(Dictionary<string, string> folders) { List<SharePointFolderModel> sharepointFolders = new List<SharePointFolderModel>(); DataContractJsonSerializer serializer = null; foreach (var folder in folders) { var sharepointFolder = new SharePointFolderModel(); sharepointFolder.FolderName = folder.Key; List<Result> results = new List<Result>(); string odataQuery = $"_api/Web/GetFolderByServerRelativeUrl('{folder.Value}')/Files"; string digest = _spo.GetRequestDigest(); Uri url = new Uri($"{_spo.SiteUrl}/{odataQuery}"); byte[] content = null; var webRequest = (HttpWebRequest)HttpWebRequest.Create(url); byte[] result = HttpHelper.SendODataJsonRequest(url, "GET", content, webRequest, _spo); string response = Encoding.UTF8.GetString(result, 0, result.Length); using (MemoryStream DeSerializememoryStream = new MemoryStream()) { serializer = new DataContractJsonSerializer(typeof(RootObject)); StreamWriter writer = new StreamWriter(DeSerializememoryStream); writer.Write(response); writer.Flush(); DeSerializememoryStream.Position = 0; RootObject SerializedObject = (RootObject)serializer.ReadObject(DeSerializememoryStream); results = SerializedObject.d.results; } var files = new List<Result>(results); sharepointFolder.Files = files.Select(i => GetFileForDisplay(i)).ToList(); sharepointFolders.Add(sharepointFolder); } return sharepointFolders; } private SharePointFileModel GetFileForDisplay(Result file) { SharePointFileModel sharePointFile = new SharePointFileModel() { Name = file.Name, FileRelativePath = file.ServerRelativeUrl, CreatedOn = file.TimeCreated, ModifiedOn = file.TimeLastModified, FileId = file.UniqueId, Base64FileContent = GetFileThumbnail(file.ServerRelativeUrl) }; return sharePointFile; } private string GetFileThumbnail(string filepath) { var uri = "https://yourdomain.sharepoint.com/_layouts/15/getpreview.ashx?path=" + WebUtility.UrlEncode(filepath); var thumbnail = HttpHelper.SendODataRequest(new Uri(uri),"GET",null,(HttpWebRequest)HttpWebRequest.Create(uri),_spo); return thumbnail != null ? Convert.ToBase64String(thumbnail) : null; }
The separate page was added for the retrieval – getfilebody, which contains its own web template. The body of the file returned in base64 string format.
{% Fetchxml sharepointfetch%} <fetch> <entity name="uds_sharepointfile"> <attribute name="uds_sharepointresponse"></attribute> <filter type="and"> <condition attribute="uds_filepath" operator="eq" value="{{request.params['filepath']}}"/> <condition attribute="uds_name" operator="eq" value="{{now}}"/> </filter> </entity> </fetch> {% endfetchxml %} {% assign entities = sharepointfetch.results.entities %} {% assign object = entities.first %} {{ object. uds_sharepointfilesjson }}
If request contains a condition attribute = "uds_filepath" operator = "eq" value = "folder/filename", service will receive a specific file and will return it in recoded Base64 string format.
- To receive data from SharePoint we used REST API, here’s the link with some implementation details:
https://msdn.microsoft.com/en-us/library/office/fp142380.aspx
public string GetSharePointFileAsBase64(string relativeUrlBase64) { var relativeUrl = Encoding.UTF8.GetString(Convert.FromBase64String(relativeUrlBase64)); var file = GetSharePointFile(relativeUrl); return file != null ? Convert.ToBase64String(file) : null; } byte[] GetSharePointFile(string relativeUrl) { try { var fileUri = $"https://yourdomain.sharepoint.com/_api/Web/GetFileByServerRelativeUrl('{relativeUrl}')/$value"; return HttpHelper.SendODataRequest(new Uri(fileUri), "GET", null, (HttpWebRequest)HttpWebRequest.Create(fileUri), _spo); } catch (Exception ex) { throw ex; } }
Page queries and received files rendering was implemented using JavaScript requests.