Web Test Recorder Plug-in: Extract Session ID from URI Path

ASP.NET session state enables the Web applications to store and retrieve values for a user as the user navigates ASP.NET pages. By default, ASP.NET session state is enabled for all ASP.NET applications.

Each Web session is identified by a unique identifier, i.e. SessionID. By default, SessionID value is stored in a cookie. However, the application can be configured to store SessionID value in the URL for a "cookieless" session, i.e. you will have the following configured in your Web site’s app.config file.

<system.web>
  <sessionState cookieless="true" />
</system.web>

When the SessionID is passed in the URL, VSTS does not automatically promote the correlation after Web test recording or in a correlation run. This following post will describe the details on how to extract the SessionID and correlate the recorded Web test requests by implementing a Web test recorder plug-in.

The following screenshots show the differences in the Web tests before and after enabling the SessionID recorder plug-in.

Before enabling the SessionID recorder plug-in                       After enabling the SessionID recorder plug-in
image                       image

Step 1 – Create a C# class library

Step 2 – Add a reference to Microsoft.VisualStudio.QualityTools.WebTestFramework.

Step 3 - Write an extraction rule to extract the SessionID from a url.

[ExtractionRule.cs]

 using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.WebTesting;
using System.Globalization;
using System.Text.RegularExpressions;
using System.ComponentModel;

namespace SampleRecorderPlugin
{
    //---------------------------------------------------------------------------------
    // This class creates a custom extraction rule named "Extract Session ID from Path"
    //---------------------------------------------------------------------------------
    public class ExtractSessionIDFromPath : ExtractionRule
    {
        /// Specify a name for use in the user interface.
        /// The user sees this name in the Add Extraction dialog box.
        //---------------------------------------------------------------------
        public override string RuleName
        {
            get { return "ExtractSessionIDFromPath"; }
        }

        /// Specify a description for use in the user interface.
        /// The user sees this description in the Add Extraction dialog box.
        //---------------------------------------------------------------------
        public override string RuleDescription
        {
            get { return "Extract Session ID from Path"; }
        }

        internal enum SearchResult
        {
            Found,
            StartsWithNotFound,
            EndsWithNotFound,
        }

        public string StartsWith
        {
            get { return m_startsWith; }
            set { m_startsWith = value; }
        }

        public string EndsWith
        {
            get { return m_endsWith; }
            set { m_endsWith = value; }
        }

        [DefaultValue(false)]
        public bool IgnoreCase
        {
            get { return m_ignoreCase; }
            set { m_ignoreCase = value; }
        }

        [DefaultValue(true)]
        public bool IsRequired
        {
            get { return m_required; }
            set { m_required = value; }
        }

        internal void Initialize()
        {
            string startsWith = StartsWith;
            string endsWith = EndsWith;
            startsWith = Regex.Escape(StartsWith);
            endsWith = Regex.Escape(EndsWith);

            RegexOptions options = RegexOptions.Singleline;
            if (IgnoreCase)
            {
                options |= RegexOptions.IgnoreCase;
            }

            m_startsWithRegEx = new Regex(startsWith, options);
            m_endsWithRegEx = new Regex(endsWith, options);

            m_result = null;
        }

        // Finds the match
        internal SearchResult FindMatch(string url)
        {
            Match startMatch;
            Match endMatch;
            int startIndex = 0;
            int endIndex = 0;

            if (!string.IsNullOrEmpty(StartsWith))
            {
                // Find the start
                startMatch = m_startsWithRegEx.Match(url, 0);
                if (!startMatch.Success)
                {
                    return SearchResult.StartsWithNotFound;
                }
                startIndex = startMatch.Index;
            }

            if (!string.IsNullOrEmpty(EndsWith))
            {
                // Find the end
                endMatch = m_endsWithRegEx.Match(url, startIndex);
                if (!endMatch.Success)
                {
                    return SearchResult.EndsWithNotFound;
                }
                endIndex = endMatch.Index + EndsWith.Length;
            }

            m_result = url.Substring(startIndex, endIndex - startIndex);
            return SearchResult.Found;
        }


        // Searches for the SessionID in the path.
        public override void Extract(object sender, ExtractionEventArgs e)
        {
            // Finds the match
            Initialize();
            SearchResult result = FindMatch(e.Request.Url);
            e.Success = !m_required || result == SearchResult.Found; ;

            if (result == SearchResult.Found)
            {
                e.WebTest.Context[this.ContextParameterName] = Result;
            }
            else
            {
                if (result == SearchResult.StartsWithNotFound)
                {
                    e.Message = String.Format(CultureInfo.CurrentCulture, "StartsWith not found");
                }
                else
                {
                    e.Message = String.Format(CultureInfo.CurrentCulture, "EndsWith not found");
                }
            }
        }

        // Called in the Recorder Plug-in to retrieve the sessionID string
        public string ExtractSessionID(string url)
        {
            Initialize();
            FindMatch(url);
            return Result;
        }
        // ************************************************************************
        // ************************************************************************

        internal string Result
        {
            get { return m_result; }
            set { m_result = value; }
        }

        // ****************************************************************************
        // Private Members
        // ****************************************************************************

        // Properties for saving the tag and attribute information
        private string m_startsWith;
        private string m_endsWith;
        private Regex m_startsWithRegEx;
        private Regex m_endsWithRegEx;

        private bool m_required = true;
        private bool m_ignoreCase = true;
        private string m_result;
    }
}

If you want to modify the extraction rule for your testing, the generic information to write a custom extraction rule can be found in this MSDN page.

Step 4 – Write a Web test recorder plug-in

This plug-in will extract the SessionID from a request url or the redirected page, add an extraction rule created in Step 2 to the request, save the SessionID in a Web test context parameter called “SessionID”, and correlate the SessionID in the recorded Web test requests.

[RecorderPlugin.cs]

 using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.WebTesting;
using System.Globalization;
using System.Text.RegularExpressions;
using System.ComponentModel;
using System.Diagnostics;

namespace SampleRecorderPlugin
{
    [DisplayName("WebTestRecorderPluginSessionIdInPath")]
    [Description("Adds a recorder plug-in that retrieves session ID from url")]
    public class RecorderPluginThatExtractsSessionIDInPath : WebTestRecorderPlugin
    {
        public override void PostWebTestRecording(object sender, PostWebTestRecordingEventArgs e)
        {
            ExtractSessionIDFromPath extractionRule = new ExtractSessionIDFromPath();
            extractionRule.StartsWith = "/(S(";
            extractionRule.EndsWith = "))/";
            extractionRule.IgnoreCase = true;

            string sessionID = null;
            bool foundSessionID = false;

            WebTestResultIteration recordedIteration = e.RecordedWebTestResult;
            foreach (WebTestResultUnit resultUnit in recordedIteration.Children)
            {
                WebTestResultPage pageResult = resultUnit as WebTestResultPage;
                if (pageResult != null)
                {
                    WebTestRequestResult finalRequestResult = pageResult.RequestResult;                  
                    string url = pageResult.RequestResult.Request.Url;
                    // If there were redirects then need to get the last redirected requests
                    if (pageResult.RedirectedPages.Count > 0)
                    {
                        finalRequestResult = pageResult.RedirectedPages[pageResult.RedirectedPages.Count - 1].RequestResult;
                        url = finalRequestResult.Request.Url;
                    }

                    // Check if there is an session ID in the url path
                    sessionID = extractionRule.ExtractSessionID(url);
                    if (string.IsNullOrEmpty(sessionID))
                    {
                        continue;
                    }

                    // Get the corresponding request in the Declarative Web test
                    WebTestRequest requestInWebTest =
                            e.RecordedWebTest.GetItem(pageResult.DeclarativeWebTestItemId) as WebTestRequest;

                    // Add an custom extraction rule to the current request
                    if (requestInWebTest != null)
                    {
                        ExtractionRuleReference extractionRuleReference = new ExtractionRuleReference();
                        extractionRuleReference.Type = typeof(ExtractSessionIDFromPath);
                        extractionRuleReference.Properties.Add(new PluginOrRuleProperty("StartsWith", @"/(S("));
                        extractionRuleReference.Properties.Add(new PluginOrRuleProperty("EndsWith", @"))/"));
                        extractionRuleReference.Properties.Add(new PluginOrRuleProperty("IgnoreCase", "false"));
                        extractionRuleReference.Properties.Add(new PluginOrRuleProperty("IsRequired", "true"));
                        extractionRuleReference.ContextParameterName = "SessionID";
                        requestInWebTest.ExtractionRuleReferences.Add(extractionRuleReference);
                        e.RecordedWebTestModified = true;
                    }

                    foundSessionID = true;
                    break;
                }
            }

            // replace the session ID with the context parameter
            if (foundSessionID && !string.IsNullOrEmpty(sessionID))
            {
                for (int i = 0; i < e.RecordedWebTest.Items.Count; i++)
                {
                    //get the item and see if it is a comment
                    WebTestItem item = e.RecordedWebTest.Items[i];
                    
                    if (item is WebTestRequest)
                    {
                        ((WebTestRequest)item).Url = ((WebTestRequest)item).Url.Replace(sessionID, "{{SessionID}}");
                    }
                }
            }
        }
    }
}

Step 5 – Drop the dll to %Program Files%\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\WebTestPlugins

In order to use the extraction rule created in Step 2, you will need to restart Visual Studio and add a reference to the class library to your test project. When you record a new Web test from Visual Studio, a dialog will be popped up and let you enable the RecorderPluginThatExtractsSessionIDInPath. Once the plug-in is enabled, the requests will be automatically correlated.

There is one more step. Since the request url is built dynamically during your run, the last step is to remove the default validation rule in the recorded Web test. Otherwise, the requests will fail due to validation errors. To do this, click on the “Response Time Goal” under Validation Rules, and press the Delete key.