Tuesday, March 8, 2011

Email Alerts in Team Foundation Server

If you ever wonder how to send an E-Mail notification to a user participating in the event in TFS, the solution would be a little bit tricky for a flexible, extensible mechanism for subscription and notification. One of the easiest ways to achieve this is using the Event Service to listen to the event, and then send the notification.

These are some of the situations you may want to have this feature out of box:

  • Notify the developer checked in when the build is failed;
  • Notify the reporters when a bug work item is changed by others.

Obviously, programing a web service or event handler for each of these cases requires a lot of work. It would be nice to utilize the subscription service in TFS, so that we don't have to worry about filtering and formatting events. Here are the steps:

  1. Listen the target event;
  2. Find out the E-Mail address of the user;
  3. Subscribe the user to the same event with proper filter.

In the following example, I am going to handle the WorkItemChangedEvent to notify the reporters when a bug work item is changed by others, with the helps of TeamFoundationIdentityService and TeamFoundationNotificationService. (Sadly, the documentation on MSDN doesn't actually tell you anything more than Object Browser regarding the TFS server API.)

Before performing any task, we need to get notify when the a work item has been updated. This is done by implementing the ISubscriber interface.

public sealed class EmailAlertSubscriber2 : ISubscriber
{

    public string Name
    {
        get { return "EmailAlertSubscriber" }
    }

    public SubscriberPriority Priority
    {
        get { return SubscriberPriority.Normal }
    }

    public EventNotificationStatus ProcessEvent(
        TeamFoundationRequestContext requestContext, 
        NotificationType notificationType, 
        object notificationEventArgs, 
        out int statusCode, 
        out string statusMessage,
        out ExceptionPropertyCollection properties) 
    {
        statusCode = 0;
        statusMessage = null;
        properties = null;

        if (notificationType == NotificationType.Notification)
        {
            var eventArgs = notificationEventArgs as WorkItemChangedEvent;
            if (eventArgs != null)
            {
                // Process event...
            }
        }

        return EventNotificationStatus.ActionPermitted;
    }

    public Type[] SubscribedTypes()
    {
        return new Type[] { typeof(WorkItemChangedEvent) };
    }
}

Next, we can retrieve the value of person field form notificationEventArgs. As per our example, I need the user who created the bug work item.

var createdByDisplayName = 
    eventArgs
    .CoreFields
    .StringFields
    .Single(
        f => f.ReferenceName.Equals(
            "System.CreatedBy", StringComparison.OrdinalIgnoreCase))
    .NewValue;

The field value may be a display name, domain account or Sid (Security Identifier), but the base steps to retrieve E-Mail address are the same - read the TeamFoundationIdentity; and then load the E-Mail address from Active Directory. Just make sure you reading the E-Mail address from the correct domain.

var identityService = requestContext.GetService<TeamFoundationIdentityService>();

// Read TFS identity by display name.
var tfsId = identityService.ReadIdentity(
    requestContext, 
    IdentitySearchFactor.DisplayName, 
    displayName, 
    MembershipQuery.None, ReadIdentityOptions.None);

// Read TFS identity by account name.
var tfsId = identityService.ReadIdentity(
    requestContext, 
    IdentitySearchFactor.AccountName, 
    accountName, 
    MembershipQuery.None, ReadIdentityOptions.None);

// Read TFS identity by Sid.
var idDescriptor = IdentityHelper.CreateDescriptorFromSid(sid);
var tfsId = identityService.ReadIdentity(
    requestContext, 
    idDescriptor, 
    MembershipQuery.None, ReadIdentityOptions.None);

var domainName = IdentityHelper.GetDomainName(identity);

using (var context = new PrincipalContext(ContextType.Domain, domainName))
{
    var userPrincipal = UserPrincipal.FindByIdentity(
        context, IdentityType.Sid, identity.Descriptor.Identifier);

    // Now we got the E-Mail address as userPrincipal.EmailAddress;
}

Now, we have all the information we need to subscribe the user to event. In the following code snippet, the classification is used to identify the subscription. It likely contains the event type and user identifier, so that the handler won't subscribe the user every time the event fired. On the other hand, we may supply a filter expression limiting the E-Mail going out.

var classification = // Identifer for the subscription.

var notificationService = requestContext.GetService<TeamFoundationNotificationService>();
var subscriptions = notificationService.GetEventSubscriptions(
    requestContext, 
    userIdentity.Descriptor,
    classification);

// Check for whether the subscription already exists.
if (subscriptions.Count == 0)
{
    var filterExpression = // Filter used to limit the E-Mail notifications.

    var perference = new DeliveryPreference()
    {
        Address = userPrincipal.EmailAddress,
        Schedule = DeliverySchedule.Immediate,
        Type = DeliveryType.EmailHtml
    };

    notificationService.SubscribeEvent(
        requestContext,
        userIdentity.Descriptor, 
        "WorkItemChangedEvent", 
        filterExpression, 
        perference, 
        classification);
}

After all these, the handler is ready to be deployed. To use it, simply compile the code to an assembly, and copy it to %Program Files%\Microsoft Team Foundation Server 2010\Application Tier\Web Services\bin\Plugins of the TFS server. The E-Mail will start filling up your mail box now.

You may want to unsubscribe the user, such as when the work item is closed. This can be done by:

if (subscriptions.Count > 0)
{
    foreach (var subscription in subscriptions)
    {
        requestContext.GetService<TeamFoundationNotificationService>()
            .UnsubscribeEvent(requestContext, subscription.ID);
    }
}

The problem here is that the E-Mail is send by the delayed job. By the time the job being executed, the subscription is removed, and the user won't get notify when the work item is closed. Interestingly, this delay is exactly what ensuring the user get notify the first time when we subscribing the user.

If you don't mind the event subscriptions table get fill up, or you willing to clean the table once a while. This approach will work quite well.

So, in order to find a better solution, I will show you how to filter events using VSEFL , and then wire up everything in other posts.