What immediately strikes you when you start with ASP.NET MVC is how similar it is to Rails. No one can steal ideas like Microsoft!
Rails | ASP.NET MVC | Purpose (if not obvious) |
---|---|---|
/app/models | /Models | |
/app/controllers | /Controllers | |
/app/views | /Views | |
/public/javascript | /Scripts | |
/public | /Content | |
/db | /App_Data | Database data, such as migrations, and models. |
/test (unit, functional, fixtures, performance) | Separate VS projects | |
/config | /Global.asax, /Properties, Web.config | Configuration |
Project Generation
Both Rails and ASP.NET MVC relies on code generation to get you started, but the methods they use are different.Rails uses the command line, which is natural since the Rails approach is to not rely on anything but a good programming editor and the command line.
$ rails tapir create .gitignore ... create app/controllers/application_controller.rb ... create app/views/layouts create config/database.yml create db/seeds.rb ... create public/javascripts/application.js ... create test/unit ...ASP.NET MVC uses Wizards inside Visual Studio, which is also expected since Microsoft has a long history of IDE-centric application building.
Worth noting is that I didn't really want to use VS Unit Test, since I normally prefer NUnit, but after one hour of googling and testing I gave up and went with VS Unit Test anyway.
Environment
When Rails is installed it comes pre-configured with three different environment, development, test and production. It is as easy as writing RAILS_ENV=test to switch from the default development environment to the test environment. In this area Rails really shines. There is nothing similar in ASP.NET MVC.The Model
Rails Model
Rails comes pre-configured with an ActiveRecord model, if you don't write otherwise. If you want to use something else it is very easy. Supported frameworks are among others, Neo4J, MongoDB, and DataMapper.To create a model in Rails3 you use a command line generator. The generator generates a model, a migration and tests. And you can, of course, choose what kind of model you wish to generate (-o), as well as what kind of testing framework you want to use (-t). Here I just go with the default:
$ rails g model customer name:string email:string invoke active_record create db/migrate/20100419094010_create_customers.rb create app/models/customer.rb invoke test_unit create test/unit/customer_test.rb create test/fixtures/customers.ymlRails comes preconfigured with Sqlite3, and you don't have to do anything to configure the default database. Moreover, the configuration is set up to use three different databases, one for each environment.
Rails, performs all database changes through scripts, migrations. This is invaluable when you want to upgrade a database that is in production, or when multiple developers are changing the same database model. The migration scripts allow seamless migrations between the different databases.
# An example of a migration class CreateCustomers < ActiveRecord::Migration # Called when migrating up to this version def self.up create_table :customers do |t| t.string :name t.string :email t.timestamps end end # Called when migrating down from this version def self.down drop_table :customers end endTo move between the different versions of the database we use the rake db:migrate command.
# Migrate to the latest version $ rake db:migrate # Migrate to a specific version $ rake db:migrate VERSION=20080906120000 # Rollback one version $ rake db:rollback # Rollback three versions $ rake db:rollback STEP=3Read more about migrations in the Migrations Guide
ASP.NET MVC Model
ASP.NET MVC is not a full stack framework and it does not come preconfigured with a model. It is possible to choose between many solutions, such as NHibernate, Entity Framework or LINQ-to-SQL. I choose LINQ-to-SQL because I think it is an elegant, lightweight OR-Mapper, that is easy to work with. Too bad, it isn't prioritized by Microsoft.Unfortunately, there is nothing like migrations in LINQ-to-SQL. So I use the LINQ-to-SQL design tool to design the classes.
You can then create the database from the model. It is also possible to do it the opposite way, to create the database first and then generate the model, but I prefer this way.
// Creating a database from a LINQ-to-SQL DataContext public void CreateDatabase(bool force) { var db = new TapirDataContext(@"c:\tapirdb.mdf"); if (db.DatabaseExists() || force) { Console.WriteLine("Deleting old database..."); db.DeleteDatabase(); } db.CreateDatabase(); }As you understand this is not a viable solution for migrating a production database, but it is good enough (almost) for development and testing, since I can allow myself to recreate the database every time. Once I get a larger database, the time it takes to set it up will force me to go with a better way. The lack of migrations hurts.
Query Language
Rails3 uses AREL, Active Record Relations, and LINQ-to-SQL uses LINQ (surprise!). They are both beautiful solutions and remarkably similar. Both solutions create lazy, composable queries, that are not executed until the latest possible time, when a result is needed. In LINQ, you can see Eric Meijer's Haskell shining through like magic, and in AREL, the beauty of Ruby.# A simple query with AREL User.where(users[:name].eq('Anders')).order('users.id DESC').limit(20)
// The same with C# // Lambda Syntax db.Users.where(u => u.Name == "Anders").orderBy(u => u.Id).Take(20) // LINQ Syntax (from u in db.Users where u.Name == "Anders" orderby u.Id descending select u).Take(20);One thing that you don't get with LINQ-to-SQL is all the methods that are dynamically created, by need, in Rails, such as find_by_name, find_by_name_and_age, etc.
The Controller
In ASP.NET MVC, it is easy to create a controller, you just right-click on the controllers folder and select Add > Controller. You then get a dialog where you can type in the name of the controller and, optionally, if you like to create default methods for the standard CRUD scenario, just select the checkbox.public class CustomersController : Controller { // GET: /Customers/ public ActionResult Index() { return View(); } // GET: /Customers/Details/5 public ActionResult Details(int id) { return View(); } // GET: /Customers/Create public ActionResult Create() { return View(); } // POST: /Customers/Create [HttpPost] public ActionResult Create(FormCollection collection) { try { // TODO: Add insert logic here return RedirectToAction("Index"); } catch { return View(); } } }As you can see the code generated is simple and clear. That is the benefit of MVC, simpler models, views, and controllers.
The automatic generation of code, that will probably be changed later is commonly known as scaffolding.
Scaffolding is a temporary structure used to support people and material in the construction or repair of buildings and other large structures. --WikipediaScaffolding is not meant to be used as is. It is meant to get you started!
In Rails3 you add controllers with a generator. You have a lot more options, below are just a few of them.
$ rails g controller Usage: rails generate controller NAME [action action] [options] Options: -e, [--template-engine=NAME] # Template engine to be invoked # Default: erb -t, [--test-framework=NAME] # Test framework to be invoked # Default: test_unitAs you can see it is possible to choose what template-engine you want to use, and exactly what actions you want to create, the view templates are created automatically with every action you create.
$ rails g controller Customer index create conflict app/controllers/customer_controller.rb create app/controllers/customer_controller.rb route get "customer/create" route get "customer/index" invoke erb create app/views/customer create app/views/customer/index.html.erb create app/views/customer/create.html.erb invoke test_unit create test/functional/customer_controller_test.rb invoke helper create app/helpers/customer_helper.rb invoke test_unit create test/unit/helpers/customer_helper_test.rb If you want to create all the default actions you should instead invoke the scaffold_generator.
$ rails g scaffold_controller Usage: rails generate scaffold_controller NAME [options]It will create all the default actions and the views that go with them.
The generated code is shown below. Again, you see the similarity between the two solutions. Rails, automatically generates support for both HTML and XML, making it easy to consume the data from other clients.
class FishController < ApplicationController # GET /fish # GET /fish.xml def index @fish = Fish.all respond_to do |format| format.html # index.html.erb format.xml { render :xml => @fish } end end # GET /fish/1 # GET /fish/1.xml def show @fish = Fish.find(params[:id]) respond_to do |format| format.html # show.html.erb format.xml { render :xml => @fish } end end # GET /fish/new # GET /fish/new.xml def new @fish = Fish.new respond_to do |format| format.html # new.html.erb format.xml { render :xml => @fish } end end # GET /fish/1/edit def edit @fish = Fish.find(params[:id]) end # POST /fish # POST /fish.xml def create @fish = Fish.new(params[:fish]) respond_to do |format| if @fish.save format.html { redirect_to(@fish, :notice => 'Fish was successfully created.') } format.xml { render :xml => @fish, :status => :created, :location => @fish } else format.html { render :action => "new" } format.xml { render :xml => @fish.errors, :status => :unprocessable_entity } end end end ... end
Filters
In Rails it is easy to create filters, that can be applied to specific actions.class ItemsController < ApplicationController before_filter :require_user_admin, :only => [ :destroy, :update ] before_filter :require_user, :only => [ :new, :create] endIn ASP.NET MVC, the same thing can be accomplished by overriding OnActionExecuting, in the controller.
override void OnActionExecuting(ActionExecutingContext filterContext) { var action = filterContext.ActionDescriptor.ActionName; if (new List<string>{"Delete", "Edit"}.Contains(action)) { RequireUserAdmin(); } if ("Create".Equals(action)) { RequireUserAdmin(); } }The functionality may also be extracted into an ActionFilter, which can be applied to the controller or an individual method via an Attribute.
[RequireUserAdmin("Delete", "Edit")] [RequireUser("Create")] public class CustomersController : ControllerThis is a pattern that can be seen everywhere, when Rails uses Class Macros, a term from Meta-Programming Ruby, ASP.NET MVC uses attributes. And this is appropriate since it usually constitutes meta-data.
Routing
The Routing in both Rails and ASP.NET MVC is incredibly flexible.Rails Routing
In Rails it is possible to put constraints right in the routing table on just about anything.# config/routes.rb Tapir::Application.routes.draw do |map| resources :animals get "customer/index" get "customer/create" match "/:year(/:month(/:day))" => "info#about", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ } match "/secret" => "info#about", :constraints => { :user_agent => /Firefox/ } endAs you can see Rails comes pre-configures with RESTful routing, and it is indeed the recommended way to set up your routes in Rails. To see what the routing result is is easy:
$ rake routes GET /animals(.:format) {:action=>"index", :controller=>"animals"} animals POST /animals(.:format) {:action=>"create", :controller=>"animals"} new_animal GET /animals/new(.:format) {:action=>"new", :controller=>"animals"} GET /animals/:id(.:format) {:action=>"show", :controller=>"animals"} PUT /animals/:id(.:format) {:action=>"update", :controller=>"animals"} animal DELETE /animals/:id(.:format) {:action=>"destroy", :controller=>"animals"} edit_animal GET /animals/:id/edit(.:format) {:action=>"edit", :controller=>"animals"} customer_index GET /customer/index {:controller=>"customer", :action=>"index"} customer_create GET /customer/create {:controller=>"customer", :action=>"create"} /:year(/:month(/:day)) {:year=>/\d{4}/, :month=>/\d{2}/, :day=>/\d{2}/, :controller=>"info", :action=>"about"} /secret {:user_agent=>/Firefox/, :controller=>"info", :action=>"about"}
ASP.NET MVC Routing
ASP.NET MVC Routing is not quite as easy, but everything is possible. The standard routing with paths and parameters works easily out of the box.// Global.asax.cs public class MvcApplication : System.Web.HttpApplication { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); // Constrained route routes.MapRoute( "Product", "Product/{productId}", new {controller="Product", action="Details"}, new {productId = @"\d+" } ); // Constraint // Route with custom constraint, defined below routes.MapRoute( "Admin", "Admin/{action}", new {controller="Admin"}, new {isLocal=new LocalhostConstraint()} ); } ... } public class LocalhostConstraint : IRouteConstraint { public bool Match ( HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection ) { return httpContext.Request.IsLocal; } }As you can see, you have the same power in the ASP.NET MVC Routing, but it requires you to add constraints separately.
Views
In both Rails and ASP.NET MVC, the views are automatically matched in the directory with the same name as the controller, and the file name is named by the action.app/controllers/customer_controller.rb app/views/customer app/views/customer/create.html.erb app/views/customer/index.html.erbIn Visual Studio it is possible to scaffold a view based on a model, just as it is in Rails. Rails is, of course, more flexible, allowing me to scaffold all or some actions at once. In Visual Studio, it is only possible to do one at a time.
Partials
In both Rails and ASP.NET MVC, a partial is a file that contains a part of a HTML file. The file is written in ASP in ASP.NET MVC and in Rails it is possible to pick the templating language of your choice. Default is ERB, but HAML is also a popular choice.<!-- Rails --> <%= render 'form' %>
<!-- ASP.NET MVC --> <% Html.RenderPartial("Form", Model);%>In ASP.NET MVC version 2, the preferred way is actually to use two alternate methods to generate the code. They have the advantage that they are type-safe (if that gives you pleasure) and they keep track of nesting.
<%= DisplayFor("Address", m => m.Address ) %> <%= EditorFor("Address", m => m.Address ) %>(For Rubyist, the m => m.Address is not a hash key-value expression, but an anonymous function.)
Helpers
A helper is similar to a partial, but it is not a template, it's a method that generates code.In Rails you create a helper by creating a simple method.
module ApplicationHelper def label target, text "<label for='#{target}'>#{text}</label>" end endAnd so you can in ASP.NET MVC.
public class LabelHelper { public static string Label(string target, string text) { return String.Format("<label for='{0}'>{1}</label>", target, text); } }
Conclusion
All in all, ASP.NET MVC with LINQ-to-SQL, is not a bad experience. Eric Meijer and friends have managed to overcome a lot of the hurdles of static typing. They also, let go of it in cases, where dynamic typing is obviously better, such as in the routing and when populating objects from forms. The lambda expressions and the type inference also removes a lot of the boilerplate common in statically typed languages.Rails is of course a lot more flexible, and faster to work with. The scaffolding is faster and more flexible, the migrations are, as I said before, invaluable when it comes to move between versions.
But, if I'm thrown out of the dynamic Garden of Eden, I'd rather pick my static apples in Redmond, than in California.