Asp.Net Core Routing using a parameter transformer and depending on the culture - url

I want to customize routes depending on the culture of web application to have a 'friendly' url (to optimize SEO).
For example, for the controller called "FirstItemController", and view called "TheIndexView":
www.myapp.com/fr/mon-premier-element/la-vue-index
www.myapp.com/en/my-first-item/the-index-view
www.myapp.com/es/mi-primer-elemento/la-vista-index
Asp.Net Core 2.2 introduces the concept of Parameter Transformers to routing (https://blogs.msdn.microsoft.com/webdev/2018/10/17/asp-net-core-2-2-0-preview3-now-available/ + https://www.hanselman.com/blog/ASPNETCore22ParameterTransformersForCleanURLGenerationAndSlugsInRazorPagesOrMVC.aspx)
I know, theses articles was wrote for 2.2.0 preview 3.
For the moment, I've create the SlugifyParameterTransformer class and register it in AddMvc options :
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
if (value == null) { return null; }
// Slugify value
return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
}
}
I've tried two approaches in routes options:
app.UseMvc(routes => routes.MapRoute("default", "{culture=fr}/{controller=Home}/{action=Index}/{id?}"));
// works
// AND
app.UseMvc(routes => routes.MapRoute("default", "{culture=fr}/{controller=Home:slugify}/{action=Index:slugify}/{id?}"));
// dosn't works, I don't know where I declare "slugify" for the url template
It work's: MyFirstItemController.TheIndexView url is "my-first-item/the-index-view". Now, how can I localize my urls?
I've tried to add the RouteAttribute and HttpGetAttribute:
[Route("{culture=fr}/mon-premier-element")]
[Route("{culture=en}/my-first-item")]
public class MyFirstItemController : Controller
{
[HttpGet("???????")] // for moment: [controller]/[action]
public IActionResult TheIndexView()
{ /*...*/ }
}
I don't know what can I use in Name value of HttpGetAttribute.
These urls works:
fr/mon-premier-element/ (good)
en/mon-premier-element/ (NOT good)
fr/my-first-item (NOT good)
en/my-first-item (good)

Related

ASP.NET Core Middleware and URL Parsing

Trying to add special endpoints to an ASP.NET Core MVC through a middleware.
In a app.UseWhen, I need to parse the request URL. In a Controller context, MVC does a great job extracting userId using the following template:
GET http://contoso.com/users/{userId}/addresses
How could this be cleanly done in a middleware where MVC Controller constructs aren't setup?
Bonus points if the answer helps figuring out if the address conforms to this pattern in the first place.
All I have on hand is a DefaultHttpContext.
Solution based on Mark Vincze's blog
This method used to extract the the user id and work with it...
private static void AddAddressesRoute(IApplicationBuilder app, RouteBuilder builder)
{
builder.MapVerb(
HttpMethod.Get.Method,
"users/{userId}/addresses",
async context =>
{
var routeData = context.GetRouteData();
var userId = routeData.Values["userId"];
// userId available from here
}
);
}
Should be initiated from an application builder extension method.
public static IApplicationBuilder UseAddresses(
this IApplicationBuilder app
)
{
RouteBuilder builder = new RouteBuilder(app);
AddAddressesRoute(app, builder);
app.UseRouter(builder.Build());
return app;
}
Becomes a middleware that can be added to the Startup.Configure method just like this:
app.UseAddresses()
It doesn't even interfere with MVC that still gets triggered if the route doesn't match.
URL parsing comes to play in MVC pipeline, not in ASP.NET Core one.
You might want to consider MVC filters instead, which have access to routing context.
You can access the HttpContext from middleware and parse out key-value pairs from the query string but you can not access the path parameters via key-value.
For example:
You make a GET to the following controller via http://contoso.com/api/users/5?zip=90210:
// GET api/users/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
Custom Middleware:
public class MyCustomMiddleware
{
public Task InvokeAsync(HttpContext context)
{
// get full path from context request path
var queryPath = context.Request.Path().ToString();
// will return /api/users/5
// get id from query string
var queryStringId = context.Request.Query["zip"].ToString();
// will return 90210
}
}
There isn't any mapping from your Controller parameters to the HttpContext.

MVC routing a repeatable pattern?

A design goal for a website I'm working on is to keep the URL in the browser in a state where the user can copy it, and the link can be used from another browser/user/machine to return to the spot that the url was copied. (The actual changes will happen via AJAX, but the URL will change to reflect where they are.)
Example: If you were on the customer page looking at customer 123, and had details pulled up on their order #456, and full details on line 6 of this order, your url could simply be /customer/123/456/6
The challenge comes with a second feature: Users can add UI columns (analogous to adding a new tab in a tab view, or a new document in an MDI app) Each column can easily generate a routable url, but I need the url to reflect one or more columns. (E.G. User has both /customer/123/456/6 and /customer/333/55/2 in two side by side columns)
In a perfect world, I'd like the url to be /customer/123/456/6/customer/333/55/2 for the above scenario, but I don't know if MVC routing can handle repetitive patterns, or, if so, how it is done.
Can this be done via routing? If not is there a way to get this type of one-or-more functionality from Url?
You could create a custom route handler (see my previous answer) or derive from a RouteBase like NightOwl888 suggested. Another approach would be to simply use a model binder and a model binder attribute.
public class CustomerInvoiceLineAttribute : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new CustomerInvoiceLineModelBinder();
}
}
public class CustomerInvoiceLineModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var path = (string)bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue;
var data = path.Split(new[] { "/customer/" }, StringSplitOptions.RemoveEmptyEntries);
return data.Select(d =>
{
var rawInfo = d.Split('/');
return new CustomerInvoiceLine
{
CustomerId = int.Parse(rawInfo[0]),
InvoiceId = int.Parse(rawInfo[1]),
Line = int.Parse(rawInfo[2])
};
});
}
}
You define your route by specifying a star route data. This mean that the route parameter will contains everything following the action
routes.MapRoute(
name: "CustomerViewer",
url: "customer/{*customerInfo}",
defaults: new { controller = "Customer", action = "Index" });
Then in your controller, you bind your parameter with the same name as the star route parameter using the custom model binder defined above:
public ActionResult Index([CustomerInvoiceLine] IEnumerable<CustomerInvoiceLine> customerInfo)
{
return View();
}
You will need to add validation during the parsing and probably security too, so that a customer cannot read the invoice of other customers.
Also know that URL have a maximum length of 2000 characters.
You can do this with the built-in routing as long as you don't anticipate that any of your patterns will repeat or have optional parameters that don't appear in the same segment of the URL as other optional parameters.
It is possible to use routing with optional parameters by factoring out all of the permutations, but if you ask me it is much simpler to use the query string for this purpose.
NOTE: By definition, a URL must be unique. So you must manually ensure your URLs don't have any collisions. The simplest way to do this is by matching the page with the path (route) and adding this extra information as query string values. That way you don't have to concern yourself with accidentally making routes that are exactly the same.
However, if you insist on using a route for this purpose, you should probably put your URLs in a database in a field with a unique constraint to ensure they are unique.
For the most advanced customization of routing, subclass RouteBase or Route. This allows you to map any URL to a set of route values and map the route values back to the same URL, which lets you use it in an ActionLink or RouteLink to build the URLs for your views and controllers.
public class CustomPageRoute : RouteBase
{
// This matches the incoming URL and translates it into RouteData
// (typically a set of key value pairs in the RouteData.Values dictionary)
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Trim the leading slash
var path = httpContext.Request.Path.Substring(1);
if (/* the path matches your route logic */)
{
result = new RouteData(this, new MvcRouteHandler());
result.Values["controller"] = "MyController";
result.Values["action"] = "MyAction";
// Any other route values to match your action...
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
// This builds the URL for ActionLink and RouteLink
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData result = null;
if (/* all of the expected route values match the request (the values parameter) */)
{
result = new VirtualPathData(this, page.VirtualPath);
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
}
Usage
routes.Add(
name: "CustomPage",
item: new CustomPageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Multi-Lingual text in Razor pages

I have an ASP.NET MVC Razor website which is supposed to be live in multiple countries having different cultures and hence the languages. My Development team is concerned about english only and whole text on UI pages is also written in plain english. I want this english text to be converted into culture specific language. I'm using resource file to manage the strings on my website.
One way is to create multiple resource files based on each language and then using each file based on specific culture. This thing needs to be managed manually. If someone has done this thing please come up with any reference or any sample code for this implementation.
If there is any way where I can automate this thing then it will be best way to go for a multi lingual website. Like as culture can be easily detected by user IP address and based on the culture I should be able to convert all english based text into current culture specific language.
1. Automatically Use User-Culture
Have the user culture be set automatically on the current Thread/HttpContext. In your Web.Config:
<system.web>
...
<globalization enableClientBasedCulture="true" culture="auto" uiCulture="auto" />
...
</system.web>
2. Helper Function
Introduce a global method that will translate input text using the appropriate resource:
public static class Resources
{
public static string GetResource(string key, params object[] data)
{
if (String.IsNullOrEmpty(key))
return key;
// the actual call to your Resources
var res = Texts.ResourceManager.GetString(key.ToUpper(), Thread.CurrentThread.CurrentUICulture);
if (String.IsNullOrEmpty(res))
return data != null && data.Length > 0 ? String.Format(key.ToUpper(), data) : key;
if (data != null && data.Length > 0)
return String.Format(res, data);
return res;
}
}
The method also lets you pass an additional (optional) parameters to be used in a String.Format way. For example:
// in your Resource file (Texts.es.resx)
GREETING_TEXT: "Hola amigo {0}, el tiempo es {1}"
// invocation
Resources.GetResource("GREETING_TEXT", "Chaim", DateTime.Now);
3. Controller Helper:
Introducing a _ methods that will let you translate texts quickly in the Controller:
public class BaseController : Controller
{
public string _(string key, params object[] data)
{
return Resources.GetResource(key, null, data);
}
}
In your controller you have to make sure you're inheriting your BaseController and use it as follows:
public HomeController : BaseController:
{
public ActionResult GreetMe()
{
var msg = _("GREETING_TEXT", this.User, DateTime.Now);
return Content(msg);
}
}
4. Razor Helper
And for your Razor pages:
// non-generic version (for model-less pages)
public abstract class BaseWebPage : WebViewPage
{
public string _(string key, params object[] data)
{
return Resources.GetResource(key, null, data);
}
}
// generic version (for model defined pages)
public abstract class BaseWebPage<T> : WebViewPage<T>
{
public string _(string key, params object[] data)
{
return Resources.GetResource(key, null, data);
}
}
Now we have to set this new base WebPage as the base type for our pages in ~/Views/Web.Config:
<system.web.webPages.razor>
...
<pages pageBaseType="Full.NameSpace.ToYour.BaseWebPage">
...
</system.web.webPages.razor>
(If you're using Areas, you'll have to modify each ~/Areas/AREA_NAME/Views/Web.Config as well)
You can now use it in your Razor pages:
<h1>#_("HELLO")</h1>

Difference between RouteCollection.Ignore and RouteCollection.IgnoreRoute?

What's the difference between RouteCollection.Ignore(url, constraints) and RouteCollection.IgnoreRoute(url, constraints)?
Background
New MVC projects include this IgnoreRoute call in Global.asax RegisterRoutes method to skip routing for requests to .axd locations that are handled elsewhere in the ASP.NET system.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
I wanted to add an additional ignored route to a project and I started to type out the new line. After routes.I, Intellisense pops up with .Ignore and .IgnoreRoute, both sounding about the same.
According to the MSDN docs, you can see that one is an instance method of the System.Web.Routing.RouteCollection class and the other is an extension method on that class from System.Web.Mvc.RouteCollectionExtensions.
RouteCollection.Ignore: "Defines a URL pattern that should not be checked for matches against routes if a request URL meets the specified constraints" (MSDN docs).
RouteCollection.IgnoreRoute: "Ignores the specified URL route for the given list of the available routes and a list of constraints" (MSDN docs).
Both take a route URL pattern and a set of constraints restricting the application of the route on that URL pattern.
Between the source for System.Web.Mvc.RouteCollectionExtensions on CodePlex and running a little ILSpy on my local GAC for System.Web.Routing.RouteCollection, it doesn't appear there is a difference, though they seem to have completely independent code to do the same thing.
RouteCollection.IgnoreRoute (via CodePlex source)
public static void IgnoreRoute(this RouteCollection routes, string url, object constraints) {
if (routes == null) {
throw new ArgumentNullException("routes");
}
if (url == null) {
throw new ArgumentNullException("url");
}
IgnoreRouteInternal route = new IgnoreRouteInternal(url) {
Constraints = new RouteValueDictionary(constraints)
};
routes.Add(route);
}
RouteCollection.Ignore (via ILSpy decompile)
public void Ignore(string url, object constraints) {
if (url == null) {
throw new ArgumentNullException("url");
}
RouteCollection.IgnoreRouteInternal item = new RouteCollection.IgnoreRouteInternal(url) {
Constraints = new RouteValueDictionary(constraints)
};
base.Add(item);
}
Differences
The only real difference is the obvious difference in location, one being an instance method in the RouteCollection class itself and one being an extensions method on that class. After you factor in the code differences that come from instance vs. extension execution (like the vital null check on the extended instance), they appear identical.
At their core, they both use the exact same StopRoutingHandler class. Both have their own versions of a sealed IgnoreRouteInternal class, but those versions are identical in code.
private sealed class IgnoreRouteInternal : Route {
public IgnoreRouteInternal(string url)
: base(url, new StopRoutingHandler()) {
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues) {
return null;
}
}

Asp.Net MVC - Best approach for "dynamic" routing

I am trying to come up with an approach to create "dynamic" routing. What I mean, exactly, is that I want to be able to assign the controller and action of a route for each hit rather than having it mapped directly.
For example, a route may look like this "path/{object}" and when that path is hit, a lookup is performed providing the appropriate controller / action to call.
I've tried discovering the mechanisms for creating a custom route handler, but the documentation / discoverability is a bit shady at the moment (I know, its beta - I wouldn't expect any more). Although, I'm not sure if thats even the best approach and perhaps a controller factory or even a default controller/action that performs all of the mappings may be the best route (no pun intended) to go.
Any advice would be appreciated.
You can always use a catch all syntax ( I have no idea if the name is proper).
Route:
routeTable.MapRoute(
"Path",
"{*path}",
new { controller = "Pages", action = "Path" });
Controller action is defined as:
public ActionResult Path(string path)
In the action for controller you will have a path, so just have to spilt it and analyse.
To call another controller you can use a RedirectToAction ( I think this is more proper way). With redirection you can set up a permanent redirectionfor it.
Or use a something like that:
internal class MVCTransferResult : RedirectResult
{
public MVCTransferResult(string url) : base(url)
{
}
public MVCTransferResult(object routeValues)
: base(GetRouteURL(routeValues))
{
}
private static string GetRouteURL(object routeValues)
{
UrlHelper url = new UrlHelper(
new RequestContext(
new HttpContextWrapper(HttpContext.Current),
new RouteData()),
RouteTable.Routes);
return url.RouteUrl(routeValues);
}
public override void ExecuteResult(ControllerContext context)
{
var httpContext = HttpContext.Current;
// ASP.NET MVC 3.0
if (context.Controller.TempData != null &&
context.Controller.TempData.Count() > 0)
{
throw new ApplicationException(
"TempData won't work with Server.TransferRequest!");
}
// change to false to pass query string parameters
// if you have already processed them
httpContext.Server.TransferRequest(Url, true);
// ASP.NET MVC 2.0
//httpContext.RewritePath(Url, false);
//IHttpHandler httpHandler = new MvcHttpHandler();
//httpHandler.ProcessRequest(HttpContext.Current);
}
}
However this method require to run on IIS or a IIS Expres Casinni is not supporting a Server.Transfer method

Resources