ASP.NET MVC 4 – Auto-correcting unknown actions

During the IT Camp 2013 “MVC – Common pitfalls and how to resolve them”, Andrei Ignat, the speaker gave us some homework. His question was something like this: “How can you auto-correct misspelled actions inside the controller and redirect the user to the right action”. For example if the user types “URL/Company/Edt” instead of “URL/Company/Edit”, how can we, as software developers handle this and redirect the user to the correct action? Is Auto-correcting unknown actions so hard to do?

The answer is pretty simple. All you have to do is to override the HandleUnknownAction method inside the controller, enumerate the available actions using .NET Reflection and get the action name that “sounds like” the action the user typed.

For the “sounds like” part all you have to do is to implement the Levenshtein distance algorithm. The Levenshtein distance is a string metric used to measure the difference between two strings. The smaller the distance the better the “sounds like” part. So, here is my auto-correcting unknown actions homework.

I took the implementation for Levenshtein distance from Sten Hjelmqvist`s article on CodeProject.
You can view the article here: Fast, memory efficient Levenshtein algorithm

Below is the full source code for the Levenshtein distance implementation.

public class Levenshtein
{
    /// <summary>
    /// Calculates the levenstein distance between two strings
    /// </summary>
    public int Compute(string inputA, string inputB)
    {
        int aLen = inputA.Length;
        int bLen = inputB.Length;
        int aIdx;                
        int bIdx;                
        char a_i;                
        char b_j;                
        int cost;                  

        /// Test string length
        if (Math.Max(inputA.Length, inputB.Length) > Math.Pow(2, 31))
            throw (new Exception("\nMaximum string length exceded." ));

        // Step 1

        if (aLen == 0)
        {
            return bLen;
        }

        if (bLen == 0)
        {
            return aLen;
        }

        /// Create the two vectors
        int[] v0 = new int[aLen + 1];
        int[] v1 = new int[aLen + 1];
        int[] vTmp;

        /// Step 2
        /// Initialize the first vector
        for (aIdx = 1; aIdx <= aLen; aIdx++)
        {
            v0[aIdx] = aIdx;
        }

        // Step 3

        /// Fore each column
        for (bIdx = 1; bIdx <= bLen; bIdx++)
        {
            /// Set the 0'th element to the column number
            v1[0] = bIdx;

            b_j = inputB[bIdx - 1];


            // Step 4

            /// Fore each row
            for (aIdx = 1; aIdx <= aLen; aIdx++)
            {
                a_i = inputA[aIdx - 1];


                // Step 5

                if (a_i == b_j)
                {
                    cost = 0;
                }
                else
                {
                    cost = 1;
                }

                // Step 6

                /// Find minimum
                int m_min = v0[aIdx] + 1;
                int b = v1[aIdx - 1] + 1;
                int c = v0[aIdx - 1] + cost;

                if (b < m_min)
                {
                    m_min = b;
                }
                if (c < m_min)
                {
                    m_min = c;
                }

                v1[aIdx] = m_min;
            }

            /// Swap the vectors
            vTmp = v0;
            v0 = v1;
            v1 = vTmp;

        }


        // Step 7

        /// Value between 0 - 100
        /// 100==perfect match 0==totaly different
        /// 
        /// The vectors where swaped one last time at the end of the last loop,
        /// that is why the result is now in v0 rather than in v1
        System.Console.WriteLine("iDist=" + v0[aLen]);
        int max = System.Math.Max(aLen, bLen);
        return 100 - ((100 * v0[aLen]) / max);
    }
}

After handling the “sounds like” issue we can write the actual code to correct unknown actions

protected override void HandleUnknownAction(string actionName)
{
    //Use reflection to get information about the current controller
    ReflectedControllerDescriptor controllerDesc 
        = new ReflectedControllerDescriptor(this.GetType());

    //Get all action names from the current controller
    IEnumerable<string> allActions = controllerDesc.GetCanonicalActions().Select(a => a.ActionName);

    //Declare some variables
    Levenshtein levenshtein = new Levenshtein();
    string bestMatch = actionName;
    int closestSoundsLike = 0, crtSoundsLike;

    //Get the action that "sounds more like" the action the user has typed
    foreach (string action in allActions)
    {
        crtSoundsLike = levenshtein.Compute(action, actionName);

        if (crtSoundsLike > closestSoundsLike)
        {
            closestSoundsLike = crtSoundsLike;
            bestMatch = action;
        }
    }

    //Render the corrected view
    this.View(bestMatch).ExecuteResult(this.ControllerContext);
}

T3ZlciBBbmQgT3V0IQ==

Recent Posts

Be First to Comment

Leave a Reply