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:
- Searching was very limited, if offered at all, which made it very hard to find a snippet once you uploaded it.
- 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.
- 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:
- We would make our branding more in line with CodeProject colors, come on bring the green/orange.
- 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?
- 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:
- Encryption is enabled: This means that all users will have their CodeStash login details encrypted.
- 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:
- OpenId token (only applicable if the user logged in uses OpenId provider)
- Email address
- 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.
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
namespace CodeStash.Common.Encryption
{
public class RijndaelSimple
{
public static string Encrypt(string plainText,
string passPhrase,
string saltValue,
string hashAlgorithm,
int passwordIterations,
string initVector,
int keySize)
{
byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
byte[] saltValueBytes = Encoding.ASCII.GetBytes(saltValue);
byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
PasswordDeriveBytes password = new PasswordDeriveBytes(
passPhrase,saltValueBytes,hashAlgorithm,passwordIterations);
byte[] keyBytes = password.GetBytes(keySize / 8);
RijndaelManaged symmetricKey = new RijndaelManaged();
symmetricKey.Mode = CipherMode.CBC;
ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes,initVectorBytes);
MemoryStream memoryStream = new MemoryStream();
CryptoStream cryptoStream = new CryptoStream(memoryStream,encryptor,CryptoStreamMode.Write);
cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
cryptoStream.FlushFinalBlock();
byte[] cipherTextBytes = memoryStream.ToArray();
memoryStream.Close();
cryptoStream.Close();
string cipherText = Convert.ToBase64String(cipherTextBytes);
return cipherText;
}
public static string Decrypt(string cipherText,
string passPhrase,
string saltValue,
string hashAlgorithm,
int passwordIterations,
string initVector,
int keySize)
{
byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
byte[] saltValueBytes = Encoding.ASCII.GetBytes(saltValue);
byte[] cipherTextBytes = Convert.FromBase64String(cipherText);
PasswordDeriveBytes password = new PasswordDeriveBytes(
passPhrase,saltValueBytes,hashAlgorithm,passwordIterations);
byte[] keyBytes = password.GetBytes(keySize / 8);
RijndaelManaged symmetricKey = new RijndaelManaged();
symmetricKey.Mode = CipherMode.CBC;
ICryptoTransform decryptor = symmetricKey.CreateDecryptor(keyBytes,initVectorBytes);
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
CryptoStream cryptoStream = new CryptoStream(memoryStream,
decryptor,
CryptoStreamMode.Read);
byte[] plainTextBytes = new byte[cipherTextBytes.Length];
int decryptedByteCount = cryptoStream.Read(plainTextBytes,0,plainTextBytes.Length);
memoryStream.Close();
cryptoStream.Close();
string plainText = Encoding.UTF8.GetString(plainTextBytes,0,decryptedByteCount);
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"; private static readonly string saltValue = "s@1tValue"; private static readonly string hashAlgorithm = "SHA1"; private static readonly int passwordIterations = 2; private static readonly string initVector = "@1B2c3D4e5F6g7H8"; private static readonly int keySize = 256; private static readonly bool encryptionEnabled = false;
static EncryptionHelper()
{
encryptionEnabled = CodeStash.Common.Helpers.ConfigurationSettings.EncryptionEnabled;
}
public static bool EncryptionEnabled
{
get { return encryptionEnabled; }
}
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);
}
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
{
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();
requestContext.HttpContext.Items["controller.lifetime.context"] = lifeTimeContext;
return lifeTimeContext.Value;
}
catch
{
return base.CreateController(requestContext, controllerName);
}
}
public override void ReleaseController(IController controller)
{
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:
- 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.
- 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)
{
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
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"> -->
<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
- DotNetOpenAuth: Openid authentication
- Entity Framework 4.1: ORM (we just talked about where to install that from)
- log4Net: Apache logging
- MEF2 Preview 3: The IOC container used
- NUnit: Unit testing framework (only really important if you wish to run the tests)
- 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
- Cinch WPF: MVVM framework for WPF
- MEFedMVVM: ViewModel/View resolution framework for WPF
- AvalonEdit: Syntax highlighting and code collapsing editor control for WPF
- Microsoft.Expression.Interactions / System.Windows.Interactivity: Blend behavior DLLs for WPF
- 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
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.