Paul Selles
Computers and cats
Category Archives: .NET
Renaming File with Team Foundation Server API: Extending Workspace to Rename a Large Group of Files
January 29, 2014
Posted by on The Problem
We have TFS check-in policies that will enforce naming conventions for certain files. The policy will allow the developer to programmatically rename any files that do not match the acceptable naming convention. Behind the scene the renaming is done using the WorkSpace.PendRename method [1]. This policy works fine for one or two files but for a large number of files we start having performance issues. Anyone who has experience renaming multiple files on TFS knows that it is a painfully slow ordeal. So how can we speed it up.
The Solution
I have solved this issue through the use of extending methods in the Microsoft.TeamFoundation.VersionControl.Client.Workspace class, and incorporating Task Parallelism [2][3][4]. Since the original Workspace.PendRename returns the number of files that it has renamed as an Int32, we want to make sure that we preserve that functionality. I have only created a solution for the Workspace.PendRename(string,string) case since this is the only variation of the method that I am currently using; to expand this to all Workspace.PendRename implementations is trivial.
This the extended method:
public static class WorkSpaceExtension { // Extend PendRename(String, String) public static int PendRename(this Workspace workspace, string[] oldPaths, string[] newPaths) { // Make sure that the oldPath and new Paths match if (oldPaths.Count() != newPaths.Count()) throw new ArgumentException("Every oldPath must have corresponding newPath"); // This list will contain all our tasks List pendRenameTasks = new List(); // Loop throuh all newPath and oldPath pairs for (int i = 0; i < oldPaths.Count(); i++) { // Add each new task in out task container pendRenameTasks.Add(Task.Factory.StartNew((Object obj) => { // We are going to pass the current old path and the // current new path into the path as an anonymous type var path = (dynamic)obj; return workspace.PendRename(path.oldPath, path.newPath); }, new { oldPath = oldPaths[i], newPath = newPaths[i] })); } // Wait for all tasks to complete Task.WaitAll(pendRenameTasks.ToArray()); // Sum up the result of all the original PendRename method int taskCount=0; foreach (Task pendRenameTask in pendRenameTasks) { taskCount += pendRenameTask.Result; } // Return the number of file names changed return taskCount; } }
References
[1] Workspace.PendRename Method. MSDN Library.
[2] Extension Methods (C# Programming Guide). MSDN Library.
[3] WorkSpace Class. MSDN Library.
[4] Extension Methods (C# Programming Guide). MSDN Library.
Team Foundation Server API: Programmatically Downloading Files From Source Control
January 8, 2014
Posted by on For several of my custom check-in polices, I need to compare the developer’s local copy to a server copy of the same file. The challenge then becomes how to I find and download a file from source control programmatically using the Team Foundation Server API.
Finding the Server Items in Source Control
The first thing that you need to do is find file on the source control server that we want to download, all our searches will return an Item object or an ItemSet object (I will call this object our Server Item) [1][2]. This Server Item can be found multiple ways for example, the Item ID, the Server Item path, the Local Item Path, the Changeset Number, the Changeset Owner, or a Date Range.
Server Item Path Within Custom Check-in Policies
Within my custom check-in policy the path and ID of our Server Item can be easily found from the IPendingCheckin Interface, within the PendingChange array IPendingCheckinPendingChanges.CheckedPendingChanges Property (IPendingCheckin.PendingChanges.CheckedPendingChanges) [3][4][5]. Each CheckedPendingChanges will reference a PendingChange object that is associated with each checked out, included item. The PendingChange.ServerItem Property gives us the path to our Server Item and the PendingChange.ItemId Property gives us the Server Item ID [6][7].
Creating an Instance of VersionControlServer
All our Server Item Queries are done using VersionControlServer object and all methods mentioned below are fromthe VersionControlServer class [8]. First we need an instance of VersionControlServer:
string teamProjectCollectionUrl = "https://YourTfsUrl.com/tfs/YourTfsProjectCollection"; TfsTeamProjectCollection teamProjectCollection = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri(teamProjectCollectionUrl)); VersionControlServer versionControlServer = teamProjectCollection.GetService<VersionControlServer>();
GetItem Method
The VersionControlServer.GetItem Method have several options, but the first three are the ones that I most commonly use [9]. They are:
- GetItem(String) //Get single Item from Local Item or Server Item path for the latest version
- GetItem(Int32, Int32) // Get single Item from item ID and Changeset Number
- GetItem(String, VersionSpec) // get single Item from Local Item or Server Item path which matches the given VersionSpec [10]
Examples of usage:
// Get the latest Item for local path "C:\projects\myfiles.cs" Item item1 = versionControlServer.GetItem("C:\projects\myfiles.cs"); // Get ItemId = 12345 for changesetId = 54321 Item item2 = versionControlServer.GetItem(12345,54321); // Get the latest Item for server path "$/ProjectName/myfile.cs" Item item1 = versionControlServer.GetItem("$/ProjectName/myfile.cs", VersionSpec.Latest);
GetItems Method
The VersionControlServer.GetItems methods are very similar to the GetItem methods with the exception that it will return a collection of Server Items [11]. The most useful GetItems methods I have found, are those that provide a RecursionType option to get a collection Server Items within a directory structure [12].
QueryHistory Method
The added benefit of using VersionControlServer.QueryHistory Method is to use the QueryHistoryParameters [13][14]. QueryHistoryParameters allows for substantial customization of our search parameters, for example, if we wanted to download the the content of a project file a the time of a given Changeset that is not associated with the project file, we could. Since the project file in this example is not referenced to that particular Changeset, we will have to perform our query based on the DateTime of the Changeset.
// Example ChangsetNumber and ServerItemPath int changesetNumber = 12345; string serverItemPath = "$/ProjectName/myproject.csproj"; // Get the changeset for a given Changeset Number Changeset changeset = versionControlServer.GetChangeset(changesetNumber); // Create a queryHistoryParameters class based on our desired project file server path QueryHistoryParameters queryHistoryParameters = mew QueryHistoryParameters(serverItemPath,RecursionType.None); // We want to sort descending queryHistoryParameters.SortAscending = false; // Interested only one result queryHistoryParameters.MaxResults = 1; // Set the version end date to that of the changeset date queryHistoryParameters.VersionEnd = new DateVersionSpec(changeset.CreationDate); // Perform Query ItemSet items = versionControlServer.QueryHistory(queryHistoryParameters);
Downloading the Sever Item Content
Once we have the specific Item or ItemSet of interest it’s just a matter of downloading the file contents locally. This process is quite simple and my methods make uses of the Stream and MemoryStream [15][16]. After some difficulties with the downloaded file’s encoding I use StreamReader to download the Server Item to a string [17]. Here is the method I use to retrieve the Server Item as a byte array:
public byte[] GetFileByteArray(Item item) { // create a container byte[] content; // Download file into stream using (Stream stream = item.DownloadFile()) { // Use MemoryStream to copy steam into a byte array using (MemoryStream memoryStream = new MemoryStream()) { stream.CopyTo(memoryStream); content = memoryStream.ToArray(); } } // return byte array return content; }
Here is the method I use to retrieve the Server Item as a string:
public string GetFileString(Item item) { // Setup string container string content = string.Empty; // Download file into stream using (Stream stream = item.DownloadFile()) { // Use MemoryStream to copy downloaded Stream using (MemoryStream memoryStream = new MemoryStream()) { stream.CopyTo(memoryStream); // Use StreamReader to read MemoryStream created from byte array using (StreamReader streamReader = new StreamReader(new MemoryStream(memoryStream.ToArray()))) { content = streamReader.ReadToEnd(); } } } // return string return content ; }
Putting it all Together
With our new found knowledge we can create a simple console application that will retrieve the latest version for a given file.
using System; using Microsoft.TeamFoundation.Client; using Microsoft.TeamFoundation.VersionControl.Client; using System.IO; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { string teamProjectCollectionUrl = "https://YourTfsUrl/tfs/YourTeamProjectCollection"; string filePath = @"C:\project\myfile.cs"; // Get the version control server TfsTeamProjectCollection teamProjectCollection = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri(teamProjectCollectionUrl)); VersionControlServer versionControlServer = teamProjectCollection.GetService<VersionControlServer>(); // Get the latest Item for filePath Item item = versionControlServer.GetItem(filePath, VersionSpec.Latest); // Download and display content to console string fileString = string.Empty; using (Stream stream = item.DownloadFile()) { using (MemoryStream memoryStream = new MemoryStream()) { stream.CopyTo(memoryStream); // Use StreamReader to read MemoryStream created from byte array using (StreamReader streamReader = new StreamReader(new MemoryStream(memoryStream.ToArray()))) { fileString = streamReader.ReadToEnd(); } } } Console.WriteLine(fileString); Console.ReadLine(); } } }
Paul
Reference
[1] Item Class. MSDN Library
[2] ItemSet Class. MSDN Library
[3] IPendingCheckin Interface. MSDN Library
[4] PendingChange Class. MSDN Libary
[5] IPendingCheckinPendingChanges.CheckedPendingChanges Property. MSDN Library
[6] PendingChange.ServerItem Property. MSDN Library
[7] PendingChange.ItemId Property. MSDN Library
[8] VersionControlServer Class. MSDN Library
[9] VersionControlServer.GetItem Method. MSDN Library
[10] VersionSpec Class. MSDN Library
[11] VersionControlServer.GetItems Method. MSDN Library
[12] RecursionType Enumeration. MSDN Library
[13] VersionControlServer.QueryHistory Method. MSDN Library
[14] QueryHistoryParameters Class. MSDN Library
[15] Stream Class. MSDN Library
[16] MemoryStream Class. MSDN Library
[17] StreamReader Class. MSDN Library
Threading: Task Parallelism and Task Waiting with EventWaitHandle, ManualResetEvent, and AutoResetEvent
December 20, 2013
Posted by on Introduction to the Parallel Task Library
Since the introduction of Parallelism with the .NET Framework 4, we’ve had access to new run time libraries that give .NET developers control over multithreading in their projects [1]. Working with with Tasks allows the developer concentrate on performing a specific tasks (for the lack of a better word) rather than concentrating on low level worker threads. For .NET developers, this means working with the Parallel Task Library (PTL) where MSDN identifies a task as being
an asynchronous operation. In some ways, a task resembles a thread or ThreadPool work item, but at a higher level of abstraction [2].
Traditional Task Waiting
Here is a really simple Tasking example where we are going to want to print a list in a certain order to the Console [3]. We want the console output to have a certain order, established by a for loop, and a certain hierarchy (modelled after xml). Here is an example of a function using a asynchronous task with no wait handler:
static void NoWaitHandlerExample(int events=1) { // Open Events Console.WriteLine("Start Events"); for (int i = 0; i { // Open Close StartEvent Id=eventId+1 Console.WriteLine("\t\tStart Task: " + (eventId + 1).ToString()); Thread.Sleep(_random.Next(500)); Console.WriteLine("\t\tEnd Task: " + (eventId + 1).ToString()); }); // Close Event Id=eventId+1 Console.WriteLine("\tEnd Event: " + (eventId + 1).ToString()); } // Close Events Console.WriteLine("End Events"); }
If we are to call the function with 5 events, here is a sample of the output:
Start Events
Start Event: 1
End Event: 1
Start Event: 2
End Event: 2
Start Event: 3
End Event: 3
Start Event: 4
End Event: 4
Start Event: 5
Start Task: 1
End Event: 5
End Events
Start Task: 3
Start Task: 4
Start Task: 2
Start Task: 5
End Task: 4
End Task: 1
End Task: 2
End Task: 3
End Task: 5
As you can see neither the order nor the hierarchy are preserved and most errors are caused by tasks starting and ending prior to the main thread (with the exception of the first task which started before the the main thread finished). To fix this we can implement a task waiting solution as outlined in the TPL [2].
static void TaskWaitExample(int events=1) { // Open Events Console.WriteLine("Start Events"); for (int i = 0; i { // Open Close StartEvent Id=eventId+1 Console.WriteLine("\t\tStart Task: " + (eventId + 1).ToString()); Thread.Sleep(_random.Next(500)); Console.WriteLine("\t\tEnd Task: " + (eventId + 1).ToString()); }); // *** Wait for task to complete *** task.Wait(); // Close Event Id=eventId+1 Console.WriteLine("\tEnd Event: " + (eventId + 1).ToString()); } // Close Events Console.WriteLine("End Events");
The only difference is highlighted with *** in the comments which from this point on will represent task waiting events. In the code above, we wait for each task to complete before continuing. As for the results:
Start Events
Start Event: 1
Start Task: 1
End Task: 1
End Event: 1
Start Event: 2
Start Task: 2
End Task: 2
End Event: 2
Start Event: 3
Start Task: 3
End Task: 3
End Event: 3
Start Event: 4
Start Task: 4
End Task: 4
End Event: 4
Start Event: 5
Start Task: 5
End Task: 5
End Event: 5
End Events
The output preserve both order and hierarchy.
Task Waiting Through Event Wait Handling using EventWaitHandle, ManualResetEvent, and AutoResetEvent Classes
A very powerful but sometimes overlooked tool for task wait handling is to use the System.Threading Namespace,Threading Objects EventWaitHandle, ManualResetEvent, and AutoResetEvent [4],[5],[6],[7],[8]. The principle behind these classes is the same, the event object has two basic states signalled or not signalled, for convenience sake I will refer to them as ON or OFF. An OFF state will block the current thread wherever at the WaitOne method. The thread will continue to be blocked until the object calls the Set method, which sets the state to ON. We can then reset the signal back to OFF. The difference between the two EventResetMode (ManualReset and AutoReset) is how the signal is reset [9]. For the AuotReset, the signal is reset to OFF once the thread becomes unblock, this is trigger by calling the Set method and the unblocking WaitOne, if there are not waiting threads, the signal will remain ON until Reset is called. For the ManualReset, the signal remains OFF until the Reset method is called. The MSDN entry of the EventWaitHandle implementation of methods Set, WaitOne, and Reset should provides more details on thread signalling [6],[10],[11],[12]. WaitOne methods come in different flavours that allow for more functionality such as timeouts.
We can rewrite our program to use the ManuaResetEvent Class:
static void ManualResetEventExample(int events=1) { // *** New reset event unsignaled by default ManualResetEvent manualResetEvent = new ManualResetEvent(false); // Open Events Console.WriteLine("Start Events"); for (int i = 0; i { // Open Close StartEvent Id=eventId+1 Console.WriteLine("\t\tStart Task: " + (eventId + 1).ToString()); Thread.Sleep(_random.Next(500)); Console.WriteLine("\t\tEnd Task: " + (eventId + 1).ToString()); // *** Set: Sets ManualResetEvent to signaled *** manualResetEvent.Set(); }); // *** WaitOne: Block thread until ManualResetEvent is signalled *** manualResetEvent.WaitOne(); // Close Event Id=eventId+1 Console.WriteLine("\tEnd Event: " + (eventId + 1).ToString()); // *** Reset: Set state to not signalled *** manualResetEvent.Reset(); } // Close Events Console.WriteLine("End Events"); }
and the AutoResetEvent Class:
static void AutoResetEventExample(int events=1) { // *** New reset event unsignaled by default AutoResetEvent autoResetEvent = new AutoResetEvent(false); // Open Events Console.WriteLine("Start Events"); for (int i = 0; i { // Open Close StartEvent Id=eventId+1 Console.WriteLine("\t\tStart Task: " + (eventId + 1).ToString()); Thread.Sleep(_random.Next(500)); Console.WriteLine("\t\tEnd Task: " + (eventId + 1).ToString()); // *** Set: Sets AutoResetEvent to signalled *** autoResetEvent.Set(); }); // *** WaitOne: Block thread until AutoResetEvent is signaled and clear signalled state *** autoResetEvent.WaitOne(); // Close Event Id=eventId+1 Console.WriteLine("\tEnd Event: " + (eventId + 1).ToString()); } // Close Events Console.WriteLine("End Events"); }
The EventWaitHandle becomes trivial as we state if the reset is manual or automatic in with the EventResetMode enumerator. We can reuse the code above with the following constructors.
// *** New reset event unsignaled by default and set for a manual EventResetMode EventWaitHandle manualResetEvent = new EventWaitHandle(false, EventResetMode.ManualReset); // *** New reset event unsignaled by default and set for an automatic EventResetMode EventWaitHandle autoResetEvent = new EventWaitHandle(false, EventResetMode.AutolReset);
For short wait times where performance is a concerned and are not planning on using all the features that come with ManualResetEvent Class, you can use ManualResetEventSlim [13]. ManualResetEventSlim uses busy spinning while waiting for an event to become signalled. After a certain time limit ManualResetEventSlim will revert to work like ManualResetEvent class.
Server Example
Now that we are more comfortable with using these wait events we can start working on a real world example. For this example we will be creating a simple TCP server that will accept a connection, read a string, print the string it to the Console, and reverse the string, then finally return it to the caller. The class is an extension of an example provided by the MSDN Library under the NetworkStream.BeginRead Method [14].
Now without further ado, here is our simple server example making full use of the ManualResetEvent class:
public class ManualResetEventTcpServer { // *** Listener wait handler, initial state is Initial State is notsignales (false) private static ManualResetEvent ManualResetEvent = new ManualResetEvent(false); // Buffer length private int _bufferLength = 2048; // Will be access by multiple threads private bool _running; // Start listener loop public void Start() { _running = true; TcpListener listener = new TcpListener(IPAddress.Any, 3000); listener.Start(); try { while (_running) { // *** Reset to unsignalled ManualResetEvent.Reset(); listener.BeginAcceptTcpClient( ListenerCallback, listener); // *** Block until signalled ManualResetEvent.WaitOne(); } } catch {} } // Stop listener loop private void Stop() { _running = false; } // Async BeginAcceptTcpClient listener callback private void ListenerCallback(IAsyncResult ar) { try { // *** Signal ManualResetEvent.Set(); // Get listener and end async AcceptTcpClient TcpListener listener = ar.AsyncState as TcpListener; TcpClient client = listener.EndAcceptTcpClient(ar); using (NetworkStream stream = client.GetStream()) { // Read message string message = ReadMessage(stream); // Write to console Console.WriteLine(message); // Reverse and return message WriteMessage(stream, new string(message.ToArray().Reverse().ToArray())); } } catch {} } // Read message private string ReadMessage(NetworkStream networkStream) { // Incoming buffer and message string byte[] buffer = new byte[_bufferLength]; string message = string.Empty; // Loop through the entire stream in case the message is bigger than our buffer while (networkStream.DataAvailable) { int bytesRead = networkStream.Read(buffer, 0, _bufferLength); message += Encoding.Default.GetString(buffer, 0, bytesRead); } return message; } // Return message private void WriteMessage(NetworkStream networkStream, string message) { // Incoming buffer and message string byte[] buffer = Encoding.UTF8.GetBytes(message); networkStream.Write(buffer, 0, buffer.Length); } }
This class can be attached to a Console Application where Start and Stop will start and stop the listener. The server is currently configured to listen on port 3000, find a port that works with you.
Looking though the class above your attention should be focused on both the Start function and the ListenerCallbackFunction, as they are the only two function using our wait handler. Within the listener we are looping over the TcpListener Class asynchronous method BeginAcceptTcpClient [15],[14]. We want to be able to handle multiple Tcp clients but we don’t want to be spinning in the loop, so before we begin to accept a client we will Reset the wait handler to an OFF state. The asynchronous method BeginAcceptTcpClient is then called and the main thread is blocked at WaitOne. Once our listener picks up a client the callback function will be called and the wait handler will be set to the ON state, removing the block at WaitOne, and the main thread can continue through the while loop.
Of course this is no fun without a client so to mix things up here is our Powershell client:
#Simple Tcp Client Set-StrictMode -Version Latest ## Message as argurment $Message = "$Args" ## Create message buffer $BufferOut = [System.Text.Encoding]::UTF8.GetBytes($Message); ## Address and port $Address = "PAULS-W530-W8.iQmetrixHO.local" $DnsAddress = [System.Net.Dns]::GetHostAddresses($Address) $Port = 3000 ## Connect Tcp Client $Client = New-Object System.Net.Sockets.TcpClient $Client.Connect($DnsAddress,$Port) ## Get stream and write message buffer $Stream = $Client.GetStream() $Stream.Write($BufferOut, 0, $BufferOut.Length) $Stream.Flush() ## Setup input buffer $BufferInLength = 1024 $BufferIn = New-Object byte[] $BufferInLength $BufferIndex = 0 ## Set a read date timeout $TimeOut = [TimeSpan]"00:00:15" $StartTime = Get-Date do { if ($Stream.CanRead -and $Stream.DataAvailable) { break } } while (((Get-Date) - $StartTime) -le $TimeOut) if (!($Stream.CanRead -and $Stream.DataAvailable)) { Write-Error "Timeout" } ## Read data while ($Stream.DataAvailable) { $BytesRead = $Stream.Read($BufferIn, 0, $BufferInLength) $Message = [System.Text.Encoding]::UTF8.GetString($BufferIn, 0, $BytesRead) Write-Host $Message -NoNewline } Write-Host ""
Paul
References
[1] Parallel Programming in the .NET Framework. MSDN Library
[2] Task Parallelism (Task Parallel Library). MSDN Library
[3] Console Class. MSDN Library
[4] System.Threading Namespace. MSDN Library
[5] EventWaitHandle, AutoResetEvent, CountdownEvent, ManualResetEvent. MSDN Library
[6] EventWaitHandle Class. MSDN Library
[7] ManualResetEvent Class. MSDN Library
[8] AutoResetEvent Class. MSDN Library
[9] EventResetMode Enumeration. MSDN Library
[10] EventWaitHandle.Set. MSDN Library
[11] EventWaitHandle.WaitOne. MSDN Library
[12] EventWaitHandle.Reset. MSDN Library
[13] ManualResetEventSlim Class. MSDN Library
[14] NetworkStream.BeginRead Method. MSDN Library
[15] TcpListener Class. MSDN Library
TFS 2012: WorkItemChangedEventHandler and where is WorkItemChangedEvent
December 4, 2013
Posted by on I am creating a custom action for our TFS2012 Work Item for State Transitions. I found a multitude of resources on how the get the ball rolling create a WorkItemChangedEventHandler class [1]; but I quickly hit a snag. I could not find WorkItemChangedEvent for the life of me. I was expecting to find WorkItemChangedEvent in the assembly Microsoft.TeamFoundation.WorkItemTracking.Server but due to a poorly documented reason there are some dependencies with the assembly Microsoft.TeamFoundation.WorkItemTracking.Server.Dataaccesslayer. Both assemblies must be present to access any members of Microsoft.TeamFoundation.WorkItemTracking.Server.
Both of these assemblies can be found on the TFS2012 Server machine under the path %ProgramFiles%\Microsoft Team Foundation Server 11.0\Application Tier\Web Services\bin.
Reference
[1] Jakob Ehn. Developing and debugging Server Side Event Handlers in TFS 2010. 23-Jan-2012
Tfs Build: Query Build Table for historical Build Status, Compilation Status, and Test Status
November 20, 2013
Posted by on If you are having trouble finding historical build data using the TFS API, build information can be found in your Team Foundation Server Databases [1]. The table Tbl_Build keeps a log of all your historical builds including, build number, start and stop times, controller id, drop location, and statuses. This can be and extremely powerful tool to look up Build Id values to look up historic build logs which contain detailed reports (as covered in Tfs Build Log: Querying Build Log Data).
You can find the Tbl_Build table within your Collection database (Tfs_YourTeamProjectCollection, where YourTeamProjectCollection is the name of your Team Project Collection.) The Status columns that we are interested in are BuildStatus, CompilationStatus, and TestStatus as seen in the SQl Server Management Studio screen capture below:
The BuildStatus column values map to the Microsoft.TeamFoundation.Build.Client BuildStatus Enumeration [2]. Here is the Database specific table:
BuildStatus Description 1 InProgress 2 Succeeded 4 PartiallySucceeded 8 Failed 16 Stopped
The CompliationStatus and TestStatus columns are trickier to deal with than BuildStatus column. Where the BuildStatus is pretty well self explanatory, both the CompliationStatus and TestStatus are heavily based on user definitions within the Build Process Template. That is to say that a Complitation/Test failure may not mean what you think that it means out of the box. Please review your Build Process Template and see how the CompliationStatus and TestStatus are set.
Moving forward, both CompliationStatus and TestStatus columns map to the Microsoft.TeamFoundation.Build.Client BuildPhaseStatus Enumeration [3]. Here are the Database specific table:
CompliationStatus Description 1 Failed 2 Succeeded NULL Unknown
TestStatus Description 1 Failed 2 Succeeded NULL Unknown
We can now query the database for historical build information, for example we can look for all failed builds between Jan 5th and Jan 18th of this year:
SELECT * FROM [Tfs_YouTeamProjectCollection].[dbo].[Tbl_Build] WHERE [BuildStatus]=8 AND [StartTime] BETWEEN '1/5/2013 12:00:00 AM' AND '1/18/2013 12:00:00 AM'
Be sure to check this out my previous post Tfs Build Log: Querying Build Log Data to learn how to programmatically read historical build data.
References
[1] Team Foundation Server Databases. MSDN Library.
[2] BuildStatus Enumeration. MSDN Libary.
[3] BuildPhaseStatus Enumeration. MSDN Libary.
Recent Comments