Thursday, November 28, 2019

Fetching Azure Blob File Attachments and listing in Dynamic 365 CRM


This blogpost is in a continuation of my previous blogpost where I had explained how to move attachment linked under Notes Section of Dynamic CRM to Azure Blob storage.


For a quick Demo, please refer to below video.




WebResource Output
Files are in hyperlink format and when clicked made Azure called to get the content of the file.

Custom Attachment Entity storing Attachment details when migrated to Azure Blob


WebResource Code content to display Notes details and generate a download link from Azure Blob using SAS_TOKEN authentication mechanism.

HTML Content

<html>
<body onload="resourceOnload()">
    <div id="div_attachmentContent"></div>
    <table id="tbl_listAttachments" style="border:none">
    </table>

</body>
</html>

JavaScript Content

<script>
var eks_Xrm = null;
var clientCrmUrl = null;

function initializeXrm() {
    if (typeof Xrm != 'undefined')
        eks_Xrm = Xrm;
    else if (typeof parent.Xrm != 'undefined')
        eks_Xrm = parent.Xrm;

    clientrmUrl = eks_Xrm.Page.context.getClientUrl();
}

function resourceOnload() {
    debugger;
    initializeXrm();
    var entityId = eks_Xrm.Page.data.entity.getId().replace('{', '').replace('}', '');
    var entitySetName = eks_Xrm.Page.data.entity.getEntitySetName();
    getAttachmentDetails(entitySetName, entityId);
}

function getAttachmentDetails(regardingEntityType, regardingEntityId) {
    var entity = "new_customattachments?";
    var selectClause = "$select=new_blobuniqueid,new_customattachmentid,new_filesize,new_filetype,new_fileurl,new_name,new_notedescrption,new_notetitle,new_regardingentityid,new_regardingentitytype";
    var filterClause = "&$filter=new_regardingentitytype eq '" + regardingEntityType + "' and  new_regardingentityid eq '" + regardingEntityId + "'";
    var orderbyClause = "&$orderby=createdon asc";

    var req = new XMLHttpRequest();
    req.open("GET", clientrmUrl + "/api/data/v9.1/" + entity + selectClause + filterClause + orderbyClause, false);
    req.setRequestHeader("OData-MaxVersion", "4.0");
    req.setRequestHeader("OData-Version", "4.0");
    req.setRequestHeader("Accept", "application/json");
    req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
    req.setRequestHeader("Prefer", "odata.include-annotations=\"*\"");
    req.onreadystatechange = function () {
        if (this.readyState === 4) {
            req.onreadystatechange = null;
            if (this.status === 200) {
                var results = JSON.parse(this.response);
                bindDataToGrid(results);
            } else {
                Xrm.Utility.alertDialog(this.statusText);
            }
        }
    };
    req.send();
}

function bindDataToGrid(results) {
    debugger;
    var divOfAttachments = document.getElementById("div_attachmentContent");
    var tableOfAttachments = document.getElementById("tbl_listAttachments");

    if (results.value.length == 0) {
        divOfAttachments.innerHTML = "No Record Found";
        return;
    }

    for (var i = 0; i < results.value.length; i++) {
        var new_blobuniqueid = results.value[i]["new_blobuniqueid"];
        var new_customattachmentid = results.value[i]["new_customattachmentid"];
        var new_filesize = results.value[i]["new_filesize"];
        var new_filetype = results.value[i]["new_filetype"];
        var new_fileurl = results.value[i]["new_fileurl"];
        var new_name = results.value[i]["new_name"];
        var new_notedescrption = results.value[i]["new_notedescrption"];
        var new_notetitle = results.value[i]["new_notetitle"];
        var new_regardingentityid = results.value[i]["new_regardingentityid"];
        var new_regardingentitytype = results.value[i]["new_regardingentitytype"];

        var row1 = tableOfAttachments.insertRow(0);
        var row2 = tableOfAttachments.insertRow(1);
        var row3 = tableOfAttachments.insertRow(2);

        var title_td = row1.insertCell(0);
        title_td.colSpan = "2";
        title_td.style = "font:bold 12px Segoe UI; border:none";

        var description_td = row2.insertCell(0);
        description_td.colSpan = "2";
        description_td.style = "font:normal 11px Segoe UI; border:none";

        var file_td = row3.insertCell(0);
        file_td.style = "font:normal 11px Segoe UI; border-bottom:0.5px solid grey";

        title_td.innerHTML = new_notetitle;
        description_td.innerHTML = new_notedescrption;
        file_td.innerHTML = getUrlOfAttachmentFileFromBlob(new_fileurl,new_name);
    }
    divOfAttachments.appendChild(tableOfAttachments);
}

function getUrlOfAttachmentFileFromBlob(new_fileurl,new_fileName) {
    var fileHyperlink = '';
    var blobUri = 'https://' + 'Storage_Account_Name' + '.blob.core.windows.net';
    var containerName = 'vjtest';
    var sas_token = 'SAS_TOKEN';
    var blobService = AzureStorage.Blob.createBlobServiceWithSas(blobUri, sas_token).withFilter(new AzureStorage.Blob.ExponentialRetryPolicyFilter());

    var downloadLink = blobService.getUrl('vjtest', new_fileurl.replace('/'+containerName+'/',''), sas_token);

    if (downloadLink != null)
        fileHyperlink = ' + downloadLink + '" >' + new_fileName + '
'
    return fileHyperlink;
}
</script>

Note: You need to change highlighted parameter accordingly to get connected to Azure Blob. You may also think of storing them in custom configuration entity for security reasons.

Azure Blob Library Reference

We need to make Azure blob library as a CRM Resource and use it in a reference in our web-resource.

<script src="https://Vipinsandbox.crm4.dynamics.com//WebResources/new_/script/azure-storage.blob.js"></script>


Thanks
Vipin Jaiswal
vipinjaiswal12@gmail.com

Tuesday, November 26, 2019

Moving Attachments of Dynamic 365 CRM to Azure Blob - Power Automate


Many Dynamic CRM customers might have experience on high storage cost which is due to continuous upload of attachments in Note sections.

With an advancement in Power Automate one can easily move attachments to Azure Blob and manage them well within Dynamic CRM. In this Blog, I would be explaining the detail approach and concept of how smartly we can manage notes and attachments.

For a compete Demo, please refer to below video.



Before we dive deeper into technical stuff, here are some important fact
  • SharePoint storage cost is very small about $0.20 per GB/Month compared to CRM’s $9.99/GB/Month. So, CRM space is around 50 times costlier than SharePoint space.
  • Azure blob costs about $0.03/GB/month- which is 6.5x less than SharePoint storage cost and 300x less than CRM storage cost.

Azure Blob storage: Massively scalable object storage for unstructured data. Store any type of unstructured data—images, videos, audio, documents and more.


Here are the Technical Steps.

Create custom entity (new_CustomAttachment) for storing migrated data about Notes and Attachments.

Here is a screen shot of a custom entity, all fields are single line of text, except Note description which is Multiple line of text.



Handling Azure Storage
  1. Create a Resource Group – CRM_NotesAndAttachment
  2. Create a Root Folder within a Container – CRM_OrganizationName

For Azure Blob Connection in Power automate, you will require Account Name and Key Please refer here how-to setup Azure Blob Connection here.


MS Flow to move attachment to Azure Blob

Step 1) Select the Triggering Action for When an Annotation (Notes) is created and select Notes as an Entity



Step 2) Add a condition branch to check if Notes have Attachment to it.




Step 3) For True condition, we have three further steps.


Step 3.1) Create Blob


Step 3.2) Create Custom CRM Record for Blob Storage


Step 3.3) Delete annotation record



Thanks.

vipinjaiswal12@gmail.com

Friday, November 22, 2019

Entity isn't enabled for change tracking.


I got this error when I was trying to delete a record and there was a flow registered on Delete of a record.

Flow operation was requesting entity tracking data and entity was not configured for change tracking, so we need to enable change tracking for an entity.

1. Navigate to Settings> CustomizationsCustomize the System and select the entity for which you want to track changes.
2. On the entity’s General tab, scroll down to Data Services, and select the Change Tracking option box.


There is no plugin in XrmToolBox yet to update change tracking for all entities, so here is C# code.

UpdateEntityRequest updateCustomEntityRequest = new UpdateEntityRequest
{
     Entity = myCustomEntity,
     ChangeTrackingEnabled = true //or false here
};
_serviceProxy.Execute(updateCustomEntityRequest);

Tuesday, November 19, 2019

Paging FetchXml Queries in Dynamic CRM


When paging FetchXML queries, things are a little bit different in comparison to Rest API:
  • Item count per page is specified by setting the count attribute in the fetch node.
  • The page number is specified in the page attribute of the fetch node. To retrieve the next page, update the page number to the next page.
  • Need to evaluate @Microsoft.Dynamics.CRM.fetchxmlpagingcookie attribute when verifying if next set of records are available to be fetched



CODE EXAMPLE


var serverUrl = Xrm.Page.context.getClientUrl();

function getallRecords(OriginalQueryUrl)
{
// we return an object with a similar structure
var allRecords = new Object();
allRecords.results = new Array();

// we loop until we have an url to query
var queryUrl = OriginalQueryUrl;

// Initially defining the PageNumber
var pageNumber = 1;

while (queryUrl != null)
{
    // we build the request
    var httpRequest = new XMLHttpRequest();
    httpRequest.open("GET", queryUrl, false); // false = synchronous request
    httpRequest.setRequestHeader("Accept", "application/json");
    httpRequest.setRequestHeader("OData-MaxVersion", "4.0");
    httpRequest.setRequestHeader("OData-Version", "4.0");
    httpRequest.setRequestHeader("Content-Type", "application/json; charset=utf-8");
    httpRequest.setRequestHeader("Prefer", "odata.include-annotations=\"*\"");
    httpRequest.send();

    if (httpRequest.status === 200)
    {
        var parsedResults = JSON.parse(httpRequest.responseText);

        if (parsedResults != null && parsedResults.value != null)
        {
            // we add the results to our object
            for (var i = 0; i < parsedResults.value.length; i++) {
                allRecords.results.push(parsedResults.value[i]);
            }
               
            // check if there are more records and set the new url, otherwise we set to null the url
            if (parsedResults["@Microsoft.Dynamics.CRM.fetchxmlpagingcookie"] != null 
                && 
                parsedResults["@Microsoft.Dynamics.CRM.fetchxmlpagingcookie"] != 'undefined')
            {
             pageNumber++;
            // Updating Query with page number to fetch Next set of records.
queryUrl = queryUrl.replace("page='" + (pageNumber-1) +"'","page='"+ pageNumber.toString()+"'")                   
            }
            else
            {
                queryUrl = null;
            }
        }
    }
    else
    {
        // if the request has errors we stop and return a null result
        queryUrl = null;
        allRecords = null;
    }
}
return allRecords;
}