azureazure-devopsazure-functions

Issue with Azure DevOps API: Newly Created Area Paths Not Recognized Immediately


I'm working with the Azure DevOps API to create nested area paths in a project. My code creates a child, then immediately tries to add a child under it, and so on, to create a multi-level structure.

The issue I'm facing is that, right after creating the first child, when I try to create the childchild, the API throws an error saying the path doesn’t exist. But if I re-run the code or wait a bit and try again, the path is there, and the child gets created just fine.

It seems like there’s a delay in Azure DevOps fully committing the newly created area paths, which causes the next API call to fail because it can't find the newly added parent path yet.

public async Task<WorkItemClassificationNode> CreateAreaPath(WorkItemTrackingHttpClient client, string projectName, WorkItemClassificationNode currentNode, string areaPath)
{
    string[] areaParts = areaPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
    string[] fullAreaParts = new string[areaParts.Length + 1];
    fullAreaParts[0] = projectName; // Add project name as the root
    Array.Copy(areaParts, 0, fullAreaParts, 1, areaParts.Length);
    int matchIndex = Array.FindIndex(fullAreaParts, part => part.Equals(currentNode.Name, StringComparison.OrdinalIgnoreCase));

    if (matchIndex >= 0)
    {
        string cleanedPath = CleanePath(currentNode.Path);
        string[] remainingParts = fullAreaParts.Skip(matchIndex + 1).ToArray();
        
        foreach (string part in remainingParts)
        {
            var newNode = new WorkItemClassificationNode()
            {
                Name = part,
                StructureType = TreeNodeStructureType.Area,
            };

            var resultNode = await client.CreateOrUpdateClassificationNodeAsync(newNode, projectName, TreeStructureGroup.Areas, cleanedPath);
            
            currentNode = resultNode;
            cleanedPath += $"/'{part}'";
        }

        return currentNode;
    }
    else
    {
        Console.WriteLine($"Deepest Node '{currentNode.Name}' not found in '{areaPath}'");
        return null;
    }
}

With a Main() like this

WorkItemClassificationNode deepestNode = await helper.CreateAreaPath(workItemTrackingClient, projectName2, furthestChild, "Child1/ChildChild/ChildChildChild");

So if my furthestChild is Child1 the function should add the ChildChild and then the ChildChildChild.

So after calling the method the first time, only the ChildChild gets created and then the error occurs for creating the ChildChildChild.

After running the method again, the method successfully creates the ChildChildChild. So right now it's only possible to add one level of children to an existing node.


Solution

  • I added some output for checking, did slight change on your code, the ChildChild and ChildChildChild hierarchical area path is created successfully.

    Please check the script below:

    using System;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
    using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
    using Microsoft.VisualStudio.Services.WebApi;
    using System.Collections.Generic;
    using Microsoft.VisualStudio.Services.Common;
    
    public class AreaPathHelper
    {
        public async Task<WorkItemClassificationNode> CreateAreaPath(WorkItemTrackingHttpClient client, string projectName, WorkItemClassificationNode currentNode, string areaPath)
        {
            string[] areaParts = areaPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            string[] fullAreaParts = new string[areaParts.Length + 1];
            fullAreaParts[0] = projectName; // Add project name as the root
            Array.Copy(areaParts, 0, fullAreaParts, 1, areaParts.Length);
    
            Console.WriteLine("Full Area Parts: " + string.Join(", ", fullAreaParts));
    
            int matchIndex = Array.FindIndex(fullAreaParts, part => part.Equals(currentNode.Name, StringComparison.OrdinalIgnoreCase));
    
            Console.WriteLine("Match Index: " + matchIndex);
    
            if (matchIndex >= 0)
            {
                string cleanedPath = CleanPath(currentNode.Path);
                string[] remainingParts = fullAreaParts.Skip(matchIndex + 1).ToArray();
    
                foreach (string part in remainingParts)
                {
                    var newNode = new WorkItemClassificationNode()
                    {
                        Name = part,
                        StructureType = TreeNodeStructureType.Area,
                    };
    
                    var resultNode = await client.CreateOrUpdateClassificationNodeAsync(newNode, projectName, TreeStructureGroup.Areas, cleanedPath);
    
                    currentNode = resultNode;
                    cleanedPath += $"/{part}";
                }
    
                return currentNode;
            }
            else
            {
                Console.WriteLine($"Deepest Node '{currentNode.Name}' not found in '{areaPath}'");
                return null;
            }
        }
    
        private string CleanPath(string path)
        {
            // Implement your path cleaning logic here if needed
            return path;
        }
    }
    
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var workItemTrackingClient = new WorkItemTrackingHttpClient(new Uri("https://dev.azure.com/orgname"), new VssBasicCredential(string.Empty, "yourPAT"));
            var helper = new AreaPathHelper();
            string projectName2 = "project1";
            var furthestChild = new WorkItemClassificationNode { Name = "Child1", Path = "Child1" };
    
            WorkItemClassificationNode deepestNode = await helper.CreateAreaPath(workItemTrackingClient, projectName2, furthestChild, "Child1/ChildChild/ChildChildChild");
    
            if (deepestNode != null)
            {
                Console.WriteLine($"Deepest Node Created: {deepestNode.Name}");
            }
        }
    }
    

    Make sure to replace "https://dev.azure.com/orgname", "yourPAT", "ProjectName", node name and path name with your actual value respectively.

    My original Area path:

    enter image description here

    The script executed:

    enter image description here

    enter image description here