4

I'm trying to create a generic route to work with slugs, but I always got an error

The idea is, instead of www.site.com/controller/action I get in the url a friendly www.site.com/{slug}

e.g. www.site.com/Home/Open would be instead www.site.com/open-your-company

Error

server error in '/' application The Resource cannot be found

In my Global.asax I have

public static void RegisterRoutes(RouteCollection routes)
{
    //routes.Clear();
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute("DefaultSlug", "{slug}", new { controller = "Home", action = "Open", slug = "" });
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new
        {
            area = "",
            controller = "Home",
            action = "Index",
            id = UrlParameter.Optional,
            slug = ""
        }
    );
}

In one of my cshtml I have the following link (even when it's commented, there is still the same error).

@Html.ActionLink("Open your company", "DefaultSlug", new { controller = "Home", action = "Open", slug = "open-your-company" })

EDIT: HomeController

public ActionResult Open() { 
    return View(new HomeModel()); 
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
Michel Ayres
  • 5,891
  • 10
  • 63
  • 97

2 Answers2

2

In Global.asax you slug can not be empty,if empty ,the url will be not go to the default route

public static void RegisterRoutes(RouteCollection routes)
{
    //routes.Clear();
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "DefaultSlug",
        url: "{slug}",
        defaults: new { controller = "Home", action = "Open" },
        constraints: new{ slug=".+"});

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new
        {
            area = "",
            controller = "Home",
            action = "Index",
            id = UrlParameter.Optional
        }
    );
}

And update the HomeController

public ActionResult Open(string slug) {
    HomeModel model = contentRepository.GetBySlug(slug);

    return View(model); 
}

Testing Route link...

@Html.RouteLink("Open your company", routeName: "DefaultSlug", routeValues: new { controller = "Home", action = "Open", slug = "open-your-company" })

and Action link...

@Html.ActionLink("Open your company", "Open", routeValues: new { controller = "Home", action = "Open", slug = "open-your-company" })

both produces...

http://localhost:35979/open-your-company
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Still testing, but this code will not create "controller/action?slug" ? I'm trying to get something like ***www.site.com/slug*** instead of "www.site.com/controller/action" – Michel Ayres May 12 '16 at 17:22
  • Then use the original action link format you had in your post where you call the route by name `DefaultSlug` – Nkosi May 12 '16 at 17:26
  • This is what I'm trying, but didn't seem to work, the error persists. Any idea? – Michel Ayres May 12 '16 at 17:37
  • 1
    i believe that the error is because I'm using ActionLink instead I should be using RouteLink ... trying some code to see if work. – Michel Ayres May 12 '16 at 17:42
  • In the html `@Html.RouteLink("Open company", "DefaultSlug", new { slug = "open-your-company", controller = "Home", action = "Open" })` in the asax `routes.MapRoute( name: "DefaultSlug", url: "{slug}", defaults: new { slug = "", controller = "Home", action = "Open" } );` I just didn't figured out how to make it a gereric route, or at LEAST not the need to pass `controller` and `action` in the html too (removing cause the same error) – Michel Ayres May 12 '16 at 17:56
  • @MichelAyres The issue was how the link methods were being called. Once the correct format is used everything worked as expected. – Nkosi May 12 '16 at 18:22
  • Now it only gets the first declared route (ignoring even the default)... `routes.MapRoute(name: "OpenSlug", url: "{slug}", defaults: new { controller = "Home", action = "Open", slug = "" });` and `routes.MapRoute(name: "TransferSlug", url: "{slug}", defaults: new { controller = "Home", action = "Transfer", slug = "" });` – Michel Ayres May 12 '16 at 18:49
  • That's because slug cannot be empty. Check my answer and see how the routes were mapped. Also, in your last comment it looks like the two routes you show have the same template. Is your intention to create a mapping for each action? – Nkosi May 12 '16 at 18:53
  • My intent is to create one single route that accepts 'controller' and 'action' as parameters. BUT I could do that, So I was trying to do one route for action (there is only 5 after all) and he is reading as the same route (even the Default is not being read) – Michel Ayres May 12 '16 at 18:56
  • The link `@Html.RouteLink("Open company", "DefaultSlug", new { slug = "open-company" })` works like a charm, perfect. When I try to use it in another link, like `@Html.RouteLink("About us", "DefaultSlug", new { slug = "about-us", controller = "Home", action = "About" })` ... he redirect to the same page *(like a **#**)* the asax is `routes.MapRoute( name: "DefaultSlug", url: "{slug}", defaults: new { controller = "Home", action = "Open" }, constraints: new{ slug=".+"} );` – Michel Ayres May 12 '16 at 19:05
0

Here's the steps I took to accomplish a similar task. This relies on a custom Slug field on the model to match against the route.

  1. Set up your controller e.g. Controllers\PagesController.cs:

    public class PagesController : Controller
    {
        // Regular ID-based routing
        [Route("pages/{id}")]
        public ActionResult Detail(int? id)
        {
            if(id == null)
            {
                return new HttpNotFoundResult();
            }
    
            var model = myContext.Pages.Single(x => x.Id == id);
            if(model == null)
            {
                return new HttpNotFoundResult();
            }
            ViewBag.Title = model.Title;
            return View(model);
        }
    
        // Slug-based routing - reuse View from above controller.
        public ActionResult DetailSlug (string slug)
        {
            var model = MyDbContext.Pages.SingleOrDefault(x => x.Slug == slug);
            if(model == null)
            {
                return new HttpNotFoundResult();
            }
            ViewBag.Title = model.Title;
            return View("Detail", model);
        }
    }
    
  2. Set up routing in App_Start\RouteConfig.cs

    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            // Existing route register code
    
            // Custom route - top priority
            routes.MapRoute(
                    name: "PageSlug", 
                    url: "{slug}", 
                    defaults: new { controller = "Pages", action = "DetailSlug" },
                    constraints: new {
                        slug = ".+", // Passthru for no slug (goes to home page)
                        slugMatch = new PageSlugMatch() // Custom constraint
                    }
                );
            }
    
            // Default MVC route setup & other custom routes
        }
    }
    
  3. Custom IRouteConstraint implementation e.g. Utils\RouteConstraints.cs

    public class PageSlugMatch : IRouteConstraint
    {
        private readonly MyDbContext MyDbContext = new MyDbContext();
    
        public bool Match(
            HttpContextBase httpContext,
            Route route,
            string parameterName,
            RouteValueDictionary values,
            RouteDirection routeDirection
        )
        {
            var routeSlug = values.ContainsKey("slug") ? (string)values["slug"] : "";
            bool slugMatch = false;
            if (!string.IsNullOrEmpty(routeSlug))
            {
                slugMatch = MyDbContext.Pages.Where(x => x.Slug == routeSlug).Any();
            }
            return slugMatch;
        }
    }
    
murraybiscuit
  • 839
  • 7
  • 15