:::: MENU ::::

Monday, February 13, 2012

 
url: "Customers/{id}",
        defaults: new { controller = "Customer", action = "Get" },
        constraints: new { httpMethod = new HttpMethodConstraint("GET") }
        );
  
     routes.MapRoute(
         name: "CustomerUpdate",
         url: "Customers/{id}",
         defaults: new { controller = "Customer", action = "Update" },
         constraints: new { httpMethod = new HttpMethodConstraint("POST") }
         );
  
     routes.MapRoute(
         name: "MVC Default",
         url: "{controller}/{action}/{id}",
         defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
     );
  
    
 }

You can override the default route for "Customers/{id}" for getting or updating an specific customer by using an http method constraint. I also added the first  route for adding a new customer so the "Add" segment is not used as the "{id}" wildcard.

As you could see, we could split all the responsibilities in two controllers, which look simpler at first glance. In conclusion, you don't need to assume the same URL means the same controller.

The "delete" action is implemented as an http post. This can be sent from the browser by using an Ajax call or a Http form. For example, the following code shows how to do that using a JQuery Ajax call.

 @Html.ActionLink("Delete", "Delete", new { id = item.Id }, new { @class = "delete" })
 <script type="text/javascript" language="javascript">
    1:  
    2:     $(function () {
    3:         $('.delete').click(function () {
    4:             var that = $(this);
    5:             var url = that.attr('href');
    6:  
    7:             $.post(url, function () {
    8:                 alert('delete called');
    9:             });
   10:  
   11:             return false;
   12:         });
   13:     });
</script>
The code is available for download from here

More

Although JQuery provides a very good support for caching responses from AJAX calls in the browser, it is always good to know how you can use http as protocol for making an effective use of it.

The first thing you need to do on the server side is to supports HTTP GETs, and identify your resources with different URLs for retrieving the data (The resource in this case could be just a MVC action). If you use the same URL for retrieving different resource representations, you are doing it wrong. An HTTP POST will not work either as the response can not cached. Many devs typically use HTTP POSTs for two reason, they want to make explicit the data can not be cached or they use it as workaround for avoidingJSON hijacking attacks, which can be avoided anyways in HTTP GETs by returning a JSON array.

The ajax method in the JQuery global object provides a few options for supporting caching and conditional gets,

 $.ajax({
     ifModified: [true|false],
     cache: [true|false],
 });

The "ifModified" flag specifies whether we want to support conditional gets in the ajax calls. JQuery will automatically handle everything for us by picking the last received "Last-Modified" header from the server, and sending that as "If-Modified-Since" in all the subsequent requests. This requires that our MVC controller implements conditional gets. A conditional get in the context of http caching is used to revalidate an expired entry in the cache. If JQuery determines an entry is expired, it will be first try to revalidate that entry using a conditional get against the server. If the response returns an status code 304 (Not modified), JQuery will reuse the entry in the cache. In that way, we can save some of the bandwidth required to download the complete payload associated to that entry from the server.

The "cache" option basically overrides all the caching settings sent by the server as http headers. By setting this flag to false, JQuery will add an auto generated timestamp in the end of the URL to make it different from any previous used URL, so the browser will not know how to cache the responses.

Let's analyze a couple scenarios.

The server sets a No-Cache header on the response

The server is the king. If the server explicitly states the response can not be cached, JQuery will honor that. The "cache" option on the ajax call will be completely ignored.

 $('#nocache').click(function () {
     $.ajax({
         url: '/Home/NoCache',
         ifModified: false,
         cache: true,
         success: function (data, status, xhr) {
             $('#content').html(data.count);
         }
     });
 });
 public ActionResult NoCache()
 {
    Response.Cache.SetCacheability(HttpCacheability.NoCache);
    return Json(new { count = Count++ }, JsonRequestBehavior.AllowGet);
 }

The server sets an Expiration header on the response

Again, the server is always is the one in condition for setting an expiration time for the data it returns. The entry will cached on the client side using that expiration setting.

 $('#expires').click(function () {
     $.ajax({
         url: '/Home/Expires',
         ifModified: false,
         cache: true,
         success: function (data, status, xhr) {
             $('#content').html(data.count);
         }
     });
 });
 public ActionResult Expires()
 {
     Response.Cache.SetExpires(DateTime.Now.AddSeconds(5));
     return Json(new { count = Count++ }, JsonRequestBehavior.AllowGet);
 }

The client never caches the data

The client side specifically states the data must be always fresh and the cache not be used. This means the "cache" option is set to false. No matter what the server specifies, JQuery will always generate an unique URL so that will be impossible to cache.

 $('#expires_nocache').click(function () {
     $.ajax({
         url: '/Home/Expires',
         ifModified: false,
         cache: false,
         success: function (data, status, xhr) {
             $('#content').html(data.count);
         }
     });
 });
 public ActionResult Expires()
 {
     Response.Cache.SetExpires(DateTime.Now.AddSeconds(5));
     return Json(new { count = Count++ }, JsonRequestBehavior.AllowGet);
 }

The client and server use conditional gets for validating the cached data.

The client puts a new entry in the cache, which will be validated after its expiration.  The server side must implement a conditional GET using either ETags or the last modified header.

 $('#expires_conditional').click(function () {
     $.ajax({
         url: '/Home/ExpiresWithConditional',
         ifModified: true,
         cache: true,
         success: function (data, status, xhr) {
             $('#content').html(data.count);
         }
     });
 });
 public ActionResult ExpiresWithConditional()
 {
     if (Request.Headers["If-Modified-Since"] != null && Count % 2 == 0)
     {
         return new HttpStatusCodeResult((int)HttpStatusCode.NotModified);
     }
     
     Response.Cache.SetExpires(DateTime.Now.AddSeconds(5));
     Response.Cache.SetLastModified(DateTime.Now);
  
     return Json(new { count = Count++ }, JsonRequestBehavior.AllowGet);
 }

The MVC action in the example above is only an example. In a real implementation, the server should be able to know whether the data has changed since the last time it was served or not.


More

Wednesday, February 8, 2012

This post will demonstrate how to do rock-solid phone number validation using the .NET port of Google'slibphonenumber. When validating phone numbers you tend to end up either frustrating users with strict input requirements or with complicated regexes to cover all the interesting ways that users input phone numbers. If you have to also check international numbers (as we do at AppHarbor) the task becomes almost impossible.

Enter libphonenumber, a library from Google containing years of accumulated wisdom on how to parse phone numbers from all over the world. Patrick Mézard has created a .NET port of the Java version and there is even a NuGet package.

This makes using libphonenumber for validation as easy as installing the NuGet package and wrapping it in aDataAnnotation attribute like the one below (or just use the library straight up). The Attribute below will parse US numbers with and without country code while international numbers require a country code. Example of use:

 using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using MyNamespace.DataAnnotations;

public class Address
{
   
[DisplayName("Phone number")]
   
[Editable(allowEdit: true)]
   
[PhoneNumberAttribute]
   
[Required]
   
public virtual string PhoneNumber
   
{
       
get;
       
set;
   
}
}

Attribute:

 using System.ComponentModel.DataAnnotations;
using PhoneNumbers;

namespace MyNamespace.DataAnnotations
{
   
public class PhoneNumberAttribute : ValidationAttribute
   
{
       
public override bool IsValid(object value)
       
{
           
var valueString = value as string;
           
if (string.IsNullOrEmpty(valueString))
           
{
               
return true;
           
}

           
var util = PhoneNumberUtil.GetInstance();
           
try
           
{
               
var number = util.Parse(valueString, "US");
               
return util.IsValidNumber(number);
           
}
           
catch (NumberParseException)
           
{
               
return false;
           
}
       
}
   
}
}

Unit tests for attribute:

 using MyNamespace.DataAnnotations;
using Xunit;
using Xunit.Extensions;

namespace MyNamespace.UnitTest.DataAnnotations
{
   
public class PhoneNumberAttributeTest
   
{
       
private readonly PhoneNumberAttribute _subjectUnderTest;

       
public PhoneNumberAttributeTest()
       
{
            _subjectUnderTest
= new PhoneNumberAttribute();
       
}

       
[Fact]
       
public void GivenNull_WhenValidate_ThenIsValid()
       
{
           
Assert.True(_subjectUnderTest.IsValid(null));
       
}

       
[Fact]
       
public void GivenEmptyString_WhenValidate_ThenIsValid()
       
{
           
Assert.True(_subjectUnderTest.IsValid(string.Empty));
       
}

       
[InlineData("+4527122799")]
       
[InlineData("6503181051")]
       
[InlineData("+16503181051")]
       
[InlineData("1-650-318-1051")]
       
[InlineData("+1-650-318-1051")]
       
[InlineData("+1650-318-1051")]
       
[Theory]
       
public void GivenValidPhoneNumber_WhenValidate_ThenIsValid(string phoneNumberString)
       
{
           
Assert.True(_subjectUnderTest.IsValid(phoneNumberString));
       
}

       
[InlineData("123")]
       
[InlineData("foo")]
       
[Theory]
       
public void GivenInValidPhoneNumber_WhenValidate_ThenIsNotValid(string phoneNumberString)
       
{
           
Assert.False(_subjectUnderTest.IsValid(phoneNumberString));
       
}
   
}
}
 More