Javascript dependency order and ASP.NET MVC Bundles

Inevitably you find yourself working on contract on a “simple” project that has become increasingly complex, growing from a few javascript files to hundreds.

***side note***

This post is targeted at solely .NET environments. Yes there is AMD with requirejs and commonjs and the various module loaders and build tools, but sometimes not every tool is available and find yourself working within a stubborn locked down environment operated by the BOFH, limiting whats available for the job.

tl;dr;

The problem: “I am using System.Web.Optimization but I want my javascript in my bundle ordered by dependency!!!”

The solution: Custom IBundleOrderer implementation with regular expressions and a topological sort.

Continuing on

With ASP.NET MVC 4 (and 3)  you should be taking advantage of System.Web.Optimization and bundle and minify those scripts to keep your page times snappy.

When creating your bundle you can include files individually or by folder using a wildcard.

Adding files individually allows you to control the order that files appear in the bundle, but for your massive project who wants to maintain that through refactors and new features.

Adding via wildcard is so much simpler but the files are added alphabetically which hurts when abc.js refers to components in xyz.js.

So to control the order of the wildcard bundle you could create a custom IBundleOrderer implementation and specify which files should appear first, which can lead you back towards the maintenance problem mentioned above.

JavascriptDependencyOrderer: A smarter IBundleOrderer

Our implementation will apply a topological sorting algorithm to the list of files supplied by the bundle, …err… sorting the files according to their dependencies.

To use the topsort however we need to create a adjacency graph describing the files and their relationships.

Each vertex in the graph is represented by the individual file names provided to the bundle.

Each edge is represented by the file name of each file’s dependency.

But how do we discover the dependencies??

First we need to add a little bit of ceremony to our javascript files and list the required dependencies.

Here we borrow some similar syntax used by client side module loaders using a “require” statement which is wrapped in some comments as to not give us any client script errors.

So an example ModelA.js might be….

/* require('ModelB.js') */

(function (mynamespace) {
 var mynamespace.ModelA = mynamespace.ModelB.extend({

 });
} (window.myuni = window.myuni || {} ));

I like the require statement because if in the future I ever could use a more appropriate tool, part of the work is kinda already done. But you could use any pattern you desired.

The commented section is also removed by the bundle when it is minified.

Listing the dependency here makes the it more explicit on what components this bit of code is using and frees us from having to manage not only this file’s dependencies but also how their order may impact the dependency order on other files.

Next, a regular expression is then used as each file is processed to grab the dependencies from the require statement and build up the graph edges.

What about 3rd party libraries and their dependencies?

A good example is Backbone.js having a dependency on underscore.js

Not wanting to modify the source of these external libraries with our extra “require” ceremony, we add some extra logic to our IBundleOrderer implementation to explicitly add these files first and in the order they are listed.

These explicit items are then excluded from the topological sort as they are assumed to be already added.

Building the topological sort

I chose to use the QuickGraph nuget package but you can also roll your own for finer control over performance, error handling, and debugging.

By using the QuickGraph package some extra guard clauses and exception handling was needed to better identify potential sources of error.

Putting it together


public class JavascriptDependencyOrderer : IBundleOrderer
{
   private readonly string dependencyRegex;
   private readonly List<Field> fields;

   public List<string> ExplicitOrder { get; set; }

   public JavascriptDependencyOrderer()
     : this(@"(?<=require\(['\""]).*?(?=['\""]\))")
   {}

   public JavascriptDependencyOrderer(string dependencyRegex)
   {
      this.dependencyRegex = dependencyRegex;
      fields = new List<Field>();
      ExplicitOrder = new List<string>();
   }

   #region IBundleOrderer Members
   public IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files)
   {
     FindDependecies(files);
     List<FileInfo> result = ExplicitOrder
                              .Select(fileName => files.FirstOrDefault(x => x.Name == fileName))
                              .Where(f => f != null).ToList();
     result.AddRange(BuildAndSortDependencies()
                        .Select(sortedVertex => files.FirstOrDefault(x => x.Name == sortedVertex)));

     return result;
   }
   #endregion

    private void FindDependecies(IEnumerable<FileInfo> files)
    {
       var regex = new Regex(dependencyRegex);
       foreach (FileInfo fileInfo in files)
       {
          if (!ExplicitOrder.Contains(fileInfo.Name))
          {
             var file = new StreamReader(fileInfo.OpenRead());
             MatchCollection matches = regex.Matches(file.ReadToEnd());

             fields.Add(new Field
             {
                Name = fileInfo.Name,
                DependsOn = matches.OfType<Match>()
                           .Select(match =>
                            {
                                if (files.Any(x => x.Name == match.Groups[0].Value))
                                {
                                   return match.Groups[0].Value;
                                }
                                else
                                {
                                     throw new Exception(
                                          string.Format("Dependency {0} for {1} could not be found in supplied list of files",
                                          match.Groups[0].Value,
                                          fileInfo.Name));
                                }
                            }).ToArray()
             });
           }
       }
    }

   private IEnumerable<string> BuildAndSortDependencies()
   {
      try
      {
          var adjacencyGraph = new AdjacencyGraph<string, Edge<string>>();

          foreach (Field field in fields)
          {
              adjacencyGraph.AddVertex(field.Name);
              foreach (string dependecy in field.DependsOn)
              {
                 adjacencyGraph.AddEdge(new Edge<string>(field.Name, dependecy));
              }
           }

           var topSort = new TopologicalSortAlgorithm<string, Edge<string>>(adjacencyGraph);

           topSort.Compute();
           return topSort.SortedVertices.Reverse();
      }
      catch(NonAcyclicGraphException cyclicException)
      {
          throw new Exception("Circular reference detected while processing javascript dependency order",cyclicException);
      }
      catch(KeyNotFoundException keyNotFoundException)
      {
          throw new Exception("Dependency could not be found. Check that the file names match.", keyNotFoundException);
      }
    }

   #region Nested type: Field
    private class Field
    {
       public string Name { get; set; }
       public string[] DependsOn { get; set; }

       public override string ToString(){ return Name; }
     }
     #endregion
}

And now we use it

Finally we can get our global.asax / bundle config back under control and more maintainable using the wildcard search and our orderer

// Site Javascript Bundle
 Bundle bundle = new ScriptBundle("~/Scripts/site.js");
 bundle.Transforms.Add(new JsMinify());
 bundle.IncludeDirectory("~/scripts", "*.js", true);

 var jsOrder = new JavascriptDependencyOrderer();
 jsOrder.ExplicitOrder.Add("jquery-1.8.0.js");
 jsOrder.ExplicitOrder.Add("underscore.js");
 jsOrder.ExplicitOrder.Add("backbone.js");
 bundle.Orderer = jsOrder;

BundleTable.Bundles.Add(bundle);

Last words…

There are still a few improvements I would like to make, such as

  • allowing the dependencies to be case insensitive
  • having the vertex key be more unique by including the part of the path
  • or perhaps some type of shim for 3rd party files

Either way, for our project in our environment its working well and removed some heartache.

As always comments, criticisms, and corrections are eagerly welcomed.

An improved guide to the multicultural festival

The national multicultural festival is fantastic; It’s probably the best thing Canberra does.

tl;dr

  1. I <3 festival especially the food :)
  2. Played around with a new build / tech stack
  3. HTML5: Given the network congestion, perhaps native would be a better choice?

Anyway, some boring background…

I’m a big fan of the food spectacular event that they have where they bring together hundreds of stalls representing various cultures and community groups and everyone puts on a massive feast.

The official guide however, doesn’t really help out all that much.

I mainly wanted to know who would sell me something tasty to eat with a refreshing beverage to wash it down with and where among the action were they!

With the official guide there doesn’t seem to be much rhyme or reason to stall locations and its extremely difficult to figure out where a vendor may be.

So I put together http://cultfoodfest.net (probably a bit too late for others from this blog post perspective :) )   to help me out and further .

I used the information available from the official site to gather event  and stallholder details, and the general layout of the area.
I also built up some latitude, longitude estimates for the stall locations using google maps. I later hit the streets to confirm the co ordinates as best i could as the tents were going up and on the main day itself.

Tech

jQuery 2.0b

I went with the beta of jQuery 2.0 to see if there would be any immediate gotchas for even this tiny app (especially working with 3rd party libs)

jQuery 2.0 is supposed to be faster and smaller than 1.9 and a jsPerf indicates that it is indeed mostly faster. Minified however there is only a few kb difference. I was hoping for a larger reduction, but it is a beta with more work to come.

One of the best things to come out of 1.9 & 2.0 imo is actually the jQuery-migrate plugin. The plugin restores deprecated features and behaviors so that older code will still run properly on jQuery 1.9 (no jQuery 2.0 tho it seems from my testing).

jQuery-migrate would log to console any usage of deprecated features just served as a  healthy reminder to clean up my own coding practices.

jQuery Mobile 1.3rc1

I really wanted to test our 1.3 for use of their new panels widget, but i just didnt get time to get the functionality that I was thinking of in before the festival.

1.3rc1 also isn’t compatible with jQuery 2.0 since it had some uses of deprecated functionality. From the 1.3 source attrFn seemed only used for old IE so for my app, I just removed it. There were also a couple of usages of attr, but they were simple enough to change to use .prop instead. (Mental note, submit patch)

I also found I didnt get the greatest, responsive performance compared to other versions I had used elsewhere. It wasnt bad, it just could have been better imo.

It could have been a few things or combinations there of.

  • Perhaps it was because its 1.3rc1 and not final
  • maybe my above “hacks”
  • maybe the integration with backbone and “play nice” with routing that is needed to be done
  • In the other projects I messed with the click delay
  • maybe the default theme threw me off.

Eitherway, using the chrome dev tools didnt reveal any delays from the stuff that I had written and seemed to be buried among calls in jquery and JQM.

Backbonejs

I just love the simple, understandable structure that it provides. Given the deployed functionality I probably could gotten away without it + the added dependency/download on underscore/lodash given I already had jQuery as well for a mobile site.

But whats 10kb between friends? Besides from little things, big things grow and I had plans for much more.

yeoman.io

“Yeoman is a robust and opinionated client-side stack, comprising tools and frameworks that can help developers quickly build beautiful web applications”

The .NET space leaves a lot to be desired in the form of tooling targeting front end development. I’d be really interested to know what others (assuming anyone reads this :) ) have found works for them. From my experience its been a mix match of different tools and Visual studio plugins which, while helpful, has caused me pain for my CI.

I found yeoman removed a lot of that friction.

The “watch” auto builds, scaffolding, linting, minifying, testing support, image optimizations and more… a nice all in one workflow built on established tools and practices.

I’d definitely recommend checking it out.

Final thoughts

For real world usage, I ran into a problem I hadn’t immediately thought about. Network congestion.

With tens of thousands of people in such a small area it was difficult to even get a connection on the phone. Calling friends was impossible let alone loading google maps.

The site loaded and cached fine making it somewhat usable offline, even the REST calls to update and refresh data were ok. But it was that “where is the german beer tent” type of functionality that relied on google maps that failed. The HTML5 geo location didnt really help either. Sometimes it was spot on, but mostly it but me a block away from where i was.

Maybe native would have been best for this type of application. It probably would have alleviated most of the network congestion issues using the internal map controls. The GPS seemed to be more consistent as well.
Things to do for next year I guess. Phonegap or complete native and getting it in the app store would have been impossible given my 24 hour dev cycle this year anyway.

Google Identity toolkit & ASP.NET MVC3

*UPDATE* I have applied a fix for legacy accounts in Step 9, find out more there.

I’ve been wanting to learn how to incorporate OpenId into my ASP.NET applications.

I looked at the excellent DotNetOpenAuth library, but I was curious on what others were doing.

In my travels I came across the Google Identity Toolkit

What is the Google Identity Toolkit (GITKit)?

A quick excerpt from the GITKit site…
Google Identity Toolkit (GITkit) is a free toolkit for website operators who currently allow users to login with their email address and password, and would like to replace that password with federated login.

After working through the samples and watching some related youtube videos, I thought it was pretty cool so I signed up for the API access and started to go through the documentation and samples.

At the time, I found the documentation a bit hit and miss. It felt like sections of the documentation was missing and didn’t exactly align with the sample code (which is only available in PHP or java).

Thankfully the documentation has been receiving updates and it is a bit better.

Currently, the GITKit supports federated logons that are managed by the following providers

  • Google
  • Yahoo
  • AOL
  • Windows Live

The first three work out of the box, but Live requires a bit of extra work.
You need to register your application and domain with Live and then add your Live developer key into your Google API Console. GITKit has doco on what needs to be done here.

Also to make things more enjoyable, Live does not like localhost (or atleast not mine). The quick solution there is to provide a domain name to Live and then modify your hosts file to point that domain to your dev box.

Anyway, moving forwards……

How to implement in ASP.NET MVC3

Anyway, I thought I would put together a bit of a walkthrough of how to integrate the GITKit into a new ASP.NET MVC3 Project.

So create a new ASP.NET MVC3 Solution and lets get modifying.

Step 1 – Add the widget scripts to your Master/Layout page

Add these script blocks to your page. The GITKit doco says to add to the HEAD block, but I am not entirely sure if that is an absolute requirement or if the scripts can be moved to the bottom of your page.

<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/googleapis/0.0.4/googleapis.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/jsapi"></script>

<script type="text/javascript">
    google.load("identitytoolkit", "1.0", { packages: ["ac"] });
</script>
<script type="text/javascript">
  $(function () {
     window.google.identitytoolkit.setConfig({
       developerKey: "your dev api key goes here",
       companyName: "Your company",
       callbackUrl: "@string.Format("https://yoursite.com{0}",Url.Action("Callback","Account"))", 
       userStatusUrl: "@Url.Action("UserStatus","Account")", // these can just be partial paths
       loginUrl: "@Url.Action("LogOn","Account")",
       signupUrl: "@Url.Action("Register","Account")",
       homeUrl: "@Url.Action("Index","Home")",
       logoutUrl: "@Url.Action("LogOff","Account")",
       realm: "", // optional
       language: "en",
       idps: ["Gmail", "AOL", "Hotmail", "Yahoo"],
       tryFederatedFirst: true,
       useCachedUserStatus: false
      });
      
      $('#navbar').accountChooser();
  });
</script>
  • Any version >= jquery 1.4.2 can be used
  • Any version >= jquery-ui 1.8.2 can be used
  • The callback url MUST be a full url. Any of the others can be partial paths
Step 2 – change _LogOnPartial

Replace the existing markup with

@if(Request.IsAuthenticated) {
    <text>Welcome</text>
}
<div id="navbar"></div>

You can have a quick test now if you like and you should see the sign in widget in the top right corner.
Clicking the widget should popup the account chooser screen.

On this screen you can click one of the supported IDP logos or provide an email address. If the email address does not belong to one of the supported providers, the GITKit will redirect the user to the logon url that you specified in the configuration.

Step 3 – The Callback Action Method

In the callback action you should validate ID providers response by calling the GITKit verifyAssertion API.

Once you have the have assertion, you can check to see if the user should be logged in or needs to be registered.

Here is my example callback action

public virtual ActionResult Callback()
{
            GitApiClient gitClient = new GitApiClient("your-developer-api-key-goes-here");
            GitAssertion assertion = gitClient.Verify();
            string BaseSiteUrl = Request.Url.Scheme + "://" + Request.Url.Authority.TrimEnd('/');

            ViewBag.GitRedirectUrl = BaseSiteUrl + Url.Action(MVC.Home.Index());
            ViewBag.FederatedResponse = GitApiClient.FederatedError;

            if (!string.IsNullOrEmpty(assertion.VerifiedEmail))
            {
                var user = Membership.GetUser(assertion.VerifiedEmail);
                Session["GitAssertion"] = assertion;

                if (user == null)
                {
                    //create the new user
                   var newUser = Membership.CreateUser(assertion.VerifiedEmail, Guid.NewGuid().ToString());
                    
                   FormsAuthentication.SetAuthCookie(newUser.UserName, true);

                    //if you wanted to collect more details before creating the user account,
                    // then specify the location of that page.
                   // ViewBag.GitRedirectUrl = BaseSiteUrl + Url.Action(MVC.Account.FederatedRegister());

                }
                else
                {
                    //you can decide how you want to manage the "remember me" boolean
                    FormsAuthentication.SetAuthCookie(user.UserName, true);

                }

                ViewBag.GitRedirectUrl = BaseSiteUrl + Url.Action(MVC.Home.Index());

                ViewBag.FederatedResponse = GitApiClient.FederatedSuccess;
            }

            return View();
}

Some things you might have noticed.

  • I am using the ASP.NET Membership provider for the account management
  • I also use the T4 MVC templates from http://mvccontrib.codeplex.com or nuget.
  • And I created a class to encapsulate the call to the GITKit (which I will get to next).

Something extra to note with the Membership provider account creation. In this example I am only dealing with federated logons which have no use for a password. The Membership provider requires a password for account creation, so I chose to set it to a random guid.

Step 4 – GitApiClient & GitAssertion

The GITKit may make either a GET or a POST request to our callback action depending on the user action. Its the GitApiClient’s job to wrap up the HttpContext and WebRequest call to the verification API, forwarding the data contained within the callback request.

The GITKit is expecting JSON objects, so I used the awesomo JSON.NET to handle the JSON de/serialization tasks.

The GitAssertion class is just a POCO to strongly type the response back from the verification API.

Here are the two classes that you should add to your solution.

public class GitApiClient
    {
        public static string FederatedSuccess = "federatedSuccess";
        public static string FederatedError = "federatedError";

        private readonly string _verifyUrl = "https://www.googleapis.com/identitytoolkit/v1/relyingparty/verifyAssertion?key=";
        private string _apiKey;

        public GitApiClient(string apiKey)
        {
            _apiKey = apiKey;
        }

        private string GitVerifyPost()
        {
            string result = "";
            try
            {
                Uri address = new Uri(_verifyUrl + _apiKey);
                
                HttpRequest request = HttpContext.Current.Request;
                
                HttpWebRequest gitWebRequest = WebRequest.Create(address) as HttpWebRequest;
                gitWebRequest.Method = "POST";
                gitWebRequest.ContentType = "application/json";

                StreamReader requestReader = new StreamReader(request.InputStream);

                var requestBody = requestReader.ReadToEnd();
                
                string myRequestUri = string.Format("{0}://{1}{2}",request.Url.Scheme,request.Url.Authority.TrimEnd('/'), request.RawUrl);

                var verifyRequestData = new { requestUri = myRequestUri, postBody = requestBody };

                byte[] gitRequestData = UTF8Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(verifyRequestData));

                using (Stream stream = gitWebRequest.GetRequestStream())
                {
                    stream.Write(gitRequestData, 0, gitRequestData.Length);
                }

                using (HttpWebResponse response = gitWebRequest.GetResponse() as HttpWebResponse)
                {
                    // Get the response stream  
                    StreamReader responseReader = new StreamReader(response.GetResponseStream());
                    result = responseReader.ReadToEnd();
                }
            }
            catch (WebException web)
            {
                throw new Exception("An error occurred while verifying the IDP response", web);
            }

            return result;

        }

        public GitAssertion Verify()
        {
            var result = GitVerifyPost();

            return JsonConvert.DeserializeObject<GitAssertion>(result);
        }

    }
public class GitAssertion
    {
        public string Kind { get; set; }
        public string Identifier { get; set; }
        public string Authority { get; set; }
        public string VerifiedEmail { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string FullName { get; set; }
        public string NickName { get; set; }
        public string Language { get; set; }
        public string TimeZone { get; set; }
        public string ProfilePicture { get; set; }
    }
Step 5 – Returning the callback response to the GITKit

Once GITKit has made the call to your callback action, you need to inform it of the outcome. Does the user already exist in your system? Do they need to be registered?

To do this, you need to respond back to GITKit by rendering some HTML that calls some of GITKit JavaScript functions.

So create a view under the account folder called “Callback” containing the following html.

<html>
<head>
<script type='text/javascript'>
    function notify() {
       window.opener.google.identitytoolkit.easyrp.util.notifyWidget('@ViewBag.FederatedResponse');
       window.opener.location.href = '@ViewBag.GitRedirectUrl';
       window.close();
    }
</script>
</head>
<body onload='notify();'>
</body>
</html>
Step 6 – Provide the user information to the GITKit widget

Now that the user is authenticated we need to inform the widget of the user details and tell it to change modes and display the user information.

In the _Layout.cshtml add the following piece of code to our script block after the $(‘#navbar’).accountChooser(); call.

@if (Request.IsAuthenticated)
        {
            var user = Session["GitAssertion"] as GitAssertion;
            if(user != null)
            {
            <text>
                var userData = {
                    email: '@user.VerifiedEmail', // required
                    displayName: '@user.FirstName', // optional
                    photoUrl: 'https://account-chooser.appspot.com/image/nophoto.png', // optional
                };
                
                window.google.identitytoolkit.updateSavedAccount(userData);
                window.google.identitytoolkit.showSavedAccount(userData.email);
            </text>
            }
        }

If you now test your solution and attempt to sign in, your IDP should prompt you for your details, the GITKit will then verify, your callback will fire, the popup window will close and your GITKit widget should be updated with the display name and user image.

That is unless you and me both have missed a key section or made a typo somewhere.

Step 7 – Fix the widget menu

At this point you should (hopefully) have everything working with the user logging on and the account chooser widget updating.

Clicking on the updated widget displays a menu with two default menu items, switch account and sign out. In case your wondering, yes, these can be changed and added, but that is a completely different exercise.

You may have noticed that your page content overlaps this menu.

To fix this, add the following CSS hack (if anyone has a cleaner solution please let me know)

ol.widget-navbar-menu { position:absolute;}

li.widget-navbar-menuitem {display:list-item; position:relative;z-index: 9999;}

Now you should be able to see the full menu.

Selecting “Switch account” will bring up the widget with all the user details that are currently stored in
localstorage.

Choosing a different account will fire a AJAX request to the specified UserStatus url in the widget configuration.

SignOut is self explanatory.

Step 8 – Implement the UserStatus Action Method – optional – sorta…

By default when an account is chosen from the GITKit Account Chooser, it will attempt to authenticate the user as a federated logon.
If the chosen account does not support federated logon or you configured the GITKit to not try federated first; when a different account is chosen, a call will be made to your UserStatus Action method.

Its here where you should sign out the currently signed in user, verify new chosen account details and inform the GITKit of the result.

The GITKit expects a JSON result informing it if the user is registered, is a legacy account or not.

In your Account Controller add this UserStatus Action Method. It is just a *very* lightweight example and you can start to see the limitations of just using the Membership provider.
A proper implementation really needs to be storing these user details (display name, photo etc) somewhere.

Anyway….

public virtual JsonResult userStatus(string email)
{
    string userName = Membership.GetUserNameByEmail(email);

    //if the user was switching accounts we need make sure we log out the previous user.
    Session.Abandon();
    FormsAuthentication.SignOut();

    if (userName != null)
    {
         var authUser = Membership.GetUser(userName);
         var user = new { displayName = authUser.UserName, photoUrl = "", registered = true, legacy = false };
          return Json(user);
     }
     else
     {
          var user = new { displayName = "", photoUrl = "", registered = false, legacy = false };
          return Json(user);
      }

}
Step 9 – Handle those non federated users

Since this example is updating the GITKit user details from session, we need to make sure that session is appropriately populated when the user is authenticated.

In the default MVC3 project template scenario this means we need to slightly modify our Register POST Action to something like this

 [HttpPost]
public virtual ActionResult Register(RegisterModel model)
{
    if (ModelState.IsValid)
    {
         // Attempt to register the user
         MembershipCreateStatus createStatus;
         Membership.CreateUser(model.UserName, model.Password, model.Email, null, null, true, null, out createStatus);

         if (createStatus == MembershipCreateStatus.Success)
         {
              Session["GitAssertion"] = new GitAssertion() { VerifiedEmail = model.Email, FirstName = model.UserName };
              FormsAuthentication.SetAuthCookie(model.UserName, false /* createPersistentCookie */);
              return RedirectToAction("Index", "Home");
          }
          else
          {
                ModelState.AddModelError("", ErrorCodeToString(createStatus));
           }
     }

     // If we got this far, something failed, redisplay form
     return View(model);
}

Now when a non federated account attempts to logon, the GITKit will send to your logon Url the users email and the provided password.
This is done via a AJAX request and we need to return a JSON response back to the GITKit with the logon result.

To use the existing LogOn Post action method we need to make some more subtle changes.

GITKit only gives us an email address and password, so we need to add the email property to our LogOnModel

  public class LogOnModel
    {
        
        [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; }

        public string Email { get; set; }
    }

Now we need to update our LogOn action method. For simplicity I’ve chosen to keep the existing UserName logic.
To account for this though, if the username has not been provided I query the Membership provider with the supplied email address.
*UPDATE* To avoid the model state error, I removed the [Required] attribute from the UserName property on the LogOnModel. I feel this is fine for the example, but a real world solution would need appropriate validation rules as your situation called for it.
Also update the username condition in the code below.

Once the user has been authenticated, I again set the GitAssertion into session and if this was a ajax request, I return the expected JSON object back to the GITKit.

Here is the LogOn action method.


[HttpPost]
public virtual ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (model.UserName == string.Empty && model.Email == string.Empty)
    {
         ModelState.AddModelError("username", "username or email is required");
     }
     else if (string.IsNullOrEmpty(model.UserName))
     {
          model.UserName = Membership.GetUserNameByEmail(model.Email);
     }

     if (ModelState.IsValid)
     {
           if (Membership.ValidateUser(model.UserName, model.Password))
           {
               var user = Membership.GetUser(model.UserName);
               Session["GitAssertion"] = new GitAssertion() { VerifiedEmail = user.Email,
                                                              FirstName = model.UserName };
               FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);

               if (Request.IsAjaxRequest())
               {
                   return Json(new { status = "ok", displayName = user.UserName, photoUrl = "" });
               }
                    
               if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
                   && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
               {
                    return Redirect(returnUrl);
                }
                else
                {
                    return RedirectToAction("Index", "Home");
                }
            }
            else
            {
                 ModelState.AddModelError("", "The user name or password provided is incorrect.");
            }
       }
 
       // If we got this far, something failed, redisplay form
       return View(model);
}

You should now be able to compile and test federated and legacy accounts.
Supplying “test@test.com” to the account chooser widget should redirect you to the default Register page.
Once the account is created the “Test” user should be logged onto the system.

If you switch accounts between say a federated logon and back to the “Test” account, you should be prompted to supply the password for the test account and then be logged back in.

Final words

Well, that should be basically it. I quite like the Account chooser direction that GITKit offers, hopefully it gets promoted out of the labs.

The GITKit is being updated and does have a few other functions and JavaScript hooking points that you can utilize if you want to dig through its source.

Comments, criticisms, and corrections are eagerly welcomed. The example code listed here is exactly that, an example. Ideally magic strings would be removed, DRY principle applied, etc etc.

Also this is a “Hello World” blog post for me. I am not entirely sure what I will come of this experience, maybe I will stick with it, maybe it will just wither and die. Time will tell.

Anyway, please forgive my lack of blogging skills. I hope someone found this helpful haha.

Follow

Get every new post delivered to your Inbox.