Restful routing for ASP.NET MVC 2

posted: March 21st, 2010

Restful routing has a new syntax and better support for ASP.NET MVC 2 areas. Routes are now grouped into route sets and a route set can be composed of multiple route sets using the Connect<RouteSet>(“pathprefix”) method described later.

Routes.cs
public class Routes : RouteSet
{
  public Routes()
  {
    Resources<BlogsController>(() =>
    {
      As("weblogs");
      Only("index", "show");
      Collection("latest", HttpVerbs.Get);

      Resources<PostsController>(() =>
      {
        Except("create", "update", "destroy");
        Resources<CommentsController>(() => Except("destroy"));
      });
    });
  }
}

public class MvcApplication : System.Web.HttpApplication
{
  protected void Application_Start()
  {
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new RestfulRoutingViewEngine());
    
    RouteTable.Routes.MapRoutes<Routes>();
    }
}

This will generate the following routes:

Http Method Path Endpoint (controller#action)
GET weblogs blogs#index
GET weblogs/latest latest
GET weblogs/{id} blogs#show
GET weblogs/{blogId}/posts posts#index
GET weblogs/{blogId}/posts/new posts#new
GET weblogs/{blogId}/posts/{id}/edit posts#edit
GET weblogs/{blogId}/posts/{id} posts#show
GET weblogs/{blogId}/posts/{postId}/comments comments#index
POST weblogs/{blogId}/posts/{postId}/comments comments#create
GET weblogs/{blogId}/posts/{postId}/comments/new comments#new
GET weblogs/{blogId}/posts/{postId}/comments/{id}/edit comments#edit
GET weblogs/{blogId}/posts/{postId}/comments/{id} comments#show
PUT weblogs/{blogId}/posts/{postId}/comments/{id} comments#update

Http method overrides

Http methods can be overridden on POST requests by specifying a form parameter name _method with the value of the method you wanted to use. We need this because you can only use GET and POST as the HTTP method on HTML forms. There are a couple of helper methods available to generate these tags.

Form.ascx
<%= Html.PutOverrideTag() %>
<input type="hidden" name="_method" value="put">

<%= Html.DeleteOverrideTag() %>
<input type="hidden" name="_method" value="delete">

Resources and Resource

Resources maps a collection of resources and Resource maps a singleton resource. You would use the singleton resource mapping for resources that the user only has one of. Session is a typical example of this.

Routes.cs
Resource<SessionsController>(() =>
{
  Resource<AvatarsController>();
});

Note the controller name is still plural, as the application handles many of them, it is just the path to the resource that is singular. This produces the following routes:

Http Method Path Endpoint (controller#action)
GET session sessions#show
POST session sessions#create
GET session/new sessions#new
GET session/edit sessions#edit
PUT session sessions#update
DELETE session sessions#destroy
GET session/avatar avatars#show
POST session/avatar avatars#create
GET session/avatar/new avatars#new
GET session/avatar/edit avatars#edit
PUT session/avatar avatars#update
DELETE session/avatar avatars#destroy

Areas

Routes.cs
Area<Controllers.Admin.BlogsController>("admin", () =>
{
  Resources<BlogsController>(() =>
  {
    Resources<PostsController>(() =>
    {
      Resources<CommentsController>();
    });
  });
});

This maps an area called admin with the controllers located in the Controllers.Admin namespace. You can specify that the whole RouteSet is to be put into an area without putting a path prefix on the mapping. The following would just constrain all the routes to the namespace of BlogsController, but still look for views based on the area name blogs.

Routes.cs
Area<BlogsController>("blogs");

Resources<BlogsController>();

If you want to nest mappings under a specific path you can use a non-namespaced area. This will just prepend admin/ to the blogs resource, without constraining to a namespace of controllers.

Routes.cs
Area("admin", () => Resources<BlogsController>());

To generate URL’s for area’s you need to specify the are name

New.aspx
Url.Action("index", "blogs", new { area = "admin "}, new {})

Connecting route sets together

You can connect routes together using the Connect method.

Routes.cs
public class BlogRoutes : RouteSet
{
  public BlogRoutes()
  {
    Area<BlogsController>("blogs");
    Resources<BlogsController>();
    Resources<PostsController>();
  }
}

public class Routes : RouteSet
{
  public Routes()
  {
    Connect<BlogRoutes>(""); // maps the BlogRoutes route set.
  }
}

// Application_Start()
RouteTable.Routes.MapRoutes<Routes>();

RestfulRoutingViewEngine

By default the WebformViewEngine looks for area views in this format: Areas/{area}/Views/{controller}/{action}. The RestfulRoutingViewEngine looks for views in this format: Views/{area}/{controller}/{action}. Using the RestfulRoutingViewEngine is completely optional.

Member and collection actions

Member actions are those which are to be performed on a member of the collection, and include the ID in the path. Collection actions are mapped against the collection and do not include the ID in the generated route.

Routes.cs
Resources<BlogsController>(() =>
{
  Member("print", HttpVerbs.Get); // blogs/{id}/print
  Collection("latest", HttpVerbs.Get); // blogs/latest
});

Standard mappings

A way to specify standard mappings is also provided.

Routes.cs
Map("posts/{year}/{slug}").To<PostsController>(x => x.Post(-1, "")).Constrain("slug", @"\w+").Constrain("year", @"\d+");

You can also add custom routes.

Routes.cs
Route(new Route("posts/{action}", new RouteValueDictionary(new { controller = "posts" }), new MvcRouteHandler());

The code is on GitHub and there is a sample project in there with a few examples in it.

Automatic Rails-like console using ActiveRecord

posted: March 15th, 2010

If you are already using active record for migrations then it’s pretty easy to add a console task which behaves like the script/rails command in Rails 2. ActiveRecord reads information about the database and uses it to dynamically define methods on your objects for accessing the associated columns in the table. It does this for all classes that inherit from ActiveRecord::Base. We can take this a step further by dynamically generating classes that inherit from ActiveRecord::Base and use some simple conventions to map a basic domain model.

lib/console.rb
require 'sqlite3'
require 'active_record'

ENV['RAILS_ENV'].nil? ? RAILS_ENV = 'development' : RAILS_ENV = ENV['RAILS_ENV']

@logger = Logger.new STDOUT
config = YAML::load(IO.read('db/database.yml'))
ActiveRecord::Base.logger = @logger
ActiveRecord::Base.logger.level = Logger::ERROR
config[RAILS_ENV]["database"] = config[RAILS_ENV]["database"] if config[RAILS_ENV]["adapter"] == "sqlite3" and RAILS_ENV == 'development'
ActiveRecord::Migration.verbose = true
ActiveRecord::Base.configurations = config
ActiveRecord::Base.establish_connection(config[RAILS_ENV])
ActiveRecord::Base.colorize_logging = false
RAILS_ROOT = Dir.pwd

excluded_tables = %w{schema_migrations sqlite_sequence sysdiagrams}

data = []

ActiveRecord::Base.connection.tables.reject{|t| excluded_tables.include? t}.each do |table|
  code = "class #{table.camelize} < ActiveRecord::Base; set_table_name "#{table}\"; set_primary_key \"Id\"; "
  eval "#{code} end"
  has_manys = []
  table.camelize.constantize.columns.map{|m|m.name}.select{|m|m.ends_with? "_id"}.each do |col|
    col = col.gsub(/_id/,"")
    code << "belongs_to :#{col};"
    has_manys << col
  end
  code << "end"
  eval code

  has_manys.each do |hm|
    code = "class #{hm.camelize} < ActiveRecord::Base;"
    code << "has_many :#{table.tableize}, :class_name => '#{table}', :foreign_key => '#{hm}_id', :primary_key => 'Id';"
    code << "end\n"
    eval code
  end
end

Quick and dirty, but it does the job. This maps a domain model for a database that doesn’t use the same conventions ActiveRecord uses. You should now be able to start an irb session, load the console file and have access to the dynamically generated ActiveRecord classes. Below is the command wrapped up in a rake task.

Rakefile
task :console do
  sh "irb -rubygems -I lib -r lib/console"
end

I’d also recommend installing Hirb and enable it by default in your ~/.irbrc file.

gem install hirb
~/.irbrc
require 'rubygems'
require 'hirb'
Hirb.enable

I’ve updated the activerecord-migrator-standalone repository on github to use this if you want to quickly try it out.

Command line
C:\code\activerecord-migrator-standalone>rake console
(in C:/code/activerecord-migrator-standalone)
irb -rubygems -I lib -r lib.console
irb(main):001:0>Page.create! :Name => 'test'
+----+------+------+-----+
| Id | Name | Slug | Url |
+----+------+------+-----+
| 1  | test |      |     |
+----+------+------+-----+
1 row in set
irb(main):002:0>Page.first.content_items.create :Data => 'd'
+----+------+------+---------+
| Id | Name | Data | Page_id |
+----+------+------+---------+
| 1  |      | d    | 1       |
+----+------+------+---------+
irb(main):003:0>ContentItem.all
+----+------+------+---------+
| Id | Name | Data | Page_id |
+----+------+------+---------+
| 1  |      | d    | 1       |
+----+------+------+---------+

Speed up your git workflow with bash aliases

posted: January 27th, 2010

Git allows you to define aliases under after the git command, so you could alias git s to git status. Bash allows you to define aliases at the shell level, so you could alias gs to git status.

Ryan Bates’ dotfiles repository contains some useful aliases that I’ve been using on my mac for a while now, and have only recently got them hooked up on my windows machine at work. Paste the following into your .bashrc file, located in c:\Users\username\

# cd
alias ..='cd ..'

# ls
alias ls="ls -F"
alias l="ls -lAh"
alias ll="ls -l"
alias la='ls -A'

# git
alias gl='git pull'
alias gp='git push'
alias gpom='git push origin master'
alias gd='git diff'
alias gc='git commit'
alias gca='git commit -a'
alias gco='git checkout'
alias gb='git branch'
alias gs='git status'
alias grm="git status | grep deleted | awk '{print \$3}' | xargs git rm"
alias pull='git pull origin master'

# rails
alias sc='script/console'
alias ss='script/server'
alias sg='script/generate'
alias a='autotest -rails'
alias tlog='tail -f log/development.log'
alias scaffold='script/generate nifty_scaffold'
alias migrate='rake db:migrate db:test:clone'
alias rst='touch tmp/restart.txt'{% endhighlight %}

Then whenever you’re in a git bash shell on Windows, you can use these handy shortcut commands to speed up your workflow.

Using ActiveRecord Migrator standalone with SQLite and SQLServer on Windows

posted: November 14th, 2009

I have recently started using ActiveRecord’s Migrator outside of Rails, in my .NET projects. This is how you can do the same.

Ruby and Rails

Follow the instructions on rubyonrails.org for installing ruby and rails, then download the sqlite binary from sqlite.org (choose the one without TCL bindings) and put sqlite3.dll in c:\Ruby\bin. Next run gem install sqlite3-ruby to install the sqlite driver for ruby.

ActiveRecord SQLServer adapter and the ruby ADO driver

SQLServer is not commonly used in the rails community and ActiveRecord doesn’t have a built in SQLServer adapter. If you have already tried to get SQLServer working with ActiveRecord it’s probably best to uninstall the previous adapters you have tried.

gem install activerecord-sqlserver-adapter --source=http://gemcutter.org

A few older blog posts tell you to add the ruby-dbi ADO.rb driver to your ruby installation, thankfully the driver is included with this gem so that hack is no longer necessary. You should be able to use SQLServer with ActiveRecord now.

Using the migrator with Rake

Rails’ comes with a set of rake tasks to run migrations and it was quite easy to pull the tasks out and use them on their own. I have a repository on GitHub with the tasks and a few migrations to test it’s working. Once you have cloned the repository, run rake db:migrate to test the migrator using SQLite.

Create a database called activerecord_migrator_standalone on your local sql express instance, make sure the connection details are correct in db/database.yml and run rake db:migrate RAILS_ENV=production to test it with the sqlserver adapter. Below are the connection details that worked for me.

production:
  adapter: sqlserver
  database: activerecord_migrator_standalone
  host: .\sqlexpress
  username: sa
  password: your_password
  mode: ADO
Or use Windows authentication:
production:
  adapter: sqlserver
  mode: odbc
  dsn: Driver={SQL Server};Server Name=server_name;Database=activerecord_migrator_standalone;Integrated Security=True;

Bootstrapping an existing project

If you are adding database migrations to an existing project and would prefer not to write the initial migration by hand, you’re in luck because ActiveRecord comes with a schema dumper that you can use to get a dump of your current database schema in migration format.

rake db:schema:dump

This will attempt to dump the schema of the current database state in migration format to the file db/schema.rb. It does this after every db:migrate so the schema.rb file is always an up to date copy of your database schema. You can configure it to use SQL if you prefer.

Copy everything inside the ActiveRecord::Schema.define(:version => 0) do block into your first migration file migrate/1_bootstrap.rb. If you run rake db:migrate on the database now it will attempt to run all the migrations and fail because the tables are already there. To get around this you can add return if table_exists? "SomeExistingTable" to the beginning of the first migration so that it skips it if one of the tables are there and treats it as completed. This makes it easier to upgrade existing databases to use migrations.

You can find out more about activerecord and migrations on the ruby on rails guides site.

Namespaces now supported in restful-routing

posted: October 18th, 2009

Namespaces are now supported in restful-routing, and you can choose to separate these namespaces into areas for a more modular application. As blogged about last year by Phil Haack and Steve Sanderson, areas allow you to split your application up into groups of controllers. These areas can have controllers with the same name as controllers in other areas. You can also choose to put their views in a different sub directory based on area.

This is how you map a namespace in restful-routing, without creating an area:

C#
var map = new RestfulRouteMapper(RouteTable.Routes);

map.Namespace("admin", m =>
{
  m.Resources<BlogsController>();
  m.Resources<PostsController>();
});

// NOTE: this is equivalent to
map.Resources<BlogsController>(config => config.PathPrefix = "admin");
map.Resources<PostsController>(config => config.PathPrefix = "admin");

The only benefit is a DRYer syntax, you only specify admin once. If you want to map an area you need to specify the namespace to constrain it to.

C#
var configuration = new RouteConfiguration {Namespaces = new[] {typeof (BlogsController).Namespace}};

var map = new RestfulRouteMapper(RouteTable.Routes, configuration);

// this is mapped using the default namespaces defined above
map.Resources<BlogsController>(m => m.Resources<PostsController>());

map.Namespace("admin", typeof(Controllers.Admin.BlogsController).Namespace, m =>
    {
    m.Resources<Controllers.Admin.BlogsController>();
    m.Resources<Controllers.Admin.PostsController>();
    });

This allows you to use controllers with the same name because the namespace is used to identify which controller to use. Now the problem here is that both blogs controllers will look in the same place for their views. I have included a modified version of Phil’s AreaViewEngine to get around this.

The AreaViewEngine enables views to be split up by area, so the area location Views/{area}/{controller}/{action} will take precedence over the default Views/{controller}/{action}. I have updated the sample project to work like this. It’s good to know that ASP.NET MVC 2 will include built-in area support.

RESTful routes for ASP.NET MVC

posted: October 11th, 2009

Update: This post refers to an old version of restful routing, please see this updated post for a more recent reference.

Rails’ RESTful routing is something I miss when developing for ASP .NET MVC. The style it imposes on your application architecture has many benefits. I wanted this style for my ASP.NET MVC apps. I started looking at Adam Tybor’s SimplyRestfulRouting handler in MvcContrib. This works well for simple applications, but it doesn’t have a lot of the features/configuration options rails’ routing does. I needed more, so I decided to build my own.

RestfulRouting

RestfulRouting is based on Rails’ RESTful routing and has nested routes, shallow nested routes, singleton resources and a lot of configuration options. You map a resource like this:

C#
var map = new RestfulRouteMapper(RouteTable.Routes);

map.Resources<BlogsController>();

This will map a blogs resource with the following routes.

Path Default Action Constraints
blogs index GET
blogs create POST
blogs/new new GET
blogs/{id}/action show GET, action = show,edit or delete
blogs/{id} update PUT
blogs/{id} destroy DELETE

It also includes a route to accept post overrides, which will let you specify put or delete on a POST request to a resource and have it map to update or destroy . This is needed because HTML forms only support POST or GET.

Configuration

C#
// maps it to admin/blogs:
map.Resources<BlogsController>(config => config.PathPrefix = "admin"); 

// maps the resource as weblogs instead of blogs:
map.Resources<BlogsController>(config => config.As = "weblogs");

// only accepts integer ids
map.Resources<BlogsController>(config => config.IdValidationRegEx = @"\d+");

// adds an extra collection action blogs/latest GET
map.Resources<BlogsController>(config => config.AddCollectionRoute("latest");

// excludes the delete route from the mappings
map.Resources<BlogsController>(config => config.Except("delete");

// only maps the index and show actions
map.Resources<BlogsController>(config => config.Only("index", "show"));

// adds an extra member action blogs/1/moveup that only accepts POSTs
map.Resources<BlogsController>(config => config.AddMemberRoute("moveup", HttpVerbs.Post));{% endhighlight %}</div>

Nested Resources

Resources can be nested

C#
map.Resources<BlogsController>(m => {
  m.Resources<PostsController>(p => {
    p.Resources<CommentsController>();
  })
});

So a comment path would look like this

blogs/1/posts/2/comments/3

Shallow nested resources

If you don’t want to nest 3 levels down you can specify the shallow option.

C#
map.Resources<BlogsController>(config => config.Shallow = true, m => {
  m.Resources<PostsController>(p => {
    p.Resources<CommentsController>();
  })
});

This maps the index of posts under a blog resource and at the root. You would have paths like this

blogs/1/posts
posts/1
posts/1/comments
comments/1

A more complex example

Route mappings for a case study, which has many images and videos. The CaseStudyImagesController is mapped as images and has a member action to get a thumbnail. The path for a case study thumbnail would be casestudies/2/images/3/thumb.

C#
var map = new RestfulRouteMapper(routes);

map.Resources<CaseStudiesController>(study => {
  study.Resources<CaseStudyImagesController>(config => {
    config.As = "images";
    config.AddMemberRoute("thumb");
  });
  study.Resources<CaseStudyVideosController>(config => config.As = "videos");
});

More info and a sample project can be found on the GitHub project page.