'How could I get a parameter of an action from the ResultExecutingContext when using a Filter

I'm creating a custom result filter that implements IAsyncResultFilter interface. In this filter I need the actual value returned by the Action, but I also need a parameter from that action (This parameter comes from the query string and is used for pagination). The thing is that I haven't found a way to get the parameter value directly from the ResultExecutingContext that is the variable I have available in the result filter implementation

I've tried to get the parameter value using the context.ActionDescriptor.Parameters, but it's a collection of ParameterDescriptor, and I haven't been able to get the actual value from it, just an instance of ParameterDescriptor

I've been able to get the mediaType from the headers and also the query string itself, but not the query string binded to the PagingModel type.

Is there any way to get this parameter from the ResultExecutingContext variable?


// Action from the ApiController
[HttpGet]
[AddPaginationHeader]
public async Task<IActionResult> Get([FromQuery]PagingModel pagingModel, 
    [FromHeader(Name = "Accept")]string mediaType) {
    var pagedCollection = repository.GetPage(pagingModel);
    var shapedCollection = ShapeCollectionOfData(pagedCollection);
    return Ok(shapedCollection);
}

// AddPaginationHeader Implementation (Result Filter)
public Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) {
    var result = context.Result as ObjectResult;
    if (result?.Value != null && result?.StatusCode >= 200 &&
        result?.StatusCode < 300) {
        // Trying to get the pagingModel (but getting a ParameterDescriptor type)
        var pagingModel = context.ActionDescriptor.Parameters.Where(t=>t.Name.Equals("pagingModel")).FirstOrDefault();       
        //Getting the media type
        string mediaType = context.HttpContext.Request.Headers["Accept"]
        // Getting the query string itself
        string queryString = context.HttpContext.Request.QueryString.ToString();
        //Implementation of the actual logic that needs the paging model
        // ...
        next();
    }
    return Task.CompletedTask;
}



Solution 1:[1]

For getting PagingModel, you could try TryUpdateModelAsync like

public class AddPaginationHeader : Attribute, IAsyncResultFilter
{
    // AddPaginationHeader Implementation (Result Filter)
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        //PagingModel model = new PagingModel();
        //await controller.TryUpdateModelAsync(model);
        var result = context.Result as ObjectResult;
        if (result?.Value != null && result?.StatusCode >= 200 &&
            result?.StatusCode < 300)
        {
            // Trying to get the pagingModel (but getting a ParameterDescriptor type)
            var controller = context.Controller as Controller;
            var parameterDescriptor = context.ActionDescriptor.Parameters.Where(t => t.Name.Equals("pagingModel")).FirstOrDefault();
            var parameter = Activator.CreateInstance(parameterDescriptor.ParameterType);
            await controller.TryUpdateModelAsync(parameter, parameterDescriptor.ParameterType, "");
            var pagingModel = parameter;
            //Getting the media type
            string mediaType = context.HttpContext.Request.Headers["Accept"];
            // Getting the query string itself
            string queryString = context.HttpContext.Request.QueryString.ToString();
            //Implementation of the actual logic that needs the paging model
            // ...
            next();
        }
        //return Task.CompletedTask;
    }
}

Solution 2:[2]

Based on the answer of Tao I created a nice static method that allows you to get any parameter of any type from a Controller:

private static async Task<TParameter> GetParameterFromAction<TParameter>(ResultExecutingContext context, string parameterName) {
    var parameterDescriptor = context.ActionDescriptor.Parameters.Where(t => t.Name.Equals(parameterName)).FirstOrDefault();
    ControllerBase controller = context.Controller as ControllerBase;
    TParameter parameter = (TParameter)Activator.CreateInstance(parameterDescriptor.ParameterType);
    await controller.TryUpdateModelAsync(parameter, parameterDescriptor.ParameterType, string.Empty);
    return parameter;
}

Solution 3:[3]

In .net core 3.1 you can retrieve parameters and their values in your filter like this:

foreach (var argument in context.ActionArguments)
{
    object parameterValue = argument.Value;
    string parameterName = argument.Key;
}

This code retrieves parameter values for basic datatypes. You can extend this code to handle objects and lists of values. The argument.Value property will contain the values of the objects properties or list of values.

Solution 4:[4]

using the answer from @carlos as a starting point I augmented the implementation a bit to fit my needs. Because Action methods are very unlikely to have multiple parameters with the same type we can use that as the filter instead of parameter name.

public static class ResultExecutingContextExtensions
{
    public static async Task<TParameter> GetParameterFromAction<TParameter>(this ResultExecutingContext context) where TParameter : class, new()
    {
        var parameterDescriptor = context.ActionDescriptor.Parameters
            .FirstOrDefault(t => t.ParameterType == typeof(TParameter));
        ControllerBase controller = context.Controller as ControllerBase;
        TParameter parameter = new TParameter();
        await controller.TryUpdateModelAsync(parameter, parameterDescriptor.ParameterType, string.Empty);
        return parameter;
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Edward
Solution 2 carlos chourio
Solution 3 Pontus Magnusson
Solution 4 Matt M