For a multi tenant application in ASP.NET MVC 5, I have created a custom IRouteConstraint to check if a subdomain exists in the base url, like client1.myapplication.com or client2.application.com.
public class TenantRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string appSetting = ConfigurationManager.AppSettings[AppSettings.IsMultiTenancyEnabled];
bool isMultiTenancyEnabled = false;
bool isParsedCorrectly = bool.TryParse(appSetting, out isMultiTenancyEnabled);
if (isMultiTenancyEnabled)
{
string subdomain = httpContext.GetSubdomain();
if (subdomain != null && !values.ContainsKey("subdomain"))
{
values.Add("subdomain", subdomain);
}
return string.IsNullOrEmpty(subdomain) ? false : true;
}
else
{
return true;
}
}
}
Here is the route config setup:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreWindowsLoginRoute();
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "Dime.Scheduler.WebUI.Controllers" },
constraints: new { TenantAccess = new TenantRouteConstraint() }
);
}
}
The route constraint works very well but I need to understand this process better. I want to know what happens exactly when this method below returns FALSE. The end result is a HTTP Error 403.14 - Forbidden page, but is there some way I can intercept this to present my own custom page? I usually capture these errors in the Global Asax file but in this case, it never gets there.
Could this have something to do with the fact that there won't be any routes that match the request? Is there any way to redirect to a custom page if no matches are found?
After any route constraint that is associated with the route returns false, the route is considered a non-match and the .NET routing framework will check the next route registered in the collection (the matching is performed in order from the first route down to the last route that is registered in the collection).
The end result is a HTTP Error 403.14 - Forbidden page, but is there some way I can intercept this to present my own custom page?
You can get more precise control over routing by inheriting RouteBase (or inheriting Route). There is a pretty good example of domain-based routing here.
The key to doing this is to make sure you implement both methods. GetRouteData is where you analyze the request to determine if it matches and return a dictionary of route values if it does (and null if it doesn't). GetVirtualPath is where you get a list of route values, and if they match you should (typically) return the same URL that was input in the GetRouteData method that matched. GetVirtualPath is called whenever you use ActionLink or RouteLink within MVC, so it is usually important that an implementation be provided.
You can determine the page that the route will direct to by simply returning the correct set of route values in GetRouteData.
Related
I am creating an ASP.NET MVC application which will be shown in clients like WindowsPhone, iOS, Android apps in a WebView control or iFramed in other websites. I want to control some behavior and styles differently for my ASP.NET application depending on the client requesting my site.
One way I can achieve this is passing this information through headers like ClientType=WindowsPhone etc. and reading the header value where ever I need. But I am thinking if there is a clean way I can pass this information in URL. Something like
https://www.example.com/windowsphone/Manage/Index
https://www.example.com/ios/Manage/Index
and read the client type from the URL where ever needed in my code.
I was confused if RouteConfig can help me here.
I was able to do it the following way,
routes.MapRoute(
name: "Default",
url: "{clientType}/{controller}/{action}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { clientType = new ClientTypeConstraint() });
and my ClientTypeConstraint looked like the following
public class ClientTypeConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// Validate routeValueDictionary["clientType"] here
}
}
I want to return a HTTP status 404 if invalid arguments are passed to my controller. For example if I have a controller that looks like:
public ActionResult GetAccount(int id)
{
...
}
Then I want to return a 404 if say urls such as these are encountered:
/GetAccount
/GetAccount/notanumber
i.e. I want to trap the ArgumentException that is thrown.
I know I could use a nullable type:
public ActionResult GetAccount(int? id)
{
if(id == null) throw new HttpException(404, "Not found");
}
But that's pretty icky and repetitious.
I was hoping I could add this to my controllers where necessary:
[HandleError(View="Error404", ExceptionType = typeof(ArgumentException))]
public class AccountsController : Controller
{
public ActionResult GetAccount(int id)
{
...
}
}
But that doesn't appear to work well.
I saw this post and this answer which nearly solves my problem:
In that answer an abstract BaseController is created from which you derive all your other controllers from:
public abstract class MyController : Controller
{
#region Http404 handling
protected override void HandleUnknownAction(string actionName)
{
// If controller is ErrorController dont 'nest' exceptions
if (this.GetType() != typeof(ErrorController))
this.InvokeHttp404(HttpContext);
}
public ActionResult InvokeHttp404(HttpContextBase httpContext)
{
IController errorController = ObjectFactory.GetInstance<ErrorController>();
var errorRoute = new RouteData();
errorRoute.Values.Add("controller", "Error");
errorRoute.Values.Add("action", "Http404");
errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
errorController.Execute(new RequestContext(
httpContext, errorRoute));
return new EmptyResult();
}
#endregion
}
This works great at handling unknown actions with a 404 but doesn't allow me to handle invalid data as a 404.
Can I safely override Controller.OnException(ExceptionContext filterContext) like this:
protected override void OnException(ExceptionContext filterContext)
{
if(filterContext.Exception.GetType() == typeof(ArgumentException))
{
filterContext.ExceptionHandled = true;
this.InvokeHttp404(filterContext.HttpContext);
}
else
{
base.OnException(filterContext);
}
}
On the surface it seems to work, but am I storing up any problems by doing this?
Is this semantically correct thing to do?
Best way? Action method selector attribute!
To actually avoid nullable method arguments I suggest that you write an Action Method Selector attribute that will actually only match your action method when id is supplied. It won't say that argument wasn't supplied but that it couldn't match any action methods for the given request.
I would call this action selector RequireRouteValuesAttribute and would work this way:
[RequireRouteValues("id")]
public ActionResult GetAccount(int id)
{
...
}
Why is this the best solution for your problem?
If you look at your code you'd like to return a 404 on actions that match name but parameter binding failed (either because it wasn't supplied or any other reason). Your action so to speak requires particular action parameter otherwise a 404 is returned.
So when adding action selector attribute adds the requirement on the action so it has to match name (this is given by MVC) and also require particular action parameters. Whenever id is not supplied this action is not matched. If there's another action that does match is not the issue here because that particular action will get executed. The main thing is accomplished. Action doesn't match for invalid route request and a 404 is returned instead.
There's an app code for that!
Check my blog post that implements this kind of attribute that you can use out of the box. It does exactly what you're after: it won't match your action method if route data provided doesn't have all required values.
Disclaimer: this does not cover all the cases
For urls in your examples, returning 404 can be done in single line. Just add route constraint for id parameter.
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index" }, // Parameter defaults
new { id = #"\d+" } // restrict id to be required and numeric
);
And that's all. Now any matching url that has no id or id is not numeric, autimatically triggers not found error (for which there are plenty of ways to handle, one in your example, another by using custom HandleErrorAttribute, etc). And you can use non-nullable int parameters on your actions.
I managed to get this working by adding this route at the end of all routes:
routes.MapRoute("CatchAllErrors", "{*url}",
new { controller = "Error", action = "NotFound" }
);
Note: First I followed this: How can I properly handle 404 in ASP.NET MVC?
I would like to create a simple route which will allow me to have ONLY one item listed after the base URL (other than when it's a controller), and for that item to be passed into a controller as a parameter. for example:
www.mydomain.com/user1
www.mydomain.com/user2
www.mydomain.com/user3
www.mydomain.com/user3
where user1, user2 etc are usernames and are being passed dynamically, ie i don't want to have to create a controller for each username.
naturally i would want to make sure if something like this is possible that it wont cause conflicts with other genuine controller names, thus i guess the other controllers would have to have rules created specifically for them and listed above the wildcard route
i'm not sure how to do this as i guess the first item after the slash is usually a controller.
any ideas how to tackle this?
the examples i provided may seem ambiguous, when i put www.mydomain.com/user1 etc it represents that it could be anything (ie a username),for example, www.mydomain.com/jsmith, www.mydomain.com/djohnson, www.mydomain.com/sblake, www.mydomain.com/fheeley
the idea is that a users profile can be looked up simply by entering the domain name then a fwd slash and the username.
ASP.Net MVC routes are process from the top down, and as soon as a match is found it won't look any further for a match. So put your most specific routes first, and your wildcard route last. If none of the specific routes match, control will be passed to the wildcard route.
Use a route definition such as
routes.MapRoute("{username}", new { controller = "User", action = "View"});
in your global.asax.cs file but put it last in your list of route definitions.
MVC processes your route mappings from top to bottom, so put your most general route mappings at the top, and your more specific ones at the bottom.
After you do this you will need the corresponding controller/action so create a new Controller named "UsersController" with an action named "View", like as follows:
using System.Web.Mvc;
namespace Routes.Controllers
{
public class UsersController : Controller
{
//
// GET: /{username}
public ActionResult List(string username)
{
// Get User from database/repository and pass to view
var user = ...;
return View(user);
}
}
}
You have to do following thing.
Create one controller or Action for Handle above scenario. For example controller="Home" and Action="GetUser"
public ActionResult GetUser(string username){
// do your work
}
In Global.asax ( Top route)
// Look at point 3 for UserNameConstraint
route.MapRoute("UserRoute" , "{username}" , new { controller="Home" , action="GetUser" } , new { username = new UserNameConstraint() });
// Put your default route after above here
Now for Create Route constraint.
Hope this help you.
public class UserNameConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
List<string> userName = GetUserName from DB
userName.Contain(values[parameterName].ToString());
}
}
Create a route like as follows,
routes.MapRoute(
"DefaultWithUser", // Route name
"{user}/{controller}/{action}/{id}", // URL with parameters
new { user=UrlParameter.Optional, controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
I am using ASP.NET MVC to develop an application framework. Essentially, the end goal is to be able to log into an admin interface, create a new tenant with custom settings, enable the modules they want (blog, shopping basket, etc)... job done - satisfied customer with new website. I'm not using separate applications because there will be a lot of shared code and it would be easier to maintain this way, and also because it would be pretty easy to bring a new, identical node online at peak times.
Depending on what modules are loaded for the tenant, different routes are applicable for each tenant. As I see it, there are three options:
Have all tenants share the same route collection - however if there are a lot of modules it'll be searching through a lot of routes it doesn't need to, and some modules may well have conflicting routes.
Add the necessary routes for each tenant to the global route collection and extend the route class to look at the domain as well - but this could quickly end up with hundreds of routes as more tenants are added.
Work out what tenant is being accessed first and then only search their own private route collection - this would be ideal, but I've searched for hours and have absolutely no idea how to do it!
So can anyone point me in the correct direction for the third option or explain why either of the first two aren't really that bad?
How will each website be distinguished in your app? If we assume each tenant will be identified by a unique domain name or subdomain name, then you can accomplish your routing with one route and some RouteConstraints. Create two constraints, one for controllers, the other for actions. Assuming that you will have tables in your database which list the available controllers/actions for a specific tenant, your constraints would be as follows:
using System;
using System.Web;
using System.Web.Routing;
namespace ExampleApp.Extensions
{
public class IsControllerValidForTenant : IRouteConstraint
{
public IsControllerValidForTenant() { }
private DbEntities _db = new DbEntities();
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// determine domain
var domainName = httpContext.Request.Url.DnsSafeHost;
var siteId = _db.Sites.FirstorDefault(s => s.DomainName == domainName).SiteId;
// passed constraint if this controller is valid for this tenant
return (_db.SiteControllers.Where(sc => sc.Controller == values[parameterName].ToString() && sc.SiteId == siteId).Count() > 0);
}
}
public class IsActionValidForTenant : IRouteConstraint
{
public IsActionValidForTenant() { }
private DbEntities _db = new DbEntities();
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// determine domain
var domainName = httpContext.Request.Url.DnsSafeHost;
var siteId = _db.Sites.FirstorDefault(s => s.DomainName == domainName).SiteId;
// passed constraint if this action is valid for this tenant
return (_db.SiteActions.Where(sa => sa.Action == values[parameterName].ToString() && sa.SiteId == siteId).Count() > 0);
}
}
}
Then, in Global.asax.cs, define your route as follows:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new { controller = new IsControllerValidForTenant(), action = new IsActionValidForTenant(),}
);
}
When a request comes in, the constraints will examine whether the controller and action are valid for the domain, so that only valid controllers and actions for that tenant will pass the RouteConstraints.
I am working on a very simple application, using MVC2 Preview 1.
I have a controller named ContentController. My problem is that /Content/Index works correctly, but /Content/ returns a 404. I am running the application on the Studio Development Server.
Tested with RouteDebugger but /Content/ returns a 404, and does not display any debugging information.
I have not changed the routing code:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
This is my controller:
public class ContentController : Controller
{
IRepository _repo = new SimpleRepository("db", SimpleRepositoryOptions.RunMigrations);
public ActionResult Index()
{
var content = _repo.GetPaged<Content>(0, 20);
return View(content);
}
It's a shot in the dark, but do you have a directory named /Content/ as well?
/Content is a controller, which is basically just a collection of actions. ASP.NET MVC needs to know WHICH action you want to run, so by leaving out the action asp.net mvc doesn't know what action to return and gives a 404.
You can tell it a default either by adding a route:
eg:
routes.MapRoute("ContentDefault", "Content", new {controller = "Content", action = "Index"});
The attributes are defined as follows:
'ContentDefault`: Name of the Route (must be unique in your routing table)
Content: The URL segment (try changing this to 'Content/Much/Longer/URL' and then go to http://localhost/Content/Much/Longer/URL to see how this works)
new {controller=.., action=...}: which controller/action combo to run for this route.
You could also override HandleUnknownAction in your controller:
protected override void HandleUnknownAction(string actionName)
{
return RedirectToAction("index");
}
Oh and incidentally, an extra piece of advice about routing.... if you add something to the route in braces { } these will be passed to the action as an attribute.
e.g. /Content/Much/Longer/Url/{page}
so the URL http://localhost/Content/Much/Longer/Url/999
will pass the 999 into your action, as the page attribute
public ActionResult Index(int Page) { }
I love MVC - never going back to WebForms - this is how web development should be!