Defining conditional routes - asp.net

I've been searching for something similar but no luck. I want to build an app that uses different controllers for same urls. Basic idea is like that if a user is logged in as admin he uses lets say admin controller, if user is just a user he uses user controller. This is just an example, basically I want to have a function that decides what controller route takes.
Thank u everyone. Any help is greatly appreciated.
PS
Use of this:
Admin has different UI and options,
Output catching,
Separation of concern

You need to create a RouteConstraint to check the user's role, as follows:
using System;
using System.Web;
using System.Web.Routing;
namespace Examples.Extensions
{
public class MustBeAdmin : IRouteConstraint
{
public MustBeAdmin()
{ }
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return true if user is in Admin role
return httpContext.User.IsInRole("Admin");
}
}
}
Then, before your default route, declare a route for the Admin role, as follows:
routes.MapRoute(
"Admins", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Admin", action = "Index", id = UrlParameter.Optional }, // Parameter default
new { controller = new MustBeAdmin() } // our constraint
);
counsellorben

Related

What happens after IRouteConstraint.Match returns false

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.

MVC 3 wildcard route ie www.mydomain.com/username

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
);

Tenant-specific routes for dynamically loaded modules

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.

Asp.net MVC RenderAction method in multiple controller

I am doing some plugin kind of work using asp.net mvc.
I have two plugins(mvc projects) Say users and home. In both the plugins, I have Home Controllers. Yes the names are home controller in both the plugins(projects). When I build the project the Dlls are copied to the host project (third one).
this is home plugin
namespace Plugin.Home.Controllers
{
public class HomeController : Controller
{
public string Index()
{
return "Home from home";
}
public string JustATest()
{
return "Just a test from home";
}
}
}
Here is another controller in different project(User Plugin)
this is home controller
namespace Plugin.Users.Controllers
{
public class HomeController : Controller
{
public string Index()
{
return "Home from Users";
}
public string JustATest()
{
return "Just a test from Users";
}
public string JustAnotherTest()
{
return "Just another test from Users";
}
}
}
In global.asax I have register routes using namespaces.
routes.MapRoute(
"Default", // Route name
"Home/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "Plugin.Home.Controllers" }
routes.MapRoute(
"Users", // Route name
"users/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "Plugin.Users.Controllers" }
);
I can access all the routes in the browser without any problem.
Now the problems
I try to use #{Html.RenderAction("JustATest","Home");}
It only renders from the home controller in home module. How can i render it from Users module.
It throws an exception saying method not found in home controller,
when I say #{Html.RenderAction("JustAnotherTest","Home");}
At what stage I can check if a given controller has the given method or not (Some ControllerFactory). Or how can i make sure it picks up the right controller.
Help will be appreciated.
Regards
Parminder
If you really need to save controller names both HomeController, i see simple solution to use hack like this:
routes.MapRoute(
null,
"{home}/{action}/{id}",
new { controller = "Home", action = "Index", home="Home", id = UrlParameter.Optional });
routes.MapRoute(
null,
"{home}/{action}/{id}",
new { controller = "Home", action = "Index", home="Users", id = UrlParameter.Optional }
);
And you can access to this actions both by url and with RenderAction:
#{Html.RenderAction("JustATest","Home", new {home="Home"});}
#{Html.RenderAction("JustATest","Home", new home="Users");}
But I think that problem is artificial (because it is more hard that you imagine), and you use means only for using means, but not for solving real problem. For real plugin architecture you need to create at least:
1. Custom Route Constraint for checking, that controller type is from assembly, where controller was defined.
2. Installer, that will install all routes independently of main application
3. Create unit tests for each plugin application Routes to ensure that all routes, that was installed from plugins, that do not know anything about each other, works properly (and don't break each other).
I recommend to take your controllers at main web application and give them different names.
See the format used here:
render action with lambda
#Html.RenderACtion<Plugin.Home.Controllers.HomeController>(...)
Edit
I see your issue now. It's that they are dynamically loaded at runtime and hence the names aren't known when you compile. Check out using DefaultControllerFactory as mentioned in various postings here:
How to achieve a dynamic controller and action method in ASP.NET MVC?

Custom route with more than one parameter in url (customer, extra info)

I have a multi tenant system and I need to allow administrators to edit the information of other customers.
I believe the correct way to do this is to append the customer ID to each request, and update my routes accordingly.
How do I make ASP.NET accept and use something like this in the controller (order doesn't mean much to me):
mysite.com/files/delete/{file}/{customerID}
and how would I pass and consume that in my controller? The ASP.NET tutorials skip this... (or I don't know where to search)
Define a route:
//mysite.com/files/delete/{file}/{customerID}
routes.MapRoute(
"Files", // Route name
"files/delete/{file}/{customerID}", // URL with parameters
new { controller = "File", action = "Delete" } // Parameter defaults
);
Create a Controller:
public class FileController : Controller
{
public ActionResult Delete(string file, string customerID)
{
//do something
return View();
}
}
I presume that {file} is a filename that should be deleted.

Resources