Using XML Data Transform (XDT) to automatically configure app.config during Nuget Package Install

This should be fairly straight forward as mentioned on nuget.org’s site right? Well, not quite. I’ve spent some time reading through the blog posts and it’s not quite straightforward. Hopefully this post is the simplified version. In my case, the scenario is simply to add entries in the appSettings key node within the app.config file. Nuget.org’s site has the following docs:

Configuration File and Source Code Transformations

https://docs.nuget.org/create/configuration-file-and-source-code-transformations

How to use XDT in NuGet – Examples and Facts

http://blog.nuget.org/20130920/how-to-use-nugets-xdt-feature-examples-and-facts.html

The steps below will hopefully guide you through the initial steps to get your app.config (or web.config) files to be modified during and after installing your nuget packages. After which you can look at all different XDT transformation processes in the following doc:

Web.config Transformation Syntax for Web Project Deployment Using Visual Studio

https://msdn.microsoft.com/en-us/library/dd465326(v=vs.110).aspx

Step 1: Create both app.config.install.xdt and app.config.uninstall.xdt

From Nuget site: “Starting with NuGet 2.6, XML-Document-Transform (XDT) is supported to transform XML files inside a project. The XDT syntax can be utilized in the .install.xdt and .uninstall.xdt file(s) under the package’s Content folder, which will be applied during package installation and uninstallation time, respectively.”

The location of these files don’t quite matter. If these files are located in the same directory as where you have your assemblies for nuget package, even better. You’ll need to reference these 2 files as “content” folder locations in the .nuspec file. Nuspec file is the blue print for creating your nuget package.

app.config.install.xdt

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <appSettings xdt:Transform="InsertIfMissing">
    </appSettings>
  <appSettings>
    <add key="Key1" xdt:Transform="Remove" xdt:Locator="Match(key)" />
    <add key="Key1" value="Value1" xdt:Transform="Insert"/>
    <add key="Key2" xdt:Transform="Remove" xdt:Locator="Match(key)"/>
    <add key="Key2" value="Value2" xdt:Transform="Insert" />
  </appSettings>
</configuration>

Let’s break this down. There are 2 appSettings node in this xml file. One to check if the appSettings node exist (InsertIfMissing) and the 2nd, if it does exist, it will remove the key value pair matching the keyword and then add it again. Why do this 2 step process? This is to ensure that you will only have one entry per key. However, you could probably get away using InsertIfMissing as well.

app.config.uninstall.xdt

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <appSettings xdt:Transform="InsertIfMissing">
    </appSettings>
  <appSettings>
    <add key="Key1" xdt:Transform="Remove" xdt:Locator="Match(key)" />
    <add key="Key2" xdt:Transform="Remove" xdt:Locator="Match(key)"/>
 </appSettings>
</configuration>
The uninstall file is pretty straightforward. Remove the app setting keys if they exist. Although, in this case, I’m not deleting the appSettings node. Leaving the appSettings node in your config file will not cause any issues.

Step 2: Modify your nuspec file to include both the .install.xdt and .uninstall.xdt file(s) as content folders.

.nuspec file is the core or blue print for generating your nuget package. Here’s an example of a .nuspec file. For more information, go here: http://docs.nuget.org/Create/Nuspec-Reference

In this example, you’ll need to refer for both .install.xdt and .uninstall.xdt file(s) as target content folders:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
  <metadata>
    <id>Package1</id>
    <version>1.1</version>
    <title>Nuget Package 1</title>
    <authors>QE Dev</authors>
    <owners>Don Tan</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Package 1 Testing</description>
<summary>Application Config change</summary>

    <releaseNotes>
      - Support for Application Config change
    </releaseNotes>
    <copyright>Copy Right</copyright>
    <language>en-US</language>
    <dependencies>
      <dependency id="Microsoft.ApplicationInsights" version="2.1.0" />
    </dependencies>
    <references>
      <reference file="Package1.dll" />
    </references>
  </metadata>
  <files>
    <file src="Package1.dll" target="lib\net45\Package1.dll.dll" />
    <!--Add Section to Uninstall and Re-install Application.Config files-->
    <file src="app.config.install.xdt" target="content" />
    <file src="app.config.uninstall.xdt" target="content" />
  </files>
</package>

Step 3: Test the generated nuget package and verify if your application config (app.config) settings have been modified

Working with VSTS Rest APIs

I’ve been working with VSTS for quite some time now and wanted to share some of the sample code I’ve written to work with VSTS data. As I work with many teams, there have been requests such as getting specific metadata during and/or after build. Examples would be people wanting to get specific data from associated work-items during builds or collection level licensing information for your users. Here are I’ll tap in to specific areas:

VSTS Builds, VSTS Work-Items, VSTS GIT (Commits), VSTS User License Information

You’ll need to the following Nuget Packages:

  • Microsoft.TeamFoundationServer.ExtendedClient
  • Microsoft.TeamFoundationServer.Client
  • Microsoft.VisualStudio.Services.Client
  • Microsoft.VisualStudio.Services.InteractiveClient

Sample Code to retrieve all Builds from a given VSTS Team Project Build Definition containing associated commits and work-items:

using System;
using System.Configuration;
using System.Linq;
using System.Text;
using AAG.Test.Core.Logger;
using Microsoft.TeamFoundation.Build.WebApi;
using Microsoft.TeamFoundation.SourceControl.WebApi;
using Microsoft.VisualStudio.Services.Client;
using VSTSApi.Entities;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;

namespace VSTSApi
{
    class Program
    {
        private static BuildOutputModel _buildoutputmodel;

        private static string VssAccountUrl { get; set; } = ConfigurationManager.AppSettings["VssAccountUrl"];

        static void Main(string[] args)
        {
            try
            {
                StringBuilder outputStringBuilder = new StringBuilder();
                var buildoutputmodel = SetBuildOutputModel(args);
                var creds = new VssClientCredentials(false);
                creds.PromptType = CredentialPromptType.PromptIfNeeded;
                var vssConnection = new VssConnection(new Uri(buildoutputmodel.VSOUrl + "/defaultcollection"), new VssBasicCredential(buildoutputmodel.UserName, buildoutputmodel.Password));

                var buildserver = vssConnection.GetClient<BuildHttpClient>();
                var workitems = vssConnection.GetClient<WorkItemTrackingHttpClient>();
                var commititems = vssConnection.GetClient<GitHttpClient>();
                var builds = buildserver.GetBuildsAsync(_buildoutputmodel.TeamProjectName).Result;
                var targetbuilds = builds.Where(definition => definition.Definition.Name.Contains(buildoutputmodel.BuilDefinitionName));
                foreach (var build in targetbuilds)
                {
                    outputStringBuilder.AppendLine($"Name: {build.Definition.Name} : BuildID: {build.Id}");
                    var associatedcommits = buildserver.GetBuildCommitsAsync(build.Definition.Project.Name,
                        build.Id).Result;
                    if (associatedcommits.Any())
                        outputStringBuilder.AppendLine($"All Commits Made for this Build:  {Environment.NewLine} ========= {Environment.NewLine} ");
                    associatedcommits.ForEach(commit =>
                    {
                        var user = commititems.GetCommitAsync(buildoutputmodel.TeamProjectName, commit.Id, buildoutputmodel.GitRepo).Result.Author;
                        outputStringBuilder.AppendLine($"ID: {commit.Id} Committed By: {user.Name}  E-mail: {user.Email} {Environment.NewLine} Description: {commit.Message} {Environment.NewLine}");
                    });
                    var commits = associatedcommits.Select(change => change.Id);
                    var associatedworkitems = buildserver.GetBuildWorkItemsRefsAsync(commits,
                        build.Definition.Project.Name, build.Id).Result;
                    if (associatedworkitems.Any())
                        outputStringBuilder.AppendLine($"All Associated Workitems for this Build:  {Environment.NewLine} ========= {Environment.NewLine} ");
                    foreach (var wi in associatedworkitems)
                    {
                        outputStringBuilder.AppendLine($"ID : {wi.Id} : URL: {wi.Url}");
                        var workitem = workitems.GetWorkItemAsync(int.Parse(wi.Id)).Result;
                        outputStringBuilder.AppendLine($"Title : {workitem.Fields["Title"]} : Description: {workitem.Fields["Description"]}");
                    }
                    outputStringBuilder.AppendLine($"{Environment.NewLine} ========= {Environment.NewLine} ");
                }
                //}

                DumpData(outputStringBuilder.ToString(), Console.WriteLine);
                DumpData(outputStringBuilder.ToString(), print => AsLogger.Info(print));
                Console.WriteLine("Press Any Key to Continue...");
                Console.ReadKey();
            }
            catch (Exception exception)
            {
                throw new Exception($"Error with Application: {exception.Message}", exception.InnerException);
            }
        }

        static void DumpData(string stringoutput, Action<string> print)
        {
            print(stringoutput);
        }

        static BuildOutputModel SetBuildOutputModel(string[] args)
        {
            _buildoutputmodel = new BuildOutputModel
            {
                UserName = ConfigurationManager.AppSettings["username"],
                Password = ConfigurationManager.AppSettings["password"],
                VSOUrl = ConfigurationManager.AppSettings["vsourl"],
                TeamProjectName = ConfigurationManager.AppSettings["teamproject"],
                BuilDefinitionName = ConfigurationManager.AppSettings["builddefinition"],
                GitRepo = ConfigurationManager.AppSettings["gitrepo"]
            };
            return _buildoutputmodel;
        }
    }
}

Entity

namespace VSTSApi.Entities
{
    public class BuildOutputModel
    {
        public string UserName { get; set; }

        public string Password { get; set; }

        public string BuilDefinitionName { get; set; }

        public string TeamProjectName { get; set; }

        public string VSOUrl { get; set; }

        public string GitRepo { get; set; }
    }
}

Sample Code for Getting User Information/Licenses:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using VSTSAccountAdmin.Model;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.Identity.Client;
using Microsoft.VisualStudio.Services.Licensing;
using Microsoft.VisualStudio.Services.Licensing.Client;

namespace VSTSAccountAdmin
{
    public class Program
    {
        private static string VssAccountUrl { get; set; } = ConfigurationManager.AppSettings["VssAccountUrl"];
        private static string VssAccountName { get; set; }
        private static License VssLicense { get; set; }

        private static List<VSOUserInfo> _vsousers;



        public static void Main(string[] args)
        {
            try
            {
                _vsousers = new List<VSOUserInfo>();

                // Create a connection to the specified account.
                // If you change the false to true, your credentials will be saved.
                var creds = new VssClientCredentials(false);
                creds.PromptType = CredentialPromptType.PromptIfNeeded;
                var vssConnection = new VssConnection(new Uri(VssAccountUrl), creds);

                // We need the clients for tw4o services: Licensing and Identity
                var licensingClient = vssConnection.GetClient<LicensingHttpClient>();
                var identityClient = vssConnection.GetClient<IdentityHttpClient>();

                var entitlements = licensingClient.GetAccountEntitlementsAsync().Result;
                IEnumerable<AccountEntitlement> accountEntitlements = entitlements as IList<AccountEntitlement> ??
                                                                      entitlements.ToList();
                var userIds = accountEntitlements.Select(entitlement => entitlement.UserId).ToList();
                var users = identityClient.ReadIdentitiesAsync(userIds).Result.ToDictionary(item => item.Id);
                foreach (var entitlement in accountEntitlements)
                {
                    var user = users[entitlement.UserId];
                    _vsousers.Add(new VSOUserInfo()
                    {
                        DisplayName = user.DisplayName,
                        LastAccessDate = entitlement.LastAccessedDate,
                        License = entitlement.License.ToString().ToLowerInvariant(),
                        UserID = entitlement.UserId
                    });
                    var stringoutput =
                        $"{Environment.NewLine}Name: {user.DisplayName}, UserId: {entitlement.UserId}, License: {entitlement.License}.";
                    Console.WriteLine(stringoutput);
                }
            }
            catch (Exception ex)
            {
                throw new ArgumentException(ex.Message, ex.InnerException);
            }

        }
    }
}

Entity:

namespace VSTSAccountAdmin.Model
{
    public class VSOUserInfo
    {
        public string DisplayName { get; set; }

        public Guid UserID { get; set; }

        public string License { get; set; }

        public DateTimeOffset LastAccessDate { get; set; }

    }
}