Se utilizzate le query di TFS per monitorare l’andamento di uno sprint vi sarete trovati nella condizione di dover modificare l’IterationPath al cambio dello sprint corrente. Questa noiosa attività può essere evitata con qualche riga di codice e l’aiuto delle API di TFS.

Prima di tutto aggiungiamo come referenze le seguenti librerie al progetto:

Microsoft.TeamFoundation.Client;
Microsoft.TeamFoundation.Common;
Microsoft.TeamFoundation.ProjectManagement;
Microsoft.TeamFoundation.WorkItemTracking.Client;

Se non le trovate tra gli assembly del framework cercatele nella seguente cartella di Visual Studio: C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\ReferenceAssemblies\v2.0\.

Poi ci colleghiamo alla collection:

Uri uri = new Uri("http://localhost:8080/tfs/DefaultCollection");
var collection = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(uri);

Recuperiamo le informazioni sulla struttura dei progetti presenti nella collection attraverso il servizio ICommonStructureService:

var service = collection.GetService();
ProjectInfo[] projects = service.ListAllProjects();

Il servizio ICommonStructureService non è l’unico modo per conoscere le iterazioni di un progetto, ma è il solo che ci permette di recuperare le date di inizio e di fine sprint indispensabili per capire qual è lo sprint corrente.

Con il metodo ListStructures otteniamo le aree e le iterazioni del progetto come struttura di nodi. Da questa struttura prendiamo il nodo principale delle iterazioni e attraverso il metodo GetNodesXml portiamo il tutto in un documento xml che possiamo poi ispezionare con LINQ:

NodeInfo root = service.ListStructures(pi.Uri)
                       .Where(node => node.Name.Equals(Iteration.Type))
                       .FirstOrDefault();
var rootNode = service.GetNodesXml(new string[] { root.Uri }, true);
XDocument doc = XDocument.Parse(rootNode.OuterXml);

Tra tutti i nodi prendiamo quelli che non hanno figli (ovvero gli sprint):

XElement[] nodes = doc.Root.Descendants()
                           .Where(x => !x.Descendants().Any()).ToArray();

Per ogni nodo carichiamo le informazioni in una lista:

Iterations iterations = new Iterations();
Array.ForEach(nodes, node => iterations.Add(node));

Per rendere più agevole l’operazione ho creato una classe Iteration contente tutte le informazioni di uno sprint:

public class Iteration
{
    public static string Type = "Iteration";

    public string Uri { get; set; }

    public string Name { get; set; }

    public string Path { get; set; }

    public DateTime? StartDate { get; set; }

    public DateTime? FinishDate { get; set; }

    public static implicit operator Iteration(XElement element)
    {
        string path = element.Attribute("Path").Value;
        path = path.Replace(@"\\", @"\").Replace(@"Iteration\"", string.Empty) 
                                        .Remove(0,1);

        return new Iteration()
        {
            StartDate = Convert.ToDateTime(element.Attribute("StartDate").Value),
            FinishDate = Convert.ToDateTime(element.Attribute("FinishDate").Value),
            Name = element.Attribute("Name").Value,
            Path = path,
            Uri = element.Attribute("NodeID").Value
        };
    }
}

… e una classe Iterations, come lista di Iteration, che fornisce dei metodi per recuperare lo sprint corrente e quelli passati:

public class Iterations : List
{
    public Iteration[] Past()
    {
        return (from i in this
                where i.FinishDate.HasValue && 
                      DateTime.Compare(i.FinishDate.Value, DateTime.UtcNow) <= 0
                select i).ToArray();
    }

    public Iteration Current
    {
        get
        {
            DateTime now = DateTime.UtcNow;
            var current = from i in this
                          where (i.StartDate.HasValue && i.FinishDate.HasValue) &&
                                 DateTime.Compare(i.StartDate.Value, now) < 0 && 
                                 DateTime.Compare(i.FinishDate.Value, now) >= 0 
                          select i;
            return current.FirstOrDefault();
        }    
     }

     public bool ContainsPath(string path)
     {
        return this.Any(x => x.Path.Equals(path));
     }
}

Attraverso il WorkItemStore recuperiamo le query di ogni progetto e per ogni query andiamo a sostituire l’iterazione passata con la corrente:

var wiStore = collection.GetService();
Project project = wiStore.Projects[pi.Name];

bool saveChanges = true;
try
{
  foreach (QueryDefinition query in project.AllQueries())
  {
    foreach (var pastIteration in iterations.Past())
    {
      if (query.QueryText.Contains(pastIteration.Path))
      {
        query.QueryText = query.QueryText
                               .Replace(pastIteration.Path, iterations.Current.Path);
        break;
      }
    }
  }
}
catch (Exception ex)
{
  saveChanges = false;
  Console.WriteLine("Error: {0}", ex.Message);
}

Il metodo AllQueries è un extension method che recupera tutte le query di un progetto come mostrato da Ewald Hofman in questo articolo. Se tutto è andato secondo i piani possiamo salvare le modifiche alle query attraverso il metodo Save dell’oggetto QueryHierarchy:

if (saveChanges)
{
    project.QueryHierarchy.Save();
}

Come ulteriore conferma della correttezza dell’operazione possiamo eseguire la query sullo store. Ecco come fare:

private static void ExecuteQuery(QueryDefinition query)
{
    var store = query.Project.Store;
    var text = query.QueryText.Replace("@project", string.Format("\"{0}\"", query.Project.Name));
    var queryToExecute = new Query(store, text);

    Console.WriteLine("Query: {0}", query.Name);

    if (queryToExecute.IsLinkQuery)
    {
        var results = queryToExecute.RunLinkQuery();
        foreach (WorkItemLinkInfo result in results)
        {
            WorkItem item = store.GetWorkItem(result.TargetId);
            if (item != null)
            {
                Console.WriteLine("\t{0}", item.Title);
            }
        }
    }
    else
    {
        var results = queryToExecute.RunQuery();
        foreach (WorkItem result in results)
        {
            Console.WriteLine("\t{0}", result.Title);
        }
    }
}

Nella sezione materiale eventi del sito di 1nn0va, sotto la cartella ALM Saturday, trovate la solution di Visual Studio completa.