Click here to Skip to main content
Click here to Skip to main content

CodeStash - A useful (hopefully) tool for devs

, , 15 May 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
A distributed code-snippet storage tool.

CodeStash Article Listings

Table of Contents

Introduction

Those of you that read my articles will have seen me bleating on about an Open Source project I have been working on for a while with fellow CodeProject member and all round hero Peter O'Hanlon who was mad enough to accept a proposal from me to work on this project I sent out a spec for some time ago, silly Pete, very silly.

Anyway, some time goes by (a lot, or so it feels), and well we have finally managed to wrestle the beast into submission, and are ready to see what people think of what we have been working on. We are calling it "CodeStash".

So what is this project all about?

Well, CodeStash is a productivity tool for a single developer or team of developers (where they could be part of the same team).

The tool itself can be thought of as a web based centralized snippet repository for the single developer (or team of developers), where the developer(s) may manage useful code snippets that they wish to use to carry out their day to day tasks.

The website will provide the following functionality:

  • OpenId authorization which will work in conjunction with standard ASP.NET Forms Authentication.
  • Tag cloud for most common snippet types, to allow quick search lookup for these types of snippets.
  • The ability to search for existing code snippets (ones that have been stored), by keyword tag, group, language, code content.
  • The ability to create/delete/edit existing code snippets.
  • The ability to group snippets into certain groups such that when searching for a single code snippet all related snippets will be shown. For example, if I search for INPC, I would get a C# snippet for declaring an INPC based model/ViewModel but would possibly get a XAML snippet that would also come back as part of the search.

The website is largely concerned with CRUD (Create/Retrieve/Update/Delete) of code snippets, which in itself is not that tricky, or even revolutionary (although none of the existing solutions out there deal with the concept of grouping at all).

There is however another angle to this project, in that it is intended to have seamless integration with Visual Studio (2010 and above), in that it will come with a managed VS add-in that will integrate with VS by way of certain content menus and property pages, that will allow the VS user to upload code to CodeStash, and also allow the VS user to search CodeStash by the use of a set of standard ASP.NET MVC controller actions that will expose/accept RESTful data to the Bespoke UI that will be launched from inside of the VS.

When a user searches for a code snippet that has previously been stored within CodeStash, they will have the option to insert the matched CodeStash snippet into the VS editing pane (providing the CodeStash snippet type matches the current VS editor file type).

Existing Solutions

Like I say, the website itself is not such a novel thing, there are a number of existing solutions out there that do a similar job to the website aspect of CodeStash. After all it really just comes down to CRUD operations, but what great idea doesn't really boil down to that in the end? Even Facebook is just CRUD with some marketing veneer.

Thing was once you looked at all these existing solutions, they all seemed to have certain areas where the functionality was lacking, for example, some of the existing solutions suffered in these areas:

  1. Searching was very limited, if offered at all, which made it very hard to find a snippet once you uploaded it.
  2. No concept of grouping, which is pretty bad I feel. These days code is made up of many elements, you may have an HTML file, a CSS file, a JavaScript file, all of which could form a logic snippet. There may of course be a single file, but where there are multiple related files, the ability to bring these back within the same search is extremely important and aids productivity.
  3. No Visual Studio integration.

For completeness, here is a list of some of the existing solutions out there that we examined before embarking on the mission to create CodeStash.

That is obviously not an exhaustive list, but they were the best of the bunch we found while doing our research into what was out there. You know there was no point inventing a wheel if someone had already come out with a Ferrari.

So in a nutshell, that's what CodeStash is all about. Now if any of you have bothered to click the link, you will notice that it just takes you to a CodePlex website, with not much there apart from the source code. That is down to hosting, which we need to talk about next.

Plea for Help / Canadian Mounties to the Rescue

One of the biggest issues we have had while developing CodeStash is how we should deploy it, and get it to end users for use. We wanted to keep it free but if it does end up being used by a lot of people (which would be truly great), this will obviously cost Pete and I a lot of money in hosting costs. As such we could not really see an avenue open to us in terms of providing hosting for CodeStash. There are of course full notes on how to get CodeStash up and running in your own IIS installation/intranet within this article, but when we first started writing this article, we honestly could not think of a way of hosting CodeStash, due to the possible monetary constraints that may or may not occur. 

The thought had crossed our mind to include a section (this section actually) within the article where we would kind of grovel for someone that may be willing to offer free hosting that may work for an ISP, for something that is obviously a community driven tool.

However since then, we put our thinking caps on, and I (Sacha this is) thought about this a bit more, and thought I wonder if our good buddies at CodeProject would be up for giving us a bit of space.....I'll just ping them an email.

The response from Chris Maunder (CodeProject Co-Founder) was more than cool, Chris basically stated that not only would they be up for hosting this little old project of ours, but CodeProject would "love" to host it. It turns out that our project idea was one that they had had themselves but have never got around to implementing, so yeah basically Chris was into it.

I was stunned by this response truth be told, I was just going out on a limb when I asked them and honestly expected them to say, "nah you're OK, I think we'll pass on this one man"....But the opposite happened, we were met with so much positivity it's not even funny, the only thing I can liken it to, is Peter Jackson trying to raise money to create the first film of the Lord of the Rings trilogy of films, where he had only created a few props, and was seeking budget for the first film only, and had visited pretty much all the big studios only to be told no!!!! Until he visited "NewLine" cinemas who simply went, hey aren't there three of these films, you should make all three, now here is the money, go get busy.

That's how Chris Maunder and CodeProject's response made us feel, Pete and I were and still are very grateful for this fantastic offer.

So CodeProject, we humbly thank you for allowing this to happen.

Note: One thing we should mention is that by hosting this on CodeProject, certain tweaks will be done, which will no longer be discussed in these articles, but they will be for the greater good. You can expect to see the CodeProject hosted version get revamped (well, different from the screenshots/content presented here at least) for things like:

  1. We would make our branding more in line with CodeProject colors, come on bring the green/orange.
  2. We may move to a CodeProject credential based login and ditch the ability to register and also use OpenId. Would you like it to stick to using the OpenId/Register approach or would you rather see it move to use your existing CodeProject credentials? You would still need to login though, as CodeStash would be a separate entity from CodeProject. Could you let us know what your thoughts are on this?
  3. The current CodePlex version would have to be tombstoned such that the version prior to us branching off to create the CodeProject revamped version would be the last one that would allow you to self host (which I am guessing not many people are going to want to do anyway).

These changes will obviously come some way down the line, we need to know what we have built is of use to people first. No point flogging a dead horse and all that. So please let us know what you think, would you use this service if it is hosted and just available for use?

Special Thanks

Before we get into the nitty gritty of the whys and the hows of CodeStash, I would just like the opportunity to thank two people, without whose help this project would not have been possible, so without further ado. massive thanks go out to:

Pete O'Hanlon

When I first came up with the idea for CodeStash, I sent out a 60 page spec and a link to the proof of concept version of the website I mentioned above, and asked various people whether they thought it was an idea worth pursuing, and whether they would be willing to help me out. Pete O'Hanlon was the only one that was foolish enough to volunteer for this task. Pete has been working on the add-in while I have been working on the website portion. Pete has been an absolute dream to work for er.. with (ROFL). Pete and I have put quite a lot of our spare time into this, both of us have young kids demanding our time, and Pete has never once said that he could only really work on it up to point x, or say well it's nearly there you can take it from here. He has always maintained that he is in it until the bitter end, and even has plans for V2. Pete, you have been an absolute legend to work with, and this project could not have happened without you. Honestly mate, I could not have done it without you, you totally rock.

Ryan Worsley

Ryan Worsley is my old Uni buddy and current work colleague who has pretty much been a rock throughout the entire lifecycle of CodeStash (and I have written the web portion twice, the first one was an ASP MVC learning vehicle). So Ryan has had to deal with my dumbass queries (yes, MVPs can be dumb, we truly can) and answered all my inane and sometimes stupid web related questions for quite some time. So Ryan, thanks a lot man, you have been a star.

Gents I salute you both, heart felt thanks. Oh and by the way, Ryan's stipulation for helping with this has always been, that I put up a funny image of a cat. So without further ado, one funny cat:

The Mile High Overview

This diagram may help to illustrate the different parts of the proposed CodeStash system:

Screenshots

Probably the easiest way to show people what we have developed is to show you some screenshots of the various parts of the system. I don't want to steal Pete's thunder too much concerning the add-in, but I thought it was a good idea to at least show you some screenshots of it in action. Pete will be writing a whole article on the add-in, so the inner workings of that will be covered in that article and are not really in scope for this article.

For now, let's just enjoy some screenshots.

Note: with all the screenshots below, you can click on the images to see a larger version.

Login

This is the main login page, where you can choose to use a previously registered email/password or use OpenId authentication:

Register

This is the page you would use to register a new user which would use username/password authentication.

Master Page

If you are not currently logged in, you will see a master page like this:

Once you are logged in, you will see a master page like this:

Profile Settings

This page allows each logged in user to have the ability to alter their profile specific data:

Team Settings

This page allows logged in users to create teams. The basic idea is that the first user to create a team will be the team owner, who can then alter the team by adding/removing team members.

Open From Web

This page allows you to enter a URL to an existing web based snippet and have it highlighted:

Add Snippet

This page allows logged in users to create a new code snippet:

Edit Snippet

This page allows logged in users to edit an existing code snippet (providing it's yours). It starts by finding a snippet you want to edit.

Then after you click the edit icon, you will be redirected to the edit snippet page which is shown below:

Delete Snippet

You may delete a snippet (providing it's yours), which starts by finding a snippet you want to delete. Once you find a snippet, you may use the delete icon which will show you a popup asking you to confirm your delete. Now remember that code snippets can be grouped (you know C#/ASPX/HTML all being in a logical group), as such the delete dialog may ask you to delete all snippets in a group, if the current snippet is in a group. Or if there is no group, you will see a standard confirm dialog.

This is what is shown if the snippet is in a group:

This is what is shown if the snippet is not in a group:

Search

This page allows logged in users to search for code snippets, where the user may tailor the search using tags/keyword/language and a visibility modifier:

And here is what you get once you have found some search results:

You can also choose to see more detail about the code snippet, by using the popup columns "View Details" link, which shows a popup of the form shown below:

Display Snippets

This page allows logged in users to see a list of their current matched search criteria. You would typically see this page either by clicking on an item in the tag cloud, or clicking on one of the shown search results:

ReadOnly Display Snippets

You are able to share a link with a non-registered CodeStash user for viewing, without them needing to register, which will load in a cut-down master page, which is readonly. This is shown below:

The link can be found using one of the existing snippets, which shows a popup:

And assuming the user copies the URL and browses to that location, they would see something like this. See how there are none of the usual links available as there is with the full site:

Add-in: Settings

This page is what you would get in Visual Studio to configure your specific settings to allow the Visual Studio add-in to connect to the CodeStash website:

Add-in: Context Menu

When you right click within an editor pane, you will be presented with a CodeStash context menu, from where you will have two menus:

Add-in: Save Menu

When you click the "Save" add-in CodeStash menu, it allows you to save a code snippet:

Add-in: Search Menu

When you click the "Search" add-in CodeStash menu, it allows you to search for code snippet(s) the same way you do using the main CodeStash website:

Add-in: Search Preview

When you click the "View Snippet" link from the obtained CodeStash search results shown in the grid, a preview of the clicked snippet is shown. An example of this can be seen below:

High Level Architecture

In this section we will talk about the high level components/helpers/techniques that the website uses.

General Structure

There are a number of considerations in determining how data should be sent between layers (database, Entity Framework, website) and different applications (Addin and Website). This diagram will hopefully illustrate how these layers are intended to work (click the image for a bigger version):

Website Philosophy

Wherever possible the website tries to use AJAX loading to offer a richer user experience. There are places where this does not make sense such as when the user has to fill in a bunch of stuff, and the only possible option is to show the page again with validation errors.

To facilitate these AJAX enabled features, it is quite common to see something like the following markup where an ASP MVC PartialView will be initially loaded but can expect to be reloaded via AJAX.

In order to ensure that the AJAX requests work correctly, a specialized ActionFilter has been developed that the Controller Actions can use. This is called AjaxOnlyFilter and the code is as follows:

using System.Web.Mvc;

namespace CodeStash.Filters
{
    public class AjaxOnlyAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (!filterContext.HttpContext.Request.IsAjaxRequest())
                filterContext.HttpContext.Response.Redirect("/Error/Error404");
        }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {

        }
    }
}

Where an example of using this might be as follows:

[AjaxOnly]
public ActionResult OpenFromWeb(OpenFromWebViewModel vm)
{

}

Databases

This section will discuss the two different databases that CodeStash uses, namely the:

  • ASP Membership database
  • "CodeStash" specific database

ASP Membership Database

This is just an instance of the standard ASP Membership database that you need to setup (which is discussed at ASP Membership DB Setup), it must be named "AspNetMembership" in order for the CodeStash website to work.

Here is a screenshot of what a typical ASP Membership database looks like:

ASP Membership Custom Profile Object

As you now know, CodeStash makes use of the standard ASP Membership database, which is all very standard stuff. It does however use a custom Profile object, which can be seen in the Web.Config file, as shown below:

<profile inherits="CodeStash.Models.Security.UserSettingsProfileModel">
  <providers>
    <clear />
    <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" 
          connectionStringName="ApplicationServices" applicationName="/" />
  </providers>
</profile>

Where the actual CodeStash.Models.Security.UserSettingsProfileModel object looks like this:

using System.Web.Profile;
using System.Web.Security;

namespace CodeStash.Models.Security
{
    public class UserSettingsProfileModel : ProfileBase
    {
        [SettingsAllowAnonymous(false)]
        public bool IsOpenIdLoggedInUser
        {
            get { return (bool)base["IsOpenIdLoggedInUser"]; }
            set { base["IsOpenIdLoggedInUser"] = value; }
        }

        [SettingsAllowAnonymous(false)]
        public int HighlightingCSSId
        {
            get { return (int)base["HighlightingCSSId"]; }
            set { base["HighlightingCSSId"] = value; }
        }

        [SettingsAllowAnonymous(false)]
        public int MaxSnippetsToDisplay
        {
            get { return (int)base["MaxSnippetsToDisplay"]; }
            set { base["MaxSnippetsToDisplay"] = value; }
        }

        public static UserSettingsProfileModel GetUserProfile(string username)
        {
            return Create(username) as UserSettingsProfileModel;
        }

        public static UserSettingsProfileModel GetUserProfile()
        {
            return Create(Membership.GetUser().UserName) as UserSettingsProfileModel;
        }
    }
}

CodeStash DB

The CodeStash database schema (at the time of writing this article) is pretty simple, and looks like this:

Security

CodeStash uses several security techniques such as:

  • Forms authentication / mixed with OpenId authentication is used for loginAll controller actions that do anything of value, are secured using the standard AuthoriseAttribute action filter (more on this in part II).
  • Anti forgery is considered and hopefully protected using the standard ASP MVC Html.AntiForgeryToken and the standard ValidateAntiForgeryTokenAttribute action filter.

We will be talking about most of this in part II, but for the Anti forgery stuff, it's pretty much just a question of doing this in your views form.

@Html.AntiForgeryToken("Add")

And this on your controller actions:

[ValidateAntiForgeryToken(Salt = "Edit")]
public ActionResult Edit(EditSnippetViewModel vm)

Encryption

The CodeStash website/add-in is capable of being run in two modes:

  1. Encryption is enabled: This means that all users will have their CodeStash login details encrypted.
  2. Encryption is disabled: This means that all users will have their CodeStash login details stored in plain text.

In either case, the following user specific details are effected:

  1. OpenId token (only applicable if the user logged in uses OpenId provider)
  2. Email address
  3. Registration password (only applicable if the user creates new registration and uses that to log in)

In order to deal with the encryption/decryption process, there are a number of areas that come into play, these are all discussed below.

Web.Config
<appSettings>
  <add key="EncryptionEnabled" value="false" />
</appSettings>

General Encryption Class

This class was obtained from http://www.obviex.com/samples/Code.aspx?Source=EncryptionCS&Title=Symmetric%20Key%20Encryption&Lang=C%23.

///////////////////////////////////////////////////////////////////////////////
// SAMPLE: Symmetric key encryption and decryption using Rijndael algorithm.
// 
// To run this sample, create a new Visual C# project using the Console
// Application template and replace the contents of the Class1.cs file with
// the code below.
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED 
// WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// 
// Copyright (C) 2002 Obviex(TM). All rights reserved.
//
// http://www.obviex.com/samples/Code.aspx?Source=EncryptionCS&Title=Symmetric%20Key%20Encryption&Lang=C%23
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;

namespace CodeStash.Common.Encryption
{
    /// <summary>
    /// This class uses a symmetric key algorithm (Rijndael/AES) to encrypt and 
    /// decrypt data. As long as encryption and decryption routines use the same
    /// parameters to generate the keys, the keys are guaranteed to be the same.
    /// The class uses static functions with duplicate code to make it easier to
    /// demonstrate encryption and decryption logic. In a real-life application, 
    /// this may not be the most efficient way of handling encryption, so - as
    /// soon as you feel comfortable with it - you may want to redesign this class.
    /// </summary>
    public class RijndaelSimple
    {
        /// <summary>
        /// Encrypts specified plaintext using Rijndael symmetric key algorithm
        /// and returns a base64-encoded result.
        /// </summary>
        /// <param name="plainText">
        /// Plaintext value to be encrypted.
        /// </param>
        /// <param name="passPhrase">
        /// Passphrase from which a pseudo-random password will be derived. The
        /// derived password will be used to generate the encryption key.
        /// Passphrase can be any string. In this example we assume that this
        /// passphrase is an ASCII string.
        /// </param>
        /// <param name="saltValue">
        /// Salt value used along with passphrase to generate password. Salt can
        /// be any string. In this example we assume that salt is an ASCII string.
        /// </param>
        /// <param name="hashAlgorithm">
        /// Hash algorithm used to generate password. Allowed values are: "MD5" and
        /// "SHA1". SHA1 hashes are a bit slower, but more secure than MD5 hashes.
        /// </param>
        /// <param name="passwordIterations">
        /// Number of iterations used to generate password. One or two iterations
        /// should be enough.
        /// </param>
        /// <param name="initVector">
        /// Initialization vector (or IV). This value is required to encrypt the
        /// first block of plaintext data. For RijndaelManaged class IV must be 
        /// exactly 16 ASCII characters long.
        /// </param>
        /// <param name="keySize">
        /// Size of encryption key in bits. Allowed values are: 128, 192, and 256. 
        /// Longer keys are more secure than shorter keys.
        /// </param>
        /// <returns>
        /// Encrypted value formatted as a base64-encoded string.
        /// </returns>
        public static string Encrypt(string plainText,
                                     string passPhrase,
                                     string saltValue,
                                     string hashAlgorithm,
                                     int passwordIterations,
                                     string initVector,
                                     int keySize)
        {
            // Convert strings into byte arrays.
            // Let us assume that strings only contain ASCII codes.
            // If strings include Unicode characters, use Unicode, UTF7, or UTF8 
            // encoding.
            byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
            byte[] saltValueBytes = Encoding.ASCII.GetBytes(saltValue);

            // Convert our plaintext into a byte array.
            // Let us assume that plaintext contains UTF8-encoded characters.
            byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);

            // First, we must create a password, from which the key will be derived.
            // This password will be generated from the specified passphrase and 
            // salt value. The password will be created using the specified hash 
            // algorithm. Password creation can be done in several iterations.
            PasswordDeriveBytes password = new PasswordDeriveBytes(
                passPhrase,saltValueBytes,hashAlgorithm,passwordIterations);

            // Use the password to generate pseudo-random bytes for the encryption
            // key. Specify the size of the key in bytes (instead of bits).
            byte[] keyBytes = password.GetBytes(keySize / 8);

            // Create uninitialized Rijndael encryption object.
            RijndaelManaged symmetricKey = new RijndaelManaged();

            // It is reasonable to set encryption mode to Cipher Block Chaining
            // (CBC). Use default options for other symmetric key parameters.
            symmetricKey.Mode = CipherMode.CBC;

            // Generate encryptor from the existing key bytes and initialization 
            // vector. Key size will be defined based on the number of the key 
            // bytes.
            ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes,initVectorBytes);

            // Define memory stream which will be used to hold encrypted data.
            MemoryStream memoryStream = new MemoryStream();

            // Define cryptographic stream (always use Write mode for encryption).
            CryptoStream cryptoStream = new CryptoStream(memoryStream,encryptor,CryptoStreamMode.Write);
            // Start encrypting.
            cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);

            // Finish encrypting.
            cryptoStream.FlushFinalBlock();

            // Convert our encrypted data from a memory stream into a byte array.
            byte[] cipherTextBytes = memoryStream.ToArray();

            // Close both streams.
            memoryStream.Close();
            cryptoStream.Close();

            // Convert encrypted data into a base64-encoded string.
            string cipherText = Convert.ToBase64String(cipherTextBytes);

            // Return encrypted string.
            return cipherText;
        }

        /// <summary>
        /// Decrypts specified ciphertext using Rijndael symmetric key algorithm.
        /// </summary>
        /// <param name="cipherText">
        /// Base64-formatted ciphertext value.
        /// </param>
        /// <param name="passPhrase">
        /// Passphrase from which a pseudo-random password will be derived. The
        /// derived password will be used to generate the encryption key.
        /// Passphrase can be any string. In this example we assume that this
        /// passphrase is an ASCII string.
        /// </param>
        /// <param name="saltValue">
        /// Salt value used along with passphrase to generate password. Salt can
        /// be any string. In this example we assume that salt is an ASCII string.
        /// </param>
        /// <param name="hashAlgorithm">
        /// Hash algorithm used to generate password. Allowed values are: "MD5" and
        /// "SHA1". SHA1 hashes are a bit slower, but more secure than MD5 hashes.
        /// </param>
        /// <param name="passwordIterations">
        /// Number of iterations used to generate password. One or two iterations
        /// should be enough.
        /// </param>
        /// <param name="initVector">
        /// Initialization vector (or IV). This value is required to encrypt the
        /// first block of plaintext data. For RijndaelManaged class IV must be
        /// exactly 16 ASCII characters long.
        /// </param>
        /// <param name="keySize">
        /// Size of encryption key in bits. Allowed values are: 128, 192, and 256.
        /// Longer keys are more secure than shorter keys.
        /// </param>
        /// <returns>
        /// Decrypted string value.
        /// </returns>
        /// <remarks>
        /// Most of the logic in this function is similar to the Encrypt
        /// logic. In order for decryption to work, all parameters of this function
        /// - except cipherText value - must match the corresponding parameters of
        /// the Encrypt function which was called to generate the
        /// ciphertext.
        /// </remarks>
        public static string Decrypt(string cipherText,
                                     string passPhrase,
                                     string saltValue,
                                     string hashAlgorithm,
                                     int passwordIterations,
                                     string initVector,
                                     int keySize)
        {
            // Convert strings defining encryption key characteristics into byte
            // arrays. Let us assume that strings only contain ASCII codes.
            // If strings include Unicode characters, use Unicode, UTF7, or UTF8
            // encoding.
            byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
            byte[] saltValueBytes = Encoding.ASCII.GetBytes(saltValue);

            // Convert our ciphertext into a byte array.
            byte[] cipherTextBytes = Convert.FromBase64String(cipherText);

            // First, we must create a password, from which the key will be 
            // derived. This password will be generated from the specified 
            // passphrase and salt value. The password will be created using
            // the specified hash algorithm. Password creation can be done in
            // several iterations.
            PasswordDeriveBytes password = new PasswordDeriveBytes(
                passPhrase,saltValueBytes,hashAlgorithm,passwordIterations);

            // Use the password to generate pseudo-random bytes for the encryption
            // key. Specify the size of the key in bytes (instead of bits).
            byte[] keyBytes = password.GetBytes(keySize / 8);

            // Create uninitialized Rijndael encryption object.
            RijndaelManaged symmetricKey = new RijndaelManaged();

            // It is reasonable to set encryption mode to Cipher Block Chaining
            // (CBC). Use default options for other symmetric key parameters.
            symmetricKey.Mode = CipherMode.CBC;

            // Generate decryptor from the existing key bytes and initialization 
            // vector. Key size will be defined based on the number of the key 
            // bytes.
            ICryptoTransform decryptor = symmetricKey.CreateDecryptor(keyBytes,initVectorBytes);

            // Define memory stream which will be used to hold encrypted data.
            MemoryStream memoryStream = new MemoryStream(cipherTextBytes);

            // Define cryptographic stream (always use Read mode for encryption).
            CryptoStream cryptoStream = new CryptoStream(memoryStream,
                                                          decryptor,
                                                          CryptoStreamMode.Read);

            // Since at this point we don't know what the size of decrypted data
            // will be, allocate the buffer long enough to hold ciphertext;
            // plaintext is never longer than ciphertext.
            byte[] plainTextBytes = new byte[cipherTextBytes.Length];

            // Start decrypting.
            int decryptedByteCount = cryptoStream.Read(plainTextBytes,0,plainTextBytes.Length);

            // Close both streams.
            memoryStream.Close();
            cryptoStream.Close();

            // Convert decrypted data into a string. 
            // Let us assume that the original plaintext string was UTF8-encoded.
            string plainText = Encoding.UTF8.GetString(plainTextBytes,0,decryptedByteCount);

            // Return decrypted string.   
            return plainText;
        }
    }
}

CodeStash Specific Encryption Helper Class

There is also a specific CodeStash encryption helper class that knows what values to encrypt/decrypt. This class is as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Reflection;

namespace CodeStash.Common.Encryption
{
    public class EncryptionHelper
    {

        private static readonly string passPhrase = "Pas5pr@se";        // can be any string
        private static readonly string saltValue = "s@1tValue";         // can be any string
        private static readonly string hashAlgorithm = "SHA1";          // can be "MD5"
        private static readonly int passwordIterations = 2;             // can be any number
        private static readonly string initVector = "@1B2c3D4e5F6g7H8"; // must be 16 bytes
        private static readonly int keySize = 256;                      // can be 192 or 128
        private static readonly bool encryptionEnabled = false;

        /// <summary>
        /// Static constructor reads the EncryptionEnabled from the App.Config or Web.Config
        /// </summary>
        static EncryptionHelper()
        {
            encryptionEnabled = CodeStash.Common.Helpers.ConfigurationSettings.EncryptionEnabled;
        }

        /// <summary>
        /// Is encryption enabled, as dictated by the App.Config / Web.config 
        /// EncryptionEnabled AppSetting
        /// </summary>
        public static bool EncryptionEnabled
        {
            get { return encryptionEnabled; }
        }

        /// <summary>
        /// Encrypts a single value which is returned
        /// </summary>
        public static string GetEncryptedValue(string valueToEncrypt)
        {
            return RijndaelSimple.Encrypt(valueToEncrypt, passPhrase, saltValue, hashAlgorithm,
                passwordIterations, initVector, keySize);
        }


        public static string GetDecryptedValue(string valueToDecrypt)
        {
            return RijndaelSimple.Decrypt(valueToDecrypt, passPhrase, saltValue, hashAlgorithm,
                passwordIterations, initVector, keySize);
        }

        /// <summary>
        /// Decrypts the 3 input values, and passes back a DecryptedUserValues object with the 3
        /// decrypted values as the following 3 properties
        /// - DecryptedCodeStashToken
        /// - DecryptedEmail
        /// - DecryptedPassword
        /// </summary>
        public static DecryptedUserValues GetDecryptedValues(string codeStashToken, string email, string password)
        {
            string decryptedCodeStashToken = !string.IsNullOrEmpty(codeStashToken) && 
                  !string.Equals("\"\"",codeStashToken) ?
                  RijndaelSimple.Decrypt(codeStashToken, passPhrase, saltValue, hashAlgorithm,
                  passwordIterations, initVector, keySize) : "";

            string decryptedEmail = RijndaelSimple.Decrypt(email, passPhrase, saltValue, hashAlgorithm,
                passwordIterations, initVector, keySize);

            string decryptedPassword = !string.IsNullOrEmpty(password) && 
                !string.Equals("\"\"", password) ?
                RijndaelSimple.Decrypt(password, passPhrase, saltValue, hashAlgorithm,
                passwordIterations, initVector, keySize) : "";

            return new DecryptedUserValues(decryptedCodeStashToken,decryptedEmail,decryptedPassword);
        }
    }
}

Where there are methods for encrypting/decrypting a single value, or to get all three user values in one go, which returns a DecryptedUserValues which looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CodeStash.Common.Encryption
{
    public class DecryptedUserValues
    {
        public String DecryptedCodeStashToken { get; private set; }
        public String DecryptedEmail { get; private set; }
        public String DecryptedPassword { get; private set; }

        public DecryptedUserValues(String decryptedCodeStashToken, 
            String decryptedEmail, String decryptedPassword)
        {
            this.DecryptedCodeStashToken = decryptedCodeStashToken;
            this.DecryptedEmail = decryptedEmail;
            this.DecryptedPassword = decryptedPassword;

        }
    }
}

And you can probably imagine these encrypted/decrypted values are used by the controllers that make up the website.

Dependency Injection Using MEF

As some of you may recall, I recently wrote an entire article on MEF/UnitOfWork/Respository, that article actually came from this website, as such the sections in that article are good to read for more information. Here I am providing bear bones explanations. The other article can be found at: http://www.codeproject.com/Articles/316068/Restful-WCF-EF-POCO-UnitOfWork-Respository-MEF-1-o#rep.

The CodeStash website uses MEF (as it is my IOC container of choice). So how do we use MEF in the CodeStash website?

It essentially boils down to four steps:

Have a Default Controller Factory

This is done using the following code:

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;

namespace CodeStash.Mef
{
    [Export(typeof(IControllerFactory))]
    public class MefControllerFactory : DefaultControllerFactory
    {
        [ImportMany]
        public IEnumerable<ExportFactory<IController, IControllerMeta>> Controllers { get; set; }

  

        public override IController CreateController(RequestContext requestContext, string controllerName)
        {
            try
            {
                // finds the ExportFactory instance that matches the controller name
                var factory = Controllers.
                    Where(fac => fac.Metadata.ControllerName.Equals(controllerName, 
                    	StringComparison.OrdinalIgnoreCase)).
                    FirstOrDefault();

                if (factory == null)
                {
                    throw new HttpException(404, "Not found for " + controllerName);
                }

                var lifeTimeContext = factory.CreateExport();

                // saves lifetime object for later disposal
                requestContext.HttpContext.Items["controller.lifetime.context"] = lifeTimeContext;

                return lifeTimeContext.Value;
            }
            catch
            {
                return base.CreateController(requestContext, controllerName);
            }
        }

        public override void ReleaseController(IController controller)
        {
            // retrives the lifetime object for disposal
            var context = (ExportLifetimeContext<IController>)
                HttpContext.Current.Items["controller.lifetime.context"];

            if (context != null)
            {
                context.Dispose();
            }
        }

        public SessionStateBehavior GetControllerSessionBehavior(
        	RequestContext requestContext, string controllerName)
        {
            return SessionStateBehavior.Default;
        }
    }
}

Where the expected controller metadata is as follows:

public interface IControllerMeta
{
    string ControllerName { get; }
}

Register the Controller Factory

We now need to make sure this default controller factory is used by the ASP MVC framework to resolve controller instances. This is done as follows inside global.asax.cs.

AggregateCatalog catalog = new AggregateCatalog(
    new AssemblyCatalog(typeof(Repository<>).Assembly, context),
    new AssemblyCatalog(typeof(MefControllerFactory).Assembly, context));

container = new CompositionContainer(catalog, CompositionOptions.DisableSilentRejection | 
					CompositionOptions.IsThreadSafe);
IControllerFactory factory = container.GetExportedValue<IControllerFactory>();

ControllerBuilder.Current.SetControllerFactory(factory);

Configure the MEF Container

The version of MEF that is used, uses a more common builder like syntax that you see in a lot of other IOC containers. So using the builder/registration APIs, we are able to configure the MEF container. Here is the relevant configuration code:

private static void SetupControllerFactory()
{
    RegistrationBuilder context = new RegistrationBuilder();

    context.OfType(typeof(Repository<>)).
        Export(builder => builder.AsContractType(typeof(IRepository<>)))
        .SetCreationPolicy(CreationPolicy.NonShared);

    context.OfType(typeof(GetUserForRestService)).
        Export(builder => builder.AsContractType(typeof(IGetUserForRestService)))
        .SetCreationPolicy(CreationPolicy.NonShared);

    context.OfType(typeof(CodeStashEntities)).
        Export(builder => builder.AsContractType(typeof(IUnitOfWork)))
        .SetCreationPolicy(CreationPolicy.NonShared);

    context.OfType(typeof(Log4NetLoggerService)).
        Export(builder => builder.AsContractType(typeof(ILoggerService)))
        .SetCreationPolicy(CreationPolicy.Shared);

    context.OfType(typeof(AccountMembershipService)).
        Export(builder => builder.AsContractType(typeof(IMembershipService)))
        .SetCreationPolicy(CreationPolicy.NonShared);


    context.OfType(typeof(MembershipDataProvider)).
        Export(builder => builder.AsContractType(typeof(IMembershipDataProvider)))
        .SetCreationPolicy(CreationPolicy.NonShared);

    context.OfType(typeof(FormsAuthenticationService)).
        Export(builder => builder.AsContractType(typeof(IFormsAuthenticationService)))
        .SetCreationPolicy(CreationPolicy.NonShared);

    context.OfType(typeof(TagCloudService)).
        Export(builder => builder.AsContractType(typeof(ITagCloudService)))
        .SetCreationPolicy(CreationPolicy.NonShared);

    context.Where(type => typeof(IController).IsAssignableFrom(type) && !type.IsAbstract).
        Export(builder => builder.AsContractType<IController>().
                AddMetadata("ControllerName", type => type.GetControllerName()))
        .SetCreationPolicy(CreationPolicy.NonShared);

    AggregateCatalog catalog = new AggregateCatalog(
        new AssemblyCatalog(typeof(Repository<>).Assembly, context),
        new AssemblyCatalog(typeof(MefControllerFactory).Assembly, context));

    container = new CompositionContainer(catalog, CompositionOptions.DisableSilentRejection | 
                                                  CompositionOptions.IsThreadSafe);
    IControllerFactory factory = container.GetExportedValue<IControllerFactory>();

    ControllerBuilder.Current.SetControllerFactory(factory);
}

Use the MEF Parts

Now that we have all of the pieces in place, we are able to MEF in our controllers and their dependencies, so we can expect something like this in a typical controller:

public class TeamController : BaseTagCloudEnabledController
{
    private readonly IMembershipService membershipService;
    private readonly IMembershipDataProvider membershipDataProvider;
    private readonly ILoggerService loggerService;
    private readonly IRepository<OwnedTeam> ownedTeamRepository;
    private readonly IRepository<CreatedTeam> createdTeamRepository;
    private readonly IUnitOfWork unitOfWork;

    public TeamController(IMembershipService membershipService,
        IMembershipDataProvider membershipDataProvider,
        ILoggerService loggerService, 
        ITagCloudService tagCloudService,
        IRepository<OwnedTeam> ownedTeamRepository, 
        IRepository<CreatedTeam> createdTeamRepository,
        IUnitOfWork unitOfWork)
        : base(tagCloudService)
    {
        this.membershipService = membershipService;
        this.membershipDataProvider = membershipDataProvider;
        this.loggerService = loggerService;
        this.ownedTeamRepository = ownedTeamRepository;
        this.createdTeamRepository = createdTeamRepository;
        this.unitOfWork = unitOfWork;
    }
}

Unit Of Work Pattern

This pattern keeps track of everything that happens during a business transaction that affects the database. At the conclusion of the transaction, it determines how to update the database to conform to the changes.

Martin Fowler has an excellent article on this: http://www.martinfowler.com/eaaCatalog/unitOfWork.html.

Now since we are using LINQ to Entities 4.1 POCO, we already have some of the necessary bits to go about creating a nice Unit Of Work pattern implementation. As before, we start by examining the actual LINQ to Entities 4.1 POCO context object. That for us starts life as a class that extends DbContext.

Here is the website one:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;

namespace CodeStash.Common.DataAccess.UnitOfWork
{
    public abstract class EfDataContextBase : DbContext, IUnitOfWork
    {
        public EfDataContextBase(string nameOrConnectionString)
            : base(nameOrConnectionString)
        {
        }

        public IQueryable<T> Get<T>() where T : class
        {
            return Set<T>();
        }

        public bool Remove<T>(T item) where T : class
        {
            try
            {
                Set<T>().Remove(item);
            }
            catch (Exception)
            {
                return false;
            }
            return true;
        }

        public void Commit()
        {
            base.SaveChanges();
        }

        public void Attach<T>(T obj)
            where T : class
        {
            Set<T>().Attach(obj);
        }

        public void Add<T>(T obj) where T : class
        {
            Set<T>().Add(obj);
        }
    }
}

Which as you now know provides a basic Entity Framework Unit of Work base class. But we need to extend that further to make a specific implementation for our EF 4.1 POCO objects. So we then end up with this:

using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using CodeStash.Common.DataAccess.UnitOfWork;
using System.ComponentModel.Composition;

namespace CodeStash.Common.DataAccess.EntityFramework
{

    public class ApplicationSettings
    {
        [Export("EFConnectionString")]
        public string ConnectionString
        {
            get { return "name=CodeStashEntities"; }
        }
    }

    [Export]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public partial class CodeStashEntities : EfDataContextBase, IUnitOfWork
    {
        [ImportingConstructor()]
        public CodeStashEntities(
            [Import("EFConnectionString")]
            string connectionString) : base(connectionString)
        {
            this.Configuration.ProxyCreationEnabled = false;
            this.Configuration.LazyLoadingEnabled = true;
        }

        
        public DbSet<CodeCategory> CodeCategories { get; set; }
        public DbSet<CodeSnippet> CodeSnippets { get; set; }
        public DbSet<CodeTag> CodeTags { get; set; }
        public DbSet<CreatedTeam> CreatedTeams { get; set; }
        public DbSet<Grouping> Groupings { get; set; }
        public DbSet<Language> Languages { get; set; }
        public DbSet<OwnedTeam> OwnedTeams { get; set; }
        public DbSet<Visibility> Visibilities { get; set; }
    }
}

The basic idea is that we simply expose DbSet properties and also methods to Add/Remove and Save changes. The interaction with these methods will be done by enrolling repositories with the Unit of Work, which we shall see in just a minute.

Repository Pattern

The Repository pattern has been around for a very long time, and comes from Domain Driven Design. MSDN has this to say about the objectives of the Repository pattern:

Use the Repository pattern to achieve one or more of the following objectives:

  • You want to maximize the amount of code that can be tested with automation and isolate the data layer to support unit testing.
  • You access the data source from many locations and want to apply centrally managed, consistent access rules and logic.
  • You want to implement and centralize a caching strategy for the data source.
  • You want to improve the code's maintainability and readability by separating business logic from data or service access logic.
  • You want to use business entities that are strongly typed so that you can identify problems at compile time instead of at run time.
  • You want to associate a behavior with the related data. For example, you want to calculate fields or enforce complex relationships or business rules between the data elements within an entity.
  • You want to apply a domain model to simplify complex business logic.

Sounds cool, doesn't it? But how does that translate into code? Well, here is my take on it (again, I abstract this behind interfaces to allow alternative implementations, or mocking):

Where we have a IRepository interface as follows:

namespace CodeStash.Common.DataAccess.Repository
{
    public interface IRepository>T> where T : class
    {
        void EnrolInUnitOfWork(IUnitOfWork unitOfWork);
        int Count { get; }
        void Add(T item);
        bool Contains(T item);
        void Remove(T item);
        IQueryable>T> FindAll();
        IQueryable>T> FindAll(string lazyIncludeStrings);
        IQueryable>T> FindBy(Func>T, bool> predicate);
        IQueryable>T> FindByExp(Expression>Func>T, bool>> predicate);
        IQueryable>T> FindBy(Func>T, bool> predicate, string lazyIncludeString);
        IQueryable>T> FindByExp(Expression>Func>T, bool>> predicate, string lazyIncludeString);
        
    }
}

And Repository code looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using CodeStash.Common.DataAccess.UnitOfWork;
using System.ComponentModel.Composition;
using System.Data.Entity;
using System.Linq.Expressions;

namespace CodeStash.Common.DataAccess.Repository
{
    public class Repository<T> : IRepository<T> where T : class
    {
        protected IUnitOfWork _context;

        public void EnrolInUnitOfWork(IUnitOfWork unitOfWork)
        {
            this._context = unitOfWork;
        }

        public int Count
        {
            get { return _context.Get<T>().Count(); }
        }
        public void Add(T item)
        {
            _context.Add(item);
        }
        public bool Contains(T item)
        {
            return _context.Get<T>().FirstOrDefault(t => t == item) != null;
        }
        public void Remove(T item)
        {
            _context.Remove(item);
        }
        public IQueryable<T> FindAll()
        {
            return _context.Get<T>();
        }

        public IQueryable<T> FindAll(Func<DbSet<T>, IQueryable<T>> lazySetupAction)
        {

            DbSet<T> set = ((DbSet<T>)_context.Get<T>());
            return lazySetupAction(set);
        }


        public IQueryable<T> FindAll(string lazyIncludeStrings)
        {
            DbSet<T> set = ((DbSet<T>)_context.Get<T>());
            return set.Include(lazyIncludeStrings).AsQueryable<T>();
        }



        public IQueryable<T> FindBy(Func<T, bool> predicate)
        {
            return _context.Get<T>().Where(predicate).AsQueryable<T>();
        }

        public IQueryable<T> FindByExp(Expression<Func<T, bool>> predicate)
        {
            return _context.Get<T>().Where(predicate).AsQueryable<T>();
        }

        public IQueryable<T> FindBy(Func<T, bool> predicate, string lazyIncludeString)
        {
            DbSet<T> set = (DbSet<T>)_context.Get<T>();
            return set.Include(lazyIncludeString).Where(predicate).AsQueryable<T>();
        }

        public IQueryable<T> FindByExp(Expression<Func<T, bool>> predicate, string lazyIncludeString)
        {
            return _context.Get<T>().Where(predicate).AsQueryable<T>();
        }
    }
}

At first glance, this may seem confusing, but all the repository does is abstract away dealing with the raw data from the database. It can also be seen above that my implementation relies on an IUnitOfWork object. In my case, that is the actual LINQ to Entities 4.1 DbContext class that we use to talk to the database.

So by allowing your repositories to interact with the IUnitOfWork (LINQ to Entities 4.1 DbContext) code, we can get the repositories to enroll in a simple transactional piece of work (Unit of Work, basically).

And here is how it would typically work along side the UnitOfWork pattern implementation that is used within the CodeStash website:

public ActionResult SaveNewTeam(string teamName)
{
    if (!string.IsNullOrEmpty(teamName))
    {
        using (unitOfWork)
        {
            ownedTeamRepository.EnrolInUnitOfWork(unitOfWork);
            createdTeamRepository.EnrolInUnitOfWork(unitOfWork);


            MembershipUser user = membershipService.GetUserByUserName(User.Identity.Name);
            if (!ownedTeamRepository.FindBy(x => x.TeamDescription.ToLower() == teamName.ToLower()).Any())
            {
                OwnedTeam ownedTeam = new OwnedTeam(Guid.Parse(user.ProviderUserKey.ToString()), teamName);
                ownedTeamRepository.Add(ownedTeam);
                unitOfWork.Commit();
                createdTeamRepository.Add(new CreatedTeam(
                   ownedTeam.TeamId, Guid.Parse(user.ProviderUserKey.ToString())));
                unitOfWork.Commit();
                return Json(new { Message = "Team Added Successfully", Success = true });
            }
            else
            {
                return Json(new { Message = "Team Already Exists", Success = false });
            }

        }
    }
    else
    {
        return Json(new { Message = "You did not enter a valid TeamName", Success = false });
    }
}

Like I say, I have written a big article on these three things, it would be well worth your while reading them if you do not get how they work from this cut down explanation. See http://www.codeproject.com/Articles/316068/Restful-WCF-EF-POCO-UnitOfWork-Respository-MEF-1-o.

General Page Structure

Each full page within the CodeStash website pretty much follows the same rules:

_Layout.cshtml

The ~/Views/Shared/_Layout.cshtml layout page (Master page essentially is used), where the most relevant parts are as follows:

<head>
    @RenderSection("SpecificPageHeadStuff", false)
</head>
<body>
    ....
    ....
    <div id="main-wrapper">
 	....
 	....
        <div id="main">

            <div id="mainbar">
                @RenderBody()
            </div>
        </div>
    </div>
</body>

See how there are essentially two place holders:

  1. One for page specific header stuff, which ensures only the relevant scripts/CSS are loaded. We don't have one big layout page with everything in it.
  2. There is a place holder for the main body content.

A Typical Page

A typical page will then look like this:

@model CodeStash.Models.Team.TeamModel
@{
	ViewBag.Title = "Team Settings";
	Layout = "~/Views/Shared/_Layout.cshtml";
}
@section SpecificPageHeadStuff
 {
     @Html.CssTag(Url.Content("~/Content/Controllers/Team/team.css"))
     @Html.ScriptTag(Url.Content("~/Scripts/Controllers/Team/team.js"))
}
@using CodeStash.ExtensionsMethods


<div id="TeamPanel">

	....
	....
	....
	....
</div>

Don't be alarmed by @Html.CssTag and @Html.ScriptTag, they are discussed just below.

Hashing

One of the nice aspects of the web portion of CodeStash is that whenever a change is made to one of the content files (CSS/JavaScript files), the browser's cache is immediately invalid. This is thanks to a fancy caching MVC HtmlHelper extension method that my work colleague Ryan Worsley was using at work on a massive ASP MVC project we are working on. So I can take no credit for the next bit of code, it's all Ryans work, but it does work very nicely.

Using the Hashing Helper

All you need to do to make use of the hashing HtmlHelper extension method is use as follows, where there are two helpers: one for CSS and one for JavaScript files:

@Html.CssTag(Url.Content("~/Content/openid-shadow.css"))
@Html.ScriptTag(Url.Content("~/Scripts/jquery-1.6.4.min.js"))

So let's have a  look at one of these, I will pick the CssTag, which simply looks like this:

public static MvcHtmlString CssTag(this HtmlHelper html, string path)
{
    //Build up a string like this one
    //   <link href="@Url.Content("~/Content/site.css")"
    //            rel="stylesheet" type="text/css" />
    HttpContextBase context = html.ViewContext.RequestContext.HttpContext;
    UrlHelper url = new UrlHelper(html.ViewContext.RequestContext);

    if (System.IO.File.Exists(context.Request.MapPath(path)))
    {
        path = AppendHash(html, path);

        TagBuilder script = new TagBuilder("link");
        script.MergeAttribute("href", url.Content(path));
        script.MergeAttribute("rel", "stylesheet");
        script.MergeAttribute("type", "text/css");
        return MvcHtmlString.Create(script + "\r\n");
    }

    return MvcHtmlString.Empty;
}


public static string AppendHash(this HtmlHelper html, string path)
{
    if (html.ViewContext.HttpContext.Cache.Get("__ContentWatcher") != null)
    {
        ContentWatcher contentWatcher = 
          html.ViewContext.HttpContext.Cache["__ContentWatcher"] as ContentWatcher;
        if (contentWatcher != null && contentWatcher.ContainsKey(path))
        {
            return string.Format("{0}?v={1}", path, contentWatcher[path]);
        }
    }
    return path;
}

What the Content Files Look Like in the Browser

When the page is served, this is what the hashing described above produces:

<link href="http://www.codeproject.com/Content/openid-shadow.css?v=bc8473b315edb851a802ba53464ac0a0" 
    rel="stylesheet" type="text/css">

Extra HTML Helpers

I think any ASP MVC developer would quickly come across the need to create their own Select HtmlHelper extension methods as the one that comes with ASP MVC is really not a great fit, and forces your ViewModel to contain/expose UI junk, which they really shouldn't.

To this end, CodeStash makes use of a rather cool HtmlHelper extension method that allows your ViewModel to be plain properties and we use ExpressionTrees and Func<T> delegates to allow the user to express what the final SELECT HTML tag will look like when rendered by the HtmlHelper.

Starting With the View Model

Where an example may look like this:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using CodeStash.Common.DataAccess.EntityFramework;

namespace CodeStash.Models.Snippet
{
    public class AddSnippetViewModel : ISnippetViewModel
    {
        #region Ctor
        /// <summary>
        /// For Post request where ModelBinding takes care of
        /// matching up properties for us
        /// </summary>
        public AddSnippetViewModel()
        {
            VisibilityList = new List<Visibility>();

        }
        #endregion

        #region Public Properties

 

        public List<Visibility> VisibilityList { get; set; }

        [Required(ErrorMessage = "You must enter a value for Visibility")]
        public int Visibility { get; set; }
        

        public Guid AspNetMembershipUserId { get; set; }
        #endregion
    }
}

Which we might use like this in some .cshtml file:

<strong>Code Snippet Visibility</strong>
<div>
        @Html.ComboFor(
		x => x.Visibility, 
		x => x.VisibilityList, 
		x => x.Id, 
		x => x.VisibilityDescription,
        new Dictionary<string, object> { 
            { "style", "width:400px"}, 
            { "class", "selectBox"  }})
     
    @Html.ValidationMessageFor((x) => x.Visibility)
</div>

See how easy that was to use? So let's now see the HtmlHelper extension method that does this, shall we?

public static MvcHtmlString ComboFor<TModel, TItem, TValue, TKey>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TValue>> selectedItemExpr,
    Expression<Func<TModel, IEnumerable<TItem>>> enumerableExpr,
    Expression<Func<TItem, TValue>> valueExpr,
    Expression<Func<TItem, TKey>> keyExpr,
    IDictionary<string, object> htmlAttributes) where TValue : IComparable
{
    TModel model = (TModel)htmlHelper.ViewData.Model;

    string id = ExpressionUtils.GetPropertyName(selectedItemExpr);

    TValue selectedItem = selectedItemExpr.Compile()(model);
    IEnumerable<TItem> sourceItems = enumerableExpr.Compile()(model);

    Func<TItem, TKey> keyFunc = keyExpr.Compile();
    Func<TItem, TValue> valueFunc = valueExpr.Compile();

    List<SelectListItem> selectList =
        (from item in sourceItems
            let itemValue = valueFunc(item)
            let itemKey = keyFunc(item)
            select new SelectListItem()
                {
                    Selected = selectedItem.CompareTo(itemValue) == 0,
                    Text = itemKey.ToString(),
                    Value = itemValue.ToString()
                }).ToList();

    return htmlHelper.DropDownList(id.ToString(), selectList, htmlAttributes);
}

Where this HtmlHelper extension method makes use of the the following helper code:

public static class ExpressionUtils
{
    public static string GetPropertyName<TModel, TItem>(
        Expression<Func<TModel, TItem>> propertyExpression)
    {
        var lambda = propertyExpression as LambdaExpression;
        MemberExpression memberExpression;
        if (lambda.Body is UnaryExpression)
        {
            var unaryExpression = lambda.Body as UnaryExpression;
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
        {
            memberExpression = lambda.Body as MemberExpression;
        }

        var propertyInfo = memberExpression.Member as PropertyInfo;

        return propertyInfo.Name;
    }

}

Error Handling

As much as possible, the standard ASP.NET MVC error handling strategies have been used. Strangely enough, these "standard" processes do not seem to be written down anywhere, and you kind of have to use a delicate mixture of a bit of noose, shaking the odd chicken bone at it, whilst uttering 1000nd year old incantations helps for sure.

If you want to read more about the voodoo I am talking about, this is a good place to start: //http://community.codesmithtools.com/CodeSmith_Community/b/tdupont/archive/2011/03/01/error-handling-and-customerrors-and-mvc3-oh-my.aspx.

Anyway I digress, the main error handling techniques used are as follows:

  • HandleError attribute
  • Error Controller
  • Strongly typed error page
  • Specific error pages for known HTML errors
  • Error handling config

We will now look at the specifics of how that all works.

Handle Error Attribute

All controllers automatically get the HandleErrorAttribute applied as default, which is done in the global.asax.cs file, which is shown below.

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    HandleErrorAttribute handleError = new HandleErrorAttribute();
    handleError.View = "_Error";
    filters.Add(handleError);
}

See how the HandleErrorAttribute points to a view called "_Error", this is a strongly typed view that the ASP MVC framework supplies with a System.Web.Mvc.HandleErrorInfo type model.

Strongly Typed Error Page

This is what the "_Error" view looks like that the HandleErrorAttribute will load when an Exception occurs:

@model System.Web.Mvc.HandleErrorInfo

@{
    ViewBag.Title = "Error";
}

<h2>
    Sorry, an error occurred while processing your request.
</h2>
<h3>ActionName:</h3>
<p>@Model.ActionName</p>
<h3>ControllerName:</h3>
<p>@Model.ControllerName</p>
<h3>Exception:</h3>
<p>@Model.Exception</p>

Error Controller

There is also a dedicated ErrorController, which is as shown below:

using System.Web.Mvc;

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

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

        .....
        .....


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

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

Specific Error Pages For Known HTML Errors

As you can see above, there is a specific View that gets rendered by using the routes available within the ErrorController, let's have a look at one of these Views. They all follow the same sort of pattern:

@{
    Layout = null;
}
@using CodeStash.ExtensionsMethods


<!DOCTYPE html>

<html>
<head>
    <title>Ooops : 301 Error</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    @Html.CssTag(Url.Content("~/Content/Site.css"))
</head>
<body>
    <div class="errorContainer">
        <div class="errorHeader">
            <img src="../../Content/images/Logo.png" class="errorLogo" />
        </div>
        <div class="errorContainerInner">
            <img src="../../Content/images/error.png" />
            <div class="errorWords">
                <h1 class="errorH1">Oooops : 301 Error</h1>
                <p>Moved Permanently</p>
                <p>Requested a directory instead of a file</p>
                <p>The web server substituted the index.html file</p>
                <br />
                <p>
                <span><a href="http://www.codeproject.com/Home/Index">Return to the home page</a></span>
                </p>
            </div>
        </div>
    </div>
</body>
</html>

Error Handling Config

These specific error pages also need some configuration to add the final magic to make it all work. This is done in the Web.Config as follows:

<customErrors mode="On" defaultRedirect="~/Error"> <!-- This will be the Error controllers Index action -->
  <error statusCode="301" redirect="~/Error/Error301"></error>
  <error statusCode="302" redirect="~/Error/Error302"></error>
  <error statusCode="303" redirect="~/Error/Error303"></error>
  <error statusCode="304" redirect="~/Error/Error304"></error>
  <error statusCode="305" redirect="~/Error/Error305"></error>
  <error statusCode="401" redirect="~/Error/Error401"></error>
  <error statusCode="402" redirect="~/Error/Error402"></error>
  <error statusCode="403" redirect="~/Error/Error403"></error>
  <error statusCode="404" redirect="~/Error/Error404"></error>
  <error statusCode="405" redirect="~/Error/Error405"></error>
  <error statusCode="406" redirect="~/Error/Error406"></error>
  <error statusCode="407" redirect="~/Error/Error407"></error>
  <error statusCode="408" redirect="~/Error/Error408"></error>
  <error statusCode="409" redirect="~/Error/Error409"></error>
  <error statusCode="410" redirect="~/Error/Error410"></error>
  <error statusCode="411" redirect="~/Error/Error411"></error>
  <error statusCode="412" redirect="~/Error/Error412"></error>
  <error statusCode="413" redirect="~/Error/Error413"></error>
  <error statusCode="414" redirect="~/Error/Error414"></error>
  <error statusCode="415" redirect="~/Error/Error415"></error>
</customErrors>

Setting Up CodeStash Yourself

In this section, we will talk about what you need to do to setup all the bits and bobs you need to get CodeStash up and running.

Prerequisites

There are a number of prerequisites that are required. This section outlines them all and tells you where they can be obtained.

SQL Server

Well, I can't really tell you where to download this, you either have it or you don't, but you will need SQL Server. I used SQL server 2005.

.NET Framework

CodeStash is written in .NET 4.0, as such you will need to have the .NET Framework v4.0, which is available here: .NET Framework 4.0 download.

MVC 3

CodeStash is written using ASP MVC3 and uses the Razor view engine. As such, you will need to install ASP MVC 3: ASP MVC 3 download.

Entity Framework 4.1

CodeStash is written using Entity Framework 4.1 POCO. As such you will need to install Entity Framework 4.1 POCO. This is possible by using the EXE that comes with the download code from the CodeStash CodePlex website.

You simply need to run "CodeStash\Lib\EntityFramework4.1_POCO\EntityFramework41.exe" to install Entity Framework 4.1 POCO.

Reference Binaries

CodeStash relies on a few third party libraries that Pete and I both bought in as we needed them, we make use of the following:

Website Referenced DLLs

  1. DotNetOpenAuth: Openid authentication
  2. Entity Framework 4.1: ORM (we just talked about where to install that from)
  3. log4Net: Apache logging
  4. MEF2 Preview 3: The IOC container used
  5. NUnit: Unit testing framework (only really important if you wish to run the tests)
  6. Telerik MVC: This is used for the DataGrid that displays the search results

You will find all these binaries in the download code from the CodeStash CodePlex website, within a folder. You simply need to run "CodeStash\Lib" that has all the binaries that are used.

Add-in Referenced DLLs

  1. Cinch WPF: MVVM framework for WPF
  2. MEFedMVVM: ViewModel/View resolution framework for WPF
  3. AvalonEdit: Syntax highlighting and code collapsing editor control for WPF
  4. Microsoft.Expression.Interactions / System.Windows.Interactivity: Blend behavior DLLs for WPF
  5. Rhino Mocks: Mocking framework

You will find all these binaries in the download code from the CodeStash CodePlex website. You simply need to run "CodeStash\References" that has all the binaries that are used.

Setting up the Databases

This section talks about what you need to setup from a database point of view:

ASP Membership DB Installation

You will need to setup the standard ASP.NET membership database, which you can do using this command aspnet_regsql.exe, where the ASP Membership database should be called "AspNetMembership".

ASP Membership DB Setup

The next thing you will need to do is open your SQL Server installation and run the following SQL command against your newly created ASP Membership database "AspNetMembership".

From the downloaded code from the CodeStash CodePlex website:

  • 01 Add sp_GetUserForRest.sql
  • 02 Add sp_GetUserDataByEmail.sql
  • 03 Add sp_GetUserDataForUserId.sql

CodeStash DB Installation

The next thing you will need to do is within your SQL Server installation, you will need to do one of the following:

  • Create a new database called "CodeStash"
  • From the downloaded code from the CodeStash CodePlex website, run the following SQL commands
    • 01 Create Database.sql

CodeStash DB Setup

Now that you have a "CodeStash" database in your SQL Server installation, you need to do one of the following:

  • From the downloaded code from the CodeStash CodePlex website, run the following SQL commands
    • 02 Create The Database Tables And Static Data.sql

Website Installation

This should just be a question of pushing a "Publish" of the "CodeStash" project in the CodeStash website's downloadable code to your IIS installation.

Website DB Config

You will also need to setup the CodeStash website's Web.Config for the two databases. The two connection strings are as follows, where you should replace XXX with your SQL Server installation details and login:

<connectionStrings>
    
    <add name="ApplicationServices" 
        connectionString="Data Source=localhost;User ID=XXX;Password=XXX;
    	Initial Catalog=AspNetMembership;Integrated Security=False;Timeout=180" 
    	providerName="System.Data.SqlClient" />
    
    <add name="CodeStashEntities" 
        connectionString="metadata=res://*/DataAccess.EntityFramework.CodeStashEntities.csdl|
           	res://*/DataAccess.EntityFramework.CodeStashEntities.ssdl|res://*/
           	DataAccess.EntityFramework.CodeStashEntities.msl;
        provider=System.Data.SqlClient;provider connection string="data source=XXX;
          initial catalog=CodeStash;persist security info=True;user id=xxx;password=XXX;
          multipleactiveresultsets=True;App=EntityFramework"" 
    	providerName="System.Data.EntityClient"/>
</connectionStrings>

You will find these in the downloaded CodeStash code.

Website Encryption Config

The decision to turn encryption On/Off is one that is vitally important to the running of the CodeStash website, and is one that needs to be done up front before you allow any users to logon/register.

If you allow people to logon/register with encryption turned on, and then turn it off, all the users who logged on/registered prior to the encryption being turned off will still have their details stored as encrypted values, and as such will not work any more.

Once you have decided to encrypt or not, you will also need to setup the CodeStash website Web.Config to say whether you want user login details encrypted or not.

This is done using the following Web.config setting:

<appSettings>
    <add key="EncryptionEnabled" value="false" />
</appSettings>

Add-in Installation

Pete will discuss this in his forthcoming article dedicated to the add-in.

Add-in Config

Pete will discuss this in his forthcoming article dedicated to the add-in, but for the record, this config should point to the address where you deployed your CodeStash installation. Something like this:

<appSettings>
	<add key="RestAddress" value="http://localhost:8300/Rest/" />
</appSettings>

Add-in Encryption Config

Pete will discuss this in his forthcoming article dedicated to the add-in, but for the record, there is also an "EncryptionEnabled" app setting that must match the Web.Config "EncryptionEnabled" value, so should be something like that shown below if you have "EncryptionEnabled" set to false in the CodeStash Web.Config.

This is done using the following Web.config setting:

<appSettings>
    <add key="EncryptionEnabled" value="false" />
</appSettings>

Supported Browsers

At present we only support the following browsers, it may work in others, but we have tested the following ones:

  • Google Chrome v17 and above
  • Firefox v3.6 and above

You may notice there is no IE support mentioned there, and we don't apologies for that at all, it's just too wack is IE, it may work in IE, but it may not, and you know what, whatever happens on that front, is just the way it is. I have enough gray hair already and do not wish to get any more messing around with that beast.

That's It

Anyway, there you have it. I hope you all like the work Pete and I have put together, we have both spent lots of time on this and we both believe it could be a most useful tool. As we say, we would love to hear from you on this one. Do you think it's useful? Would you use it? What improvements could we make?

Would you like it to stick to using the OpenId/Register approach or would you rather see it move to use your existing CodeProject credentials? You would still need to login though, as CodeStash would be a separate entity from CodeProject.

It really would be good to hear from you all.

We have tried to make it work the way we think would be best for developers, but we have probably missed some things, so we are kind of reliant on you guys to tell us that, so please don't hold back if there are things you want us to do for V2. We have a few things up our sleeves for V2, but we wanted to see what the general feeling was for this first offering before we set about sweating more blood and tears over this project.

Anyway, in the next article, I will be talking you through how the website works in fine detail, which should be of interest I hope. Then after that, I will be handing over to Pete for him to talk you through the add-in.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Authors

Pete O'Hanlon
CEO
United Kingdom United Kingdom
A developer for over 30 years, I've been lucky enough to write articles and applications for Code Project as well as the Intel Ultimate Coder - Going Perceptual challenge. I live in the North East of England with 2 wonderful daughters and a wonderful wife.
 
I am not the Stig, but I do wish I had Lotus Tuned Suspension.
Follow on   Twitter   Google+

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralRe: Not many self hosting?? PinmvpSacha Barber22-Mar-12 1:18 
GeneralRe: Not many self hosting?? PinmemberMichel Renaud22-Mar-12 2:51 
GeneralRe: Not many self hosting?? PinmvpSacha Barber22-Mar-12 2:59 
QuestionMy 5 for you Pinmemberbaskinhu21-Mar-12 8:46 
AnswerRe: My 5 for you PinmvpSacha Barber21-Mar-12 9:04 
GeneralMy vote of 5 PinmemberBenjamin Uboegbu21-Mar-12 8:07 
GeneralRe: My vote of 5 PinmvpSacha Barber21-Mar-12 8:31 
GeneralMy vote of 5 Pinmemberdefwebserver21-Mar-12 7:10 
Wow
GeneralRe: My vote of 5 PinmvpSacha Barber21-Mar-12 7:19 
GeneralMy vote of 5 Pinmembermark merrens21-Mar-12 6:23 
GeneralRe: My vote of 5 PinmvpSacha Barber21-Mar-12 6:25 
QuestionMy vote of 5 ... PinmemberAngelo Cresta21-Mar-12 5:06 
AnswerRe: My vote of 5 ... PinmvpSacha Barber21-Mar-12 5:09 
GeneralSacha missing... PinmvpNish Sivakumar21-Mar-12 4:42 
GeneralRe: Sacha missing... PinmvpSacha Barber21-Mar-12 4:53 
GeneralRe: Sacha missing... PinmvpNish Sivakumar21-Mar-12 4:56 
QuestionMy volte of 5 PinmemberMike Hankey21-Mar-12 3:51 
AnswerRe: My volte of 5 PinmvpSacha Barber21-Mar-12 3:57 
GeneralMy vote of 5 Pinmemberlinuxjr21-Mar-12 3:25 
GeneralRe: My vote of 5 PinmvpSacha Barber21-Mar-12 3:42 
GeneralMy vote of 5 PinmvpEspen Harlinn21-Mar-12 2:22 
GeneralRe: My vote of 5 PinmvpSacha Barber21-Mar-12 3:42 
QuestionREAD THIS : Broken links and formatting PinmvpSacha Barber21-Mar-12 2:02 
SuggestionRe: READ THIS : Broken links and formatting PinmemberJulien Villers21-Mar-12 4:31 
GeneralMy vote of 5 PinmemberNagy Vilmos21-Mar-12 1:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.1411023.1 | Last Updated 15 May 2012
Article Copyright 2012 by Pete O'Hanlon, Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid