I am trying to understand how to short-circuit my controller method in case content negotiation is going to fail in ASP.NET 6.
Suppose I have this method:
[Consumes("application/json")]
[Produces("application/json")]
public async Task<IActionResult> Something()
{
var result = await do_a_bunch_of_expensive_work();
return Ok(result);
}
And I call it with "Accept: application/xml". This returns, as expected, 406/Not Acceptable.
The problem is that the mismatch between Accept and Produces attribute is determined when the result is prepared (last line, after the expensive work), but I don't want to do the "do_a_bunch_of_expensive_work" in cases when it's going to end up in a 406 anyway.
Do I need to read the value of the Produces attribute and match it against the Accept header myself before doing the expensive work, or is there a more elegant way I'm missing?
Related
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).
If I have an Apex function that is named authorize() that just gets a username, password, and session token, and another function called getURL('id#', 'key'), that takes an id# for the record as a string and a key for the image to return as a string as parameters. getURL calls the authorize function inside it in order to get the credentials for its callout. The authorize is a post request, and the getURL is a get request.
I am trying to figure out how to test both of these callouts just so I can make sure that getURL is returning the proper JSON as a response. It doesn't even have to be the URL yet which is its intention eventually. But I just need to test it to make sure these callouts are working and that I am getting a response back for the 75% code coverage that it needs.
I made a multiRequestMock class that looks like this:
public class MultiRequestMock implements HttpCalloutMock {
Map<String, HttpCalloutMock> requests;
public MultiRequestMock(Map<String, HttpCalloutMock> requests) {
this.requests = requests;
}
public HTTPResponse respond(HTTPRequest req) {
HttpCalloutMock mock = requests.get(req.getEndpoint());
if (mock != null) {
return mock.respond(req);
} else {
throw new MyCustomException('HTTP callout not supported for test methods');
}
}
public void addRequestMock(String url, HttpCalloutMock mock) {
requests.put(url, mock);
}
}
I then began to write a calloutTest.cls file but wasn't sure how to use this mock class in order to test my original functions. Any clarity or assistance on this would be helpful Thank you.
I believe in your calloutTest class you use Test.setMock(HttpCalloutMock.class, new MultiRequestMock(mapOfRequests)); then call the getUrl and/or authorize methods and instead of the request really executing the response returned will be that which is specified in the response(HttpRequest) method you have implemented in the MultiRequestMock class. That is basically how I see it working, for more info and an example you can see this resource on testing callout classes. This will get you the code coverage you need but unfortunately cannot check you are getting the correct JSON response. For this, you may be able to use the dev console and Execute Anonymous?
You may want to look at simplifying your HttpCalloutMock Implementation and think about removing the map from the constructor as this class really only needs to return a simple response then your calloutTest class can be where you make sure the returned response is correct.
Hope this helps
If I have a controller with an action method that uses attribute based routing and declare it like this, all is well:
[HttpGet]
[Route("/dev/info/{*somevalue}")]
public IActionResult Get(string somevalue) {
return View();
}
I can route to the above action method for example by specifying a url that ends in /dev/info/hello-world or /dev/info/new-world
However my business requirement is to have a urls that look like this: /dev/hello-world/info or /dev/new-world/info And there is an endless set of such urls that all need to route to the same action method on the controller.
I thought to set up the attribute based route on the action method as follows:
[HttpGet]
[Route("/dev/{*somevalue}/info/")]
public IActionResult Get(string somevalue) {
return View();
}
But when I do that I get the following error as soon as the web project runs:
An unhandled exception occurred while processing the request.
RouteCreationException: The following errors occurred with attribute routing information:
For action: 'App.SomeController.Get (1-wwwSomeProject)'
Error: A catch-all parameter can only appear as the last segment of the route template.
Parameter name: routeTemplate
Microsoft.AspNetCore.Mvc.Internal.AttributeRoute.GetRouteInfos(IReadOnlyList actions)
There has to be some way to work around this error. Know a way?
Middleware is the way to achieve this.
If you need an api response is easy to implement inline.
if (app.Environment.IsDevelopment())
{
app.Use(async (context, next) =>
{
Console.WriteLine(context.Request.Path);
if (context.Request.Path.ToString().EndsWith("/info"))
{
// some logic
await context.Response.WriteAsync("Terminal Middleware.");
return;
}
await next(context);
});
}
If you need to call a controller you can simply edit request path via middleware to achieve your requirement.
You can find an example here: https://stackoverflow.com/a/50010787/3120219
It is possible to achieve this by using the regular expression:
[HttpGet]
[Route(#"/dev/{somevalue:regex(^.*$)}/info/")]
public IActionResult Get(string somevalue)
{
return View();
}
About routing constrain using the regular expressions see in the documentation: Route constraint reference
The regular expression tokens explanation:
Token
Explanation
^
Asserts position at start of a line
.
Matches any character (except for line terminators)
*
Matches the previous token between zero and unlimited times, as many times as possible
$
Asserts position at the end of a line
If it's required to have the “world”suffix in the second segment then add this suffix to the pattern like the following: [Route(#"/dev/{somevalue:regex(^.*world$)}/info/")].
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 } };
}
}
I have the following code:
[HttpPost]
public async Task<ReturnStatus> Delete([FromBody]int id)
{
await new BusinessLogic.Templates().DeleteTemplate(id);
return ReturnStatus.ReturnStatusSuccess();
}
When I run this as an AJAX request, the id is null. I've inspected the data coming in through Fiddler and the body is:
{"id":"11"}
The header has Content-Type: application/json; charset=UTF-8.
If I modify the code slightly to
[HttpPost]
public async Task<ReturnStatus> Delete([FromBody]string id)
{
await new BusinessLogic.Templates().DeleteTemplate(Convert.ToInt64(id));
return ReturnStatus.ReturnStatusSuccess();
}
it works just fine.
What am I doing wrong here?
Please read this part, number 3 in particular:
http://encosia.com/using-jquery-to-post-frombody-parameters-to-web-api/
3. [FromBody] parameters must be encoded as =value
(quoting the section for future reference:)
There are two ways to make jQuery satisfy Web API’s encoding requirement. First, you can hard code the = in front of your value, like this:
$.post('api/values', "=" + value);
Personally, I’m not a fan of that approach. Aside from just plain looking kludgy, playing fast and loose with JavaScript’s type coercsion is a good way to find yourself debugging a “wat” situation.
Instead, you can take advantage of how jQuery encodes object parameters to $.ajax, by using this syntax:
$.post('api/values', { '': value });