Dynamic subdomains in asp.net mvc - asp.net-mvc

I am fairly new to asp.net, and have little experience with iis. I would like to have each user of my application get their own sub-domain, but all use the same controllers. The subdomain would then control what content is displayed.
Example:
user1subdomain.mydomain.com/Whatever
user2subdomain.mydomain.com/Whatever
Will both use the same controller. Ideally a parameter could give the user name to the controller, which could then display the appropriate content. I would like it to be flexible enough that new subdomains could be added to the database without rewriting routing rules every time a new subdomain is added.

MVC is not bound to the domain, just to the path (e.g. http://domain/path).
To do this properly you need the following...
Wildcard DNS setup for
*.yourdomain.com pointing to your server.
The site in IIS setup with
no Host Header. Any other sites
hosted in that instance of IIS on
the same IP must have Host headers
specified.
Your application will need to check the
request host header either on page load,
session start or some other event.

I found an easier answer on this person's blog. Very surprised this works as well as it does and that this solution is more than 4 years old.
http://blog.maartenballiauw.be/post/2009/05/20/aspnet-mvc-domain-routing.aspx
A custom route implementation:
public class DomainRoute : Route
{
public string Domain { get; set; }
public override RouteData GetRouteData(HttpContextBase httpContext)
{
// Build regex
domainRegex = CreateRegex(Domain);
pathRegex = CreateRegex(Url);
// Request information
string requestDomain = httpContext.Request.Headers["host"];
if (!string.IsNullOrEmpty(requestDomain))
{
if (requestDomain.IndexOf(":") > 0)
{
requestDomain = requestDomain.Substring(0, requestDomain.IndexOf(":"));
}
}
else
{
requestDomain = httpContext.Request.Url.Host;
}
string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
// Match domain and route
Match domainMatch = domainRegex.Match(requestDomain);
Match pathMatch = pathRegex.Match(requestPath);
// Route data
RouteData data = null;
if (domainMatch.Success && pathMatch.Success)
{
data = new RouteData(this, RouteHandler);
// Add defaults first
if (Defaults != null)
{
foreach (KeyValuePair<string, object> item in Defaults)
{
data.Values[item.Key] = item.Value;
}
}
// Iterate matching domain groups
for (int i = 1; i < domainMatch.Groups.Count; i++)
{
Group group = domainMatch.Groups[i];
if (group.Success)
{
string key = domainRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key] = group.Value;
}
}
}
}
// Iterate matching path groups
for (int i = 1; i < pathMatch.Groups.Count; i++)
{
Group group = pathMatch.Groups[i];
if (group.Success)
{
string key = pathRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key] = group.Value;
}
}
}
}
}
return data;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return base.GetVirtualPath(requestContext, RemoveDomainTokens(values));
}
public DomainData GetDomainData(RequestContext requestContext, RouteValueDictionary values)
{
// Build hostname
string hostname = Domain;
foreach (KeyValuePair<string, object> pair in values)
{
hostname = hostname.Replace("{" + pair.Key + "}", pair.Value.ToString());
}
// Return domain data
return new DomainData
{
Protocol = "http",
HostName = hostname,
Fragment = ""
};
}}
And here is how it can be used.
routes.Add("DomainRoute", new DomainRoute(
"{controller}-{action}.example.com", // Domain with parameters
"{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
));

Mostly not a problem. I think!
In terms of the application/routing the routing starts where the domain ends so mapping multiple domains to the same application is not a problem, that will just work.
In terms of IIS you can map as many domains as you want (well there's bound to be a limit) to a single site - I'm not sure if you can use a wildcard - what version of IIS are you using?
When a request arrives there are events you can hook to look at the domain and hence set up parameters you want (user for example), the root URL for the request is available from the context later in the cycle too - but you'll want to pick it up early.
If you can do wildcards it becomes fairly trivial - pick up the request, validate the subdomain against the users in the database (if not valid redirect to the default site), set the user and carry on through the normal routing.
If you can't do wildcards then the challenge is adding host headers to the IIS application (website) on the fly from your application as users are added to the database.

Related

MvcSiteMapProvider bug with pagination

It's continue ASP.NET MVC incorect generation url when using pagination, but there I found how fix it. How fix that when using #Html.MvcSiteMap().SiteMapPath() I can't understand.
Problem in that when in actions ShowForum or ShowTopic and when I using pagination some forum or topic. In #Html.MvcSiteMap().SiteMapPath() I get url at parent page with number of page
UPDATE
For route configuration I'm using route attribute
[HttpGet]
[Route("{forumName}", Name = "showForum", Order = 6)]
[Route("{forumName}/Page/{page}", Order = 5)]
[OutputCache(Duration = 30, VaryByParam = "forumName;page", Location = OutputCacheLocation.ServerAndClient)]
public async Task<ActionResult> ShowForum(string forumName, int page = 1)
[HttpGet]
[RefreshDetectFilter]
[Block(VisibleBlock = false)]
[Route("{forum}/{topicName}", Name = "showTopic", Order = 8)]
[Route("{forum}/{topicName}/Page/{page}", Order = 7)]
[OutputCache(Duration = 30, VaryByParam = "topicName;page", Location = OutputCacheLocation.ServerAndClient)]
public async Task<ActionResult> ShowTopic(string forum, string topicName, int page = 1)
My ForumDynamicNodeProvider
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
var rootTitle = ManagerLocalization.Get("Forums", "FORUMS");
var pageParameter = new List<string> { "page" };
var url = "~/Forums";
var attr = new Dictionary<string, object> { { "Controller", "Forums" } };
var nodes = new List<DynamicNode>
{
new DynamicNode
{
Key = "forum_home",
Title = rootTitle,
Url = url,
Attributes = attr
}
};
var forums = this._forumsService.GetAllForumsForMap();
var topics = this._forumsService.GetAllTopicsForMap();
foreach (var forum in forums)
{
var forumRouteValue = new Dictionary<string, object> { { "forumName", forum.NameTranslit } };
nodes.Add(new DynamicNode
{
ParentKey = forum.ForumId != -1 ? $"forum_{forum.ForumId}" : "forum_home",
Key = $"forum_{forum.Id}",
Title = forum.Name,
PreservedRouteParameters = pageParameter,
Controller = "Forums",
Action = "ShowForum",
RouteValues = forumRouteValue,
});
var forumTopics = topics.Where(item => item.ForumId == forum.Id);
foreach (var topic in forumTopics)
{
var topicRouteValue = new Dictionary<string, object> { { "forum", forum.NameTranslit }, { "topicName", topic.TitleTranslite } };
nodes.Add(new DynamicNode
{
ParentKey = $"forum_{forum.Id}",
Key = $"topic_{topic.Id}",
Title = topic.Title,
PreservedRouteParameters = pageParameter,
Controller = "Forums",
Action = "ShowTopic",
RouteValues = topicRouteValue,
});
}
}
return nodes;
}
The problem is that you are using the same route key name {page} in two different places in the same node ancestry in combination with PreservedRouteParameters. PreservedRouteParameters gets its data from the current request. So, it is important that a route key have the same meaning in each request in the same node ancestry. For it to work correctly with PreservedRouteParamters, you need to do three things:
Use a different route key for each separate page parameter (for example, {forumPage} and {page}).
Ensure the ancestor page parameter is passed to the request of its descendants, so when building the URL to an ancestor node the value is in the current request. The simplest way is to build the URL with the page information of all ancestors ({forumName}/Page/{forumPage}/{topicName}/Page/{page}).
Any route keys that have the same meaning between nodes should stay the same ({forumName} in both routes).
Then you need to add the parameters when building the URL of the child node. You must build the URL manually within your application because the request will not have all of the parameters unless you do.
#Html.ActionLink("TheTopicName", "ShowTopic", "Forums",
new { forumName = 1, forumPage = 2, topicName = "foo", page = 1 }, null)
The reason you must supply all of the data in the child node request is because the ancestor node needs it to build its URL. It pulls this information from the request, so it must be present in the request for it to function. MvcSiteMapProvider has no way of knowing what the current page number of the ancestor node is unless it is provided in the request by a URL that is built outside of your menu.
See the MvcSiteMapProvider-Forcing-A-Match-2-Levels project in the code download for How to Make MvcSiteMapProvider Remember a User's Position for a similar configuration and the solution. In that case, it is using productId instead of forumPage as the parameter that is preserved on the descendant nodes so you can navigate back to the parent product.
Note that you could use a similar configuration (with PreservedRouteParameters and SiteMapTitleAttribute) for your entire forum rather than using a dynamic node provider. However, in that case I would suggest you disable the /sitemap.xml endpoint and roll your own.
I found how this fix, thank you to NightOwl888. I'm not the first time understood what should to do.
First I removed initialization PreservedRouteParameters in ForumDynamicNodeProvider
Second I added in action
if (forumPage > 1)
{
var node = SiteMaps.Current.FindSiteMapNodeFromKey(forumName);
if (node != null)
{
node.RouteValues["forumPage"] = forumPage;
}
}
Also I need change generation tree in ForumDynamicNodeProvider because SiteMaps.Current doesn't work in async

Include Entity navigation properties using a Service Reference

I am using a WCF Data Services class that exposes an entity framework model via the OData protocol like so:
public class Service : EntityFrameworkDataService<MyEntities>
{
public static void InitializeService(DataServiceConfiguration config)
{
config.UseVerboseErrors = true;
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
}
}
I consume this service through a service reference in a web solution. I am having problems including all the navigation properties for the entity. I cannot use the following syntax because I do not know what type of entity the user may be requesting:
I CANNOT USE
MyEntities.Customer.Expand("Address");
or
MyEntities.Customer.Include("Address");
What I am currently doing is building a URI string with the $expand=Entity1,Entity2 syntax and then executing that against my service as follows:
public static QueryOperationResponse<object> GetList(string entitySetName, params string[] preloads)
{
StringBuilder stringBuilder = new StringBuilder();
string queryString = string.Empty;
object result = null;
Uri dataAccessURI;
stringBuilder.Append(ServiceReferenceURI.AbsoluteUri);
stringBuilder.Append(entitySetName);
if (preloads != null)
{
for (int i = 0; i <= preloads.Length - 1; i++)
{
queryString = i == 0 ? "?$expand=" : ",";
stringBuilder.AppendFormat("{0}{1}", queryString, preloads[i]);
}
}
dataAccessURI = new Uri(stringBuilder.ToString());
try
{
result = TitanEntities.Execute<object>(dataAccessURI, "GET", true);
}
catch (Exception ex)
{
// log any errors to the console
WriteConsoleMessage(ex.Message, DataAccessEventType.Error);
}
return (QueryOperationResponse<object>)result;
resulting URI string is similar to this:
http://192.168.0.196/Service.svc/AliquotPreparation?$expand=Aliquot,AliquotPrepBatch,AnalysisPreparationMethod,Unit,Employee,Unit,PreparationMethod,State
To me this is a crappy implementation. It is all I could come up with right now though. The problem is, if there are A LOT of navigation properties the $expand command gets too long and the URI reaches it's character limit!
So how can I implement this through a service reference? I would greatly appreciate someone's help!!!

MVC redirect changing domain

I am trying to redirect to a page in MVC website so I have the following code in my controller:
return Redirect("/test");
I am running my site locally through IIS on a domain of test.local and when I hit this controller I would expect to go off to http://test.local/test but instead, for some reason, it is redirecting me to http://localhost/test
Does anyone know how I can make it stay on the same domain without having to put the domain name into the redirect or do I have to include the domain name as well?
Please note as well that I am unable to use RedirectToRoute or RedirectToAction as the url is a separate application (under the same domain as the current site)
I have created a extension method that provides to redirect on the same domain. Maybe this helps
public static class ControllerExtension
{
public static string FullyQualifiedApplicationPath
{
get
{
//Return variable declaration
var appPath = string.Empty;
//Getting the current context of HTTP request
var context = HttpContext.Current;
//Checking the current context content
if (context != null)
{
//Formatting the fully qualified website url/name
appPath = string.Format("{0}://{1}{2}{3}",
context.Request.Url.Scheme,
context.Request.Url.Host,
context.Request.Url.Port == 80
? string.Empty
: ":" + context.Request.Url.Port,
context.Request.ApplicationPath);
}
if (!appPath.EndsWith("/"))
appPath += "/";
return appPath;
}
}
public static RedirectResult RedirectSameDomain(this Controller controller, string url)
{
return new RedirectResult(FullyQualifiedApplicationPath + url);
}
}
You can use it like this
return this.RedirectSameDomain("/test");
Thanks to SO User Brian Hasden for FullyQualifiedApplicationPath and his answer on How can I get the root domain URI in ASP.NET?

Get CultureInfo from current visitor and setting resources based on that?

How can I (in ASP .NET MVC) get the CultureInfo of the current visitor (based on his/her browser languages)?
I have no idea where to start. I tried looking into the "Accept-Languages" header sent by the browser. But is that the best way of doing it?
Request.UserLanguages is the property you're looking for. Just keep in mind that this array may contain arbitrary (even non-exsitent) languages as set by request headers.
UPDATE
Example:
// Get Browser languages.
var userLanguages = Request.UserLanguages;
CultureInfo ci;
if (userLanguages.Count() > 0)
{
try
{
ci = new CultureInfo(userLanguages[0]);
}
catch(CultureNotFoundException)
{
ci = CultureInfo.InvariantCulture;
}
}
else
{
ci = CultureInfo.InvariantCulture;
}
// Here CultureInfo should already be set to either user's prefereable language
// or to InvariantCulture if user transmitted invalid culture ID
Asp.Net Core version: using RequestLocalization ie the culture is retrieved form the HTTP Request.
in Startup.cs - Configure
app.UseRequestLocalization();
Then in your Controller/Razor Page.cs
var locale = Request.HttpContext.Features.Get<IRequestCultureFeature>();
var BrowserCulture = locale.RequestCulture.UICulture.ToString();
You can use code similar to the following to get various details from your user (including languages):
MembershipUser user = Membership.GetUser(model.UserName);
string browser = HttpContext.Request.Browser.Browser;
string version = HttpContext.Request.Browser.Version;
string type = HttpContext.Request.Browser.Type;
string platform = HttpContext.Request.Browser.Platform;
string userAgent = HttpContext.Request.UserAgent;
string[] userLang = HttpContext.Request.UserLanguages
It appears Request.UserLanguages is not available in later mvc versions (Asp.net core mvc 2.0.2 didn't have it.)
I made an extension method for HTTPRequest. Use it as follows:
var requestedLanguages = Request.GetAcceptLanguageCultures();
The method will give you the cultures from the Accept-Language header in order of preference (a.k.a. "quality").
public static class HttpRequestExtensions
{
public static IList<CultureInfo> GetAcceptLanguageCultures(this HttpRequest request)
{
var requestedLanguages = request.Headers["Accept-Language"];
if (StringValues.IsNullOrEmpty(requestedLanguages) || requestedLanguages.Count == 0)
{
return null;
}
var preferredCultures = requestedLanguages.ToString().Split(',')
// Parse the header values
.Select(s => new StringSegment(s))
.Select(StringWithQualityHeaderValue.Parse)
// Ignore the "any language" rule
.Where(sv => sv.Value != "*")
// Remove duplicate rules with a lower value
.GroupBy(sv => sv.Value).Select(svg => svg.OrderByDescending(sv => sv.Quality.GetValueOrDefault(1)).First())
// Sort by preference level
.OrderByDescending(sv => sv.Quality.GetValueOrDefault(1))
.Select(sv => new CultureInfo(sv.Value.ToString()))
.ToList();
return preferredCultures;
}
}
Tested with ASP.NET Core MVC 2.0.2
It's similar to #mare's answer, but a bit more up-to-date and the q (quality) is not ignored. Also, you may want to append the CultureInfo.InvariantCulture to the end of the list, depending on your usage.
I am marking this question for myself with a star and sharing here some code that essentially turns the Request.UserLanguages into an array of CultureInfo instances for further use in your application. It is also more flexible to work with CultureInfo than just the ISO codes, because with CultureInfo you get access to all the properties of a culture (like Name, Two character language name, Native name, ...):
// Create array of CultureInfo objects
string locale = string.Empty;
CultureInfo[] cultures = new CultureInfo[Request.UserLanguages.Length + 1];
for (int ctr = Request.UserLanguages.GetLowerBound(0); ctr <= Request.UserLanguages.GetUpperBound(0);
ctr++)
{
locale = Request.UserLanguages[ctr];
if (!string.IsNullOrEmpty(locale))
{
// Remove quality specifier, if present.
if (locale.Contains(";"))
locale = locale.Substring(0, locale.IndexOf(';'));
try
{
cultures[ctr] = new CultureInfo(locale, false);
}
catch (Exception) { continue; }
}
else
{
cultures[ctr] = CultureInfo.CurrentCulture;
}
}
cultures[Request.UserLanguages.Length] = CultureInfo.InvariantCulture;
HTH
var userLanguage = CultureInfo.CurrentUICulture;

Asp.net mvc 3.0 tree structure page custom routing

I want to map all CMS pages url to single controller(PageController) and action(Details).
How can I create custom routing to map all these urls?
/teacher
/teacher/kindergarten
/teacher/kindergarten/1
/teacher/primary
/teacher/primary/english
/teacher/primary/language
/teacher/primary/language/chinese
/teacher/primary/math
/teacher/primary/science
/parent
/parent/kindergarten
/parent/primary1-3
/parent/primary4-6
/leader
/leader/kindergarten
/leader/kindergarten/1
If you have these URLs in a database you could map the routes when the application starts up:
var pages = siteDB.Pages.ToList();
string pagePath = "";
foreach (var page in pages)
{
routeVals = new RouteValueDictionary();
constraints = new RouteValueDictionary();
routeVals.Add("controller", "page");
routeVals.Add("action", "details");
constraints.Add("path", "[a-zA-Z0-9\\-]*");
// any child pages? must add these routes before their parent pages.
var childPages = siteDB.Pages.Where(p => p.ParentPageId == page.PageId).ToList();
foreach (var childPage in childPages)
{
pagePath = BuildPath(childPage);
RouteTable.Routes.Add(new Route(pagePath, new MvcRouteHandler())
{
Defaults = routeVals,
Constraints = constraints,
DataTokens =
new RouteValueDictionary {
{ "pageid", childPage.PageId },
{ "path", pagePath }
}
});
// Any further child pages? (Only 3 levels supported)
var childSubPages = siteDB.Pages.Where(p => p.ParentPageId == childPage.PageId).ToList();
foreach (var childSubPage in childSubPages)
{
pagePath = BuildPath(childSubPage);
RouteTable.Routes.Add(new Route(pagePath, new MvcRouteHandler())
{
Defaults = routeVals,
Constraints = constraints,
DataTokens =
new RouteValueDictionary {
{ "pageid", childSubPage.PageId },
{ "path", pagePath }
}
});
}
}
This code takes the pages from a database where they are linked by parent id.
Here's the BuildPath function which generates a full path to each page:
public static string BuildPath(Page page)
{
if (page.ParentPageId == 1)
{
return page.PageKey;
}
else
{
SiteDataEntities siteDB = new SiteDataEntities();
string path = page.PageKey;
Page parent = siteDB.Pages.Find(page.ParentPageId);
while (parent != null)
{
path = parent.PageKey + "/" + path;
parent = siteDB.Pages.Find(parent.ParentPageId);
if (parent.PageKey == "home") break;
}
return path;
}
}
Previous proposed solution is working only for small amount of pages.
Because according to the code:
application generate and register Route for each of site page. In result we have at least same amount of routes as pages in our site. As you probably know RouteModule have to check route by route each of them to find first right one and execute correct handler, controller, action, view...
There are two other way to solve this:
You can create a class that derives from RouteBase and implement the properties and methods that you need: split url to segments, determinate current page fill RouteValueDictionary with pageid, path, parents etc
You can customize UrlRewriteModule with custom rewrite provider. Idea to transform all requests url from tree base structure to mvc default route:
{controller}/{action}/{id}?path=parentlevel1/parent2/parent3/....
90% -same code for both variants could be prepared.
that solution also could be useful when you have different controllers, correct one we could determinate by current page (by page data: type)

Resources