I'm trying to piece together a C# console app that accesses the work items in TFS/DevOps via it's API and compares the original estimate field parent work item with that of all its children and then spits out the name and ID of any work items that do not add up.
So far, I have been able to get back a list of all my work items with the original estimates included, but I still need to get the children of each work item so that I can loop through them and compare the summation of their original estimates with that of their parent. Given how little I know about C# and queries I am pretty stuck right now.
Since linked items are not reportable fields, I have to use $expand to execute a query to get the info I need (at least that's what the doc linked below says). This is where I am stuck. Any tips?
https://learn.microsoft.com/en-us/azure/devops/report/extend-analytics/work-item-links?view=azure-devops
Here is what I have so far.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
namespace QueryWorkitems0619
{
class Program
{
static void Main(string[] args)
{
string orgName = "{Organization's name}";
string PAT = "{Personal Access Token}";
Uri uri = new Uri($"https://dev.azure.com/{orgName}");
string project = "Wingnit_2";
VssBasicCredential credentials = new VssBasicCredential("", PAT);
//create a wiql object and build our query
Wiql wiql = new Wiql()
{
Query = "Select * " +
"From WorkItems " +
"Where [System.TeamProject] = '" + project + "' " +
"And [System.State] <> 'Closed' " +
"And [System.RelatedLinkCount] > '0'" +
"Order By [State] Asc, [Changed Date] Desc"
};
//create instance of work item tracking http client
using (WorkItemTrackingHttpClient workItemTrackingHttpClient = new WorkItemTrackingHttpClient(uri, credentials))
{
//execute the query to get the list of work items in the results
WorkItemQueryResult workItemQueryResult = workItemTrackingHttpClient.QueryByWiqlAsync(wiql).Result;
//some error handling
if (workItemQueryResult.WorkItems.Count() != 0)
{
//need to get the list of our work item ids and put them into an array
List<int> list = new List<int>();
foreach (var item in workItemQueryResult.WorkItems)
{
list.Add(item.Id);
}
int[] arr = list.ToArray();
//build a list of the fields we want to see
string[] fields = new string[5];
fields[0] = "System.Id";
fields[1] = "System.Title";
fields[2] = "System.RelatedLinkCount";
fields[3] = "System.Description";
fields[4] = "Microsoft.VSTS.Scheduling.OriginalEstimate";
//get work items for the ids found in query
var workItems = workItemTrackingHttpClient.GetWorkItemsAsync(arr, fields, workItemQueryResult.AsOf).Result;
//loop though work items and write to console
foreach (var workItem in workItems)
{
foreach (var field in workItem.Fields)
{
Console.WriteLine("- {0}: {1}", field.Key, field.Value);
}
}
Console.ReadLine();
}
}
}
}
}
You are in the right direction, you need to add the expand when you execute the GetWorkItemsAsync method:
var workItems = workItemTrackingHttpClient.GetWorkItemsAsync(arr, expand: WorkItemExpand.Relations workItemQueryResult.AsOf).Result;
Note: you can't use fields with expand together, so you need to remove it (you will get all the fields).
Loop the results, inside the work item result you will see Relations, check inside the Relations if the Rel is System.LinkTypes.Hierarchy-Forward, if yes - it's a child:
Now, you have the child id inside the Url, extract it and make an API to get his details.
Related
So I am trying to retrieve results from a collection based on a property. I wanna get any results that hold that value within the list.
This is my code
I have tried with dynamic linq. It's not working
This is dynamic linq. Not working
var list = new List<string>(2) { "11111", "22222" };
accounts = accounts.Where("#0.Contains(outerIt.PartnerCompanyId)", list);
This is not working as well
accounts = accounts .Where(a =>
a.PartnerCompanyId.Contains(list.Any().ToString()));
Also I want the SQL to generate something like this
WHERE PartnerCompanyId IN (#gp1, #gp2, #gp3, …)
I have been getting this even there are more than 1 value in the list. I want the same number of elements in the list in the parameters.
…WHERE PartnerCompanyId IN (#gp1)
Is thre any way to accomplish this?
If I have understood your question correctly, you have a list of accounts and you want to check whether the accounts contain 'list[0] OR list[1] OR list[2] ...'.
I have managed to get a similar implementation working using Dynamic Linq.
Using your code as a base, here is what I did to get the query to work:
List<string> list = new List<string>(2) { "11111", "22222" };
string argumentString = "";
for (int i = 0; i < list.Length; i++)
{
argumentString = argumentString + "#" + i;
argumentString = argumentString + ".Contains(outerIt.PartnerCompanyId)";
if (i != (list.Length - 1))
{
argumentString = argumentString + " or ";
}
}
var accounts = accounts.Where(argumentString, list.ToArray());
The loop will create the string: "#0.Contains(outerIt.PartnerCompanyId) or #1.Contains(outerIt.PartnerCompanyId)"
Once this string is created all you need is a simple Linq query to check all of the items in the list.
Note: you can refer to arguments in order via and array but not a list. As shown here https://stackoverflow.com/a/40885380/10253157.
I hope this helps, I had a similar project and it took me quite a while to figure it out.
This the right way:
var myaccounts = accounts.Where(a =>list.Contains(a.PartnerCompanyId));
You can see it a text case to run to show it works here.
You can just use Dynamic Linq, but make sure that the type from the values you search for and the type are the same.
So this will only work if the PartnerCompanyId is also a string.
var list = new List<string>(2) { "11111", "22222" };
accounts.Where("#0.Contains(outerIt.PartnerCompanyId)", list);
Testing this in LinqPad shows the SQL you would expect:
-- Region Parameters
DECLARE #p0 Int = 7065
DECLARE #p1 Int = 7066
-- EndRegion
SELECT [t0].[Id], *** FROM [MyTable] AS [t0]
WHERE [t0].[Id] IN (#p0, #p1)
I've got a LINQ query in a WCF service that runs and returns the correct number of results that I'm looking for, but repeats the first result 25 times instead of showing me all 25 different records.
The weird thing is that when I take the SQL query that it generates from the debugger and plug it into SQL Management studio, I get the correct results.
I have tried refreshing the view I'm querying from the edmx, and I've tried rewriting the query a few different ways, but I'm starting to run out of ideas.
I've included some of the code below. Any suggestions would be helpful. Thanks!
try
{
using (Entities db = new Entities())
{
var qInventory = db.vw_Web_Store_Inventory_Live
.Where(qi => qi.Sku_Number == inputSKU)
.ToList();
resultPInventory.SKU = inputSKU;
resultPInventory.StoreInventory = new List<StoreItem>();
foreach (var qi in qInventory)
{
resultPInventory.StoreInventory.Add(new StoreItem
{
StoreNum = qi.Store_Number,
Quantity = qi.Curr_Inv
});
}
}
}
catch (Exception e)
{
log.Error("[" + e.TargetSite + "] | " + e.Message);
}
log.Info("ProductInventory(" + inputSKU + ") returned " + resultPInventory.StoreInventory.Count + " results");
return resultPInventory;
As Gert's link in the comments points out, sometimes LINQ may do that if your primary keys are not set up well, or if there are multiple rows with no unique values in your database.
This link also shows a similar problem.
The solution, other than rewriting your database columns (although that would be better on the long run) with better primary / unique keys, is to select specific values anonymously (later you can assign them easily):
var qInventory = db.vw_Web_Store_Inventory_Live
.Where(qi => qi.Sku_Number == inputSKU)
.Select(qi => new { qi.Store_Number, qi.Curr_Inv })
.ToList();
resultPInventory.SKU = inputSKU;
resultPInventory.StoreInventory = new List<StoreItem>();
foreach (var qi in qInventory)
{
resultPInventory.StoreInventory.Add(new StoreItem
{
StoreNum = qi.Store_Number,
Quantity = qi.Curr_Inv
});
}
Of course this won't be the best way if you later need to use qInventory for other things. (In that case you can just select more fields)
PS, here is a way to shorten your code, but I am not sure if LINQ to Entities will allow it, so test it first:
resultPInventory.SKU = inputSKU;
resultPInventory.StoreInventory = db.vw_Web_Store_Inventory_Live
.Where(qi => qi.Sku_Number == inputSKU)
.Select(qi => new StoreItem { StoreNum = qi.Store_Number, Quantity = qi.Curr_Inv })
.ToList();
Assign new StoreItem() to a variable before adding it.
I am using HtmlAgilityPack to find all items, colours and links to products on a website. I want to be able to find an item on the website by typing in the name and colour inside my application.
So far what I have working is:
The application finds items using only the item name and returns the last thing on the website with that name. There are multiple products with the same name but each have a different colour.
The problem comes in when including colour because it's in a different XPath so it's stored in a different collection.
Here is my code:
HtmlNodeCollection collection = doc.DocumentNode.SelectNodes("//*[contains(#class,'inner-article')]//h1//a");
HtmlNodeCollection collection2 = doc.DocumentNode.SelectNodes("//*[contains(#class,'inner-article')]//p//a");
foreach (var node2 in collection2)
{
string coloursv = node2.InnerHtml.ToString();
strColour = coloursv;
//txtLog.Text += Environment.NewLine + (DateTime.Now.ToString("hh:mm:ss")) + str; - This code returns all colours (If code is ran outside of collection then only last colour in string is returned.
}
foreach (var node in collection)
{
string href = node.Attributes["href"].Value;
var itemname = node.InnerHtml.ToString();
if (itemname.Contains(txtKeyword.Text))
{
txtLog.Text = (DateTime.Now.ToString("hh:mm:ss")) + " - Item Found: " + href + " " + itemname + " " + strColour; //Successfully returns item name, colour and link but always gives last availible on website
}
}
This is because you are continually setting the Text property of a textbox within a loop (so each item will continually overwrite the previous):
foreach (var node in collection)
{
// Omitted for brevity
// This will continually overwrite the contents of your Text property
txtLog.Text = ...;
}
If you want to store multiple items, you'll either need to store the results in some type of a collection object (such as a ListBox, etc.) or by simply concatenating your values into the textbox:
foreach (var node in collection)
{
// Omitted for brevity
var stringToAdd = ...;
txtLog.Text += stringToAdd + Environment.NewLine;
}
You can also accomplish this by using the StringBuilder class to be a bit more efficient:
StringBuilder sb = new StringBuilder();
foreach (var node in collection)
{
// Omitted for brevity
var stringToAdd = ...;
// Append this item to the results
sb.AppendLine(stringToAdd);
}
// Store the results
txtLog.Text = sb.ToString();
I'm having a list of strings whit some values and I want to make some kind of variable for keeping code that I will be using in template file.
For example lets say I have list with this 3 string values: configService, scaleCoefConfigService, sessionService. Name of the list is chItemName.
And I need to generate this kind of code that I will parse later into template:
[Dependency("configService")]
[Dependency("scaleCoefConfigService")]
[Dependency("sessionService")]
So my question is can make some variable and mechanism for iterating thou list of strings that adds every single item from list to variable?
I've tried this:
foreach (var tp in controllerChecked)
{
var genCode = "[Dependency](" '"' + chItemName + '"'")] \n"
}
controllerChecked is collection of objects and one of the objects value is Name that I'm getting like this:
var chItemName = controllerChecked.Select(c => c.Name).ToList();
This is how the list chItemName is getting those strings.
But of course it is impossible to use + with lists and this kind of stuff will never work. Someone has better idea?
In your example, you are not using the tp variable, which contains will contain each of the values within controllerChecked, one at a time.
You could just iterate through the chItemName list and add the result to a StringBuilder:
StringBuilder codeBuilder = new StringBuilder();
foreach (string tp in chItemName)
{
codeBuilder.AppendLine("[Dependency(\"" + tp + "\")]");
}
string code = codeBuilder.ToString();
If controllerChecked contains more information, you could also directly access it:
StringBuilder codeBuilder = new StringBuilder();
foreach (var item in controllerChecked)
{
string propertyName = item.Name.SubString(1);
codeBuilder.AppendLine("[Dependency(\"" + item.Name + "\")]");
codeBuilder.AppendLine("public " + item.Type + " " + propertyName + " { get; set; }");
codeBuilder.AppendLine();
}
string code = codeBuilder.ToString();
PS. I would definitely change the name of chItemName to chItemNames as it is a list, but that is up to you of course.
This worked perfectly good. I have little bit harder version of this, if you can figure out how to do this:
Lets say that instead of one chItemName list I have 2 more: fName and chItemType, both are string lists.
And I have to generate this kind of code:
[Dependency("alarmsService")]
public IAlarmsService AlarmsService { get; set; }
[Dependency("jsonFactory")]
public IJSONFactoryService JsonFactory { get; set; }
[Dependency("dataBean")]
public IDataBean DataBean { get; set; }
alarmsServise, jsonFactory and dataBean are items of chItemName.
IAlarmsService, IJSONFactoryService and IDataBean are items of chItemType.
AlarmsService, Json Factory and DataBean are items of fName list.
fName is list that I got from chItemType by trimming the first letter from each string in list:
List<string> fName = new List<string>();
foreach(var i in chItemType)
{
var newName = i.Remove(0,1);
fName.Add(newName);
}
So only that list is not a part of controllerChecked list. The othere two are defined like this:
var chItemType = controllerChecked.Select(c => c.Type).ToList();
var chItemName = controllerChecked.Select(c => c.Name).ToList();
Can I edit foreach somehow or maybe I can make parts of code with StringBulider and after that merged them together?
I have working code (thank you John Socha-Leialoha) that uses the TFS API to retrieve work items, along with all their linked work items (code below). However, what I'm trying to do is access the names of the linked Files (TFS calls it a "Versioned Item") for each work item. In the TFS GUI you can link a file to a work item. Say Work Item 1234 is linked to file foo.txt. Now when I run this query to find linked items, that file is not in the list - only other children WIs or parent WIs are returned. It's the same result if I create and run the query entirely in the GUI. How can I find out which files are linked to a given WI? The only way I can now is to look at the WI in the TFS GUI, and it shows in the Files list in the lower right.
Perhaps I just need to do a normal flat query, fetch the WI fields, and somehow the names of the linked files would be one of the fields of that WI? I don't need to download the linked file, I just need the filename/location.
Code to return all linked WIs is here:
public List<string> GetLinkedItems()
{
//executes a linked item query, returning work items, as well as the items that are link to them.
//gets digital asset work item that contains the given part number in the Assoc. Parts field
var result = new List<string>();
var tpc = new TfsTeamProjectCollection(new Uri(_tfsUri));
var workItemStore = (WorkItemStore) tpc.GetService(typeof (WorkItemStore));
//and [Schilling.TFS.TechPub.AssocParts] CONTAINS '101-4108'
var query =
"SELECT [System.Id], [System.Links.LinkType], [System.TeamProject]," +
" [System.WorkItemType], [System.Title], [System.AssignedTo]," +
" [System.State] FROM WorkItemLinks " +
" WHERE ([Source].[System.TeamProject] = 'Tech Pubs' AND " +
" [Source].[System.WorkItemType] = 'DigitalAsset' AND " +
" [Source].[System.State] <> '') And " +
" ([System.Links.LinkType] <> '') And " +
" ([Target].[System.WorkItemType] <> '') " +
" ORDER BY [System.Id] mode(MayContain)";
var treeQuery = new Query(workItemStore, query);
//Note we need to call RunLinkQuery here, not RunQuery, because we are doing a link item type of query
var links = treeQuery.RunLinkQuery();
//// Build the list of work items for which we want to retrieve more information//
int[] ids = (from WorkItemLinkInfo info in links
select info.TargetId).Distinct().ToArray();
//
// Next we want to create a new query that will retrieve all the column values from the original query, for
// each of the work item IDs returned by the original query.
//
var detailsWiql = new StringBuilder();
detailsWiql.AppendLine("SELECT");
bool first = true;
foreach (FieldDefinition field in treeQuery.DisplayFieldList)
{
detailsWiql.Append(" ");
if (!first)
detailsWiql.Append(",");
detailsWiql.AppendLine("[" + field.ReferenceName + "]");
first = false;
}
detailsWiql.AppendLine("FROM WorkItems");
//
// Get the work item details
//
var flatQuery = new Query(workItemStore, detailsWiql.ToString(), ids);
WorkItemCollection details = flatQuery.RunQuery();
return
(from WorkItem wi in details
select wi.Id + ", " + wi.Project.Name + ", " + wi.Title + ", " + wi.State).ToList();
}
Work item queries can only show WorkItemLinkType links. To get links of other types (i.e. files in source control), you need to go through the list of links in the WorkItem object itself. Here's a snippet of code that will get you the artifact of the file linked to the work item:
TfsTeamProjectCollection tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(
new Uri("http://<server>:8080/tfs/<collection>"));
WorkItemStore wiStore = tpc.GetService<WorkItemStore>();
VersionControlServer vc = tpc.GetService<VersionControlServer>();
WorkItem task = wiStore.GetWorkItem(<work item with a linked file>);
var externalLinks = task.Links.OfType<ExternalLink>();
foreach (var link in externalLinks)
{
XmlDocument artifact = vc.ArtifactProvider.GetArtifactDocument(new Uri(link.LinkedArtifactUri));
}
The XML document contains all necessary information needed to grab the correct file version from the VersionControlServer using the GetItem() method.