Clean Up your Query String and Use Inline Forms to Pass Data (PostActionLink)

The Challenge

One requirements for my latest MVC application was that no data sensitive be passed via the query string.  This includes elements like IDs, Usernames, Email, etc.  Many of the MVC examples use the query string to pass value type data between screens.  I needed a reusable method to add links to the screen which could pass information more securely and not require a ton if code.

An example use case is the need for “Edit” and “Delete” links on each record of a data list as seen below:

The default MVC Route suggests that IDs should be placed in the query string, and any additional information be added as additional values afterward. This is an easier method, but its far less secure and leaves a rather messy query string such as this:
[sourcecode language=”plain”]
http://www.mysampledomain.com/contoller/action/1047?email=contact@email.com&type=administrator
[/sourcecode]
Obviously there are ways to minimize the data in the query string, but what if you need to remove the information altogether? What if you use Guids for object IDs instead of integers?  Here’s the solution that worked for me…

The Solution

Handling Your Routes

The Visual Studio template MVC application (as well as most sample applications) set the default Route as the following:
[csharp]
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
[/csharp]
This works well if you have a database whose objects have a key with a datatype of integer.  A simple integer value type looks cleaner in the query string over something such as a GUID.  However, most developers who have been around the sun a few times know that having incremental integers as ID always the best plan, and certainly not the most secure.  Displaying integer IDs to the world in the query string is even more risky.  Anyone with a few extra minutes and a little programming knowledge can increment the number in the URL until they gain access to a different set of information than was intended.

In order to prevent this from happening, the first thing we do is remove the ID field from the route.
[csharp]
routes.MapRoute(
"Default", // Route name
"{controller}/{action}", // URL with parameters
new { controller = "Home", action = "Index" } // Parameter defaults
);
[/csharp]
From here on out, we need to manually pass IDs and information from screen to screen.

From Get to Post

Since we are abstaining from using the query string to pass information, we must use the post action to get elements of data from page to page. I’ve seen a number of different methods for handling this type of situation, and all have their upsides and downsides. For this example, we’re going to use an Html Helper to create an inline form to wrap around a plain link.

Html Helpers

Html Helpers are similar to user controls in that they render small chunks of markup that can be used anywhere in the application. In this case, we need to render a form, with a dynamic list of hidden fields and a way to submit it.  Shouldn’t be too painful, right?

Click here to read more on creating custom Html Helpers in MVC.

Create the Helper Class

We need three main elements to make this concept function: a form, hidden fields with data, and a method to submit that form.  The form created by this helper also needs to be able to exist on pages without interfering with existing functionality.

In order to keep things consistent and easy to find, I create my “HtmlHelpers” class in my <Project>.Web.Display namespace. I use this namespace is exclusively for Html Helpers and other functions which output HTML or other type of string element to be rendered by the browser.

To construct the form, we need to determine where to send the information.  The first parameters in my PostActionLink helper method allow the Action, Controller, and Area to be passed in for just this purpose.  Next, a simple Dictionary object is all that is needed to capture simple name/value pairs for hidden fields

[sourcecode language=”csharp” highlight=”4,5,6,7″]
public static MvcHtmlString PostActionLink(
this HtmlHelper Html,
string LinkText,
string LinkAction,
string LinkController,
string LinkArea,
IDictionary HiddenFields)
// String to store the TabBuilder Html Output
StringBuilder returnValue = new StringBuilder();
[/sourcecode]

The form action is constructed using the UrlHelper class and the context of the page implementing the helper.

[sourcecode language=”csharp”]
UrlHelper urlHelper = new UrlHelper(Html.ViewContext.RequestContext);

// Build Form Action URL
string formAction = "";
if (String.IsNullOrEmpty(LinkArea))
{
formAction = urlHelper.Action(LinkAction, LinkController);
}
else
{
formAction = urlHelper.Action(LinkAction, LinkController, new { area = LinkArea });
}

[/sourcecode]

MVC Areas are a great way to organize applications into functional sections.  Determining URLs can be a tad tricky at times, but the benefits warrant the struggles.  Click here to learn more about “Organizing an ASP.NET MVC Application using Areas”

Next, the Form Element is constructed using the TagBuilder class. The form ID is set to a new GUID in order to ensure no conflict can occur with other form elements in the page that implements this helper.

[sourcecode language=”csharp” highlight=”1,5,8″]
Guid formID = Guid.NewGuid();

// Build the Form Element
var formTag = new TagBuilder("form");
formTag.MergeAttribute("id", formID.ToString());
formTag.MergeAttribute("method", "post");
formTag.MergeAttribute("action", formAction);
returnValue.Append(formTag.ToString(TagRenderMode.StartTag));
[/sourcecode]

Now that the Form tag is in place, the hidden fields need to be added to the form.

[sourcecode language=”csharp”]
if (HiddenFields != null)
{
// Loop through the array of form fields and build the hidden inputs tags.
foreach (var hiddenField in HiddenFields)
{
TagBuilder inputTag = new TagBuilder("input");
inputTag.MergeAttribute("type", "hidden");
inputTag.MergeAttribute("name", hiddenField.Key);
inputTag.MergeAttribute("value", hiddenField.Value);
returnValue.Append(inputTag.ToString(TagRenderMode.Normal));
}
}
[/sourcecode]

Lastly, the link to submit the form is added. This could just as easily be a button, image, DIV or SPAN as the concept is the same.  The Onclick event of the link is used submit the form.  A jQuery selector uses the defined Form ID GUID to find the form specific to this helper and submit it.  The “return.false” prevents the anchor tag from executing its HREF action which, for this implementation is not defined.

[sourcecode language=”csharp” highlight=”1,5,8″]
// Build the anchor tag
var linkTag = new TagBuilder("a");
linkTag.MergeAttribute("href", "#");
linkTag.MergeAttribute("onclick", "$(‘#" + formID + "’).submit();return false;");
linkTag.InnerHtml = LinkText;
returnValue.Append(linkTag.ToString(TagRenderMode.Normal));
[/sourcecode]

NOTE: In order to use the jQuery selector, the jQuery libraries must be referenced on the page that implements this helper. If jQuery is not available, “document.getElementById()” can be used in its place.

The form tag can now be closed and the response rendered to an MvcHtmlString using the create method.

[sourcecode language=”csharp” highlight=”4″]
// Close form Tag
returnValue.Append(formTag.ToString(TagRenderMode.EndTag));

return MvcHtmlString.Create(returnValue.ToString());
[/sourcecode]

Download the source code: PostActionLink.txt

Implementation

The PostActionLink can now be implemented in views as necessary using the following syntax:

[sourcecode language=”csharp” highlight=”4″]
@Html.PostActionLink("[Label]", "[Action]", "[Controller]", "[Area]", [Dictionary])
[/sourcecode]

One method for adding form fields to the dictionary is to add the dictionary items inline.  This is a great method if you have one or two items to add, but it’s easy to see how any more can become quite cumbersome.

[sourcecode language=”csharp”]
@Html.PostActionLink("[Label]", "[Action]", "[Controller]", "[Area]", new Dictionary { [DictionaryItems] })
[/sourcecode]

To implement the PostActionLink helper, a reference is needed to the custom library on the appropriate view.

[sourcecode language=”html”]
@model IEnumerable<MvcApplication1.Web.Data.SomeModel>
@using MvcApplication1.Web.Display;
<div>
@Html.PostActionLink("View", "ViewAction", "MyController", "MyArea", new Dictionary{ { "ModelID", model.ID.ToString() } })
<div>[/sourcecode]

Conclusion

The PostActionLink helper can be used in a vareity of scenarios with a few caveats:

  • The PostActionLink call must  exist outside of any other forms in the view.  Nested forms are a no-no in web development, and this is no exception.
  • Since the Form ID is generated on the fly, there is no good way to get a reference to the form via client side script. An overload could be created to take in a Form ID as a parameter if needed.

Leave a Reply

Your email address will not be published. Required fields are marked *