Visualize CRM Solution Deployments across Environments

One of the things I love about Azure functions is that they are great for scenarios where you need to use a bit of code to do something useful but its not a real world production component, for example if it’s a utility to help the team work better.

In this case one of the problems I had was allowing the team to visualise CRM deployments across environments so it was easy to see what was deployed where. My plan to help solve this problem was to create an Azure Function which would execute a query on each CRM environment in a list to get a list of the solutions and their versions. The other great thing about functions is that I can also write this straight in the browser and also get the function to return html so its easy for the team to see.

Step 1

At this point assume we have created a function plan and in the function plan I want to add the CRM sdk. It needs a project.json file adding to the function which you can do via the browser so that you can reference some nuget packages to be downloaded.

Step 2

Next up I need to write the function to do the work. In the code snippet below you can see I have a user who has access to each environment and who can query the solution entity. I then list each of the environments and go through them querying what solutions are available.

Once I get to the end of the function I convert my object containing the data I have collected into an HTML table. I then return html from the function using the text/html media type.

#r "Newtonsoft.Json"

using System.Net;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Tooling.Connector;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http.Headers;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    log.Info("C# HTTP trigger function processed a request.");

    var connectionStringTemplate = "Url=${CRMUrl}; Username={username goes here}; Password={password goes here}; authtype=Office365";

    var environments = new Dictionary<string, string>();
    environments.Add("Production", "https://acme-prod.crm4.dynamics.com");
    environments.Add("Dev1", "https://acme-dev1.crm11.dynamics.com");
    environments.Add("Dev2", "https://acme-dev2.crm4.dynamics.com");
    environments.Add("SystemTest1", "https://acme-systest1.crm4.dynamics.com");
    environments.Add("SystemTest2", "https://bupa-gs-systest2.crm4.dynamics.com");
    environments.Add("UAT1", "https://acme-uat1.crm4.dynamics.com");
    environments.Add("UAT2", "https://acme-uat2.crm11.dynamics.com");
    environments.Add("Pre-Prod", "https://acme-preprod.crm11.dynamics.com");
    

    var solutionDictionary = new Dictionary<string, SolutionOverview>();

    foreach (var environment in environments)
    {
        var query = new QueryExpression("solution");
        query.ColumnSet.AllColumns = true;

        var connectionString = connectionStringTemplate.Replace("${CRMUrl}", environment.Value);
        var client = new CrmServiceClient(connectionString);
        //client.IsReady
        var response = client.RetrieveMultiple(query);
        foreach(var solutionEntity in response.Entities)
        {
            var solutionName = (string)solutionEntity.Attributes["friendlyname"];
            var solutionId = (string)solutionEntity.Attributes["uniquename"];
            var version = (string)solutionEntity.Attributes["version"];

            if(solutionDictionary.ContainsKey(solutionId))
            {
                var solutionItem = solutionDictionary[solutionId];
                solutionItem.EnvironmentVersions.Add(new EnvironmentVersion() { EnvironmentName = environment.Key, VersionNumber = version });
            }
            else
            {
                var solutionItem = new SolutionOverview();
                solutionItem.SolutionFriendlyName = solutionName;
                solutionItem.SolutionName = solutionId;
                solutionItem.EnvironmentVersions.Add(new EnvironmentVersion() { EnvironmentName = environment.Key, VersionNumber = version });
                solutionDictionary.Add(solutionId, solutionItem);
            }
        }
    }

    var htmlBuilder = new StringBuilder();
    htmlBuilder.Append("<table style=\"border: 1px solid black;\">");
    htmlBuilder.Append("<tr style=\"border: 1px solid black;\"><th style=\"border: 1px solid black;\">Solution</th><th style=\"border: 1px solid black;\">Environment Versions</th></tr>");
    foreach (var solution in solutionDictionary)
    {
        htmlBuilder.Append("<tr style=\"border: 1px solid black;\">");

        htmlBuilder.AppendFormat("<td style=\"border: 1px solid black;\">{0}</td>", solution.Value.SolutionFriendlyName);
        htmlBuilder.Append("<td style=\"border: 1px solid black;\">");
        foreach (var environment in solution.Value.EnvironmentVersions)
        {
            htmlBuilder.AppendFormat("{0}: {1}", environment.EnvironmentName, environment.VersionNumber);
            htmlBuilder.Append("</br>");
        }
        htmlBuilder.Append("</td>");
        htmlBuilder.Append("</tr>");
    }

    var html = htmlBuilder.ToString();
    

    var functionResponse = new HttpResponseMessage(HttpStatusCode.OK);
    functionResponse.Content = new StringContent(html);
    functionResponse.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");
    return functionResponse;
}


public class SolutionOverview
{
    public SolutionOverview()
    {
        EnvironmentVersions = new List<EnvironmentVersion>();
    }

    public string SolutionName { get; set; }

    public string SolutionFriendlyName { get; set; }
    
    public List<EnvironmentVersion> EnvironmentVersions { get; set; }
}

public class EnvironmentVersion
{
    public string EnvironmentName { get; set; }

    public string VersionNumber { get; set; }
}

I am now able to share around the url if I want for the function and people can get a list of the solutions and the versions deployed.

Step 3

Rather than share the function url, I thought it would be better to embed it into one of the tools we are using. First off I chose to embed the function result into Confluence which we use for most of our documentation. In confluence you can use the iFrame widget and it will call out to an external url. I used that and it means that everytime someone looks at that page in confluence it will refresh the table to show the latest list of solutions deployed in CRM. An example is below:

If you are a confluence user then this pattern could be a really interesting way for you to add some dynamic content to confluence by calling out to code hosted in Azure.

In addition to confluence we also use Microsoft Teams. It is also easy for me to embed the dashboard into a teams channel for our operations team. You can see in the below picture I have added a website widget to the teams channel which I just used the url for the function and it returns the html table which is displayed to the user to provide an easy way to see the information about solution versions that are deployed.

Conclusion

Im hoping this article is useful in 3 ways:

  • Shows how you can create a consolidated view of deployments of CRM solutions across your environments
  • Shows how you can use an Azure Function to add dynamic content to Confluence
  • Shows how you can use an Azure Function to add dynamic content to Microsoft Teams

You May Also Like

About the Author: michaelstephensonuk