Customize aspnet core routing attribute so that Url.Action() returns a different url? - asp.net-core

This is an example of what I want to achieve, however I want to do my own custom attribute that also feeds itself from something other than the request url. In the case of HttpGet/HttpPost these built-in attributes obviously have to look at the http request method, but is there truly no way to make Url.Action() resolve the correct url then?
[HttpGet("mygeturl")]
[HttpPost("myposturl")]
public ActionResult IndexAsync()
{
// correct result: I get '/mygeturl' back
var getUrl = Url.Action("Index");
// wrong result: It adds a ?method=POST query param instead of returning '/myposturl'
var postUrl = Url.Action("Index", new { method = "POST" });
return View();
}
I've looked at the aspnet core source code and I truly can't find a feature that would work here. All the LinkGenerator source code seems to require routedata values but routedata always seems to require to be in the url somewhere, either in the path or in the query string. But even if I add the routedata value programmatically, it won't be in time for the action selection or the linkgenerator doesn't care.
In theory what I need is to pass something to the UrlHelper/LinkGenerator and have it understand that I want the url back out that I defined in my custom attribute, in this case the HttpPost (but I'll make my own attribute).

Related

How to return a status code from an endpoint that can then be handled by app.UseStatusCodePages() middleware?

If I return StatusCode(403) or any other error code from an endpoint, any configuration of app.UseStatusCodePages<whatever> will be ignored.
I believe this is because the StatusCode(<whatever>) will automatically create a result object, and UseStatusCodePages only kicks in if there is an error status code and no content.
So how do I set a status code result in an IActionResult type endpoint and then return without setting any content so that UseStatusCodePages will handle the job of providing a suitable resonse?
As far as I know, the UseStatusCodePages will just be fired when the action result is the StatusCodeResult.
If you put some value inside the status codes, it will return the object result which will not trigger the UseStatusCodePages.
So I suggest you could directly use StatusCodeResult(403), then if you want to put some value to the StatusCodeResult, I suggest you could put it inside the httpcontext's item.
More details, you could refer to below codes:
public IActionResult OnGet()
{
HttpContext.Items.Add("test","1");
return StatusCode(403);
}
Program.cs:
app.UseStatusCodePages(async statusCodeContext =>
{
var status = statusCodeContext.HttpContext.Items["test"];
// using static System.Net.Mime.MediaTypeNames;
statusCodeContext.HttpContext.Response.ContentType = Text.Plain;
await statusCodeContext.HttpContext.Response.WriteAsync(
$"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
Result:
The issue was that I have the ApiController attribute on the endpoint controller. One of the things this attribute does is to automatically create a ProblemDetails response body for any failed requests, and it is this that prevents UseStatusCodePages from having any effect.
The solution is to either remove the ApiController attribute if you do not require any of its features, or alternatively its behaviour of automatically creating ProblemDetails responses can be disabled using the following configuration in Program.cs (or Startup.cs in old style projects).
builder.Services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
options.SuppressMapClientErrors = true;
});

Efficient way to bring parameters into controller action URL's

In ASP.Net Core you have multiple ways to generate an URL for controller action, the newest being tag helpers.
Using tag-helpers for GET-requests asp-route is used to specify route parameters. It is from what I understand not supported to use complex objects in route request. And sometimes a page could have many different links pointing to itself, possible with minor addition to the URL for each link.
To me it seems wrong that any modification to controller action signature requires changing all tag-helpers using that action. I.e. if one adds string query to controller, one must add query to model and add asp-route-query="#Model.Query" 20 different places spread across cshtml-files. Using this approach is setting the code up for future bugs.
Is there a more elegant way of handling this? For example some way of having a Request object? (I.e. request object from controller can be put into Model and fed back into action URL.)
In my other answer I found a way to provide request object through Model.
From the SO article #tseng provided I found a smaller solution. This one does not use a request object in Model, but retains all route parameters unless explicitly overridden. It won't allow you to specify route through an request object, which is most often not what you want anyway. But it solved problem in OP.
<a asp-controller="Test" asp-action="HelloWorld" asp-all-route-data="#Context.GetQueryParameters()" asp-route-somestring="optional override">Link</a>
This requires an extension method to convert query parameters into a dictionary.
public static Dictionary GetQueryParameters(this HttpContext context)
{
return context.Request.Query.ToDictionary(d => d.Key, d => d.Value.ToString());
}
There's a rationale here that I don't think you're getting. GET requests are intentionally simplistic. They are supposed to describe a specific resource. They do no have bodies, because you're not supposed to be passing complex data objects in the first place. That's not how the HTTP protocol is designed.
Additionally, query string params should generally be optional. If some bit of data is required in order to identify the resource, it should be part of the main URI (i.e. the path). As such, neglecting to add something like a query param, should simply result in the full data set being returned instead of some subset defined by the query. Or in the case of something like a search page, it generally will result in a form being presented to the user to collect the query. In other words, you action should account for that param being missing and handle that situation accordingly.
Long and short, no, there is no way "elegant" way to handle this, I suppose, but the reason for that is that there doesn't need to be. If you're designing your routes and actions correctly, it's generally not an issue.
To solve this I'd like to have a request object used as route parameters for anchor TagHelper. This means that all route links are defined in only one location, not throughout solution. Changes made to request object model automatically propagates to URL for <a asp-action>-tags.
The benefit of this is reducing number of places in the code we need to change when changing method signature for a controller action. We localize change to model and action only.
I thought writing a tag-helper for a custom asp-object-route could help. I looked into chaining Taghelpers so mine could run before AnchorTagHelper, but that does not work. Creating instance and nesting them requires me to hardcode all properties of ASP.Net Cores AnchorTagHelper, which may require maintenance in the future. Also considered using a custom method with UrlHelper to build URL, but then TagHelper would not work.
The solution I landed on is to use asp-all-route-data as suggested by #kirk-larkin along with an extension method for serializing to Dictionary. Any asp-all-route-* will override values in asp-all-route-data.
<a asp-controller="Test" asp-action="HelloWorld" asp-all-route-data="#Model.RouteParameters.ToDictionary()" asp-route-somestring="optional override">Link</a>
ASP.Net Core can deserialize complex objects (including lists and child objects).
public IActionResult HelloWorld(HelloWorldRequest request) { }
In the request object (when used) would typically have only a few simple properties. But I thought it would be nice if it supported child objects as well. Serializing object into a Dictionary is usually done using reflection, which can be slow. I figured Newtonsoft.Json would be more optimized than writing simple reflection code myself, and found this implementation ready to go:
public static class ExtensionMethods
{
public static IDictionary ToDictionary(this object metaToken)
{
// From https://geeklearning.io/serialize-an-object-to-an-url-encoded-string-in-csharp/
if (metaToken == null)
{
return null;
}
JToken token = metaToken as JToken;
if (token == null)
{
return ToDictionary(JObject.FromObject(metaToken));
}
if (token.HasValues)
{
var contentData = new Dictionary();
foreach (var child in token.Children().ToList())
{
var childContent = child.ToDictionary();
if (childContent != null)
{
contentData = contentData.Concat(childContent)
.ToDictionary(k => k.Key, v => v.Value);
}
}
return contentData;
}
var jValue = token as JValue;
if (jValue?.Value == null)
{
return null;
}
var value = jValue?.Type == JTokenType.Date ?
jValue?.ToString("o", CultureInfo.InvariantCulture) :
jValue?.ToString(CultureInfo.InvariantCulture);
return new Dictionary { { token.Path, value } };
}
}

How to get current url in view in asp.net core 1.0

In previous versions of asp.net, we could use
#Request.Url.AbsoluteUri
But it seems it's changed. How can we do that in asp.net core 1.0?
You have to get the host and path separately.
#Context.Request.Host
#Context.Request.Path
You need scheme, host, path and queryString
#string.Format("{0}://{1}{2}{3}", Context.Request.Scheme, Context.Request.Host, Context.Request.Path, Context.Request.QueryString)
or using new C#6 feature "String interpolation"
#($"{Context.Request.Scheme}://{Context.Request.Host}{Context.Request.Path}{Context.Request.QueryString}")
You can use the extension method of Request:
Request.GetDisplayUrl()
This was apparently always possible in .net core 1.0 with Microsoft.AspNetCore.Http.Extensions, which adds extension to HttpRequest to get full URL; GetEncodedUrl.
e.g. from razor view:
#using Microsoft.AspNetCore.Http.Extensions
...
Link to myself
Since 2.0, also have relative path and query GetEncodedPathAndQuery.
Use the AbsoluteUri property of the Uri, with .Net core you have to build the Uri from request like this,
var location = new Uri($"{Request.Scheme}://{Request.Host}{Request.Path}{Request.QueryString}");
var url = location.AbsoluteUri;
e.g. if the request url is 'http://www.contoso.com/catalog/shownew.htm?date=today' this will return the same url.
You can consider to use this extension method (from Microsoft.AspNetCore.Http.Extensions namespace:
#Context.Request.GetDisplayUrl()
For some my projects i prefer more flexible solution. There are two extensions methods.
1) First method creates Uri object from incoming request data (with some variants through optional parameters).
2) Second method receives Uri object and returns string in following format (with no trailing slash): Scheme_Host_Port
public static Uri GetUri(this HttpRequest request, bool addPath = true, bool addQuery = true)
{
var uriBuilder = new UriBuilder
{
Scheme = request.Scheme,
Host = request.Host.Host,
Port = request.Host.Port.GetValueOrDefault(80),
Path = addPath ? request.Path.ToString() : default(string),
Query = addQuery ? request.QueryString.ToString() : default(string)
};
return uriBuilder.Uri;
}
public static string HostWithNoSlash(this Uri uri)
{
return uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped);
}
Usage:
//before >> https://localhost:44304/information/about?param1=a&param2=b
Request.GetUri(addQuery: false);
//after >> https://localhost:44304/information/about
//before >> https://localhost:44304/information/about?param1=a&param2=b
new Uri("https://localhost:44304/information/about?param1=a&param2=b").GetHostWithNoSlash();
//after >> https://localhost:44304
There is a clean way to get the current URL from a Razor page or PageModel class. That is:
Url.PageLink()
Please note that I meant, the "ASP.NET Core Razor Pages", not the MVC.
I use this method when I want to print the canonical URL meta tag in the ASP.NET Core razor pages. But there is a catch. It will give you the URL which is supposed to be the right URL for that page. Let me explain.
Say, you have defined a route named "id" for your page and therefore, your URL should look like
http://example.com/product?id=34
The Url.PageLink() will give you exactly that URL as shown above.
Now, if the user adds anything extra on that URL, say,
http://example.com/product?id=34&somethingElse
Then, you will not get that "somethingElse" from this method. And that is why it is exactly good for printing canonical URL meta tag in the HTML page.
The accepted answer helped me, as did the comment for it from #padigan but if you want to include the query-string parameters as was the case for me then try this:
#Context.Request.PathBase#Context.Request.GetEncodedPathAndQuery()
And you will need to add #using Microsoft.AspNetCore.Http.Extensions in the view in order for the GetEncodedPathAndQuery() method to be available.
public string BuildAbsolute(PathString path, QueryString query = default(QueryString), FragmentString fragment = default(FragmentString))
{
var rq = httpContext.Request;
return Microsoft.AspNetCore.Http.Extensions.UriHelper.BuildAbsolute(rq.Scheme, rq.Host, rq.PathBase, path, query, fragment);
}
If you're looking to also get the port number out of the request you'll need to access it through the Request.Host property for AspNet Core.
The Request.Host property is not simply a string but, instead, an object that holds both the host domain and the port number. If the port number is specifically written out in the URL (i.e. "https://example.com:8080/path/to/resource"), then calling Request.Host will give you the host domain and the port number like so: "example.com:8080".
If you only want the value for the host domain or only want the value for the port number then you can access those properties individually (i.e. Request.Host.Host or Request.Host.Port).
var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : $"~{Context.Request.Path.Value}{Context.Request.QueryString}";
You may want to get the URL to use it on the razor side, there is an alternative way to get the home app URL:
Url.Content("~/.....")
Example
In the following example, I wanted to generate a QR code and display it in an img tag src, because of using custom route annotation in the Action, I can't use #URL.Action so as an alternative solution I use ~ like this:
<script>
$("#imgCode").attr("src", "#(Url.Content("~/generateQr/"))"+ code);
</script>
Controller Side
[Route("/generateQr/{code}")]
...
ILSpy show how it was done in Microsoft.Owin.dll.
// Microsoft.Owin.OwinRequest
using System;
/// <summary>
/// Gets the uniform resource identifier (URI) associated with the request.
/// </summary>
/// <returns>The uniform resource identifier (URI) associated with the request.</returns>
public virtual Uri Uri => new Uri(string.Concat(Scheme, Uri.SchemeDelimiter, Host, PathBase, Path, QueryString));
I wonder why they removed this property.

How to turn relative route into a relative url?

I have a JSON API action filter that intercepts the output an inserts hypermedia URLs.
I am injecting Microsoft.AspNet.Mvc.ApiExplorer.IApiDescriptionGroupCollectionProvider to give me access to all of the routes.
For a known action, I have the route relative path from Microsoft.AspNet.Mvc.ApiExplorer.ApiDescription.RelativePath. For example:
/articles/{id}
I also have the parameters for the route, in this case it's simply {id}.
I need to turn that relative route into a real Url. For example, for an id of 2, I would expect:
/articles/2
Is there functionality in ASP.NET 5/Core/MVC 6 that will allow me to take the parameter and the route template and create a real Url?
Edit
Thanks for the comments, here's the results of some testing. #dealdiane is correct that Hyprlinkr is not compatible with DNX/aspnet core. I investigated TypedRouting but the helpers do not use the Route templates.
I tried Microsoft.AspNetCore.Mvc.Routing.UrlHelper to create a link. You can inject IUrlHelper like any other service and use the extension methods such as:
UrlHelper helper; // Constructor injected
ApiDescription action; // I have this as a starting point
var actionDescriptor = (ControllerActionDescriptor)action.ActionDescriptor;
var actionName = actionDescriptor.Name;
var controllerName = actionDescriptor.ControllerName;
object values = new {id};
var url = helper.Action(actionName, controllerName, values);
Unfortunately, url is set to:
/articles?id=2
Which is not correct.

An interesting Restlet Attribute behavior

Using Restlet 2.1 for Java EE, I am discovering an interesting problem with its ability to handle attributes.
Suppose you have code like the following:
cmp.getDefaultHost().attach("/testpath/{attr}",SomeServerResource.class);
and on your browser you provide the following URL:
http://localhost:8100/testpath/command
then, of course, the attr attribute gets set to "command".
Unfortunately, suppose you want the attribute to be something like command/test, as in the following URL:
http://localhost:8100/testpath/command/test
or if you want to dynamically add things with different levels, like:
http://localhost:800/testpath/command/test/subsystems/network/security
in both cases the attr attribute is still set to "command"!
Is there some way in a restlet application to make an attribute that can retain the "slash", so that one can, for example, make the attr attribute be set to "command/test"? I would like to be able to just grab everything after testpath and have the entire string be the attribute.
Is this possible? Someone please advise.
For the same case I usually change the type of the variable :
Route route = cmp.getDefaultHost().attach("/testpath/{attr}",SomeServerResource.class);
route.getTemplate().getVariables().get("attr") = new Variable(Variable.TYPE_URI_PATH);
You can do this by using url encoding.
I made the following attachment in my router:
router.attach("/test/{cmd}", TestResource.class);
My test resource class looks like this, with a little help from Apache Commons Codec URLCodec
#Override
protected Representation get() {
try {
String raw = ResourceWrapper.get(this, "cmd");
String decoded = new String(URLCodec.decodeUrl(raw.getBytes()));
return ResourceWrapper.wrap(raw + " " + decoded);
} catch(Exception e) { throw new RuntimeException(e); }
}
Note my resource wrapper class is simply utility methods. The get returns the string of the url param, and the wrap returns a StringRepresentation.
Now if I do something like this:
http://127.0.0.1/test/haha/awesome
I get a 404.
Instead, I do this:
http://127.0.0.1/test/haha%2fawesome
I have URLEncoded the folder path. This results in my browser saying:
haha%2fawesome haha/awesome
The first is the raw string, the second is the result. I don't know if this is suitable for your needs as it's a simplistic example, but as long as you URLEncode your attribute, you can decode it on the other end.