Export and Import Solution from one environment to another using Azure DevOps CI/CD Pipeline.

Hello Everyone,

DevOps is very vast, we can perform n number of automation but today i will be focusing on import and export Dynamics 365 solution using Azure DevOps CI/CD Pipeline.This blog article is going to be bit longer because we have a lots of step, so guys bear with me and read it thoroughly you will get answer of your questions. We have lots of trigger where we can call our build and release, for your reference https://docs.microsoft.com/en-us/azure/devops/pipelines/build/triggers?view=azure-devops. Also you can call your Pipeline events from Power Automate. I would not go deep into it.

Prerequisite:

1.Azure Pipeline subscription.

2.Dynamics 365 Customer Engagement Online.

In previous blog ,I have explained you how to setup repository, next we are going to use that repository for exporting our solution.

  1. Create a folder inside your repository where you can keep solution after the solution is exported.

Give a proper name , I am using “Dynamics365Solution”. You can not create empty folder so i just created an text file with the same name.

Once file is created click on commit changes for saving the file in DevOps repository.

Once the folder added we are going create Variable groups for storing connection string(which will be used while make connection do Dynamics 365 CE Online for export and import of the solution).It is a good practice for storing connection string inside the Variable group. Go to Library then click on +Variable group.

Now give variable group a name. I will assume you got a Dev and a UAT environment. The package from the build automation blog will be deployed to these environments.So, we will create two connection string for the Dev and UAT environments.

1.Connection String-AuthType=$(AuthType);Username=$(Username);Password=$(Password);Url=$(Url)

  • URL- Enter your instance URL.
  • Username – Enter the username of your instance
  • Password – Enter the password of your instance
  • Authtype – Office365

Below is the sample connection string details for your reference

AuthType=Office365;Username=jsmith@contoso.onmicrosoft.com; Password=passcode;Url=https://contoso.crm.dynamics.com

After that we need to create a build pipeline. Navigate to Pipeline->Create Pipeline.

We are using Azure Repos Git as a source , after that choose your Team project and repository where you want to perform your pipeline operation. Specify your branch for your build , using master for the demonstration.

After that choose your template, but currently we do not have template for Dynamics 365 so we are going to use Empty job.

Microsoft provided job Agents which give you more control to install dependent software needed for your builds and deployment. For exporting solution we need to add three task.

First, search for the Power DevOps Tool Installer(it will copy all the tools to agent, optimize the process) by Wael Hamze.

Second, we are going to add Export Solution in similar way we added installer. Once it is added we need to specify three parameter. We already created in variable groups so lets utilize those connection string.

We need to link Variable group within pipeline, navigate to Variables click on link Variable group.

Choose your variable group, we are using “Dev Credential” here. Click on link. Because first we are going to export solution from our Dev environment.

Now First pass the Connection string. Second, the Name of your solution which you want to export, My solution Name is “DevopsExport”. Third you need to specify the output path.Click on … for browse and select the path which we created a folder inside the repository on start of our blog which was “Dynamics365Solution” .

You have lots of option in Additional setting of the Export Solution. Which you can add based on your requirement.

Once we exported the solution we need to publish it as an artifact. Add third task “Publish Artifact”. You can give a name to Artifact and need to specify the path same as your exported folder. After that click on Save & Queue.

it will open a window where you can give comments and other settings, Click on Save and Run.

It will take some seconds or minute to export and publish artifact based on your solution size.As you can see in below image Agent job run successfully and it produced 1 artifact. you can click on that link and navigate to the exported solution.

Now we need create a release pipeline for importing the solution in UAT environment. Navigate to the Release click on New pipeline.

Click on Empty job template.

Here we can add multiple stages. But i am using only one stage for this demonstration which is UAT. After giving stage name close it from X on right corner.

Now we need to add Artifact which we produced in the last build pipeline. Choose Project, Source(from Build Pipeline which is SolutionMove-CI), Build Version(using the latest, you can have multiple build, but here you need to select which build you want use for the release),Source Alias(by default it will take based on source, renamed it as Dev)

Again link your UAT credential with your pipeline using Variable group.

After linking,Now we need to add Task(select your stage where you want to add task if you have multiple stages it will show you when you will click on Task button), so i have on one stage which is UAT so just click on it.

Add “Power DevOps Tool Installer” and “Import Solution“. Specify the connection string and solution file location(browse to artifact folder where the solution is exported and select the solution Zip File). Below you can see other operation which you can perform when import will take place. Just click on save. After that Create release button will not be read only.you can click on it.

It will open window which is shown in below image. Select the stage and click on create.

Once release is created , we need to deploy it. Navigate to the +Deploy button, we can deploy current stage as well as multiple stage at a time. It will open one more window where you can give your comment and click on Deploy button.

It will take few second to deploy your solution into UAT environment, on successful deployment it will show you a Succeeded status.

That’s it. Now you can go to the UAT environment and check your solution.

Hope you all able to understand it. if you have any question feel free to reach out to me. Ping me in comment box if you have any query.

Sending Email to Customer With SSRS PDF Attachment Using JavaScript Dynamics 365 CRM(Online)

Hello everyone ,

I got a task to send an email to the customer(contact), in which I have to send a quote to the customer as a pdf attachment. But every quoted line contains lots of images as note attachment those also should come in pdf and after creating the Report, attach to an email and then sent to the customer. All I have to do dynamic on click of a button. So I am sharing my learning how I did it.

I am creating a blog on how to send an email attachment to the customer. Hope it will you guys.

Part 1:

Creating Email activity with attachment(SSRS pdf report). Also adding the user Signature with the email.

var base64 = null;

function form_onsave(executionContext) {
    debugger;
    var formContext = executionContext.getFormContext();
    if (formContext.getAttribute("ccc_senttocustomer") !== null && formContext.getAttribute("ccc_senttocustomer") !== undefined) {
        var senttocustomer = formContext.getAttribute("ccc_senttocustomer").getValue();
        if (senttocustomer === true) {
            createAttachment();
        }
    }
}
function getReportingSession() {
    var reportName = "Estimate.rdl"; //set this to the report you are trying to download
    //var reportGuid = "170faa66-19ad-e811-a96f-000d3af43d1a"; //set this to the guid of the report you are trying to download 
    var reportGuid = "75cdd688-8bbd-e811-a971-000d3af42a5a"; //set this to the guid of the report you are trying to download 
    var rptPathString = ""; //set this to the CRMF_Filtered parameter            
    var selectedIds = Xrm.Page.data.entity.getId();
    var pth = Xrm.Page.context.getClientUrl() + "/CRMReports/rsviewer/reportviewer.aspx";
    var retrieveEntityReq = new XMLHttpRequest();

    var strParameterXML = "<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'><entity name='quote'><all-attributes /><filter type='and'><condition attribute='quoteid' operator='eq' value='" + selectedIds + "' /> </filter></entity></fetch>";

    retrieveEntityReq.open("POST", pth, false);

    retrieveEntityReq.setRequestHeader("Accept", "*/*");

    retrieveEntityReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

    retrieveEntityReq.send("id=%7B" + reportGuid + "%7D&uniquename=" + Xrm.Page.context.getOrgUniqueName() + "&iscustomreport=true&reportnameonsrs=&reportName=" + reportName + "&isScheduledReport=false&p:CRM_quote=" + strParameterXML);
    var x = retrieveEntityReq.responseText.lastIndexOf("ReportSession=");
    var y = retrieveEntityReq.responseText.lastIndexOf("ControlID=");
    // alert("x" + x + "y" + y);
    var ret = new Array();

    ret[0] = retrieveEntityReq.responseText.substr(x + 14, 24);
    ret[1] = retrieveEntityReq.responseText.substr(x + 10, 32);
    return ret;
}

function createEntity(ent, entName, upd) {
    var jsonEntity = JSON.stringify(ent);
    var createEntityReq = new XMLHttpRequest();
    var ODataPath = Xrm.Page.context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc";
    createEntityReq.open("POST", ODataPath + "/" + entName + "Set" + upd, false);
    createEntityReq.setRequestHeader("Accept", "application/json");
    createEntityReq.setRequestHeader("Content-Type", "application/json; charset=utf-8");
    createEntityReq.send(jsonEntity);
    var newEntity = JSON.parse(createEntityReq.responseText).d;

    return newEntity;
}

function createAttachment() {
    var params = getReportingSession();

    if (msieversion() >= 1) {
        encodePdf_IEOnly(params);
    } else {
        encodePdf(params);
    }
}

var StringMaker = function () {
    this.parts = [];
    this.length = 0;
    this.append = function (s) {
        this.parts.push(s);
        this.length += s.length;
    }
    this.prepend = function (s) {
        this.parts.unshift(s);
        this.length += s.length;
    }
    this.toString = function () {
        return this.parts.join('');
    }
}

var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

function encode64(input) {
    var output = new StringMaker();
    var chr1, chr2, chr3;
    var enc1, enc2, enc3, enc4;
    var i = 0;

    while (i < input.length) {
        chr1 = input[i++];
        chr2 = input[i++];
        chr3 = input[i++];

        enc1 = chr1 >> 2;
        enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
        enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
        enc4 = chr3 & 63;

        if (isNaN(chr2)) {
            enc3 = enc4 = 64;
        } else if (isNaN(chr3)) {
            enc4 = 64;
        }

        output.append(keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4));
    }

    return output.toString();
}

function encodePdf_IEOnly(params) {
    var bdy = new Array();
    var retrieveEntityReq = new XMLHttpRequest();

    var pth = Xrm.Page.context.getClientUrl() + "/Reserved.ReportViewerWebControl.axd?ReportSession=" + params[0] + "&Culture=1033&CultureOverrides=True&UICulture=1033&UICultureOverrides=True&ReportStack=1&ControlID=" + params[1] + "&OpType=Export&FileName=Public&ContentDisposition=OnlyHtmlInline&Format=PDF";
    retrieveEntityReq.open("GET", pth, false);
    retrieveEntityReq.setRequestHeader("Accept", "*/*");

    retrieveEntityReq.send();
    bdy = new VBArray(retrieveEntityReq.responseBody).toArray(); // minimum IE9 required

    createNotesAttachment(encode64(bdy));
}

function encodePdf(params) {
    var xhr = new XMLHttpRequest();
    var pth = Xrm.Page.context.getClientUrl() + "/Reserved.ReportViewerWebControl.axd?ReportSession=" + params[0] + "&Culture=1033&CultureOverrides=True&UICulture=1033&UICultureOverrides=True&ReportStack=1&ControlID=" + params[1] + "&OpType=Export&FileName=Public&ContentDisposition=OnlyHtmlInline&Format=PDF";
    xhr.open('GET', pth, true);
    xhr.responseType = 'arraybuffer';

    xhr.onload = function (e) {
        if (this.status == 200) {
            var uInt8Array = new Uint8Array(this.response);
            base64 = encode64(uInt8Array);
            CreateEmail(base64);
            createNotesAttachment(base64);
        }
    };
    xhr.send();
}

function msieversion() {

    var ua = window.navigator.userAgent;
    var msie = ua.indexOf("MSIE ");

    if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./))      // If Internet Explorer, return version number
        return parseInt(ua.substring(msie + 5, ua.indexOf(".", msie)));
    else                 // If another browser, return 0
        return 0;
}

function createNotesAttachment(base64data) {

    var propertyName;
    var propertyAddress;
    var propertyCity;
    var estimateNumber;
    if (Xrm.Page.getAttribute("name") !== null && Xrm.Page.getAttribute("name") !== undefined) {
        estimateNumber = Xrm.Page.getAttribute("name").getValue();
        //alert(estimateNumber);
    }
    if (Xrm.Page.getAttribute("ccc_propertyid") !== null && Xrm.Page.getAttribute("ccc_propertyid") !== undefined) {
        var propertyNameRef = Xrm.Page.getAttribute("ccc_propertyid");
        if (propertyNameRef != null && propertyNameRef != undefined) {
            propertyId = propertyNameRef.getValue()[0].id.slice(1, -1);
            propertyAddress = propertyNameRef.getValue()[0].name;
            var object = getPropertyAddress(propertyId);
            propertyName = object[1];
            propertyCity = object[2];
        }
    }

    var post = Object();
    post.DocumentBody = base64data;
    post.Subject = estimateNumber+propertyAddress;
    post.FileName = estimateNumber + "-" + propertyName + "-" + propertyAddress + "-" + propertyCity + ".pdf";
    post.MimeType = "application/pdf";
    post.ObjectId = Object();
    post.ObjectId.LogicalName = Xrm.Page.data.entity.getEntityName();
    post.ObjectId.Id = Xrm.Page.data.entity.getId();
    createEntity(post, "Annotation", "");
}

function CreateEmail(base64) {

    var recordURL;
    var propertyName;
    var propertyAddress;
    var propertyCity;
    var estimateNumber;
    var serverURL = Xrm.Page.context.getClientUrl();
    var email = {};
    var qid = Xrm.Page.data.entity.getId().replace(/[{}]/g, "");
    var OwnerLookup = Xrm.Page.getAttribute("ownerid").getValue();
    var OwnerGuid = OwnerLookup[0].id;
    OwnerGuid = OwnerGuid.replace(/[{}]/g, "");
    var ContactLookUp = Xrm.Page.getAttribute("ccc_contact").getValue();
    var ContactId = ContactLookUp[0].id.replace(/[{}]/g, "");
    var contactTypeName = ContactLookUp[0].typename;
    var contactName = ContactLookUp[0].name;
    var signature = getSignature(OwnerGuid);
    if (Xrm.Page.getAttribute("ccc_recordurl") !== null && Xrm.Page.getAttribute("ccc_recordurl") !== undefined) {
        recordURL = Xrm.Page.getAttribute("ccc_recordurl").getValue();
        //alert(estimateNumber);
    }
    if (Xrm.Page.getAttribute("name") !== null && Xrm.Page.getAttribute("name") !== undefined) {
        estimateNumber = Xrm.Page.getAttribute("name").getValue();
        //alert(estimateNumber);
    }
    if (Xrm.Page.getAttribute("ccc_propertyid") !== null && Xrm.Page.getAttribute("ccc_propertyid") !== undefined) {
        var propertyNameRef = Xrm.Page.getAttribute("ccc_propertyid");
        if (propertyNameRef != null && propertyNameRef != undefined) {
            propertyId = propertyNameRef.getValue()[0].id.slice(1, -1);
            propertyAddress = propertyNameRef.getValue()[0].name;
            var object = getPropertyAddress(propertyId);
            propertyName = object[1];
            propertyCity = object[2];
        }
    }

    if (signature == null || signature == undefined) {
        signature = "";
    }
    var string = "Hello " + contactName + " ,</br>Here is the estimate you requested for this location:</br>Estimate # " + estimateNumber + " </br>" + propertyName + "  " + propertyAddress + ",  " + propertyCity + "</br></br>A PDF copy is attached to this email, as well.</br>If you have any questions about this estimate, please reply back to this email or call me.Thank you for considering us!</br></br>" + signature;


    email["subject"] = estimateNumber +"-"+ propertyAddress;
    email["description"] = string;
    email["regardingobjectid_quote@odata.bind"] = "/quotes(" + qid + ")";
    //activityparty collection
    var activityparties = [];
    //from party
    var from = {};
    from["partyid_systemuser@odata.bind"] = "/systemusers(" + OwnerGuid + ")";
    from["participationtypemask"] = 1;
    //to party
    var to = {};
    to["partyid_contact@odata.bind"] = "/contacts(" + ContactId + ")";
    to["participationtypemask"] = 2;

    activityparties.push(to);
    activityparties.push(from);

    //set to and from to email
    email["email_activity_parties"] = activityparties;

    var req = new XMLHttpRequest();
    req.open("POST", Xrm.Page.context.getClientUrl() + "/api/data/v8.2/emails", 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.onreadystatechange = function () {
        if (this.readyState === 4) {
            req.onreadystatechange = null;
            if (this.status === 204) {
                var uri = this.getResponseHeader("OData-EntityId");
                var regExp = /\(([^)]+)\)/;
                var matches = regExp.exec(uri);
                var newEntityId = matches[1];
                createEmailAttachment(newEntityId, base64);
            } else {
                Xrm.Utility.alertDialog(this.statusText);
            }
        }
    };
    req.send(JSON.stringify(email));
    ///////////////////
}
function createEmailAttachment(emailUri, base64) {
    var activityId = emailUri.replace(/[{}]/g, "");
    var propertyId;
    var propertyName;
    var propertyAddress;
    var propertyCity;
    var estimateNumber;
    if (Xrm.Page.getAttribute("name") !== null && Xrm.Page.getAttribute("name") !== undefined) {
        estimateNumber = Xrm.Page.getAttribute("name").getValue();
    }
    if (Xrm.Page.getAttribute("ccc_propertyid") !== null && Xrm.Page.getAttribute("ccc_propertyid") !== undefined) {
        var propertyNameRef = Xrm.Page.getAttribute("ccc_propertyid");
        if (propertyNameRef != null && propertyNameRef != undefined) {
            propertyId = propertyNameRef.getValue()[0].id.slice(1, -1);
            propertyAddress = propertyNameRef.getValue()[0].name;
            var object = getPropertyAddress(propertyId);
            propertyName = object[1];
            propertyCity = object[2];

        }
    }


    var activityType = "email"; //or any other entity type
    var entity = {};
    entity["objectid_activitypointer@odata.bind"] = "/activitypointers(" + activityId + ")";
    //entity.body = "ZGZnZA=="; //your file encoded with Base64
    entity.body = base64; //your file encoded with Base64
    entity.filename = estimateNumber + "-" + propertyName + "-" + propertyAddress + "-" + propertyCity + ".pdf";
    entity.subject = estimateNumber + "-" + propertyName;
    entity.objecttypecode = activityType;
    var req = new XMLHttpRequest();
    req.open("POST", Xrm.Page.context.getClientUrl() + "/api/data/v8.2/activitymimeattachments", 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.onreadystatechange = function () {
        if (this.readyState === 4) {
            req.onreadystatechange = null;
            if (this.status === 204) {
                var uri = this.getResponseHeader("OData-EntityId");
                var regExp = /\(([^)]+)\)/;
                var matches = regExp.exec(uri);
                var newEntityId = matches[1];
                //alert("attachement created "+newEntityId);
            } else {
                Xrm.Utility.alertDialog(this.statusText);
            }
        }
    };
    req.send(JSON.stringify(entity));
}
function getPropertyAddress(pid) {
    debugger;
    var ccc_name;
    var ccc_property1;
    var ccc_propertycity;
    var req = new XMLHttpRequest();
    req.open("GET", Xrm.Page.context.getClientUrl() + "/api/data/v8.2/ccc_properties(" + pid + ")?$select=ccc_name,ccc_property1,ccc_propertycity", 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 result = JSON.parse(this.response);
                ccc_name = result["ccc_name"];
                ccc_property1 = result["ccc_property1"];
                ccc_propertycity = result["ccc_propertycity"];
            } else {
                Xrm.Utility.alertDialog(this.statusText);
            }
        }
    };
    req.send();
    return [ccc_name, ccc_property1, ccc_propertycity];
}
function getSignature(OwnerGuid) {
    debugger;
    var sig;
    var req = new XMLHttpRequest();
    req.open("GET", Xrm.Page.context.getClientUrl() + "/api/data/v8.2/emailsignatures?$select=presentationxml&$filter=_ownerid_value eq " + OwnerGuid, 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);
                for (var i = 0; i < results.value.length; i++) {
                    var presentationxml = results.value[i]["presentationxml"];
                    oXml = CreateXmlDocument(presentationxml);
                    sig = oXml.lastChild.lastElementChild.textContent;
                }
            } else {
                Xrm.Utility.alertDialog(this.statusText);
            }
        }
    };
    req.send();
    return sig;
}
function CreateXmlDocument(signatureXmlStr) {
    // Function to create Xml formate of return email template data
    var parseXml;

    if (window.DOMParser) {
        parseXml = function (xmlStr) {
            return (new window.DOMParser()).parseFromString(xmlStr, "text/xml");
        };
    }
    else if (typeof window.ActiveXObject != "undefined" && new window.ActiveXObject("Microsoft.XMLDOM")) {
        parseXml = function (xmlStr) {
            var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
            xmlDoc.async = "false";
            xmlDoc.loadXML(xmlStr);

            return xmlDoc;
        };
    }
    else {
        parseXml = function () { return null; }
    }

    var xml = parseXml(signatureXmlStr);
    if (xml) {
        return xml;
    }
}

Part 2: Sending the email to customer using plugin.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xrm.Sdk.Workflow;
using System.Activities;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk;
namespace ccc_SendEmail
{
    public class sendEmail : CodeActivity
    {
        [RequiredArgument]
        [Input("RegardingEmail")]
        [ReferenceTarget("email")]
        public InArgument<EntityReference> RegardingEmail { get; set; }

        protected override void Execute(CodeActivityContext executionContext)
        {
            //Create the tracing service

            ITracingService tracingService = executionContext.GetExtension<ITracingService>();

            //Create the context
            IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();
            IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
            IOrganizationService service = serviceFactory.CreateOrganizationService(null);
            EntityReference emailRefrence = RegardingEmail.Get<EntityReference>(executionContext);

            SendEmailRequest SendEmail = new SendEmailRequest();

            SendEmail.EmailId = emailRefrence.Id;

            SendEmail.TrackingToken = "";

            SendEmail.IssueSend = true;

            SendEmailResponse res = (SendEmailResponse)service.Execute(SendEmail);
            //throw new NotImplementedException();
        }
    }
}

If need any help please use comment section so I can help you more. Thank you.