D365 Authentication – connect my apps

With the constant push to make data more secure D365 now includes Application Users that can be configured to allow access to D365 from external applications. There are numerous articles around explaining how to create your Application User in D365 using Azure AD App Registrations – this one from PowerObjects has a good explanation of steps required to achieve this.

In this post I wanted to briefly describe the next steps for using that authentication method in your app to perform CRUD operations. To demonstrate this I created a simple Console App to connect to a D365 instance and perform some simple operations using both the OrganizationWebProxyClient and the CrmServiceClient. For reference, I added a package from NuGet called Microsoft.CrmSdk.XrmTooling.CoreAssembly which contains the official Microsoft.Xrm.Tooling.Connector assembly. The same NuGet package also contains the Microsoft.CrmSdk.CoreAssemblies dependency which contains the Microsoft.Xrm.Sdk.dll. Within the console app I created a class that manages to authentication.

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Collections.Generic;

namespace OrgService.Connect
{
    class AuthHook : Microsoft.Xrm.Tooling.Connector.IOverrideAuthHookWrapper
    {
        // In memory cache of access tokens
        Dictionary<string, AuthenticationResult> accessTokens = new Dictionary<string, Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult>();

        public void AddAccessToken(Uri orgUri, Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult accessToken)
        {
            // Access tokens can be matched on the hostname,
            // different endpoints in the same organization can use the same access token
            accessTokens[orgUri.Host] = accessToken;
        }

        public string GetAuthToken(Uri connectedUri)
        {
            // Check if you have an access token for this host
            if (accessTokens.ContainsKey(connectedUri.Host) && accessTokens[connectedUri.Host].ExpiresOn > DateTime.Now)
            {
                return accessTokens[connectedUri.Host].AccessToken;
            }
            else
            {
                accessTokens[connectedUri.Host] = GetAccessTokenFromAzureAD(connectedUri);
            }
            return accessTokens[connectedUri.Host].AccessToken;
        }

        private AuthenticationResult GetAccessTokenFromAzureAD(Uri orgUrl)
        {
            string organizationUrl = "https://myD365instance.crm6.dynamics.com";
            string clientId = "00000000-0000-0000-0000-000000000001";    // This is the Application ID from your App Registration
            string appKey = "<the client secret for your app>";          // The Client Secret from your App Registration
            string aadInstance = "https://login.microsoftonline.com/";
            string tenantID = "00000000-0000-0000-0000-000000000001";    // The GUID of your Azure Tenant ID. See the article above for details on finding this value.

            ClientCredential clientcred = new ClientCredential(clientId, appKey);
            AuthenticationContext authenticationContext = new AuthenticationContext(aadInstance + tenantID);
            AuthenticationResult authenticationResult = authenticationContext.AcquireToken(organizationUrl, clientcred);

            accessTokens[new Uri(organizationUrl).Host] = authenticationResult;

            return authenticationResult;
        }
    }
}

To access the D365 instance using the OrganizationWebProxyClient we can use the following code

                string organizationUrl = "https://myD365instance.crm6.dynamics.com";

                var hook = new AuthHook();

                var requestedToken = hook.GetAuthToken(new Uri(organizationUrl));

                using (var webProxyClient = new OrganizationWebProxyClient(new Uri($"{organizationUrl}/XRMServices/2011/Organization.svc/web"), false))
                {
                    webProxyClient.HeaderToken = requestedToken;

                    // Test with a basic WhoAmI request first
                    OrganizationRequest request1 = new OrganizationRequest()
                    {
                        RequestName = "WhoAmI"
                    };
                    OrganizationResponse response1 = webProxyClient.Execute(request1);

                    // We are also able to create an instance of the OrganizationService and run queries against it
                    IOrganizationService organizationService = webProxyClient as IOrganizationService;

                    Entity entity = organizationService.Retrieve("account", new Guid("92348762-0D32-E611-80EC-B38A27891203"), new Microsoft.Xrm.Sdk.Query.ColumnSet("name", "preferredcontactmethodcode"));

                }

Using the Microsoft.Xrm.Tooling.Connector we require a slightly different approach when using the authenticated Application User. It still uses the AuthHook class but through the AuthOverrideHook property. This uses the same AuthHook object we created earlier for the OrganizationWebProxyClient.

                // Register the hook with the CrmServiceClient
                CrmServiceClient.AuthOverrideHook = hook;

                // Create a new instance of CrmServiceClient, pass your organization url and make sure useUniqueInstance = true!
                var client = new CrmServiceClient(new Uri(organizationUrl), useUniqueInstance: true);

                // Test with a basic WhoAmI request first
                OrganizationRequest request2 = new OrganizationRequest()
                {
                    RequestName = "WhoAmI"
                };
                OrganizationResponse response2 = client.Execute(request2);

                Entity entity1 = client.Retrieve("account", new Guid("92348762-0D32-E611-80EC-B38A27891203"), new Microsoft.Xrm.Sdk.Query.ColumnSet("name", "preferredcontactmethodcode"));

SharePoint Online List View Thresholds

Running CAML queries against List Views with a large number of items (> 5000) can fail with the following error.

The attempted operation is prohibited because it exceeds the list view threshold enforced by the administrator.

To overcome this problem we have to ensure our query meets these requirements when running against SharePoint Online.

  • Run the query in a loop a number of times using the ListItemCollectionPosition to keep track of the looping process.
  • Ensure that the columns specified in any Query Where clause and the OrderBy are already indexed on the list view. If you include columns in either of these parts of the query that are not indexed then you will still get the same error.
  • Ensure the Query AllowIncrementalResults is set to true
  • If you want to restrict the Query to a particular folder in the List View set the FolderServerRelativeUrl property.

I used PowerShell to test out my CAML query.

#Load SharePoint CSOM Assemblies
$folder = "C:\Nuget\packages\Microsoft.SharePointOnline.CSOM.16.1.8412.1200\lib\net45"
Add-Type -Path "$folder\Microsoft.SharePoint.Client.dll"
Add-Type -Path "$folder\Microsoft.SharePoint.Client.Runtime.dll"
   
#Variables for Processing
$SiteUrl = "https://mysharepoint.sharepoint.com/sites/mysite"
$ListName="MyList"
 
$UserName="me@mysharepoint.onmicrosoft.com"
$Password ="nottellingyou"
  
#Setup Credentials to connect
$Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName,(ConvertTo-SecureString $Password -AsPlainText -Force))
  
#Set up the context
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl) 
$Context.Credentials = $credentials

try {
   
    #Get the List
    $List = $Context.web.Lists.GetByTitle($ListName)

    $Context.Load($List)
    $Context.ExecuteQuery()

    $position = $null

    $allItems = @()

    Do {
        $Query = New-Object Microsoft.SharePoint.Client.CamlQuery;
        # Define the starting position
        $Query.ListItemCollectionPosition = $position

        $Query.ViewXml = "<View Scope='RecursiveAll'>  
                            <Query> 
                                <Where>
                                    <And>
                                        <And>
                                            <And>
                                                <Eq><FieldRef Name='ReportName' /><Value Type='Text'>Monthly Report</Value></Eq>
                                                <Eq><FieldRef Name='Category' /><Value Type='TaxonomyFieldType'>Statement</Value></Eq>
                                            </And>
                                            <Eq><FieldRef Name='ProductLine' /><Value Type='TaxonomyFieldType'>Credits</Value></Eq>
                                        </And>
                                        <Eq><FieldRef Name='ContentType' /><Value Type='Computed'>Report</Value></Eq>
                                    </And>
                                </Where>
                            </Query> 
                        </View>"

        # Define a folder for the starting point of the recursive search
        $Query.FolderServerRelativeUrl = "/sites/MySite/MyList/Folder1"
        $Query.AllowIncrementalResults = $true

        $ListItems = $List.GetItems($Query) 
        $Context.Load($ListItems)

        $Context.ExecuteQuery()    
    
        # Increment the next position for the search
        $position = $ListItems.ListItemCollectionPosition
    
        $allItems += $ListItems
    }
    Until($position -eq $null)
}
Catch {
    Write-Error $_.Exception.Message
    Break
} 


write-host "Total Number of List Items found:"$allItems.Count
 
#Loop through each item
$allItems | ForEach-Object {
    #Get the Title field value

    $Context.Load( $_.File)
    $Context.Load( $_.File.ListItemAllFields)
    $Context.ExecuteQuery()
    write-host $_.File.Name
}  

D365 – Web API – Custom Actions & Entity Reference Inputs

Using v9.0 of the D365 Web Api we are able to trigger Actions. Using a standard URL structure we can call those Actions from client-side script, for example – POST /api/data/v9.0/incidents(00000000-0000-0000-0000-000000000000)/Microsoft.Dynamics.CRM.new_CustomIncidentAction.

When these Actions have Input parameters they can be added to the body of the request but knowing how to structure the body can be a bit of a challenge. For numeric, character and boolean values it’s just a case of constructing the relevant JSON object. For EntityReference’s there is a specific structure which will require you to find some details in the ODataV4Metadata.xml file. This file can be downloaded by going to your D365 instance under Settings | Customizations | Developer Resources and clicking on the Download OData Metadata link under the Instance Web API section. Search the file for the name of your Action you want to call and you should get something similar to this…

<Action Name="new_CustomIncidentAction" IsBound="true">
<Parameter Name="entity" Type="mscrm.incident" Nullable="false" />
<Parameter Name="TeamId" Type="mscrm.team" Nullable="false" />
</Action>

The first is the GUID of the incident we are running the Action against. The second is the one we are interested in as this is our input EntityReference.

To construct the JSON body of our request we need three pieces of information

  1. Parameter name – this we can get from the XML (above). The name is case sensitive.
  2. The OData type – this is also specified in the XML, but needs to be defined in a slightly different format than is defined in the XML.
  3. The EntityReference Id – the GUID value for the entity

The JSON object for our EntityReference input parameter can be specified like this.

{ "TeamId": {
	"@odata.type": "Microsoft.Dynamics.CRM.team",
	"teamid": "2ab7a6d5-9a36-e611-80e7-c4346bc48ef4" 
	} 
}