1

Imagining an online store written in ASP.Net MVC, where I have a controller for displaying products and product details. Given the following code

public class ProductsController : Controller {

    public IActionResult Index(){
        return View(ProductList);
    }
    public IActionResult Details(String id){
        return View(FindProduct(id));
    }
}

The URL displayed in the web browser's address bar when looking up a product is something similar to this: www.onlineshop.com/Products/Details?id=1234.

I have been studding SEO and I have been thinking that it would be a better idea if I re-route the URL to something more detailed such as:

www.onlineshop.com/Products/Sony/Gaming/PlayStation/PS4Model12345

How can this be achieved in ASP.Net MVC (Core)? (given the example controller I provided)

Arnold Zahrneinder
  • 4,788
  • 10
  • 40
  • 76
  • Maybe go read some tutorials, this is a very broad question. – DavidG Feb 26 '17 at 18:41
  • @DavidG: It isn't, it is limited to routing only David. and more limited to routing asp.net core. – Arnold Zahrneinder Feb 26 '17 at 18:42
  • No, it's MVC routing, adding slugs to your entities, it affects the entire pipeline of your logic. – DavidG Feb 26 '17 at 18:44
  • @DavidG: Do you think forwarding the `Details` to another action in order to re-write the URL is a good idea? – Arnold Zahrneinder Feb 26 '17 at 18:46
  • No I don't think that's a good idea. – DavidG Feb 26 '17 at 18:47
  • @DavidG: If I use the route attribute or the HttpGet attribute and define a route, then I have to pass in all the required parameters which may not available in the UI. I mean I can get those info only in the server side. So what is your suggestion? – Arnold Zahrneinder Feb 26 '17 at 18:50
  • My suggestion was in the [first comment I made](http://stackoverflow.com/questions/42472547/mvc-core-routing-for-displaying-detailed-information-in-the-url#comment72085615_42472547). – DavidG Feb 26 '17 at 18:50
  • @DavidG: Then I assume you have no idea. – Arnold Zahrneinder Feb 26 '17 at 18:51
  • Then you would be wrong, my point (again) is that the question is far too broad for the Stack Overflow format. – DavidG Feb 26 '17 at 18:52
  • @DavidG: I respect what you think David, not here to start a war on what is right and what is wrong. – Arnold Zahrneinder Feb 26 '17 at 18:53
  • Search for how to [implement `IRouter`](http://www.inversionofcontrol.co.uk/asp-net-core-1-0-routing-under-the-hood/) - that is the interface that allows you to totally control what the URL looks like (and possibly even make it database-driven). – NightOwl888 Feb 26 '17 at 18:57
  • @NightOwl888: That's really interesting, thanks. – Arnold Zahrneinder Feb 26 '17 at 18:58
  • @NightOwl888: The IRouter did not solve the problem as I still have to change my controllers. It is exactly same as forwarding one action to another as I mentioned earlier. – Arnold Zahrneinder Feb 26 '17 at 19:17
  • You need to make a custom IRouter implementation. I created one for an early beta release of MVC Core [here](http://stackoverflow.com/a/32586837/181087), but the API has changed a bit since then and I haven't had a chance to come up with the solution to fix it. You don't necessarily have to change your controller actions, but it is more complicated to map them if you use multiple route values that change per URL than if you just use a primary key to URL map (and a single controller action per route). – NightOwl888 Feb 26 '17 at 19:22
  • @NightOwl888: The way it works is trough looking for requests with a certain route and forward them to another, correct me if I'm wrong. So that means it does not work if the a URL such as 'www.shop.com/Sony/123/123' is totally nonexistent. – Arnold Zahrneinder Feb 26 '17 at 19:26
  • No, you can make up whatever URL that you want - see my example - it is pulling the URL from the database (and can basically be anything). IRouter allows you to make any virtual path (URL) and map it to any set of route values (controller, action, id, and anything else you want). Implementing IRouter also ensures when you use ActionLink, you get the same URL that your application responds to. – NightOwl888 Feb 26 '17 at 19:30
  • @NightOwl888: In your example you await `await _target.RouteAsync(context);` inside the `RouteAsync`. This results in recursion, I'm not sure, maybe that's why that example does not work. – Arnold Zahrneinder Feb 26 '17 at 19:42
  • No, that example doesn't work because Microsoft changed the API (eliminating the 2 properties you used to use to tell it that it is completed). Perhaps by analyzing the [MVC source](https://github.com/aspnet/Mvc), you can work out how to fix it (and if you do, let me know). – NightOwl888 Feb 26 '17 at 20:26

1 Answers1

1

The answer for this question is actually very simple. I don't know why the other users here recommended considerably lengthy solutions but in my opinion it can be carried out by adding the Route attribute on the Details API. When someone needs to display a list of products inside of a View it is very unlikely and illogical if they only display the Product Id and normally an IEnumerable<Product> is passed to the Index view in the situation asked in the question. Accordingly, the following can be done:

[Route("Products/Details/{Manufacturer=unnamed}/{Category=unnamed}/{ActualProduct=unnamed}/{Model=unnamed}/{URLTrail=unnamed}")]
public IActionResult Details(GUID id) {
    return View(FindProduct(id));
} 

Example output: www.MyOnlineShop.com/Products/Details/Microsoft/Surface/SurfacePro/xxxx-xxxx-....

Now it really does not matter if some of these URL parameters are missing but the user can have the option to render the URL as they like !.

Update:

Building the URL inside of the view is more SEO friendly as it creates reasonable links!

A better example, in case of stackoverflow, the URL for this question is

http://stackoverflow.com/questions/42472547/mvc-core-routing-for-displaying-detailed-information-in-the-url

In this example the actual question id is 42472547 and the trailing part is for SEO so that Google can search the question. In this example the following route applies:

[Route("Questions/{id}/{SEOHint}"]

and the {id} part is used for looking up the question not the {SEOHint} part.

Updated Solution

I have written the following attribute that can be customized for implementing this functionality.

public abstract class URLOptimizerAttributeBase : Attribute, IRouteTemplateProvider, IActionFilter
{
    public String Template { get; private set; }
    public int? Order { get; set; }
    public string Name { get; set; }
    public URLOptimizerAttributeBase(String Template) {
        this.Template = Template;
    }
    public abstract Boolean Validate(RouteData routeData);
    public abstract RedirectToRouteResult Redirect(RouteValueDictionary routeValueDictionary);
    public abstract void OnActionExecuted(ActionExecutedContext context);
    public virtual void OnActionExecuting(ActionExecutingContext context)
    {
        if (!Validate(context.RouteData))
        {
            context.Result = Redirect(context.RouteData.Values);
        }    
    }
}

The following provides an example of the implementation of this attribute.

public class URLOptimization : URLOptimizerAttributeBase
{
    private String SEOHint;
    public URLOptimization(string Template) : base(Template)
    {
    }

    public object DbContext { get; private set; }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
    }
    public override bool Validate(RouteData routeData)
    {
        return routeData.Values["SEOHint"].Equals(SEOHint);
    }
    public override RedirectToRouteResult Redirect(RouteValueDictionary routeValueDictionary)
    {
        routeValueDictionary["SEOHind"] = SEOHint;
        return new RedirectToRouteResult(routeValueDictionary);
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var id = context.RouteData.Values["Id"];
        DbContext db = (DbContext)context.HttpContext.RequestServices.GetService(typeof(DbContext));
        var product = db.Find<Products>(id);
        if (product != null)
        {
            SEOHint = product.SEOURL;
            ((Controller)context.Controller).ViewBag.Data = product;
             base.OnActionExecuting(context);
        }
        else {
            context.Result = new RedirectResult("Error Page");
        }
    }
}

And the example of using this attribute would be as follows:

[URLOptimization("Products/Details/{Id}/{SEOHint=unnamed}")]
public IActionResult Details(){
    return View(ViewBag.Data);
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Transcendent
  • 5,598
  • 4
  • 24
  • 47
  • but how we get information: manufacturer, category and productname inside controller? – Alexan Feb 26 '17 at 21:00
  • @Alex: When you wanna display a list of products, you pass something like `IEnumerable` to your view where the products are listed. When you wanna view details, then since the model is already in the view, you can create the URL and look it up by Id. – Transcendent Feb 26 '17 at 21:12
  • but should these parameters be included into Detail signature? – Alexan Feb 26 '17 at 21:34
  • @Alex: not necessarily, they are not mandatory. However, if there are parameters that are not available in the view but must be part of the URL, an ActionFilter can resolve the problem. – Transcendent Feb 26 '17 at 21:46