How to create a custom result filter to add a pagination header to a response from a webapi controller

Viewed 893

I have the following scenario:

I want to create a custom result filter that allows me to Add a pagination Header to a Response using a child class of the ResultFilterAttribute class. To create the Pagination Metadata used in the header I created a pagination helper service, that service is injected in the controller class and needs a PagingModel object to generate the metadata.

To add the pagination metadata to the response in the filter I'd need access to both the pagingMetadata and the actual value (entity, dto, or whatever) I want to return from the controller. To be able to pass both objects to the result filter I've used a tuple.

The thing is that I want to make this filter kinda generic, and I'm trying to cast the actual value (entity, dto...) coming from the controller to an object and that's throwing me an exception saying that the cast is not possible.

How could I perform the casting? or maybe Should I use a different approach?

I tried to use the paginationHelperService directly in the result filter instead of using it in the controller, but it's not possible to pass the instance of the service from the controller since the attribute classes don't allow it.

I also tried to use generics, I think it'd be the easiest way to achieve the casting. If I just could make the attribute generic I could cast the object to its actual type but unfortunately that's not possible since it's an attribute class.


// Controller
[HttpGet]
[AddPaginationHeader]
public async Task<IActionResult> Get([FromQuery]PagingModel pagingModel, 
    [FromHeader(Name = "Accept")]string mediaType) {
    var pagedCollection = repository.GetPage(pagingModel);
    PaginationMetadata paginationMetadata = paginationHelperService.GetPagingMetadata(pagingModel);
    if (mediaType == "mycustommediatype") {
        var shapedCollection = ShapeCollectionOfData(pagedCollection);
        return Ok((shapedCollection, pagingModel));
    } else {
        return Ok((pagedCollection, pagingModel));
    }
}

// Custom Result Filter
public override void OnResultExecuting(ResultExecutingContext context) {
    var result = context.Result as ObjectResult;
    if (result?.Value != null && result?.StatusCode >= 200 &&
        result?.StatusCode < 300) {
        (object value, PaginationMetadata paginationMetadata) = ((object, PaginationMetadata))result.Value; // Casting
        string paginationMetadataString = (context.HttpContext.Request.Headers["Accept"] == "mycustommediatype")
            ? JsonConvert.SerializeObject(paginationMetadata.FullMetadata)
            : JsonConvert.SerializeObject(pagingMetadata.GenericMetadata);
        context.HttpContext.Response.Headers.Add("X-Pagination", paging);
        context.Result.Value = value;
    }
}

1 Answers

I tried to use the paginationHelperService directly in the result filter instead of using it in the controller, but it's not possible to pass the instance of the service from the controller since the attribute classes don't allow it.

Although you can't inject service into an Attribute, you could use an attribute of ServiceFilter(typeof(Your_Filter_Type)) to enable any filter so that you could inject services as you like.

For example, create a AddPaginationHeader result filter ( which is not an Attribute):

public class AddPaginationHeader : IResultFilter
{
    private readonly IRepository repository;

    // inject services
    public AddPaginationHeader(IRepository repository, ... other services)
    {
        this.repository = repository;
    }

    public void IResultFilter.OnResultExecuting(ResultExecutingContext context)
    {
        ...
    }

    public void IResultFilter.OnResultExecuted(ResultExecutedContext context) { ... }

}

And don't forget to register this service within Startup.cs:

services.AddScoped<AddPaginationHeader>();

Lastly, you could enable this filter by [ServiceFilterAttribute]:

[HttpGet]
[ServiceFilter(typeof(AddPaginationHeader))]
public async Task Get([FromQuery]PagingModel pagingModel, [FromHeader(Name = "Accept")]string mediaType) 
{
    ....
}

I also tried to use generics, I think it'd be the easiest way to achieve the casting. If I just could make the attribute generic I could cast the object to its actual type but unfortunately that's not possible since it's an attribute class.

The same trick can be performed for generic types too. For example, Change the above AddPaginationHeader filter to class AddPaginationHeader<TResult> : IResultFilter , and you could enable this filter by :

[ServiceFilter(typeof(AddPaginationHeader<(object,PaginationMetadata)>))]

You could extend the generic TResult as you want. The only trick is adding the filter by ServiceFilter.

Related