C# Mvc Generic Route using Slug - c#

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());
}

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

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.
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);
}
}
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
}
}
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;
}
}

Related

Routing with only {id} parameter returns "resource cannot be found" error

I'm trying to setup routing as follows.
Right now My URL looks like www.mysite.com/Products/index/123
My goal is to setup URL like www.mysite.com/123
Where: Products is my controller name , index is my action name and 123 is nullable id parameter.
This is my route :
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"OnlyId",
"{id}",
new { controller = "Products", action = "index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
and this is my Action Method
public ActionResult index (WrappedViewModels model,int? id)
{
model.ProductViewModel = db.ProductViewModels.Find(id);
model.ProductImagesViewModels = db.ProductImagesViewModels.ToList();
if (id == null)
{
return HttpNotFound();
}
return View(model);
}
this is my model wrapper :
public class WrappedViewModels
{
public ProductViewModel ProductViewModel { get; set; }
public ProductImagesViewModel ProductImagesViewModel { get; set; }
public List<ProductImagesViewModel> ProductImagesViewModels { get; set;
}
Error is thrown on this URL : www.mysite.com/123
The question is:
Why my view returns this error and how to avoid this behavior?
Thanks in advance.
In RegisterRoutes you need to specify little bit more.
routes.MapRoute(
name: "OnlyId",
url: "{id}",
defaults: new { controller = "Products", action = "index" },
constraints: new{ id=".+"});
and then you need to specify the routing of each anchor tag as
#Html.RouteLink("123", routeName: "OnlyId", routeValues: new { controller = "Products", action = "index", id= "id" })
I think this will resolve you immediate.
If you're sure that id parameter is nullable integer value, place a route constraint with \d regex like this, so that it not affect other routes:
routes.MapRoute(
name: "OnlyId",
url: "{id}",
defaults: new { controller = "Products", action = "index" }, // note that this default doesn't include 'id' parameter
constraints: new { id = #"\d+" }
);
If you're not satisfied for standard parameter constraints, you can create a class which inheriting IRouteConstraint and apply that on your custom route as in this example:
// adapted from /a/11911917/
public class CustomRouteConstraint : IRouteConstraint
{
public CustomRouteConstraint(Regex regex)
{
this.Regex = regex;
}
public CustomRouteConstraint(string pattern) : this(new Regex("^(" + pattern + ")$", RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.IgnoreCase))
{
}
public Regex Regex { get; set; }
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.IncomingRequest && parameterName == "id")
{
if (values["id"] == UrlParameter.Optional)
return true;
if (this.Regex.IsMatch(values["id"].ToString()))
return true;
// checking if 'id' parameter is exactly valid integer
int id;
if (int.TryParse(values["id"].ToString(), out id))
return true;
}
return false;
}
}
Then place custom route constraint on id-based route to let other routes work:
routes.MapRoute(
name: "OnlyId",
url: "{id}",
defaults: new { controller = "Products", action = "index", id = UrlParameter.Optional },
constraints: new CustomRouteConstraint(#"\d*")
);
I think you missed Routing order. So create first route definition which handles all available controllers and then define one which will handle the rest of the requests, say, one that handles the www.mysite.com/{id} kind of requests.
So swap the OnlyId and Default rules and No more changes required. I believe It should work fine now.

Asp.net Mvc Route By Only Slug

I'm using ASP.NET MVC 4 with C#. I'm using areas and it's named like "Admin"
Here is my route config;
public static class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(name: "PageBySlug",
url: "{slug}",
defaults: new {controller = "Home", action = "RenderPage"},
constraints: new {slug = ".+"});
routes.MapRoute(name: "Default",
url: "{controller}/{action}/{id}",
defaults: new {controller = "Home", action = "Index", id = UrlParameter.Optional},
namespaces: new[] { "Web.Frontend.Controllers.Controllers" });
}
}
I generated frontend page links like; "products/apple-iphone"
So I want to call them like this.
But the error is: The code can't get the controller / action method.
I used frontend page links like;
#Html.ActionLink(linkItem.Title, "RenderPage", routeValues: new {controller = "Home", slug = linkItem.PageSlug})
#Html.RouteLink(linkItem.Title, routeName: "PageBySlug", routeValues: new { controller = "Home", action = "RenderPage", slug = linkItem.PageSlug })
#linkItem.Title
#linkItem.Title
They are rendering url links like; http://localhost:1231/products/apple-iphone
It's like what I want. But when I click any link, asp.net mvc gives me this error:
Server Error in '/' Application.
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
Requested URL: /products/apple-iphone
Version Information: Microsoft .NET Framework Version:4.0.30319; ASP.NET Version:4.6.1069.1
Here is my controller;
namespace Web.Frontend.Controllers
{
public class HomeController : BaseFrontendController
{
public ActionResult Index()
{
return View();
}
public ActionResult RenderPage(string slug)
{
return View();
}
}
}
So how can I catch every link request like this combined slug and turn my coded view ?
The problem is, When you request products/iphone, the routing engine don't know whether you meant the slug "products/iphone" or the controller "products" and action method "iphone".
You can write a custom route constraint to take care of this. This constraint will check whether the slug part of the urls is a valid controller or not, if yes,the controller action will be executed.
public class SlugConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
var asm = Assembly.GetExecutingAssembly();
//Get all the controller names
var controllerTypes = (from t in asm.GetExportedTypes()
where typeof(IController).IsAssignableFrom(t)
select t.Name.Replace("Controller", ""));
var slug = values["slug"];
if (slug != null)
{
if (controllerTypes.Any(x => x.Equals(slug.ToString(),
StringComparison.OrdinalIgnoreCase)))
{
return false;
}
else
{
var c = slug.ToString().Split('/');
if (c.Any())
{
var firstPart = c[0];
if (controllerTypes.Any(x => x.Equals(firstPart,
StringComparison.OrdinalIgnoreCase)))
{
return false;
}
}
}
return true;
}
return false;
}
}
Now use this route constraint when you register your custom route definition for the slug. make sure you use {*slug} in the route pattern. The * indicates it is anything(Ex : "a/b/c")(Variable number of url segments- more like a catch all)
routes.MapRoute(name: "PageBySlug",
url: "{*slug}",
defaults: new { controller = "Home", action = "RenderPage" },
constraints: new { slug = new SlugConstraint() }
, namespaces: new string[] { "Web.Frontend.Controllers.Controllers" });
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
, new string[] { "Web.Frontend.Controllers.Controllers" });
you can provide only this type of link
#linkItem.Title
Because Routetable find your route using Route name provided by you. so controller name and action name is not necessary.

Handle Multiple action with same name in MVC

In my project there is an action
public ActionResult Lead(int leadId)
{
return View();
}
and in the View an ActionLink was created like this
#Html.ActionLink("Old Link", "Lead", "Home", new { leadId = 7 }, null)
But after some time, to make clean URL, I have changed the name of parameter of that action
public ActionResult Lead(int id)
{
return View();
}
And ActionLink change accordingly
#Html.ActionLink("New Link", "Lead", "Home", new { id = 5 }, null)
But old link was shared in multiple social network sites. Whenever anyone clicks on that old link, he is redirect to the page www.xyx.com/Home/Lead?leadId=7
But now in my application, no such URL exists.
To handle this problem, I was thinking of overloading, but MVC action doesn't support overloading.
I have created another Action with same name with extra parameter, and redirect to new action, but it doesn't work.
public ActionResult Lead(int leadId, int extra=0)
{
return RedirectToAction("Lead", "Home", new { id = leadId });
}
I have found one link to handle such situation, but It is not working in my case.
ASP.NET MVC ambiguous action methods
One possibility to handle this would be to write a custom route:
public class MyRoute : Route
{
public MyRoute() : base(
"Home/Lead/{id}",
new RouteValueDictionary(new
{
controller = "Home",
action = "Lead",
id = UrlParameter.Optional,
}),
new MvcRouteHandler()
)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var leadId = httpContext.Request.QueryString["leadid"];
if (!string.IsNullOrEmpty(leadId))
{
rd.Values["id"] = leadId;
}
return rd;
}
}
that you will register before the default one:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new MyRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
and now you could only have a single action:
public ActionResult Lead(int id)
{
return View();
}
Now both the following urls will work as expected:
www.xyx.com/Home/Lead/7
www.xyx.com/Home/Lead?leadId=7

How do I configure my Route config to get a URL Pattern like localhost/Product (Controller)/List (Action)/Category (Id)?

As the title says all about what I want but to be kind of specific I would like to have a URL pattern like localhost/Product/List/Category/Page but I couldn't succeed finding a solution for it. I am sure it's belong to routing and as it is difficult topic in MVC I would need your help to find a solution for it.
My route config is:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(null, "",
new
{
controller = "Home",
action = "Shop",
});
routes.MapRoute(null, "",
new
{
controller = "Product",
action = "list",
category = (string)null,
page = 1
}
);
routes.MapRoute(null, "Page{page}",
new
{
controller = "Product",
action = "List",
category = (string)null,
subcategory = (string)null
},
new { page = #"\d+" }
);
routes.MapRoute(null,
"{category}",
new { controller = "Product", action = "List", page = 1 }
);
routes.MapRoute(null,
"{category}/Page{page}",
new { controller = "Product", action = "List" },
new { page = #"\d+" }
);
routes.MapRoute(null, "{controller}/{action}");
}
}
My Controller product is:
public class ProductController : Controller
{
EFDbContext db = new EFDbContext();
private IProductsRepository repository;
public int PageSize = 4;
public ProductController (IProductsRepository productrepository)
{
this.repository = productrepository;
}
public ViewResult List(string category, int page = 1)
{
ProductsListViewModel model = new ProductsListViewModel()
{
Products = repository.Products
.Where(p => category == null || p.ProductCategory == category || p.MenSubCategory == category)
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo
{
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = category == null ? repository.Products.Count():repository.Products.Where(e => e.ProductCategory == category).Count()
},
CurrentCategory = category
};
return View(model);
}
public PartialViewResult Menu(string subcategory = null )
{
ViewBag.SelectedCategory = subcategory;
IEnumerable<string> categories = repository.MenSubCategories
.Select(x => x.MenSubCategory)
.Distinct()
.OrderBy(x => x);
return PartialView(categories);
}
}
I hope I get answer for this as far as I really tried but couldn't find how to do it.
To generate an URL like you want: localhost/Product/List/Cars you can create a custom route like this:
routes.MapRoute(
name: "ProductList",
url: "Product/List/{category}",
defaults: new { controller = "Product", action = "List" }
);
Remember that custom routes have to come before the most general route (the one that comes with the default template).
Regarding your page parameter, if you are comfortable with this URL: localhost:3288/Product/List/teste?page=10 the above already work. But if you want this: localhost:3288/Product/List/teste/10 10 meaning the page number, then the simplest solution would be create two different routes:
routes.MapRoute(
name: "ProductList",
url: "Product/List/{category}",
defaults: new { controller = "Product", action = "List" }
);
routes.MapRoute(
name: "ProductListPage",
url: "Product/List/{category}/{page}",
defaults: new { controller = "Product", action = "List" , page = UrlParameter.Optional}
);
Another cleaner way, is to create a custom route constraint for your optional parameter. This question has a lot of answers to that:
ASP.NET MVC: Route with optional parameter, but if supplied, must match \d+
With attribute routing, you just need to decorate your action method with your specific route pattern.
public class ProductController : Controller
{
EFDbContext db = new EFDbContext();
private IProductsRepository repository;
public int PageSize = 4;
public ProductController (IProductsRepository productrepository)
{
this.repository = productrepository;
}
[Route("Product/list/{category}/{page}")]
public ViewResult List(string category, int page = 1)
{
// to do : Return something
}
}
The above route definition will send the request like yourSite/Product/list/phones and yourSite/Product/list/phones/1 to the List action method and the url segments for category and page will be mapped to the method parameters.
To enable attribute routing, you need to call the method MapMvcAttributeRoutes
method inside the RegisterRoutes method.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Creating an Html.ActionLink to a dynamic content page

I have functionality on my site to create/edit/delete pages for the front end. Here's my controller:
namespace MySite.Controllers
{
public class ContentPagesController : Controller
{
readonly IContentPagesRepository _contentPagesRepository;
public ContentPagesController()
{
MyDBEntities entities = new MyDBEntities();
_contentPagesRepository = new SqlContentPagesRepository(entities);
}
public ActionResult Index(string name)
{
var contentPage = _contentPagesRepository.GetContentPage(name);
if (contentPage != null)
{
return View(new ContentPageViewModel
{
ContentPageId = contentPage.ContentPageID,
Name = contentPage.Name,
Title = contentPage.Title,
Content = contentPage.Content
});
}
throw new HttpException(404, "");
}
}
}
And in my global.asax:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Page", // Route name
"Page/{name}", // URL with parameters
new { controller = "ContentPages", action = "Index" }, // Parameter defaults
new[] { "MySite.Controllers" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new[] { "MySite.Controllers" }
);
}
So I have a dynamic page in my database, named About. If I go to mysite.com/Page/About, I can view the dynamic content.
I want to create an ActionLink to this page. I've tried it like this:
#Html.ActionLink("About Us", "Index", "ContentPages", new { name = "About" })
But when I look at the link on the page, the url just goes to the current page with Length=12 in the query string. For instance, if I'm on the homepage, the link goes to mysite.com/Home?Length=12
What am I doing wrong here?
You are not using the correct ActionLink overload. Try like this:
#Html.ActionLink(
"About Us", // linkText
"Index", // action
"ContentPages", // controller
new { name = "About" }, // routeValues
null // htmlAttributes
)
whereas in your example:
#Html.ActionLink(
"About Us", // linkText
"Index", // action
"ContentPages", // routeValues
new { name = "About" } // htmlAttributes
)
which pretty obviously explains why your doesn't generate the expected link.

Categories

Resources