34

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.

Reza Ahmadi
  • 862
  • 2
  • 11
  • 23
  • 1
    beside this your async handling is wrong. Using .Result is dangerous, you should avoid that if possilbe. Rather override OnActionExecutingAsync method that allows you awaiting. – Lukas K Aug 09 '16 at 10:54
  • @LukasK actually this was written just to explain my question. but you'r right. – Reza Ahmadi Jan 21 '17 at 07:25

4 Answers4

37

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
            }
        }
    }
Stoyan Dimov
  • 5,250
  • 2
  • 28
  • 44
Khanh TO
  • 48,509
  • 13
  • 99
  • 115
  • Thanks bro. What should I do if I have multi parameters and I don't know their name? In this solution I should set a contract that 'always my parameter name is myModel'? yes? – Reza Ahmadi Jan 25 '14 at 14:37
  • @Reza Ahmadi: `ActionArguments` is just a dictionary. You can loop though it. – Khanh TO Jan 25 '14 at 14:41
  • @Reza Ahmadi: usually, when you want to apply this kind of filter for some specific action against a class of similar objects. You could have your models implement an interface, when looping though the dictionary, you have a clue to indicate this is the model you need to work on. (maybe call methods though the interface) – Khanh TO Jan 25 '14 at 14:43
17

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

Community
  • 1
  • 1
Anytoe
  • 1,605
  • 1
  • 20
  • 26
  • @darasd One alternative would be to extract the request objects from `actionContext.ActionArguments` since arguments will hold everything passed into the request. Check out this link for more information. (https://github.com/aspnet/Mvc/issues/5260#issuecomment-245947206) – Eliyah Oct 18 '19 at 22:27
  • I use Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll and do not have a Popertiy "MS_HttpContext". – yBother May 07 '20 at 11:19
2
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());
0

This worked for me:

public override async Task OnActionExecutedAsync(HttpActionExecutedContext context, CancellationToken cancellationToken) {

                var requestLog = context.Request;
                if (requestLog != null)
                {
                    _logger.DebugFormat("Request: {0}", requestLog?.ToString());
                    var requestBody = context.ActionContext?.ActionArguments;
                    if (requestBody != null)
                    {
                        _logger.DebugFormat("Body: {0}", JsonConvert.SerializeObject(requestBody));
                    }
                }                   
    }