I promptly went to my favorite search engine and began to research this problem...but after many hours and searches I came up with nada, zip, zilch. A lot of stuff about SharePoint, and even some stuff about interacting with SharePoint from within a CRM plugin, but nothing related to the use case I was working on.
So after much research, time, and general reverse engineering of how CRM shows related documents in SharePoint, I came up with something.
The way that the SharePoint integration actually works when you click the 'Documents' link on a record is that it simply shows you the files in that document library and folder. CRM only stores a 'location' record for that regardingobjectid in an entity called SharePointDocumentLocation. That record has a parent reference in the same entity.
Once you establish the location reference, you can go directly to that folder in SP, add a new file, go back to CRM and you will see the new file in the documents view.
First, let me explain that, as far as I know, this scenario only works with CRM Online and SharePoint Online in the same Office 365 tenant. If you have some other combination of Online and On-Premise, or all On-Premise I have no idea if this will work. Feel free to give it a try, but I don't offer any warranty expressed or implied.
CRM and SharePoint Prep Work:
- Make sure you have already setup the SharePoint integration in CRM. There is plenty of documentation for this so I won't bother to describe it here.
- Make sure that your OOTB or Custom Entity is setup for document management. Again, there is plenty of documentation on this.
- As part of step 2, you 'should' end up with a document library in SharePoint with the same name as your entity.
Code Notes:
- I have this code setup as a console app, but you should be able to convert it to use in a web page as necessary. It won't work with an CRM Online Plugin because it requires some SharePoint .NET libraries that you can't get access to, or even ILMerge. There are some docs on that process and using the SharePoint REST endpoint, but I didn't use that method.
- .NET Framework is 4.5.2 for this code
- You will have to get the following nuget packages for this to work
- "Microsoft.CrmSdk.CoreAssemblies" version="7.1.0"
- "Microsoft.CrmSdk.Deployment" version="7.1.0"
- "Microsoft.CrmSdk.Extensions" version="7.1.0"
- "Microsoft.IdentityModel" version="6.1.7600.16394"
- "Microsoft.SharePointOnline.CSOM" version="16.1.3912.1204"
- You also have to have generated your XRM Class file using the CRMSVCUTIL tool.
=================
using Microsoft.SharePoint.Client;
using System;
using System.Linq;
using System.Security;
using XRMClass;
namespace zSPTest1
{
class Program
{
static void Main()
{
string SharePointCRMDocumentsSiteName = "https://contoso.sharepoint.com/sites/crmdocs";
string SharePointCRMDocumentsLoginName = "user.name@contoso.onmicrosoft.com";
string SharePointCRMDocumentsPwd = "ThisIsYourPassword!";
string SharePointCRMSchedulingRequestSiteName = "Account";
string fileName = "attachment.pdf";
byte[] fileContent = System.IO.File.ReadAllBytes("C:\\temp\\attachment.pdf");
// Note, this would normally be the id of the CRM Record to relate the document to
Guid recordId = new Guid();
// This requires the CrmSvcUtil.exe code generator to create the class file.
// Sample :: CrmSvcUtil.exe /out:XRMClass.cs /url:https://contoso.crm.dynamics.com/XRMServices/2011/Organization.svc /username:user.name@contoso.onmicrosoft.com /password:ThisIsYourPassword! /namespace:XRMClass /serviceContextName:XrmServiceContext /codeCustomization:"Microsoft.Xrm.Client.CodeGeneration.CodeCustomization,Microsoft.Xrm.Client.CodeGeneration"
XrmServiceContext xrm = new XrmServiceContext("xrm");
// Setup the Connect context to the SP Server.
ClientContext ctx = SetupSPContext(SharePointCRMDocumentsSiteName, SharePointCRMDocumentsLoginName, SharePointCRMDocumentsPwd);
// This is the path to the documents library in SharePoint to hold the attachments
string sitePath = SharePointCRMDocumentsSiteName + "/" + Account.EntityLogicalName.ToLower();
// Retrieve the Site record from CRM to get the relative path
SharePointSite spSite = xrm.SharePointSiteSet.Where(n => n.Name == SharePointCRMSchedulingRequestSiteName).SingleOrDefault();
// Here we build the URL with the site and path to the document library and folder to see if it already exists
string relativeURL = GetSPFolderPathFromCRM(recordId, Account.EntityLogicalName, spSite.Id);
// If it does not exist, we need to create a folder to hold the files for this record
// Note: For simplicity, we are using the guid of the record, but you can use whatever you like.
// CRM by default uses the record name, underscore, and the guid (e.g. Contoso_[record id guid] )
// When working with existing entity records and folders, you may need some more advanced code depending on how your
// SharePoint integration with CRM is setup.
if (string.IsNullOrEmpty(relativeURL))
{
CreateSPFolder(recordId, ctx, SharePointCRMSchedulingRequestSiteName, recordId.ToString().ToLower(), sitePath);
}
// Now we append the filename to the path
string fullURLWithFileName = sitePath + "/" + recordId.ToString() + "/" + fileName;
// Upload the file!
UploadFileToSP(ctx, "account", fullURLWithFileName, fileContent);
}
public static ClientContext SetupSPContext(string documentLibrarySiteURL, string userName, string password)
{
// This builds the connection to the SP Online Server
ClientContext clientContext = new ClientContext(documentLibrarySiteURL);
SecureString secureString = new SecureString();
foreach (char c in password.ToCharArray()) secureString.AppendChar(c);
clientContext.Credentials = new SharePointOnlineCredentials(userName, secureString);
Web web = clientContext.Web;
clientContext.Load(web);
clientContext.ExecuteQuery();
return clientContext;
}
public static string GetSPFolderPathFromCRM(Guid regardingObjectId, string regardingObjectEntityName, Guid siteId)
{
// We query the SharePointDocumentLocation entity to get the parentid
string returnValue = string.Empty;
XrmServiceContext xrm = new XrmServiceContext("xrm");
SharePointDocumentLocation parentLocationRecord = xrm.SharePointDocumentLocationSet
.Where(d => d.ParentSiteOrLocation.Id == siteId)
.OrderByDescending(d => d.ModifiedOn)
.FirstOrDefault();
if (parentLocationRecord == null) throw new Exception("parentLocationRecord not found, check CRM for SP Site!");
// Now we check if the location has already been entered into CRM for this record.
SharePointDocumentLocation documentLocation = xrm.SharePointDocumentLocationSet
.Where(d => d.RegardingObjectId.Id == regardingObjectId)
.Where(d => d.ParentSiteOrLocation.Id == parentLocationRecord.Id)
.FirstOrDefault();
// If it's there, then we return the path
if (documentLocation != null)
{
returnValue = documentLocation.RelativeUrl;
}
else
{
// Document Location record does not exist, so let's create it.
CreateSPDocumentLocationRecord(regardingObjectId, regardingObjectEntityName, parentLocationRecord);
returnValue = string.Empty;
}
return returnValue;
}
private static void CreateSPDocumentLocationRecord(Guid regardingObjectId, string regardingObjectEntityName, SharePointDocumentLocation parentLocationRecord)
{
// We have to link up the document location with a new record in CRM.
Microsoft.Xrm.Client.CrmConnection c = new Microsoft.Xrm.Client.CrmConnection("xrm");
XrmServiceContext xrm2 = new XrmServiceContext(c);
SharePointDocumentLocation newDocumentLocation = new SharePointDocumentLocation();
newDocumentLocation.RelativeUrl = regardingObjectId.ToString().ToLower();
newDocumentLocation.Name = regardingObjectEntityName + " :: " + regardingObjectId.ToString().ToLower();
newDocumentLocation.RegardingObjectId = new Microsoft.Xrm.Client.CrmEntityReference(regardingObjectEntityName, regardingObjectId);
newDocumentLocation.ParentSiteOrLocation = new Microsoft.Xrm.Client.CrmEntityReference(SharePointDocumentLocation.EntityLogicalName, parentLocationRecord.Id);
xrm2.Create(newDocumentLocation);
}
public static void CreateSPFolder(Guid regardingObjectId, ClientContext ctx, string listName, string folderName, string folderUrl)
{
// Create a new folder in the SP document library
// Note: For simplicity, we are using the guid of the record, but you can use whatever you like.
// CRM by default uses the record name, underscore, and the guid (e.g. Contoso_[record guid] )
// When working with existing entity records and folders, you may need some more advanced code depending on how your
// SharePoint integration with CRM is setup.
Microsoft.SharePoint.Client.List list = ctx.Web.Lists.GetByTitle(listName);
folderName = folderName.Trim();
ListItemCreationInformation info = new ListItemCreationInformation();
info.UnderlyingObjectType = FileSystemObjectType.Folder;
info.LeafName = folderName.Trim();
info.FolderUrl = folderUrl;
Microsoft.SharePoint.Client.ListItem newItem = list.AddItem(info);
newItem["Title"] = folderName.Trim();
newItem.Update();
ctx.ExecuteQuery();
}
public static void UploadFileToSP(ClientContext ctx, string listName, string fullUrlWithFileName, byte[] content)
{
// Send the file content to SP
Microsoft.SharePoint.Client.List list = ctx.Web.Lists.GetByTitle(listName);
FileCreationInformation file1 = new FileCreationInformation();
file1.Overwrite = true;
file1.Url = fullUrlWithFileName;
file1.Content = content;
Microsoft.SharePoint.Client.File uploadFile = list.RootFolder.Files.Add(file1);
ctx.Load(uploadFile);
ctx.ExecuteQuery();
}
}
}