A non-technical blog this month, dear reader, forced upon me because of a misleading blog I read today. Imagine that! What is the Internet coming too when you can't trust a blog?
Anyway, I Googled for the "Darwin Awards 2010" and stumbled on a blog that claimed to announce the Darwin Awards results for 2010. While some of the alleged awards were amusing - I particularly liked the guy who tried to siphon off diesel from a camper van but mistakenly put the hose into the sceptic tank - they were clearly not true Darwin Awards. I need scarcely remind you dear reader that the Darwin Awards "commemorate those who improve our gene pool by removing themselves from it". In short, they are only ever awarded posthumously for unparalleled stupidity.
The real Darwin Awards web site can be found at this location so please don't confuse it with imitations.
For reasons of good taste, I won't reproduce the winning entry here. However I do recount this runner up which unfortunately has not been confirmed as true.
In the late fall and early winter months, snow-covered mountains become infested with hunters. One ambitious pair climbed high up a mountain in search of their quarry. The trail crossed a small glacier that had crusted over. The lead hunter had to stomp a foot-hold in the snow, one step at a time, in order to cross the glacier.
Somewhere near the middle of the glacier, his next stomp hit not snow but a rock. The lead hunter lost his footing and fell. Down the crusty glacier he zipped, off the edge and out of sight.
Unable to help, his companion watched him slide away. After a while, he shouted out, "Are you OK?"
"Yes!" came the answer.
Reasoning that it was a quick way off the glacier, the second hunter plopped down and accelerated down the ice, following his friend. There, just over the edge of the glacier, was his friend...holding onto the top of a tree that barely protruded from the snow.
There were no other treetops nearby, nothing to grab, nothing but a hundred-foot drop onto the rocks below. As the second hunter shot past the first, he uttered his final epitaph: a single word, which we may not utter lest our mothers soap our mouths.
Monday, April 4, 2011
Monday, March 7, 2011
Windows Azure AppFabric and Integration with CRM 2011 Online
This post describes how to send messages in real time from CRM 2011 Online back to a company premises using Windows Azure. If you were using CRM 2011 On Premise you would use a plug-in and attach it to the create or update event of an entity. The plug-in code might write out the mesage in XML format for integration with an internal system. So how do you do this with CRM 2011 Online? The answer is to use the Windows Azure AppFabric because there is built in support for this in CRM 2011 Online.
Basically what you need to do is create a Service Bus on Windows Azure and then create a namespace. So you need to go through the steps of creating a Windows Azure account and set up a unique namespace. That will generate a "Current Management Key" whic you will need when you register the endpoint in the Plugin-tool. This post has a good description on how to do it.
You need to log into CRM Online and dowlnload the certificate that you need for the Plugin. Goto Settings -> Customizations -> Developer Resources and select download certificate. Make a note of the Issuer name directly above the link (e.g. crm4.dynamics.com).
Back in your development environment you will need to ensure you have the Windows Azure SDK and CRM 2011 SDK installed. You can use the sample code provided in the CRM SDK located at C:\CRMSDK\sdk\samplecode\cs\azure\onewaylistener. This is the application that will listen for messages that are sent to the Azure endpoint. The class implements IServiceEndpointPlugin and the class name serves as your "Contract" (you will use it as the "Path" when you configure the endpoint).
Open the CRM Plugin tool and configure a Service EndPoint. This post describes how to configure the Service Endpoint. The Solution Namespace is the same as the namespace you created in Azure. The Path is where you enter the class name of your listener application. Click Save and Configure ACS. You will be prompted for the location of the certificate and the issuer name. You are also prompted for the Management Key and this is current Management Key from the Windows Azure namespace. Click on "Save and verify authentication" which should verify the connection with a success message. Click on "Close".
Beneath the Service Endpoint you can now add a Step and configure it as you would have done for an On Premise plugin only now the event handler is the service endpoint you just created. You could for example set the plugin to fire on the Create event for the Contact entity.
That done you can now configure the app.comfig with your (Azure) Service Namespace, Isssuer Secret (your Azure Management Key) and the Service Path which is the name of your listener class. Run your OneWayListener application, and that should open a connection to your Azure endpoint and will print out messages as they are received to the console window.
So now go to CRM 2011 Online and perform the action that your Plugin is attached to (e.g. create contact). After you save the Contact, the Asynchronous process runs and your listener will receive the message.
The Onewaylistener code uses a RemoteExecutionContext object which has all the information you need from the User Guid to the Organization Guid. The interesting stuff is in the context.InputParameters collection. It contains all the data you entered. Its a simple task to turn this into XML and then you are all set to integrate with whatever you want.
Basically what you need to do is create a Service Bus on Windows Azure and then create a namespace. So you need to go through the steps of creating a Windows Azure account and set up a unique namespace. That will generate a "Current Management Key" whic you will need when you register the endpoint in the Plugin-tool. This post has a good description on how to do it.
You need to log into CRM Online and dowlnload the certificate that you need for the Plugin. Goto Settings -> Customizations -> Developer Resources and select download certificate. Make a note of the Issuer name directly above the link (e.g. crm4.dynamics.com).
Back in your development environment you will need to ensure you have the Windows Azure SDK and CRM 2011 SDK installed. You can use the sample code provided in the CRM SDK located at C:\CRMSDK\sdk\samplecode\cs\azure\onewaylistener. This is the application that will listen for messages that are sent to the Azure endpoint. The class implements IServiceEndpointPlugin and the class name serves as your "Contract" (you will use it as the "Path" when you configure the endpoint).
Open the CRM Plugin tool and configure a Service EndPoint. This post describes how to configure the Service Endpoint. The Solution Namespace is the same as the namespace you created in Azure. The Path is where you enter the class name of your listener application. Click Save and Configure ACS. You will be prompted for the location of the certificate and the issuer name. You are also prompted for the Management Key and this is current Management Key from the Windows Azure namespace. Click on "Save and verify authentication" which should verify the connection with a success message. Click on "Close".
Beneath the Service Endpoint you can now add a Step and configure it as you would have done for an On Premise plugin only now the event handler is the service endpoint you just created. You could for example set the plugin to fire on the Create event for the Contact entity.
That done you can now configure the app.comfig with your (Azure) Service Namespace, Isssuer Secret (your Azure Management Key) and the Service Path which is the name of your listener class. Run your OneWayListener application, and that should open a connection to your Azure endpoint and will print out messages as they are received to the console window.
So now go to CRM 2011 Online and perform the action that your Plugin is attached to (e.g. create contact). After you save the Contact, the Asynchronous process runs and your listener will receive the message.
The Onewaylistener code uses a RemoteExecutionContext object which has all the information you need from the User Guid to the Organization Guid. The interesting stuff is in the context.InputParameters collection. It contains all the data you entered. Its a simple task to turn this into XML and then you are all set to integrate with whatever you want.
Labels:
Cloud,
CRM 2011,
Integration,
Plugins,
Windows Azure
Saturday, March 5, 2011
Connecting to CRM 2011 in the Cloud
Connecting to CRM 2011 in the Cloud is trickier than connecting to CRM 2011 On Premise.
There is a great article by Deepak Kumar but it is worth recapping on his points.
Check c:\Program Files to see if you have Windows Identity Foundation. If not you will need to install it.
You need to get some device credentials and this is done using a utility in the CRMSDK called DEVICEREGISTRATION. Go to \sdk\tools\deviceregistration directory and load deviceregistration.csproj. Compile the application and navigate to the \bin\debug folder. Open a command window and type
deviceregistration.exe /operation:Register
The Device ID and Password that are generated are used in the method GetDeviceCredentials (that comes next).
Preparation work done you can now create your VS 2010 project, in this example I am using a WCF Service (I left the default name of Service)
Copy the CRMServiceHelper.cs file from \sdk\samplecode\cs\helpercode into your project and modify three methods:
GetDeviceCredentials
GetUserLogonCredentials
GetServerConfiguration
I used the code in Deepak Kumar's blog and set my Windows LiveID credentials in the GetUserLogonCredentials method. When you set the endpoints in GetServerConfiguration be aware that endpoints vary depending where you are in the world so see this blog .
Then if you want Intellisense on your custom entities and attributes you need to generate a class with them all in using CRMSVCUTIL.EXE. This tool is in the SDK too in the \sdk\bin directory. Amend your Environment Path so you have a reference to its location. Open a Command window, navigate to where you want the class to be generated then use
CrmSvcUtil.exe /url:https://{organisation}.crm4.dynamics.com/XRMServices/2011/Organization.svc /out:GeneratedCode.cs /username:"windows live id" /password:"live id password" /deviceid:"deviceid" /devicepassword:"device password"
Add the class you just generated to your project. OK, this takes a while and you will have to keep regenerating it every time you add an attribute but think of the time you'll save by having fewer bugs.
You will need to add references to the CRM SDK dlls (microsoft.crm.sdk.proxy.dll, microsoft.xrm.sdk.dll and microsoft.crm.sdk.dll) and then add these using statements
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Sdk.Client;
using Microsoft.Crm.Sdk.Messages;
Add 3 declarations in the class Service.
private OrganizationServiceContext context;
private OrganizationServiceProxy proxy;
private IOrganizationService service;
In the class constructor add a call to ConsumeIOrganization().
public Service()
{
ConsumeIOrganization();
}Then add two methods
public void ConsumeIOrganization()
{
ServerConnection serverConnection = new ServerConnection();
ServerConnection.Configuration serverConfig = serverConnection.GetServerConfiguration();
Connect(serverConfig);
}
public void Connect(ServerConnection.Configuration serverConfig)
{
try
{
using (proxy = new OrganizationServiceProxy(serverConfig.OrganizationUri, serverConfig.HomeRealmUri,
serverConfig.Credentials, serverConfig.DeviceCredentials))
{
proxy.ServiceConfiguration.CurrentServiceEndpoint.Behaviors.Add(new ProxyTypesBehavior());
context = new OrganizationServiceContext(proxy);
service = (IOrganizationService)proxy;
}
}
catch (FaultException <microsoft.xrm.sdk.organizationservicefault>)
{
throw;
}
}
Now you can add your code and reference service or context methods depending on which you prefer. Welcome to programming in the cloud!
There is a great article by Deepak Kumar but it is worth recapping on his points.
Check c:\Program Files to see if you have Windows Identity Foundation. If not you will need to install it.
You need to get some device credentials and this is done using a utility in the CRMSDK called DEVICEREGISTRATION. Go to \sdk\tools\deviceregistration directory and load deviceregistration.csproj. Compile the application and navigate to the \bin\debug folder. Open a command window and type
deviceregistration.exe /operation:Register
The Device ID and Password that are generated are used in the method GetDeviceCredentials (that comes next).
Preparation work done you can now create your VS 2010 project, in this example I am using a WCF Service (I left the default name of Service)
Copy the CRMServiceHelper.cs file from \sdk\samplecode\cs\helpercode into your project and modify three methods:
GetDeviceCredentials
GetUserLogonCredentials
GetServerConfiguration
I used the code in Deepak Kumar's blog and set my Windows LiveID credentials in the GetUserLogonCredentials method. When you set the endpoints in GetServerConfiguration be aware that endpoints vary depending where you are in the world so see this blog .
Then if you want Intellisense on your custom entities and attributes you need to generate a class with them all in using CRMSVCUTIL.EXE. This tool is in the SDK too in the \sdk\bin directory. Amend your Environment Path so you have a reference to its location. Open a Command window, navigate to where you want the class to be generated then use
CrmSvcUtil.exe /url:https://{organisation}.crm4.dynamics.com/XRMServices/2011/Organization.svc /out:GeneratedCode.cs /username:"windows live id" /password:"live id password" /deviceid:"deviceid" /devicepassword:"device password"
Add the class you just generated to your project. OK, this takes a while and you will have to keep regenerating it every time you add an attribute but think of the time you'll save by having fewer bugs.
You will need to add references to the CRM SDK dlls (microsoft.crm.sdk.proxy.dll, microsoft.xrm.sdk.dll and microsoft.crm.sdk.dll) and then add these using statements
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Sdk.Client;
using Microsoft.Crm.Sdk.Messages;
Add 3 declarations in the class Service.
private OrganizationServiceContext context;
private OrganizationServiceProxy proxy;
private IOrganizationService service;
In the class constructor add a call to ConsumeIOrganization().
public Service()
{
ConsumeIOrganization();
}Then add two methods
public void ConsumeIOrganization()
{
ServerConnection serverConnection = new ServerConnection();
ServerConnection.Configuration serverConfig = serverConnection.GetServerConfiguration();
Connect(serverConfig);
}
public void Connect(ServerConnection.Configuration serverConfig)
{
try
{
using (proxy = new OrganizationServiceProxy(serverConfig.OrganizationUri, serverConfig.HomeRealmUri,
serverConfig.Credentials, serverConfig.DeviceCredentials))
{
proxy.ServiceConfiguration.CurrentServiceEndpoint.Behaviors.Add(new ProxyTypesBehavior());
context = new OrganizationServiceContext(proxy);
service = (IOrganizationService)proxy;
}
}
catch (FaultException <microsoft.xrm.sdk.organizationservicefault>)
{
throw;
}
}
Now you can add your code and reference service or context methods depending on which you prefer. Welcome to programming in the cloud!
Labels:
Cloud,
CRM 2011,
data connections,
Online,
OrganizationService,
Windows Live ID
Saturday, February 19, 2011
Create SharePoint Document Locations in CRM 2011 - Part 3
This is the last of 3 posts describing how to add SharePoint document locations into CRM.
Part 1
Part 2
This code should be added to that in the previous blog post. I had a devil of a job with a 401 error, but this finally cracked it.
Add a web reference to
http://server01/_vti_bin/Lists.asmx and name the reference listservice
http://server01/_vti_bin/Views.asmx and name the reference views
Add a using statement
using system.Xml;
Add this method into the code in previous blog post.
private static void CreateSharePointFolder(string docfolderUrl)
{
if (docfolderUrl == String.Empty || docfolderUrl.IndexOf("/") == -1)
{
return;
}
try
{
// last part is the folder name
string folderName = docfolderUrl.Substring(docfolderUrl.LastIndexOf("/") + 1);
// remove the folder name
docfolderUrl = docfolderUrl.Replace("/" + folderName, "");
// get the document libray name
string docLib = docfolderUrl.Substring(docfolderUrl.LastIndexOf("/") + 1);
// now remove the doc lib to leave the sharepoint site url
string sharePointSiteUrl = docfolderUrl.Replace("/" + docLib, "");
listservice.Lists myLists = new listservice.Lists();
views.Views myViews = new views.Views();
myLists.Url = sharePointSiteUrl + "/_vti_bin/lists.asmx";
myViews.Url = sharePointSiteUrl + "/_vti_bin/views.asmx";
myLists.UseDefaultCredentials = true;
myViews.UseDefaultCredentials = true;
XmlNode viewCol = myViews.GetViewCollection(docLib);
XmlNode viewNode = viewCol.SelectSingleNode("*[@DisplayName='All Documents']");
string viewName = viewNode.Attributes["Name"].Value.ToString();
/*Get Name attribute values (GUIDs) for list and view. */
System.Xml.XmlNode ndListView = myLists.GetListAndView(docLib, viewName);
/*Get Name attribute values (GUIDs) for list and view. */
string strListID = ndListView.ChildNodes[0].Attributes["Name"].Value;
string strViewID = ndListView.ChildNodes[1].Attributes["Name"].Value;
// load the CAML query
XmlDocument doc = new XmlDocument();
string xmlCommand;
xmlCommand = "<Method ID='1' Cmd='New'><Field Name='FSObjType'>1</Field><Field Name='BaseName'>" + folderName + "</Field> <Field Name='ID'>New</Field></Method>";
XmlElement ele = doc.CreateElement("Batch");
ele.SetAttribute("OnError", "Continue");
ele.SetAttribute("ListVersion", "1");
ele.SetAttribute("ViewName", strViewID);
ele.InnerXml = xmlCommand;
XmlNode resultNode = myLists.UpdateListItems(strListID, ele);
// check for errors
NameTable nt = new NameTable();
XmlNamespaceManager nsmgr = new XmlNamespaceManager(nt);
nsmgr.AddNamespace("tns", "http://schemas.microsoft.com/sharepoint/soap/");
if (resultNode != null)
{ // look for error text in case of duplicate folder or invalid folder name
XmlNode errNode = resultNode.SelectSingleNode("tns:Result/tns:ErrorText", nsmgr);
if (errNode != null)
{
// Write error to log;
}
}
}
catch (Exception ex)
{
throw ex ;
}
}
Part 1
Part 2
This code should be added to that in the previous blog post. I had a devil of a job with a 401 error, but this finally cracked it.
Add a web reference to
http://server01/_vti_bin/Lists.asmx and name the reference listservice
http://server01/_vti_bin/Views.asmx and name the reference views
Add a using statement
using system.Xml;
Add this method into the code in previous blog post.
private static void CreateSharePointFolder(string docfolderUrl)
{
if (docfolderUrl == String.Empty || docfolderUrl.IndexOf("/") == -1)
{
return;
}
try
{
// last part is the folder name
string folderName = docfolderUrl.Substring(docfolderUrl.LastIndexOf("/") + 1);
// remove the folder name
docfolderUrl = docfolderUrl.Replace("/" + folderName, "");
// get the document libray name
string docLib = docfolderUrl.Substring(docfolderUrl.LastIndexOf("/") + 1);
// now remove the doc lib to leave the sharepoint site url
string sharePointSiteUrl = docfolderUrl.Replace("/" + docLib, "");
listservice.Lists myLists = new listservice.Lists();
views.Views myViews = new views.Views();
myLists.Url = sharePointSiteUrl + "/_vti_bin/lists.asmx";
myViews.Url = sharePointSiteUrl + "/_vti_bin/views.asmx";
myLists.UseDefaultCredentials = true;
myViews.UseDefaultCredentials = true;
XmlNode viewCol = myViews.GetViewCollection(docLib);
XmlNode viewNode = viewCol.SelectSingleNode("*[@DisplayName='All Documents']");
string viewName = viewNode.Attributes["Name"].Value.ToString();
/*Get Name attribute values (GUIDs) for list and view. */
System.Xml.XmlNode ndListView = myLists.GetListAndView(docLib, viewName);
/*Get Name attribute values (GUIDs) for list and view. */
string strListID = ndListView.ChildNodes[0].Attributes["Name"].Value;
string strViewID = ndListView.ChildNodes[1].Attributes["Name"].Value;
// load the CAML query
XmlDocument doc = new XmlDocument();
string xmlCommand;
xmlCommand = "<Method ID='1' Cmd='New'><Field Name='FSObjType'>1</Field><Field Name='BaseName'>" + folderName + "</Field> <Field Name='ID'>New</Field></Method>";
XmlElement ele = doc.CreateElement("Batch");
ele.SetAttribute("OnError", "Continue");
ele.SetAttribute("ListVersion", "1");
ele.SetAttribute("ViewName", strViewID);
ele.InnerXml = xmlCommand;
XmlNode resultNode = myLists.UpdateListItems(strListID, ele);
// check for errors
NameTable nt = new NameTable();
XmlNamespaceManager nsmgr = new XmlNamespaceManager(nt);
nsmgr.AddNamespace("tns", "http://schemas.microsoft.com/sharepoint/soap/");
if (resultNode != null)
{ // look for error text in case of duplicate folder or invalid folder name
XmlNode errNode = resultNode.SelectSingleNode("tns:Result/tns:ErrorText", nsmgr);
if (errNode != null)
{
// Write error to log;
}
}
}
catch (Exception ex)
{
throw ex ;
}
}
Create SharePoint Document Locations in CRM 2011 - Part 2
This is part 2 of three posts
Part 1
Part 3
This code expects the Guid for the Contact and then returns the SharePointDocumentLocation AbsoluteUrl. For example it should return <a href="http://server01/contact/Charles Emes">http://server01/contact/Charles Emes</a>. That folder should exist in the SharePoint document library 'Contact'. Note that the code for creating the SharePoint folder is in the following blog post.
Add references to Microsoft.Crm.Sdk.Proxy.dll and Microsoft.Xrm.Sdk.dll to your project. Add the file crmsdktypes.cs into your project (note the SharePointDocumentLocation object is defined within this class and the code won't work without it). All of these files you will find in the CRM SDK directory.
Add the following using statements
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Sdk.Client;
using Microsoft.Crm.Sdk.Messages;
public class IntegrationService
{
// private static string ContactId = "3E339B88-C632-E011-BCDE-00155D110735";
public static string GetSharePointLocation(string ContactId)
{
try
{
// Connect to the Organization service.
System.ServiceModel.Description.ClientCredentials cred = new System.ServiceModel.Description.ClientCredentials();
cred.Windows.ClientCredential = new System.Net.NetworkCredential("Username", "Password", "Domain");
Uri organizationUri = new Uri("http://server01:5555/orgName/XRMServices/2011/Organization.svc");
Uri homeRealmUri = null;
OrganizationServiceProxy orgService = new OrganizationServiceProxy(organizationUri, homeRealmUri, cred, null);
// This statement is required to enable early-bound type support.
// IMPORTANT ADD THIS LINE
orgService.ServiceConfiguration.CurrentServiceEndpoint.Behaviors.Add(new ProxyTypesBehavior());
Guid guidSPDocLoc = RetrieveSharePointLocation(orgService, ContactId);
string absouteUrl = GetAbslouteUrl(orgService, guidSPDocLoc);
orgService.Dispose();
return absouteUrl;
}
// Catch any service fault exceptions that Microsoft Dynamics CRM throws.
catch (FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault> ex)
{
// You can handle an exception here or pass it back to the calling method.
return ex.Message;
}
}
private static Guid RetrieveSharePointLocation(OrganizationServiceProxy orgService, string ContactId)
{
Guid _spDocLocId = Guid.Empty;
// get the fullname from the contactid - that will be foolder name
string FolderName = GetEntityNamefromGuid(orgService, ContactId);
// replace any illegal chars with '-'
// TO DO
string fetch2 = @"
<fetch mapping='logical'>
<entity name='sharepointdocumentlocation'>
<attribute name='sharepointdocumentlocationid'/>
<filter type='and'>
<condition attribute='regardingobjectid' operator='eq' value='[GUID]' />
</filter>
</entity>
</fetch> ";
fetch2 = fetch2.Replace("[GUID]", ContactId);
EntityCollection result = orgService.RetrieveMultiple(new FetchExpression(fetch2));
foreach (var c in result.Entities)
{
// TO DO there can be more than one so add condition
_spDocLocId = (Guid) c.Attributes["sharepointdocumentlocationid"];
}
if (_spDocLocId == Guid.Empty)
{
// there is no location so create one
_spDocLocId = CreateSharePointDocLocation(orgService, FolderName, ContactId);
// get the abslouteURL from the doc location just created
string absouteUrl = GetAbslouteUrl(orgService, _spDocLocId);
// We still need to create a SharePoint folder
// THIS METHOD IN FOLLOWING POST **********
CreateSharePointFolder(absouteUrl);
}
return _spDocLocId;
}
private static string GetEntityNamefromGuid(OrganizationServiceProxy orgService, string ContactId)
{
string fetch1 = @"
<fetch mapping='logical'>
<entity name='contact'>
<attribute name='fullname'/>
<filter type='and'>
<condition attribute='contactid' operator='eq' value='[GUID]' />
</filter>
</entity>
</fetch> ";
fetch1 = fetch1.Replace("[GUID]", ContactId);
string fullname = string.Empty;
EntityCollection result = orgService.RetrieveMultiple(new FetchExpression(fetch1));
foreach (var c in result.Entities)
{ // there can be more than one so add condition
fullname = c.Attributes["fullname"].ToString();
}
return fullname;
}
private static Guid CreateSharePointDocLocation(OrganizationServiceProxy _serviceProxy, string FolderName, string ContactId)
{
// use the Parent Location Id NOT the SharePointSiteId
// Parent Location will create url http://sharepoint/contact/CharlesEmes
// SharePointSiteID will create url http://sharepoint/CharlesEmes
Guid _spParentLocId = new Guid("415FF5BA-CA39-E011-92D1-00155D110735");
// Instantiate a SharePoint document location object.
SharePointDocumentLocation spDocLoc = new SharePointDocumentLocation
{
Name = "Documents on Default Site 1",
Description = null,
// Set the Regarding Object id - in this case its a contact
RegardingObjectId = new EntityReference(Contact.EntityLogicalName , new Guid(ContactId)),
// Set the Parent Location ID
ParentSiteOrLocation = new EntityReference(SharePointDocumentLocation.EntityLogicalName, _spParentLocId),
RelativeUrl = FolderName
};
// Create a SharePoint document location record named Documents on Default Site 1.
Guid _spDocLocId = _serviceProxy.Create(spDocLoc);
// Console.WriteLine("{0} created.", spDocLoc.Name);
return _spDocLocId;
}
private static string GetAbslouteUrl(OrganizationServiceProxy orgService, Guid _spDocLocId)
{
IOrganizationService _service = (IOrganizationService)orgService;
RetrieveAbsoluteAndSiteCollectionUrlRequest retrieveRequest = new RetrieveAbsoluteAndSiteCollectionUrlRequest
{
Target = new EntityReference(SharePointDocumentLocation.EntityLogicalName, _spDocLocId)
};
RetrieveAbsoluteAndSiteCollectionUrlResponse retrieveResponse = (RetrieveAbsoluteAndSiteCollectionUrlResponse)_service.Execute(retrieveRequest);
return retrieveResponse.AbsoluteUrl.ToString();
}
}
Part 1
Part 3
This code expects the Guid for the Contact and then returns the SharePointDocumentLocation AbsoluteUrl. For example it should return <a href="http://server01/contact/Charles Emes">http://server01/contact/Charles Emes</a>. That folder should exist in the SharePoint document library 'Contact'. Note that the code for creating the SharePoint folder is in the following blog post.
Add references to Microsoft.Crm.Sdk.Proxy.dll and Microsoft.Xrm.Sdk.dll to your project. Add the file crmsdktypes.cs into your project (note the SharePointDocumentLocation object is defined within this class and the code won't work without it). All of these files you will find in the CRM SDK directory.
Add the following using statements
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Sdk.Client;
using Microsoft.Crm.Sdk.Messages;
public class IntegrationService
{
// private static string ContactId = "3E339B88-C632-E011-BCDE-00155D110735";
public static string GetSharePointLocation(string ContactId)
{
try
{
// Connect to the Organization service.
System.ServiceModel.Description.ClientCredentials cred = new System.ServiceModel.Description.ClientCredentials();
cred.Windows.ClientCredential = new System.Net.NetworkCredential("Username", "Password", "Domain");
Uri organizationUri = new Uri("http://server01:5555/orgName/XRMServices/2011/Organization.svc");
Uri homeRealmUri = null;
OrganizationServiceProxy orgService = new OrganizationServiceProxy(organizationUri, homeRealmUri, cred, null);
// This statement is required to enable early-bound type support.
// IMPORTANT ADD THIS LINE
orgService.ServiceConfiguration.CurrentServiceEndpoint.Behaviors.Add(new ProxyTypesBehavior());
Guid guidSPDocLoc = RetrieveSharePointLocation(orgService, ContactId);
string absouteUrl = GetAbslouteUrl(orgService, guidSPDocLoc);
orgService.Dispose();
return absouteUrl;
}
// Catch any service fault exceptions that Microsoft Dynamics CRM throws.
catch (FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault> ex)
{
// You can handle an exception here or pass it back to the calling method.
return ex.Message;
}
}
private static Guid RetrieveSharePointLocation(OrganizationServiceProxy orgService, string ContactId)
{
Guid _spDocLocId = Guid.Empty;
// get the fullname from the contactid - that will be foolder name
string FolderName = GetEntityNamefromGuid(orgService, ContactId);
// replace any illegal chars with '-'
// TO DO
string fetch2 = @"
<fetch mapping='logical'>
<entity name='sharepointdocumentlocation'>
<attribute name='sharepointdocumentlocationid'/>
<filter type='and'>
<condition attribute='regardingobjectid' operator='eq' value='[GUID]' />
</filter>
</entity>
</fetch> ";
fetch2 = fetch2.Replace("[GUID]", ContactId);
EntityCollection result = orgService.RetrieveMultiple(new FetchExpression(fetch2));
foreach (var c in result.Entities)
{
// TO DO there can be more than one so add condition
_spDocLocId = (Guid) c.Attributes["sharepointdocumentlocationid"];
}
if (_spDocLocId == Guid.Empty)
{
// there is no location so create one
_spDocLocId = CreateSharePointDocLocation(orgService, FolderName, ContactId);
// get the abslouteURL from the doc location just created
string absouteUrl = GetAbslouteUrl(orgService, _spDocLocId);
// We still need to create a SharePoint folder
// THIS METHOD IN FOLLOWING POST **********
CreateSharePointFolder(absouteUrl);
}
return _spDocLocId;
}
private static string GetEntityNamefromGuid(OrganizationServiceProxy orgService, string ContactId)
{
string fetch1 = @"
<fetch mapping='logical'>
<entity name='contact'>
<attribute name='fullname'/>
<filter type='and'>
<condition attribute='contactid' operator='eq' value='[GUID]' />
</filter>
</entity>
</fetch> ";
fetch1 = fetch1.Replace("[GUID]", ContactId);
string fullname = string.Empty;
EntityCollection result = orgService.RetrieveMultiple(new FetchExpression(fetch1));
foreach (var c in result.Entities)
{ // there can be more than one so add condition
fullname = c.Attributes["fullname"].ToString();
}
return fullname;
}
private static Guid CreateSharePointDocLocation(OrganizationServiceProxy _serviceProxy, string FolderName, string ContactId)
{
// use the Parent Location Id NOT the SharePointSiteId
// Parent Location will create url http://sharepoint/contact/CharlesEmes
// SharePointSiteID will create url http://sharepoint/CharlesEmes
Guid _spParentLocId = new Guid("415FF5BA-CA39-E011-92D1-00155D110735");
// Instantiate a SharePoint document location object.
SharePointDocumentLocation spDocLoc = new SharePointDocumentLocation
{
Name = "Documents on Default Site 1",
Description = null,
// Set the Regarding Object id - in this case its a contact
RegardingObjectId = new EntityReference(Contact.EntityLogicalName , new Guid(ContactId)),
// Set the Parent Location ID
ParentSiteOrLocation = new EntityReference(SharePointDocumentLocation.EntityLogicalName, _spParentLocId),
RelativeUrl = FolderName
};
// Create a SharePoint document location record named Documents on Default Site 1.
Guid _spDocLocId = _serviceProxy.Create(spDocLoc);
// Console.WriteLine("{0} created.", spDocLoc.Name);
return _spDocLocId;
}
private static string GetAbslouteUrl(OrganizationServiceProxy orgService, Guid _spDocLocId)
{
IOrganizationService _service = (IOrganizationService)orgService;
RetrieveAbsoluteAndSiteCollectionUrlRequest retrieveRequest = new RetrieveAbsoluteAndSiteCollectionUrlRequest
{
Target = new EntityReference(SharePointDocumentLocation.EntityLogicalName, _spDocLocId)
};
RetrieveAbsoluteAndSiteCollectionUrlResponse retrieveResponse = (RetrieveAbsoluteAndSiteCollectionUrlResponse)_service.Execute(retrieveRequest);
return retrieveResponse.AbsoluteUrl.ToString();
}
}
Create SharePoint Document Locations in CRM 2011 - Part 1
This post is the first of three on how to programmatically create SharePoint 2010 Document Folders in CRM 2011 so that documents can be uploaded.
Part 2
Part 3
There is quite a lot of code for this solution which is why I've broken it up into 3 parts. This first part outlines the scenario and the assumptions.
So here is the scenario. I have CRM 2011 and I want to store documents related to the Contact entity in SharePoint 2010. In my development environment I have both of these on the same virtual image but I've designed the code to work as a web service so it can sit it anywhere.
What CRM 2011 does is create a document library for each Entity that is enabled for document storage, and then creates a folder for each record. This example focuses on the Contact entity but it can be easily applied to other entities. When a new entity record is created in CRM 2011 and the Document menu item is clicked, it will create a) a SharePointDocumentLocation record in CRM that has the path to the SharePoint folder and b) warns you that it is about to create a document folder. The name of the folder for a Contact if based on full name. So what happens if you have two John Smiths? If the first already has created a document folder it detects that and prompts with 'the folder already exists do you want to use it?' If not, then you can modify the name of the folder to ensure it is unique.
This is an imprtant point. Assume you have a contact called Robin Wright. When you create a document folder it will be called 'Robin Wright'. When Robin gets married and changes her name to 'Robin Wrigth-Penn' then the document folder name remains the same name. CRM 2011 uses the SharePoint document location record to point to the orginal folder name. So you need to be careful, you can't assume that the full name in CRM is the same as the document folder name.
For this example I am going to assume you have the Guid for the Contact in question. This unique identifier will be used to determine if there is already a SharePoint Document Location created for this record. If so, it returns the url for you. If not, it will create the SharePoint Document Location in CRM and then create the actual folder in SharePoint based on the full name and finally returns the url to the folder.
So this code either uses the existing SharePoint Document Location or will create a new one for you and create the folder in SharePoint.
I have made another assumption. I assume you have created the SharePoint site already with the document library for the entity "Contact". It will help if you have a few SharePoint document locations already created because then you will be able to see how the database field 'ParentSiteorLocation' is populated in the SharePointDocumentLocationBase table. You need to use the 'root' Guid for the Contact document library and it should be obvious what this is if you have a few records in the table.
You can recognise this row in the table as the relativeurl reads 'contact' and the RegardingObjectID is null. The Guid you need is the SharePointDocumentLocationId from this record. It will become clearer (I hope) when you see the code.
There is another issue you will need to be aware of. Folder names in SharePoint will not allow certain characters. If they exist in your CRM Contact record e.g. the first name reads 'John & Jane' then CRM replaces the & with -. I have not checked all illegal characters are substituted with hyphen but it is my working assumption. So be warned, you will need to modify full name of contact to replace illegal characters. Also please note this code does NOT handle duplicate names - you will need to check for a duplicate and create a unique folder name.
So the next blog has the CRM code for using an existing SharePoint Document Location or create a new location. There is a reference in the code to a function that will create the actual SharePoint folder but the code for that is in the last blog.
Part 2
Part 3
There is quite a lot of code for this solution which is why I've broken it up into 3 parts. This first part outlines the scenario and the assumptions.
So here is the scenario. I have CRM 2011 and I want to store documents related to the Contact entity in SharePoint 2010. In my development environment I have both of these on the same virtual image but I've designed the code to work as a web service so it can sit it anywhere.
What CRM 2011 does is create a document library for each Entity that is enabled for document storage, and then creates a folder for each record. This example focuses on the Contact entity but it can be easily applied to other entities. When a new entity record is created in CRM 2011 and the Document menu item is clicked, it will create a) a SharePointDocumentLocation record in CRM that has the path to the SharePoint folder and b) warns you that it is about to create a document folder. The name of the folder for a Contact if based on full name. So what happens if you have two John Smiths? If the first already has created a document folder it detects that and prompts with 'the folder already exists do you want to use it?' If not, then you can modify the name of the folder to ensure it is unique.
This is an imprtant point. Assume you have a contact called Robin Wright. When you create a document folder it will be called 'Robin Wright'. When Robin gets married and changes her name to 'Robin Wrigth-Penn' then the document folder name remains the same name. CRM 2011 uses the SharePoint document location record to point to the orginal folder name. So you need to be careful, you can't assume that the full name in CRM is the same as the document folder name.
For this example I am going to assume you have the Guid for the Contact in question. This unique identifier will be used to determine if there is already a SharePoint Document Location created for this record. If so, it returns the url for you. If not, it will create the SharePoint Document Location in CRM and then create the actual folder in SharePoint based on the full name and finally returns the url to the folder.
So this code either uses the existing SharePoint Document Location or will create a new one for you and create the folder in SharePoint.
I have made another assumption. I assume you have created the SharePoint site already with the document library for the entity "Contact". It will help if you have a few SharePoint document locations already created because then you will be able to see how the database field 'ParentSiteorLocation' is populated in the SharePointDocumentLocationBase table. You need to use the 'root' Guid for the Contact document library and it should be obvious what this is if you have a few records in the table.
You can recognise this row in the table as the relativeurl reads 'contact' and the RegardingObjectID is null. The Guid you need is the SharePointDocumentLocationId from this record. It will become clearer (I hope) when you see the code.
There is another issue you will need to be aware of. Folder names in SharePoint will not allow certain characters. If they exist in your CRM Contact record e.g. the first name reads 'John & Jane' then CRM replaces the & with -. I have not checked all illegal characters are substituted with hyphen but it is my working assumption. So be warned, you will need to modify full name of contact to replace illegal characters. Also please note this code does NOT handle duplicate names - you will need to check for a duplicate and create a unique folder name.
So the next blog has the CRM code for using an existing SharePoint Document Location or create a new location. There is a reference in the code to a function that will create the actual SharePoint folder but the code for that is in the last blog.
Friday, February 11, 2011
BizTalk Receive Location Error - Verify the schema deployed properly.
I was getting an error with Biztalk Server when receiving a file from MSMQ although you get a similar error on FILE receive locations. The message was
There was a failure executing the receive pipeline: "Microsoft.BizTalk.DefaultPipelines.XMLReceive, Microsoft.BizTalk.DefaultPipelines, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Source: "XML disassembler" Receive Port: "Portname" URI: "FORMATNAME:DIRECT=OS:.\PRIVATE$\QNAME" Reason: Finding the document specification by message type "RootNode" failed. Verify the schema deployed properly.
It is caused because the message does not conform to a schema that Biztalk recognises, in my case because the RootNode is not recognised. The problem is BizTalk has not saved the original message. It's gone. The method described here shows how to keep a copy of the original file. There maybe other solutions but this works well for me.
The solution I outline is for FILE ports but once you understand the principle you will see how to apply it for MSMQ. The principle is to use a Receive Location and a Send Port Group configured for the Pass Through pipeline.
The Receive Location becomes the new port which polls for inbound messages that arrive in the directory c:\inbound. The Receive Location is bound to a Recieve Port which I'll call ReceivePort1.
The Send Port Group has 2 Send Ports in it. Both Send Ports, for the sake of simplicity, are FILE ports that are configured to write out the file using %SourceFileName% to the following directories c:\filecopy and c:\biztalkinbound. Both are configured to use the Pass Through pipeline.
Got to the Filter on the Send Port Group and add the following filter
BTS.ReceivePortName == ReceivePort1
Enable the Receive Location and start both Send Ports and the Send Port Group. If you drop a file into c:\inbound it will appear in both directories c:\filecopy and c:\biztalkinbound and it will retain the original file name.
Your original Receive Location should now point to the location c:\biztalkinbound and it will be configured for whatever pipeline you were using (XML Receive or a custom Flat File pipeline).
The benefit of this is you always have a copy of the original file in its original format in the directory c:\filecopy. As an added treat I got my orchestration to delete files that were successfully processed from this directory. So c:\filecopy became the place which stored files that failed to be processed - the classic being that they failed to match the correct schema as described by the error at the beginning of the post. Send an email alert to the Sysem Administrator and you'll be a hero.
There was a failure executing the receive pipeline: "Microsoft.BizTalk.DefaultPipelines.XMLReceive, Microsoft.BizTalk.DefaultPipelines, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Source: "XML disassembler" Receive Port: "Portname" URI: "FORMATNAME:DIRECT=OS:.\PRIVATE$\QNAME" Reason: Finding the document specification by message type "RootNode" failed. Verify the schema deployed properly.
It is caused because the message does not conform to a schema that Biztalk recognises, in my case because the RootNode is not recognised. The problem is BizTalk has not saved the original message. It's gone. The method described here shows how to keep a copy of the original file. There maybe other solutions but this works well for me.
The solution I outline is for FILE ports but once you understand the principle you will see how to apply it for MSMQ. The principle is to use a Receive Location and a Send Port Group configured for the Pass Through pipeline.
The Receive Location becomes the new port which polls for inbound messages that arrive in the directory c:\inbound. The Receive Location is bound to a Recieve Port which I'll call ReceivePort1.
The Send Port Group has 2 Send Ports in it. Both Send Ports, for the sake of simplicity, are FILE ports that are configured to write out the file using %SourceFileName% to the following directories c:\filecopy and c:\biztalkinbound. Both are configured to use the Pass Through pipeline.
Got to the Filter on the Send Port Group and add the following filter
BTS.ReceivePortName == ReceivePort1
Enable the Receive Location and start both Send Ports and the Send Port Group. If you drop a file into c:\inbound it will appear in both directories c:\filecopy and c:\biztalkinbound and it will retain the original file name.
Your original Receive Location should now point to the location c:\biztalkinbound and it will be configured for whatever pipeline you were using (XML Receive or a custom Flat File pipeline).
The benefit of this is you always have a copy of the original file in its original format in the directory c:\filecopy. As an added treat I got my orchestration to delete files that were successfully processed from this directory. So c:\filecopy became the place which stored files that failed to be processed - the classic being that they failed to match the correct schema as described by the error at the beginning of the post. Send an email alert to the Sysem Administrator and you'll be a hero.
Subscribe to:
Posts (Atom)