Search code examples
.net.net-coreazure-devopsjson-patch

Having issue creating work item in Azure DevOps API C# .net core 3


I have read through the API, online help articles, etc. and am getting 400 errors creating work items.

I have a proper token, etc. Not sure if I am doing too many things at once or not either. I need to create a parent Issue item and n number of sub child tasks linked to it. I am creating a Azure DevOps helper application for my manager to more quickly create tasks from our ticketing system. For user I am passing the email address (of a user in DevOps) and for sprint and projectID I am passing name of them.

The error I get from just creating the first parent item is :

{
   "$id":"1",
   "innerException":null,
   "message":"You must pass a valid patch document in the body of the request.",
   "typeName":"Microsoft.VisualStudio.Services.Common.VssPropertyValidationException, Microsoft.VisualStudio.Services.Common",
   "typeKey":"VssPropertyValidationException",
   "errorCode":0,
   "eventId":3000
}

I don't know what I am passing wrong in this patch call.

From appSetttings.json

 "AzureDevOpsConfig": {
    "ApiURL": "https://dev.azure.com",
    "Token": "........" }

Here is my code: ...

    [HttpPost("create")]
    public async Task<string> CreateStory(AzureStoryCreationInfo model)
    {
        var workItemData = new List<dynamic>()
        {
            new
            {
                op = "add",
                path = "/fields/System.Title",
                value = model.Title
            },
            new
            {
                op = "add",
                path = "/fields/System.IterationPath",
                value = $"{model.ProjectID}\\{model.Sprint}"
            },
            new
            {
                op = "add",
                path = "/fields/System.Description",
                value = model.Description ?? string.Empty
            }
        };

        if (!string.IsNullOrWhiteSpace(model.User))
        {
            workItemData.Add(new
            {
                op = "add",
                path = "/fields/System.AssignedTo",
                value = model.User
            });
        }

        var parentResponse = await this.CreateWorkItem(model.Organization, model.ProjectID, model.WorkItemType, workItemData);
        var parentResponseData = JsonConvert.DeserializeObject<AzureWorkItemCreateResponse>(parentResponse);

        if (parentResponseData != null && parentResponseData.Id > 0 && model.Tasks != null && model.Tasks.Any())
        {
            foreach (var task in model.Tasks)
            {
                var workItemTaskData = new List<dynamic>()
                {
                    new
                    {
                        op = "add",
                        path = "/fields/System.Parent",
                        value = parentResponseData.Id
                    },
                    new
                    {
                        op = "add",
                        path = "/fields/System.Title",
                        value = task.Title
                    },
                    new
                    {
                        op = "add",
                        path = "/fields/System.IterationPath",
                        value = $"{model.ProjectID}\\{model.Sprint}"
                    }//,
                    //new
                    //{
                    //    op = "add",
                    //    path = "/fields/System.Description",
                    //    value = string.Empty
                    //}
                };

                if (!string.IsNullOrWhiteSpace(task.User))
                {
                    workItemTaskData.Add(new
                    {
                        op = "add",
                        path = "/fields/System.AssignedTo",
                        value = task.User
                    });
                }

                var taskResponse = await CreateWorkItem(model.Organization, model.ProjectID, "Task", workItemTaskData);

                //Maybe Check if tasks fail
                var taskResponseData = JsonConvert.DeserializeObject<AzureWorkItemCreateResponse>(taskResponse);
            }

        }

        return await Task.FromResult(parentResponse);
    }

private async Task<string> CreateWorkItem(
            string organization,
            string projectIDorName,
            string workItemType,
            List<dynamic> values)
        {
            var workItemValue = new StringContent(JsonConvert.SerializeObject(values), Encoding.UTF8, "application/json-patch+json");

            return await PostData($"{organization}/{projectIDorName}/_apis/wit/workitems/${workItemType}?api-version=5.1&validateOnly=true", workItemValue);

            //var result = await PostData($"{organization}/{projectIDorName}/_apis/wit/workitems/${workItemType}?api-version=5.1", workItemValue);
        }

private async Task<string> PostData(string subUrl, StringContent data, string baseApi = null)
{
    if (client == null)
        client = new HttpClient();

    if (baseApi == null)
        baseApi = this.config.Value.ApiURL;

    client.DefaultRequestHeaders.Clear();

    client.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json-patch+json"));

    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
        Convert.ToBase64String(
            Encoding.ASCII.GetBytes(
                string.Format("{0}:{1}", "", this.config.Value.Token))));

    using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, $"{baseApi}/{subUrl}"))
    using (HttpResponseMessage response = await client.SendAsync(request))
    {
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}

...

I even tried this as well :

JsonPatchDocument test = new JsonPatchDocument(
                values.Select(val => new Microsoft.AspNetCore.JsonPatch.Operations.Operation(val.op, val.path, null, val.value)).ToList(),
                new Newtonsoft.Json.Serialization.DefaultContractResolver());
            var workItemValue = new StringContent(JsonConvert.SerializeObject(test), Encoding.UTF8, "application/json-patch+json");

Solution

  • In your sample you use PATCH method. However, you have to use POST: Work Items - Create. Additionally, you do not pass any content, the PostData method does not use StringContent data. Here is the sample to create Task based on your code:

            string pat = "<your_pat>";
    
            var workItemTaskData = new List<dynamic>()
            {
                 new
                    {
                        op = "add",
                        path = "/fields/System.Title",
                        value = "MyTitle"
                    }
            };
    
            var workItemValue = new StringContent(JsonConvert.SerializeObject(workItemTaskData), Encoding.UTF8, "application/json-patch+json");
    
            var client = new HttpClient();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json-patch+json"));
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
                Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", "", pat))));
    
    
            using (HttpResponseMessage response = client.PostAsync("https://dev.azure.com/<org_name>/<team_project_name>/_apis/wit/workitems/$Task?api-version=5.1", workItemValue).Result)
            {
                response.EnsureSuccessStatusCode();
                var result = response.Content.ReadAsStringAsync().Result;
            }