tech·nic·al·ly agile

Log Elmah errors in Team Foundation Server

Learn how to log Elmah errors in Team Foundation Server effectively. Discover methods to streamline error tracking and enhance your development process.

Published on
17 minute read
Image
https://nkdagility.com/resources/FGTZV3eWHR9

I am not sure if this is a good idea, but I was bored one day and decided to add a TFS Error Log provider for Elmah  . There are 2 ways you can do this. You can create a new WorkItem type and log an error report for each of the errors or you can create one work item for each error type/title. To do this you can create a title that is the combination of error message and application name and then search TFS for an existing work item. If it exists then add the error to it, if it does not then create a work item for that instance. You can use any work item type, and the errors are added as Elmah  xml log files.

There are a number of things you need to override when you inherit from Elmah  .ErrorLog. The first is the Log method.

 1''' <summary>
 2''' Logs the error as an attachment to an existing work item, or adds a new work item if this error has not occurred.
 3''' </summary>
 4''' <param name="error">The error to be logged</param>
 5''' <returns>The ID of the error</returns>
 6''' <remarks></remarks>
 7Public Overrides Function Log(ByVal [error] As [Error]) As String
 8    Dim errorId = Guid.NewGuid().ToString()
 9    Dim timeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddHHmmssZ", CultureInfo.InvariantCulture)
10    Dim Filename = String.Format("error-{0}-{1}.elmah", timeStamp, errorId)
11    Dim temp = System.IO.Path.Combine(".", Filename)
12    ' Temp Log to disk
13    Using writer = New XmlTextWriter(temp, Encoding.UTF8)
14        writer.Formatting = Formatting.Indented
15        writer.WriteStartElement("error")
16        writer.WriteAttributeString("errorId", errorId)
17        ErrorXml.Encode([error], writer)
18        writer.WriteEndElement()
19        writer.Flush()
20    End Using
21
22    Dim Title As String = String.Format("{0}-{1}", [error].ApplicationName, [error].Message)
23
24    Dim wi As WorkItem = GetWorkItemForException(Title, [error])
25
26    Dim a As New Attachment(temp, "Elmah error log")
27
28    wi.Attachments.Add(a)
29    If wi.IsValid Then
30        wi.Save()
31        Return String.Format("{0}|{1}", wi.Id, errorId.ToString)
32    Else
33        Dim message As New System.Text.StringBuilder
34        Dim results = wi.Validate()
35        Dim isFirst As Boolean = True
36        For Each r In results
37            message.AppendLine(String.Format(IIf(isFirst, "{0}", ", {0}"), r))
38            isFirst = False
39        Next
40        Throw New ApplicationException(String.Format("Unable to save the work item because the following fields produced a validation error '{0}'.", message.ToString))
41    End If
42End Function

The idea is that you attach the Elmah  log file to the work item with a . elmah  extension. This will allow us to find all the error logs in the future. So we create the temporary log file, and then create our key/title for our work item. You can customize this by customizing your exception messages on the server side. We then get our work item, and add the file as an attachment.

Because I am doing this the quick and dirty way, i.e. just for fun, I have utilised the API’s provided in the Templates add-on for the Power Tools to customize the work items. So when we are creating the Work item:

1Protected Function GetWorkItemForException(ByVal Title As String, ByVal [error] As [Error]) As WorkItem
2    Dim wi As WorkItem = GetExistingWorkItem(Title)
3    If wi Is Nothing Then
4        wi = CreateNewWorkItem(Title)
5    End If
6    m_TemplateDefault.Fields.ApplyFieldValues(wi, False)
7    ApplyErrorFieldValues(wi, [error])
8    Return wi
9End Function

So, we either get an existing work item, or we create a new one, but then we need to apply some values to the work item. In the constructor of the class Elmah  passes an IDictionary object that we use to pass the template names.

 1Public Sub New(ByVal config As IDictionary)
 2    If config Is Nothing Then
 3        Throw New ArgumentNullException("config")
 4    End If
 5    sm_Config = config
 6
 7    Dim store As ITemplateStore = GetStore()
 8    m_TemplateDefault = GetTemplate("Defaults", store)
 9    m_TemplateErrorMap = GetTemplate("ErrorMap", store)
10
11    If m_TemplateDefault Is Nothing Or m_TemplateErrorMap Is Nothing Then
12        Throw New ApplicationException("Unable to load the templates from the store.")
13    End If
14
15End Sub

I created a Store (Microsoft.TeamFoundation.PowerTools.Client.WorkItemTracking.Templates.ITemplateStore) for the templates and attempt to load both a “defaults” template and a dynamic “mapping” template. The latter will need some special mapping, but as you can see from the GetWorkItemForException there is already a method on the Template object to Apply all of the values to a work item. Here is an example default template:

 1<?xml version="1.0"?>
 2<Template xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 3  <FieldValues>
 4    <FieldValue>
 5      <ReferenceName>System.AreaPath</ReferenceName>
 6      <Value>TestProject1TestArea1</Value>
 7    </FieldValue>
 8    <FieldValue>
 9      <ReferenceName>System.IterationPath</ReferenceName>
10      <Value>TestProject1TestIteration1</Value>
11    </FieldValue>
12    <FieldValue>
13      <ReferenceName>System.AssignedTo</ReferenceName>
14      <Value>Martin Hinshelwood</Value>
15    </FieldValue>
16    <FieldValue>
17      <ReferenceName>Microsoft.VSTS.CMMI.FoundInEnvironment</ReferenceName>
18      <Value>DEV</Value>
19    </FieldValue>
20    <FieldValue>
21      <ReferenceName>Microsoft.VSTS.Build.FoundIn</ReferenceName>
22      <Value>Build_v1.13_20090312.1</Value>
23    </FieldValue>
24  </FieldValues>
25  <WorkItemTypeName>Bug</WorkItemTypeName>
26  <TeamServerUri>http://tfs01.company.biz:8080/</TeamServerUri>
27  <TeamProjectName>TestProject1</TeamProjectName>
28  <Description />
29</Template>

These values are now mapped onto the work item. But what about any dynamic values that we want to use from the Error. I added a second template called “ErrorMap” that will use the same format, but use something like:

 1<?xml version="1.0"?>
 2<Template xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 3  <FieldValues>
 4    <FieldValue>
 5      <ReferenceName>System.AreaPath</ReferenceName>
 6      <Value>TestProject1{ApplicationName}</Value>
 7    </FieldValue>
 8    <FieldValue>
 9      <ReferenceName>System.Description</ReferenceName>
10      <Value>{WebHostHtmlMessage}</Value>
11    </FieldValue>
12    <FieldValue>
13      <ReferenceName>Company.Custom.MethodName</ReferenceName>
14      <Value>{Exception.TargetSite.Name}</Value>
15    </FieldValue>
16  </FieldValues>
17  <WorkItemTypeName>Bug</WorkItemTypeName>
18  <TeamServerUri>http://tfs01.company.biz:8080/</TeamServerUri>
19  <TeamProjectName>TestProject1</TeamProjectName>
20  <Description />
21</Template>

We can then apply those values with a little reflection by parsing out the value and applying the retrieved object values to the work item.

 1Private Sub ApplyErrorFieldValues(ByVal wi As WorkItem, ByVal [error] As [Error])
 2    For Each i In m_TemplateErrorMap.Fields
 3        Dim value As String = GetPropertyValue(i.Value, [error])
 4        If wi.Fields(i.ReferenceName).AllowedValues.Contains(value) Then
 5            wi.Fields(i.ReferenceName).Value = value
 6        Else
 7            Throw New ApplicationException(String.Format("Unable to set the work item field '{0}' to '{1}' as '{1}' is not in the Allowed Values list.", i.ReferenceName, value))
 8        End If
 9    Next
10End Sub
11
12Private Function GetPropertyValue(ByVal path As String, ByVal target As Object) As String
13    Dim bits() As String = path.Split(".")
14    Dim ll As New LinkedList(Of String)
15    Array.ForEach(bits, Function(b) ll.AddLast(b))
16    Return GetPropertyRecurse(ll.First, target)
17End Function
18
19Private Function GetPropertyRecurse(ByVal node As LinkedListNode(Of String), ByVal target As Object) As String
20    ' ToDo: add ability to support propertyName(0) [arrays]
21    Dim r As System.Reflection.PropertyInfo = target.GetType.GetProperty(node.Value, BindingFlags.Static Or BindingFlags.Public Or BindingFlags.GetField Or BindingFlags.GetProperty)
22    If r.PropertyType.IsClass And Not node.Next Is Nothing Then
23        Return GetPropertyRecurse(node.Next, r.GetValue(target, Nothing))
24    Else
25        Return r.GetValue(target, Nothing).ToString
26    End If
27End Function

Like I said this is work in progress and it does not support arrays as sub values, but it does add a certain level of versatility to the logging. My last project used a logging system, not Elmah  , to log errors to TFS in this way and I also added functionality to update the work item in different ways if it was Closed or Resolved to reactivate it depending on the Build number values.

We have now created a new work item, but what about loading an existing one?

 1Private Function GetExistingWorkItem(ByVal Title As String) As WorkItem
 2    ' Query for work items
 3    Dim query As String = "SELECT [System.Id], [System.Title] " _
 4                         & "FROM WorkItems " _
 5                         & "WHERE [System.TeamProject] = @Project  " _
 6                         & "AND  [System.WorkItemType] = @WorkItemType  " _
 7                         & "AND  [System.Title] = @Title  " _
 8                         & "ORDER BY [System.Id]"
 9    Dim paramiters As New Hashtable
10    paramiters.Add("Project", m_TemplateDefault.TeamProjectName)
11    paramiters.Add("WorkItemType", m_TemplateDefault.WorkItemTypeName)
12    paramiters.Add("Title", m_TemplateDefault.WorkItemTypeName)
13    Dim y As WorkItemCollection = TfsWorkItemStore.Query(query, paramiters)
14    Return y(0)
15End Function

This is a simple search for the title that we created and pass back the first match, just in case we have duplicates.

And that’s all there is to saving your logs into VSTS, but how do we get them out! This is pretty easy as all of our log entries have now been saved to a TFS work item and if you remember from before we used the “String.Format("{0}|{1}", wi.Id, errorId.ToString)“ for the ID so we can find the work item again.

The two thing we have left is loading a single error, and loading all of the errors. Getting a single error is a little tricky, which is why we passed back the ID in a format that included the Work Item ID.

 1Public Overrides Function GetError(ByVal id As String) As ErrorLogEntry
 2    Dim idBits() As String = id.Split("|")
 3    Dim wiId As Integer
 4    Dim errGuid As String
 5    If Not idBits.Length = 2 Then
 6        Throw New ArgumentException("Invalid ID, it must be made in the format {workItemId}|{guid}", "id")
 7    End If
 8    If Not IsNumeric(idBits(0)) Then
 9        Throw New ArgumentException("The workItemId part of the ID must be an integer. Format: {workItemId}|{guid}", "id")
10    End If
11    wiId = CInt(idBits(0))
12    Try
13        errGuid = New Guid(idBits(1)).ToString
14    Catch ex As Exception
15        Throw New ArgumentException("The guid part of the ID must be an integer. Format: {workItemId}|{guid}", "id")
16    End Try
17    Dim wi As WorkItem = TfsWorkItemStore.GetWorkItem(wiId)
18    If wi Is Nothing Then
19        Throw New ApplicationException("A work item with that id does not exits")
20    End If
21    Dim a = (From attachemnt As Attachment In wi.Attachments Where attachemnt.Name.Contains(errGuid) Select attachemnt).SingleOrDefault
22    If a Is Nothing Then
23        Throw New ApplicationException("The attachment does not exits or has been removed")
24    End If
25    Return GetErrorLogEntryFromTfsAttachement(wi, a)
26End Function

In this method we do a little validation while parsing out the Work Item ID and the Elmah  ID, we then load the specified work item, and find the attachment, and return it. I have a little helper method to make a log item from an attachment, but it fairly simple:

 1Private Function GetErrorLogEntryFromTfsAttachement(ByVal wi As WorkItem, ByVal a As Attachment) As ErrorLogEntry
 2    Using reader = XmlReader.Create(a.Uri.ToString)
 3        If Not reader.IsStartElement("error") Then
 4            Return Nothing
 5        End If
 6        Dim errid = String.Format("{0}|{1}", wi.Id, reader.GetAttribute("errorId"))
 7        Dim [error] = ErrorXml.Decode(reader)
 8        Return New ErrorLogEntry(Me, errid, [error])
 9    End Using
10    Return Nothing
11End Function

And voila! You havve a single Error Log Entry. As you have probably guesses, getting all the errors is easy now. We just need to find all attachements that have a . elmah  extension in our project. A little linq can help with this.

 1Public Overrides Function GetErrors(ByVal pageIndex As Integer, ByVal pageSize As Integer, ByVal errorEntryList As System.Collections.IList) As Integer
 2    If pageIndex < 0 Then Throw New ArgumentOutOfRangeException("pageIndex", pageIndex, Nothing)
 3    If pageSize < 0 Then Throw New ArgumentOutOfRangeException("pageSize", pageSize, Nothing)
 4
 5    ' Query for work items
 6    Dim query As String = "SELECT [System.Id], [System.Title] " _
 7                         & "FROM WorkItems " _
 8                         & "WHERE [System.TeamProject] = @Project  " _
 9                         & "AND  [System.WorkItemType] = @WorkItemType  " _
10                         & "ORDER BY [System.Id]"
11    Dim paramiters As New Hashtable
12    paramiters.Add("Project", m_TemplateDefault.TeamProjectName)
13    paramiters.Add("WorkItemType", m_TemplateDefault.WorkItemTypeName)
14    Dim y As WorkItemCollection = TfsWorkItemStore.Query(query, paramiters)
15    ' Query work items for attachments
16    Dim wiats = From wi As WorkItem In y, a As Attachment In wi.Attachments Where a.Name.Contains(".elmah") Order By a.Name Select a, wi
17    If Not wiats Is Nothing Then
18        ' Select specific attachemnts
19        Dim results = From wiat In wiats Skip pageIndex * pageSize Take pageSize Select wiat
20        ' Add to output
21        For Each el In results
22            errorEntryList.Add(GetErrorLogEntryFromTfsAttachement(el.wi, el.a))
23        Next
24    End If
25    ' return count
26    Return errorEntryList.Count
27End Function

And there we go, errors from Elmah  saved into Team Foundation Server and then loaded back out. I don’t know how useful this would be in the real world, but it was good for a little boredom relief.

Full Source

  1Imports Elmah
  2Imports Microsoft.TeamFoundation.Client
  3Imports Microsoft.TeamFoundation.WorkItemTracking.Client
  4Imports Microsoft.TeamFoundation.PowerTools.Client.WorkItemTracking.Templates
  5Imports System.Globalization
  6Imports System.Xml
  7Imports System.Text
  8Imports System.Web
  9Imports System.Reflection
 10
 11Public Class TfsErrorLog
 12    Inherits ErrorLog
 13
 14    Private Shared m_TemplateDefault As Template
 15    Private Shared m_TemplateErrorMap As Template
 16    Private Shared sm_Tfs As TeamFoundationServer
 17    Private Shared sm_TfsStore As WorkItemStore
 18    Private Shared sm_TfsProject As Project
 19    Private Shared sm_Config As IDictionary
 20
 21    Public ReadOnly Property TfsServer() As TeamFoundationServer
 22        Get
 23            If sm_Tfs Is Nothing Then
 24                sm_Tfs = GetTeamFoundationServer()
 25            End If
 26            Return sm_Tfs
 27        End Get
 28    End Property
 29
 30    Public ReadOnly Property TfsWorkItemStore() As WorkItemStore
 31        Get
 32            If sm_TfsStore Is Nothing Then
 33                sm_TfsStore = GetTeamFoundationServerWorkItemStore()
 34            End If
 35            Return sm_TfsStore
 36        End Get
 37    End Property
 38
 39    Public ReadOnly Property TfsProject() As Project
 40        Get
 41            If sm_TfsProject Is Nothing Then
 42                sm_TfsProject = GetTeamFoundationServerProject()
 43            End If
 44            Return sm_TfsProject
 45        End Get
 46    End Property
 47
 48    Public Sub New(ByVal config As IDictionary)
 49        If config Is Nothing Then
 50            Throw New ArgumentNullException("config")
 51        End If
 52        sm_Config = config
 53
 54        Dim store As ITemplateStore = GetStore()
 55        m_TemplateDefault = GetTemplate("Defaults", store)
 56        m_TemplateErrorMap = GetTemplate("ErrorMap", store)
 57
 58        If m_TemplateDefault Is Nothing Or m_TemplateErrorMap Is Nothing Then
 59            Throw New ApplicationException("Unable to load the templates from the store.")
 60        End If
 61
 62    End Sub
 63
 64    Private Function GetStore()
 65        Dim TfsWorkItemTemplateStore As String = GetStorePath()
 66        Try
 67            Dim storeProvider As New FileSystemTemplateStoreProvider
 68            Return New TemplateStore(storeProvider, TfsWorkItemTemplateStore, ":)Store")
 69        Catch ex As Exception
 70            Throw New ApplicationException(String.Format("Unable to load the store from '{0}'.", TfsWorkItemTemplateStore), ex)
 71        End Try
 72    End Function
 73
 74    Private Function GetStorePath() As String
 75        Dim storePath As String = sm_Config("TfsWorkItemTemplateStore")
 76        If String.IsNullOrEmpty(storePath) Then
 77            Throw New ApplicationException("Tfs Server Name is missing for the TFS based error log.")
 78        End If
 79        Try
 80            If storePath.StartsWith("~/") Then
 81                storePath = HttpContext.Current.Server.MapPath(storePath)
 82            End If
 83            Return storePath
 84        Catch ex As Exception
 85            Throw New ApplicationException(String.Format("Unable to produce the store path from '{0}'.", storePath), ex)
 86        End Try
 87    End Function
 88
 89    Private Function GetTemplate(ByVal TemplateName As String, ByVal store As ITemplateStore) As ITemplate
 90        Try
 91            Dim t As ITemplate
 92            If Not store.TemplateExists("/", TemplateName) Then
 93                t = store.CreateTemplate()
 94                t.Name = TemplateName
 95                t.ParentFolder = "/"
 96                t.TeamServerUri = "https://tfs01.codeplex.biz:443"
 97                t.TeamProjectName = "RDdotNet"
 98                t.WorkItemTypeName = "WorkItem"
 99                store.AddTemplate(t)
100            End If
101            Return store.GetTemplate("/", TemplateName)
102        Catch ex As Exception
103            Throw New ApplicationException(String.Format("Unable to load the template '{0}' from the store.", TemplateName), ex)
104        End Try
105    End Function
106
107    Private Function GetTeamFoundationServer() As TeamFoundationServer
108        Dim tfs As TeamFoundationServer = Nothing
109        Try
110            tfs = New TeamFoundationServer(m_TemplateDefault.TeamServerUri)
111            tfs.Authenticate()
112            If Not tfs.HasAuthenticated Then
113                Throw New ApplicationException("Unable to authenticate against TFS server")
114            End If
115        Catch ex As Exception
116            Throw New ApplicationException("Failed to authenticate against TFS server", ex)
117        End Try
118        Return tfs
119    End Function
120
121    Private Function GetTeamFoundationServerWorkItemStore() As WorkItemStore
122        Dim store As WorkItemStore = Nothing
123        If TfsServer.HasAuthenticated Then
124            store = DirectCast(TfsServer.GetService(GetType(WorkItemStore)), WorkItemStore)
125        End If
126        Return store
127    End Function
128
129    Private Function GetTeamFoundationServerProject() As Project
130        Dim Project As Project = Nothing
131        Try
132            If TfsServer.HasAuthenticated Then
133                Project = TfsWorkItemStore.Projects(m_TemplateDefault.TeamProjectName)
134            End If
135        Catch ex As Exception
136            Throw New ApplicationException("Unable to retrieve Tfs Project", ex)
137        End Try
138        If Project Is Nothing Then
139            Throw New ApplicationException(String.Format("Unable to locate project with the name '{0}'", m_TemplateDefault.TeamProjectName))
140        End If
141        Return Project
142    End Function
143
144    Public Overrides Function GetError(ByVal id As String) As ErrorLogEntry
145        Dim idBits() As String = id.Split("|")
146        Dim wiId As Integer
147        Dim errGuid As String
148        If Not idBits.Length = 2 Then
149            Throw New ArgumentException("Invalid ID, it must be made in the format {workItemId}|{guid}", "id")
150        End If
151        If Not IsNumeric(idBits(0)) Then
152            Throw New ArgumentException("The workItemId part of the ID must be an integer. Format: {workItemId}|{guid}", "id")
153        End If
154        wiId = CInt(idBits(0))
155        Try
156            errGuid = New Guid(idBits(1)).ToString
157        Catch ex As Exception
158            Throw New ArgumentException("The guid part of the ID must be an integer. Format: {workItemId}|{guid}", "id")
159        End Try
160        Dim wi As WorkItem = TfsWorkItemStore.GetWorkItem(wiId)
161        If wi Is Nothing Then
162            Throw New ApplicationException("A work item with that id does not exits")
163        End If
164        Dim a = (From attachemnt As Attachment In wi.Attachments Where attachemnt.Name.Contains(errGuid) Select attachemnt).SingleOrDefault
165        If a Is Nothing Then
166            Throw New ApplicationException("The attachment does not exits or has been removed")
167        End If
168        Return GetErrorLogEntryFromTfsAttachement(wi, a)
169    End Function
170
171    Public Overrides Function GetErrors(ByVal pageIndex As Integer, ByVal pageSize As Integer, ByVal errorEntryList As System.Collections.IList) As Integer
172        If pageIndex < 0 Then Throw New ArgumentOutOfRangeException("pageIndex", pageIndex, Nothing)
173        If pageSize < 0 Then Throw New ArgumentOutOfRangeException("pageSize", pageSize, Nothing)
174
175        ' Query for work items
176        Dim query As String = "SELECT [System.Id], [System.Title] " _
177                             & "FROM WorkItems " _
178                             & "WHERE [System.TeamProject] = @Project  " _
179                             & "AND  [System.WorkItemType] = @WorkItemType  " _
180                             & "ORDER BY [System.Id]"
181        Dim paramiters As New Hashtable
182        paramiters.Add("Project", m_TemplateDefault.TeamProjectName)
183        paramiters.Add("WorkItemType", m_TemplateDefault.WorkItemTypeName)
184        Dim y As WorkItemCollection = TfsWorkItemStore.Query(query, paramiters)
185        ' Query work items for attachments
186        Dim wiats = From wi As WorkItem In y, a As Attachment In wi.Attachments Where a.Name.Contains(".elmah") Order By a.Name Select a, wi
187        If Not wiats Is Nothing Then
188            ' Select specific attachemnts
189            Dim results = From wiat In wiats Skip pageIndex * pageSize Take pageSize Select wiat
190            ' Add to output
191            For Each el In results
192                errorEntryList.Add(GetErrorLogEntryFromTfsAttachement(el.wi, el.a))
193            Next
194        End If
195        ' return count
196        Return errorEntryList.Count
197    End Function
198
199    ''' <summary>
200    ''' Logs the error as an attachement to an existing work item, or adds a new work item if this error has not occured.
201    ''' </summary>
202    ''' <param name="error">The error to be logged</param>
203    ''' <returns>The ID of the error</returns>
204    ''' <remarks></remarks>
205    Public Overrides Function Log(ByVal [error] As [Error]) As String
206        'TODO: Log
207        Dim errorId = Guid.NewGuid().ToString()
208        Dim timeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddHHmmssZ", CultureInfo.InvariantCulture)
209        Dim Filename = String.Format("error-{0}-{1}.elmah", timeStamp, errorId)
210        Dim temp = System.IO.Path.Combine(".", Filename)
211        ' Temp Log to disk
212        Using writer = New XmlTextWriter(temp, Encoding.UTF8)
213            writer.Formatting = Formatting.Indented
214            writer.WriteStartElement("error")
215            writer.WriteAttributeString("errorId", errorId)
216            ErrorXml.Encode([error], writer)
217            writer.WriteEndElement()
218            writer.Flush()
219        End Using
220
221        Dim Title As String = String.Format("{0}-{1}", [error].ApplicationName, [error].Message)
222
223        Dim wi As WorkItem = GetWorkItemForException(Title, [error])
224
225        Dim a As New Attachment(temp, "Elmah error log")
226
227        wi.Attachments.Add(a)
228        If wi.IsValid Then
229            wi.Save()
230            Return String.Format("{0}|{1}", wi.Id, errorId.ToString)
231        Else
232            Dim message As New System.Text.StringBuilder
233            Dim results = wi.Validate()
234            Dim isFirst As Boolean = True
235            For Each r In results
236                message.AppendLine(String.Format(IIf(isFirst, "{0}", ", {0}"), r))
237                isFirst = False
238            Next
239            Throw New ApplicationException(String.Format("Unable to save the work item becuse the following fields produced a validation error '{0}'.", message.ToString))
240        End If
241    End Function
242
243    Protected Function GetWorkItemForException(ByVal Title As String, ByVal [error] As [Error]) As WorkItem
244        Dim wi As WorkItem = GetExistingWorkItem(Title)
245        If wi Is Nothing Then
246            wi = CreateNewWorkItem(Title)
247        End If
248        m_TemplateDefault.Fields.ApplyFieldValues(wi, False)
249        ApplyErrorFieldValues(wi, [error])
250        Return wi
251    End Function
252
253    Private Function GetExistingWorkItem(ByVal Title As String) As WorkItem
254        ' Query for work items
255        Dim query As String = "SELECT [System.Id], [System.Title] " _
256                             & "FROM WorkItems " _
257                             & "WHERE [System.TeamProject] = @Project  " _
258                             & "AND  [System.WorkItemType] = @WorkItemType  " _
259                             & "AND  [System.Title] = @Title  " _
260                             & "ORDER BY [System.Id]"
261        Dim paramiters As New Hashtable
262        paramiters.Add("Project", m_TemplateDefault.TeamProjectName)
263        paramiters.Add("WorkItemType", m_TemplateDefault.WorkItemTypeName)
264        paramiters.Add("Title", m_TemplateDefault.WorkItemTypeName)
265        Dim y As WorkItemCollection = TfsWorkItemStore.Query(query, paramiters)
266        Return y(0)
267    End Function
268
269    Private Function CreateNewWorkItem(ByVal Title As String) As WorkItem
270        Dim wit As WorkItemType = (From t As WorkItemType In TfsProject.WorkItemTypes Where t.Name = m_TemplateDefault.WorkItemTypeName).SingleOrDefault
271        If wit Is Nothing Then
272            Throw New ApplicationException(String.Format("Unable to find the work item type '{0}' in the project '{1}'", m_TemplateDefault.WorkItemTypeName, TfsProject.Name))
273        End If
274        Dim wi As New WorkItem(wit)
275        wi.Title = Title
276        Return wi
277    End Function
278
279    Private Sub ApplyErrorFieldValues(ByVal wi As WorkItem, ByVal [error] As [Error])
280        For Each i In m_TemplateErrorMap.Fields
281            Dim value As String = GetPropertyValue(i.Value, [error])
282            If wi.Fields(i.ReferenceName).AllowedValues.Contains(value) Then
283                wi.Fields(i.ReferenceName).Value = value
284            Else
285                Throw New ApplicationException(String.Format("Unable to set the work item field '{0}' to '{1}' as '{1}' is not in the Allowed Values list.", i.ReferenceName, value))
286            End If
287        Next
288    End Sub
289
290    Private Function GetPropertyValue(ByVal path As String, ByVal target As Object) As String
291        Dim bits() As String = path.Split(".")
292        Dim ll As New LinkedList(Of String)
293        Array.ForEach(bits, Function(b) ll.AddLast(b))
294        Return GetPropertyRecurse(ll.First, target)
295    End Function
296
297    Private Function GetPropertyRecurse(ByVal node As LinkedListNode(Of String), ByVal target As Object) As String
298        ' ToDo: addd ability to support propertyName(0) [arrays]
299        Dim r As System.Reflection.PropertyInfo = target.GetType.GetProperty(node.Value, BindingFlags.Static Or BindingFlags.Public Or BindingFlags.GetField Or BindingFlags.GetProperty)
300        If r.PropertyType.IsClass And Not node.Next Is Nothing Then
301            Return GetPropertyRecurse(node.Next, r.GetValue(target, Nothing))
302        Else
303            Return r.GetValue(target, Nothing).ToString
304        End If
305    End Function
306
307    Private Function GetErrorLogEntryFromTfsAttachement(ByVal wi As WorkItem, ByVal a As Attachment) As ErrorLogEntry
308        Using reader = XmlReader.Create(a.Uri.ToString)
309            If Not reader.IsStartElement("error") Then
310                Return Nothing
311            End If
312            Dim errid = String.Format("{0}|{1}", wi.Id, reader.GetAttribute("errorId"))
313            Dim [error] = ErrorXml.Decode(reader)
314            Return New ErrorLogEntry(Me, errid, [error])
315        End Using
316        Return Nothing
317    End Function
318
319End Class

Technorati Tags: WIT    ALM    .NET    CodeProject    TFS 

Azure DevOps Software Development Troubleshooting Technical Mastery
Comments

Related blog posts

No related videos found.

Connect with Martin Hinshelwood

If you've made it this far, it's worth connecting with our principal consultant and coach, Martin Hinshelwood, for a 30-minute 'ask me anything' call.

Our Happy Clients​

We partner with businesses across diverse industries, including finance, insurance, healthcare, pharmaceuticals, technology, engineering, transportation, hospitality, entertainment, legal, government, and military sectors.​

Workday Logo
Freadom Logo
Slicedbread Logo
ProgramUtvikling Logo
Schlumberger Logo
Big Data for Humans Logo
Sage Logo
Epic Games Logo
Brandes Investment Partners L.P. Logo
Xceptor - Process and Data Automation Logo
Akaditi Logo
MacDonald Humfrey (Automation) Ltd. Logo
Graham & Brown Logo
Genus Breeding Ltd Logo
Boxit Document Solutions Logo
Bistech Logo
Ericson Logo
Higher Education Statistics Agency Logo
Washington Department of Transport Logo
Ghana Police Service Logo
Department of Work and Pensions (UK) Logo
New Hampshire Supreme Court Logo
Washington Department of Enterprise Services Logo
Royal Air Force Logo
Alignment Healthcare Logo
Microsoft Logo
Epic Games Logo

CR2

YearUp.org Logo
MacDonald Humfrey (Automation) Ltd. Logo