ASP.NET Core Razor Page multiple path routing - c#

I'm building a system using ASP.NET Core 2.0 Razor Pages (not MVC) and I'm having trouble adding multiple routes for pages. For example, all pages should be able to be reached by abc.com/language/segment/shop/mypage or abc.com/language/shop/mypage, where both paths point to the same page. The segment path section is optional, then the pages do stuff with the optional segment info.
The question mark syntax in the CombineTemplates markup doesn't seem to work, it seems to only work in the last section of the path. Browsing to a url without a value in the {segment?} section resulted in 404. For example:
AttributeRouteModel.CombineTemplates("{language}/{segment?}/shop", selector.AttributeRouteModel.Template);
I tried code like this below but it only appends the two paths to each other, and I need to be able to enable them both as valid.
options.Conventions.Add(new DefaultPageRouteModelConvention());
options.Conventions.Add(new SegmentPageRouteModelConvention());
In ASP.NET MVC, I could just add two different routes pointing to the same area/controller/action with two different named MapRouteWithName.
Any ideas how to do this with .NET Razor Page syntax?

This code works:
Add a single convention (not two different conventions):
options.Conventions.Add(new CombinedPageRouteModelConvention());
In the new convention, add both route selectors:
private class CombinedPageRouteModelConvention : IPageRouteModelConvention
{
private const string BaseUrlTemplateWithoutSegment = "{language}/shop";
private const string BaseUrlTemplateWithSegment = "{language}/{segment}/shop";
public void Apply(PageRouteModel model)
{
var allSelectors = new List<SelectorModel>();
foreach (var selector in model.Selectors)
{
//setup the route with segment
allSelectors.Add(CreateSelector(selector, BaseUrlTemplateWithSegment));
//setup the route without segment
allSelectors.Add(CreateSelector(selector, BaseUrlTemplateWithoutSegment));
}
//replace the default selectors with new selectors
model.Selectors.Clear();
foreach (var selector in allSelectors)
{
model.Selectors.Add(selector);
}
}
private static SelectorModel CreateSelector(SelectorModel defaultSelector, string template)
{
var fullTemplate = AttributeRouteModel.CombineTemplates(template, defaultSelector.AttributeRouteModel.Template);
var newSelector = new SelectorModel(defaultSelector)
{
AttributeRouteModel =
{
Template = fullTemplate
}
};
return newSelector;
}
}

Related

Manually match an url against the registered endpoints in .NET Core 3.0

For my application I would like to match an url against all the registered routes to see there is a match.
When there is a match, I would like to extract the routevalues from the match.
I got this working in ASP.NET Core 2.1, but i do not seem to be able to retrieve the routes the way they are retrieved in .NET Core 3
Working ASP.NET Core 2.1 sample:
string url = "https://localhost/Area/Controller/Action";
// Try to match against the default route (but can be any other route)
Route defaultRoute = this._httpContextAccessor.HttpContext.GetRouteData().Routers.OfType<Route>().FirstOrDefault(p => p.Name == "Default");
RouteTemplate defaultTemplate = defaultRoute.ParsedTemplate;
var defaultMatcher = new TemplateMatcher(defaultTemplate, defaultRoute.Defaults);
var defaultRouteValues = new RouteValueDictionary();
string defaultLocalPath = new Uri(url).LocalPath;
if (!defaultMatcher.TryMatch(defaultLocalPath, defaultRouteValues))
{
return null;
}
string area = defaultRouteValues["area"]?.ToString();
string controller = defaultRouteValues["controller"]?.ToString();
string actiondefaultRouteValues["action"]?.ToString();
Is there a way to obtain all registered endpoints (templates) and match against these templates?
In ASP.NET Core 2.1 and below, routing was handled by implementing the IRouter interface to map incoming URLs to handlers. Rather than implementing the interface directly, you would typically rely on the MvcMiddleware implementation added to the end of your middleware pipeline.
In ASP.NET Core 3.0, we use endpoint routing, so the routing step is separate from the invocation of the endpoint. In practical terms that means we have two pieces of middleware:
EndpointRoutingMiddleware that does the actual routing i.e.
calculating which endpoint will be invoked for a given request URL
path.
EndpointMiddleware that invokes the endpoint.
So you could try the following method to match an url against all the registered routes to see there is a match in asp.net core 3.0.
public class TestController : Controller
{
private readonly EndpointDataSource _endpointDataSource;
public TestController ( EndpointDataSource endpointDataSource)
{
_endpointDataSource = endpointDataSource;
}
public IActionResult Index()
{
string url = "https://localhost/User/Account/Logout";
// Return a collection of Microsoft.AspNetCore.Http.Endpoint instances.
var routeEndpoints = _endpointDataSource?.Endpoints.Cast<RouteEndpoint>();
var routeValues = new RouteValueDictionary();
string LocalPath = new Uri(url).LocalPath;
//To get the matchedEndpoint of the provide url
var matchedEndpoint = routeEndpoints.Where(e => new TemplateMatcher(
TemplateParser.Parse(e.RoutePattern.RawText),
new RouteValueDictionary())
.TryMatch(LocalPath, routeValues))
.OrderBy(c => c.Order)
.FirstOrDefault();
if (matchedEndpoint != null)
{
string area = routeValues["area"]?.ToString();
string controller = routeValues["controller"]?.ToString();
string action = routeValues["action"]?.ToString();
}
return View();
}
}
You could refer to this blog for more details on the endpoint routing in ASP.NET Core 3.0.

How can I supply a path parameter to the displayed path in SwaggerUi?

I have setup Swagger/Swashbuckle on my WebAPI project. I have followed the guide from Microsoft which covers how to setup Aspnet.WebApi.Versioning with Swagger. My API has multiple versions, so there is a {version} parameter set in the route attributes, like this:
[ApiVersion("2")]
[RoutePrefix("api/{version:apiVersion}/values")]
public class AccountController : ApiController
{
[Route("UserInfo")]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
My issue is that this shows a {version} attribute in the path shown in the docs, like this:
Instead I want this path attribute to actually have the value in the ApiVersion attribute, so that there can be no confusion for the clients who read the documentation. Ideally, given that the UrlDiscoverySelector is set to v2 the paths above should be:
/api/2/account/userinfo
/api/2/account/externallogin
/api/2/account/manageinfo
My attempts to simply replace the {version} in the RelativePath of the ApiExplorer worked in the UI, but broke the test functionality as {version} was changed to a query parameter instead of a path, which is not how my API is configured.
Is it possible I can amend the values in the ApiExplorer before swagger builds the documentation while still retaining test functionality?
The API Explorer for API versioning now supports the behavior out-of-the-box using:
options.SubstituteApiVersionInUrl = true
This will do the substitution work for you and removes the API version parameter from the action descriptor. You generally don't need to change the default format applied to the substituted value, but you can change it using:
options.SubstitutionFormat = "VVV"; // this is the default
I'm using a Swashbuckle and used a Document filter
public class VersionedOperationsFilter : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var apiDescriptionsGroup in context.ApiDescriptionsGroups.Items)
{
var version = apiDescriptionsGroup.GroupName;
foreach (ApiDescription apiDescription in apiDescriptionsGroup.Items)
{
apiDescription.RelativePath = apiDescription.RelativePath.Replace("{version}", version);
}
}
}
}
and in ConfigureServices method in Startup.cs add this filter:
services.AddMvc();
var defaultApiVer = new ApiVersion(1, 0);
services.AddApiVersioning(option =>
{
option.ReportApiVersions = true;
option.AssumeDefaultVersionWhenUnspecified = true;
option.DefaultApiVersion = defaultApiVer;
});
services.AddMvcCore().AddVersionedApiExplorer(e=>e.DefaultApiVersion = defaultApiVer);
services.AddSwaggerGen(
options =>
{
var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();
options.DocumentFilter<VersionedOperationsFilter>();
//// add a swagger document for each discovered API version
//// note: you might choose to skip or document deprecated API versions differently
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName.ToString(),
CreateInfoForApiVersion(description));
}
//// integrate xml comments
options.IncludeXmlComments(Path.ChangeExtension(Assembly.GetEntryAssembly().Location, "xml"));
});
end result in Swagger UI

UrlHelper.Action includes undesired additional parameters

I have a method in the controller ApplicationsController, in which I need to get the base URL for an action method:
public ActionResult MyAction(string id)
{
var url = Url.Action("MyAction", "Applications");
...
}
The problem is that this includes the string id from the current route data, when I need the URL without (the URL is used to fetch content from a CMS on a URL-based lookup).
I have tried passing null and new { } as the routeValues parameter to no avail.
The matching route is as follows (above all other routes):
routes.MapLowercaseRoute(
name: "Applications",
url: "applications/{action}/{id}",
defaults: new { controller = "Applications",
action = "Index", id = UrlParameter.Optional });
I've seen a couple of other questions touch on this but none of them seem to have a viable solution. At present, I am resorting to hardcoding the path in the controller; however, I'd like to be able to abstract this into an action filter, so I need to be able to generate the URL.
Is there a clean/conventional way to prevent this behaviour?
Ok, I see the problem. It's something called "Segment variable reuse". When generating the routes for outbound URLs, and trying to find values for each of the segment variables in a route’s URL pattern, the routing system will look at the values from the current request. This is a behavior that confuses many programmers and can lead to a lengthy debugging session. The routing system is keen to make a match against a route, to the extent that it will reuse segment variable values from the incoming URL. So I think you have to override the value like Julien suggested :
var url = Url.Action("MyAction", "Applications", new { id = "" })
Ended up getting around this with a different approach. The only way I could come up with to prevent arbitrarily-named route values from being inserted into the generated URL was to temporarily remove them from RouteData when calling Url.Action. I've written a couple of extension methods to facilitate this:
public static string NonContextualAction(this UrlHelper helper, string action)
{
return helper.NonContextualAction(action,
helper.RequestContext.RouteData.Values["controller"].ToString());
}
public static string NonContextualAction(this UrlHelper helper, string action,
string controller)
{
var routeValues = helper.RequestContext.RouteData.Values;
var routeValueKeys = routeValues.Keys.Where(o => o != "controller"
&& o != "action").ToList();
// Temporarily remove routevalues
var oldRouteValues = new Dictionary<string, object>();
foreach (var key in routeValueKeys)
{
oldRouteValues[key] = routeValues[key];
routeValues.Remove(key);
}
// Generate URL
string url = helper.Action(routeValues["Action"].ToString(),
routeValues["Controller"].ToString());
// Reinsert routevalues
foreach (var kvp in oldRouteValues)
{
routeValues.Add(kvp.Key, kvp.Value);
}
return url;
}
This allows me to do this in an action filter where I won't necessarily know what the parameter names for the action are (and therefore can't just pass an anonymous object as in the other answers).
Still very much interested to know if someone has a more elegant solution, however.
Use a null or empty value for id to prevent Url.Action from using the current one:
var url = Url.Action("MyAction", "Applications", new { id = "" })
I was not entirely comfortable with the altering, transient or otherwise, of the RouterData in #AntP's otherwise fine solution. Since my code for creating the links was already centralized, I borrowed #Tomasz Jaskuλa and #AntP to augment the ExpandoObject, I was already using.
IDictionary<string,object> p = new ExpandoObject();
// Add the values I want in the route
foreach (var (key, value) in linkAttribute.ParamMap)
{
var v = GetPropertyValue(origin, value);
p.Add(key, v);
}
// Ideas borrowed from https://stackoverflow.com/questions/20349681/urlhelper-action-includes-undesired-additional-parameters
// Null out values that I don't want, but are already in the RouteData
foreach (var key in _urlHelper.ActionContext.RouteData.Values.Keys)
{
if (p.ContainsKey(key))
continue;
p.Add(key, null);
}
var href = _urlHelper.Action("Get", linkAttribute.HRefControllerName, p);

Why dataTokens are in Route?

context.MapRoute("authorized-credit-card", "owners/{ownerKey}/authorizedcreditcard/{action}",
new { controller = "authorizedcreditcard", action = "index" },
new { ownerKey = nameFormat }, dataTokens: new { scheme = Uri.UriSchemeHttps });
In my route file I am having above kind of Route.
So, could any one tell me what is the meaning of dataTokens: new { scheme = Uri.UriSchemeHttps ?
And usage of above dataTokens inside the controller's action method ?
According to the documentation:
You use the DataTokens property to retrieve or assign values associated with the route that are not used to determine whether a route matches a URL pattern. These values are passed to the route handler, where they can be used for processing the request.
So DataTokens is kind of additional data which can be passed with the route. There are 3 DataToken's keys being predefined (the class below comes form source code of ASP.NET MVC 4 but the same keys are used in version 2):
internal class RouteDataTokenKeys
{
public const string UseNamespaceFallback = "UseNamespaceFallback";
public const string Namespaces = "Namespaces";
public const string Area = "area";
}
I don't think the framework uses DataToken named "scheme" so it is difficult to answer your question. You may want to search your custom application code for DataTokens["scheme"] and see where and why it is needed.
EDIT:
I've found an article on "Adding HTTPS/SSL support to ASP.NET MVC routing". There is an example of using "scheme" data token. So I'm pretty sure that your application uses it in the very same way.

ASP.NET MVC: Redirect from query string params to a canonical url

In my Asp.Net Mvc project I'd like to have a good looking urls, e.g. mysite.com/Page2, and I want to redirect from my old style urls (such as mysite.com?page=2) with 301 state so that there won't be two urls with identical content. Is there a way to do it?
As far as I know Asp.Net binding framework doesn't make difference between query string and curly brace params
I am not sure, I got your question right. It seems, your current setup relies on those GET parameters (like mysite.com?page=2). If you dont want to change this, you will have to use those parameters further. There would be no problem in doing so, though. Your users do not have to use or see them. In order to publish 'new style URLs' only, you may setup a URL redirect in your web server. That would change new style URLs to old style URLs.
The problem is the 301. If the user requests an old style URL, it would be accepted by the webserver as well. Refusing the request with a 301 error seems hard to achieve for me.
In order to get around this, I guess you will have to change your parameter scheme. You site may still rely on GET parameters - but they get a new name. Lets say, your comments are delivered propery for the following (internal) URL in the old scheme:
/Article/1022/Ms-Sharepoint-Setup-Manual?newpage=2
Note the new parameter name. In your root page (or master page, if you are using those), you may handle the redirect permanent (301) manually. Therefore, incoming 'old style requests' are distinguishable by using old parameter names. This could be used to manually assemble the 301 in the response in ASP code.
Personally, I would sugesst, to give up the 301 idea and just use URL redirection.
Well, as far as I can see performing such redirection in ASP.NET MVC might be tricky. This is how I did it:
global.asax:
routes.Add(new QueryStringRoute());
routes.MapRoute(null, "Article/{id}/{name}",
new { controller = "Article", action = "View", page = 1 },
new { page = #"\d+" }
);
routes.MapRoute(null, "Article/{id}/{name}/Page{page}",
new { controller = "Article", action = "View" },
new { page = #"\d+" }
);
QueryStringRoute.cs:
public class QueryStringRoute : RouteBase
{
private static string[] queryStringUrls = new string[]
{
#"~/Article/\d{1,6}/.*?page=\d{1,3}"
};
public override RouteData GetRouteData(HttpContextBase httpContext)
{
string url = httpContext.Request.AppRelativeCurrentExecutionFilePath;
foreach (string queryStringUrl in queryStringUrls)
{
Regex regex = new Regex(queryStringUrl);
if (regex.IsMatch(url))
{
long id = 0; /* Parse the value from regex match */
int page = 0; /* Parse the value from regex match */
string name = ""; /* Parse the value from regex match */
RouteData rd = new RouteData(this, new MvcRouteHandler());
rd.Values.Add("controller", "QueryStringUrl");
rd.Values.Add("action", "Redirect");
rd.Values.Add("id", id);
rd.Values.Add("page", page);
rd.Values.Add("name", name);
rd.Values.Add("controllerToRedirect", "Article");
rd.Values.Add("actionToRedirect", "View");
return rd;
}
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
QueryStringUrlController.cs:
public class QueryStringUrlController : Controller
{
public RedirectToRouteResult Redirect(long id, int page, string name,
string controllerToRedirect, string actionToRedirect)
{
return RedirectToActionPermanent(actionToRedirect, controllerToRedirect, new { id = id, page = page, name = name });
}
}
Assuming you have such routing as in my global.asax file (listed above) you can create a custom Route class that will handle incoming requests and map them on a special redirection controller which will then redirect them to appropriate urls with 301 state. Then you must add this route to global.asax before your "Article" routes
If you're using IIS 7, the URL Rewrite Module should work for your scenario.

Categories

Resources