Retrieve image in Work Item Description with TFS API

When you try to export content of Work Item from Azure DevOps (online or sever) you need to deal with external images that are referenced in HTML fields of Work Item. I’ve dealt in the past on this subject, showing how you can retrieve images with Store and Attachment Work Item Property.

Sadly enough, I’ve encountered situation with on-premise version of TFS where I found this type of image src inside HTML fields.


As you can see the image is stored inside TFS as attachment, because it is served by AttachFileHandler, but you do not find any information of this image in Work Item Attachments property.

Tfs  / Azure DevOps has different technique to attach images to Work Item Description and image file is not always a real Attachment of the Work Item

This format has no FileId to retrieve the image with the Store interface, nor is present in the Attachments collection of current Work Item, so we need to download it with a standard WebClient, instead of relying to some specific API call. The problem is authentication, because if you simply try to use a WebClient to download the previous url you will got a 401.

The solution is to populate the Credentials property of WebClient with the current credentials used to connect to the TFS, in my situation I’ve this value into an helper class called ConnectionManager.


Figure 1: How to correctly retrieve image attached to a work item.

The code is really simple, just get the credential from Connection manager, generate a temp file name and use WebClient to download image content.

TfsTeamProjectCollection class, once connected to TFS / Azure Devops instance, contains an instance of used credentials in Credentials property. This can be used to do standard request with WebClient to the server.

The GetCredentials() method of ConnectionManager is really simple because, after connection is stabilished, the instance of TfsTeamProjectCollection class has a Credentials property that contains credentials used to connect to the server.

Armed with correct credentials, we can use WebClient standard class to download images from the server.

Gian Maria

Create Word document from Work Items

Post in the series:
1) API Connection
2) Retrieve Work Items Information
3) Azure DevOps API, Embed images into  HTML

Now we have all the prerequisites in place to connect to an Azure DevOps account, execute a query to grab all work items of a sprint and modifying HTML of Rich Edit fields to embed images. It is time to create a word document.

To have a better look and feel of exported document, the best approach is using the concept of Templates created by simple Word documents. With this technique we can use all the styles, formatting directly in Word, then use some placeholder to specify where you want to include fields of work Items.


Figure 1: A simple example of a Word Template used to export content of a Work Item

As you can see from Figure 1, a template is a simple word file where I have some special placeholder like {{title}} in the text to identify the point where I want to insert content taken from Work Items. This approach is really useful because Open XML format has a really nice feature that allows you to embed word documents inside other Word documents. This will allows me to open the template, perform substitution keeping all formatting, finally save everything to a temp file and append to the main document. With this approach I do not need to do any formatting in code, while giving the user of the tool the ability to decide the template of the output simply editing a word file.

The concept of template made extremely simple for a user to specify the formatting while keeping the code simple because it should only look for specific tokens and perform substitution.

I will really thanks Proximo S.r.L. a company I’m collaborating with for giving me the permission to share the code to manipulate Word Document, and to publish it open source. The whole code is in the example hosted in GitHub,

If take an high level look to the routine, I simply grab a reference to a list of WorkItems object, then proceed to generate a new Word Document with the help of an object called WordManipulator that contains all the routines I needs to generate a word starting from templates.

var fileName = Path.GetTempFileName() + ".docx";
using (WordManipulator manipulator = new WordManipulator(fileName, true))
    foreach (var workItem in workItems)
        manipulator.InsertWorkItem(workItem, @"Templates\WorkItem.docx", true);

WordManipulator class simply accept a name of a file, and a boolean value to specify if we need to create a new file, in this example I request for creation of a new file, then InsertWorkItem method will accept the template file and a boolean value that specify if you want to add a page break after the Work Item.

public void InsertWorkItem(WorkItem workItem, String workItemTemplateFile, Boolean insertPageBreak = true)
    //ok we need to open the template, give it a new name, perform substitution and finally append to the existing document
    var tempFile = Path.GetTempFileName();
    File.Copy(workItemTemplateFile, tempFile, true);
    using (WordManipulator m = new WordManipulator(tempFile, false))

    AppendOtherWordFile(tempFile, insertPageBreak);

As promised the routine is really simple, just create a temporary file name, copy the template file over it, then open with another instance of WordManipulator and call the SubstituteTokens function, passing a dictionary with all the fields of Work Items we want to export.

private Dictionary CreateDictionaryFromWorkItem(WorkItem workItem)
    var retValue = new Dictionary();
    retValue["title"] = workItem.Title;
    retValue["description"] = new HtmlSubstitution(workItem.EmbedHtmlContent(workItem.Description));
    retValue["assignedto"] = workItem.Fields["System.AssignedTo"].Value?.ToString() ?? String.Empty;
    retValue["createdby"] = workItem.Fields["System.CreatedBy"].Value?.ToString() ?? String.Empty;
    return retValue;

For this first example I export only four fields, but what it is interesting is that use an helper class called HmlSubstitution for the WorkItem.Description field, to specify to the substitution engine that I do not want a simple text substitution but I need a piece of HTML to be inserted into the document. The helper method EmbedHtmlContent was previously discussed and it is needed only to have an HTML with all the image embedded as base64.

Thanks to the concept of templates, creating a Word Document from Work Items it is just a series of  simple operations: open template, perform substitution and append to the main document.

The SubstituteTokens is a slightly more complex, because it scans all paragraphs of the document looking for keys of the substitution dictionary; when a key is found it will perform substitution using corresponding value. The code is complex because when you put a token like {{token}} inside a word file, it could be stored in XML format using more than one simple Run object (consult the ECMA for specifications). Given this premise, the code will try to find all run objects that contains the token, then perform substitution.

Even if a paragraphs seems really simple in Word, it could be saved with many Runs in OpenXml format, thus when you perform substitution you should never assume that a token will fit into an entire run.

Some of the routine are really interesting, the AppendOtherWordFile will simply append another file to the current one, and it is using the concept of the AltChunk an object in the SDK that allows me to embed one document into another. The trick about AltChunk is that it is a simple object where you can store complex data with the FeedData method, simply passing a stream as argument.

public WordManipulator AppendOtherWordFile(String wordFilePath, Boolean addPageBreak = true)
    MainDocumentPart mainPart = _document.MainDocumentPart;
    string altChunkId = "AltChunkId" + Guid.NewGuid().ToString();
    AlternativeFormatImportPart chunk = mainPart.AddAlternativeFormatImportPart(AlternativeFormatImportPartType.WordprocessingML, altChunkId);

    using (FileStream fileStream = File.Open(wordFilePath, FileMode.Open))
        AltChunk altChunk = new AltChunk();
        altChunk.Id = altChunkId;
            .InsertAfter(altChunk, mainPart.Document.Body
    if (addPageBreak)
            new Paragraph(
            new Run(
                new Break() { Type = BreakValues.Page })));
    return this;

The real magic is in the AddAlternativeFormatImportPart of the MainDocumentPart of the destination document, that allows you to specify the creation of a special chunk, containing a AlternativeFormatImportPartType.WorprocessingML (another word document). Thanks to this method we can create an alternate part, copying the entire content of the word document to attach and finally add this part to the original document (at the last position).

This method is so powerful that it can be used to create an alternate Import part of HTML type.

private AltChunk CreateChunkForHtmlPage(string htmlPage)
    var realHtml = $"{htmlPage}";
    string altChunkId = "myid" + Guid.NewGuid().ToString();
    using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(realHtml)))
        // Create alternative format import part.
        AlternativeFormatImportPart formatImportPart = _document.MainDocumentPart.AddAlternativeFormatImportPart(

        // Feed HTML data into format import part (chunk).
    var altChunk = new AltChunk();
    altChunk.Id = altChunkId;
    return altChunk;

OpenXML format is really fascinating and it is a real fantastic effort made by Microsoft to create a standard that is easy to use. As you can see from the above snippet, inserting HTML code inside a Word Document is done with a couple of calls.

All the rest of the code in the example is boilerplate, and here is the result of a test export. The code relating to this example is in GitHub with the tag 0.2.0. Here is an example of an exported document


Figure 2: An exported document with complex description

The original Work Item in Figure 2 was created with AIT WordToTfs tools, that allows bidirectional editing of Work Item in Word. As you can see, thanks to this tool I was able to change font in the Work Item description, and you can also verify to the Rigth that exported document maintains the formatting, and it is also using the Word template file.

Output Word Document still maintain all formatting of the template (color, bold, font etc) but also the Work Item description maintains its formatting, so the export is high fidelity.

To run this example I used this command line that allows me to specify all the information needed by the tool to export everything.

--tokenfile C:\develop\Crypted\patOri.txt 
--teamproject "zoalord insurance" 
--iterationpath "zoalord insurance\Release 1\Sprint 6"
--areapath "zoalord insurance"

Happy new Year and Happy Azure DevOps.

Gian Maria.

Azure DevOps API, Embed images into html

Post in the series:
1) API Connection
2) Retrieve Work Items Information

Before going to generate a Word File from Work Item Data we need to solve a little problem with HTML content in Work Item fields. As you know Azure DevOps has a rich web editor that allows you to create complex text in some fields, like Description, the problem is: whenever you copy and paste images inside the Web Editor, those images were added as Work Item attachments and the real HTML content is just a reference to the attachmen Url. If you want to generate a consistent Word or export to whatever destination you want, you should manipulate html to embed the image, or the html will be not consistent.

Focus of this article will be: how I can download attachment of Work Items and how I can embed image attachment directly in HTML code.

Html content in Work Item support images, but images are usually a reference to attachment of  the Work Item itself, thus it is not consistent because it refers protected resources.

Here is an example, I have a work item, I embedded an image in the description and the HTML content of System.Description field is an <img> tag with this src value:;FileName=System.Description.0.png. Actually this could be seen as a no-problem, because if you copy this url into a browser the image will be correctly downloaded, but the problem rely in authentication. If you are going to embed this HTML into a Word Document no one will be able to visualize the image, because word is not authenticated to Azure DevOps, thus you need to download locally and embed into the html document.

A possible approach is reference the HtmlAgilityToolkit library, then build a routine that programmatically download every attachment, and finally embeds the image in src attribute value using Base64 encoding, here is the code.

public static String EmbedHtmlContent(this WorkItem workItem, String htmlContent)
    HtmlDocument doc = new HtmlDocument();

    var images = doc.DocumentNode.SelectNodes("//img");
    if (images != null)
        foreach (var image in images)
            //need to understand if it is in base 64 or no, if the answer is no, we need to embed image
            var src = image.GetAttributeValue("src", "");
            if (!String.IsNullOrEmpty(src))
                if (src.Contains("base64")) // data:image/jpeg;base64,
                    //image already embedded
                    Log.Debug("found image in html content that was already in base64");
                    Log.Debug("found image in html content that point to external image {src}", src);
                    //is it a internal attached images?
                    var match = Regex.Match(src, @"FileID=(?\d*)");
                    if (match.Success)
                        var attachment = workItem.Attachments
                            .FirstOrDefault(_ =&gt; _.Id.ToString() == match.Groups["id"].Value);
                        if (attachment != null)
                            //ok we can embed in the image as base64
                            WorkItemServer wise = workItem.Store.TeamProjectCollection.GetService();
                            var downloadedAttachment = wise.DownloadFile(attachment.Id);
                            byte[] byteContent = File.ReadAllBytes(downloadedAttachment);
                            String base64Encoded = Convert.ToBase64String(byteContent);
                            var newSrcValue = $"data:image/{attachment.Extension.Trim('.')};base64,{base64Encoded}";
                            image.SetAttributeValue("src", newSrcValue);

    return doc.DocumentNode.OuterHtml;

This code is really simple, it is an extension method of the WorkItem type so you can simply use whenever you have a reference to a Work Item. The code will simply search in all HTML text img tags, for each img tag it will verify if it already contains string base64 (because the image could be already embedded), if the answer is no, we need to download the image locally and embed.

If you look at the attachment url you can notice a FileID=xxxx that points to the attachment of the work item. With a simple regex I can find if the url conform to this pattern, and if the answer is yes, I’ll search into WorkItem.Attachments collection for the right attachment.

Work Item object in C# library has a nice Attachments collection that allows you to iterate through all attachments to find any information you need

Having a reference to the Attachment is crucial, because I need to know the extension of the file. Once the attachment object is found, I can use Store property of the work item to grab a reference to the TfsTeamProjectCollection object that allows me to grab a reference to the WorkItemServer object, that is needed to download the file locally. Thanks to C# object model, if I have a simple reference to a Work Item I can still traverse properties to grab a reference to the original collection object that was still authenticated to the server.

Using Store property of Work Item allows you to access the original Collection object that is authenticated to the server, thus you can ignore authentication problems

Once I have a reference to WorkItemServer, its method DownloadFile will simply download attachment by id to a temp local file, then a simple conversion to Base64 will perform the trick. The result is a src attribute that embed the image.


Figure 1: Src attribute with image embedded

Now I can simply change the attribute of the image thanks to HtmlAgilityToolkit library, and finally return modified HTML to the caller.

Now I have html code that embed all images and has no reference to external resources in Azure DevOps, so I can embed it everywhere I want without any problem.

Gian Maria.

Azure DevOps API, Retrieve Work Items Information

Post in the series:
1) API Connection

Now that we know how to connect to Azure DevOps services, it is time to understand how to retrieve information about Work Items to accomplish the requested task: export Work Items data inside a Word Document.

Once you connected to Azure DevOps account you start retrieving helper classes to work with the different functions of the service, if you need to interact with Work Items  you need a reference to the WorkItemStore class. Since this is the most common service I need to interact I simply get a reference in the connection class


Figure 1: Retrieving reference to the WorkItemStore helper class

At this point usually people goes to find a way to execute stored queries present in the service, but this is quite often the wrong way to got if you are working on a tool that needs to export information from Work Item, the right way to go is build your own query in code.

Creating a query in WIQL is the best way to programmatically query WorkItemStore to find what you need

Thanks to Microsoft, you have a full SQL Like query engine that supports a custom syntax called WIQL (Work Item Query Language) that you can use to find what you want. In my example I’m interested in exporting data belonging to a specific area path and iteration path (usually data of a sprint), and here is the code to retrieve what I need.

public List LoadAllWorkItemForAreaAndIteration(string areaPath, string iterationPath)
    StringBuilder query = new StringBuilder();
    query.AppendLine($"SELECT * FROM WorkItems Where [System.AreaPath] UNDER '{areaPath}' AND [System.IterationPath] UNDER '{iterationPath}'");
    if (!String.IsNullOrEmpty(_teamProjectName))
        query.AppendLine($"AND [System.TeamProject] = '{_teamProjectName}'");

    return _connection.WorkItemStore.Query(query.ToString())

As you can verify this is really a SQL LIKE query, but it is expressed with some concepts of Work Items, such as UNDER operator that allows me to query for Work Items whose AreaPath is under a certain path. As you can see, specifying the team project is completely optional, and it is present only for completeness, because if you specify area path and iteration you are actually filtering for Work Item of a specific team project. Once the query text is ready, just call the Query method of the WorkItemStore and do some LINQ manipulation to cast everything to WorkItem Class (helper class included in the Nuget Packages).

Querying Work Items is just: creating text query and call a method of the WorkItemStore

Another great advantage in using Nuget Packages is having intellisense that helps you working with results, here is a sample code used to dump all Work Items returned from the query.

WorkItemManger workItemManger = new WorkItemManger(connection);
workItemManger.SetTeamProject("zoalord insurance");
var workItems = workItemManger.LoadAllWorkItemForAreaAndIteration(
    "zoalord insurance", 
    "zoalord insurance\\Release 1\\Sprint 6");
foreach (var workItem in workItems)
    Log.Debug("[{Id}/{Type}]: {Title}", workItem.Id, workItem.Type.Name, workItem.Title);

Nothing could be simpler, thanks to intellisense I can simply dump id name and type of the work items in few lines of code, here is a sample output of the code.


Figure 2: Dump output of Work Items returned from the query.

As you can see, with very few lines of code I was able to connect to my Azure DevOps account, query for Work Items belonging to a specific iteration then dump information to console.

Next step will be creating a skeleton Word document with data instead of dumping data to console.

Gian Maria.

Azure Devops API, Connection

One of the great benefit of using Azure DevOps is the ability to interact with the service through API calls, making it possible to extend the service with a few bunch of C#, or PowerShell or whatever language you want, because almost everything is exposed with REST API, and a simple HTTP call is enough.

Since I’m mostly a C# and .NET guy, I’ll explain how to build a C# program that interact with an Azure DevOps account, because thanks to Nuget Packages offered by Microsoft, you can interact with your account with Strongly Typed C# classes, so you can have intellisense and compile type checking to verify that everything is good.

C# helps using Azure DevOps API because you have helper client libraries that guides the programmer with Intellisense and documentation.

The first example I’m going to show, is how to retrieve a bunch of Work Items to export to a Word Document, a requirement that is really needed by everyone that uses the services. While there are commercial and non commercial tools out there, if you really need to extract Word Document with maximum customization a bunch of C# code can made your life easier.

Sample code accompanying this series of blog posts can be found at this address: where I’m pushing some code to export (migrate) data from Azure DevOps to word.

First of all we need to know how to setup the project and how to connect to an account and it turns out that is really simple, first of all create a Full Framework .NET project and add these nuget references.


With these packages you have a bunch of libraries that makes your life easier, especially for connection and interacting with base functions of the services.

Authentication can be done in several ways, but a token is the real way to go because it has several advantages over standard credentials.

When it is time to authenticate to the service, you have several options, but the most useful is using an Access Token because it has several benefits. First of all it does not require interactivity, second it can be revoked whenever you want, third you can generate a token with a reduced permission set, fourth it will expire automatically after one Year.

Using a token is perfect for a tool that should run unattended and this will be my usual first choice. To make debug life simpler I allow to specify the access token in a couple of way in command line, I can directly specify the token or I can specify the token that contains the file. The second method is useful for debugging, because I write my token in a file, then encrypt with standard NTFS routine to allow only my user to decrypt and use it, then I can configure the debugger to launch my console application with that token file and everything is secure, I do not incur the risk of incorrectly store my token file in some commit.


Figure 1: Project options, where I specify start options to specify token and other parameter to my application

As you see in Figure 1 I simply specify the c:\crypted\patOri.txt in the command line to made my software authenticate to the service, startup program is usually a normal console application.

Having a console application is really useful because it can be automated in a simple Bat or powershell file, it can be easily converted to a service with TopShelf and it is my usual way to go over a full WPF or Winform or Web application that require user interactivity.

Once I have an access token, here is my Connection helper class used to connect to my service.

 public class Connection
        /// <summary>
        /// Perform a connection with an access token, simplest way to give permission to a program
        /// to access your account.
        /// </summary>
        public Connection(String accountUri, String accessToken)
            ConnectToTfs(accountUri, accessToken);
            _workItemStore = _tfsCollection.GetService();

        private TfsTeamProjectCollection _tfsCollection;
        private WorkItemStore _workItemStore;

        public WorkItemStore WorkItemStore =&gt; _workItemStore;

        private bool ConnectToTfs(String accountUri, String accessToken)
            //login for VSTS
            VssCredentials creds = new VssBasicCredential(
            creds.Storage = new VssClientCredentialStorage();

            // Connect to VSTS
            _tfsCollection = new TfsTeamProjectCollection(new Uri(accountUri), creds);
            return true;

The class does nothing more than create a VssBasicCredential with empty user and token as a password, use a standard VssClientCredentialStorage and pass everything to TfsTeamProjectCollection class, finally it calls the Authenticate method and if no exception is thrown, you are connected to the service.

As you can see, connecting and authenticating to an Azure DevOps account is just a matter of a few bunch of Nuget Packages and few C# lines of code.

Gian Maria.