Web Api Request Content is empty in action filter

I have an attribute named Log that tries to log the content of request and response into a text file. I've put that over my Controller to cover all the actions. In LogAttribute I'm reading content as a string (ReadAsStringAsync) so I don't lose request body.

public class LogAttribute : ActionFilterAttribute
{
    // ..
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        // stuff goes here
        var content = actionContext.Request.Content.ReadAsStringAsync().Result; 
        // content is always empty because request body is cleared
    }

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        // other stuff goes here
        var content = actionContext.Request.Content.ReadAsStringAsync().Result;
        // content is always empty because request body is cleared
    }

    // ..
}

On the other hand, I've put the FromBody attribute before my action parameter class to take advantage of its benefits.

[Log]
public class SomethingController
{
    public HttpResponseMessage Foo([FromBody] myModel)
    {
        // something
    }
}

The problem is the content is always empty either in ActionExecuting or ActionExecuted.

I think this is because FromBody runs before my Log attribute unlike their order in the code. And again I think its because of finding the best action/controller match for the request according to action parameters (Route Processing). After that my request body is cleared since request body is non-buffered in WebApi.

I want to know if there is any way to change the run time order of the FromBody attribute and my Log attribute? or something else that solves the problem! I should mention that I don't want to remove the FromBody and using HttpRequestMessage instead of my Model or something like that.


Solution 1:

The request body is a non-rewindable stream; it can be read only once. The formatter has already read the stream and populated the model. We're not able to read the stream again in the action filter.

You could try:

public class LogAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var myModel = actionContext.ActionArguments["myModel"]; 

    }

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        var myModel = actionContext.ActionArguments["myModel"]; 
    }
}

Actually, ActionArguments is just a dictionary, we can loop though it if we need to avoid hardcoded parameter name ("myModel"). When we create a generic action filter that needs to work on a class of similar objects for some specific requirements, we could have our models implement an interface => know which argument is the model we need to work on and we can call the methods though the interface.

Example code:

public class LogAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            foreach(var argument in actionContext.ActionArguments.Values.Where(v => v is ILogable)))
            {
                 ILogable model = argument as ILogable;//assume that only objects implementing this interface are logable
                 //do something with it. Maybe call model.log
            }
        }

        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            foreach(var argument in actionContext.ActionArguments.Values.Where(v => v is ILogable)))
            {
                 ILogable model = argument as ILogable;//assume that only objects implementing this interface are logable
                 //do something with it. Maybe call model.log
            }
        }
    }

Solution 2:

This approach worked for me:

using (var stream = new MemoryStream())
{
    var context = (HttpContextBase)Request.Properties["MS_HttpContext"];
    context.Request.InputStream.Seek(0, SeekOrigin.Begin);
    context.Request.InputStream.CopyTo(stream);
    string requestBody = Encoding.UTF8.GetString(stream.ToArray());
}

Returned for me the json representation of my action parameter object triggering the logging or exception case.

Found as accepted answer here

Solution 3:

public class ContentInterceptorHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Content != null)
        {
            var requestBody = await request.Content.ReadAsStringAsync();
            request.Properties["Content"] = requestBody;
            request.Content = new StringContent(requestBody, Encoding.UTF8, request.Content.Headers.ContentType.MediaType);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

public class LogRequestAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.Request.Properties.TryGetValue("Content", out var body))
            return;

        Console.WriteLine(body);
    }
}

and add in Startup

httpConfiguration.MessageHandlers.Add(new ContentInterceptorHandler());