Hi,
I wanted to create an application that will allow users to pick there own urls just like twitter (http://twitter.com/anativ). It seems that there is a very simple way for doing it using MVC.
I saw many solution to this problem that catch the 404 and handle it. I personally didn’t like that solution it was too complicated and had many small issues that you need to solve.
Here is the sample controller that handle the custom urls:
public class UsersController : Controller
{
private static HashSet<string> _pages = new HashSet<string>();
public ActionResult Add(string id)
{
_pages.Add(id);
string msg = "Page added " + id;
if (string.IsNullOrWhiteSpace(id))
{
msg = "The site name can't be empty...";
}
ViewBag.Message = msg;
return View();
}
public ActionResult Get(string id)
{
if (_pages.Contains(id))
{
ViewBag.Message = "Welcome " + id;
return View();
}
return RedirectToRoute("Error","NotFound");
}
}
The idea is to support customize urls without losing the default routing mechanism (controller/action/id).
First try:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
routes.MapRoute(
"UserPage", // Route name
"{id}", // URL with parameters
new { controller = "Users", action = "Get" } // Parameter defaults
);
So how do you do that? lets see the problem… if we create a “UserPage” route like the example above we will never catch it because the “Default” MVC route will catch all the patterns before it. If we will change the order that the “UserPage” route will be before the default route the “UserPage” route will catch everything before the “Default” route.
Lets see how we can solve the problem… In order to do that we need to understand what is the job of the “Default” route. The MVC “Default” route catches 3 types of request:
- Empty request: the empty request –> handled by /Home/Index
- Controller + method –> handled by {controller}/{index}
- Controller with empty request –> handled by {controller}/Index
So lets split the controller into rules that will answer all types of the requests above:
for rule number 1 we will create a rule:
routes.MapRoute(
"Home", // Route name
"", // URL with parameters
new { controller = "Home", action = "Index" } // Parameter defaults
);
for rule number 2 we will create a rule:
routes.MapRoute(
"Controller_Action", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { id = UrlParameter.Optional } // Parameter defaults
);
This rule is almost the same as the “Default” MVC route the only deference is the “Parameter defaults” section this rule must get a controller name and an action name.
Now to solve number 3 we need to do a small trick. I created a custom rule for each controller in my project – you can do it manually or you can do it with reflection. I added the method below to my global.asax file it runs on every controller that I have in my project and creates a rule for it (controller/Index).
private static IEnumerable<Route> GetDefaultRoutes()
{
//My controllers assembly (can be get also by name)
Assembly assembly = typeof (HomeController).Assembly;
// get all the controllers that are public and not abstract
var types = assembly.GetTypes().Where(t => t.IsSubclassOf(typeof (Controller)) && t.IsPublic && !t.IsAbstract);
// run for each controller type
foreach (var type in types)
{
//Get the controller name - each controller should end with the word Controller
string controller = type.Name.Substring(0, type.Name.IndexOf("Controller"));
// create the default
RouteValueDictionary routeDictionary = new RouteValueDictionary
{
{"controller", controller}, // the controller name
{"action", "index"} // the default method
};
yield return new Route(controller,routeDictionary, new MvcRouteHandler());
}
}
Now we will need to do is add the call for this method in the RegisterRoutes method just above the last rule
foreach (var route in GetDefaultRoutes())
{
routes.Add(route);
}
That’s it. Now the global.asax looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Home", // Route name
"", // URL with parameters
new { controller = "Home", action = "Index" } // Parameter defaults
);
routes.MapRoute(
"Controller_Action", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { id = UrlParameter.Optional } // Parameter defaults
);
foreach (var route in GetDefaultRoutes())
{
routes.Add(route);
}
routes.MapRoute(
"UserPage", // Route name
"{id}", // URL with parameters
new { controller = "Users", action = "Get" } // Parameter defaults
);
}
Now I can create an url per user and save the default routing logic.
If you don’t want to apply the default rule on all of your controllers you can add an attribute that will “ignore” the controller or change the default action method from “Index” to something else.
Keep Writing, Compiling, and Debugging
Alon Nativ
Hi,
I am creating windows services using Topshelf for a while and I forget how “hard” (not that hard but harder) it was to create a windows service without Topshelf.
Topshelf is a lightweight framework for building Windows services using the .NET framework. The idea is to create a console application and “publishing” it as a service with command line. No more dedicated window service project and an installer.
So, how does it work? here is a simple example:
1. Create new “console application”
2. Create your service class
public class MyService : IService
{
public void Start()
{
Console.WriteLine("Running...");
}
public void Stop()
{
Console.WriteLine("Done!");
}
}
3. write something like the code below: (here we are using topshelf)
static void Main(string[] args)
{
RunConfiguration cfg = RunnerConfigurator.New(x =>
{
x.ConfigureService<MyService>(s =>
{
s.Named("MySampleService");
s.HowToBuildService(service => new MyService());
s.WhenStarted(service => service.Start());
s.WhenStopped(service => service.Stop());
});
x.RunAsLocalSystem();
x.SetDescription("Sample Topshelf Host");
x.SetDisplayName("The service");
x.SetServiceName("MySampleService");
});
Runner.Host(cfg, args);
}
That’s it!
now you have a console application that will run the Start() method when you start it and run the Stop() method when you stop it. Very comfortable when you are in the development process and you want to run or debug your service.
Now to install it or uninstall run the command line:
myservice.exe install
myservice.exe uninstall
Enjoy your new service, I personally wrapped Topshelf that I wont need to copy the code above every time:
I created an interface called IService
public interface IService
{
void Start();
void Stop();
}
And a simple method called ServiceCreator
public static void ServiceCreator<T>(string name, string displayName, string description, string[] args) where T : IService, new()
{
RunConfiguration cfg = RunnerConfigurator.New(x =>
{
x.ConfigureService<MyService>(s =>
{
s.Named(name);
s.HowToBuildService(service => new T());
s.WhenStarted(service => service.Start());
s.WhenStopped(service => service.Stop());
});
x.RunAsLocalSystem();
x.SetDescription(description);
x.SetDisplayName(displayName);
x.SetServiceName(name);
});
Runner.Host(cfg, args);
}
Now my main looks like this:
static void Main(string[] args)
{
ServiceCreator<MyService>("Name", "Display Name", "desciption", args);
}
Hope it will help you to create and debug windows services
Update: MAY-2012
The API has changed a bit, now in order to install the server you need to run "{your_exe} install" and to uninstall "{your_exe} uninstall"
Keep Writing, Compiling, and Debugging
Alon Nativ