Search code examples
c#asp.net-coreasp.net-core-webapiactionfilterattribute

Wrapping a complex object as Response with ResultFilterAttribute


In my controller, I've inherited from a ControllerBase which there is a Result<T> method that is used to wrap the response into a ResponseBase object like this:

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public abstract class BaseApiController : ControllerBase
{ 
    protected async Task Result<T>(T content, Dictionary<string, string> headers = null,
        HttpStatusCode statusCode = HttpStatusCode.OK, 
        string contentType = "application/json")
    {
        Response.StatusCode = (int)statusCode;
        Response.ContentType = contentType;
        if (headers != null)
        {
            foreach (var header in headers)
            {
                Response.Headers.Add(header.Key, header.Value);
            }
        }
        var data = Encoding.UTF8.GetBytes
            (MySerializer.Serialize(new ResponseBase<T> { Data = content }));

        await Response.Body.WriteAsync(data.AsMemory(0, data.Length));
    } 
}

And the ResponseBase is:

public class ResponseBase
{
    [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
    public List<ErrorBase> Errors { get; set; }
}

public class ResponseBase<T> : ResponseBase
{ 
    [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
    public T Data { get; set; }
}

public class ErrorBase
{
    [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
    public string FieldName { get; set; }

    [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
    public string ErrorMessage { get; set; }
}

And finally my controller:

[ApiVersion("1")]
public class ConfigurationController : BaseApiController
{
    private readonly IConfigurationService _configurationService;

    public ConfigurationController(IConfigurationService configurationService)
    {
        _configurationService = configurationService;
    }

    [HttpGet("getData")]  
    public async Task GetData()
    {
        await Result(await _configurationService.GetRelatedData());
    }
}

Now, the question here is, how can I wrap my response into a ResponseBase with a help of ResultFilterAttribute without explicitly calling the Result method in the ControllerBase?

I've tried to use a ResultFilter to wrap my response but I couldn't find any sample to do this. I've also read this solution but didn't help.

I appreciate any help.


Solution

    1. Implement ResultFilter.

    In short,

    1.1. Get the values of context.Result such as StatusCode, ContentType, Value.

    1.2. Bind the Value to the root class (ResponseBase).

    1.3. Lastly, produce a new Response.

    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        Dictionary<string, string> headers = null;
        int statusCode = (int)HttpStatusCode.OK;
        string contentType = "application/json";
    
        var responseBaseType = typeof(ResponseBase<>).MakeGenericType(typeof(object));
        dynamic? responseBase = Activator.CreateInstance(responseBaseType);
    
        var result = context.Result as ObjectResult;
        if (result?.Value == null)
        {
            await next();
            return;
        }
    
        if (result.StatusCode != null)
            statusCode = (int)result.StatusCode;
    
        if (result.ContentTypes != null
            && result.ContentTypes.Any())
            contentType = result.ContentTypes[0];
    
        if (statusCode == (int)HttpStatusCode.OK)
            responseBase.Data = result.Value;
    
        byte[] data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(responseBase));
    
        context.HttpContext.Response.StatusCode = statusCode;
        context.HttpContext.Response.ContentType = contentType;
    
        await context.HttpContext.Response.Body.WriteAsync(data.AsMemory(0, data.Length));
    }
    
    1. Modify the API method to return the value with IActionResult type.
    [HttpGet("getData")]
    public async Task<IActionResult> GetData()
    {
        return new ObjectResult(await _configurationService.GetRelatedData());
    }
    
    1. Register ResultFilter as global filter to controller in Program.cs.
    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<ResultFilter>();
    });
    

    Note: The ResultFilter isn't a complete solution, while implementing you should consider different scenarios of IActionResult such as BadRequestObjectResult etc.


    Reference

    Result Filter in ASP.NET CORE MVC