There are a lot of material written about Subdomain routing in ASP.NET MVC. Some of them use Areas as target for subdomains other use another Controllers.
There are some of them:
Subdomains for a single application with ASP.NET MVC
Asp.Net MVC 2 Routing SubDomains to Areas
MVC 3 Subdomain Routing
MVC-Subdomain-Routing on Github
They do all explain how to accept and route requests with subdomain.
But:
None of them explains how to generate URLs with subdomain. I.e. I tried #Html.RouteLink("link to SubDomain", "SubdomainRouteName") but what it ignores subdomain and generates url without it
How to deal with the same names of controllers from different areas. All those solutions (they use namespaces for these purpose) throw exception that exist several controllers and suggest using namespaces :)
Purpose:
create mobile version of site using subdomain
I've wrote a post on how I use subdomain routing in my application. The source code is available on the post, but I'll try to explain how I did my custom RouteLink method.
The helper method uses the RouteTable class to get the Route object based on the current Url and cast it to a SubdomainRoute object.
In my case all routes are defined using the SubdomainRoute and everytime I need to add a link to some other page I use my custom RouteLink helper, this is why I consider this cast safe. With the SubdomainRoute variable available I'm able to get the subdomain name and then build the Url using the UriBuilder class.
This is the code I'm currently using.
public static IHtmlString AdvRouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, object routeValues, object htmlAttributes)
{
RouteValueDictionary routeValueDict = new RouteValueDictionary(routeValues);
var request = htmlHelper.ViewContext.RequestContext.HttpContext.Request;
string host = request.IsLocal ? request.Headers["Host"] : request.Url.Host;
if (host.IndexOf(":") >= 0)
host = host.Substring(0, host.IndexOf(":"));
string url = UrlHelper.GenerateUrl(routeName, null, null, routeValueDict, RouteTable.Routes, htmlHelper.ViewContext.RequestContext, false);
var virtualPathData = RouteTable.Routes.GetVirtualPathForArea(htmlHelper.ViewContext.RequestContext, routeName, routeValueDict);
var route = virtualPathData.Route as SubdomainRoute;
string actualSubdomain = SubdomainRoute.GetSubdomain(host);
if (!string.IsNullOrEmpty(actualSubdomain))
host = host.Substring(host.IndexOf(".") + 1);
if (!string.IsNullOrEmpty(route.Subdomain))
host = string.Concat(route.Subdomain, ".", host);
else
host = host.Substring(host.IndexOf(".") + 1);
UriBuilder builder = new UriBuilder(request.Url.Scheme, host, 80, url);
if (request.IsLocal)
builder.Port = request.Url.Port;
url = builder.Uri.ToString();
return htmlHelper.Link(linkText, url, htmlAttributes);
}
private static IHtmlString Link(this HtmlHelper htmlHelper, string text, string url, object htmlAttributes)
{
TagBuilder tag = new TagBuilder("a");
tag.Attributes.Add("href", url);
tag.InnerHtml = text;
tag.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
return MvcHtmlString.Create(tag.ToString(TagRenderMode.Normal));
}
Related
I am working on a mvc core 3.1 application. Seo requirements are to show product name with main site instead of complete url.
My original url is
www.abc.com/Fashion/ProductDetail?productId=5088&productName=AliviaBlack&brandId=3
Requirement are
www.abc.com/alivia-black
I have tried following by using attribute routing
endpoints.MapControllerRoute(
name: "SpecificRoute",
pattern: "/{productName}",
defaults: new { controller = "Fashion", action = "ProductDetail", id= "{productId}" });
In view page
<a asp-route-productId="#product.ProductId"
asp-route-productName="#Common.FilterURL(product.ProductName)"
asp-route-brandId="#product.FashionBrand.FashionBrandId" asp-route="SpecificRoute">
Result is
www.abc.com/alivia-black?productId=5223&brandId=4
How to remove question mark and parameters after it.
First off, URIs need to be resilient. You say your current requirement is to have URIs like this:
www.abc.com/alivia-black
i.e.:
{host}/{productName}
That's a very bad URI template because:
It does not uniquely identify the product (as you could have multiple products with the same name).
It will break existing links from external websites if you ever rename a product or replace a product with the same name. And this happens a lot in any product database.
Because you're putting the {productName} in the "root" of your URI structure it means it's much harder to handle anything else besides viewing products (e.g. how would you have a /contact-us page? What if you had a product that was named contact-us?)
I stress that is is very important to include an immutable key to the entity being requested (in this case, your productId value) in the URI and use that as a primary-reference, so the productName can be ignored when handling an incoming HTTP request. This is how StackOverflow's and Amazon's URIs work (you can trim off the text after a StackOverflow's question-id and it will still work: e.g.
https://stackoverflow.com/questions/69748993/how-to-show-seo-friendly-url-in-mvc-core-3-1
https://stackoverflow.com/questions/69748993
I strongly recommend you read this article on the W3.org's website all about designing good URIs, as well as other guidance from that group's homepage.
I suggest you use a much better URI template, such as this one:
{host}/products/{productId}/{productName}
Which will give you a URI like this:
abc.com/products/5088/alivablack
Handling such a link in ASP.NET MVC (and ASP.NET Core) is trivial, just set the route-template on your controller action:
[Route( "/products/{productId:int}/{productName?}" )]
public async Task<IActionResult> ShowProduct( Int32 productId, String? productName = null )
{
// Use `productId` to lookup the product.
// Disregard `productName` unless you want to use it as a fallback to search your database if `productId` doesn't work.
}
As for generating URIs, as I recommend against using TagHelpers (e.g. <a asp-route-) because they don't give you sufficient control over how the URI is rendered, instead you can define a UrlHelper extension method (ensure you #import the namespace into your .cshtml pages (or add it to ViewStart):
public static class MyUrls
{
public static String ShowProduct( this IUrlHelper u, Int32 productId, String productName )
{
const String ACTION_NAME = nameof(ProductsController.ShowProduct);
const String CONTROLLER_NAME = "Products"; // Unfortunately we can't use `nameof` here due to the "Controller" suffix in the type-name.
String url = u.Action( action: ACTION_NAME, controller: CONTROLLER_NAME, values: new { productId = productId, productName = productName } );
return url;
}
}
Then you can use it like so:
View AliviaBlack
You can also make ShowProduct accept one of your Product objects directly and then pass the values on to the other overload (defined above) which accepts scalars:
public static String ShowProduct( this IUrlHelper u, Product product )
{
String url = ShowProduct( u, productId: product.ProductId, productName: product.ProductName );
return url;
}
Then you can use it like so (assuming product is in-scope):
#product.ProductName
I have seen some very helpful posts about testing Microsoft's routing. One in particular www.strathweb.com/2012/08/testing-routes-in-asp-net-web-api/ seems to deal just with WebApi. Though similiar they are not the same. If I have an MVC application how do I see the method that will be invoked for a given URL. It seems to boils down to creating a 'Request' that can be passed to the constructor of HttpControllerContext and obtaining a reference to the 'current' config (like HttpConfiguration) in testing. Ideas?
Thank you.
Testing Incoming URL
If you need to test routes, you need to mock three classes from the MVC Framework: HttpRequestBase, HttpContextBase and HttpResponseBase(only for outgoing URL´s)
private HttpContextBase CreateHttpContext(string targetUrl = null, string httpMethod = "GET")
{
// create mock request
Mock<HttpRequestBase> mockRequest = new Mock<HttpRequestBase>();
// url you want to test through the property
mockRequest.Setup(m => m.AppRelativeCurrentExecutionFilePath).Returns(targetUrl);
mockRequest.Setup(m => m.HttpMethod).Returns(httpMethod);
// create mock response
Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
mockResponse.Setup(m => m.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(s => s);
// create the mock context, using the request and response
Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
mockContext.Setup(m => m.Request).Returns(mockRequest.Object);
mockContext.Setup(m => m.Response).Returns(mockResponse.Object);
// return the mock context object
return mockContext.Object;
}
then you need an additional helper method that let´s you specify the URL to test and the expected segment variables and an object for additional variables.
private void TestRouteMatch(string url, string controller, string action,
object routeProperties = null, string httpMethod = "GET")
{
// arrange
RouteCollection routes = new RouteCollection();
// loading the defined routes about the Route-Config
RouteConfig.RegisterRoutes(routes);
RouteData result = routes.GetRouteData(CreateHttpContext(url, httpMethod));
// assert
Assert.IsNotNull(result);
// here you can check your properties (controller, action, routeProperties) with the result
Assert.IsTrue(.....);
}
You don´t need to define your routes in the test methodes, because they were load directly using the RegisterRoutes method in the RouteConfig class.
The mechanism by wich inbound URL matching works.
GetRouteData(HttpContextBase httpContext)
referencesource.microsoft
The framework calls this method for each route table entry, until one of thems returns a non-null value.
You have to call the helper method as example in this way
[TestMethod]
public void TestIncomingRoutes() {
// check for the URL that is hoped for
TestRouteMatch("~/Home/Index", "Home", "Index");
}
the method check the URL you expecting as in the example above, call the Index action in the Home controller. You must prefix the URL with tilde (~) this is they way how the ASP.NET Framework presents the URL to the routing system.
In reference to the book Pro ASP.NET MVC 5 by Adam Freeman i can recommand it to every ASP.NET MVC developer!
I am using MVC 3, i am passing some url in the string type property,
for this i have used
var uri = new UrlHelper().Action("ActionName", "ControllerName");
But it is giving error. which is
{"Value cannot be null.\r\nParameter name: routeCollection"}
i know .Action(actionName, controllerName, routeValues, scheme); has four parameters but i only want to pass two, what will be the default value for it??
If you're inside your Controller, you use the static Url.Action()instead, and it will work:
var uri = Url.Action("ActionName", "ControllerName");
And if you're outside of a Controller(e.g in your Model) you have to use the UrlHelper as you did, but passing a Context in parameter, so the method can make the correct url for you.
You can get the request context in this way:
HttpContext.Current.Request.RequestContext
So, if you can use it like this:
UrlHelper url = new UrlHelper(HttpContext.Current.Request.RequestContext);
var uri = url.Action("ActionName", "ControllerName");
i searched it and found the answer.
Same Question
var uri = new UrlHelper(HttpContext.Current.Request.RequestContext).Action("ActionName", "ControllerName");
It is working fine after adding HttpContext.Current.Request.RequestContext.
So this really applies to several different classes like HttpContext, ConfigurationManager, etc. There are several different ways to handle this, and I have always used wrapper classes to handle this stuff, but I wanted to see what the most common community practice is...
Wrapper classes - e.g. I would have an HttpContextService which I pass in via constructor that exposes all the same functionality via flat method calls.
Wrapper classes (part 2) - e.g. I would have SPECIFIC service classes, like MembershipService, which LEVERAGES HttpContext behind the scenes. Functions the same as 1, but the naming scheme / usage pattern is a little different as you are exposing specific functions through specific services instead of one monolithic wrapper. The downside is that the number of service classes that need to be injected goes up, but you get some modularity for when you don't need all the features of the monolithic wrapper.
ActionFilters and parameters - Use an ActionFilter to automatically pass in certain values needed on a per function basis. MVC only, and limits you to the controller methods, whereas 1 and 2 could be used generically throughout the project, or even in conjunction with this option.
Directly mocking HttpContextBase and setting ControllerContext - There are several mocking framework extension methods out there to help with this, but essentially requires you to directly set things as needed. Doesn't require abstractions, which is nice, and can be reused across non-controller tests as well. Still leaves open the question for ConfigurationManager and other static method calls though, so you could end up with injecting that ANYWAY, but leaving HttpContext to be accessed in this other way.
Right now I am kind of doing number 1, so I have an HttpContextService and a ConfigurationManagerService, etc. which I then inject, though I'm leaning toward 2 in the future. 3 seems to be a little too messy for my tastes, but I can see the appeal for controller methods, and the need for a completely separate solution for other areas of code that also use these static classes makes that one kind of poor for me... 4 is still interesting to me as it seems the most "natural" in terms of basic functionality and leverages the built-in methodologies of MVC.
So what is the prevailing Best Practice here? What are people seeing and using in the wild?
There are already "wrapper" classes for HttpContext, HttpRequest, HttpResponse, etc. The MVC framework uses these and you can supply mocks of them to the Controller via the controller context. You don't need to mock the controller context as you can simply create one with the appropriate values. The only thing I've found difficult to mock are the helpers, UrlHelper and HtmlHelper. Those have some relatively deep dependencies. You can fake them in a somewhat reasonable way, UrlHelper shown below.
var httpContext = MockRepository.GenerateMock<HttpContextBase>();
var routeData = new RoutedData();
var controller = new HomeController();
controller.ControllerContext = new ControllerContext( httpContext, routeData, controller );
controller.Url = UrlHelperFactory.CreateUrlHelper( httpContext, routeDate );
where
public static class UrlHelperFactory
{
public static UrlHelper CreateUrlHelper( HttpContextBase httpContext, RouteData routeData )
{
return CreateUrlHelper( httpContext, routeData, "/" );
}
public static UrlHelper CreateUrlHelper( HttpContextBase httpContext, RouteData routeData, string url )
{
string urlString = string.Format( "http://localhost/{0}/{1}/{2}", routeData.Values["controller"], routeData.Values["action"], routeData.Values["id"] ).TrimEnd( '/' );
var uri = new Uri( urlString );
if (httpContext.Request == null)
{
httpContext.Stub( c => c.Request ).Return( MockRepository.GenerateStub<HttpRequestBase>() ).Repeat.Any();
}
httpContext.Request.Stub( r => r.Url ).Return( uri ).Repeat.Any();
httpContext.Request.Stub( r => r.ApplicationPath ).Return( "/" ).Repeat.Any();
if (httpContext.Response == null)
{
httpContext.Stub( c => c.Response ).Return( MockRepository.GenerateStub<HttpResponseBase>() ).Repeat.Any();
}
if (url != "/")
{
url = url.TrimEnd( '/' );
}
httpContext.Response.Stub( r => r.ApplyAppPathModifier( Arg<string>.Is.Anything ) ).Return( url ).Repeat.Any();
return new UrlHelper( CreateRequestContext( httpContext, routeData ), GetRoutes() );
}
public static RequestContext CreateRequestContext( HttpContextBase httpContext, RouteData routeData )
{
return new RequestContext( httpContext, routeData );
}
// repeat your route definitions here!!!
public static RouteCollection GetRoutes()
{
RouteCollection routes = new RouteCollection();
routes.IgnoreRoute( "{resource}.axd/{*pathInfo}" );
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "home", action = "index", id = "" } // Parameter defaults
);
return routes;
}
}
I have 1 website on IIS ("myWebsite") and another inside this one ("secondWebsite") as an application. Both are ASP.NET Mvc websites.
I have a method who works perfectly on the first one :
public static string AbsolutePath(this UrlHelper url, string path)
{
Uri requestUrl = url.RequestContext.HttpContext.Request.Url;
string absoluteAction = string.Format("{0}{1}", requestUrl.GetLeftPart(UriPartial.Authority), path);
return absoluteAction;
}
The result is : http://myWebsite.com/path
I have the same method in the second Website, the result is the same, that's logic, but I don't want it !
The result should be : myWebsite.com/secondWebsite/path. (miss the http:// cause of spam prevention ^^).
Is there a good way to do that ?
Thanks.
You could try using
string absoluteAction = string.Concat(Request.Url.Authority,
Request.ApplicationPath, path);
Can you not use Server.ResolveUrl("~/Path"); as that rebases from application root.