Cacomania: Validating a mongoDB ObjectID in ASP .net MVC 4

Cacomania

Validating a mongoDB ObjectID in ASP .net MVC 4

Guido Krömer - 22. January 2013 - Tags: , , ,

A little learning project I am working on, uses ASP.NET MVC 4 with mongoDB as the back end database instead of Microsoft SQL Server. During this learning project I ran into a little problem, SQL Databases uses an integer as primary key, mongoDB uses a ObjectId. This ObjectId has some advantages especially if mongoDB runs into a sharded environment.

The ObjectId

MongoDB ObjectID
A ObjectId is a 12 bytes long construct consisting out of a timestamp, a machine identifier, a process id and an incremented number, the counter. The process id and machine identifier part makes the usage of an ObjectId save in a sharded environment. The external representation of this ObjectId is a 24 characters long string containing hexadecimal characters.

The "Problem" with the ObjectId in the controller and model context

This is the point where my problem started. Using a SQL Database a typical controller action could look like this one:

public ActionResult Show(int id)
{
    return View(MyModel.Read(id));
}

A model would look like this one:

public class Comment
{
    [Required]
    [EmailAddress]
    [Display(Name = "E-Mail")]
    public string EMail { get; set; }

    [Required]
    [StringLength(255, MinimumLength = 5)]
    [Display(Name = "Comment")]
    public string Text { get; set; }

    [Required]
    public int PostId { get; set; }
}

The primary key would be validated in both cases by the data type, but the ObjectId, which is a string need additional validation. Validating the ObjectId just by the length of the string would be not enough and limited to the model validation:

[Required]
[StringLength(24, MinimumLength = 24)]
public string PostId { get; set; }

The ObjectId ValidationAttribute

For validating an ObjectId in the model context I had to create a new ValidationAttribute. For validating the given string at first the most obvious invalidation cases like an empty string and the string length which has to be 24 characters get checked. Instead of writing my own ObjectId validator it seems to be more precise using a given component from the official mongoDB .net library for performing this task. Creating a new ObjectId object with an invalid string would throw a FormatException, if no exception has been thrown the given sting must be a valid ObjectId.

using MongoDB.Bson;
using System;
using System.ComponentModel.DataAnnotations;

namespace MongoDBValidationAttributes.Attributes
{
    public class MongoDBObjectIdAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            string objectId = value as string;

            if (string.IsNullOrEmpty(objectId))
            {
                return new ValidationResult("Invalid Id, the Id can't be null or empty!");
            }

            if (objectId.Length != 24)
            {
                return new ValidationResult("Invalid Id, the Id length must be equal to 24!");
            }

            try
            {
                new ObjectId(objectId);
                return ValidationResult.Success;
            }
            catch (FormatException e)
            {
                return new ValidationResult("Invalid Id, the Id contains some invalid characters!");
            }
        }
    }
}

The PostId in the model below will now be validated with the MongoDBObjectIdAttribute:

using MongoDBValidationAttributes.Attributes;
using System.ComponentModel.DataAnnotations;

namespace MongoDBValidationAttributes.Models
{
    public class Comment
    {
        [Required]
        [EmailAddress]
        [Display(Name = "E-Mail")]
        public string EMail { get; set; }

        [Required]
        [StringLength(255, MinimumLength = 5)]
        [Display(Name = "Comment")]
        public string Text { get; set; }

        [Required]
        [MongoDBObjectIdAttribute]
        public string PostId { get; set; }
    }
}

The ObjectId AuthorizeAttribute

Validating prams directly send to the action, like in the example below, is a little bit more tricky. If the given id is no ObjectId the action should not be executed. This can archived with a custom AuthorizeAttribute which redirects the request if the "Authorisation" fails. My approach using the AuthorizeAttribute might sound a little bit extreme, but invalid ObjectIds have this three reasons:

  • A bug which generates a wrong link.
  • A foreign link which is wrong.
  • An user who's playing around with the query params.

The first reason can be avoided with testing, the second is something where the influence is equal to null and the third should be catched!

public ActionResult Show(string id)
{
    return View(MyModel.Read(id));
}

There are two possibilities how an id can be send to an action, by a link like this one: "http://www.cacodaemon.de/mymodel/show/507f1f77bcf86cd799439011" where the id does not appear in the url query part or with a link like this: "http://www.cacodaemon.de/mymodel/edit/?commentId=507f1f77bcf86cd799439011&text=Foo%20Bar" where the id is part of the query. By default my ObjectIdFilterAttribute looks for an id in links like the first one, but by defining the name with the queryStringParam, links following the second scheme can be validated, too. The example shows both cases:

[ObjectIdFilterAttribute]
public ActionResult DeleteComment(string id)
{
    return RedirectToAction("Success");
}

[ObjectIdFilterAttribute("postId")]
public ActionResult EditComment(string postId, string text)
{
    return RedirectToAction("Success");
}

Instead of writing some duplicate code I reuse the MongoDBObjectIdAttribute for validation. The overridden HandleUnauthorizedRequest method performs the redirection if the validation fails. The rest of the code is really self explaining.

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

namespace MongoDBValidationAttributes.Attributes
{
    public class ObjectIdFilterAttribute : AuthorizeAttribute
    {
        protected string queryStringParam;

        public ObjectIdFilterAttribute() { }

        public ObjectIdFilterAttribute(string queryStringParam) 
        {
            this.queryStringParam = queryStringParam;
        }

        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            string objectId;
            
            if (string.IsNullOrEmpty(queryStringParam)) 
            {
                string url = httpContext.Request.RawUrl;
                objectId = url.Substring(url.LastIndexOf("/") + 1);
            }
            else 
            {
                objectId = httpContext.Request.QueryString[queryStringParam];
            }

            return new MongoDBObjectIdAttribute().IsValid(objectId);
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            UrlHelper urlHelper = new UrlHelper(filterContext.RequestContext);
            filterContext.Result = new RedirectResult(urlHelper.Action("Error", "Home"));
        }
    }
}

A demo controller and model.

I have created a small demo which can be downloaded here since I cannot host any .net stuff. The demo consists out of a single controller, one model and some views, it does not need an existing mongoDB because nothing gets stored or read.
ASP .net MVC 4 MongoDB filter and validation attributes project folder view.

Here is a screenshot of the application with some valid and invalid links and a form were the ObjectId field for adding a new comment is not a hidden field for easy manipulation. Screenshot of the MongoDB attributes validation test ASP .net MVC 4 application.

This is just the whole test controller class:

using MongoDBValidationAttributes.Attributes;
using MongoDBValidationAttributes.Models;
using System.Web.Mvc;

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

        public ActionResult NewComment(string id)
        {
            return PartialView(new Comment {
                PostId = id
            });
        }

        public ActionResult AddComment(Comment comment)
        {
            if (!ModelState.IsValid)
            {
                return View("NewComment", comment);
            }

            return RedirectToAction("Index");
        }

        [ObjectIdFilterAttribute]
        public ActionResult DeleteComment(string id)
        {
            return RedirectToAction("Success");
        }

        [ObjectIdFilterAttribute("postId")]
        public ActionResult EditComment(string postId, string text)
        {
            return RedirectToAction("Success");
        }

        public ActionResult Success() 
        {
            return View();
        }

        public ActionResult Error()
        {
            return View();
        }
    }
}

The form for adding a new comment is a strictly typed view which displays the validation messages, too.

@model MongoDBValidationAttributes.Models.Comment

<h2>NewComment</h2>

@using (Html.BeginForm("AddComment", "Home")) {
    <small>This would be a hidden field:</small><br />
    @Html.LabelFor(m => m.PostId)
    @Html.TextBoxFor(m => m.PostId)
    @Html.ValidationMessageFor(m => m.PostId)
    <hr />
    @Html.LabelFor(m => m.EMail)
    @Html.TextBoxFor(m => m.EMail)
    @Html.ValidationMessageFor(m => m.EMail)
    <hr />
    @Html.LabelFor(m => m.Text)
    @Html.TextBoxFor(m => m.Text)
    @Html.ValidationMessageFor(m => m.Text)
    <hr />
    <input type="submit" value="Add"/>
}

The PostId entered is invalid, take a look at the "zzzz"s in the id. The message comes from the MongoDBObjectIdAttribute, but actually lacks of globalisation :) .

ASP .net MVC 4 ValidationAttribute MongoDB ObjectID example error message.

That's All Folks! I hope you enjoyed my little post.