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
No related videos found.
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.
We partner with businesses across diverse industries, including finance, insurance, healthcare, pharmaceuticals, technology, engineering, transportation, hospitality, entertainment, legal, government, and military sectors.
CR2