Identity URLs are all of the form : /Identity/Account/Login etc
How can I change them (all) to be of the form /myapp/Identity/Account/Login etc ?
Is there a single "base" property or setter ?
(using latest .NET Core 3 preview 8)
The default UI uses Razor Pages, and by convention, the URLs are based on the filesystem path, similar to how Web Forms used to work back in the day. In other words, that's the URL because the page is literally located at /Areas/Identity/Pages/Account/Login.cshtml (the Areas and Pages portions of the path are logical, and removed from the URL by convention, leaving just /Identity/Account/Login.
If you want to modify this, you'll need to specify custom routes, via something like:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AddPageRoute("/Identity/Account/Login", "Login");
});
You can also change the route on the actual page via the #page directive in the cshtml file:
`#page "Login"`
However, for the Identity UI, that approach would require you to scaffold the page into your project, obviously, in order to be able to change that.
For chaning Razor Page route, you could try Use a parameter transformer to customize page routes
Detail steps below:
IdentityParameterTransformer
public class IdentityParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
if (value == null) { return null; }
// Slugify value
if (value.ToString().StartsWith("Identity"))
{
return $"/MyApp/{ value.ToString() }";
}
return value.ToString();
}
}
Register
services.AddMvc().AddRazorPagesOptions(options =>
{
options.Conventions.Add(
new PageRouteTransformerConvention(
new IdentityParameterTransformer()));
});
Related
My environment: ASP.NET Core 5 with RazorPages, Webpack 5.
In razor pages (.cshtml) that reference svg files, I want to inline them. This is something Webpack can do (via a plugin), but I'm not sure how to integrate these two tech stacks.
I could write templatised cshtml files, and populate them via webpack:
ContactUs.cshtml.cs
ContactUs.cshtml <------ read by webpack
ContactUs.generated.cshtml <------ generated by webpack
But then how do I force msbuild / aspnet to use the generated file (ContactUs.generated.cshtml) instead of the template file (ContactUs.cshtml) when building?
I suspect the answer is to use IPageRouteModelConvention but I'm unsure how.
(A dirty workaround is to instead use the filenames ContactUs.template.cshtml and ContactUs.cshtml but I prefer something like the above, as "generated" is clearer.)
UPDATE
To simplify the problem:
The compiler looks for Foo.cshtml.cs and Foo.cshtml.
How do I tell it to instead look for Foo.cshtml.cs and Foo.generated.cshtml?
When loading the app, the framework loads for you a set of PageRouteModels which is auto-generated from the razor page folders (by convention). Each such model contains a set of SelectorModel each one of which has an AttributeRouteModel. What you need to do is just modify that AttributeRouteModel.Template by removing the suffixed part from the auto-generated value.
You can create a custom IPageRouteModelConvention to target each PageRouteModel. However that way you cannot ensure the routes from being duplicated (because after modifying the AttributeRouteModel.Template, it may become duplicate with some other existing route). Unless you have to manage a shared set of route templates. Instead you can create a custom IPageRouteModelProvider. It provides all the PageRouteModels in one place so that you can modify & add or remove any. This way it's so convenient that you can support 2 razor pages in which one page is more prioritized over the other (e.g: you have Index.cshtml and Index.generated.cshtml and you want it to pick Index.generated.cshtml. If that generated view is not existed, the default Index.cshtml will be used).
So here is the detailed code:
public class SuffixedNamePageRouteModelProvider : IPageRouteModelProvider
{
public SuffixedNamePageRouteModelProvider(string pageNameSuffix, int order = 0)
{
_pageNameSuffixPattern = string.IsNullOrEmpty(pageNameSuffix) ? "" : $"\\.{Regex.Escape(pageNameSuffix)}$";
Order = order;
}
readonly string _pageNameSuffixPattern;
public int Order { get; }
public void OnProvidersExecuted(PageRouteModelProviderContext context)
{
}
public void OnProvidersExecuting(PageRouteModelProviderContext context)
{
if(_pageNameSuffixPattern == "") return;
var suffixedRoutes = context.RouteModels.Where(e => Regex.IsMatch(e.ViewEnginePath, _pageNameSuffixPattern)).ToList();
var overriddenRoutes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var route in suffixedRoutes)
{
//NOTE: this is not required to help it pick the right page we want.
//But it's necessary for other related code to work properly (e.g: link generation, ...)
//we need to update the "page" route data as well
route.RouteValues["page"] = Regex.Replace(route.RouteValues["page"], _pageNameSuffixPattern, "");
var overriddenRoute = Regex.Replace(route.ViewEnginePath, _pageNameSuffixPattern, "");
var isIndexRoute = overriddenRoute.EndsWith("/index", StringComparison.OrdinalIgnoreCase);
foreach (var selector in route.Selectors.Where(e => e.AttributeRouteModel?.Template != null))
{
var template = Regex.Replace(selector.AttributeRouteModel.Template, _pageNameSuffixPattern, "");
if (template != selector.AttributeRouteModel.Template)
{
selector.AttributeRouteModel.Template = template;
overriddenRoutes.Add($"/{template.TrimStart('/')}");
selector.AttributeRouteModel.SuppressLinkGeneration = isIndexRoute;
}
}
//Add another selector for routing to the same page from another path.
//Here we add the root path to select the index page
if (isIndexRoute)
{
var defaultTemplate = Regex.Replace(overriddenRoute, "/index$", "", RegexOptions.IgnoreCase);
route.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel() { Template = defaultTemplate }
});
}
}
//remove the overridden routes to avoid exception of duplicate routes
foreach (var route in context.RouteModels.Where(e => overriddenRoutes.Contains(e.ViewEnginePath)).ToList())
{
context.RouteModels.Remove(route);
}
}
}
Register the IPageRouteModelProvider in Startup.ConfigureServices:
services.AddSingleton<IPageRouteModelProvider>(new SuffixedNamePageRouteModelProvider("generated"));
Successfully using the project laid out at Localized routing using ASP.NET Core MVC 2. The project is developed to display the Default language using just the controller/Action and Localized Routes for the alternate languages.
My Controller
[LocalizationRoute("en", "Portal/Dashboard")]
[LocalizationRoute("fr", "Portail/tableau-de-bord")]
[LocalizationRoute("es", "Portal/Tablero")]
public class DashboardController : PortalBaseController
{
private const string Title = "Dashboard";
[LocalizationRoute("en", "Dashboardv1")]
[LocalizationRoute("fr", "tableau-de-bordv1")]
[LocalizationRoute("es", "Tablerov1")]
public IActionResult Dashboardver1()
In my SignIn controller I wish to create a localised RedirectUrl...the below code provides a fully localized route for the the french and spanish but the default route is always just controller/action. If I manually type in the english, fully localized route, the page loads but the code below always returns just a regular path.
var culture = CultureInfo.CurrentCulture.Name;
var redirectUrl = LocalizationRouteDataHandler.GetUrl("Dashboard", "Dashboardver1", culture).Url;
//should be "en/portal/Dashboard/Dashboardv1"
//currently resolves to "Dashboard/Dashboardver1"
Suggestions as ?
I find that In LocalizationRouteDataHandler.AddControllerRouteData ,code ControllerRoutes[controllerKey].Names.TryAdd(culture, route); adds culture="en" and route="Dashboard" first to the Dictionary of Controller Name and then when it tries to add the condition culture="en" and route="en/Portal/Dashboard",it fails since the "en" key has added.
A workaround is that you could remove the unnecessary key and add the new key.
LocalizationRouteDataHandler.cs
public static void AddControllerRouteData(string controller, string culture, string route)
{
if (controller == "Dashboard")
{
Console.WriteLine("test");
}
string controllerKey = controller.ToLower();
// If the controller doesn't exist, create it!
if (!ControllerRoutes.ContainsKey(controllerKey))
{
ControllerRoutes.TryAdd(controllerKey, new CultureControllerRouteData());
}
// key removed
if (culture=="en" && ControllerRoutes[controllerKey].Names.Remove("en"))
{
ControllerRoutes[controllerKey].Names.TryAdd(culture, route);
}
// dictionary doesn't contain the key
else
{
ControllerRoutes[controllerKey].Names.TryAdd(culture, route);
}
}
When I test with
var redirectUrl = LocalizationRouteDataHandler.GetUrl("Dashboard", "Dashboardver1", "en").Url;
It returns /en/Portal/Dashboard/Dashboardver1
Can we use dashes (-) in the Route template in ASP.NET Core?
// GET: api/customers/5/orders?active-orders=true
[Route("customers/{customer-id}/orders")]
public IActionResult GetCustomerOrders(int customerId, bool activeOrders)
{
.
.
.
}
(The above code doesn't work)
The route parameters usually directly map to the action's variable name, so [Route("customers/{customerId}/orders")] should work since that's the name of your variable (int customerId).
You don't need dashes there, the part within the curly braces {} will never appear as a part of the generated url, it will always be replaced by the content you pass from browser or the variables you pass to the url generator.
customers/{customerId}/orders will always be customers/1/orders when customerId is set to 1, so there's no point trying to force it to {customer-id}.
However, you can try public
[Route("customers/{customer-id}/orders")]
IActionResult GetCustomerOrders([FromRoute(Name = "customer-id")]int customerId, bool activeOrders)
to bind the customerId from a unconventional route name, if you wish. But I'd strongly advise against it, as it just adds unnecessary code which has absolutely zero-effect on your generated urls.
The above generates (and parses) the exactly same url as
[Route("customers/{customerId}/orders")]
IActionResult GetCustomerOrders(int customerId, bool activeOrders)
and is much more readable code.
For the query part, as you figured it out in the comments, it makes sense to add the dashes via [FromQuery(Name = "active-orders")] bool activeOrders, since that really affects the generated url.
New in ASP.NET Core 2.2
In ASP.NET Core 2.2 you'll get a new option to 'slugify' your routes (only supported when using the new Route Dispatcher instead of the default Mvc Router).
A route of blog\{article:slugify} will (when used with Url.Action(new { article = "MyTestArticle" })) generate blog\my-test-article as url.
Can also be used in default routes:
routes.MapRoute(
name: "default",
template: "{controller=Home:slugify}/{action=Index:slugify}/{id?}");
For further details see the ASP.NET Core 2.2-preview 3 annoucement.
Just expanding on Tseng answer to the question. for ASP NET CORE to use "slugify" transformer you need to register it first like so:
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
if (value == null) { return null; }
return Regex.Replace(value.ToString(),
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
}
}
and then in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddRouting(options =>
{
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
}
Code from Microsoft
I have an action in my ASP.Net Core WebAPI Controller which takes one parameter. I'm trying to configure it to be able to call it in following forms:
api/{controller}/{action}/{id}
api/{controller}/{action}?id={id}
I can't seem to get the routing right, as I can only make one form to be recognized. The (simplified) action signature looks like this: public ActionResult<string> Get(Guid id). These are the routes I've tried:
[HttpGet("Get")] -- mapped to api/MyController/Get?id=...
[HttpGet("Get/{id}")] -- mapped to api/MyController/Get/...
both of them -- mapped to api/MyController/Get/...
How can I configure my action to be called using both URL forms?
if you want to use route templates
you can provide one in Startup.cs Configure Method Like This:
app.UseMvc(o =>
{
o.MapRoute("main", "{controller}/{action}/{id?}");
});
now you can use both of request addresses.
If you want to use the attribute routing you can use the same way:
[HttpGet("Get/{id?}")]
public async ValueTask<IActionResult> Get(
Guid id)
{
return Ok(id);
}
Make the parameter optional
[Route("api/MyController")]
public class MyController: Controller {
//GET api/MyController/Get
//GET api/MyController/Get/{285A477F-22A7-4691-AA51-08247FB93F7E}
//GET api/MyController/Get?id={285A477F-22A7-4691-AA51-08247FB93F7E}
[HttpGet("Get/{id:guid?}"
public ActionResult<string> Get(Guid? id) {
if(id == null)
return BadRequest();
//...
}
}
This however means that you would need to do some validation of the parameter in the action to account for the fact that it can be passed in as null because of the action being able to accept api/MyController/Get on its own.
Reference Routing to controller actions in ASP.NET Core
I am currently working on an Umbraco MVC 4 project version 6.0.5. The project currently uses Vega.USiteBuilder to build the appropriate document types in the backoffice based on strongly typed classes with mapping attributes. Consequently, all my razor files inherit from UmbracoTemplatePageBase
I am coming across a road block trying to invoke a HTTP GET from a razor file. For example a search form with multiple fields to submit to a controller action method, using a SurfaceController using Html.BeginUmbracoForm.
My Html.BeginUmbracoForm looks like this
#using (Html.BeginUmbracoForm("FindTyres", "TyreSearch"))
{
// Couple of filter fields
}
I basically have a scenario where I will like to retrieve some records from an external database outside of Umbraco (external to Umbraco Database) and return the results in a custom view model back to my Umbraco front end view. Once my controller and action method is setup to inherit from SurfaceController and thereafter compiling it and submitting the search, I get a 404 resource cannot be found where the requested url specified: /umbraco.RenderMVC.
Here is my code snippet:
public ActionResult FindTyres(string maker, string years, string models, string vehicles)
{
var tyreBdl = new Wheels.BDL.TyreBDL();
List<Tyre> tyres = tyreBdl.GetAllTyres();
tyres = tyres.Where(t => string.Equals(t.Maker, maker, StringComparison.OrdinalIgnoreCase)
&& string.Equals(t.Year, years, StringComparison.OrdinalIgnoreCase)
&& string.Equals(t.Model, models, StringComparison.OrdinalIgnoreCase)
&& string.Equals(t.Version, vehicles, StringComparison.OrdinalIgnoreCase)).ToList();
var tyreSearchViewModel = new TyreSearchViewModel
{
Tyres = tyres
};
ViewBag.TyreSearchViewModel = tyreSearchViewModel;
return CurrentUmbracoPage();
}
I then resort to using standard MVC, Html.BeginForm (the only difference). Repeating the steps above and submitting the search, I get the following YSOD error.
Can only use UmbracoPageResult in the context of an Http POST when
using a SurfaceController form
Below is a snippet of the HTML BeginForm
#using (Html.BeginForm("FindTyres", "TyreSearch"))
{
// Couple of filter fields
}
I feel like I am fighting the Umbraco routes to get my controller to return a custom model back to the razor file. I have googled alot trying to figure out how to do a basic search to return a custom model back to my Umbraco front end view till the extent that I tried to create a custom route but that too did not work for me.
Does my controller need to inherit from a special umbraco controller class to return the custom model back? I will basically like to invoke a HTTP GET request (which is a must) so that my criteria search fields are reflected properly in the query strings of the url. For example upon hitting the search button, I must see the example url in my address browser bar
http://[domainname]/selecttyres.aspx/TyresSearch/FindTyresMake=ASIA&Years=1994&Models=ROCSTA&Vehicles=261
Therefore, I cannot use Surface Controller as that will operate in the context of a HTTP Post.
Are there good resource materials that I can read up more on umbraco controllers, routes and pipeline.
I hope this scenario makes sense to you. If you have any questions, please let me know. I will need to understand this concept to continue on from here with my project and I do have a deadline.
There are a lot of questions about this and the best place to look for an authoritative approach is the Umbraco MVC documentation.
However, yes you will find, if you use Html.BeginUmbracoForm(...) you will be forced into a HttpPost action. With this kind of functionality (a search form), I usually build the form manually with a GET method and have it submit a querystring to a specific node URL.
<form action="#Model.Content.Url"> ... </form>
On that page I include an #Html.Action("SearchResults", "TyresSearch") which itself has a model that maps to the keys in the querystring:
[ChildAction]
public ActionResult(TyreSearchModel model){
// Find results
TyreSearchResultModel results = new Wheels.BDL.TyreBDL().GetAllTyres();
// Filter results based on submitted model
...
// Return results
return results;
}
The results view just need to have a model of TyreSearchResultModel (or whatever you choose).
This approach bypasses the need for Umbraco's Controller implementation and very straightforward.
I have managed to find my solution through route hijacking which enabled me to return a custom view model back to my view and work with HTTP GET. It worked well for me.
Digby, your solution looks plausible but I have not attempted at it. If I do have a widget sitting on my page, I will definitely attempt to use your approach.
Here are the details. I basically override the Umbraco default MVC routing by creating a controller that derived from RenderMvcController. In a nutshell, you implement route hijacking by implementing a controller that derives from RenderMvcController and renaming your controllername after your given documenttype name. Recommend the read right out of the Umbraco reference (http://our.umbraco.org/documentation/Reference/Mvc/custom-controllers) This is also a great article (http://www.ben-morris.com/using-umbraco-6-to-create-an-asp-net-mvc-4-web-applicatio)
Here is my snippet of my code:
public class ProductTyreSelectorController : Umbraco.Web.Mvc.RenderMvcController
{
public override ActionResult Index(RenderModel model)
{
var productTyreSelectorViewModel = new ProductTyreSelectorViewModel(model);
var maker = Request.QueryString["Make"];
var years = Request.QueryString["Years"];
var models = Request.QueryString["Models"];
var autoIdStr = Request.QueryString["Vehicles"];
var width = Request.QueryString["Widths"];
var aspectRatio = Request.QueryString["AspectRatio"];
var rims = Request.QueryString["Rims"];
var tyrePlusBdl = new TPWheelBDL.TyrePlusBDL();
List<Tyre> tyres = tyrePlusBdl.GetAllTyres();
if (Request.QueryString.Count == 0)
{
return CurrentTemplate(productTyreSelectorViewModel);
}
if (!string.IsNullOrEmpty(maker) && !string.IsNullOrEmpty(years) && !string.IsNullOrEmpty(models) &&
!string.IsNullOrEmpty(autoIdStr))
{
int autoId;
int.TryParse(autoIdStr, out autoId);
tyres = tyres.Where(t => string.Equals(t.Maker, maker, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Year, years, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Model, models, StringComparison.OrdinalIgnoreCase) &&
t.AutoID == autoId)
.ToList();
productTyreSelectorViewModel.Tyres = tyres;
}
else if (!string.IsNullOrEmpty(width) && !string.IsNullOrEmpty(aspectRatio) && !string.IsNullOrEmpty(rims))
{
tyres = tyres.Where(t => string.Equals(t.Aspect, aspectRatio, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Rim, rims, StringComparison.OrdinalIgnoreCase)).ToList();
productTyreSelectorViewModel.Tyres = tyres;
}
var template = ControllerContext.RouteData.Values["action"].ToString();
//return an empty content result if the template doesn't physically
//exist on the file system
if (!EnsurePhsyicalViewExists(template))
{
return Content("Could not find physical view template.");
}
return CurrentTemplate(productTyreSelectorViewModel);
}
}
Note my ProductTyreSelectorViewModel must inherit from RenderModel for this to work and my document type is called ProductTyreSelector. This way when my model is returned with the action result CurrentTemplate, the Umbraco context of the page is retained and my page is rendered appropriately again. This way, all my query strings will show all my search/filter fields which is what I want.
Here is my snippet of the ProductTyreSelectorViewModel class:
public class ProductTyreSelectorViewModel : RenderModel
{
public ProductTyreSelectorViewModel(RenderModel model)
: base(model.Content, model.CurrentCulture)
{
Tyres = new List<Tyre>();
}
public ProductTyreSelectorViewModel(IPublishedContent content, CultureInfo culture)
: base(content, culture)
{
}
public ProductTyreSelectorViewModel(IPublishedContent content)
: base(content)
{
}
public IList<Tyre> Tyres { get; set; }
}
This approach will work well perhaps with one to two HTTP GET forms on a given page. If there are multiple forms within in a page, then a good solution will may be to use ChildAction approach. Something I will experiment with further.
Hope this helps!