We want to migrate a Website from Asp.Net Webforms to Asp.Net Core Webapplication (Razor Pages). We now have readable Urls without visible identifiers (for readability, SEO and to prevent changing Urls after possible database migrations...).
For that i generated dynamically Routes in the Global.Asax at Startup:
RouteTable.Routes.MapPageRoute("myroute1",
"banana/",
"~/content.aspx", false, new RouteValueDictionary { { "id", "4711" }, , { "otherid", "4812" }
RouteTable.Routes.MapPageRoute("myroute2",
"apple/",
"~/content.aspx", false, new RouteValueDictionary { { "id", "4913" }, , { "otherid", "5014" }
That way users could call the content.aspx like this:
https://example.com/banana
https://example.com/apple
In the content.aspx i got the mapped "id" and "otherid" from the RouteValues.
How can i achieve this in Razor Pages?
Now i have a "Content.cshtml" and there i need access to id and otherid. I added this at the top:
#page "/content/{id:int}/{otherid:int}/{title:string}"
The above code allows mit to call Urls like this:
https://example.com/content/4711/4812/banana // still have the ids in it and prefix "content"
Is there a possibility to add all Routes at Startup with fixed Parameters? I have not been able to find anything similar.
Greetings
cpt.oneeye
I found a solution thanks to this thread:
Is there a way to do dynamic routing with Razor Pages?
Another full example can be found here:
https://github.com/dotnet/AspNetCore.Docs/issues/12997
According to my example at the top you first make your own DynamicRouteValueTransformer (see Namespace Microsoft.AspNetCore.Mvc.Routing):
public class NavigationTransformer : DynamicRouteValueTransformer
{
private readonly MyDatabaseContext _repository;
public NavigationTransformer(MyDatabaseContext repository)
{
_repository = repository;
}
public override async ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
{
// all requests which are incoming and fit a specific pattern will go through this method
var slug = values["slug"] as string;
// just get your needed info according to the "slug"
// in my case i have to map according to NavigationNodes that are stored in the Database
var myNavigationNode = _repository.NavigationNodes.FirstOrDefault(nn => nn.Name == slug);
if (node == null)
{
// User will get a 404
return null;
}
// set Path to Page according to Type (or whatever fits your logic)
string pagename = "";
if(node.Type == "ContentPage")
{
pagename = "/Content" // Pages/Content.cshtml
}
else if(node.Type == "ContactForm")
{
pagename = "/Contact" // Pages/Contact.cshtml
}
else
{
pagename = "/Someotherpage"
}
// return all RouteValues you need on your Page
return new RouteValueDictionary()
{
{ "page", pagename },
{ "id", node.Id },
{ "otherid", node.OtherId }
};
}
}
In the Startup.ConfigureServices() you register the new Service:
services.AddScoped<NavigationTransformer>();
In the Startup.Configure() you add the NavigationTransformer to Endpoints:
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapDynamicPageRoute<NavigationTransformer>("{slug}"); // just "slug" so that every request will be handled by NavigationTransformer, you can also add a prefix-folder
});
Now when you call a url like the following you will came through the Transformer and you are able to reroute on the fly:
https://example.com/banana
https://example.com/apple
Be aware the Routings of existing Pages are stronger. So if we have a Apple.cshtml the second Url will still be routed to Apple.cshtml and not to Content.cshtml
To have a static url, you can put your routes in startup class. for example:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default1",
pattern: "{controller=Home}/{action=Index}/{id?}"
);
});
In this case, you can define the endpoints as much as you want. For example, for your routes, you can write like this:
endpoints.MapControllerRoute(
name: "default",
pattern: "{content}/{id?}/{id?}/{banana}"
);
Or you can use Attribute Routing that you can use for each of your controllers. read more about that in
sorry for my English🙏
Related
I'm using https://github.com/ardalis/ApiEndpoints (one action per controller) for my project, and I run into issue that [Route("[controller]")] is not really suitable for me since controllers look like this:
I need something more like [Route("[namespace]")], but that's not supported in ASP.NET Core.
Is there a way, to add custom route token resolution in Startup.cs?
My solutions so far:
Hardcode routes
Create custom attribute that would contain route with custom tokens, and source generator that would resolve custom tokens and generate Route attribute.
Big thanks to #Kahbazi for pointing me in the right direction!
Here's what I came up with:
private class CustomRouteToken : IApplicationModelConvention
{
private readonly string _tokenRegex;
private readonly Func<ControllerModel, string?> _valueGenerator;
public CustomRouteToken(string tokenName, Func<ControllerModel, string?> valueGenerator)
{
_tokenRegex = $#"(\[{tokenName}])(?<!\[\1(?=]))";
_valueGenerator = valueGenerator;
}
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
string? tokenValue = _valueGenerator(controller);
UpdateSelectors(controller.Selectors, tokenValue);
UpdateSelectors(controller.Actions.SelectMany(a => a.Selectors), tokenValue);
}
}
private void UpdateSelectors(IEnumerable<SelectorModel> selectors, string? tokenValue)
{
foreach (var selector in selectors.Where(s => s.AttributeRouteModel != null))
{
selector.AttributeRouteModel.Template = InsertTokenValue(selector.AttributeRouteModel.Template, tokenValue);
selector.AttributeRouteModel.Name = InsertTokenValue(selector.AttributeRouteModel.Name, tokenValue);
}
}
private string? InsertTokenValue(string? template, string? tokenValue)
{
if (template is null)
{
return template;
}
return Regex.Replace(template, _tokenRegex, tokenValue);
}
}
Configure the token in Startup.cs (this can be wrapped in an extension method):
services.AddControllers(options => options.Conventions.Add(
new CustomRouteToken(
"namespace",
c => c.ControllerType.Namespace?.Split('.').Last()
));
After that custom token can be used for routing:
[ApiController]
[Route("api/[namespace]")]
public class Create : ControllerBase {}
[ApiController]
public class Get : ControllerBase
{
[HttpGet("api/[namespace]/{id}", Name = "[namespace]_[controller]")]
public ActionResult Handle(int id) {}
}
You can achieve that with implementing IApplicationModelConvention. more info on here : Custom routing convention
public class NamespaceRoutingConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel()
{
Template = controller.ControllerType.Namespace.Replace('.', '/') + "/[controller]}"
};
}
}
}
And then add it in startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Conventions.Add(new NamespaceRoutingConvention());
});
}
Areas are exactly what you need. As mentioned in .net core documentation:
Areas are an ASP.NET feature used to organize related functionality
into a group as a separate:
Namespace for routing.
Folder structure for views and Razor Pages.
To use area routes you need only to add them to the startup.
Like this:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "MyArea",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
After that the route will follow your folder structure inside the Areas folder.
For more information visit the Areas in ASP.NET Core documentation.
I am trying to localize my app's URLs. Unfortunately, most of the pages show me examples of app localization Like :
http://localhost/en-US/Home/Index
This is not what I want. I would to localize the URLs like that:
http://localhost/Welcome
http://localhost/Bienvenue [ welcome word in French ]
The culture has aleady been managed on my side with a cookie and working well with "CookieRequestCultureProvider" class.
So I have this information and localization in pages are OK.
I succeeded to register all the routes I need. Both of example above working and display the page. Thanks to this :
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
foreach (var action in controller.Actions)
{
var localizedRouteAttributes = action.Attributes.OfType<LocalizedRouteAttribute>().ToArray();
if (localizedRouteAttributes.Any())
{
foreach (var localizedRouteAttribute in localizedRouteAttributes)
{
var localizedVersions = GetLocalizedVersionsForARoute(localizedRouteAttribute.Name); // GetLocalizedVersionsForARoute contains all routes translated and group by culture.
foreach (var localizedVersion in localizedVersions)
{
if (!action.Selectors.Any(s => s.AttributeRouteModel.Template == localizedVersion.Template))
action.Selectors.Add(new SelectorModel(action.Selectors.First()) { AttributeRouteModel = localizedVersion });
}
}
}
}
}
}
So mvc take the last route register in Selectors (if FR, it take FR route).
I can't manage the other routes by this piece of code because it's load with the app. And can't work with a dynamic use (The app permit to change the lang when I want).
Thanks in advance.
I found this example project works: https://github.com/tomasjurasek/AspNetCore.Mvc.Routing.Localization
Once it's set up, you can tag routes with
[LocalizedRoute("culture", "RouteName")]
Do that for each culture you want a unique name for, and the dynamic route it creates will translate to the proper action and execute it. It's also got a tag helper for creating translated links, though if you want to use Url.Action or Html.ActionLink, I find you have to create extension methods that take the culture into account to get them to work fully.
In your case wanting them at the route level instead of /culture/Controller/Action may take some more work, but it might be a useful starting place for you.
look in this little example I hope to help you :)
1) in your controller :
[RoutePrefix("/")]
public HomeController : Controller {
[HttpGet]
[Route("Welcome")]
public ActionResult Index() {
return View();
}
}
And enable it in route table " routes.MapMvcAttributeRoutes(); " like this
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//enable attribute routing
routes.MapMvcAttributeRoutes();
//convention-based routes
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = "" }
);
}
}
I suggest reading this article from this URL:
https://blogs.msdn.microsoft.com/webdev/2013/10/17/attribute-routing-in-asp-net-mvc-5/
if you have any other question you can ask me
Currently I am using the default route but I need a route that works like this:
localhost/Admin/Users/Index
localhost/Admin/Users/Add
localhost/Admin/Users/Delete
Where index add and delete are views with controllers in the AdminController.cs
The current structure everywhere else is fine, as it doesn't require multiple subdirectories.
Currently I have the the file I need to start with in:
{Project}/Views/Admin/Users/Index.cshtml
How would I create this route and how do I apply it to the controller?
Am I approaching this incorrectly?
This can be easily resolved using Route attributes, like:
[Route("Admin/Users/Edit/{id?}")]
public ActionResult TestView(string id)
{
if (!string.IsNullOrEmpty(id))
{
return View(“OneUser”, GetUser(id));
}
return View(“AlUsers”, GetUsers());
}
MSDN: https://blogs.msdn.microsoft.com/webdev/2013/10/17/attribute-routing-in-asp-net-mvc-5/
You can register another route specifying the route path and the controller in RegisterRoutes:
routes.MapRoute(
name: "Admin",
url: "{controller}/Users/{action}/{id}",
defaults: new { id = UrlParameter.Optional },
constraints: new { controller = "Admin" }
);
To handle your directory structure you need to extend the default view engine to add the new view paths:
public class ExtendedRazorViewEngine : RazorViewEngine
{
public ExtendedRazorViewEngine()
{
List<string> existingPaths = new List<string>(ViewLocationFormats);
existingPaths.Add("~/Views/Admin/Users/{0}.cshtml");
ViewLocationFormats = existingPaths.ToArray();
}
}
And register the engine in Application_Start:
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ExtendedRazorViewEngine engine = new ExtendedRazorViewEngine();
ViewEngines.Engines.Add(engine);
...
}
Hi I have problem with routes in plugin, in nopcommerce 3.6
I have in folder Controller TestPohodaController.cs contains method ImportProductInfo()
There is my RegisterRoutes:
namespace Nop.Plugin.Test.Pohoda
{
public partial class RouteProvider : IRouteProvider
{
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("Plugin.Test.Pohoda.ImportProductInfo",
"Plugins/TestPohoda/ImportProductInfo",
new { controller = "TestPohoda", action = "ImportProductInfo" },
new[] { "Nop.Plugin.Test.Pohoda.Controllers" }
);
}
public int Priority
{
get
{
return 0;
}
}
}
}
Installation to nopCommerce is ok, but when I go to mypage/Plugins/TestPohoda/ImportProductInfo page return 404.
I need url of TestPohodaController to call this controller from economic system. Can You help me please? Thanks.
ASP.NET MVC Routing evaluates routes from top to bottom. So if two routes match, the first one it hits (the one closer to the 'top' of the RegisterRoutes method) will take precedence over the subsequent one.
With that in mind, you need to do two things to fix your problem:
Your default route should be at the bottom.
Your routes need to have constraints on them if they contain the same number of segments:
What's the difference between:
example.com/1
and
example.com/index
To the parser, they contain the same number of segments, and there's no differentiator, so it's going to hit the first route in the list that matches.
To fix that, you should make sure the routes that use ProductIds take constraints:
routes.MapRoute(
"TestRoute",
"{id}",
new { controller = "Product", action = "Index3", id = UrlParameter.Optional },
new { id = #"\d+" } //one or more digits only, no alphabetical characters
);
You don't need to start with Plugins for your route url. it is enough
to follow this pattern {controller}/{Action}/{parameter}
Make sure also namespace for the controller is correct as you define
in the routing. Nop.Plugin.Test.Pohoda.Controllers
You can define an optional productId parameter as well. so it will
work for mypage/TestPohoda/ImportProductInfo or
mypage/TestPohoda/ImportProductInfo/123
You can also set the priority higher than 0 which is priority of the
default routeprovider in the nop.web. this way you ensure that your
plugin reads it first. Indeed it is not necessary as you have
specific url. this is only required if you have similar route url
Try using this route
namespace Nop.Plugin.Test.Pohoda
{
public partial class RouteProvider : IRouteProvider
{
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("Plugin.Test.Pohoda.ImportProductInfo",
"TestPohoda/ImportProductInfo/{productId}",
new { controller = "TestPohoda", action = "ImportProductInfo" , productId = = UrlParameter.Optional },
new[] { "Nop.Plugin.Test.Pohoda.Controllers" }
);
}
public int Priority
{
get
{
return 1;
}
}
}
}
We will have a look at how to register plugin routes. ASP.NET routing is responsible for mapping incoming browser requests to particular MVC controller actions. You can find more information about routing here. So follow the next steps:
If you need to add some custom route, then create RouteProvider.cs file. It informs the nopCommerce system about plugin routes. For example, the following RouteProvider class adds a new route which can be accessed by opening your web browser and navigating to http://www.yourStore.com/Plugins/PaymentPayPalStandard/PDTHandler URL (used by PayPal plugin):
public partial class RouteProvider : IRouteProvider
{
public void RegisterRoutes(IRouteBuilder routeBuilder)
{
routeBuilder.MapRoute("Plugin.Payments.PayPalStandard.PDTHandler", "Plugins/PaymentPayPalStandard/PDTHandler",
new { controller = "PaymentPayPalStandard", action = "PDTHandler" });
}
public int Priority
{
get
{
return -1;
}
}
}
It could be cache problem, try to restart IIS
actually you do nota have to register route by default you can call your method
/TestPohoda/ImportProductInfo
I have some problem with redirecting to some action basicly if I have no paramers on route or parameter which calls Id it works fine, yet when i add some different parameters to route RedirectToAction does not work, it throws an error: No route in the route table matches the supplied values.
[Route("First")]
public ActionResult First()
{
//THIS DOESN'T WORK
//return RedirectToAction("Second", new { area = "plans"});
//THIS WORKS
return RedirectToAction("Third", new { id = "12" });
}
[Route("Second/{area}")]
public ActionResult Second(string area)
{
return new ContentResult() { Content = "Second : " + area};
}
[Route("Third/{id}")]
public ActionResult Third(string id)
{
return new ContentResult() { Content = "Third " + id };
}
So when i enter /First it redirect correctly to Third but to Second it throw error.
I don't have anything extra in RouteConfig just:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
}
Probably, you are conflicting the area parameter with area route parameters. Areas is a resource from Asp.Net MVC with the following definition:
Areas are logical grouping of Controller, Models and Views and other
related folders for a module in MVC applications. By convention, a top
Areas folder can contain multiple areas. Using areas, we can write
more maintainable code for an application cleanly separated according
to the modules.
You could try to rename this parameter and use it, for sample, try to rename the area to areaName:
[Route("First")]
public ActionResult First()
{
return RedirectToAction("Second", new { areaName= "plans"});
}
[Route("Second/{areaName}")]
public ActionResult Second(string areaName)
{
return new ContentResult() { Content = "Second : " + areaName};
}