How to page results from TFS WorkItemTrackingHttpClient QueryByWiqlAsync - tfs

I'm using WorkItemTrackingHttpClient.QueryByWiqlAsync to retrieve the results of a query. It returns the top 200 records by default. It has a top param which you can use to specify a value less then 200.
However, the result of my query is greater than 200 records. Does anyone know how to execute a query using the TFS client with proper paging .i.e. page and page size? For example, return page 10 where the page count is say 50.
I've looked around but can't see how to do this using the client which is surprising as the capability seems so fundamental.
I'm using the Nuget Package Microsoft.TeamFoundationServer.Client 16.153.0. I'm also connecting to TFS 2017 onprem.

Yeah, we can only return a list of work items which limits to Maximum 200 by calling the API. It's by design, see Work Items - List for details.
However we can use WIQL Queries to retrieve data from Azure DevOps. It is very flexible and it could be used in any situation. See WIQL queries and Azure DevOps Rest API for WIQL Queries
To get all the work items, we can try following steps:
Execute a stored query using the work item query API to retrieve a
list of work item IDs
Split the list of work item IDs into groups of 200, which is the
maximum batch size that the work items API supports
Call the work items API for each list of 200 work item IDs to get
the work item details.
You can reference this thread for details.
Besides, below code for your reference, it's working for me:
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QueryWorkitems0619
{
class Program
{
static void Main(string[] args)
{
Uri uri = new Uri("http://tfs2017:8080/tfs/DefaultCollection");
//string PAT = "xxxx";
string project = "SCRUM-TFVC";
//VssBasicCredential credentials = new VssBasicCredential(user, PAT);
VssCredentials credentials = new VssClientCredentials();
credentials.Storage = new VssClientCredentialStorage();
//create a wiql object and build our query
Wiql wiql = new Wiql()
{
Query = "Select * " +
"From WorkItems " +
"Where [Work Item Type] = 'Product Backlog Item' " +
"And [System.TeamProject] = '" + project + "' " +
"And [System.State] <> 'Closed' " +
"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;
//Splict the query result (the list of work item IDs) into groups of 200.
var QueryGroups = from i in Enumerable.Range(0, workItemQueryResult.WorkItems.Count())
group workItemQueryResult.WorkItems.ToList()[i] by i / 200;
foreach (var QueryGroup in QueryGroups)
{
//some error handling
if (QueryGroup.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 QueryGroup.ToList())
{
list.Add(item.Id);
}
int[] arr = list.ToArray();
//build a list of the fields we want to see
string[] fields = new string[3];
fields[0] = "System.Id";
fields[1] = "System.Title";
fields[2] = "System.State";
//get work items for the ids found in query
var workItems = workItemTrackingHttpClient.GetWorkItemsAsync(arr, fields, workItemQueryResult.AsOf).Result;
Console.WriteLine("\n\n----------------------------------------------------------------");
Console.WriteLine("\nQuery Results: {0} items found for Group {1}", workItems.Count, QueryGroup.Key);
Console.WriteLine("\n----------------------------------------------------------------");
//loop though work items and write to console
foreach (var workItem in workItems)
{
Console.WriteLine("ID:{0} Title:{1} State:{2}", workItem.Id, workItem.Fields["System.Title"], workItem.Fields["System.State"]);
}
}
}
Console.ReadLine();
}
}
}
}

Related

Shuffle the results of a LINQ query based on SessionID

I'm working on an ASP.NET MVC application with Entity Framework 6 and a SQL Server database.
I'm trying to shuffle the results of a query by adding a SortingCode which I'd like to assign a value based on the current SessionId, so that every time the returned rows are shuffled without affecting the pagination. SortingCode in this attempt is a string, but it can be any type, as long as it allows me to get shuffled results. I have something like this:
var sessionId = Session.SessionID.GetHashCode();
var rnd = new Random(sessionId);
var query = (from l in _context.Adverts
select new AdvertSummary
{
Id = l.Id,
Title = l.Title,
Description = l.Description,
SortingCode = l.Title.OrderBy(x => rnd.Next()).ToString(),
});
The IQueryable result is then converted into a list later on in my code with:
var pagedResults = query.Skip(skip).Take(pageSize).ToList();
The above attempt with the Random class doesn't work, and results in an error
DbExpressionBinding requires an input expression with a collection ResultType
Is there anything that I can do to get shuffled results?
I would suggest to use SqlFunctions.Checksum for such task. SortingCode will be nearly close to the seeded Random.
var sessionId = Session.SessionID;
var query =
from l in _context.Adverts
select new AdvertSummary
{
Id = l.Id,
Title = l.Title,
Description = l.Description,
SortingCode = SqlFunctions.Checksum(sessionId, l.Title)
};
var pagedResults = query
.OrderBy(x => x.SortingCode)
.ThenBy(x => x.Id)
.Skip(skip)
.Take(pageSize)
.ToList();

How to create a TFS alert for changes to the items' Stack Rank field

How can I create an alert when any team member makes changes to the Stack Rank field (only) of any work item in TFS?
You can add a alter filter in a work item team alter just including Stack Rank changes
Sample:
Update
You can also try to use TFS API to achieve this. Below code shows how to query workitems whether a field (ex. System.AssignedTo field) is changed on a given day. For stank rank, FieldName="Microsoft.VSTS.Common.StackRank"
void Main()
{
const String CollectionAddress = "http://mytfsserver/tfs/MyCollection";
using (var tfs = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri(CollectionAddress)))
{
var server = tfs.GetService<WorkItemStore>();
var changes =
server.Query("select * from WorkItems where [System.ChangedDate] = #Today")
.Cast<WorkItem>()
.SelectMany(wi =>
wi.Revisions
.Cast<Revision>()
.SelectMany(r =>
r.Fields
.Cast<Field>()
.Where(f => !String.IsNullOrEmpty(f.OriginalValue as String) && f.Value != f.OriginalValue && f.ReferenceName == "System.AssignedTo")
.Select(f => new { wi.Id, f.OriginalValue, f.Value, f.ReferenceName, })))
.Dump();
}
}
More detials about how to programilly query work items, please refer the link from MSDN:Query for Bugs, Tasks, and Other Work Items

Search a collection of objects

I'm trying to search for a collection of potential nodes but unable to do it...
I have a product that has a relationship with many instances. I would like to query the DB and get all the instances that are in a list that i get from the user.
Cypher:
var query = _context
.Cypher
.Start(new
{
instance = startBitsList,
product = productNode.Reference,
})
.Match("(product)-[:HasInstanceRel]->(instance)")
.Return(instance => instance.Node<ProductInstance>());
The problem is startBitsList... I use StringBuilder to generate a query that contains all the instances I'm looking for:
private static string CreateStartBits(IEnumerable<string> instanceNames)
{
var sb = new StringBuilder();
sb.AppendFormat("node:'entity_Name_Index'(");
foreach (var id in productIds)
{
sb.AppendFormat("Name={0} OR ", id);
}
sb.Remove(sb.Length - 4, 4);
sb.Append(")");
var startBitsList = sb.ToString();
return startBitsList;
}
I get exceptions when trying to run this cypher...
Is there a better way to search for multiple items that are stored in the collection I get from the user?
OK, I think there are a couple of issues at play here, first I'm presuming you are using Neo4j 1.9 and not 2.0 - hence using the .Start.
Have you tried taking your query and running it in Neo4j? This should be your first port of call, typically it's easy to add a breakpoint on the .Results call and add a 'watch' for query.Query.DebugText.
However, I don't think you need to use the StartBits the way you are, I think you'd be better off filtering with a .Where as you already have the start point:
private static ICypherFluentQuery CreateWhereClause(ICypherFluentQuery query, ICollection<string> instanceNames)
{
query = query.Where((Instance instance) => instance.Name == instanceNames.First());
query = instanceNames.Skip(1).Aggregate(query, (current, localInstanceName) => current.OrWhere((Instance instance) => instance.Name == localInstanceName));
return query;
}
and your query becomes something like:
var prodReference = new NodeReference<Product>(2);
var query =
Client.Cypher
.ParserVersion(1, 9)
.Start(new {product = prodReference})
.Match("(product)-[:HasInstanceRel]->(instance)");
query = CreateWhereClause(query, new[] {"Inst2", "Inst1"});
var resultsQuery = query.Return(instance => instance.As<Node<Instance>>());
2 things of note
We're not using the indexes - there is no benefit to using them as you have the start point and traversing to the 'instances' is a simple process for Neo4j.
The 'CreateWhereClause' method will probably go wrong if you pass in an empty list :)
The nice thing about not using the indexes is that - because they are legacy - you are set up better for Neo4j 2.0

load navigation properties with filter for Entity Framework 4.3

Few days back I put a question regarding mapping two classes Message and MessageStatusHistory using EF. The mapping is going fine but I am facing some problems with the navigation property StatusHistory in class Message that relates it to MessageStatusHistory objects. I am loading the messages for one user only and want to the statuses pertaining to that user only. Like I would want to show if the user has marked message as read/not-read and when. If I use default loading mechanism like following it loads all the history related to the message irrespective of the user:
IDbSet<Message> dbs = _repo.DbSet;
dbs.Include("StatusHistory").Where(x=>x.MessageIdentifier == msgIdentifier);
To filter history for one user only I tried following trick:
IDbSet<Message> dbs = _repo.DbSet;
var q = from m in dbs.Include("StatusHistory")
where m.MessageIdentifier == msgIdentifier
select new Message
{
MessageIdentifier = m.MessageIdentifier,
/*OTHER PROPERTIES*/
StatusHistory = m.StatusHistory
.Where(x => x.UserId == userId).ToList()
};
return q.ToList();//THROWING ERROR ON THIS LINE
I am getting the error:
The entity or complex type 'MyLib.Biz.Message' cannot be constructed in a LINQ
to Entities query.
I have tried by commenting StatusHistory = m.StatusHistory.Where(x => x.UserId == userId).ToList() also but it has not helped.
Please help me in getting Messages with filtered StatusHistory.
EDIT:- above is resolved with this code:
var q = from m in _repository.DBSet.Include("Histories")
where m.MessageIdentifier == id
select new {
m.Id,/*OTHER PROPERTIES*/
Histories = m.Histories.Where(x =>
x.SenderId == userId).ToList()
};
var lst = q.ToList();
return lst.Select(m => new Message{
Id = m.Id, MessageIdentifier = m.MessageIdentifier,
MessageText = m.MessageText, Replies = m.Replies,
ReplyTo = m.ReplyTo, Histories = m.Histories, SenderId =
m.SenderId, SenderName = m.SenderName, CreatedOn = m.CreatedOn
}).ToList();
But if I try to include replies to the message with:
from m in _repository.DBSet.Include("Replies").Include("Histories")
I am getting error on converting query to List with q.ToList() for Histories = m.Histories.Where(x=> x.SenderId == userId).ToList().
About your EDIT part: You cannot use ToList() in a projection, just leave it an IEnumerable<T> and convert to a List<T> when you construct the Message. You also don't need to create two list objects, you can switch from the LINQ to Entities query to LINQ to Objects (the second Select) by using AsEnumerable():
var list = (from m in _repository.DBSet
where m.MessageIdentifier == id
select new {
// ...
Histories = m.Histories.Where(x => x.SenderId == userId)
})
.AsEnumerable() // database query is executed here
.Select(m => new Message {
// ...
Histories = m.Histories.ToList(),
// ...
}).ToList();
return list;
Be aware that Include has no effect when you use a projection with select. You need to make the properties that you want to include part of the projection - as you already did with select new { Histories.....

How to retrieve TFS2010 projects from specific collection

I'm looking around for a good example to work with TFS 2010 collections,projects, and workitems to start with.
I am able to iterate through collections and Projects using the following code (thanks to original coder)
Dim tfsServer As String = "http://test.domain.com:8080/tfs"
tfsServer = tfsServer.Trim()
Dim tfsUri As Uri
tfsUri = New Uri(tfsServer)
Dim configurationServer As New TfsConfigurationServer(tfsUri)
configurationServer = TfsConfigurationServerFactory.GetConfigurationServer(tfsUri)
' Get the catalog of team project collections
Dim collectionNodes As ReadOnlyCollection(Of CatalogNode)
Dim gVar As Guid() = New Guid() {CatalogResourceTypes.ProjectCollection}
collectionNodes = configurationServer.CatalogNode.QueryChildren(gVar, False, CatalogQueryOptions.None)
Dim strName As New StringBuilder
Dim strCollection As New StringBuilder
For Each collectionNode In collectionNodes
Dim collectionId As Guid = New Guid(collectionNode.Resource.Properties("InstanceID"))
strName.Length = 0
Dim teamProjectCollection As New TfsTeamProjectCollection(tfsUri)
teamProjectCollection = configurationServer.GetTeamProjectCollection(collectionId)
Response.Write("Collection:" & teamProjectCollection.Name & "<br/>")
' Get a catalog of team projects for the collection
Dim hVar As Guid() = New Guid() {CatalogResourceTypes.TeamProject}
Dim projectNodes As ReadOnlyCollection(Of CatalogNode)
projectNodes = collectionNode.QueryChildren(hVar, False, CatalogQueryOptions.None)
' List the team projects in the collection
For Each projectNode In projectNodes
strName.AppendLine(projectNode.Resource.DisplayName & "<br>")
'System.Console.WriteLine(" Team Project: " + projectNode.Resource.DisplayName)
Next
Response.Write(strName.ToString())
Next
I want to read specific project from a collection and iterate through workitems (tasks,bugs,issues,etc). Any help would be highly appreciated.
Thanks.
You can run any query you like in the teamProjectCollection - level with:
WorkItemStore workItemStore = (WorkItemStore)teamProjectCollection.GetService(typeof(WorkItemStore));
WorkItemCollection queryResults = workItemStore.Query(query);
foreach (WorkItem workitem in queryResults)
{
Console.WriteLine(workitem.Title);
}
Now you only have to formulate the query - string in something that provides you with what you need.
Queries are WIQL - like. This very basic can give you all work items within a TeamProject:
SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AssignedTo], [System.State] FROM WorkItems WHERE [System.TeamProject] = #project
#project is in our case here the projectNode.Resource.DisplayName
(You can save any query you 've graphically set in TFS with 'Save as' as a *.wiq file & then use it's content programmatically)

Resources