Calling WebServices Asynchronously in .Net: Unique state object is required for multiple asynchronous simultaneous operations

When you call a web service aynchronously, you may want to make sure that responses arrive in the same order in which they were requested.

Is this always the case? Not necessarily. At times you just don’t care if the responses to requests A and B come in order B and A. Other times, order is crucial. One would argue that in these cases synchronous methods are better than asynchronous, but – especially when you are using third party web services, or particular technologies (PCLs are an example) – you cannot always choose to “go synchronously”.

.Net helps you enforce the correct request – response order by throwing the error that is the subject of this post. If a client requests multiple async methods in a short period of time, .Net tries to prevent the inconsistencies by complaining thusly:

ERROR: System.ArgumentException: There was an error during asynchronous processing. Unique state object is required for multiple asynchronous simultaneous operations to be outstanding. ---> System.ArgumentException: Item has already been added. Key in dictionary: 'System.Object' Key being added: 'System.Object' at System.Collections.Hashtable.Insert(Object key, Object nvalue, Boolean add)

When does this happen? Is .Net mean to us?

It happens when you call an async web service method before the previous call has received its

completed

event.

Let us reproduce the issue.
Let us imagine we have a web service that we reference in a Console project with a

myWSRef

reference.
Let us imagine the service exposes an async method called

getProductDataAsync(manufactorCode,productId)

Our client repeatedly calls the async service in a while-loop, like this:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace testwcf
{
    class Program
    {
       static void Main(string[] args)
        {
            string manufactorCode="blahblahCode";
            string productId = "blahblahCode2";

            // we define the client for the WS
            myWSRef.RemoteWS wsClient = new myWSRef.RemoteWS ();

            // we attach a handler to the "Completed" event
            wsClient.getProductDataResponseCompleted += callToProductDataCompleted;
            
            int prodNumb=0;
            while (prodNumb<=100)
            {
                try
                {
                    prodNumb++;
                    string artificialSuffix=new Random().Next().ToString();
                    wsClient.getProductDataAsync(manufactorCode,productId+artificialSuffix);
                }
                catch (Exception e)
                {
                    Console.WriteLine(e);
                }
            }
            Console.ReadLine();
        }

        public static void callToProductDataCompleted(object sender, myWSRef.GetProductDataCompletedEventArgs ea)
        {
            //this is the handler to the webserver reply

            if (ea.Error != null){
                Console.WriteLine("ERROR: " + ea.Error);
                Debug.WriteLine("ERROR: " + ea.Error);
             }
            else {
                Console.WriteLine("Call OK: ");
                Console.WriteLine(ea.Result);
            }
        }
    }
}

What happens if we run this? .Net will throw the runtime error that is the subject of this post.

Let us try to distantiate the calls by asking the Thread to sleep one second.

try
                {
                    prodNumb++;
                    string artificialSuffix=new Random().Next().ToString();
                    wsClient.EditorProductsDataResponse2Async(authCode, editorId, productId + artificialSuffix, idMachine, true, year);
                    Thread.Sleep(1000);
                 }

What happens? Temporarily, the error goes away. In fact, one second is enough for the web service response to call the callToProductDataCompleted routine, and we’re safe… unless… the next call takes three seconds rather than one second. And we’re back to square one.

How to solve this issue for good? Many suggest that every call has a unique GUID.

Stackoverflow offers
this suggestion: every call passes its own Guid to the service.

What about when the web service is done by someone else and you cannot pass it any uniqueID?

One way to solve this is with a semaphore: when the response of a web service call has not been received, you cannot call another one.

This is the code, with the IsBusy semaphore implemented

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace testwcf
{
    class Program
    {

        static Boolean IsBusy = false;  // this will be our semaphore
        static void Main(string[] args)
        {
            string manufactorCode="blahblahCode";
            string productId = "blahblahCode2";

            // we define the client for the WS
            myWSRef.RemoteWS wsClient = new myWSRef.RemoteWS ();

            // we attach a handler to the "Completed" event
            wsClient.getProductDataResponseCompleted += callToProductDataCompleted;
            
            int prodNumb=0;
            while (prodNumb<=100)
            {
               // if IsBusy, it means I am in the middle of a call
                if (IsBusy)
                {
                    continue; //I wait until the web service is not being called by another client still
                }
                try
                {
                    prodNumb++;
                    string artificialSuffix=new Random().Next().ToString();
                    wsClient.getProductDataAsync(manufactorCode,productId+artificialSuffix);
                }
                catch (Exception e)
                {

                    Console.WriteLine(e);
                    // treat the exception, then
                    IsBusy=false; // free the "semaphore" for another call
            }
            Console.ReadLine();
        }

        public static void callToProductDataCompleted(object sender, myWSRef.GetProductDataCompletedEventArgs ea)
        {
            //this is the handler to the webserver reply
             IsBusy = false; // when the WS call has been dutifully served, we "free" the loop to serve another one
            if (ea.Error != null){
                Console.WriteLine("ERROR: " + ea.Error);
                Debug.WriteLine("ERROR: " + ea.Error);
             }
            else {
                Console.WriteLine("Call OK: ");
                Console.WriteLine(ea.Result);

            }
        }
    }
}

There are other methods to avoid the request overlapping, of course, like calling the following method inside the Completed callback function (iteration). Just remember that the callToProductDataCompleted method is called even when the web server throws an error (as: 400 Bad request), so the method is the place to handle those exceptions. By contrast, client errors (as a timeout or network error) will be caught by the catch block of the getProductDataAsync call.

Controller versus view in MVC .net: is the code in the view as fast as that in the controller? Is it slower?

One of the basic rules of MVC is that views should be only – exactly – views, that is to say: objects that present to the user something that is already “worked and calculated”.

They should perform little, if not none at all, calculation. All the significant code should be in the controllers. This allows better testability and maintainability.

Is this, in Microsoft’s interpretation of MVC, also justified by performance?

We tested this with a very simple code that does this:

– creates 200000 “cat” objects and adds them to a List

– creates 200000 “owner” objects and adds them to a List

– creates 200000 “catowner” objects (the MTM relation among cats and owners) and adds them to a List

– navigates through each cat, finds his/her owner, removes the owner from the list of owners (we don’t know if cats really wanted this, but their freedom on code fits our purposes).

We’ve run this code in a controller and in a razor view.

The result seem to suggest that the code in views runs just as fast as in controllers even if don’t pre-compile views (the compilation time in our test is negligible).

The average result for the code with the logic in the controller is 18.261 seconds.

The average result for the code with the logic in the view is 18.621 seconds.

The performance seems therefore very similar.

Here is how we got to this result.

Case 1: Calculations are in the CONTROLLER

Models:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebPageTest.Models
{
public class Owner
{
public string Name { get; set; }
public DateTime DOB { get; set; }
public virtual CatOwner CatOwner { get; set; }
}
public class Cat
{
public string Name { get; set; }
public DateTime DOB { get; set; }
public virtual CatOwner CatOwner { get; set; }
}
public class CatOwner
{
public virtual Cat Cat { get; set; }
public virtual Owner Owner { get; set; }
}
}

Controller:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using WebPageTest.Models;

namespace WebPageTest.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
Stopwatch howLongWillItTake = new Stopwatch();
howLongWillItTake.Start();
List<Owner> allOwners = new List<Owner>();
List<Cat> allCats = new List<Cat>();
List<CatOwner> allCatOwners = new List<CatOwner>();
// create lists with 200000 cats, 200000 owners, 200000 relations
for (int i = 0; i < 200000; i++)
{
//Cat
Cat CatX = new Cat();
CatX.Name = “Cat ” + i.ToString();
CatX.DOB = DateTime.Now.AddDays(i / 10);
//Owner
Owner OwnerX = new Owner();
OwnerX.Name = “Owner ” + i.ToString();
OwnerX.DOB = DateTime.Now.AddDays(-i / 10);
//Relationship “table”
CatOwner CatOwnerXX = new CatOwner();
CatOwnerXX.Cat = CatX;
// Relations
CatOwnerXX.Owner = OwnerX;
CatX.CatOwner = CatOwnerXX;
OwnerX.CatOwner = CatOwnerXX;
//add to list
allCats.Add(CatX);
allOwners.Add(OwnerX);
allCatOwners.Add(CatOwnerXX);
}
// now I remove all the items
foreach (Cat CatToDelete in allCats)
{
Owner OwnerToRemove = CatToDelete.CatOwner.Owner;
allOwners.Remove(OwnerToRemove);
}
// now all cats are free
int numberOfCats = allCats.Count();
int numberOfOwners = allOwners.Count();
howLongWillItTake.Stop();
long elapsedTime = howLongWillItTake.ElapsedMilliseconds;
// give info to the view
ViewBag.numberOfCats = numberOfCats;
ViewBag.numberOfOwners = numberOfOwners;
ViewBag.elapsedTime = elapsedTime;
return View();
}
}
}

View:

<div class=”row”>
<div class=”col-md-12″>
<hr />
<b>Results</b>
<br/>
Cats: @ViewBag.numberOfCats
<br/>
Owners: @ViewBag.numberOfOwners
<br/>
ElapsedTime in milliseconds: @ViewBag.ElapsedTime
<hr />
</div>
</div>

Case 2: Calculations are in the VIEW (pre-compiled)

Models: same as above

Controller:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace WebPageTest.Controllers
{
public class HomeBisController : Controller
{
public ActionResult Index()
{
return View();
}
}
}

View:

@using System;
@using System.Collections.Generic;
@using System.Diagnostics;
@using System.Linq;
@using System.Web;
@using WebPageTest.Models;
@using System.Web.Mvc;
@{
Stopwatch howLongWillItTake = new Stopwatch();
howLongWillItTake.Start();
List<Owner> allOwners = new List<Owner>();
List<Cat> allCats = new List<Cat>();
List<CatOwner> allCatOwners = new List<CatOwner>();
//create lists with 200000 cats, 200000 owners, 200000 relations
for (int i = 0; i < 200000; i++)
{
//Cat
Cat CatX = new Cat();
CatX.Name = “Cat ” + i.ToString();
CatX.DOB = DateTime.Now.AddDays(i / 10);
//Owner
Owner OwnerX = new Owner();
OwnerX.Name = “Owner ” + i.ToString();
OwnerX.DOB = DateTime.Now.AddDays(-i / 10);
//Relationship “table”
CatOwner CatOwnerXX = new CatOwner();
CatOwnerXX.Cat = CatX;
// Relations
CatOwnerXX.Owner = OwnerX;
CatX.CatOwner = CatOwnerXX;
OwnerX.CatOwner = CatOwnerXX;
//add to list
allCats.Add(CatX);
allOwners.Add(OwnerX);
allCatOwners.Add(CatOwnerXX);
}
// now I remove all the items
foreach (Cat CatToDelete in allCats)
{
Owner OwnerToRemove = CatToDelete.CatOwner.Owner;
allOwners.Remove(OwnerToRemove);
}
// now all cats are free
int numberOfCats = allCats.Count();
int numberOfOwners = allOwners.Count();
howLongWillItTake.Stop();
long elapsedTime = howLongWillItTake.ElapsedMilliseconds;
// give info to the view

}
<div class=”row”>
<div class=”col-md-12″>
<hr />
<b>Results</b>
<br />
Cats: @numberOfCats
<br />
Owners: @numberOfOwners
<br />
ElapsedTime in milliseconds: @elapsedTime
<hr />
</div>
</div>

How fast is classic ADO.net compared to Entity Framework?

Or maybe I should write: how slower is Entity Framework as compared to ADO.Net?

By Entity Framework I mean Microsoft’s open source package that allows you to manage DB objects via strongly-typed classes and collections.

By ADO.Net I mean peeking into the DB using the old ADO objects SQLConnection, SQLCommand, SQLParameters

This is the little test (note that it is a very peculiar test because rarely will you in real life insert, update and delete objects one by one: more massive operations are more likely):

– we create two table: Books and Authors. They are related via Author_Id, which is on the Books table.

– we insert 1000 authors and 1000 books

. we update 1000 books with a new title (one by one)

– we delete 1000 books (one by one)

– DB Is SQLserver version 11, running on a quad-core i5 @1.9 Ghz running Windows 8

– Server is a Windows 8 machine with 8Gb Gb RAM

The code for Entity Framework?

Book Model

namespace FastEF.Models
{
 public class Book
 {
 public int Id { get; set; }
 public string Title { get; set; }
 public Author Author { get; set; }
 
 }
}

Author Model

namespace FastEF.Models
{
 public class Author
 {
 public int Id { get; set; }
 public string Name { get; set; }
 public string Address { get; set; }
 public ICollection<Book> Books { get; set; }
}
}

DbContext

namespace FastEF.Models
{
 public class DBCreator:DbContext
 {
 public DbSet<Book> Books { get; set; }
 public DbSet<Author> Authors { get; set; }
}
}

Then, the action from Entity Framework test, which:

– inserts 1000 auhors and 1000 books related to the authors

– updates the 1000 books

– deletes the 1000 books


 public ActionResult EF()
        {
            Book bookToCreate = new Book();
            Author authorToCreate = new Author();
            Stopwatch tellTime = new Stopwatch();
            long insertingTime = 0;
            long updatingTime = 0;
            long deletingTime = 0;
            List generatedBookIds = new List();

            // let us delete table contents
            try
            {
                var objCtx = ((System.Data.Entity.Infrastructure.IObjectContextAdapter)thisDB).ObjectContext;
                objCtx.ExecuteStoreCommand("DELETE FROM Books");
                objCtx.ExecuteStoreCommand("DELETE FROM Authors");

            }


            catch (Exception e)
            {
                // write exception. Maybe it's the first time we run this and have no tables
                Debug.Write("Error in truncating tables: {0}", e.Message);

            }

            // let us start the watch
            tellTime.Start();

            // INSERTING!
            // we create 1000 authors with name="John Doe nr: " + a GUID
            // and address ="5th Avenue nr: " + a GUID
            // we create a book called "The Cronicles of: " + a GUID and attach it to the author
            // we save the book, so the author is also automatically created

            for (int i = 0; i < 1000; i++)
            {

                // creating author
                authorToCreate = new Author();
                authorToCreate.Name = "John Doe nr. " + Guid.NewGuid();
                authorToCreate.Address = "5th Avenue nr. " + Guid.NewGuid();

                //creating book and linking it to the author
                bookToCreate = new Book();
                bookToCreate.Title = "The Chronicles of: " + Guid.NewGuid();
                bookToCreate.Author = authorToCreate;

                //saving the book. Automatically, the author is saved
                thisDB.Books.Add(bookToCreate);
                thisDB.SaveChanges();
                generatedBookIds.Add(bookToCreate.Id);
            }

            insertingTime = tellTime.ElapsedMilliseconds; // how did I do with inserting?

            tellTime.Restart(); // restart timer

            // We update the 1000 books by changing their title
            foreach (int bookId in generatedBookIds)
            {

                Book bookToUpdate = thisDB.Books.Find(bookId);
                bookToUpdate.Title = "New chronicles of: " + Guid.NewGuid();

                thisDB.SaveChanges();

            }

            updatingTime = tellTime.ElapsedMilliseconds; // how did I do with inserting?
            tellTime.Restart(); // restart timer

            // We delete 1000 books, one by one
            foreach (int bookId in generatedBookIds)
            {

                Book bookToDelete = thisDB.Books.Find(bookId);
                thisDB.Books.Remove(bookToDelete);

            }

            deletingTime = tellTime.ElapsedMilliseconds; // how did I do with inserting?
            tellTime.Stop(); // stop timer


            //printing the results 

            string returnedMessage = "Results with Entity Framwork 6.1: ";
            returnedMessage += "
1000 Insert operations in ms.: " + insertingTime.ToString(); returnedMessage += "
1000 Update operations in ms.: " + updatingTime.ToString(); returnedMessage += "
1000 Delete operations in ms.: " + deletingTime.ToString(); return Content(returnedMessage); }

The code for ADO.Net?

 public ActionResult SQLClient()
        {

            string insertAuthorSQL = "INSERT INTO Authors (Name, Address) VALUES (@name, @address)";
            string insertBookSQL = "INSERT INTO Books(Title, Author_Id) VALUES (@Title, @Author_Id)";
            string updateBookSQL = "UPDATE Books Set Title=@Title where Id=@Id";
            string deleteBookSQL = "DELETE Books where Id=@Id";

            Book bookToCreate = new Book();
            Author authorToCreate = new Author();
            Stopwatch tellTime = new Stopwatch();

            // SQL Objects we will use
            SqlConnection connAntiEF = new SqlConnection(WebConfigurationManager.ConnectionStrings["DefaultConnection"].ToString());
            SqlCommand cmdAntiEF = new SqlCommand();

            // Open Connection
            connAntiEF.Open();

            long insertingTime = 0;
            long updatingTime = 0;
            long deletingTime = 0;
            List generatedBookIds = new List();

            // let us delete table contents
            try
            {
                cmdAntiEF = new SqlCommand("DELETE FROM Books", connAntiEF);
                cmdAntiEF.ExecuteNonQuery();
                cmdAntiEF = new SqlCommand("DELETE FROM Authors", connAntiEF);
                cmdAntiEF.ExecuteNonQuery();
            }


            catch (Exception e)
            {
                // write exception. 
                Debug.Write("Error in truncating tables: {0}", e.Message);

            }

            // let us start the watch
            tellTime.Start();

            // INSERTING!
            // we create 1000 authors with name="John Doe nr: " + a GUID
            // and address ="5th Avenue nr: " + a GUID
            // we create a book called "The Cronicles of: " + a GUID and attach it to the author
            // we save the book, so the author is also automatically created

            for (int i = 0; i < 1000; i++)
            {

                // creating author
                authorToCreate = new Author();
                authorToCreate.Name = "John Doe nr. " + Guid.NewGuid();
                authorToCreate.Address = "5th Avenue nr. " + Guid.NewGuid();

                //creating book and linking it to the author
                bookToCreate = new Book();
                bookToCreate.Title = "The Chronicles of: " + Guid.NewGuid();
                bookToCreate.Author = authorToCreate;

                // INSERT book with SQL and get its Id


                SqlParameter parmName = new SqlParameter("Name", authorToCreate.Name);
                SqlParameter parmAddress = new SqlParameter("Address", authorToCreate.Address);
                cmdAntiEF.CommandText = insertAuthorSQL;
                cmdAntiEF.Parameters.Add(parmName);
                cmdAntiEF.Parameters.Add(parmAddress);
                cmdAntiEF.ExecuteNonQuery();

                cmdAntiEF.Parameters.Clear();
                cmdAntiEF.CommandText = "SELECT @@IDENTITY";

                int insertedAuthorID = Convert.ToInt32(cmdAntiEF.ExecuteScalar());

                // INSERT book with SQL and get its Id


                parmName = new SqlParameter("title", bookToCreate.Title);
                parmAddress = new SqlParameter("author_id", insertedAuthorID);

                cmdAntiEF.CommandText = insertBookSQL;
                cmdAntiEF.Parameters.Add(parmName);
                cmdAntiEF.Parameters.Add(parmAddress);
                cmdAntiEF.ExecuteNonQuery();

                // we neeed the book's Id to iterate through the Id's later
                cmdAntiEF.CommandText = "SELECT @@IDENTITY";
                int insertedBookID = Convert.ToInt32(cmdAntiEF.ExecuteScalar());
                generatedBookIds.Add(insertedBookID);


                parmName = null;
                parmAddress = null;
                cmdAntiEF.Parameters.Clear();

            }


            insertingTime = tellTime.ElapsedMilliseconds; // how did I do with inserting?

            tellTime.Restart(); // restart timer

            // We update 1000 books by changing their title
            cmdAntiEF.CommandText = updateBookSQL;
            foreach (int bookId in generatedBookIds)
            {

                //parameters are loaded with the book's new data
                SqlParameter parmTitle = new SqlParameter("Title", "New chronicles of: " + Guid.NewGuid());
                SqlParameter parmId = new SqlParameter("Id", bookId);
                cmdAntiEF.Parameters.Add(parmTitle);
                cmdAntiEF.Parameters.Add(parmId);

                cmdAntiEF.ExecuteNonQuery();
                parmTitle = null;
                cmdAntiEF.Parameters.Clear();

            }

            updatingTime = tellTime.ElapsedMilliseconds; // how did I do with inserting?
            tellTime.Restart(); // restart timer

            // We delete 1000 books one by one
            cmdAntiEF.CommandText = deleteBookSQL;
            foreach (int bookId in generatedBookIds)
            {
                SqlParameter parmId = new SqlParameter("Id", bookId);
                cmdAntiEF.Parameters.Add(parmId);
                cmdAntiEF.ExecuteNonQuery();
                parmId = null;
                cmdAntiEF.Parameters.Clear();
            }

            connAntiEF.Close();

            deletingTime = tellTime.ElapsedMilliseconds; // how did I do with inserting?
            tellTime.Stop(); // stop timer

            // printing the results
            string returnedMessage = "Results with SQL Connection: ";
            returnedMessage += "
1000 Insert operations in ms.: " + insertingTime.ToString(); returnedMessage += "
1000 Update operations in ms.: " + updatingTime.ToString(); returnedMessage += "
1000 Delete operations in ms.: " + deletingTime.ToString(); return Content(returnedMessage); }

How did they do?

Entity Framework

Results with Entity Framwork 6.1:
1000 Insert operations in ms.: 11355
1000 Update operations in ms.: 20833
1000 Delete operations in ms.: 18117

Entity framework performance

Adding, updating, deleting 1000 sqlserver objects via EF

CPU average use: 35%

Memory average use: 65%

ADO.Net

Results with SQL Connection:
1000 Insert operations in ms.: 921
1000 Update operations in ms.: 309
1000 Delete operations in ms.: 311

ado.net insert and update and delete

Inserting, updating, deleting sql server objects via ado

How to interpret the results?

They cannot be compared, because using EF means using objects rather than non-typed records.

So, I keep on thinking ORMs are the way to go.

However, if one day I was asked to speed up parts of an application that is slow when reading / writing data, I would know where to go and look for possible ameliorations.

A simple redirection mechanism using MVC 5 [and IIS8], including “Legacy” URLs

For some hours, I have been looking for good ideas on how to manage redirections in MVC without necessarily configuring IIS (which is not always available to the programmer, unfortunately).

However, very few of the walkthroughs found on the web take into account dealing with “legacy” URLs; primarily, those who have the “.html” extension.

How do I, in MVC, redirect a URL like “oldfolder/oldfilename.html” to a new MVC URL as “/newfolder/beautifulpeople” ?

The answer is not the simplest, because MVC doesn’t manage all that is asked to it by the browser:

A client request is handled, summarily, like this:

– MVC manages the “MVC” routes (as “/area/controller/action”)

– IIS8 manages the static files, which *presumably* don’t need MVC’s intervention.

If we want to manage all the redirections via MVC, we have to tell MVC that it has to take care of BOTH MVC routes AND static files (unless, of course, the latter don’t exist as such. If the static file exists, let IIS transfer it to the user).

The following is a very basic mechanism which uses MVC and a DB table to store redirections. It can be extended to use caching and OR XML files to be faster. It is also true that redirections should be an exception rather than the norm.

The idea is:

– We create a DB table holding old URLs and corresponding new URLs, along with the code we want to use for the redirection (301 is permanent) and an “active” flag.

– We create a set of MVC “catchall” routes (in the RouteConfig file) which will prevent 404 (“not found”) and will call a new controller that does the “redirection” job

– This new controller checks if the DB has an entry equal to the originating URL. If there is, it redirects  the flow to the new URL (permanenty or temporarily, depending on what we wrote in the DB entry); otherwise, it generates a custom 404 page.

– We tell MVC (in web.config) that it has to also take care of the routes that look like files, unless (of course) they are actual static files.

RouteConfig

In our MVC application’s RouteConfig, we create, AFTER the standard routes, a set of “catchall” routes which will direct to a new controller and action (“Error”/”NotFound”) all the requests that could not be routed to an existing controller.

Like this:

//Default, Real route

routes.MapRoute(

name: “Default”,
url: “{controller}/{action}/{id}”,
defaults: new { controller = “Home”, action = “Index”, id = UrlParameter.Optional },
constraints: new { controller=”Home|Search|Account|Show|Other_Controllers|Error”} // CONSTRAINTS are VERY IMPORTANT
);

//catchAll routes to grab the URLs we want to redirect

routes.MapRoute(
name: “Redirect_OrError”,
url: “{*CatchAll1*}”,
defaults: new { controller = “Error”, action = “NotFound” }
);

routes.MapRoute(
name: “Redirect_OrError_two_levels”,
url: “{CatchAll1*}/{CatchAll2*}”,
defaults: new { controller = “Error”, action = “NotFound” }
);

routes.MapRoute(
name: “Redirect_OrError_three_levels”,
url: “{CatchAll1*}/{CatchAll2*}/{CatchAll3*}”,
defaults: new { controller = “Error”, action = “NotFound” }
);
routes.MapRoute(
name: “Redirect_OrError_four_levels”,
url: “{CatchAll1*}/{CatchAll2*}/{CatchAll3*}/{CatchAll4*}”,
defaults: new { controller = “Error”, action = “NotFound” }
);

Attention: The “default, real” MapRoute method must be called with Constraints (Controller=”Home|…. all the controllers you have). If you don’t do so, MVC will not fall through to our “catchall” controllers. It will stop at the default and will give a “404”.

The “Error” Controller

The “Error” Controller does three things:

1. It checks if the request is actually found in the Redirections table as a URL we consider as “old”

2. If it is there, we redirect to the new URL (if 301, “RedirectPermanent”; if others, “Redirect”)

3. If it doesn’t, it generates a custom error page with a 404 status code

public class ErrorController:Controller
{

[HttpGet]
public ActionResult NotFound()
{

string UrlToTest = Request.Url.AbsolutePath.Trim(‘/’);
Redirections redirect = RedirectManager.GetRedirectFromDB(UrlToTest);
if (redirect != null)
{
if (redirect.RedirectType==”301″)
{
return RedirectPermanent(redirect.NewURL);
}
else
{
return Redirect(redirect.NewURL);
}
}
Response.StatusCode = 404;
ViewBag.UrlToTest = UrlToTest;
return View();
}

public ErrorController()
{

}

}

Now what is the RedirectManager we just found in the code?

Very simply, it is a class which contains some helper classes and methods that allow us to examine the DB and look for the URL we want to redirect from.

public class RedirectManager

{

public RedirectManager()

{

}

public class RedirectionsDTO
{

public int Id { get; set; }
public string OldURL { get; set; }
public string NewURL { get; set; }
public string RedirectType { get; set; }
public Nullable<bool> Active { get; set; }

}

public static Redirections GetRedirectFromDB(string OldUrl)
{
RedirectionsDTO redirects;
using (var db = new OurEntitiesDB()) // this is the name of your Entity Framework
{
string URLWithoutSlash = OldUrl;
if (!URLWithoutSlash.StartsWith(“/”))
{
URLWithoutSlash = “/” + URLWithoutSlash;
}

redirects = (from e in db.Redirections
where (e.Active == true && (e.OldURL == URLWithoutSlash))
select new RedirectionsDTO
{
OldURL = e.OldURL,
NewURL = e.NewURL,
RedirectType=e.RedirectType,
Active=e.Active
}).FirstOrDefault();
if (redirects != null)
{
return new Redirections { OldURL = redirects.OldURL, NewURL = redirects.NewURL, Active=redirects.Active, RedirectType=redirects.RedirectType };
}
else
{
return null;
}

}

}

}

}

How is the Redirect table created? It is created by Entity Framework Database first, based on a table maybe similar to this…

MVC redirection storage table

Table which holds redirection data

There should be an index on the OldURL column because we will include it in our queries.

The generated code could be something like this:

namespace YourPreferredNameSpace

{
using System;
using System.Collections.Generic;

public partial class Redirections
{
public int Id { get; set; }
public string OldURL { get; set; }
public string NewURL { get; set; }
public string RedirectType { get; set; }
public Nullable<bool> Active { get; set; }
}
}

Of course, you have to fill this table with the needed redirections! I suggest you create a controller and allow scaffolding templates to automatically generate the CRUD actions and views for these “Redirections” objects.

Web.Config

What we did so far does not help us if we want to redirect *.html files (or *.pdf files, for that matter). This is beacuse IIS will think that the *.html file should not be a part of an MVC route and it will look for it as a static file. To allow MVC to see the *.html URL as a possible MVC route (and redirect it, if needed), we have to allow MVC to examine all the file requests. To do so, we have three suggested paths. There are: an easy+smart way, a smart way and a semi-silly way. The easy+smart way didn’t work for me. The “smart way” did. It happens.

The easy+smart way is to patch the IIS as this article suggests: http://support.microsoft.com/kb/980368

It didn’t work for me because I had no access to IIS.

The “smart one” is suggested by Colin Farr in this old but powerful article:

http://www.britishdeveloper.co.uk/2010/06/dont-use-modules-runallmanagedmodulesfo.html

he suggests that in System.webServers you add

<system.webServer>
<modules >
<remove name=”UrlRoutingModule-4.0″ />
<add name=”UrlRoutingModule-4.0″ type=”System.Web.Routing.UrlRoutingModule” preCondition=”” />

… other modules…

</module>

Now the silly solution instead:

add the runAllManagedModulesForAllRequests=”true” attribute to the “module” section. Colin Farr already explains why this is silly, so I will not.

<modules runAllManagedModulesForAllRequests=”true”>