Multi Tenant application using MapRoute - asp.net-mvc

I have found a solution for implement multi-tenant in my asp.net mvc project and
I want know if it's correct or exist a better way.
I want organize more customers using the same application handling the web request, for example:
http://mysite/<customer>/home/index //home is controller and index the action
For this reason i changed the default maproute:
routes.MapRoute(
name: "Default",
url: "{customername}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
and I implemented a custom ActionFilterAttribute:
public class CheckCustomerNameFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting( ActionExecutingContext filterContext )
{
var customerName = filterContext.RouteData.Values["customername"];
var customerRepository = new CustomerRepository();
var customer = customerRepository.GetByName( customerName );
if( customer == null )
{
filterContext.Result = new ViewResult { ViewName = "Error" };
}
base.OnActionExecuting( filterContext );
}
}
and using it:
public class HomeController : Controller
{
[CheckCustomerNameFilterAttribute]
public ActionResult Index()
{
var customerName = RouteData.Values["customername"];
// show home page of customer with name == customerName
return View();
}
}
With this solution i can switch customer using customer name and correctly accept requests like this:
http://mysite/customer1
http://mysite/customer2/product/detail/2
...................................
This solution works well but I don't know if the best approach.
Does anyone know a better way?

You can model bind the customer name, and not have to pull it from route values:
public ActionResult Index(string customerName)
{
}

Related

Is there a way to have a RoutePrefix that starts with an optional parameter?

I want to reach the Bikes controller with these URL's:
/bikes // (default path for US)
/ca/bikes // (path for Canada)
One way of achieving that is using multiple Route Attributes per Action:
[Route("bikes")]
[Route("{country}/bikes")]
public ActionResult Index()
To keep it DRY I'd prefer to use a RoutePrefix, but multiple Route Prefixes are not allowed:
[RoutePrefix("bikes")]
[RoutePrefix("{country}/bikes")] // <-- Error: Duplicate 'RoutePrefix' attribute
public class BikesController : BaseController
[Route("")]
public ActionResult Index()
I've tried using just this Route Prefix:
[RoutePrefix("{country}/bikes")]
public class BikesController : BaseController
Result: /ca/bikes works, /bikes 404s.
I've tried making country optional:
[RoutePrefix("{country?}/bikes")]
public class BikesController : BaseController
Same result: /ca/bikes works, /bikes 404s.
I've tried giving country a default value:
[RoutePrefix("{country=us}/bikes")]
public class BikesController : BaseController
Same result: /ca/bikes works, /bikes 404s.
Is there another way to achieve my objective using Attribute Routing?
(And yes, I know I can do this stuff by registering routes in RouteConfig.cs, but that's what not I'm looking for here).
I'm using Microsoft.AspNet.Mvc 5.2.2.
FYI: these are simplified examples - the actual code has an IRouteConstraint for the {country} values, like:
[Route("{country:countrycode}/bikes")]
I am a bit late to the party, but i have a working solution for this problem. Please find my detailed blog post on this issue here
I am writing down summary below
You need to create 2 files as given below
_3bTechTalkMultiplePrefixDirectRouteProvider.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Web.Http.Controllers;
using System.Web.Http.Routing;
namespace _3bTechTalk.MultipleRoutePrefixAttributes {
public class _3bTechTalkMultiplePrefixDirectRouteProvider: DefaultDirectRouteProvider {
protected override IReadOnlyList GetActionDirectRoutes(HttpActionDescriptor actionDescriptor, IReadOnlyList factories, IInlineConstraintResolver constraintResolver) {
return CreateRouteEntries(GetRoutePrefixes(actionDescriptor.ControllerDescriptor), factories, new [] {
actionDescriptor
}, constraintResolver, true);
}
protected override IReadOnlyList GetControllerDirectRoutes(HttpControllerDescriptor controllerDescriptor, IReadOnlyList actionDescriptors, IReadOnlyList factories, IInlineConstraintResolver constraintResolver) {
return CreateRouteEntries(GetRoutePrefixes(controllerDescriptor), factories, actionDescriptors, constraintResolver, false);
}
private IEnumerable GetRoutePrefixes(HttpControllerDescriptor controllerDescriptor) {
Collection attributes = controllerDescriptor.GetCustomAttributes (false);
if (attributes == null)
return new string[] {
null
};
var prefixes = new List ();
foreach(var attribute in attributes) {
if (attribute == null)
continue;
string prefix = attribute.Prefix;
if (prefix == null)
throw new InvalidOperationException("Prefix can not be null. Controller: " + controllerDescriptor.ControllerType.FullName);
if (prefix.EndsWith("/", StringComparison.Ordinal))
throw new InvalidOperationException("Invalid prefix" + prefix + " in " + controllerDescriptor.ControllerName);
prefixes.Add(prefix);
}
if (prefixes.Count == 0)
prefixes.Add(null);
return prefixes;
}
private IReadOnlyList CreateRouteEntries(IEnumerable prefixes, IReadOnlyCollection factories, IReadOnlyCollection actions, IInlineConstraintResolver constraintResolver, bool targetIsAction) {
var entries = new List ();
foreach(var prefix in prefixes) {
foreach(IDirectRouteFactory factory in factories) {
RouteEntry entry = CreateRouteEntry(prefix, factory, actions, constraintResolver, targetIsAction);
entries.Add(entry);
}
}
return entries;
}
private static RouteEntry CreateRouteEntry(string prefix, IDirectRouteFactory factory, IReadOnlyCollection actions, IInlineConstraintResolver constraintResolver, bool targetIsAction) {
DirectRouteFactoryContext context = new DirectRouteFactoryContext(prefix, actions, constraintResolver, targetIsAction);
RouteEntry entry = factory.CreateRoute(context);
ValidateRouteEntry(entry);
return entry;
}
private static void ValidateRouteEntry(RouteEntry routeEntry) {
if (routeEntry == null)
throw new ArgumentNullException("routeEntry");
var route = routeEntry.Route;
if (route.Handler != null)
throw new InvalidOperationException("Direct route handler is not supported");
}
}
}
3bTechTalkRoutePrefix.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
namespace _3bTechTalk.MultipleRoutePrefixAttributes
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class _3bTechTalkRoutePrefix : RoutePrefixAttribute
{
public int Order { get; set; }
public _3bTechTalkRoutePrefix(string prefix) : this(prefix, 0) { }
public _3bTechTalkRoutePrefix(string prefix, int order) : base(prefix)
{
Order = order;
}
}
}
Once done, open WebApiConfig.cs and add this below given line
config.MapHttpAttributeRoutes(new _3bTechTalkMultiplePrefixDirectRouteProvider());
That's it, now you can add multiple route prefix in your controller. Example below
[_3bTechTalkRoutePrefix("api/Car", Order = 1)]
[_3bTechTalkRoutePrefix("{CountryCode}/api/Car", Order = 2)]
public class CarController: ApiController {
[Route("Get")]
public IHttpActionResult Get() {
return Ok(new {
Id = 1, Name = "Honda Accord"
});
}
}
I have uploaded a working solution here
Happy Coding :)
You're correct that you can't have multiple route prefixes, which means solving this particular use case is not going to be straight forward. About the best way I can think of to achieve what you want with the minimal amount of modifications to your project is to subclass your controller. For example:
[RoutePrefix("bikes")]
public class BikeController : Controller
{
...
}
[RoutePrefix("{country}/bikes")]
public class CountryBikeController : BikeController
{
}
You subclassed controller will inherit all the actions from BikeController, so you don't need to redefine anything, per se. However, when it comes to generating URLs and getting them to go to the right place, you'll either need to be explicit with the controller name:
#Url.Action("Index", "CountryBike", new { country = "us" }
Or, if you're using named routes, you'll have to override your actions in your subclassed controller so you can apply new route names:
[Route("", Name = "CountryBikeIndex")]
public override ActionResult Index()
{
base.Index();
}
Also, bear in mind, that when using parameters in route prefixes, all of your actions in that controller should take the parameter:
public ActionResult Index(string country = "us")
{
...
You could use attribute routes with two ordered options.
public partial class GlossaryController : Controller {
[Route("~/glossary", Order = 2)]
[Route("~/{countryCode}/glossary", Order = 1)]
public virtual ActionResult Index()
{
return View();
}
}
If you're planning to have region specific routes for all your pages you could add a route to the route config above the default. This will work only for views/controllers without attribute routes.
routes.MapRoute(
name: "Region",
url: "{countryCode}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { countryCode = #"\w{2}" }
);
The best solution I've come across is detailed by NightOwl888 in response to the following question: ASP.NET MVC 5 culture in route and url. The code below is my trimmed down version of his post. It's working for me in MVC5.
Decorate each controller with a single RoutePrefix, without a culture segment. When the application starts up, the custom MapLocalizedMvcAttributeRoutes method adds a localized route entry for each controller action.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
// Omitted for brevity
MapLocalizedMvcAttributeRoutes(routes, "{culture}/", new { culture = "[a-z]{2}-[A-Z]{2}" });
}
static void MapLocalizedMvcAttributeRoutes(RouteCollection routes, string urlPrefix, object constraints)
{
var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc");
var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc");
var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc");
FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance);
PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries");
MethodInfo addMethodInfo = subRouteCollectionType.GetMethod("Add");
var localizedRouteTable = new RouteCollection();
var subRoutes = Activator.CreateInstance(subRouteCollectionType);
Func<Route, RouteBase> createLinkGenerationRoute = (Route route) => (RouteBase)Activator.CreateInstance(linkGenerationRouteType, route);
localizedRouteTable.MapMvcAttributeRoutes();
foreach (var routeCollectionRoute in localizedRouteTable.Where(rb => rb.GetType().Equals(routeCollectionRouteType)))
{
// routeCollectionRoute._subRoutes.Entries
foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(subRoutesInfo.GetValue(routeCollectionRoute)))
{
var localizedRoute = CreateLocalizedRoute(routeEntry.Route, urlPrefix, constraints);
var localizedRouteEntry = new RouteEntry(string.IsNullOrEmpty(routeEntry.Name) ? null : $"{routeEntry.Name}_Localized", localizedRoute);
// Add localized and default routes and subroute entries
addMethodInfo.Invoke(subRoutes, new[] { localizedRouteEntry });
addMethodInfo.Invoke(subRoutes, new[] { routeEntry });
routes.Add(createLinkGenerationRoute(localizedRoute));
routes.Add(createLinkGenerationRoute(routeEntry.Route));
}
}
var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes);
routes.Add((RouteBase)routeEntries);
}
static Route CreateLocalizedRoute(Route route, string urlPrefix, object constraints)
{
var routeUrl = urlPrefix + route.Url;
var routeConstraints = new RouteValueDictionary(constraints);
// combine with any existing constraints
foreach (var constraint in route.Constraints)
{
routeConstraints.Add(constraint.Key, constraint.Value);
}
return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler);
}
}

ASP.NET MVC 4 Custom View Routing in Sitefinity 7

Our solution hierarchy is as follows:
language/doctor-cv/doctorMcr/doctorFullName
Ex: en\doctor-cv\12345\David
Now I'd like to map the routing so that when the user just types the name of the view in the url, it automatically maps the url to the corresponding controller
I.E: localhost:1234\en\doctor-cv\12345\David
Should map to
View\DoctorCVPage\Index.cshtml
Currently, we're using the default routing
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapHttpRoute(
name: "DoctorCVPage",
routeTemplate: "{language}/doctor-cv/{doctorMcr}/{doctorFullName}",
defaults: new
{
controller = "DoctorCVPage",
action = "Index",
doctorMcr = UrlParameter.Optional,
doctorFullName = UrlParameter.Optional,
language = UrlParameter.Optional
});
Here is MyController
public class DoctorCVPageController : BaseController
{
/// <summary>
/// Gets or sets the message.
/// </summary>
[Category("String Properties")]
public string Message { get; set; }
/// <summary>
/// This is the default Action.
/// </summary>
public ActionResult Index(string doctorMcr)
{
var id = "";
ViewBag.PageTitleLink = Request.UrlReferrer != null ? Request.UrlReferrer.ToString() : string.Empty;
}
}
And then in my view I have a tag.
<a href="/en/doctor-cv/${DoctorMcr}/${DoctorName}"/>
After user click on this tag, system should to redirect to DoctorCVPage/Index (controller: DoctorCVPage, Action = Index), but it can't do this.
Please helps me know why, thank for all helps.
Please follow this way, I think it can help you handle that thing.
protected override void HandleUnknownAction(string actionName)
{
UrlDetail = Request.Url != null ? Request.Url.ToString() : string.Empty;
Response.StatusCode = (int)HttpStatusCode.OK;
this.ActionInvoker.InvokeAction(this.ControllerContext, "Index");
return;
}
public ActionResult Index()
{
return;
}
Hope this way can help you.
It's not working because any URL in Sitefinity will be routed to a page. Your widget is not a page, but on the page. What you have to do is override the HandleUnknownAction method and parse the URL there, then take the appropriate action.
For example
protected override void HandleUnknownAction( string actionName )
{
if (!RouteHelper.GetUrlParametersResolved())
{
this.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
else
{
this.Response.StatusCode = (int)HttpStatusCode.OK;
List<string> segments = RouteHelper.SplitUrlToPathSegmentStrings( Request.Url.LocalPath, true )
.Skip(1)
.ToList();
ViewBag.PageTitleLink = String.IsNullOrEmpty(segments.ElementAt(2)):String.Empty:segments.ElementAt(2);
var lang = segments.ElementAt(0);
// etc
View( "Index", model ).ExecuteResult( this.ControllerContext );
}
}
This relies on the position of the url segments being consistent, but it works well otherwise. This is the only way I have found to deal with the requirements you have. Also notice that this is a Void method, so you have to execute the view, as shown.

How can I call the webapi controller method from the different view page in mvc4?

How can I call the webapi controller method from a different view page in mvc4?
This is my Controller:
/api/GetEmployee/1
public EmployeeVM GetEmployee(int id)
{
using (var db = new WorkerDBContext())
{
var model = db.Employee.Find(id);
if (model != null)
{
return model;
}
return null;
}
}
You need to move your data access code to another class (repository pattern would be a good fit) and just call that method from both Controllers.
public class EmployeeRepository
{
private readonly WorkerDBContext _workerDbContext = new WorkerDBContext();
public Employee GetEmployee(int id)
{
var model = _workerDbContext.Employee.Find(id);
return model;
}
}
Then from your Controllers:
private readonly EmployeeRepository _employeeRepository = new EmployeeRepository();
public EmployeeVM GetEmployee(int id)
{
var employee = _employeeRepository.GetEmployee(id);
var viewModel = new EmployeeVM { Name = employeeName, ... };
return viewModel;
}
I think what's happening is that you have multiple Get , Put, Post, etc. in the same controller and WebApi (or REST) services for that matter work based on the action type of the request. Either way the default route in WebApi does not have an action, so make this change to your route:
config.Routes.MapHttpRoute(
name: "ActionRoute",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouterParameter.Optional }
);
and then create a link/call to your api controllers this way:
#Url.RouteUrl("ActionRoute", new {httproute= "", controller = "controller", action = "action"});

How do I route a URL with a querystring in ASP.NET MVC?

I'm trying to setup a custom route in MVC to take a URL from another system in the following format:
../ABC/ABC01?Key=123&Group=456
The 01 after the second ABC is a step number this will change and the Key and Group parameters will change. I need to route this to one action in a controller with the step number key and group as paramters. I've attempted the following code however it throws an exception:
Code:
routes.MapRoute(
"OpenCase",
"ABC/ABC{stepNo}?Key={key}&Group={group}",
new {controller = "ABC1", action = "OpenCase"}
);
Exception:
`The route URL cannot start with a '/' or '~' character and it cannot contain a '?' character.`
You cannot include the query string in the route. Try with a route like this:
routes.MapRoute("OpenCase", "ABC/ABC{stepNo}",
new { controller = "ABC1", action = "OpenCase" });
Then, on your controller add a method like this:
public class ABC1 : Controller
{
public ActionResult OpenCase(string stepno, string key, string group)
{
// do stuff here
return View();
}
}
ASP.NET MVC will automatically map the query string parameters to the parameters in the method in the controller.
When defining routes, you cannot use a / at the beginning of the route:
routes.MapRoute("OpenCase",
"/ABC/{controller}/{key}/{group}", // Bad. Uses a / at the beginning
new { controller = "", action = "OpenCase" },
new { key = #"\d+", group = #"\d+" }
);
routes.MapRoute("OpenCase",
"ABC/{controller}/{key}/{group}", // Good. No / at the beginning
new { controller = "", action = "OpenCase" },
new { key = #"\d+", group = #"\d+" }
);
Try this:
routes.MapRoute("OpenCase",
"ABC/{controller}/{key}/{group}",
new { controller = "", action = "OpenCase" },
new { key = #"\d+", group = #"\d+" }
);
Then your action should look as follows:
public ActionResult OpenCase(int key, int group)
{
//do stuff here
}
It looks like you're putting together the stepNo and the "ABC" to get a controller that is ABC1. That's why I replaced that section of the URL with {controller}.
Since you also have a route that defines the 'key', and 'group', the above route will also catch your initial URL and send it to the action.
There is no reason to use routing based in querystring in new ASP.NET MVC project. It can be useful for old project that has been converted from classic ASP.NET project and you want to preserve URLs.
One solution can be attribute routing.
Another solution can be in writting custom routing by deriving from RouteBase:
public class MyOldClassicAspRouting : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
if (httpContext.Request.Headers == null) //for unittest
return null;
var queryString = httpContext.Request.QueryString;
//add your logic here based on querystring
RouteData routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values.Add("controller", "...");
routeData.Values.Add("action", "...");
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//Implement your formating Url formating here
return null;
}
}
And register your custom routing class
public static void RegisterRoutes(RouteCollection routes)
{
...
routes.Add(new MyOldClassicAspRouting ());
}
The query string arguments generally are specific of that controller and of that specific application logic.
So it will better if this isn't written in route rules, that are general.
You can embed detection of query string on action argument in the following way.
I think that is better to have one Controller for handling StepNo.
public class ABC : Controller
{
public ActionResult OpenCase(OpenCaseArguments arg)
{
// do stuff here
// use arg.StepNo, arg.Key and arg.Group as You need
return View();
}
}
public class OpenCaseArguments
{
private string _id;
public string id
{
get
{
return _id;
}
set
{
_id = value; // keep original value;
ParseQueryString(value);
}
}
public string StepNo { get; set; }
public string Key { get; set; }
public string Group { get; set; }
private void ParseQueryString(string qs)
{
var n = qs.IndexOf('?');
if (n < 0) return;
StepNo = qs.Substring(0, n); // extract the first part eg. {stepNo}
NameValueCollection parms = HttpUtility.ParseQueryString(qs.Substring(n + 1));
if (parms.Get("Key") != null) Key = parms.Get("Key");
if (parms.Get("Group") != null) Group = parms.Get("Group");
}
}
ModelBinder assign {id} value to the id field of OpenCaseArguments. The set method handle querystring split logic.
And keep routing this way. Note routing get your querystring in id argument.
routes.MapRoute(
"OpenCase",
"ABC/OpenCase/{id}",
new {controller = "ABC", action = "OpenCase"}
);
I have used this method for getting multiple fields key value on controller action.

Passing a {sitename} parameter to MVC controller actions

How can I retrieve a site-wide URL parameter in a route without cluttering each controller action with a parameter? My question is similar to this question, but I want to avoid the ModelBinder clutter. Ie. in Global.asax.cs:
routes.MapRoute(
"Default", // Route name
"{sitename}/{controller}/{action}/{id}",
new { sitename = "", controller = "SomeController", action = "Index", id = "" } );
So, instead of the following in SomeController class:
public ActionResult Index(string sitename)
{
SiteClass site = GetSite(sitename);
...
return View(site.GetViewModel());
}
I would rather have the following:
public ActionResult Index()
{
SiteClass site = CurrentSite; // where CurrentSite has already retrieved data based on unique URL sitename parameter.
...
return View(site.GetViewModel());
}
Perhaps this can be achieved with controller-wide action filter? OnActionExecuting?
First add a route to Global.aspx.cs to pass a {sitename} parameter:
routes.MapRoute(
"Sites", // Route name
"{sitename}/{controller}/{action}/{id}", // URL with parameters
new { sitename = "", controller = "Home", action = "Index", id = "" } // Parameter defaults
);
Then add the following simple code inside a base controller:
public class BaseController: Controller
{
public string SiteName = "";
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpRequestBase req = filterContext.HttpContext.Request;
SiteName = filterContext.RouteData.Values["sitename"] as string;
base.OnActionExecuting(filterContext);
}
}
And use in your derived controller:
public class HomeController: BaseController
{
public ActionResult Index()
{
ViewData["SiteName"] = SiteName;
return View();
}
}
Perhaps I misunderstand the question, but why not simply do the following inside your controller action:
var sitename = RouteData.Values["sitename"] as string;
I don't understand why you would need to override OnActionExecuting (per #pate's answer) when you can retrieve the value when you need it.
There's also no need to create a base class and have everything derive from it. If you object to copying that line into every action method of every controller, why not create an extension method?

Resources