Sending Transform Manager Job Status Notifications to a Remote Web Application

by Mario Doiron

You can configure a new notifications feature in IIS Transform Manager 1.0 to POST web requests with job status changes to a remote web application or service. This enables web developers to build a custom Transform Manager job-monitoring system. The following are some of the benefits to be gained from building such an application:

  • Users don't need to be logged-on to the local Transform Manager computer to view job status. The remote web application allows users to view job status from anywhere.
  • Users don't need to be part of the local Administrators group to view job status. The web application can allow/deny user access to reports, without having to grant administrative rights on the Transform Manager computer.
  • The web application can receive notifications from multiple Transform Manager computers, providing a global status on the cluster.
  • The web application can filter notifications from unwanted watch folders on a Transform Manager computer.
  • The web application that receives the job status information can be customized to suit your needs.

This article describes a sample ASP.NET MVC 3 web application on the Windows Azure platform that receives Transform Manager job status.


IIS Transform Manager is designed to work with other programs, such as programs that handle audio or video signals. It's solely your responsibility to ensure your compliance with any terms accompanying such other programs, and that you have obtained any necessary rights for your use of the programs.

About the POST Message Body

IIS Transform Manager sends job status information in the HTTP POST request body in a notification. The following is an example of the XML syntax in the POST message body:

<IISTransformManager >
      folder="C:\inetpub\media\Transform Manager\Smooth (VC1)\" 
      taskIndex="1" />
   <log>Optional job instance logging information.</log>

Let's take a closer look at each of the XML elements:

  • IISTransformManager. The root XML element.
  • queue. The job instance status.
  • workItem. The collection of attributes that describe the job instance.
  • jobName. The unique job identifier that is created by Transform Manager.
  • folder. The watch folder that the job was submitted to.
  • jobDefinitionId. The unique identifier that is used by Transform Manager to identify a specific job definition.
  • jobTemplateId. The unique identifier that is used by Transform Manager to identify a specific job template.
  • jobSchedulerId. The unique identifier that is used by Transform Manager to identify a specific job scheduler.
  • manifestName. The job manifest that is created by Transform Manager.
  • instanceFileName. The file that triggered the creation of this job instance.
  • queueTime. The date/time of the job status change.
  • progress. The job progress as a percentage-completed value.
  • submitTime. The date/time the job was submitted.
  • startTime. The date/time the job was started.
  • endTime. The date/time the job ended (finished, failed, or canceled).
  • priority. The watch folder priority value.
  • status. The current job status.
  • taskCount. The number of tasks in the job template.
  • taskIndex. The last task index that was reported.
  • log. The job instance log entries. This element is only included if the job status is 'Failed' and Transform Manager is configured to include job instance logs for failed jobs. For more information about how to configure notifications to include log information, see Configuring Job Status Notifications.

Creating a Sample Web Application

In this section, I'll show you how to integrate IIS Transform Manager notifications into a sample ASP.NET MVC 3 web application that is hosted in the Windows Azure Fabric. This section contains the following topics:


To create the sample web application to which Transform Manager will send notifications, install the following software on a computer that is running either the Windows 7 or Windows Server 2008 R2 operating system:

Creating the Web Application

Creating an ASP.NET MVC 3 Azure Web Role is not straightforward. Fortunately, there is a way to get MVC 3 up-and-running in Windows Azure. This section describes how to get MVC 3 assemblies deployed on a Windows Azure instance using techniques desribed in Steve Marx's blog ASP.NET MVC 3 in Windows Azure. It involves creating a blank Windows Azure solution, adding an ASP.NET MVC 3 Web Application Project to the solution, and then configuring the ASP.NET MVC 3 Web Application Project as an Azure Web Role.

Create a Blank Windows Azure Solution
  1. In Visual Studio, create a new Windows Azure solution.

  2. After you click OK in the New Project dialog box, the New Windows Azure Project dialog box is displayed.

    Click OK in this dialog box as the ASP.NET MVC 3 Web Role is not available to add to the Windows Azure solution.

Create an ASP.NET MVC 3 Web Application Project
  1. In Solution Explorer, right-click the Windows Azure solution that you created in the previous section, and then click Add > New Project.

  2. Create a new ASP.NET MVC 3 Web Application project.

  3. After you click OK in the Add New Project dialog box, the New ASP.NET MVC 3 Project dialog box is displayed.

    In this dialog box, select the Empty project template, and then click OK.

Add References to the ASP.NET MVC 3 Web Application Project
  1. In Solution Explorer, right-click References, and then click Add Reference.

  2. In the Add Reference dialog box, on the .NET tab, select the following reference assemblies:

    • Microsoft.Web.Infrastructure
    • Microsoft.WindowsAzure.CloudDrive
    • Microsoft.WindowsAzure.Diagnostics
    • Microsoft.WindowsAzure.ServiceRuntime
    • Microsoft.WindowsAzure.StorageClient
    • System.Data.Services.Client
    • System.Web.Helpers
    • System.Web.Razor
    • System.Web.WebPages
    • System.Web.WebPages.Deployment
    • System.Web.WebPages.Razor

    To select multiple components at once, press the CTRL key on your keyboard and then click each of the component names.

    After you've selected all of the components, click OK.

Configure the ASP.NET MVC 3 Web Application Project References
  1. In Solution Explorer, in References, select the following references:

    • Microsoft.Web.Infrastructure
    • System.Web.Helpers
    • System.Web.Mvc
    • System.Web.Razor
    • System.Web.Routing
    • System.Web.Services
    • System.Web.WebPages
    • System.Web.WebPages.Deployment
    • System.Web.WebPages.Razor

    To select multiple references at once, press the CTRL key on your keyboard and then click each of the reference names. After you've selected all of the references, right-click the selected references, and then click Properties.

  2. In Properties, set the Copy Local property value to True.

  3. Close the Properties box.

Configure the ASP.NET MVC 3 Web Application Project as an Azure Web Role
  1. In Solution Explorer, right-click the Roles folder, and then click Add > Web Role Project in solution.
  2. In the Associate with Role Project dialog box, select the new ASP.NET MVC3 Web Application Project, and then click OK.

Add RoleEntryPoint

In order for the MVC Web Role to launch correctly, we must include a new class in the root of the project that inherits from the RoleEntryPoint class.

  1. In Solution Explorer, right-click the ASP.NET MVC 3 Web Application Project, and then click Add > Class.

  2. In Name, enter the project name as the class file name, and then click Add.

  3. Replace the new class syntax with the following code:

    using Microsoft.WindowsAzure.StorageClient;
    public class NotifyMeMVC : Microsoft.WindowsAzure.ServiceRuntime.RoleEntryPoint
        public override bool OnStart()
            var account = Microsoft.WindowsAzure.CloudStorageAccount.Parse(
            // Required to ensure that the table is created on time
            account.CreateCloudTableClient().CreateTableIfNotExist(Models.NotificationDataServiceContext.TableName); //Note: We will implement the NotificationDataServiceContext class later in this article
    return base.OnStart();

Configure Global.asax.cs
  1. In Solution Explorer, in the ASP.NET MVC 3 Web Application Project, right-click Global.asax, and then select View Code.

  2. Locate the RegisterRoutes static function and update the Default MapRoute. In our sample application, we replace 'Home' with our own 'JobMon' controller:

         "Default", // Route name
         "{controller}/{action}/{id}", // URL with parameters
         new { controller = "JobMon", action = "Index", id = UrlParameter.Optional } // Parameter defaults
  3. In the Application_Start function, add a call to SetConfigurationSettingPublisher by pasting in the following code:

    Microsoft.WindowsAzure.CloudStorageAccount.SetConfigurationSettingPublisher((configName, configSettingPublisher) =>
        var connectionString = Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment.GetConfigurationSettingValue(configName);

Configure Data Connection Settings
  1. In Solution Explorer, right-click the Azure Web Role, and then click Properties.
  2. In Settings, add a DataConnectionString of type Connection String set to UseDevelopmentStorage.

The ASP.NET MVC 3 Azure Web Role is now configured and we can start building the Job Notification MVC 3 application in Azure.

Modeling the Data

In order to model the data, we must build the following model classes in the MVC Models project directory.

These model classes are used to insert and retrieve the POSTed XML data.

Building the NotificationModel Class

This class is the object model representation of the XML data in the POST message body. It inherits from the TableServiceEntity so that we can use it to talk to Azure Table Storage.

public class NotificationModel : Microsoft.WindowsAzure.StorageClient.TableServiceEntity

We will need to include the following empty constructor that feeds unique default values to the base constructor.

public NotificationModel()
     : base((DateTime.MaxValue - DateTime.Now).Ticks.ToString(), Guid.NewGuid().ToString())

We will also need the following list of public properties that make up the entire POSTed XML data.

#region Public Properties
public string PostedDateTime { get; set; }
public string Queue { get; set; }
public string Log { get; set; }
#region WorkItem Attributes Properties
public string JobName { get; set; }
public string Folder { get; set; }
public string JobDefinitionId { get; set; }
public string JobTemplateId { get; set; }
public string JobSchedulerId { get; set; }
public string ManifestName { get; set; }
public string InstanceFileName { get; set; }
public string QueueTime { get; set; }
public string Progress { get; set; }
public string SubmitTime { get; set; }
public string StartTime { get; set; }
public string EndTime { get; set; }
public string Priority { get; set; }
public string Status { get; set; }
public string TaskCount { get; set; }
public string TaskIndex { get; set; }

Finally, a few static classes are needed to parse the XML into an instance object.

#region Public Static Functions
/// <summary>
/// Parse the Posted XML string into a NotificationModel object instance
/// </summary>
public static bool TryParse(string value, out NotificationModel notification)
    bool success = false;
    notification = null;
        System.Xml.XmlReader xReader = System.Xml.XmlReader.Create(new System.IO.StringReader(value));
        System.Xml.Linq.XElement element = System.Xml.Linq.XElement.Load(xReader);
        notification = new NotificationModel();
        // Populate the top XML elements values 
        notification.Log = GetXElementValue(element, "log");
        notification.PostedDateTime = DateTime.Now.ToString();
        notification.Queue = GetXElementValue(element, "queue");
        // Populate the WorkItem attributes values 
        System.Xml.Linq.XElement wItem = null;
        if (element != null)
            wItem = element.Element("workItem");
        notification.Folder = GetWorkItemAttributeValue(wItem, "folder");
        notification.JobDefinitionId = GetWorkItemAttributeValue(wItem, "jobDefinitionId");
        notification.JobTemplateId = GetWorkItemAttributeValue(wItem, "jobTemplateId");
        notification.JobSchedulerId = GetWorkItemAttributeValue(wItem, "jobSchedulerId");
        notification.ManifestName = GetWorkItemAttributeValue(wItem, "manifestName");
        notification.InstanceFileName = GetWorkItemAttributeValue(wItem, "instanceFileName");
        notification.QueueTime = GetWorkItemAttributeValue(wItem, "queueTime");
        notification.Progress = GetWorkItemAttributeValue(wItem, "progress");
        notification.SubmitTime = GetWorkItemAttributeValue(wItem, "submitTime");
        notification.StartTime = GetWorkItemAttributeValue(wItem, "startTime");
        notification.EndTime = GetWorkItemAttributeValue(wItem, "endTime");
        notification.Priority = GetWorkItemAttributeValue(wItem, "priority");
        notification.Status = GetWorkItemAttributeValue(wItem, "status");
        notification.TaskCount = GetWorkItemAttributeValue(wItem, "taskCount");
        notification.TaskIndex = GetWorkItemAttributeValue(wItem, "taskIndex");
        notification.JobName = GetWorkItemAttributeValue(wItem, "jobName");
        success = true;
    catch(Exception e)
    return success;
public static string GetXElementValue(System.Xml.Linq.XElement element, System.Xml.Linq.XName name)
    string value = null; 
    if (element != null) { value = (string)element.Element(name); }
    return value;
public static string GetWorkItemAttributeValue(System.Xml.Linq.XElement element, System.Xml.Linq.XName name)
    string value = null;
    if (element != null) { value = (string)element.Attribute(name); }
    return value;

Building the NotificationDataServiceContext Class

This class defines the table name and provides a handle to query the data. It inherits TableServiceContext, which is where most of the logic takes place.

public class NotificationDataServiceContext : Microsoft.WindowsAzure.StorageClient.TableServiceContext
    public const string TableName = "Notifications";
public IQueryable<NotificationModel> Notifications
    get { return this.CreateQuery<NotificationModel>(TableName); }
public NotificationDataServiceContext(string baseAddress, Microsoft.WindowsAzure.StorageCredentials credentials)
        : base(baseAddress, credentials)

Building the NotificationDataSource Class

This class provides an abstraction from the NotificationDataServiceContext class above. We are only going to implement a Select method to fetch all of the data, and add a means to add new notifications to the database. A full implementation of this model could include much more functionality, such as Delete and Update.

using Microsoft.WindowsAzure.StorageClient;
public class NotificationDataSource
    private NotificationDataServiceContext _serviceContext = null;
    public NotificationDataSource()
        var account = Microsoft.WindowsAzure.CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
        _serviceContext = new NotificationDataServiceContext(account.TableEndpoint.ToString(), account.Credentials);
        _serviceContext.RetryPolicy = RetryPolicies.Retry(3, TimeSpan.FromSeconds(1)); 
        CloudTableClient client = account.CreateCloudTableClient(); 
    public IEnumerable<NotificationModel> Select()
        var results = from c in _serviceContext.Notifications
                      select c ;
        var query = results.AsTableServiceQuery<NotificationModel>();
        var queryResults = query.Execute();
        return queryResults;
    public void Insert(NotificationModel newItem)
        _serviceContext.AddObject(NotificationDataServiceContext.TableName, newItem);

Building the NotificationComparer Class

This class is used to ensure that the data is displayed in the right order. We want the latest POST to be at the top of the list. We will also use this to apply a Distinct() filter to avoid multiple rows per job instance.

public class NotificationComparer: IEqualityComparer<NotificationModel>, IComparer<NotificationModel>
    public bool Equals(NotificationModel x, NotificationModel y)
        //Check whether the compared objects reference the same data.
        if (Object.ReferenceEquals(x, y)) return true;
//Check if any of the compared objects are null.
if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
    return false;
    //Check if the products' properties are equal.
    return x.JobName == y.JobName;
public int GetHashCode(NotificationModel obj)
    int hashProductName = 0;
    if (!Object.ReferenceEquals(obj, null) && !string.IsNullOrEmpty(obj.JobName))
       hashProductName= obj.JobName.GetHashCode();
    return hashProductName;
    public int Compare(NotificationModel x, NotificationModel y)
        if (Object.ReferenceEquals(x, null))
            // If x is null and y is null, they're equal. otherwise Y is greater
            if (Object.ReferenceEquals(y, null)) 
            { return 0; }
            { return 1; }
            // x is not null
            //If y is null X is greater otherwise... 
            if (Object.ReferenceEquals(y, null)) 
            { return -1; }
                //if they are equal otherwise ...
                if (x.PostedDateTime == y.PostedDateTime) 
                { return 0; }
                    //If x is greater , x is greater otherwise y is greater
                    if (DateTime.Parse(x.PostedDateTime) > DateTime.Parse(y.PostedDateTime))
                    { return -1; }
                    { return 1; }

We now have a model that can be leveraged to parse new notifications, insert them into a database, and select distinct jobNames in a sorted order. We can now start configuring a Controller.

Adding a Controller

  1. In Solution Explorer, in the ASP.NET MVC 3 Web Application Project, right-click Controllers, and then click Add > Controller.

  2. In the Add Controller dialog box, add an empty controller named JobMonController, and then click Add.

  3. Update the new JobMonController class syntax with the code below. This code adds two public functions, one to handle Job Monitor pageview requests from users and another one to handle Transform Manager POST requests.


    The POST function must accept un-authenticated requests because IIS Transform Manager doesn't use authentication to POST the updates.

    public class JobMonController : Controller
        public ActionResult Index()
            List<Models.NotificationModel> tms = new List<Models.NotificationModel>();
            Models.NotificationDataSource data = new Models.NotificationDataSource();
            Models.NotificationComparer comparer = new Models.NotificationComparer();
            IEnumerable<Models.NotificationModel> notifications;
        notifications = data.Select();
        //Azure doesn't support Distinct() so we must convert this to a list<T>
        foreach (Models.NotificationModel nm in notifications)
        //Sort the POST to ensure the latest POST is at the top
        //Send a distinct job instance list to the View
        return View(tms.Distinct(comparer));
    public void Post()
            // Recommended: Filter the incoming IP address before processing
            // Optionally, use configuration settings to retrieve the IP address list
            ICollection<string> ValidIPs = new List<string>();
            string Ip = (Request.ServerVariables["HTTP_X_FORWARDED_FOR"] == null) ? 
                Request.ServerVariables["REMOTE_ADDR"] : Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
            if (ValidIPs.Contains(Ip.Split(',')[0]))
                // Get the XML data from the request
                byte[] bArr = this.Request.BinaryRead(this.Request.TotalBytes);
                string content = System.Text.Encoding.UTF8.GetString(bArr, 0, bArr.Length);
                //Parse the XML to an instance object and insert it into the database
                Models.NotificationModel tm;
                if (Models.NotificationModel.TryParse(content, out tm))
                    tm.PostedDateTime = DateTime.Now.ToString();
                    Models.NotificationDataSource data = new Models.NotificationDataSource();

Creating a View

  1. In Solution Explorer, in the ASP.NET MVC 3 Web Application Project, right-click Views, and then click Add > New Folder.

  2. Name the new folder JobMon. It is important that this name match the controller name's prefix.

  3. Right-click the JobMon folder, and then click Add > View. The Add View dialog box is displayed.

  4. Configure the following settings in the dialog box, and then click Add:

    • View name. Enter the name of the existing public function of our controller class: Index.

    • Create a strongly-typed view. Select this check box.

    • Model class. Select NotificationModel in the list.

    • Scaffold template. Select List in the list.


      Optionally, we can edit the .\Views\JobMon\Index.cshtml file to update the displayed table columns. First, we can remove most of the columns in the table header:

      <table class="dataTable">

      Then, we must update the content rows to map to the columns in the updated table header.

      @foreach (var item in Model) {
                  @Html.DisplayFor(modelItem => item.PostedDateTime)
                  @Html.DisplayFor(modelItem => item.Queue)
                  @Html.DisplayFor(modelItem => item.InstanceFileName)
                  @Html.DisplayFor(modelItem => item.TaskCount)
                  @Html.DisplayFor(modelItem => item.TaskIndex)
                  @Html.DisplayFor(modelItem => item.Log)
  5. (Optional) Add some style.

    The ASP.NET MVC 3 Web Application Project contains a file that hosts the Cascading Style Sheets (CSS) for the Views. We can make the report table borders a bit more distinct. If you know a great web designer, now is a good time to ask for the latest HTML 5 table mockups and CSS classes. For now, we can edit the project file \Content\Site.css and paste some extra style for adding table borders.

    table {
        border: solid 1px #e8eef4;
        border-collapse: collapse;
    table td {
        padding: 5px;
        border: solid 1px #e8eef4;
    table th {
        padding: 6px 5px;
        text-align: left;
        background-color: #e8eef4;
        border: solid 1px #e8eef4;

Testing the Web Application in Visual Studio

When we run the Windows Azure solution for the first time, we should see the web browser open with a web page that looks similar to this.

Earlier we wrote a Post function within the JobMonController to allow Transform Manager to send us job instance updates. Transform Manager can access this function by sending notifications to the following URI:, where JobMon is the controller name and Post is the name of the public function in the controller. We will need to remember the URI value when we configure the notifications feature in Transform Manager, for testing against the Windows Azure Development Fabric.

One very important detail to note is the port value. When the Azure Development Fabric launches, its logic initializes the port on which the Web Application will be served. During the development phase of this project, that port number is certain to change from time to time. As we can see from the previous screenshot, the port number is set to 91.

On a cleaner machine, the port value is usually 81 and increases from there. Check the port value whenever you run the project. If it has changed, you will need to update the notifications URI setting in Transform Manager with the new port value (for example to To minimize the number of port changes, wait until Transform Manager is idle (not running any jobs and posting any updates). Otherwise, if Transform Manager is actively using that port, the Azure Development Fabric will determine that the port is in use and try the next one.

Testing the Web Application in Transform Manager

To test all of the XML elements, including the <log />, we can configure a new watch folder in Transform Manager in which all jobs will fail. First we will create a new job template with a command-line task that calls a non-existent executable file, and then we will bind the job template to a new watch folder.

You can install Transform Manager either on the local computer or on a remote computer. For requirements and installation instructions, see the Installation Notes in the Transform Manager Readme.

Create a new Job Template
  1. In Transform Manager, in the Connections pane, select Job Templates.
  2. In the Actions pane, click New.
  3. In the New Job Template property sheet on the Basic Settings tab, enter a friendly name for the job template in Name.
  4. Click the Add button to add a task to the Task definitions list.
  5. In the Add Tasks dialog box, in the Available tasks list, select the built-in Command Line task, and then click OK.
  6. In the New Job Template property sheet, select the Command Line task in the Task definitions list, and then click Edit.
  7. In the Edit Command Line Task dialog box, in Command line, enter a command-line string that will cause all jobs to fail. The easiest way to do this is to specify the name of an executable file that doesn't exist.
  8. Click OK to close the Edit Command Line Task dialog box, and then click OK to close the New Job Template property sheet.
Create a new Watch Folder
  1. In Transform Manager, in the Connections pane, select Watch Folders.

  2. In the Actions pane, click New.

  3. In the New Watch Folder property sheet, on the Basic Settings tab, specify values for Name, Job template, Physical path, and File filter as shown in the following screenshot.


    In Job template, be sure to select the new job template we created in the previous section. For more information about how to configure watch folder settings, see Configuring Watch Folders .

  4. Click the Notifications tab and configure the URI and event options.

    For testing purpose, we will enter the URI of the active running project and select all of the event check boxes. For more information about how to configure these settings, see Configuring Job Status Notifications .

  5. Click OK to close the New Watch Folder property sheet.

  6. Select the new watch folder in the Watch Folders page, and then in the Actions pane, Enable and then Start the watch folder.

  7. Make sure that the Azure Web Application is running and listening to incoming requests.

  8. In the Actions pane, click Explore Watch Folder to open the watch folder directory.

  9. Drop a file that matches the watch folder's File filter setting into the watch folder directory. In our case, this is a .txt file.

The dropped file should disappear after a short time. This means that Transform Manager has picked up the file and started to process it as a job instance.

Refresh the Report

In our Azure ASP.NET MVC3 Web Application, we should start seeing some data in the table. The following screenshot displays the data that appears after our test job has failed.

From this we can see that the job failed trying to run BadFailingCommandLine.exe because the system could not find the file specified. It's quite obvious because we know that the executable file doesn't exist.

Closing Notes

During this walkthrough we saw the benefits of having an external facing web application hosting the IIS Transform Manager job status updates. We learned about the POST request message body that is sent to notify our web application of status updates. Then, we looked at how such a web application could be built using an ASP.NET MVC3 Web Application hosted within the Azure Fabric. Finally, we finished the walkthrough with a simple test to view a failed job's instance log entries.