Use .Net Interactive with Azure DevOps
Published: 2021-05-29 18:28:42

<sub>Photo by <a href="https://unsplash.com/@dev_irl">Dev Benjamin</a> on <a href="https://unsplash.com/s/photos/interaction">Unsplash</a></sub>

I will use .Net interactive to connect to Azure DevOps, and generate markdown with the output of one query.

# Connect to Azure DevOps

First, we will have to set a PAT for authentication.
```csharp
string pat = "<YOUR PAT>";
```

Then, we have to define locally the type representing WorkItem.

```csharp
class WorkItemPresentation
{
    public string Id {get; set;}
    public string Name {get; set;}
    public string State {get; set;}
    public string Description {get; set;}
}
```

Finally, we will call the query, map the output to WorkItemPresentation, and output it.

```csharp
#r "nuget:Microsoft.TeamFoundationServer.Client"
#r "nuget:Microsoft.VisualStudio.Services.InteractiveClient,16.170.0"
#r "nuget:Microsoft.VisualStudio.Services.Client"

// https://www.nuget.org/packages/Microsoft.TeamFoundationServer.Client/
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
// https://www.nuget.org/packages/Microsoft.VisualStudio.Services.InteractiveClient/
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.WebApi;
// https://www.nuget.org/packages/Microsoft.VisualStudio.Services.Client/
using Microsoft.VisualStudio.Services.Common; 

    VssConnection connection = new VssConnection(new Uri("https://XXX.visualstudio.com"), new VssBasicCredential(string.Empty, pat));

    // Create instance of WorkItemTrackingHttpClient using VssConnection
    WorkItemTrackingHttpClient witClient = connection.GetClient<WorkItemTrackingHttpClient>();

    // Get 2 levels of query hierarchy items
    List<QueryHierarchyItem> queryHierarchyItems = witClient.GetQueriesAsync("<PROJECT>", depth: 2).Result;

    // Search for 'My Queries' folder
    QueryHierarchyItem myQueriesFolder = queryHierarchyItems.FirstOrDefault(qhi => qhi.Name.Equals("My Queries"));
    if (myQueriesFolder != null)
    {
        string queryName = "<QUERY_NAME>";
        // See if our 'REST Sample' query already exists under 'My Queries' folder.
        QueryHierarchyItem newBugsQuery = null;
        if (myQueriesFolder.Children != null)
        {
            newBugsQuery = myQueriesFolder.Children.FirstOrDefault(qhi => qhi.Name.Equals(queryName));
        }
        var queryResult = witClient.QueryByIdAsync(newBugsQuery.Id).Result;
        // need to get the list of our work item id's and put them into an array
        int[] workItemIds = queryResult.WorkItems.Select<WorkItemReference, int>(wif => { return wif.Id; }).ToArray();
        // build a list of the fields we want to see
        string[] fields = new []
            {
                "System.Id",
                "System.Title",
                "System.State",
                "System.Description"
            };

        IEnumerable<WorkItemPresentation> workItems = witClient.GetWorkItemsAsync(workItemIds, fields, queryResult.AsOf).Result.Select(w => new WorkItemPresentation(){Id=w.Fields["System.Id"].ToString(), 
                                                                                                                                                                        Name=w.Fields["System.Title"].ToString(), 
                                                                                                                                                                        State=w.Fields["System.State"].ToString(), 
                                                                                                                                                                        Description=w.Fields.ContainsKey("System.Description") ? w.Fields["System.Description"].ToString() : "" });

        display(workItems);
    }
```

# Modify Output Rendering
In order to manipulate the rendering, we need to define a special Formatter for our type.

```csharp
using static Microsoft.DotNet.Interactive.Formatting.PocketViewTags;
using Microsoft.DotNet.Interactive.Formatting;
    
public string Clean(string content){
    return content.Replace("[", " ").Replace("]", " ").Replace("(", " ").Replace(")", " ");
}

Formatter.ResetToDefault();
Formatter.Register<IEnumerable<WorkItemPresentation>>((workItems, writer) => 
{
    foreach (var workItemPresentation in workItems)
    {
        writer.WriteLine("<br/>");
        writer.WriteLine("---");
        writer.WriteLine("<br/>");
        writer.WriteLine("<br/>");
        writer.WriteLine($"# {Clean(workItemPresentation.Name)}");        
        writer.WriteLine("<br/>");
        writer.WriteLine("<br/>");
        writer.WriteLine($"{Clean(!string.IsNullOrEmpty(workItemPresentation.Description) ? workItemPresentation.Description :  workItemPresentation.Name)}");
        writer.WriteLine("<br/>");
    }
}, mimeType: "text/html");
```

See the [docs](https://github.com/dotnet/interactive/blob/main/docs/formatting.md) for formatting.


[Interesting post by Lady Nagaga](https://devblogs.microsoft.com/dotnet/net-interactive-preview-3-vs-code-insiders-and-polyglot-notebooks/) on [how to pass data from one kernel to another](https://github.com/dotnet/interactive/blob/main/docs/variable-sharing.md).


Hope this helps !