MVC User Controls

Contents:

  1. Introduction
  2. Partial views
  3. Custom HtmlHelper methods
  4. Templated helpers
  5. Summary

1. Introduction Go top

The concept of user controls as we were used in the Web Forms projects doesn’t exist in MVC but we have three simmilar alternatives:

  1. Partial views
  2. HtmlHelper extentions
  3. Templated helpers

2. Partial views Go top

Partial views are similar in concept to normal views with the main difference that they can be embedded inside views. As an example, we will create a page that, for the currently logged user, displays the billing and the shipping address allowing the user to edit them.

Final result

We will continue the solution build in the previous posts, that you can download from Codeplex. Or, if you prefer, you can download directly the source code for the current post using this link.

First we will create a new table to store the addresses and we will modify the users table to have two references to the Addresses table, one for the billing and one for the shipping address.

Create a new table for storing addresses

Below is the SQL script for updating the DB:

CREATE TABLE [dbo].[Addresses](
	[AddressId] [int] IDENTITY(1,1) NOT NULL,
	[Street] [varchar](100) NOT NULL,
	[ZipCode] [varchar](50) NULL,
	[City] [varchar](100) NOT NULL,
	[Country] [varchar](100) NOT NULL,
 CONSTRAINT [PK_Addresses] PRIMARY KEY CLUSTERED
(
	[AddressId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
BEGIN TRANSACTION
SET QUOTED_IDENTIFIER ON
SET ARITHABORT ON
SET NUMERIC_ROUNDABORT OFF
SET CONCAT_NULL_YIELDS_NULL ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
COMMIT
BEGIN TRANSACTION
GO
ALTER TABLE dbo.Addresses SET (LOCK_ESCALATION = TABLE)
GO
COMMIT
BEGIN TRANSACTION
GO
ALTER TABLE dbo.Users ADD
	BillingAddressId int NULL,
	ShippingAddressId int NULL
GO
ALTER TABLE dbo.Users ADD CONSTRAINT
	FK_Users_ShippingAddresses FOREIGN KEY
	(
	ShippingAddressId
	) REFERENCES dbo.Addresses
	(
	AddressId
	) ON UPDATE  NO ACTION
	 ON DELETE  NO ACTION

GO
ALTER TABLE dbo.Users ADD CONSTRAINT
	FK_Users_BillingAddresse FOREIGN KEY
	(
	BillingAddressId
	) REFERENCES dbo.Addresses
	(
	AddressId
	) ON UPDATE  NO ACTION
	 ON DELETE  NO ACTION

GO
ALTER TABLE dbo.Users SET (LOCK_ESCALATION = TABLE)
GO
COMMIT

First we will update the entity framework model to include a new entity for the Address. Then we will create a partial view that is able to display an address in read-only or edit mode. For knowing if we are in read-only mode we will add a new property (IsReadOnly) to the Address partial class:

using System.ComponentModel.DataAnnotations;

namespace HelloWorld.Code.DataAccess
{
    [MetadataType(typeof(AddressMetaData))]
    public partial class Address
    {
        private bool _isReadOnly = true;
        public bool IsReadOnly
        {
            get { return _isReadOnly; }
            set { _isReadOnly = value; }
        }
    }

    public class AddressMetaData
    {
        [Required(ErrorMessage = "Required")]
        [StringLength(100, MinimumLength = 3, ErrorMessage = "The street must be a string of at least 3 characters and at most 100 characters")]
        [Display(Name = "Street")]
        public string Street { get; set; }

        [StringLength(50, ErrorMessage = "The ZIP code can't have more than 50 characters")]
        [Display(Name = "ZIP Code")]
        public string ZipCode { get; set; }

        [Required(ErrorMessage = "Required")]
        [StringLength(100, MinimumLength = 3, ErrorMessage = "The city must be a string of at least 3 characters and at most 100 characters")]
        [Display(Name = "City")]
        public string City { get; set; }

        [Required(ErrorMessage = "Required")]
        [StringLength(100, MinimumLength = 3, ErrorMessage = "The country must be a string of at least 3 characters and at most 100 characters")]
        [Display(Name = "Country")]
        public string Country { get; set; }
    }
}

We are ready now to create a partial view that will use the Address class as a model. Just like in the case of normal views, the partial views must be stored under the ‘Views’ folder. Because I like to store all my partial views inside a single folder I created a new sub-folder under ‘Views’ called ‘PartialViews’. Then, for adding a partial view, lets make a right click on the ‘PartialViews’ folder in the solution explorer and click ‘Add’ » ‘View’. In the pop-up that opens make sure to tick the check-box ‘Create as a partial view’:

Create a partial view for the address

We will make our partial view strongly typed by specifying, in the first line, the type of the model this partial view works with:

@model HelloWorld.Code.DataAccess.Address

And we will display the address in read-only or edit mode depending on the current value for the model property: @Model.IsReadOnly.

@model HelloWorld.Code.DataAccess.Address

@if(Model == null)
{
    @:None
    <a href="#" class="edit-address">Edit</a>  
}
else if (Model.IsReadOnly)
{
    @Html.LabelFor(model => model.Street)@: <strong>@Model.Street</strong>
    <br/>
    @Html.LabelFor(model => model.ZipCode)@: <strong>@Model.ZipCode</strong>
    <br/>
    @Html.LabelFor(model => model.City)@: <strong>@Model.City</strong>
    <br/>
    @Html.LabelFor(model => model.Country)@: <strong>@Model.Country</strong>
    <br/>
    <a href="#" class="edit-address">Edit</a>          
}
else
{
    @Html.ValidationSummary(true)
 
    @Html.LabelFor(model => model.Street, new { @class = "control-label" })
    @Html.TextBoxFor(model => model.Street)
    @Html.ValidationMessageFor(model => model.Street)
    <br/>
    @Html.LabelFor(model => model.ZipCode, new { @class = "control-label" })
    @Html.TextBoxFor(model => model.ZipCode)
    @Html.ValidationMessageFor(model => model.ZipCode)
    <br/>
    @Html.LabelFor(model => model.City, new { @class = "control-label" })
    @Html.TextBoxFor(model => model.City)
    @Html.ValidationMessageFor(model => model.City)
    <br/>
    @Html.LabelFor(model => model.Country, new { @class = "control-label" })
    @Html.TextBoxFor(model => model.Country)
    @Html.ValidationMessageFor(model => model.Country)
    <br/>
    <a href="#" class="save-address">Save</a>     
}

Next we will create a controller called UserControlsController and a view called PartialViewExample that will display the name and surname of the currently logged user plus the shipping and billing addresses using the new partial view we just created.

@model HelloWorld.Code.DataAccess.User
@{
    ViewBag.Title = "PartialView Example";
}
<h2>@ViewBag.Title</h2>
<p>You are logged in as <strong>@Model.FirstName @Model.LastName</strong>.</p>
<hr/>
<p>Billing address:</p>
<div id="BillingAddress">
    @Html.Partial("~/Views/PartialViews/AddressPartialView.cshtml", new ViewDataDictionary(Model.BillingAddress))
</div>
<hr/>
<p>Shipping address:</p>
<div id="ShippingAddress">
    @Html.Partial("~/Views/PartialViews/AddressPartialView.cshtml", new ViewDataDictionary(Model.ShippingAddress))
</div>

For inserting a partial view we make use of a helper method Html.Partial and we pass the model to use through a ViewDataDictionary object. We are using the ViewDataDictionary to pass the model to the partial view even if the HtmlHelper class has an overload method for the Partial method that receives as an input the partial view name and the model to pass to the partial view. We do this because the latter method has a curious behavior: in case the passed object is null, it will take the model of the current view and pass it to the partial view instead of passing a null value. So, if we would include the partial view using this method:

@Html.Partial("PartialViews/AddressPartialView", Model.ShippingAddress)

And the current user doesn’t have a shipping address, the Partial method will pass the view’s model which in our care is a User object and we will get a strange run-time error saying that:

The model item passed into the dictionary is of type ‘HelloWorld.Code.DataAccess.User’, but this dictionary requires a model item of type ‘HelloWorld.Code.DataAccess.Address’.

Below is the code for the controller that retrieves the data for the logged user and passes it to the view:

using System.Web.Mvc;
using System.Linq;
using System.Data.Entity;
using HelloWorld.Code.DataAccess;
using HelloWorld.Code.Security;

namespace HelloWorld.Controllers
{
    [Authorize]
    public class UserControlsController : Controller
    {
        public ActionResult PartialViewExample()
        {
            var userId = HttpContext.User.ToCustomPrincipal().CustomIdentity.UserId;
            using (var context = new MvcDemoEntities())
            {
                var user = context.Users.Include(u => u.ShippingAddress).Include(u => u.BillingAddress).First(u => u.UserId == userId);
                return View(user);
            }
        }
    }
}

If you run now the code, you will see that the address partial view was inserted twice in the view.

In practice views are great when updating content via Ajax requests, and this is what we are going to do next. When the user clicks on Edit we will replace the read-only text of the address with the edit-enabled version of the address partial view.

For this we need some JavaScript to make the Ajax calls so we will add the following script inside the view in the scripts section:

@section scripts
{
    <script type="text/javascript">
        $().ready(function () {
            Init();
        });

        function Init() {
            $(".edit-address").click(function (event) {
                event.preventDefault();
                var link = $(this);
                var container = link.parent();

                $.ajax({
                    url: '@Url.Action("EditLoggedUserAddress", "UserControls")',
                    data: { type: container.attr('id') },
                    type: 'GET',
                    timeout: 300000, //5 minutes
                    dataType: 'html',

                    success: function (response) {
                        container.html(response);
                        Init();
                    },
                    error: function () {
                        alert('Error');
                    }
                });
            });
        }
    </script>
}

Note: Sections are not supported inside partial views. That’s why I added the JavaScript code inside the view but a better place to put this script would be in an external JavaScript file.

On the server side, we need a new controller action method that returns the HTML for the partial view when the user clicks on ‘Edit’.

[HttpGet]
public ActionResult EditLoggedUserAddress(string type)
{
	var userId = HttpContext.User.ToCustomPrincipal().CustomIdentity.UserId;
	using (var context = new MvcDemoEntities())
	{
		var user = context.Users.Include(u => u.ShippingAddress).Include(u => u.BillingAddress).First(u => u.UserId == userId);
		var address = string.Compare(type, "ShippingAddress", true) == 0 ? user.ShippingAddress : user.BillingAddress;
		if (address == null)
			address = new Address();

		address.IsReadOnly = false;
		return PartialView("~/Views/PartialViews/AddressPartialView.cshtml", address);
	}
}

The method that renders the partial view is PartialView. We use it to generate the HTML markup only for the AddressPartialView so we can return it to our Ajax caller that will update the HTML content only for that partial view.

Now we need a method for saving the entered data. We will do this again via Ajax calls, adding the following at the end of our Init function inside the view:

$(".save-address").click(function (event) {
	event.preventDefault();
	var link = $(this);
	var container = link.parent();

	$.ajax({
		url: '@Url.Action("SaveLoggedUserAddress", "UserControls")',
		data: container.children("input, textarea, select").serialize() + "&type=" + container.attr('id'),
		type: 'POST',
		timeout: 300000, //5 minutes
		dataType: 'html',

		success: function (response) {
			container.html(response);
			Init();
		},
		error: function () {
			alert('Error');
		}
	});
});

The controller method that will actually save the data is:

[HttpPost]
public ActionResult SaveLoggedUserAddress(Address addressModel, string type)
{
	if (!ModelState.IsValid)
	{
		addressModel.IsReadOnly = false;
		return PartialView("~/Views/PartialViews/AddressPartialView.cshtml", addressModel);
	}

	var userId = HttpContext.User.ToCustomPrincipal().CustomIdentity.UserId;
	var isShippingAddress = System.String.Compare(type, "ShippingAddress", System.StringComparison.OrdinalIgnoreCase) == 0;
	using (var context = new MvcDemoEntities())
	{
		var user = context.Users.Include(u => u.ShippingAddress).Include(u => u.BillingAddress).First(u => u.UserId == userId);
		var address = isShippingAddress ? user.ShippingAddress : user.BillingAddress;
		if (address == null)
		{
			address = addressModel;
			context.AddToAddresses(address);
			if (isShippingAddress)
				user.ShippingAddress = address;
			else
				user.BillingAddress = address;
		}

		address.Street = addressModel.Street;
		address.ZipCode = addressModel.ZipCode;
		address.City = addressModel.City;
		address.Country = addressModel.Country;
		context.SaveChanges();

		address.IsReadOnly = true;
		return PartialView("~/Views/PartialViews/AddressPartialView.cshtml", address);
	}
}

This is it. If you want to download the complete example you can find it here on Codeplex.

3. Custom HtmlHelper methods Go top

Filtered text boxA common way of including dynamically generated HTML markup inside a view is by using the methods defined in the HtmlHelper class. This class contains helper classes that can be used to generate basic HTML elements like text boxes, text areas and check boxes. Apart from the default implementation we can write our own custom helper methods. I explained in detail how this can be done in another post: Custom HtmlHelper for filtering the input in a text box.

4. Templated helpers Go top

[Available soon]

5. Summary Go top

For a comprehensive tutorial check this link: MSDN: Walkthrough: Using Templated Helpers to Display Data in ASP.NET MVC.

Advertisements

Custom HtmlHelper for filtering the input in a text box

The MVC framework contains some helper methods that can be used inside views to programmatically generate basic HTML elements like text boxes, text areas, check boxes and radio buttons. The class that contains those helper methods is called HtmlHelper.

Filtered text box

All the methods of this class return a string that contains the HTML markup to render. Inside a view we can use the HtmlHelper methods by using the Razor syntax @Html. In this way we are accessing the Html property of the view which is an instance of the HtmlHelper class. For example, if we want to render a text box with the name “textboxId” we will write in our view:

@Html.TextBox("textboxId")

The HTML that will be rendered is:

<input id="textboxId" type="text" name="textboxId" value="" />

In addition to the default HtmlHelper methods we can create our own custom methods that generate HTML markup and “attach” them to the HtmlHelper class using extension methods.

As an example we will create a helper method for rendering a filtered text box that allows the user to enter only a given set of characters. If the user enters a character outside the allowed range then, that character will be filtered out and it will not appear in the text box. The control will allow us to specify six types of filters:

  1. AlphabeticCharacter – allows only alphabetic characters from a to z, A to Z and space characters,
  2. Numbers – allows only numbers,
  3. DecimalNumbers – allows numbers and the decimal separator as defined by the current culture info,
  4. DecimalNumbersWithGroupSeparator – allows numbers, the decimal separator and the currency group separator as defined by the current culture info,
  5. CustomValidCharacters – for this filter type we will define a string that contains all the characters that will be allowed in the text box,
  6. CustomInvalidCharacters – for this filter type we will define a string that contains all the characters that aren’t allowed in the text box.

Just to give a famous example, our filtered text box will offer a functionality similar to the FilteredTextBox control contained in the AjaxControlToolkit but the implementation, obviously, has nothing to do with the AjaxControlToolkit.

Now, let’s start writing the code. First we will create a new class called FilteredTextBox that will take care of the HTML rendering. Then we will add extension methods for the HtmlHelper class that will instantiate our new class and provide different filtering types. Because our class must render HTML markup we will have to make it implement the IHtmlString interface. This interface contains only one method ToHtmlString and it is used to tell the MVC framework that the class generates an HTML string that doesn’t need to be encoded. If we don’t specify this interface then, when the output will be rendered in the view, it will be HTML encoded automatically so, in our page, we will see HTML code instead of HTML elements.

To define the six types of supported filters I created an enumeration called FilterType. Here is the complete code of the helper class including the FilterType enumeration:

using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;

namespace Helpers
{
    public class FilteredTextBox : IHtmlString
    {
        #region Filter types

        public enum FilterType
        {
            /// <summary>
            /// Allows only alphabetic characters from a to z and A to Z and space characters.
            /// </summary>
            AlphabeticCharacter,

            /// <summary>
            /// Allows only numbers.
            /// </summary>
            Numbers,

            /// <summary>
            /// Allows numbers and the decimal separator as defined by the current culture info.
            /// </summary>
            DecimalNumbers,

            /// <summary>
            /// Allows numbers, the decimal separator and the currency group separator as defined by the current culture info.
            /// </summary>
            DecimalNumbersWithGroupSeparator,

            /// <summary>
            /// Allows all the characters specified in the ValidCharacters field.
            /// </summary>
            CustomValidCharacters,

            /// <summary>
            /// Allows all the characters except the ones specified in the InvalidCharacters field.
            /// </summary>
            CustomInvalidCharacters
        }

        #endregion

        #region Properties

        public HtmlHelper HtmlHelper { get; set; }

        public FilterType Type { get; set; }

        public string Name { get; set; }

        public object Value { get; set; }

        public string ValidCharacters { get; set; }

        public string InvalidCharacters { get; set; }

        public IDictionary<string, object> HtmlAttributes { get; set; }

        #endregion

        #region Implementation of IHtmlString

        /// <summary>
        /// Returns an HTML-encoded string.
        /// </summary>
        /// <returns>
        /// An HTML-encoded string.
        /// </returns>
        public string ToHtmlString()
        {
            return Render();
        }

        #endregion

        #region Constructors

        internal FilteredTextBox(HtmlHelper htmlHelper, string name, FilterType type, string validCharacters, string invalidCharacters)
        {
            HtmlHelper = htmlHelper;
            Name = name;
            Type = type;
            ValidCharacters = validCharacters;
            InvalidCharacters = invalidCharacters;
        }

        #endregion

        #region Fluent configuration

        /// <summary>
        /// Set the value of the text box.
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public FilteredTextBox SetValue(object value)
        {
            Value = value;
            return this;
        }

        /// <summary>
        /// Set the dictionary that contains the HTML attributes to set for the element.
        /// </summary>
        /// <param name="htmlAttributes"></param>
        /// <returns></returns>
        public FilteredTextBox SetHtmlAttributes(IDictionary<string, object> htmlAttributes)
        {
            HtmlAttributes = htmlAttributes;
            return this;
        }

        #endregion

        #region Methods

        public override string ToString()
        {
            return ToHtmlString();
        }

        private string Render()
        {
            var tagBuilder = new TagBuilder("input");

            if (HtmlAttributes != null)
                tagBuilder.MergeAttributes(HtmlAttributes);

            tagBuilder.MergeAttribute("type", "text");

            if (!string.IsNullOrEmpty(Name))
                tagBuilder.MergeAttribute("name", Name, true);

            var value = Value == null ? string.Empty : Value.ToString();
            var regex = string.Empty;
            switch (Type)
            {
                case FilterType.AlphabeticCharacter:
                    regex = "[^a-zA-Z ]";
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
                case FilterType.Numbers:
                    regex = "[^0-9]";
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
                case FilterType.DecimalNumbers:
                    regex = string.Format("[^0-9{0}]", Regex.Escape(System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator));
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
                case FilterType.DecimalNumbersWithGroupSeparator:
                    regex = string.Format("[^0-9{0}{1}]", Regex.Escape(System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator), Regex.Escape(System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberGroupSeparator));
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
                case FilterType.CustomValidCharacters:
                    regex = string.Format("[^{0}]", Regex.Escape(ValidCharacters));
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
                case FilterType.CustomInvalidCharacters:
                    /*
                    regex = string.Join("|", InvalidCharacters.ToCharArray().Select(c => Regex.Escape(c.ToString(CultureInfo.InvariantCulture))));
                    replaced with the next line thanks to Siderite - http://siderite.blogspot.com/
                    */
                    regex = string.Format("[{0}]", Regex.Escape(InvalidCharacters));
                    value = Regex.Replace(value, regex, string.Empty);
                    break;
            }

            tagBuilder.MergeAttribute("data-filter", "1");
            tagBuilder.MergeAttribute("value", value, true);
            tagBuilder.MergeAttribute("data-regex", regex, true);

            if (!string.IsNullOrEmpty(Name))
            {
                // If there are any errors for a named field, we add the css attribute.
                ModelState modelState;
                if (HtmlHelper.ViewData.ModelState.TryGetValue(Name, out modelState))
                {
                    if (modelState.Errors.Count > 0)
                    {
                        tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
                    }
                }
            }

            return tagBuilder.ToString(TagRenderMode.SelfClosing);
        }

        #endregion
    }
}

Because we want to call our class using the usual Razor expression @Html we will have to create extension methods that will invoke our new class:

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

namespace Helpers
{
    public static class HtmlExtensions
    {
        #region AlphabeticCharactersTextBox

        /// <summary>
        /// Returns a text input element that allows only alphabetic characters from a to z and A to Z and space characters.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field.</param>
        /// <returns></returns>
        public static FilteredTextBox AlphabeticCharactersTextBox(this HtmlHelper htmlHelper, string name)
        {
            return new FilteredTextBox(htmlHelper, name, FilteredTextBox.FilterType.AlphabeticCharacter, string.Empty, string.Empty);
        }

        /// <summary>
        /// Returns a text input element that allows only alphabetic characters from a to z and A to Z and space characters.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field.</param>
        /// <param name="value">The value of the input element.</param>
        /// <returns></returns>
        public static FilteredTextBox AlphabeticCharactersTextBox(this HtmlHelper htmlHelper, string name, object value)
        {
            return new FilteredTextBox(htmlHelper, name, FilteredTextBox.FilterType.AlphabeticCharacter, string.Empty, string.Empty).SetValue(value);
        }

        /// <summary>
        /// Returns a text input element that allows only alphabetic characters from a to z and A to Z and space characters.
        /// </summary>
        /// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
        /// <param name="name">The name of the form field.</param>
        /// <param name="value">The value of the input element.</param>
        /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
        /// <returns></returns>
        public static FilteredTextBox AlphabeticCharactersTextBox(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes)
        {
            return new FilteredTextBox(htmlHelper, name, FilteredTextBox.FilterType.AlphabeticCharacter, string.Empty, string.Empty).SetValue(value).SetHtmlAttributes(htmlAttributes);
        }

        #endregion

		//The rest of the code was omitted for brevity but you can download the complete source code from Codeplex: 
		//https://mvcfilteredtextbox.codeplex.com/downloads/get/843504
    }
}

This is enough for rendering the HTML code. You can see, in the Render method, that each generated HTML input element contains also an attribute called ‘data-regex’. In the ‘data-regex’ attribute we are storing a regular expression that can select the characters that are not allowed. For example, for the numeric filter, the regular expression that excludes all characters that are not numbers is [^0-9].

Now, the only thing that remains to be done before using the new helper methods is to add some client side code to filter the typed characters. We will write this JavaScript code in an external file and we will include a link to it in our page.

$(document).ready(function () {
    HtmlHelpers.Init();
});

var HtmlHelpers = {
    Init: function () {
        $("input[data-filter='1']").keypress(HtmlHelpers.FilteredTextBox.CheckValue);
    }
};

HtmlHelpers.FilteredTextBox = {
    CheckValue: function (event) {
        var code = event.keyCode || event.which;

        if (event.ctrlKey || event.metaKey || event.altKey || code == 0
            //Text editing characters
            || code ==	33 //Page Up
            || code ==	34 //Page Down
            || code ==	37 //Left arrow
            || code ==	38 //Up arrow
            || code ==	39 //Right arrow
            || code ==	40 //Down arrow            
            || code == 8 //Backspace
            || code == 36 //Home
            || code == 35 //End
            || code == 45 //Insert
            || code == 46 //Delete
            ) {
            return;
        }

        var value = String.fromCharCode(event.which);
        var regex = new RegExp($(this).attr("data-regex"), "");
        
        if (regex.test(value)) {
            event.preventDefault();
        }
    }
};

Now we are able to use our new class inside the view. For example:

@using Helpers

Allows only alphabetic characters
<br/>        
@Html.AlphabeticCharactersTextBox("tbAlphabeticCharacters", string.Empty, new Dictionary<string, object>{{"class", "testCssClass"}})
<br/>
Allows only numbers
<br/>
@Html.NumbersTextBox("tbNumbers").SetValue("123ABC").SetHtmlAttributes(new Dictionary<string, object>{ { "style", "background-color: green;" } })
<br/>
Allows numbers and the decimal separator as defined by the current culture info
<br/>
@Html.DecimalNumbersTextBox("tbDecimalNumbers")
<br/>
Allows numbers, the decimal separator and the currency group separator as defined by the current culture info
<br/>
@Html.DecimalNumbersWithGroupSeparatorTextBox("tbDecimalNumbersWithGroups")
<br/>
Allows the following characters: abcd\
<br/>
@Html.ValidCharactersFilteredTextBox("tbValidCharacters", "abcd\\")
<br/>
Allows all characters except: abcd\
<br/>
@Html.InvalidCharactersFilteredTextBox("tbInvalidCharacters", "abcd\\") 

The result of this view is:

Filtered text boxes example

Note 1: If the FilteredTextBox wouldn’t be implementing the IHtmlString interface than the rendered HTML would be automatically encoded and, when running the site, we would see something like in the following image:

Filtered text boxes example without IHtmlString

Note 2: Another thing to notice is that we created our class so that it allows method chaining:

@Html.NumbersTextBox("tbNumbers").SetValue("123ABC").SetHtmlAttributes(new Dictionary<string, object>{ { "style", "background-color: green;" } })

The method chaining is possible because the SetValue method returns the current object. This improves the readability of the code and is a good idea when writing HtmlHelpers that will be used massively. In the literature this is called a fluent approach and, if you like to read more about this, here are some links on the topic:

Stronly Typed Helpers

As you probably already noticed, there are helper methods that allow us to use lambda expressions to specify both the name of the element and the value to render. For example Html.TextBoxFor provides such a functionality. This kind of HtmlHelper methods are called strongly typed helpers and they follow the naming convention Html.HelperNameFor.

Our next goal is to create some strongly typed helper methods for our FilteredTextBox class. For this we will create new extension methods inside the static class FilteredTextBoxHelper. For example, for the alphabetic characters filtering the strongly typed helper method will be:

/// <summary>
/// Returns a text input element that allows only alphabetic characters from a to z and A to Z and space characters.
/// </summary>
/// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
/// <param name="expression">An expression that identifies the object that contains the properties to render.</param>
/// <returns></returns>
public static FilteredTextBox AlphabeticCharactersTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, System.Linq.Expressions.Expression<Func<TModel, TProperty>> expression)
{
	var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);

	var text = metadata.Model;
	var name = ExpressionHelper.GetExpressionText(expression);
	return (new FilteredTextBox(htmlHelper, name, FilteredTextBox.FilterType.AlphabeticCharacter, string.Empty, string.Empty)).SetValue(text);
}

Now we will be able to write in our view something like this:

@Html.AlphabeticCharactersTextBoxFor(model => model.PropertyName)

In a similar way we can write strongly typed extension methods also for the other supported filter types. I will not write here the implementation because it’s similar to the AlphabeticCharactersTextBoxFor method but you can find them, together with the complete source code for the FilteredTextBox and FilteredTextBoxHelper classes on CodePlex: click here to download the source code.

Forms Authentication in ASP.NET MVC 4

Contents:

  1. Introduction
  2. Implement a custom membership provider
  3. Implement a custom role provider
  4. Implement a custom user principal and identity
  5. Implement a custom authorization filter
  6. Summary

1. Introduction Go top

For adding authorization and authentication features to an ASP.NET MVC site we will be using the same approach as for a classic Web Forms project. The classes that stay at the base of the ASP.NET security model can be used in both MVC and Web Forms projects. The authentication happens like in this image:

Authentication

  1. The login page collects the user credentials and then calls the Membership class in order to validate them.
  2. The Membership class uses the web.config to determine what MembershipProvider to use.
  3. In the end the Membership class calls the ValiadateUser method of the membership provider that was determined in step 2. The ValidateUser method verifies if the specified username and password exist and are valid.

The MembershipProvider acts like a mediator between the ASP.NET authentication system and the collection of users. It defines methods for validating the user credentials, for creating new users, modifying the user password and a lot of other user account related operations. For more information here is an introduction to membership in MSDN. So, it provides a way to decouple the data source where the user information is stored (e.g. DB, active directory) from the authentication system so that the authentication will work in the same way no matter where the user information is stored.

Microsoft provides out of the box implementations for ActiveDirectoryMembershipProvider and SqlMembershipProvider but we can also create our own custom implementation by inheriting from the MembershipProvider class and implementing the methods that we need.
Note: if there is a method that you don’t need just leave an empty implementation because it will not be invoked except if you’ll call it explicitly from the code.

In a similar way works also the authorization:

Authorization

  1. We use the AuthorizeAttribute inside the controller classes to mark the action methods that can be invoked only if the user is authenticated and/or has a given role. Then the AuthorizeAttribute uses the Roles class to check if the currently logged user has the required role.
  2. The Roles class uses the web.config to understand what RoleProvider to use.
  3. The RoleProvider is an abstract class that defines the basic methods that all role providers will have. We can use the supplied role providers (e.g. SqlRoleProvider) that are included with the .NET Framework, or we can implement our own custom provider.

2. Implement a custom membership provider Go top

We will continue the demo application we used throughout the previous ASP.NET MVC 4 tutorials and we will add a login page that uses a custom membership provider to authenticate the users. You can download the source code for the demo application (that we are using here as a starting point) from CodePlex.

First we will create a custom membership provider that inherits from the MembershipProvider class.

using System;
using System.Linq;
using System.Web.Security;
using HelloWorld.Code.DataAccess;

namespace HelloWorld.Code.Security
{
    public class CustomMembershipProvider : MembershipProvider
    {
        #region Overrides of MembershipProvider

        /// <summary>
        /// Verifies that the specified user name and password exist in the data source.
        /// </summary>
        /// <returns>
        /// true if the specified username and password are valid; otherwise, false.
        /// </returns>
        /// <param name="username">The name of the user to validate. </param><param name="password">The password for the specified user. </param>
        public override bool ValidateUser(string username, string password)
        {
            if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
                return false;

            using (var context = new MvcDemoEntities())
            {
                var user = (from u in context.Users
                            where String.Compare(u.Username, username, StringComparison.OrdinalIgnoreCase) == 0
                                  && String.Compare(u.Password, password, StringComparison.OrdinalIgnoreCase) == 0
                                  && !u.Deleted
                            select u).FirstOrDefault();

                return user != null;
            }
        }

        #endregion

        #region Overrides of MembershipProvider that throw NotImplementedException

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            throw new NotImplementedException();
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new NotImplementedException();
        }

        public override string GetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            throw new NotImplementedException();
        }

        public override string ResetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        public override void UpdateUser(MembershipUser user)
        {
            throw new NotImplementedException();
        }

        public override bool UnlockUser(string userName)
        {
            throw new NotImplementedException();
        }

        public override string GetUserNameByEmail(string email)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override bool EnablePasswordRetrieval
        {
            get { throw new NotImplementedException(); }
        }

        public override bool EnablePasswordReset
        {
            get { throw new NotImplementedException(); }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { throw new NotImplementedException(); }
        }

        public override string ApplicationName
        {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { throw new NotImplementedException(); }
        }

        public override int PasswordAttemptWindow
        {
            get { throw new NotImplementedException(); }
        }

        public override bool RequiresUniqueEmail
        {
            get { throw new NotImplementedException(); }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { throw new NotImplementedException(); }
        }

        public override int MinRequiredPasswordLength
        {
            get { throw new NotImplementedException(); }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { throw new NotImplementedException(); }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { throw new NotImplementedException(); }
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

Note that we provided an implementation only for the ValidateUser method because this is the only method needed to validate the user credentials and, for the moment, we don’t need the other features of the membership provider, like, for example, the ResetPassword method.

Next we will edit the web.config file and enable forms authentication. We will also specify that we will use the CustomMembershipProvider that we just created:

<authentication mode="Forms">
  <forms loginUrl="~/Account/Login" defaultUrl="~/" timeout="20" slidingExpiration="true" />
</authentication>
<membership defaultProvider="CustomMembershipProvider">
  <providers>
	<clear />
	<add name="CustomMembershipProvider"
		 type="HelloWorld.Code.Security.CustomMembershipProvider" />
  </providers>
</membership>

Now we will create a simple log in page that will use the Membership class to check if the user credentials are valid and the FormsAuthentication class to manage the forms authentication inside our site. The implementation is straight forward and I think the code speaks for itself so I’ll copy-paste here the code with minimal annotations.

For displaying the log in page data I created a simple view model that helps with the required data validation:

using System.ComponentModel.DataAnnotations;

namespace HelloWorld.Models
{
    public class Login
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }
}

This view model is used inside the view:

@model HelloWorld.Models.Login
@{
    ViewBag.Title = "Log in";
}

<h2>@ViewBag.Title</h2>
<hr/>
@Html.ValidationSummary(true)
<br/>
@using (Html.BeginForm(null, null, new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post))
{
    @Html.LabelFor(m => m.UserName)
    @Html.TextBoxFor(m => m.UserName)
    @Html.ValidationMessageFor(m => m.UserName)
    <br/>
    @Html.LabelFor(m => m.Password)
    @Html.PasswordFor(m => m.Password)
    @Html.ValidationMessageFor(m => m.Password)
    <br/>    
    <label>
        @Html.CheckBoxFor(m => m.RememberMe)
        @Html.LabelFor(m => m.RememberMe)
    </label>
    <br/> 
    <input type="submit" value="Log in" />
}

The controller class that works with this view and authenticates the users is:

using System.Web.Mvc;
using System.Web.Security;
using HelloWorld.Models;

namespace HelloWorld.Controllers
{
    public class AccountController : Controller
    {        
        [HttpGet]
        [AllowAnonymous]
        public ActionResult Login(string returnUrl = "")
        {
            if (User.Identity.IsAuthenticated)
            {
                return LogOut();
            }

            ViewBag.ReturnUrl = returnUrl;
            return View();
        }
    
        [HttpPost]
        [AllowAnonymous]
        public ActionResult Login(Login model, string returnUrl = "")
        {
            if(ModelState.IsValid)
            {
                if (Membership.ValidateUser(model.UserName, model.Password))
                {                   
                    FormsAuthentication.RedirectFromLoginPage(model.UserName, model.RememberMe);
                }

                ModelState.AddModelError("", "Incorrect username and/or password");
            }
                          
            return View(model);
        }

        [HttpPost]
        [AllowAnonymous]
        public ActionResult LogOut()
        {
            FormsAuthentication.SignOut();
            return RedirectToAction("Login", "Account", null);
        }
    }
}

Notice here the use of the AllowAnonymousAttribute. This attribute can be used to indicate that an action method inside a controller or a whole controller doesn’t require user authorization, hence can be accessed by anonymous users. This attribute is used in conjunction with the AuthorizeAttribute that provides a way to restrict the access. Both of this attributes can be set at action method level or at controller level, if we want the same attribute to be applied to all the action methods inside a controller.

Next, we will apply the authorize attribute to our UsersController and HomeController like this:

[Authorize]
public class HomeController : Controller
{
     //code omitted for brevity
}

If we run now the application we will not be able to see the home page until we log in.
Login Page

3. Implement a custom role provider Go top

Now we would like to allow the access to the users controller only for administrators. For doing this we will use the same AuthorizeAttribute that we used before but, this time, we will provide also the name of the user role that is needed to invoke this controller:

[Authorize(Roles = "Administrator")]
public class UsersController : Controller
{
     //code omitted for brevity
}

For this to work we need to add a custom role provider that will be used to return the roles that a user has.

using System;
using System.Collections.Specialized;
using System.Linq;
using System.Data.Entity;
using System.Web;
using System.Web.Caching;
using System.Web.Security;
using HelloWorld.Code.DataAccess;

namespace HelloWorld.Code.Security
{
    public class CustomRoleProvider : RoleProvider
    {
        #region Properties

        private int _cacheTimeoutInMinutes = 30;

        #endregion

        #region Overrides of RoleProvider

        /// <summary>
        /// Initialize values from web.config.
        /// </summary>
        /// <param name="name">The friendly name of the provider.</param>
        /// <param name="config">A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider.</param>
        public override void Initialize(string name, NameValueCollection config)
        {
            // Set Properties
            int val;
            if (!string.IsNullOrEmpty(config["cacheTimeoutInMinutes"]) && Int32.TryParse(config["cacheTimeoutInMinutes"], out val))
                _cacheTimeoutInMinutes = val;

            // Call base method
            base.Initialize(name, config);
        }

        /// <summary>
        /// Gets a value indicating whether the specified user is in the specified role for the configured applicationName.
        /// </summary>
        /// <returns>
        /// true if the specified user is in the specified role for the configured applicationName; otherwise, false.
        /// </returns>
        /// <param name="username">The user name to search for.</param><param name="roleName">The role to search in.</param>
        public override bool IsUserInRole(string username, string roleName)
        {
            var userRoles = GetRolesForUser(username);
            return userRoles.Contains(roleName);
        }

        /// <summary>
        /// Gets a list of the roles that a specified user is in for the configured applicationName.
        /// </summary>
        /// <returns>
        /// A string array containing the names of all the roles that the specified user is in for the configured applicationName.
        /// </returns>
        /// <param name="username">The user to return a list of roles for.</param>
        public override string[] GetRolesForUser(string username)
        {
            //Return if the user is not authenticated
            if (!HttpContext.Current.User.Identity.IsAuthenticated) 
                return null;

            //Return if present in Cache
            var cacheKey = string.Format("UserRoles_{0}", username);
            if (HttpRuntime.Cache[cacheKey] != null)
                return (string[])HttpRuntime.Cache[cacheKey];

            //Get the roles from DB
            var userRoles = new string[] { };
            using (var context = new MvcDemoEntities())
            {
                var user = (from u in context.Users.Include(usr => usr.UserRole)
                            where String.Compare(u.Username, username, StringComparison.OrdinalIgnoreCase) == 0
                            select u).FirstOrDefault();

                if (user != null)
                    userRoles = new[]{user.UserRole.UserRoleName};
            }

            //Store in cache
            HttpRuntime.Cache.Insert(cacheKey, userRoles, null, DateTime.Now.AddMinutes(_cacheTimeoutInMinutes), Cache.NoSlidingExpiration);

            // Return
            return userRoles.ToArray();
        }      

        #endregion

        #region Overrides of RoleProvider that throw NotImplementedException

        public override void CreateRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            throw new NotImplementedException();
        }

        public override bool RoleExists(string roleName)
        {
            throw new NotImplementedException();
        }

        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override string[] GetUsersInRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override string[] GetAllRoles()
        {
            throw new NotImplementedException();
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            throw new NotImplementedException();
        }

        public override string ApplicationName
        {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }

        #endregion
    }
}

And, of course, we need to specify that we are using this role provider inside the web.config class:

<roleManager defaultProvider="CustomRoleProvider" enabled="true">
  <providers>
	<clear />
	<add name="CustomRoleProvider" 
		 type="HelloWorld.Code.Security.CustomRoleProvider" 
		 cacheTimeoutInMinutes="30" />
  </providers>
</roleManager>

Now, if we try to access the user list page while being authenticated as users we will be automatically redirected to the log in page.

Next we will add a log out link in the _Layout.chtml file, inside the header div, that will be visible only if the current user is logged in:

<span style="float:right;">@if (HttpContext.Current.User.Identity.IsAuthenticated)
						   {
							   @Html.ActionLink("Log out", "Logout", "Account");
						   }</span>

We are using HttpContext.Current to get all the HTTP-specific information for the current HTTP request. One of these properties is the User property that encapsulates the security information for the current request. The security information related to the current user is accessed through the IPrincipal interface. This interface can be used to check if the current user belong to a given role and gives access to the user’s identity through the IIdentity interface. The IIdentity encapsulates the user data and can be seen as the interface that defines who the user is while the IPrincipal object defines who the current user is and what he is allowed to do.

Sometimes it is useful to extend the default identity and principal objects to include additional user information. This is what we will do in the next section.

4. Implement a custom user principal and identity Go top

First we will define a custom identity class that inherits from IIdentity and receives as an input parameter the default identity object that is created by the forms authentication.

using System;
using System.Security.Principal;
using System.Web.Security;

namespace HelloWorld.Code.Security
{
    /// <summary>
    /// An identity object represents the user on whose behalf the code is running.
    /// </summary>
    [Serializable]
    public class CustomIdentity : IIdentity
    {
        #region Properties

        public IIdentity Identity { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string Email { get; set; }

        public int UserRoleId { get; set; }

        public string UserRoleName { get; set; }

        #endregion

        #region Implementation of IIdentity

        /// <summary>
        /// Gets the name of the current user.
        /// </summary>
        /// <returns>
        /// The name of the user on whose behalf the code is running.
        /// </returns>
        public string Name
        {
            get { return Identity.Name; }
        }

        /// <summary>
        /// Gets the type of authentication used.
        /// </summary>
        /// <returns>
        /// The type of authentication used to identify the user.
        /// </returns>
        public string AuthenticationType
        {
            get { return Identity.AuthenticationType; }
        }

        /// <summary>
        /// Gets a value that indicates whether the user has been authenticated.
        /// </summary>
        /// <returns>
        /// true if the user was authenticated; otherwise, false.
        /// </returns>
        public bool IsAuthenticated { get { return Identity.IsAuthenticated; } }

        #endregion

        #region Constructor

        public CustomIdentity(IIdentity identity)
        {
            Identity = identity;

            var customMembershipUser = (CustomMembershipUser) Membership.GetUser(identity.Name);
            if(customMembershipUser != null)
            {
                FirstName = customMembershipUser.FirstName;
                LastName = customMembershipUser.LastName;
                Email = customMembershipUser.Email;
                UserRoleId = customMembershipUser.UserRoleId;
                UserRoleName = customMembershipUser.UserRoleName;
            }
        }
 
        #endregion
    }
}

The constructor uses the name of the default identity and the current membership provider to get the user data. We will modify our custom membership provider by implementing the methods GetUser and Initialize. We need to implement the Initialize method because we will use caching to remember the user information between post backs and we want to be able to set the caching time in the web.config like this:

<membership defaultProvider="CustomMembershipProvider">
  <providers>
	<clear />
	<add name="CustomMembershipProvider"
		 type="HelloWorld.Code.Security.CustomMembershipProvider" 
		 cacheTimeoutInMinutes="30" />
  </providers>
</membership>

In the initialize method we will read the cacheTimeoutInMinutes value as it is defined in the web.config.

private int _cacheTimeoutInMinutes = 30;

/// <summary>
/// Initialize values from web.config.
/// </summary>
/// <param name="name">The friendly name of the provider.</param>
/// <param name="config">A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider.</param>
public override void Initialize(string name, NameValueCollection config)
{
	// Set Properties
	int val;
	if (!string.IsNullOrEmpty(config["cacheTimeoutInMinutes"]) && Int32.TryParse(config["cacheTimeoutInMinutes"], out val))
		_cacheTimeoutInMinutes = val;

	// Call base method
	base.Initialize(name, config);
}

Than we implement the method for retrieving the user data. Because we want to return more user information than we can store in the default MembershipUser, that is the default return type of the GetUser method, we will first create a custom implementation for the MembershipUser.

using System;
using System.Web.Security;
using HelloWorld.Code.DataAccess;

namespace HelloWorld.Code.Security
{
    public class CustomMembershipUser : MembershipUser
    {
        #region Properties

        public string FirstName { get; set; }

        public string LastName { get; set; }

        public int UserRoleId { get; set; }

        public string UserRoleName { get; set; }

        #endregion

        public CustomMembershipUser(User user)
            : base("CustomMembershipProvider", user.Username, user.UserId, user.Email, string.Empty, string.Empty, true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now)
        {
            FirstName = user.FirstName;
            LastName = user.LastName;
            UserRoleId = user.UserRoleId;
            UserRoleName = user.UserRole.UserRoleName;
        }
    }
}

Now we can implement the method for retrieving the user data inside our custom membership provider:

/// <summary>
/// Gets information from the data source for a user. Provides an option to update the last-activity date/time stamp for the user.
/// </summary>
/// <returns>
/// A <see cref="T:System.Web.Security.MembershipUser"/> object populated with the specified user's information from the data source.
/// </returns>
/// <param name="username">The name of the user to get information for. </param><param name="userIsOnline">true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user. </param>
public override MembershipUser GetUser(string username, bool userIsOnline)
{
	var cacheKey = string.Format("UserData_{0}", username);
	if (HttpRuntime.Cache[cacheKey] != null)
		return (CustomMembershipUser)HttpRuntime.Cache[cacheKey];
	
	using (var context = new MvcDemoEntities())
	{
		var user = (from u in context.Users.Include(usr => usr.UserRole)
					where String.Compare(u.Username, username, StringComparison.OrdinalIgnoreCase) == 0
						  && !u.Deleted
					select u).FirstOrDefault();

		if (user == null)
			return null;

		var membershipUser = new CustomMembershipUser(user);

		//Store in cache
		HttpRuntime.Cache.Insert(cacheKey, membershipUser, null, DateTime.Now.AddMinutes(_cacheTimeoutInMinutes), Cache.NoSlidingExpiration);

		return membershipUser;
	}            
}

We will also create a custom principal that works with our new CustomIdentity class.

using System;
using System.Security.Principal;

namespace HelloWorld.Code.Security
{
    [Serializable]
    public class CustomPrincipal : IPrincipal
    {
        #region Implementation of IPrincipal

        /// <summary>
        /// Determines whether the current principal belongs to the specified role.
        /// </summary>
        /// <returns>
        /// true if the current principal is a member of the specified role; otherwise, false.
        /// </returns>
        /// <param name="role">The name of the role for which to check membership. </param>
        public bool IsInRole(string role)
        {
            return Identity is CustomIdentity &&
                   string.Compare(role, ((CustomIdentity) Identity).UserRoleName, StringComparison.CurrentCultureIgnoreCase) == 0;
        }

        /// <summary>
        /// Gets the identity of the current principal.
        /// </summary>
        /// <returns>
        /// The <see cref="T:System.Security.Principal.IIdentity"/> object associated with the current principal.
        /// </returns>
        public IIdentity Identity { get; private set; }

        #endregion

        public CustomIdentity CustomIdentity { get { return (CustomIdentity)Identity; } set { Identity = value; } }

        public CustomPrincipal(CustomIdentity identity)
        {
            Identity = identity;
        }
    }
}

The only thing left is to replace the default HttpContext.Current.User with our new CustomProvider. We will do this by adding the method Application_PostAuthenticateRequest inside Global.asax:

protected void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
	if (Request.IsAuthenticated)
	{
		var identity = new CustomIdentity(HttpContext.Current.User.Identity);
		var principal = new CustomPrincipal(identity);
		HttpContext.Current.User = principal;
	}
}

Now we are able to use our new custom identity and principal. For example we will modify the _Layout.chtml file to display more information about the currently logged user in the header.

<div id="header">
	<i>=(^.^)= @ViewBag.Title</i>
	<span style="float:right; font-size: small;">@if (HttpContext.Current.User.Identity.IsAuthenticated)
							   {
								   var identity = ((CustomPrincipal)HttpContext.Current.User).CustomIdentity;
								   @Html.Label(string.Format("Welcome {0} {1}, you are logged as {2}", identity.FirstName, identity.LastName, identity.UserRoleName))
								   @Html.ActionLink("Log out", "Logout", "Account")
							   }</span>
</div>

And, the final touch, we create an extension method that converts an IPrincipal object to a CustomIdentity object. We do this just because the code will be more elegant:

using System.Security.Principal;

namespace HelloWorld.Code.Security
{
    public static class SecurityExtentions
    {
        public static CustomPrincipal ToCustomPrincipal(this IPrincipal principal)
        {
            return (CustomPrincipal) principal;
        }
    }
}

Now we can get the custom identity like this:

var identity = HttpContext.Current.User.ToCustomPrincipal().CustomIdentity;

The final result will be like this:
Display the user data

5. Implement a custom authorization filter Go top

It is possible to create custom attributes that, when applied to a controller class and/or a controller action method will perform an additional logic before or after the action method is executed. These attributes must inherit from the FilterAttribute class and must implement at least one of the following interfaces:

  1. IAuthorizationFilter
  2. IActionFilter
  3. IResultFilter
  4. IExceptionFilter

If an action method has more than one filter then they will be executed in the same order as in the list above, starting with the authorization filters and continuing with the action, result and, in the end, exception filters. You can find more information about action filtering and about the order in which the action filters are executed on MSDN.

As a first example, we will create a custom attribute that inherits from the System.Web.Mvc.AuthorizeAttribute and will allow us to pass the allowed user roles as a list of UserRole enumerations instead of passing a string containing the comma separated list of allowed user roles.

using System.Linq;

namespace HelloWorld.Code.Security
{
    public class UserRoleAuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
    {
        public UserRoleAuthorizeAttribute(){}

        public UserRoleAuthorizeAttribute(params UserRole[] roles)
        {
            Roles = string.Join(",", roles.Select(r => r.ToString()));
        }
    }

    public enum UserRole
    {
        Administrator = 1,
        User = 2
    }
}

Usage example:

[UserRoleAuthorize(UserRole.Administrator, UserRole.User)]

As a second example we will create a new custom filter that will allow the execution of an action method only between two given hours. For example, we want to allow the user add, edit and delete functionality only between 9 in the morning and 19 in the evening. If we are outside this time frame then we will redirect the user to the home page. Again we will be inheriting the System.Web.Mvc.AuthorizeAttribute because it is easier to start from it than from the basic FilterAttribute class.

using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace HelloWorld.Code.Security
{
    public class TimeAuthorizeAttribute : AuthorizeAttribute
    {
        public int StartTime { get; set; }
        public int EndTime { get; set; }

        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            if (DateTime.Now.Hour < StartTime)
                return false;

            if (EndTime <= DateTime.Now.Hour)
                return false;

            return true;
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } });
        }
    }
}

All we did was to override the method that decides if the current user is authorized AuthorizeCore. In case the user is not authorized we will redirect him to the home page. We accomplish this by overriding the method HandleUnauthorizedRequest.

6. Summary Go top

In this post we had a look on how to implement custom authentication in an ASP.NET MVC 4 application. You can download the source code that contains all the points discussed here from Codeplex.