our last episode for Tekpub's ASP.NET MVC 2 series. I don't particularly like to write "pimp posts" - but I'm making an exception in this case as we've decided to Open Source the code and I wanted to talk about what's in it...." /> our last episode for Tekpub's ASP.NET MVC 2 series. I don't particularly like to write "pimp posts" - but I'm making an exception in this case as we've decided to Open Source the code and I wanted to talk about what's in it...." />
Article Image
read

Steven Sanderson and I just wrapped up our last episode for Tekpub's ASP.NET MVC 2 series. I don't particularly like to write "pimp posts" - but I'm making an exception in this case as we've decided to Open Source the code and I wanted to talk about what's in it.

Another Starter Site?

Yes. And no - this isn't a project template so much as it is a foundation for you to build on. You'll have about 10 minutes of nudging/tweaking things - stuff like resetting namespaces, project names, deployment stuff. After that, though, you should have the tools you need to get rolling.

First things first, though:

the source is up here on Codeplex and you

you can watch a freebie all about it here.

Data Access

Everyone has their own flavor of how they like to do things, or perhaps how their boss likes them to do things. I've set it up so you can put together whatever makes sense for you - or you can just use what I've thrown in there.

This is going to sound a little weird at first, but let it sink in. Then think back to that long-running project you had, the one where you needed to split apart your data access to bend a bit for special circumstances: Optimized Read-only, Reporting Extraction, and Regular CRUD. Well, that's what I've faced a lot of, at least, and I think most applications have to deal with each of these things over time.

ISession

It's all built around the notion of UnitOfWork and a "Session". If you like NHibernate - you'll feel good here. If you're a Linq to SQL fan, the Data Context is a Unit of Work. The idea is you work on some objects, the Session tracks the changes, then you commit at once.

ISession has some pretty standard methods, exposing a single IQueryable for querying:using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Web.Infrastructure.Storage {
public interface ISession:IDisposable { void CommitChanges(); void Delete (Expression

expression) where T : class; void Delete (T item) where T : class; void DeleteAll () where T : class; T Single (Expression expression) where T : class; System.Linq.IQueryable All
() where T : class; void Add (T item) where T : class; void Add (IEnumerable items) where T : class;
void Update (T item) where T : class; } }

This works great for normal CRUD stuff and I can stick Linq to SQL behind it nicely, as well as MongoDB and DB4O (all of which are in the source) There are two more interfaces for special circumstances too - IReadOnlySession (which is the same as above but with Single and All() only) and IReporting, which can be an explicit thing (if you like) or you can stick ISession behind it (like I've done - see the code).

Breaking these things apart allows you to see that your data access isn't (and shouldn't be) a single "aspect" of your application. You have to store all kinds of things, CRUD ops is just one type of interaction (adding/saving Products, for instance) - and this happens least of all.

Typically, you're reading out data like a freak, and you need to cache and optimize these interactions to make them fast (turning off Object Tracking, eager loading, whatever). This is where IReadOnly session comes in - whatever you put behind it can be specially optimized with pre-compiled queries, no change tracking, and whatever read optimizations you like.

Reporting is the opposite - you dump data out of your application regularly and you usually don't read from it. Structures are flattened and don't care much for normalization rules - so you can be free to go the CQRS route if you like and have specialized reporting objects (an example of this is in the source - UserActivity).

Allowing these operations to separate frees your application to use some fun stuff - like NoSQL if you want - and let's you focus on scaling when you need to.

Authentication and Security

I think it's great that Phil and team have included "Auth in the Box" and I think it's also great they left some room for dorks like me to improve it :). Instead of an AccountController I've adopted the Rails convention of a SessionController to handle logins, and I've pushed Authentication behind an interface with 2 methods: IsValidLogin() and Register().

I like Open ID so I took

DotNetOpenAuth (from Andrew Arnott) and dropped it behind

Open ID Selector (the one they have at Stack Overflow) It's all wired up and ready to go, integrating with ASP Membership and so on.

Given that I have an opinionated bit of data access I can tweak up the CodeTemplates a bit (more below) to be more secure by default. To that end I wrapped up the AntiXSS library and pushed in 2 additional helpers: "h()" for full encoding and "Sanitize()" for stripping bad things from HTML.

I think ValidateAntiForgeryToken is underused a bit, and it's all too easy to forget - so I reworked the Controller template to make sure there's a valid anti forgery token (and I include that token on the Add/Edit pages) and in addition I force you to create a whitelist for binding your stuff using ModelBinding.

You're welcome Barry :).

Smarter CodeTemplates

The default templates are rockin and if you know more about your data access you can really go to town with the Convention/Opinion stuff. Here's an example of what I've put in there (you have to set things like the ID fields and the whitelist stuff):using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Web.Model;
using Web.Infrastructure.Storage;

//Controller for a User namespace Web.Controllers
{ public class UserController : Controller { const int PageSize=20; ISession _session; public UserController(ISession session){ _session=session; }

    public ActionResult Index(int? pg)
    {
        int pageIndex=0;
        if(pg.HasValue){
            pageIndex=pg.Value;
        }

        var items = _session.All

(); var list = new PagedList (items, pageIndex, PageSize); return View(list); }

    public ActionResult Details(string id)
    {
        var item = _session.Single

(x=>x.UserName == id); return View(item); }

    //[Authorize(Roles="Administrator")]
    public ActionResult Create()
    {
        var item = new User();
        return View(item);
    } 


    [HttpPost]
    [ValidateAntiForgeryToken]
    //[Authorize(Roles="Administrator")]
    public ActionResult Create(FormCollection collection)
    {

        var item = new User();
        //please don't omit this...
        var whiteList=new string[]{"UserName","Friendly"};
        UpdateModel(item,whiteList,collection.ToValueProvider());

        if(ModelState.IsValid){
            try
            {
                _session.Add

(item); _session.CommitChanges(); this.FlashInfo("User saved..."); return RedirectToAction("Index"); } catch { this.FlashError("There was an error saving this record"); return View(item); } } return View(item);

    }

    //[Authorize(Roles="Administrator")]
    public ActionResult Edit(string id)
    {
        var item = _session.Single

(x => x.UserName == id); return View(item); }

    [HttpPost]
    [ValidateAntiForgeryToken]
    //[Authorize(Roles="Administrator")]
    public ActionResult Edit(string id, FormCollection collection)
    {
        var item = _session.Single

(x => x.UserName == id); //please don't omit this... var whiteList=new string[]{"field1","field2"}; UpdateModel(item,whiteList,collection.ToValueProvider());

        if(ModelState.IsValid){
            try
            {
                _session.Update

(item); _session.CommitChanges(); this.FlashInfo("User saved..."); return RedirectToAction("Index"); } catch { this.FlashError("There was an error saving this record"); return View(item); } } return View(item);

    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    //[Authorize(Roles="Administrator")]
    public ActionResult Delete(string id, FormCollection collection)
    {
        var item = _session.Single

(x => x.UserName == id); try { _session.Delete (item); _session.CommitChanges(); this.FlashInfo("User deleted..."); return RedirectToAction("Index"); } catch { this.FlashError("There was an error deleting this record"); return View("Edit",item); } } } }

Fly Flash System

I really like the StackOverflow notification system, so I copied it completely :). A bit of jQuery, some styling and you have a pretty cool Flash system that works with TempData, so you can push messages across redirects.

One thing you'll want to do is tweak the CSS.

Tasks

One thing that drives me nuts is that we don't have a tool like Rake (the Ruby task/builder application) with ASP.NET. We have MSBuild, and we have Visual Studio, but we don't have Rake so we have to roll one by hand. To that end I put a Console app in the solution (called "Tasks") that is there for you to fill out as needed.

If you're scratching your head wondering "why would I use that?" - well it's a good question. What I've tried to do with this starter site is think a year ahead and what you'll need then. You might need things like:*a way to minify your javascript and CSS files

*a way to purge your log files

*a mailer that sends out nicely formatted notes to certain users of your app

*a pruner that removes old, out of date (or incorrect) info from your database

*a way to run timed jobs (aka "cron jobs")

*[Diety] knows what elseThe point with this is that there are lot of things we might want to do with/to our app that doesn't fit into Visual Studio or MSBuild.

The Rest... In Summary

It took me a while to build this thing, and if I kept going I'd lose you. As I mention above we're

giving away the final episode in which I build this thing so if you want to see more, head on over. It's 80 or so minutes long and I go pretty deep, but if you want to read more here... well here's what else I tossed in:*A bunch of my favorite Helpers, including AntiXSS and various HTML things

*Support for MongoDB using NoRM

*Logging already setup for you with NLog

*SpecFlow (Cucumber-like BDD testing tool) setup and ready to go

*960 pixel CSS powered by Blueprint CSS and Typography

*Custom error handling ready to go

*EditorTemplate/Display template examples using jQuery UI date picker

*Content directory renamed to "Public" with css, javascript, images, etc pushed into it

*IoC already setup with Ninject

*Alternate ViewEngines ready to go and configured (but commented out) - Spark and nHaml

*An "Application Environment" you can use from anywhere (using Application.Environment) which tells you if you're running Debug or Release##Enjoy!

I built this based on work I've been doing with Tekpub and some other clients, as well as work I've done in the past. I might have gone overboard (or maybe not done enough) - but it's Open Source so I can tweak as needed (or you can - whatever works). It's our attempt at leveraging what we know to help you out.

Blog Logo

Rob Conery

I am the Co-founder of Tekpub.com, Creator of This Developer's Life, an Author, Speaker, and sometimes a little bit opinionated.


Published