what is the usage of ExecuteResultAsync(ActionContext) method - c#

In a webapi/mvc Controller, a Controller will return a ActionResult<T> type, and it has a ExecuteResultAsync(ActionContext) method.
What is the usage of ExecuteResultAsync(ActionContext) method?
How the MVC will used the method? And in it, where is the ActionContext parameter from? from the Http request?

What is the usage of ExecuteResultAsync(ActionContext) method?
IActionResult is the result (object) returned by your controller action method. ASP.NET Core executes this result by invoking the IActionResult::ExecuteResultAsync(ActionContext) method.
Action method might return different kinds of IActionResult, such as JsonResult (which sends a json to client when executed), BadRequestResult(which sends 400 response when executed) and so on.
You can also create your own implementation of IActionResult. Just be aware that the ExecuteResultAsync(ActionContext) method should write bytes to the HTTP Response . For example, I created a custom IActionResult to return CSV file for your reference. This is a simple implementation that doesn't contains too much complicated logic.
Some action results are rather complicated that they introduce a new IActionResultExecutor<TResult> interface to deal with these process, for example, the ObjectResult employ an IActionResultExecutor<ObjectResult> to do that. When creating your own implementation, whether to use an IActionResultExecutor<ObjectResult> is up to you.
How the MVC will used the method?
WebApp developers don't need to invoke thisIActionResult::ExecuteResultAsync(ActionContext) method manually. This is a method that will be invoked by the MVC/RazorPage subsystem.
If you're interested, the whole process is:
a coming request received
match current request against the pre-defined routes (route table or graph). If matched :
we know the controller name, action name, and other route data.
Since we've known the controller name and action name, ASP.NET Core generates an instance of ActionDescriptor that describes the target C# action method, such as the parameters.
Since ASP.NET Core has known the Controller/Action and routes data, it creates an instance of IActionInvoker to invoke that action method pipline (including the filters, for more details, see official docs):
Action Method returns an instance of IActionResult
Before invoking the IActionResult::ExecuteResultAsync(ActionContext), invoke Result Filters OnResultExecuting() method.
invoke IActionResult::ExecuteResultAsync(ActionContext)
After that, invoke the Result Filters OnResultExecuted() method.
where is the ActionContext parameter from? from the Http request?
Firstly, HttpContext is built by the underlying server. It contains a Request property that mimics the HTTP Request.
Next, we'll get another two objects after selecting the action:
RouteData: the route data, e.g. current area name, current page name, e.t.c.
ActionDescriptor: a description about the current action that are matched with current route.
With the above three objects, ASP.NET Core creates the ActionContext by simply new it. For example, the IRouter-based Routing system creates an actionContext as below:( see source code)
// create action context
var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor);
// create action invoker
var invoker = _actionInvokerFactory.CreateInvoker(actionContext);
if (invoker == null){ throw ...;}
// invoke the pipeline
return invoker.InvokeAsync();

Related

How does Consumes filter work with action selection?

I saw some code like this:
[HttpPost]
[Consumes("application/json")]
public string SaveProductJson(ProductBindingTarget product) {
return $"JSON: {product.Name}";
}
[HttpPost]
[Consumes("application/xml")]
public string SaveProductXml(ProductBindingTarget product) {
return $"XML: {product.Name}";
}
I get the idea of how Consumes filter work, but a little bit confused about how it work internally. Below is the picture from MSDN:
From my understanding, the routing middleware will select the matching action method. Let's say I post a json document to the application, so both SaveProductJson and SaveProductXml match the request (because they have the same routing template [HttpPost]) and Consumes filter hasn't kicked in yet (filters run in endpoint middleware), since Consumes filter runs after routing middleware, how does Consumes filter tell the routing middleware to select SaveProductJson action method?
The Consumes attribute works together with Content-Type header. In your case application/json or application/xml, but it also supports other content types such as application/x-www-form-urlencoded if you want to submit a form.
The Consumes attribute allows an action to limit the supported request content types. Apply the Consumes attribute to an action or controller, specifying one or more content type.
Read more here
The ConsumeAttribute class inherits from IResourceFilter, which is an extensible hook in the MVC pipeline that is called from Controller/Page implementations of ResourceInvoker.
When the ResourceInvoker is executing during the middleware pipeline, it calls the OnResourceExecuting method of the ConsumesAttribute if it has been previously discovered on target actions.
This method then checks the incoming Content-Type header of the request and compares it
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Only execute if the current filter is the one which is closest to the action.
// Ignore all other filters. This is to ensure we have a overriding behavior.
if (IsApplicable(context.ActionDescriptor))
{
var requestContentType = context.HttpContext.Request.ContentType;
// Confirm the request's content type is more specific than a media type this action supports e.g. OK
// if client sent "text/plain" data and this action supports "text/*".
//
// Requests without a content type do not return a 415. It is a common pattern to place [Consumes] on
// a controller and have GET actions
if (!string.IsNullOrEmpty(requestContentType) && !IsSubsetOfAnyContentType(requestContentType))
{
context.Result = new UnsupportedMediaTypeResult();
}
}
}
In addition, the attribute also inherits from IActionConstraint which will call the Accept method of targetable constraints in the ActionConstraintMatcherPolicy. This component determines the appropriate target action, based on the matching policies against the content-type which is provided by the ConsumeAttribute.Accept method.

How can I get the IActionContextAccessor from the Endpoint.RequestDelegate when using Endpoint Routing and MVC?

I've got a asp .net core 3.1 application and have configured MVC and Endpoint Routing. Assume I have an Endpoint object (it won't always be the Endpoint associated with the current request), I then have it's RequestDelegate. I'd like to get the IActionContextAccessor from this RequestDelegate. In the following example, when I'm in debug mode I can see the _actionContextAccessor so I know it's there.
var endpoint = this.httpContextAccessor.HttpContext.GetEndpoint();
I'm sure you'd like more context of what I'm doing and I can give you more if you like but the gist of this question is to assume I have an Endpoint object and I'm configured to use MVC, how can I get the IActionContextAccessor?
UPDATE
What I'm ultimately trying to get is the actions parameters. Just the type of the input parameters. We follow a one input model convention so actually what I want is that one input model's type.
To determine the type of an action's parameters, there's no need to use IActionContextAccessor or the ActionContext property it exposes. An Endpoint instance contains a set of metadata: for an endpoint that represents an action, this contains an instance of ActionDescriptor, which, unsurprisingly, describes an action. One of its properties is Parameters, which exposes the set of parameters for that action.
Putting that all together, here's an example of how to get to the type of a single action parameter, as requested:
var actionDescriptor = endpoint.Metadata.GetMetadata<ActionDescriptor>();
if (actionDescriptor != null)
{
var actionParameterType = actionDescriptor.Parameters.SingleOrDefault()?.ParameterType;
// ...
}

ASP.NET Core UrlHelper and how it works

I'm rather new to ASP.NET Core, and right now I am trying to get a grasp on how UrlHelper works in general.
In my controller, I want to create an absolute URL to another action in the same controller, e.g. http://localhost:PORT/api/controller/action. The question is now, how do I do it?
I have tried with the following:
var urlHelper = new UrlHelper(new ActionContext());
var url = urlHelper.Action("ACTION", "CONTROLLER");
Furthermore, what are those different contexts like ActionContext?
You really shouldn’t create a UrlHelper yourself. It’s likely that whatever context you are currently in, there is already an IUrlHelper instance available:
ControllerBase.Url inside of controllers.
PageModel.Url inside a Razor view.
ViewComponent.Url inside a view component.
So chances are, that you can just access this.Url to get an URL helper.
If you find yourself in a situation where that does not exist, for example when implementing your own service, then you can always inject a IUrlHelperFactory together with the IActionContextAccessor to first retrieve the current action context and then create an URL helper for it.
As for what that ActionContext is, it is basically an object that contains various values that identify the current MVC action context in which the current request is being handled. So it contains information about the actual request, the resolved controller and action, or the model state about the bound model object. It is basically an extension to the HttpContext, also containing MVC-specific information.
If you are running ASP.NET Core 2.2 or later, you can also use the LinkGenerator instead of the IUrlHelper inside your services which gives you an easier way to generate URLs compared to having to construct the helper through the IUrlHelperFactory.

OData v4 Custom Action for File Upload

I have an OData controller with standard verbs for CRUD. Everything is working fine. Now I need to add a custom action to perform file upload. I try to add a method to my existing controller like this:
[HttpPost]
[Route("UploadFile")]
public async Task<HttpResponseMessage> UploadFile()
{
//handle uploaded content logic here...
}
But when I try to invoke it by doing a POST:
http://localhost/UploadFile
I get this error:
System.InvalidOperationException: No non-OData HTTP route registered.
What should I do for this custom action to allow file upload?
You need to declare the Action as part of the EdmModel, in the following example I am assuming that your Entity Type is Attachment, and your controller class name is AttachmentsController. By convention, your EntitySet name must then be Attachments
var attachments = builder.EntitySet<Attachment>("Attachments");
attachments.Action(nameof(AttachmentsController.UploadFile))
.Returns<System.Net.Http.HttpResponseMessage>();
The important part of the above statement is the return type, if you do not declare the return type correctly in your EdmModel then you will find your endpoints returning 406 errors - Unacceptable, even though your method executes correctly, which is really confusing the first time you run into it. This is because OData will still try to parse your response to match the Accept header from the request before completing the response.
Try to use 'nameof' when mapping functions and actions instead of 'magic strings' or constants so that the compiler can pickup basic issues like wrongly defined route.
With this approach you do not need the Route attribute on the method header and the action will be included in the metadata document and therefore swagger outputs.

Attribute based webapi2 routing returns 404 for some methods

I'm presently working on a project that has been upgraded to Webapi2 from Webapi. Part of the conversion includes the switch to using attribute based routing.
I've appropriately setup my routes in the Global.asax (as follows)
GlobalConfiguration.Configure(config => config.MapHttpAttributeRoutes());
and removed the previous routing configuration.
I have decorated all of my API controllers with the appropriate System.Web.Http.RouteAttribute and System.Web.Http.RoutePrefixAttribute attributes.
If I inspect System.Web.Http.GlobalConfiguration.Configuration.Routes with the debugger I can see that all my expected routes are registered in the collection. Likewise the appropriate routes are available within the included generated Webapi Help Page documentation as expected.
Even though all appears to be setup properly a good number of my REST calls result in a 404 not found response from the server.
I've found some notable similarities specific to GET methods (this is all I've tested so far)
If a method accepts 0 parameters it will fail
If a route overrides the prefix it will fail
If a method takes a string parameter it is likely to succeed
return type seems to have no affect
Naming a route seems to have no affect
Ordering a route seems to have no affect
Renaming the underlying method seems to have no affect
Worth noting is that my API controllers appear in a separate area, but given that some routes do work I don't expect this to be the issue at hand.
Example of non-functional method call
[RoutePrefix("api/postman")]
public class PostmanApiController : ApiController
{
...
[HttpGet]
[Route("all", Name = "GetPostmanCollection")]
[ResponseType(typeof (PostmanCollectionGet))]
public IHttpActionResult GetPostmanCollection()
{
return Ok(...);
}
...
}
I expect this to be available via http://[application-root]/api/postman/all
Interestingly enough a call to
Url.Link("GetPostmanCollection", null)
will return the above expected url
A very similar example of method calls within the same controller where some work and some do not.
[RoutePrefix("api/machine")]
public class MachineApiController : ApiController
{
...
[HttpGet]
[Route("byowner/{owner}", Name = "GetPostmanCollection")]
public IEnumerable<string> GetByOwner([FromUri] string owner)
{
...
}
...
[HttpGet]
[Route("~/api/oses/{osType}")]
public IEnumerable<OsAndVersionGet> GetOSes([FromUri] string osType)
{
...
}
...
}
Where a call to http://[application-root]/api/machineby/ownername succeeds and http://[application-root]/api/oses/osType does not.
I've been poking at this far too long, any idea as to what the issue may be?
Check that you configure your HttpConfiguration via the MapHttpAttributeRoutes method before any ASP.NET MVC routing registration.
In accordance to Microsoft's CodePlex entry on Attribute Routing in MVC and Web API the Design section states:
In most cases, MapHttpAttributeRoutes or MapMvcAttributeRoutes will be
called first so that attribute routes are registered before the global
routes (and therefore get a chance to supersede global routes).
Requests to attribute routed controllers would also be filtered to
only those that originated from an attribute route.
Therefore, within the Global.asax (or where registering routes) it is appropriate to call:
GlobalConfiguration.Configure(c => c.MapHttpAttributeRoutes()); // http routes
RouteTable.Routes.MapRoute(...); // mvc routes
In my case it was a stupid mistake, I am posting this so people behind me making the same mistake may read this before they check everything else at quantum level.
My mistake was, my controller's name did not end with the word Controller.
Happy new year

Categories

Resources