Handling sub-domains in ASP.Net MVC
In SaasApp which is framework for mulituser-multitenant SAAS framework for ASP.Net MVC, what I wanted was that when user types the url without a subdomain my app should display the website where user can get details about the product and plans, select a plan and register an account (in this post account refers to say a company that can have multiple users) with selected plan. Once an account is created their users should be able to go to their selected subdomain like myaccount.[SAAS app domain].com, login and access the product. I wanted the subbdomain “myaccount” to be forwarded as a parameter to required actions in the controller to identify the account I am dealing with.
Demo: http://saasapp.yatendra.com
ASP.Net MVC has a sophisticated and flexible routing mechanism. It allows developers to create their own customized routing system by extending RouteBase class and providing your own implementation of GetRouteData(HttpContextBase) function.
Following is what I have used -
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SaasApp.Utility;
namespace System.Web.Routing
{
public class DomainRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
//get subdomain, the string before first dot in the url
string subdomain = UtilityHelper.GetSubdomain(httpContext.Request.Headers[“HOST”]);
RouteData routeData = new RouteData(this, new MvcRouteHandler());
if (!string.IsNullOrEmpty(subdomain))
{
//url has subdomain
routeData.Values.Add(“account”, subdomain);
string filepath = httpContext.Request.FilePath;
string[] parts = filepath.Split(‘/’);
switch (parts[1].ToLower())
{
case “app”:
routeData.Values.Add(“controller”, “App”);
if (parts.Length > 2)
{
switch (parts[2].ToLower())
{
case “index”:
routeData.Values.Add(“action”, “Index”);
break;
case “about”:
routeData.Values.Add(“action”, “About”);
break;
default:
routeData.Values.Add(“action”, “Index”);
break;
}
}
else
{
routeData.Values.Add(“action”, “Index”);
}
break;
case “account”:
routeData.Values.Add(“controller”, “Account”);
if (parts.Length > 2)
{
switch (parts[2].ToLower())
{
case “login”:
routeData.Values.Add(“action”, “Login”);
break;
case “logout”:
routeData.Values.Add(“action”, “Logout”);
break;
}
}
break;
default:
if (httpContext.Request.IsAuthenticated)
{
httpContext.Response.Redirect(“/App/Index”);
}
httpContext.Response.Redirect(“/Account/Login”);
break;
}
}
else
{
//url does not have subdomain
string filepath = httpContext.Request.FilePath;
string[] parts = filepath.Split(‘/’);
switch (parts[1].ToLower())
{
case “home”:
routeData.Values.Add(“controller”, “Home”);
if (parts.Length > 2)
{
switch (parts[2].ToLower())
{
case “index”:
routeData.Values.Add(“action”, “Index”);
break;
case “plans”:
routeData.Values.Add(“action”, “Plans”);
break;
case “selectplan”:
routeData.Values.Add(“action”, “SelectPlan”);
if (parts.Length > 3)
{
routeData.Values.Add(“planType”, parts[3]);
}
else
{
routeData.Values.Add(“planType”, “1”);
}
break;
case “selectaplan”:
routeData.Values.Add(“action”, “SelectAPlan”);
break;
case “accountcreated”:
routeData.Values.Add(“action”, “AccountCreated”);
if (parts.Length > 3)
{
routeData.Values.Add(“accountName”, parts[3]);
}
else
{
routeData.Values.Add(“accountName”, “”);
}
break;
case “about”:
routeData.Values.Add(“action”, “About”);
break;
default:
routeData.Values.Add(“action”, “Index”);
break;
}
}
else
{
routeData.Values.Add(“action”, “Index”);
}
break;
default:
httpContext.Response.Redirect(“/Home/Index”);
break;
}
}
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//Implement your formating Url formating here
return null;
}
}
}
Compressing HTML output in ASP.Net / ASP.Net MVC
Most modern browsers have capability to get output from webservers in compressed form. This reduces number of bytes to be transferred and thus making transmission faster. Browsers that support this functionality specify supported compression type as value of “Accept-Encoding” attribute for example “Accept-Encoding: gzip, deflate”. gzip and deflate being two most commonly supported compression types. This can be configured at web server level but if you want to do have a lot of control over we server configuration like in shared hosting environment you can implement IHttpModule and set it up for your application as follows -
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.IO.Compression;
namespace MyPackage
{
public class HttpCompressionModule : IHttpModule
{
#region IHttpModule Members
void IHttpModule.Dispose()
{
}
void IHttpModule.Init(HttpApplication context)
{
context.PostAcquireRequestState += new EventHandler(context_PostAcquireRequestState);
context.EndRequest += new EventHandler(context_EndRequest);
}
void context_EndRequest(object sender, EventArgs e)
{
HttpApplication context = sender as HttpApplication;
context.PostAcquireRequestState -= new EventHandler(context_PostAcquireRequestState);
context.EndRequest -= new EventHandler(context_EndRequest);
}
void context_PostAcquireRequestState(object sender, EventArgs e)
{
this.RegisterCompressFilter();
}
private void RegisterCompressFilter()
{
HttpContext context = HttpContext.Current;
if (context.Handler is StaticFileHandler
|| context.Handler is DefaultHttpHandler) return;
HttpRequest request = context.Request;
string acceptEncoding = request.Headers[“Accept-Encoding”];
if (string.IsNullOrEmpty(acceptEncoding)) return;
//if (request.FilePath.EndsWith(“.ashx”)) return;
if (request.FilePath.Contains(“.”))
{
return;
}
acceptEncoding = acceptEncoding.ToUpperInvariant();
HttpResponse response = HttpContext.Current.Response;
if (acceptEncoding.Contains(“GZIP”))
{
response.AppendHeader(“Content-encoding”, “gzip”);
response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
}
else if (acceptEncoding.Contains(“DEFLATE”))
{
response.AppendHeader(“Content-encoding”, “deflate”);
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
}
}
#endregion
}
}
AppHarbor - Heroku like service for ASP.Net
(Migrated from old wordpress blog post dated 2/17/2011)
I recently came across AppHarbor. It looks like another interesting startup to come out of YCombinator. Its a Heroku like service that builds, runs unit tests and deploys your ASP.Net code for you. It works with Git so to upload the code all you need to do is git push. They provide shared and dedicated databases with option of MS SQL server and MySQL. Whats cool is that they provide a free application instance per applicaiton. So you pay only if your app grows to need another instance. Now if you need to test or host your next ASP.Net MVC app you know where to go.
Tonight I plan to play with it and see how it works for an ASP.Net MVC app I have been working on using Subsonic and SQLite. Details later.
jQuery ListCollapse
(Migrated from old wordpress blog post dated 1/9/2011)
This plugin is meant to collapse/expand a list of items that grows beyond certain predefined number of items. It filters and collapses a list when data is bound, added or removed. If number of items are more than specified number n it displays first n items and displays an expand(customizable) link. When a user clicks on this link it displays the entire list and changes this link to collapse(customizable).
(Note: This plugin is based on Collapsorz 1.1 created by Aaron Kuzemchak. I have customized it to be able to be called whenever an item is added or removed and some minor modifications is think were required for dynamic list where items are added and removed on the fly)
Usage
$(listelement).listcollapse();
$(listelement).listcollapse(options);
Options
(All parameters are optional)
toggle
To select elements that you wish to collapse. By default it will collapse all direct children of the list selected.
maximum
Maximum number of elements to show. Collapses all items beyond this number. It defaults to 5.
showText
Text to be shown for the expand link. It defaults to Show.
hideText
Text to be shown for the collapse link. It defaults to Hide.
linkLocation
Whether to display expand/collapse link before or after the selected list. You can choose one of the following settings:
- after—Places the link after the list. This is the default setting.
- before—Places the link before the list.
defaultState
Whether the list should be displayed expanded or collapsed by default.
- collapsed—List will be collapsed by default. This is the default setting.
- expanded—List will be expanded by default.
wrapLink
HTML to wrap around the link.
Load balancing
(Migrated from old wordpress blog post dated 12/22/2010)
I read an interesting article today about load balancing. Its a must read for anyone looking to scale a website or anyone just simple interested in knowing how these high traffic websites scale on the web server side. This documents explains different ways of doing it like hardware based approach and software based approach. It also explains various related factors like cookie persistence and using SSL certs in load balancing. http://www.exceliance.fr/en/ART-2006-making%20applications%20scalable%20with%20LB.pdf
Adding a new volume to EC2 linux instance
(Migrated from old wordpress blog post dated 12/22/2010)
Our postgres EC2 linux instance ran out of disk space so I had to add another volume to the server. Following is what I did - mkfs -t ext3 /dev/sda2 #dont do it if creating from an existing snapshot
echo "/dev/sda2 /disk2 ext3 noatime 0 0" >> /etc/fstab
mkdir /disk2
mount /disk2
df -h #you should now see /disk2 as a new volume
Moving postgres data folder
(Migrated from old wordpress blog post dated 12/22/2010)
Our postgres server ran out of disk space on our EC2 server so we figured we should move the postgres data folder to a new bigger volume. We created an additional volume mounted to /disk2 and then moved the data folder as follows -
1. Login to shell as root. Stop the Postgres if running
$service postgresql-9.0 stop
2. Copy the data folder to new location
$cp -R /var/lib/pgsql/9.0/data /disk2/pgdata/
3. Modify postgres startup script to point to new data directory
In /etc/init.d/postgresql file, change the value of PGDATA variable to new location which is /disk2/pgdata/data
4. Start the db server
$ service postgresql start
Setting up replication in Postgres 9.0
(Migrated from old wordpress blog post dated 12/22/2010)
For picksie I had to set up streaming replication in Postgres 9.0. We have a master server which is used for read/write and a hot standby where we want the data to updated using streaming replication. Following is a summary of what I did
1. Install postgres on both master and standby
2. Allow standby server to connect to the master by editing pg_hba.conf
host replication postgres 192.168.0.20/22 trust
3. Setup streaming on master by updating postgresql.conf
wal_level = hot_standby
max_wal_senders = 5
wal_keep_segments = 32
archive_mode = on
archive_command = ‘cp %p /path_to/archive/%f’
4. Start postgres on master
5. Make base backup on master
$ psql -c “SELECT pg_start_backup(‘label’, true)”
$ rsync -a ${PGDATA}/ standby:/srv/pgsql/standby/ —exclude postmaster.pid
$ psql -c “SELECT pg_stop_backup()”
6. Do authentication and streaming related configuration in postgresql.conf and pg_hba.conf in standby server so that when needed it can be promoted as master with less effort.
7. Set standby server as a hot standby by updating postgresql.conf
hot_standby = on
8. Create recovery.conf with following configuration on standby server in same folder as postgresql.conf
standby_mode = ‘on’
primary_conninfo = ‘host=192.168.0.10 port=5432 user=postgres’
trigger_file = ‘/path_to/trigger’
restore_command = ‘cp /path_to/archive/%f “%p”’
Note: trigger file is the file, presence of which will make the standby server to stop replication and failover
9. Start postgres service on standby and it will start replication
Reference: http://wiki.postgresql.org/wiki/Streaming_Replication
$ $EDITOR postgresql.conf listen_addresses = '192.168.0.10' $ $EDITOR pg_hba.conf # The standby server must have superuser access privileges. host replication postgres 192.168.0.20/22 trust
Interesting projects at work
(Migrated from old wordpress blog post dated 9/21/2010) Cloud, iPad and SAAS are some of the most sought after buzz words of software industry right now. Apart from looking after the IT infrastructure, my work involves projects bridge and picksie that together involve all of these. Bridge is a product targeting enterprise collaboration space, built on my stack of choice for web apps these days ASP.Net/Subsonic/MySQL. We are looking into SAASifying this app and see if we can host it on the cloud using some knowledge that I have gained working on picksie. Picksie is a web app for iPad, hosted on the cloud. My involvement is designing the infrastructure, making sure it is scalable and easily managable. Infrastructure of a consumer web app is really a completely different ballgame compared to enterprise IT infrastructure. So I am really getting to learn alot as picksie moves from demo to beta and from a simple 3 server setup to a more elaborate and load balanced and more elaborate setup.