Custom build activity for TFS sending report

Because the standard tfs buildreport does not contain any detailed information about the workitems I decided to write a custom activity. I want our testers to get an email with the workitems associated with the nightly build, that they know which features can be tested. A good start is this post on msdn . I used the code provided as a start for my activity. First of all I wanted a little more customization, to allow the tfs url and dashboard url to be set. Furthermore I didn't like all the strings in the code, so I used embedded resources to store the html data. To get more information about building your own custom activity and deploying it, take a look here . Now lets start with implementing our activity:

You first have to derive from CodeActivity and set the Attribute BuildActivity:

public class SendBuildSummaryEmail : CodeActivity

Then we can start to write our code. We are overriding the Execute method as shown below:

protected override void Execute(CodeActivityContext theContext)
   myContext = theContext;

   var buildInformation = theContext.GetValue(BuildDetail);

   var changesetHtml = new StringBuilder();

   foreach (Changeset changeset in theContext.GetValue(BuildAssociatedChangesets))
      WriteChangeset(changesetHtml, changeset);

   var mailHtml = new StringBuilder();
   mailHtml .Append(myHtmlWraper);
   mailHtml .Replace("<@ChangesetsHtml>", changesetHtml.ToString());
   mailHtml .Replace("<@DateTime>", buildInformation.FinishTime.ToString());
   mailHtml .Replace("<@BuildNumber>", Encoder.HtmlEncode(buildInformation.BuildNumber));
   mailHtml .Replace("<@DropPath>", Encoder.HtmlEncode(buildInformation.DropLocation));
   mailHtml .Replace("<@ProjectPortal>", ResolvedProjectPortalUrl);

First of all we are reading the files, stored as embedded resources into our private properties. After that were retrieving the build information. For each changeset associated with our build, we are writing the information to our report. The last step is to replace the placeholders in the main html page with the build information and send the mail to the configured recipients. Let's take a look at the methods actually doing the work. We start with the ReadResources method

private void ReadResources()
   myHtmlWraper = ReadResourceInto("HtmlWrapperContent.html");
   myChangesetWraper = ReadResourceInto("ChangesetWrapper.html");
   myWorkItemWraper = ReadResourceInto("WorkItemWrapper.html");
   myFileWraper = ReadResourceInto("FileWrapper.html");

private string ReadResourceInto(string theResourceName)
   var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(string.Format("{0}.{1}", BaseResourceNamespace, theResourceName));
   if (stream == null)
      return string.Empty;

   using (var sr = new StreamReader(stream))
      return sr.ReadToEnd();

In ReadResourcesInto were first retrieving a resource stream which we read with our StreamReader afterwards. The format for the embedded file is Namespace.Filename e.g. BuildExtension.Resources.HtmlContent.html if your file is located in the folder Resources of your assembly BuildExtension. Next we'll take a look at the WriteChangeset method.

private void WriteChangeset(StringBuilder theChangesetHtml, Changeset theChangeset)
      var content = myChangesetWraper.Replace("<@ChangesetLink>",
                                                    string.Format("{0}/web/UI/Pages/Scc/ViewChangeset.aspx?cs={1}", ResolvedTeamFoundationServerBaseUrl,

      content = content.Replace("<@ChangesetId>", theChangeset.ChangesetId.ToString(CultureInfo.InvariantCulture));
      content = content.Replace("<@Commiter>", Encoder.HtmlEncode(theChangeset.Committer));
      content = content.Replace("<@Comment>", Encoder.HtmlEncode(theChangeset.Comment));
      content = content.Replace("<@WorkItemCount>", theChangeset.WorkItems.Count().ToString(CultureInfo.InvariantCulture));

      content = content.Replace("<@WorkItemsHtml>", WriteWorkItems(theChangeset.WorkItems));

      List<Change> files = GetFilesAssociatedWithBuild(theChangeset.VersionControlServer, theChangeset.ChangesetId);

      content = content.Replace("<@FileCount>", files.Count.ToString(CultureInfo.InvariantCulture));
      var fileContent = new StringBuilder();
      foreach (Change file in files)
         WriteFile(fileContent, file);

      content = content.Replace("<@FileHtml>", fileContent.ToString());


In WriteChangeset we basically replace the placeholders with values we get from the changeset. We also add information about the associated WorkItems:

private string WriteWorkItems(IEnumerable<WorkItem> theWorkitems)
   var workItemsHtml = new StringBuilder();

   foreach (WorkItem workitem in theWorkitems)
      var content = myWorkItemWraper.Replace("<@WorkItemLink>",
                                                       string.Format("{0}/web/UI/Pages/WorkItems/WorkItemEdit.aspx?id={1}", ResolvedTeamFoundationServerBaseUrl,
      content = content.Replace("<@WorkItemId>", workitem.Id.ToString(CultureInfo.InvariantCulture));
      content = content.Replace("<@WorkItemName>", workitem.Type.Name);
      content = content.Replace("<@WorkItemTitle>", Encoder.HtmlEncode(workitem.Title));
      content = content.Replace("<@WorkItemDescription>", Encoder.HtmlEncode(workitem.Description));


   return workItemsHtml.ToString();

We retrieve the files associated with the changeset with GetFilesAssociatedWithBuild

private static List<Change> GetFilesAssociatedWithBuild(VersionControlServer theVersionControlServer, int theChangesetId)
   var files = new List<Change>();
   var changeset = theVersionControlServer.GetChangeset(theChangesetId);
   if (changeset.Changes != null)

   return files;

We load the changeset from the VersionControlServer and add the associated files to our list. Each of the changed files is written to the StringBuilder using WriteFile

private void WriteFile(StringBuilder theChangesetHtml, Change theFile)
   var content = myFileWraper.Replace("<@File>", Encoder.HtmlEncode(theFile.Item.ServerItem));
   content = content.Replace("<@FileLink>",
   string.Format("{0}/web/UI/Pages/Scc/ViewSource.aspx?path={1}&changeset={2}", ResolvedTeamFoundationServerBaseUrl,
                                                    Encoder.UrlPathEncode(theFile.Item.ServerItem), theFile.Item.ChangesetId));
content = content.Replace("<@ChangeType>", theFile.ChangeType.ToString());


We just add a few interesting properties for each file and a link, so that the files can be viewed directly from our email. The last piece is actually sending the email to the recipients.

private void SendMail(StringBuilder theMailBody)
   var smtp = new SmtpClient(ResolvedSmtpServer) {UseDefaultCredentials = true};
   var msg = new MailMessage
      From = new MailAddress(ResolvedMailFrom),
      Subject = ResolvedSubject,
      IsBodyHtml = true,
      Body = theMailBody.ToString()


You probably noticed that I am using properties starting with Resolved, that's becaus I don't want to get the value from the context each time. An example for these resolved properties is below:

private string myResolvedSmtpServer;
private string ResolvedSmtpServer
      if (string.IsNullOrEmpty(myResolvedSmtpServer))
         myResolvedSmtpServer = myContext.GetValue(SmtpServer);
      return myResolvedSmtpServer;

Below you can see the result of our report:

In a later post I will show you how we get this build activity to work in build process. 

Add comment

  Country flag

  • Comment
  • Preview




<<  December 2017  >>

View posts in large calendar