Is there any way to get request body in .NET Core FilterAttribute?

28,319

Solution 1

Accordingly to this "Best way to log/read request body in a middleware" thread, the following should work:

// using Microsoft.AspNetCore.Http.Internal;

public class SampleActionFilterAttribute : TypeFilterAttribute
{
    ... 

    public void OnActionExecuting(ActionExecutedContext context)
    {
        // read body before MVC action execution
        string bodyData = ReadBodyAsString(context.HttpContext.Request);
    }

    private string ReadBodyAsString(HttpRequest request)
    {
        var initialBody = request.Body; // Workaround

        try
        {
            request.EnableRewind();

            using (StreamReader reader = new StreamReader(request.Body))
            {
                string text = reader.ReadToEnd();
                return text;
            }
        }
        finally
        {
            // Workaround so MVC action will be able to read body as well
            request.Body = initialBody; 
        }

        return string.Empty;
    }
 }

Also similar approach described in Read request body twice SO post


Update: above approach in ReadBodyAsString with will work if used in middleware, not in action filter. The difference is that when action filter is calling (even for OnActionExecuting), the body stream already has been read and [FromBody] model has been populated.

The good nesw is that so it is possible to get model directly in action filter by using context.ActionArguments["<model_name>"]. In your case:

public void OnActionExecuted(ActionExecutedContext context)
{
   var model = context.ActionArguments["model"] as NoteModel;
}

Solution 2

Following snippet worked for me, to log request only if there is any exception.(.Net Core 3.1)

{

public class ExceptionFilter : IActionFilter
{
    private ConcurrentDictionary<string, string> requests = new ConcurrentDictionary<string, string>();

    public void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Exception != null)

        {
            StringBuilder parameters = new StringBuilder();

            _logger.LogError("Error while executing action:" + context.ActionDescriptor.DisplayName);

            string errRequest;
            if(requests.TryGetValue(context.HttpContext.TraceIdentifier,out errRequest))
            {
                _logger.LogError(errRequest);
            }

            _logger.LogError(context.Exception);

            context.Result = new ObjectResult("Error!!!")
            {
                StatusCode = 500,
            };
            context.ExceptionHandled = true;
        }

        string req;
            requests.Remove(context.HttpContext.TraceIdentifier, out req);


    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        StringBuilder sb = new StringBuilder();
        foreach (var arg in context.ActionArguments)
        {

            sb.Append(arg.Key.ToString() + ":" + Newtonsoft.Json.JsonConvert.SerializeObject(arg.Value) + "\n");

        }
        requests.TryAdd(context.HttpContext.TraceIdentifier, sb.ToString());
    }


}

}

Solution 3

    in .net core 3.1, I usually use this approach to handle this scenario
    
    1- Create Async Filter Attribute
    
    public class MyFilter : IAsyncAuthorizationFilter
        {
            private string _errorMessage = UserAccessErrorMessages.NO_ACCESS;
            public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
            {
                string requestBody = await ReadBodyAsString(context.HttpContext.Request);
                var reportFiltration = JsonConvert.DeserializeObject<YourModel>(requestBody);
                var _myService = (IMyService)context.HttpContext.RequestServices.GetService(typeof(IMyService));
                    if (!hasAccess)
                    {
                        context.Result = new UnauthorizedObjectResult(_errorMessage);
                    }
            }
    
            private async Task<string> ReadBodyAsString(Microsoft.AspNetCore.Http.HttpRequest request)
            {
                var initialBody = request.Body; // Workaround
    
                try
                {
                    //request.EnableRewind();
    
                    using (StreamReader reader = new StreamReader(request.Body))
                    {
                        string text = await reader.ReadToEndAsync();
                        return text;
                    }
                }
                finally
                {
                    // Workaround so MVC action will be able to read body as well
                    request.Body = initialBody;
                }
            }
        }
2- Create Your Custom Attribute
public class MyAttribute : TypeFilterAttribute, IAllowAnonymous
    {
        public MyAttribute () : base(typeof(MyFilter))
        {

        }
    }

3- use your filter

    [HttpPost]
    [ActionName("MyAction")]
    [MyAttribute]
    public async Task<IActionResult> PostData([FromBody]MyModel model)
Share:
28,319

Related videos on Youtube

wtf512
Author by

wtf512

Updated on February 08, 2021

Comments

  • wtf512
    wtf512 about 3 years

    Sample of my request

    http://localhost:8065/api/note
    POST
    content-type:application/json
    request body: { "id" : "1234", "title" : "test", "status" : "draft"}
    

    and the response should be

    { "msg" : "ok", "code" : 1}
    

    The action

    public async Task<IActionResult> Post([FromBody]NoteModel model)
    

    In order to have every request logged automatically, I create an attribute to do this job. The attribute looks like: (from Microsoft Docs)

    public class SampleActionFilterAttribute : TypeFilterAttribute
    {
        public SampleActionFilterAttribute():base(typeof(SampleActionFilterImpl))
        {
        }
    
        private class SampleActionFilterImpl : IActionFilter
        {
            private readonly ILogger _logger;
            public SampleActionFilterImpl(ILoggerFactory loggerFactory)
            {
                _logger = loggerFactory.CreateLogger<SampleActionFilterAttribute>();
            }
    
            public void OnActionExecuting(ActionExecutingContext context)
            {
    
            }
    
            public void OnActionExecuted(ActionExecutedContext context)
            {
                _logger.LogDebug("[path]" + context.HttpContext.Request.Path);
                _logger.LogDebug("[method]" + context.HttpContext.Request.Method);
                _logger.LogDebug("[body]"); //log request body, expectation: { "id" : "1234", "title" : "test", "status" : "draft"}
                _logger.LogDebug("[statuscode]" + context.HttpContext.Response.StatusCode);
                _logger.LogDebug("[response]"); //log response
            }
        }
    }
    

    I try to use streamReader to get request body only get empty string.

    StreamReader reader = new StreamReader(context.HttpContext.Request.Body);
    string text = reader.ReadToEnd();
    

    Is that because the body was read by [fromBody] from controller so the stream can not be read twice? If so, how am I supposed to get request body and response in OnActionExecuted method?


    Update:

    I've just copied Set's code into my project, not working. Here is the debug gifenter image description here

  • wtf512
    wtf512 over 6 years
    Just tried this sulution. The ReadBodyAsString method in OnActionExecuting still get noting, see the gif I pasted. (The [fromBody] model has value).
  • Set
    Set over 6 years
    @wtf512 updated answer. You may use context.ActionArguments directly to get model in action filter, or if you want to do manipulations with raw request body, move to middleware approach for logging.
  • wtf512
    wtf512 over 6 years
    The context.ActionArguments solved my problem. I just loop every item in ActionArgumetns and use newtonsoft.json to serialize as string and log.