Umbraco and dynamic URL content at root level - asp.net

I need to port a website to asp.net and decided to use Umbraco as the underlying CMS.
The issue I'm having is I need to retain the URL structure of the current site.
The current URL template looks like the following
domain.com/{brand}/{product}
This is hard to make a route for since it mixes in with all the other content on the site. Like domain.com/foo/bar which is not a brand or product.
I've coded up a IContentFinder, and injected it into the Umbraco pipeline, that check the URL structure and determins if domain.com/{brand} matches any of the known brands on the site, in which case i find the content by its internal route domain.com/products/ and pass along {brand}/{model} as HttpContext Items and return it using the IContentFinder.
This works, but it also means no MVC controller is called. So now I'm left with fetching from the database in the cshtml file which is not so pretty and kind of breaks MVC conventions.
What i really wan't is to take the url domain.com/{brand}/{product} and rewrite it to domain.com/products/{brand}/{product} and then being able to hit a ProductsController serving up the content based on the parameters brand and product.

There are a couple of ways to do this.
It depends a bit on your content setup. If your products exist as pages in Umbraco, then I think you are on the right path.
In your content finder, remember to set the page you've found on the request like this request.PublishedContent = content;
Then you can take advantage of Route Hijacking to add a ProductController that will get called for that request: https://our.umbraco.org/Documentation/Reference/Routing/custom-controllers
Example implementation:
protected bool TryFindContent(PublishedContentRequest docReq, string docType)
{
var segments = docReq.Uri.GetAbsolutePathDecoded().Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
string[] exceptLast = segments.Take(segments.Length - 1).ToArray();
string toMatch = string.Format("/{0}", string.Join("/", exceptLast));
var found = docReq.RoutingContext.UmbracoContext.ContentCache.GetByRoute(toMatch);
if (found != null && found.DocumentTypeAlias == docType)
{
docReq.PublishedContent = found;
return true;
}
return false;
}
public class ProductContentFinder : DoctypeContentFinderBase
{
public override bool TryFindContent(PublishedContentRequest contentRequest)
{
// The "productPage" here is the alias of your documenttype
return TryFindContent(contentRequest, "productPage");
}
}
public class ProductPageController : RenderMvcController {}
In the example the document type has an alias of "productPage". That means that the controller needs to be named exactly "ProductPageController" and inherit the RenderMvcController.
Notice that it does not matter what the actual pages name is.

Related

Alternative to Server.Transfer in ASP.NET Core

I am migrating an ASP.NET application to ASP.NET Core and they have some calls to HttpServerUtility.Transfer(string path). However, HttpServerUtility does not exist in ASP.NET Core.
Is there an alternative that I can use? Or is Response.Redirect the only option I have?
I want to maintain the same behaviour as the old application as much as possible since there is a difference in between Server.Transfer and Response.Redirect.
I see some options for you, depending on your case:
Returning another View: So just the HTML. See answer of Muqeet Khan
Returning another method of the same controller: This allows also the execution of the business logic of the other action. Just write something like return MyOtherAction("foo", "bar").
Returning an action of another controller: See the answer of Ron C. I am a bit in troubles with this solution since it omits the whole middleware which contains like 90% of the logic of ASP.NET Core (like security, cookies, compression, ...).
Routing style middleware: Adding a middleware similar to what routing does. In this case your decision logic needs to be evaluated there.
Late re-running of the middleware stack: You essentially need to re-run a big part of the stack. I believe it is possible, but have not seen a solution yet. I have seen a presentation of Damian Edwards (PM for ASP.NET Core) where he hosted ASP.NET Core without Kestrel/TCPIP usage just for rendering HTML locally in a browser. That you could do. But that is a lot of overload.
A word of advice: Transfer is dead ;). Differences like that is the reason for ASP.NET Core existence and performance improvements. That is bad for migration but good for the overall platform.
You are correct. Server.Transfer and Server.Redirect are quite different. Server.Transfer executes a new page and returns it's results to the browser but does not inform the browser that it returned a different page. So in such a case the browser url will show the original url requested but the contents will come from some other page. This is quite different than doing a Server.Redirect which will instruct the browser to request the new page. In such a case the url displayed in the browser will change to show the new url.
To do the equivalent of a Server.Transfer in Asp.Net Core, you need to update the Request.Path and Request.QueryString properties to point to the url you want to transfer to and you need to instantiate the controller that handles that url and call it's action method. I have provided full code below to illustrate this.
page1.html
<html>
<body>
<h1>Page 1</h1>
</body>
</html>
page2.html
<html>
<body>
<h1>Page 2</h1>
</body>
</html>
ExampleTransferController.cs
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace App.Web.Controllers {
public class ExampleTransferController: Controller {
public ExampleTransferController() {
}
[Route("/example-transfer/page1")]
public IActionResult Page1() {
bool condition = true;
if(condition) {
//Store the original url in the HttpContext items
//so that it's available to the app.
string originalUrl = $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}{HttpContext.Request.Path}{HttpContext.Request.QueryString}";
HttpContext.Items.Add("OriginalUrl", originalUrl);
//Modify the request to indicate the url we want to transfer to
string newPath = "/example-transfer/page2";
string newQueryString = "";
HttpContext.Request.Path = newPath;
HttpContext.Request.QueryString = new QueryString(newQueryString);
//Now call the action method for that new url
//Note that instantiating the controller for the new action method
//isn't necessary if the action method is on the same controller as
//the action method for the original request but
//I do it here just for illustration since often the action method you
//may want to call will be on a different controller.
var controller = new ExampleTransferController();
controller.ControllerContext = new ControllerContext(this.ControllerContext);
return controller.Page2();
}
return View();
}
[Route("/example-transfer/page2")]
public IActionResult Page2() {
string originalUrl = HttpContext.Items["OriginalUrl"] as string;
bool requestWasTransfered = (originalUrl != null);
return View();
}
}
}
Placing the original url in HttpContext.Items["OriginalUrl"] isn't strictly necessary but doing so makes it easy for the end page to know if it's responding to a transfer and if so what the original url was.
I can see this is a fairly old thread. I don't know when URL Rewriting was added to .Net Core but the answer is to rewrite the URL in the middleware, it's not a redirect, does not return to the server, does not change the url in the browser address bar, but does change the route.
resources:
https://weblog.west-wind.com/posts/2020/Mar/13/Back-to-Basics-Rewriting-a-URL-in-ASPNET-Core
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/url-rewriting?view=aspnetcore-5.0
I believe you are looking for a "named view" return in MVC. Like so,
[HttpPost]
public ActionResult Index(string Name)
{
ViewBag.Message = "Some message";
//Like Server.Transfer() in Asp.Net WebForm
return View("MyIndex");
}
The above will return that particular view. If you have a condition that governs the view details you can do that too.
I know that this is a very old question, but if someone uses Razor Pages and is looking to a Server.Transfer alternative (or a way to return a different view depending on a business rule), you can use partial views.
In this example, my viewmodel has a property called "UseAlternateView":
public class TestModel : PageModel
{
public bool UseAlternateView { get; set; }
public void OnGet()
{
// Here goes code that can set UseAlternateView=true in certain conditions
}
}
In my Razor View, I renderize a diferent partial view depending of the value of the UseAlternateView property:
#model MyProject.Pages.TestModel
#if (Model.UseAlternateView)
{
await Html.RenderPartialAsync("_View1", Model);
}
else
{
await Html.RenderPartialAsync("_View2", Model);
}
The partial views (files "_View1.cshtml" and "_View2.cshtml"), contain code like this:
#model MyProject.Pages.TestModel
<div>
Here goes page content, including forms with binding to Model properties
when necessary
</div>
Obs.: when using partial views like this, you cannot use #Region, so you may need to look for an anternative for inserting scripts and styles in the correct place on the master page.

How to convert url path to full or absolute url in ASP.NET MVC?

I am developing Web Application using ASP.NET MVC in C#. But I am having a problem with retrieving full or absolute url. In ASP.NET MVC we get url like this. Url.Content("~/path/to/page"). It will return "path/to/page". But what I want to do is I have a string like this - "~/controller/action".
Let's consider my website domain is www.example.com. If I use Url.Content("~/controller/action"), it will just return "controller/action". I want to get "www.example.com/controller/action". How can I get it?
If you can use the Controller / Action Names...
You should use the Url.Action() method for this.
Typically, Url.Action() will return something similar to what you presently expect when provided with just the Controller and Action names :
// This would yield "Controller/Action"
Url.Action("Action","Controller");
However, when you pass in the protocol parameter (i.e. http, https etc.) then the method will actually return a complete, absolute URL. For the sake of convienence, you can use the Request.Url.Scheme property to access the appropriate protocol as seen below :
// This would yield "http://your-site.com/Controller/Action"
Url.Action("Action", "Controller", null, Request.Url.Scheme);
You can see an example of this in action here.
If you only have a relative URL string...
If you only have access to something like a relative URL (i.e. ~/controller/action), then you may want to create a function that will extend the current functionality of the Url.Content() method to support serving absolute URLs :
public static class UrlExtensions
{
public static string AbsoluteContent(this UrlHelper urlHelper, string contentPath)
{
// Build a URI for the requested path
var url = new Uri(HttpContext.Current.Request.Url, urlHelper.Content(contentPath));
// Return the absolute UrI
return url.AbsoluteUri;
}
}
If defined properly, this would allow you to simply replace your Url.Content() calls with Url.AbsoluteContent() as seen below :
Url.AbsoluteContent("~/Controller/Action")
You can see an example of this approach here.
The following will render a full url, including http or https:
var url = new UrlHelper(System.Web.HttpContext.Current.Request.RequestContext);
var fullUrl = url.Action("YourAction", "YourController", new { id = something }, protocol: System.Web.HttpContext.Current.Request.Url.Scheme);
Output
https://www.yourdomain.com/YourController/YourAction?id=something

Changing website according to subdomain

I have a website written in ASP.NET.
I would like to add subdomains of states. such as nevada.mysite.com.
However, I'd like my whole site to be custom made for that subdomain.
That is, I'd like to capture in each page which subdomain context I am and show different things.
I do not want to seperate to different websites. I want them all to reside in the same website in the IIS
what is the best and proper way of handling such issue?
where do you suggest to hold and save the global variable of the state?
Thanks!
First, have all of your subdomains point to a single IP address in DNS.
Next, configure IIS to listen for all connections on that IP (don't specify a host name)
Then write an HttpModule (or perhaps use Global.asax) with a handler for the BeginRequest event. Extract the subdomain name from the incoming URL, and store it in HttpContext.Items["state"] (the Items Dictionary is unique per request).
This is a great question. I've done this before except I didn't use sub domains, I used different URL's, but they still used the same code and database. I wanted a way to integrate this a bit more tightly with my LINQ to SQL code without having to type in where clauses on each one. Here's what I did:
public static IEnumerable<T> GetDataByDomain<T>(
IQueryable<T> src) where T:IDbColumn
{
//1 == website1
//2 == website2
//3 == both
string url = HttpContext.Current.Request.Url.Host;
int i = url == "localhost"
|| url == "website1.com"
|| url == "www.website1.com" ? 1 : 2;
return src.Where(x => x.domainID == i|| x.domainID == 3);
}
Basically when querying a table with LINQ to SQL I have my own custom where clause.
Used like so:
using (var db = new MyDataContext())
{
var items = Utility.GetDataByDomain(db.GetTable<Item>()).Where(x => x.isVisible);
}
Finally in each table where I had data that needed to be specified for one web site or both I added a column that took a value of 1,2 or 3(both). Additionally in my LINQ data context I made a partial class and referenced my interface:
public partial class Item : Utility.IDbColumn
{
}
The reason we need the following interface is because the first method takes an unknown type so obviously I can't select a property from an unknown type unless I tell it that any type I pass to it relies on an interface which contains that property.
Interface:
public interface IDbColumn
{
int domainID { get; set; }
}
It's kind of an interesting system, probably could have done it in many different ways. Built it a while ago and it works great.

asp.net 4.0 web forms routing - default/wildcard route

I there a simple way when using ASP.NET 4.0 routing with Web Forms to produce a route that will act as some kind of wildcard?
It seems to me that within WebForms, you have to specify a route for every page - I am looking for some kind of generic route that can be used where nothing specific is required, perhaps mapping directly from path to path so...
http://somedomain.com/folder1/folder2/page would possibly map to folder1/folder2/page.aspx
Any suggestions?
Thanks
You can match all remaining routes like this:
routes.MapPageRoute("defaultRoute", "{*value}", "~/Missing.aspx");
In this case, we know all routes, and want to send anything else to a "missing"/404 page. Just be sure to put this as the last route, since it is a wildcard and will catch everything.
Alternatively you could register a route the same way, but internally does mapping to a page, like this:
routes.Add(new Route("{*value}", new DefaultRouteHandler()));
That handler class would do your wildcard mapping, something like this:
public class DefaultRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
//Url mapping however you want here:
var pageUrl = requestContext.RouteData.Route.Url + ".aspx";
var page = BuildManager.CreateInstanceFromVirtualPath(pageUrl, typeof(Page))
as IHttpHandler;
if (page != null)
{
//Set the <form>'s postback url to the route
var webForm = page as Page;
if (webForm != null)
webForm.Load += delegate { webForm.Form.Action =
requestContext.HttpContext.Request.RawUrl; };
}
return page;
}
}
This is broken a bit in odd places to prevent horizontal scrolling, but you get the overall point. Again, make sure this is the last route, otherwise it'll handle all your routes.
Additionally - Keep in mind that you need to add an exception for the .axd files in your Global.asax file if there are validation controls in your web app:
http://basgun.wordpress.com/2010/10/25/getting-syntax-error-in-asp-net-routing-due-to-webresource-axd/
Otherwise, you will keep getting a syntax error because the routing picks up the .axd files and not properly loads the JavaScript files needed for the validation controls.

How to use System.Web.Routing to not URL rewrite in Web Forms?

I am using System.Web.Routing with ASP.NET (3.5) Web Forms that will URL rewrite the following URL from
http://www.myurl.com/campaign/abc
to
http://www.myurl.com/default.aspx?campaign=abc
The code is as below:
public static void RegisterRoutes(RouteCollection routes)
{
routes.Add("CampaignRoute", new Route
(
"{campaign_code}",
new CustomRouteHandler("~/default.aspx")
));
}
IRouteHandler implementation:
public class CustomRouteHandler : IRouteHandler
{
public CustomRouteHandler(string virtualPath)
{
VirtualPath = virtualPath;
}
public string VirtualPath { get; private set; }
public IHttpHandler GetHttpHandler(RequestContext
requestContext)
{
if (requestContext.RouteData.Values.ContainsKey("campaign_code"))
{
var code = requestContext.RouteData.Values["campaign_code"].ToString();
HttpContext.Current.RewritePath(
string.Concat(
VirtualPath,
"?campaign=" + code));
}
var page = BuildManager.CreateInstanceFromVirtualPath
(VirtualPath, typeof(Page)) as IHttpHandler;
return page;
}
However I noticed there are too many things to change on my existing aspx pages (i.e. links to javascript, links to css files).
So I am thinking if there's a way to keep above code but in the end rather than a rewrite just do a Request.Redirect or Server.Transfer to minimize the changes needed. So the purpose of using System.Web.Routing becomes solely for URL friendly on the first entry.
How to ignore the rest of the patterns other than specificed in the code?
Thanks.
Using rewriting combined with ASP.NET URL Routing is not recommended because some implementations of ASP.NET URL Routing internally use rewriting as well (it depends on the version of ASP.NET). The combination of two different components using rewriting can cause conflicts (though I'm not 100% sure that that's why you're seeing this problem).
Regarding using transfer/redirect/rewrite:
My strongest recommendation would be to not use any of them! Instead of redirecting (or anything else) just let the page be called directly by ASP.NET by returning it from the IRouteHandler, much as you are already doing (just without the call to Rewrite). As long as your IRouteHandler saves the RouteData somewhere, the Page can then get the data from the route and you should be good to go.
Take a look at Phil Haack's Web Form routing sample to see an example of how to save the route data (or just use his code!).
Regarding ignoring patterns:
You can use an IRouteConstraint to constrain which URLs match your route. There is a built-in default route constraint implementation that uses regular expressions, but you can also write custom route constraints. Here is an example:
Route r = new Route(...);
r.Constraints = new RouteValueDictionary(new {
campaign_code = "\d{5}", // constrain to 5-digit numbers only
other_value = new CustomRouteConstraint(), // call custom constraint
});
CustomRouteConstraint is a class that you can write that derives from IRouteConstraint.
One thing I should note about static files such as CSS and JPG files is that by default they are always excluded from routing. By default routing ignores patterns that exactly match physical files on disk. You can change this behavior by setting RouteTable.Routes.RouteExistingFiles = true, but that is not the default.

Resources