Mar272010

HandleError attribute (MVC)

    In ASP.NET MVC you can decorate the action methods and the controllers with filter attributes to control their behavior.  These filters are injected into the pipeline, and depending on their type; they are executed before/after something happens. You can either use the filters that come with the framework, or write your own filters. Even though it is easy to test the functionality of the filters, it may not be easy to test if the controller/action is/are decorated with the filter (You can use reflection).  There are 4 different types of filters that come with the framework:

  • Authorization Filter: These type of filters have very high priority, and run before other types of filters or the action execution.
  • Action Filter: These type of filters have normal priority, and run before and after the action execution, by this way you can do some work before  an action is executed or after an action is executed. A good example could be logging the system activity, or calculating the time that takes to execute the action.
  • Result Filter: These type of filters have  normal priority, and run before and after the action result is executed, by this way, you can manipulate the action results.
  • Exception Filter: These filters are executed if an unhandled exception is thrown.

In this blog posting I will be explaining HandleError attribute which is a type of Exception Filter. You may want to use this filter to catch the exception for either logging the exception to a log file or display a nice error page so the user does not freak out.  If you create a default MVC project using Visual Studio, you will see that the HomeController class is decorated with HandleError filter.

[HandleError]
public class HomeController : Controller


This means whenever there is an exception that is not handled, render the Error view, so when there is an exception the framework will first look at the folder “Views/Home” and check if there is an Error.aspx page (assuming you are using the default WebForms Engine). If it can not find this page, it will check if there is a Error.ascx page, if there is none, then the same search will be applied to Shared folder. Finally if there is an Error.aspx/ascx page found it will be rendered, otherwise, in the production system, IIS will handle error, in the development system you may see the error below (depending on your web.config customErrors setting).

image

Or if you use the HandleError filter, and configure your system right, this may be the screen:

image

Of course you want to the second user friendly error page. Let’s first see what we should be doing to get the simple user friendly user page and then we will look into customizing this error page.

  1. Decorate the controller or the action method with HandleError
  2. Create an Error.aspx or Error.ascx page, and put this page either into the View folder of the controller or Shared folder.
  3. Open your web.config file and change the custom errors definition to customErrors mode=”On”


After this 3 simple steps, you have  an Error page that will be displayed whenever there is an unhandled exception.

HandleErrorAttribute is defined in the HandlErrorAttributed.cs (suprised?) file. It is a relatively simple and short code. If you check the source code, you will see that there are parameters you can customize.  Here are the list of the parameters that you can customize:

  • View: You can define a new View besides the Error to render for the error message. If you don’t specify a View name, default View name is Error
  • Master: You can define a master page for the View that will be rendered. If you don’t specify a master name, that it is empty by default, which means it will use the default master page.
  • ExceptionType: The exception type that you want to handle, by default it is the very generic System.Exception which means catch all the exceptions.

Note that there is another field you can customize however it is not a direct member of this class, but it comes with inheritance. 
Going back to our example, if we want to change error page name to let’s say “UnderMaintenance” , simple change your filter decoration to:

[HandleError(View="UnderMaintenance")]
public class HomeController : Controller


Or if you want to change the master page to Maintenance simply change your filter decoration to:

[HandleError(View="UnderMaintenance", Master="Maintenance")]
public class HomeController : Controller


Now let’s assume you want to redirect the user to different pages for different types of exceptions, then you have to use the ExceptionType property.

  • If the View you are trying to render is not found, then InvalidOperationException will be thrown.
  • If you are trying to connect to SqlServer, and there is an error, then System.Data.SqlClient.SqlException will be thrown

For other types of exception you can check MSDN, but let’s now concentrate on these 2 exceptions, and create 2 different error pages for 2 different types of Exceptions. Here is the right decoration for this goal:

[HandleError(View="Error", ExceptionType=typeof(InvalidOperationException))
[HandleError(View="DatabaseError",ExceptionType=
typeof(System.Data.SqlClient.SqlException)) public class HomeController: Controller


By this declaration we are telling the system that when there is an InvalidOperationException render the Error.aspx/ascx, when there is a SqlException  error, render DatabaseError.aspx/ascx view.

What happens when we add another HandleError that handles the SystemException which is basically all the exceptions, so if our declaration is like below, and assume SqlException is thrown:

[HandleError(ExceptionType=typeof(InvalidOperationException))]
[HandleError(ExceptionType=typeof(System.Data.SqlClient.SqlException))]
[HandleError(ExceptionType = typeof(SystemException))]
 public class HomeController : Controller


The framework will call  the all filters that can catch this type of exception, in this case, framework will call handlerror twice, one for the SqlException, and one for the SystemException.

You can write your own HandleError filter, and apply it to the controller or action. Inside this filter, you have to set the ExceptionHandled flag to true, so that framework won’t throw the yellow dead screen. If there are more than 1 filter that can catch the error message, all of them have to set the ExceptionHandled flag to true, or framework will throw the yellow dead screen.
The default HandleError attribute also creates a model for your view incase you want to get more information about the error, if you change your error page’s model to HandleErrorInfo, then you can get more information from the framework about the error. A simple example for the Error.aspx could be:

<%@ Page ..  Inherits="System.Web.Mvc.ViewPage<HandleErrorInfo>" %>
Sorry for the error, We are working hard on it to fix the problem.
<ul>
    <li>Action: <%=Model.ActionName %></li>
    <li>Controller: <%=Model.ControllerName %></li>
    <li><%=Model.Exception.ToString() %></li>
</ul>

 

Of course in the real work application, you don’t want to display this information to your user, but you can use this information to write a log to your log system.
Hopefully this article help you to understand HandleError attribute.

May the force be with you.



Tags: , , ,

E-mail | Permalink | Trackback | Post RSSRSS comment feed 0 Responses

Mar192010

TempData in MVC 1.0

    TempData is an interesting structure in MVC 1.0, and could be a little problematic if you don’t know the internals of it while using, so in MVC 2.0 TempData structure is changed. Today I want to analyze MVC 1.0’s TempData structure.

TempData is used to store information that will be consumed only once. A good example could be some information/error messages that will be passed to the view, and if the user refreshes the page, the message will be gone. For example:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Save(Order order)
{
    if(order.Validate()==false)
    {
        TempData["Msg"]="There are problems with order, please fix'em";
        return RedirectToAction("ValidationError");
    }
    //... some save to database action 
   TempData["Msg"]="Your order will be shipped ASAP";
  return RedirectToAction("OrderComplete");
}

As you can see in the code above, we are passing a message to one of the actions informing about the status of the order. If the user refreshes the page, TempData[“Msg”] will be cleared and the message will be gone. How does MVC 1.0 accomplish this? Basically, it stores the the value in the session, and after the roundtrip it deletes the value, but more technically:
If you open the MVC source code, and go to the Controller.cs file line 100, you will see that the controller has a member called TempDataProvider which is by default a SessionStateTempDataProvider. This provider implements an interface that has the below signature:

public interface ITempDataProvider {
  Dictionary<string, object> LoadTempData(ControllerContext controllerContext);
  void SaveTempData(ControllerContext controllerContext, 
IDictionary<string, object> values); }

So basically its job is to load or save a dictionary, and the SessionStateTempDataProvider is using the Session to load or save the dictionary. This dictionary is storing your temp values. 
Let’s check what SessionStateTempDataProvider LoadTempData does. The first thing the provider does is to get the dictionary from the controllercontext’s session. If the session does not return a value for the dictionary (in case you haven’t used the TempData in your code , then the session value is empty), then a new Dictionary<string,object> is instantiated and returned back to the caller.
If the session value has a value for the dictionary, then it is taken from the session, the session value is cleared, and dictionary is returned. After this point, there is no value for the dictionary in the session, so it is caller’s job to save the dictionary back to the session.
Here is the code from the mvc source code:

public virtual IDictionary<string, object> LoadTempData(ControllerContext 
controllerContext)
{ HttpContextBase httpContext = controllerContext.HttpContext; if (httpContext.Session == null) { throw new InvalidOperationException
(MvcResources.SessionStateTempDataProvider_SessionStateDisabled); } Dictionary<string, object> tempDataDictionary =
httpContext.Session[TempDataSessionStateKey] as Dictionary<string, object>; if (tempDataDictionary != null) { // If we got it from Session, remove it so that no other request gets it httpContext.Session.Remove(TempDataSessionStateKey); return tempDataDictionary; } else{ return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); } }


SaveTempData is an easy operation, just take the passed the Dictionary and save it to the controllercontext’s session.  So far what we have seen is just a provider that stores a dictionary to the session, and reads a dictionary from the session. How does the controller use this provider to store and load the temp values? If you open the ControllerBase.cs file and go to line 21, you will see that the ControllerBase has a public property TempData of TempDataDictionary type. One of the problem is happening in this area. This dictionary has a Load and Save public functions, that uses SessionStateTempDataProvider (by default), to save and load the values. In order to do that, it uses 2 HashSets, one for the initial keys (the values that come back with the post back), the other HashSet is either for the keys that are newly added to the Dictionary or modified keys from the initial keys. By this way, the dictionary keeps track of who is added, who is supposed to be removed, who is updated.

The controller executes ExecuteCore() to invoke the action. The first line in this function is TempData.Load. This is where the controller asks the TempData dictionary to load the dictionary, and TempData dictionary uses SessionStateTempDataProvider to load the dictionary from the session. The last line of the ExecuteCore is TempData.Save which simply takes the dictionary back from the controller, uses SessionStateTempDataProvider and saves it to the session. Do not forget that the unmodified values are removed from the dictionary when the Save method is called.

What are the problems with this structure?
First of all TempData by default stores the dictionary in a session, and once it is read, it cleans the session. What if the user opens a new tab right before your TempData value is read by page? Session is shared between the tabs of the same browser, so TempData may lose the data in the session. The same problem may occur when there is an ajax call to the controller, or there is a redirecttoaction. Because of these problems MVC 2.0 has fixed some issues with TempData and we will look into the source for MVC 2.0 in a later posting.



Tags:

E-mail | Permalink | Trackback | Post RSSRSS comment feed 0 Responses

Dec212009

Action’s parameters and Selection Decision

    When you have an action, with an overloaded version of it in the controller, its parameters are not used to to decide the selection.

Let’s assume you have a form, and user clicks on the submit button, to send the form to the server. The action that renders the view for the get request is: Register(), and the action that receives the request is Register(FormCollection collection) similar to something like this:

public ViewResult Register()
{
    //do some prep for the view
    return View();
}

public ViewResult Register(FormCollection collection)
{
    //do something with the collection
   // either return a view, or redirect to another action
}

I didn’t put any action selector on purpose; to prove my point. If you run this page, and try to call /Register you can get an error. Basically the framework is complaining that it can’t pick which Register() action it should call to respond /Register. You may think that because the second Register() action has a parameter, it should not be picked for a get request; however it is not the case :)

Let’s look at the MVC source code to see how an action is executed. If you open the source code, and check the Controller.cs file, around line 162, you will this function:

 

   1:    protected override void ExecuteCore() {
   2:              TempData.Load(ControllerContext, TempDataProvider);
   3:   
   4:              try {
   5:                  string actionName = RouteData.
GetRequiredString("action");
   6:                  if (!ActionInvoker.InvokeAction
(ControllerContext, actionName)) {
   7:                      HandleUnknownAction(actionName);
   8:                  }
   9:              }
  10:              finally {
  11:                  TempData.Save(ControllerContext, TempDataProvider);
  12:              }
  13:          }

 

Look carefully at line 6. This where the action is invoked, now we have to dig in more, and see what is happening inside ActionInvoker.InvokeAction method. This method is doing 1 thing that is important for the topic of this blog:

 ActionDescriptor actionDescriptor = FindAction(controllerContext,
controllerDescriptor, actionName);
 
 

It is trying to find the action to execute, given the action name, and the controller name. It adds the results into a list of methodinfo. Here is it:

   public MethodInfo FindActionMethod(ControllerContext controllerContext, 
string actionName) { List<MethodInfo> methodsMatchingName = GetMatchingAliasedMethods
(controllerContext, actionName); methodsMatchingName.AddRange(NonAliasedMethods[actionName]); List<MethodInfo> finalMethods = RunSelectionFilters(
controllerContext, methodsMatchingName); switch (finalMethods.Count) { case 0: return null; case 1: return finalMethods[0]; default: throw CreateAmbiguousMatchException(finalMethods,
actionName); } }


After it adds the methods that it finds to the list, it runs the RunSelectionFilters function to eliminate the methods. After the eliminations, if it has more than 1 method left in the list, it throws the exception; which you see when you call /Register. As you can see the parameters of the action has nothing to do with the selection.
One way to help the framework to pick the right action is to use SelectionFilters; as you can see from the code above, FindActionMethod() calls the RunSelectionFilter() to eliminate the actions.

If you use AcceptVerbs selection filter to differentiate between the two Register function, framework won’t be complaining as before; so this is one way of doing it right:

[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Register()
{
    //do some prep for the view
    return View();
}

[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Register(FormCollection collection)
{
    //do something with the collection
   // either return a view, or redirect to another action
}
 

This time, when there is a get request, RunSelectionFilter() will eliminate Register(FormCollection collection), as it has the AcceptVerbs post selecttionfilter.

I am still walking through the ASP.NET MVC source code, and really enjoying the way the developers wrote it; it is really fun to read it.

Have fun :)



Tags:

E-mail | Permalink | Trackback | Post RSSRSS comment feed 0 Responses

Dec052009

MVC Case Sensitivity & Action Names & NonAction Attribute

Today, it was the first class of our free ASP.NET MVC course. I was talking about URLs not being case sensitive. So that for example if you have the request “Home/About” this goes to HomeController and About action, as well as hOmE/AbOUT is going to the same controller and same action method.
One of the students ask me, what will happen if we have 2 about action methods in the same controller with different cases such as:

public class HomeController:Controller
{
....
    public ViewResult About()
    {
        return View();
    }

    public ViewResult aBOut()
    {
        return View();
    }
}

If you write the code above, and try to run the app, and call Home/About, here is the error message you will get:

image

The framework can’t determine which ‘about’ function to call, and throws the exception telling that the call is ambiguous. Of course one way to fix this problem is to change the action name. If for some reason you don’t want to change the action name, and one of these function is not an action, then you can decorate this non action method with NonAction attribute. Example:

[NonAction]
public ActionResult aBOut()
{
   return View();
}

So now this function is not anymore an action method that will handle the web requests. What is NonAction attribute?
It is an attribute that derives from ActionMethodSelectorAttribute. When you derive from this attribute you can override IsValidForRequest function, and return false, to refuse to serve as an action. If you check the MVC source code, here is the code for NonAction attribute:

[AttributeUsage(AttributeTargets.Method, 
AllowMultiple = false, Inherited = true)] public sealed class NonActionAttribute : ActionMethodSelectorAttribute { public override bool IsValidForRequest(ControllerContext
controllerContext, MethodInfo methodInfo) { return false; } }
 

This code is refusing to act as a request. So you can use this NonAction attribute on any of your public functions inside a controller to make them not web accessible.



Tags:

E-mail | Permalink | Trackback | Post RSSRSS comment feed 0 Responses

Nov122009

Why Controllers should have a default constructor?

    I am still in the journey of exploring MVC source code.  Yesterday, I wrote about controllers and why they need “Controller” suffix. Today, I will try to find out in the code, why does a controller need a default constructor. First of all, what is default constructor?
You can check wikipedia for its complete definition; but basically a default constructor is a constructor that has no parameters. If there is no other constructors in the code, compiler puts a default constructor for you in the code. You don’t believe me? Here is a simple class:

public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }


This class does not have any type of constructors, so the compiler put a default constructor for us. compile the project, check the assembly with either reflector or ildasm tool. You will see the default constructor in your compiled code (besides some other stuff):

image 
.ctor() is the default constructor that the compiler added for us, when we did not supply any constructor. So what happens when we add a constructor that takes a parameter but no default constructor:

public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Person(string FirstName)
        {
            this.FirstName = FirstName;
        }
    }

We now have a constructor that takes the FirstName as a parameter, and we don’t have a default constructor. This time if you check the assembly again, you will find:

image

You see that, the constructor .ctor does take 1 parameter, and there is no default constructor for us. This means when you want to create a new instance of Person, you can NOT use parameter less constructor such as: Person p = new Person();  You have to call it with a parameter.
So far, this is basic C#, where is MVC section? Here you are:

When a request comes into the MVC, we saw that the framework finds the controller first. Inside the DefaultControllerFactory (if you downloaded the source code, it is in DefaultControllerFactory.cs line # 83), an instance of the controller is being created to process the request. Here is the code from the controller factory:

 protected internal virtual IController GetControllerInstance
(Type controllerType) { // some code is here... try { return (IController)Activator.CreateInstance(controllerType); } // some other code is here...

Activator.CreateInstance is used to create an instance of the controller. Let’s see MSDN for Activator.CreateInstance:

“CreateInstance(Type)    Creates an instance of the specified type using that type's default constructor.” So Activator.CreateInstance(controllerType) will create an instance of the specified controller using the controller’s default constructor. This is the main reason controllers should have a constructor, as DefaultControllerFactory creates the controller by using its default constructor.  Luckily if we don’t have a default constructor, the framework will throw an exception at runtime when it is trying to generate a new instance (I wish we could have compile time error).  The error you will have is very self explanatory.

image 

The framework is even showing us the exact location that throws the exception. So we need to supply a default constructor if we have defined constructors with parameters.
Why would you have a constructor in your controller that takes parameter if the framework does not call it? For 2 simple reasons:

  1. Unit Testing: You want to unit test your controller, and by supplying some values in the constructor you can swap the dependencies with some other implementations that you have control of.
  2. You want to do inversion of control. Basically if you have a class, and your class depends on some other entities (such as your controller needs a class to communicate with database, or a logger class to log the errors), it shouldn’t be a task of your controller to create an instance of this classes that it depends. The classes  should be instantiated and given to your controller ready to use.

 

Let’s see an example again, assume you have a DatabaseLogger class which derives from ILogger interface and sends the logged message to the database:

public interface ILogger
{
    void SendMessage(string Message);
}

public class DatabaseLogger: ILogger
{
    public void SendMessage(string Message)
   {
      // some code to write the message to the database


Our HomeController needs the DatabaseLogger class to send some messages. One way  of doing this is to put the instantiation inside the default controller such as:

public class HomeController:Controller
{
    private ILogger logger;

    public HomeController():base(new DatabaseLogger())
    {     
    }

    public HomeController(ILogger logger)
    {
        this.logger = logger;
    }
}

There are 2 constructors in this controller. The second one that has a parameter, sets the controller’s dependency to the passed in logger interface. The default constructor is creating an instance of DatabaseLogger and passing this to the second constructor. With this technique here we can unit test our controller as the second constructor gives us the ability to swap the ILogger with an object that we can control.  However, if you look at the default constructor, the controller is creating the class that it has the dependency on. It should be passed the depedency ready to use. In this simple example maybe the benefit is not obvious however think that DatabaseLogger() needs some construction parameters that will setup the database connection string, and maybe to read the database connection string, you have some other dependencies such as a configuration file reader. Now your homecontroller has to supply all these settings to create the DatabaseLogger. This is too much now, this is where you need to use dependency injection such as Castle Windsor, StructureMap, Ninject etc…

This will be another story :)



Tags: ,

E-mail | Permalink | Trackback | Post RSSRSS comment feed 2 Responses

Nov112009

Why Controllers should have a “Controller” Suffix?

   If you are programming with ASP.NET MVC, you know that the controller’s should have a suffix of “Controller” such as “HomeController”. You may also know that this is how DefaultControllerFactory works.
Let’s look into the technical details. I will skip some unrelated facts, but we will check the MVC source code, and modify it a little. Please go to CodePlex and download the MVC source code.
When a request comes into IIS (Microsoft’s web server application), IIS first checks the request, then the MVCRouteHandler is called (assuming this is an MVC request). The MVCRouteHandler creates an instance of  MvcHandler by passing the request context. You can check out the code in the MVCRouteHandler.cs file.

protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) {
            return new MvcHandler(requestContext);
        }


After creating the instance, ProcessRequest (from MVCHandler) is invoked and HttpContext is passed as the parameter. The ProcessRequest function is below:

 protected internal virtual void ProcessRequest(HttpContextBase httpContext) {
            AddVersionHeader(httpContext);

            // Get the controller type
            string controllerName = RequestContext.RouteData.
GetRequiredString("controller"); // Instantiate the controller and call Execute IControllerFactory factory = ControllerBuilder.
GetControllerFactory(); IController controller = factory.CreateController(RequestContext,
controllerName);
.... some other calls


As you can see, this function calls the GetControllerFactory() method, to get the ControllerFactory. If you haven’t registered a new ControllerFactory, this function returns the DefaultControllerFactory. The next call in ProcessRequest function is CreateController.  So let’s see what is happening in the DefaultControllerFactory’s CreateController function.

public virtual IController CreateController(RequestContext requestContext, 
string controllerName) { if (requestContext == null) { throw new ArgumentNullException("requestContext"); } if (String.IsNullOrEmpty(controllerName)) { throw new ArgumentException(
MvcResources.Common_NullOrEmpty, "controllerName"); } RequestContext = requestContext; Type controllerType = GetControllerType(controllerName); IController controller = GetControllerInstance(controllerType); return controller; }


This function after checking requestContext, and controllerName against null values, calls GetControllerType. Here is what GetControllerType does:

protected internal virtual Type GetControllerType(string controllerName) {
            if (String.IsNullOrEmpty(controllerName)) {
                throw new ArgumentException(MvcResources.Common_NullOrEmpty,
"controllerName"); } // first search in the current route's namespace collection object routeNamespacesObj; Type match; match = GetControllerTypeWithinNamespaces
(controllerName, nsHash); if (match != null) { return match; } } }


This is not the whole function but a piece of it.

Again after checking some values against null, it is calling the main function which is GetControllerTypeWithinNamespaces.  I am sure you are already bored from one function call to another function call :) however, I will be writing more about the MVC infrastructure, so it is good to show them. So one more time, let’s look at the GetControllerTypeWithinNamespace function.

The first thing this function does is to call: ControllerTypeCache.EnsureInitialized(BuildManager). This will load all the controllers and cache them, so that the next time a controller is needed, the framework doesn’t have to find and load the controllers. This is a simple yet pretty important function.

 public void EnsureInitialized(IBuildManager buildManager) {
     if (_cache == null) {
          lock (_lockObj) {
            if (_cache == null) {
              List<Type> controllerTypes = GetAllControllerTypes(buildManager);
              var groupedByName = controllerTypes.GroupBy(           
      t => t.Name.Substring(0, t.Name.Length - "Controller".Length),
                            StringComparer.OrdinalIgnoreCase);
       _cache = groupedByName.ToDictionary(
                            g => g.Key,
                            g => g.ToLookup(t => t.Namespace ?? 
String.Empty, StringComparer.OrdinalIgnoreCase), StringComparer.OrdinalIgnoreCase); } } } }


As you can see this function caches the controllers after loading them. The _cache variable that is used in this function is a Dictionary<string,ILookup<string,type>>. So basically the framework loads the controllers into a cache. The function that loads the controllers is GetAllControllerTypes. It loads all the assemblies, iterating through them by calling a function: IsControllerType. This is why, this needs to be cached, as you don’t want to load all the assemblies and check if they are controllers. So this IsControllerType function is the reason why we have to add suffix “Controller” to our controllers:

 internal static bool IsControllerType(Type t) {
            return
                t != null &&
                t.IsPublic &&
                t.Name.EndsWith("Controller", 
                StringComparison.OrdinalIgnoreCase) &&
                !t.IsAbstract &&
                typeof(IController).IsAssignableFrom(t);
        }


This function is checking if the passed in controller, has a suffix of “Controller” or not. If it does, it returns true, and GetAllControllerTypes adds this controller to the list, and returns the list of controllers.

The call goes back to EnsureInitialized which strips out the Controller suffix, and adds it into the cache. Let’s assume we want our controllers to have suffix of “Volkan” ; what do we need to do?

First you have to change the IsControllerType so that it checks .EndsWith(“Volkan”) not “Controller”. This will let the <controllerName>Volkan controllers to be added to the list. But this is not enough, the next thing we have to do is, go back to EnsureInitialized and change the stripping part from t.Name.Length - "Controller".Length to t.Name.Length - "Volkan".Length .

Finally you have to put the “Volkan” suffix to your controllers, and now this becomes a valid controller:

      public class HomeVolkan : Controller{

 

Enjoy your time debugging the MVC code!



Tags: ,

E-mail | Permalink | Trackback | Post RSSRSS comment feed 1 Responses