asp.net mvc, generate url by custom route - asp.net-mvc-4

i have created application, where url generates depends on database values.
i parse these urls without any problem and get controller and action from database in my route handler.
but when i try to generate url, i get troubles.
in my case, it seems like:
view
#Html.ActionLink("more", MVC.Blog.Post(item.Alias)) // i use T4MVC
MyRouteConstraint
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration)
{
var data = GetDataFromDbByControllerActionAndParameters(values);
if (data == null)
return false;
var valuesToRemove = new List<string>();
var path = GenerateUrlByData(data, valuesToRemove);
values.Remove("controller");
values.Remove("action");
valuesToRemove.ForEach(v => values.Remove(v)); // remove values that is already used in path
values.Add("all", path) // path = "blog/post/postalias"
return true;
}
// skipped code
}
route rule
routes.MapRoute("Locations", "{*all}",
constraints: new { all = new LocationConstraints() },
defaults: new { },
namespaces: new []{typeof(BaseController).Namespace}).RouteHandler = new LocationRouteHandler();
and as result i got url like this
localhost:8553/?Controller=Blog&Action=Post&alias=postalias
but expect like this
localhost:8553/blog/post/postalias
how can I generate url? where it should be? i think not in the constrant, but why it is invoked in this case?

In my MVC application the closest route that matches the one you have is the one below:
routes.MapRoute(
name: "MyRouteName",
url: "{SomeFolder}/{SomePageName}",
defaults: new { controller = "MyController", action = "Index" },
constraints: new { routeConstraint = new MyRouteConstraint() }
);
SomeFolder can be fixed or changed from the database while SomePageName will be changed to a different value from database. The url should be whatever URL you want to match with this route and the one that will be replaced by the value from the database. The defaults address the Controller and Action that will render the page in the end of the MVC cycle. The constraints will lead to your Match method described.
With this configuration I have URLs like www.project.com/SomeFolder/SomePageNameFromDatabase.

Related

Change name of cshtml file in ASP.NET Core RazorPages

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"));

Netcore 2.2 Localized Routing - Route values for default culture always ignored

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

How to get #Url.Action value inside a controller

I am using ASP.net core
I can use an Html action inside a view
#Url.Action("GetOptions", "ControllerName", new { id="1"});
However I want to get a string value of it in the Controller.
e.g. something like
string Url= Url.Action("GetOptions", "ControllerName", new { id="1"}).ToString();
In previous versions of MVC you can reference the helper in the controller by
UrlHelper urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext);
Basically what I want to do is generate a URL string representation in my controller
In order for the route values to work correctly for me I had to use the static invocation of the url helpers
UrlHelperExtensions.Action(Url, "Details", "Resellers", new { id = 1 })
Edit: The shorthand way of writing this is:
this.Url.Action("Details", "Resellers", new { id = 1 })
Thanks #Learner.

OData Routing with Optional Parameter

I have an OData (v3) Web API 2 project that is a wrapper to another wcf web service. The intended client for this odata connection is SharePoint 2013. I am creating CRUD operations within this wrapper and noticed that when sharepoint is asked to delete something it send a request in this format: /Entity(Identity=XX) instead of it's normal /Entity(XX) that i have working normally. I need to be able to handle that request without breaking the other one. Here is my code:
public IHttpActionResult GetSchool([FromODataUri] int key, ODataQueryOptions<School> queryOptions)
{
// validate the query.
try
{
queryOptions.Validate(_validationSettings);
}
catch (ODataException ex)
{
return BadRequest(ex.Message);
}
SchoolDataSource data = new SchoolDataSource();
var result = data.GetByID(key);
return Ok<School>(result);
//return StatusCode(HttpStatusCode.NotImplemented);
}
This works fine for a request for /Schools(1), but not for /Schools(ID=1). i have tried adding:
[Route("Schools(ID={key}")]
And this makes the /Schools(ID=1) route work, but breaks pretty much everything else (406 Errors). i tried adding the above attribute and
[Route("Schools({key})")]to see if i can get them both working, but it doesn't function correctly either. I am very new to this, and was hoping to at least get some direction. Here is my WebApiConfig:
config.MapHttpAttributeRoutes();
config.EnableQuerySupport();
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
// Web API configuration and services
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<School>("Schools");
builder.DataServiceVersion = new Version("2.0");
config.Routes.MapODataRoute("odata", null, builder.GetEdmModel());
// Web API routes
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Errors i get:
406 if i have the route attribute set. 500 if i dont have the route attribute set. it seems as though my service has no idea how to handle the parameter unless i specify it, but if i do, all calls get 406 errors.
may not be the best approach, but made it work with this class:
public class SharePointRoutingConvention : EntitySetRoutingConvention
{
public override string SelectAction(ODataPath odataPath, HttpControllerContext context,
ILookup<string, HttpActionDescriptor> actionMap)
{
//Gets the entity type
IEdmEntityType entityType = odataPath.EdmType as IEdmEntityType;
//makes sure the format is correct
if (odataPath.PathTemplate == "~/entityset/key")
{
//parses out the path segment (Identity=X)
KeyValuePathSegment segment = odataPath.Segments[1] as KeyValuePathSegment;
//Gets the verb from the request header
string actionName = context.Request.Method.ToString();
// Add keys to route data, so they will bind to action parameters.
KeyValuePathSegment keyValueSegment = odataPath.Segments[1] as KeyValuePathSegment;
//Checks to see if the "Identity=" part is in the url
if (keyValueSegment.Value.Contains("Identity="))
{
//removes the extra text
context.RouteData.Values[ODataRouteConstants.Key] = keyValueSegment.Value.Replace("Identity=", "");
}
else
{
//parses it normally
context.RouteData.Values[ODataRouteConstants.Key] = keyValueSegment.Value;
}
//returns the verb
return actionName;
}
// Not a match.
return null;
}
}
and make the change to the webapiconfig:
var conventions = ODataRoutingConventions.CreateDefault();
//adding the custom odata routing convention
conventions.Insert(0, new SharePointRoutingConvention());
config.Routes.MapODataRoute(
routeName: "odata",
routePrefix: null,//this is so that you can type the base url and get metadata back (http://localhost/)
model: builder.GetEdmModel(),
pathHandler: new DefaultODataPathHandler(),
routingConventions: conventions //this assigns the conventions to the route
);

writing route in mvc4 to eliminate query string

I have a situation where I am redirecting to an action that accepts 3 parameters. This I am doing like -
RedirectToAction("ProductSpecific", routeValues: new { partId = m.partId, categoryId= m.categoryId, categoryName = m.categoryName});
However, when the page loads, it contains all these parameters as query string.
Parts/ProductSpecific?partId=38&categoryId=1&categoryName=Monitor
I tried writing a route, but that didn't work. Can someone please guide on how to write a route in this scenario?
Thanks
The second argument of RedirectToAction is routeValues, so these will be appended to the querystring. Creating an extra route will still require you passing the values in the querystring, but like this: parts/productspecific/{partId}/{categoryId}/{categoryname} which i dont think you want.
If you dont want the values in the querystring, have a look at the TempData object, which is similar to session but will live until the next request.
Something like this:
public ActionResult DoSomething()
{
TempData["partId"] = partId;
TempData["catId"] = catId;
TempData["catName"] = catName;
return RedirectToAction("ProductSpecific");
}
public ActionResult ProductSpecific()
{
var partId = TempData["partId"];
var catId = TempData["catId"];
var catName = TempData["catName"];
var model = service.LoadProduct(partId, catId, catName);
return View(model);
}
Update:
For a route:
routes.MapRoute(
name: "ProductRoute",
url: "{controller}/{action}/{partId}/{categoryId}/{categoryname}",
defults: new { controller = "product", action = "productspecific"}
);
Add that route in the route.config class in app_start before your default routesm, and change your product specific method signature to accept the partid, catid and category name parameters. You can also use this from phil hack to profile your routes: Route Debugger