Read Asp.Net Core Response body in ActionFilterAttribute
Asked Answered
S

5

16

I'm using ASP.NET Core as a REST API Service. I need access to request and response in ActionFilter. Actually, I found the request in OnActionExcecuted but I can't read the response result.

I'm trying to return value as follow:

[HttpGet]
[ProducesResponseType(typeof(ResponseType), (int)HttpStatusCode.OK)]
[Route("[action]")]
public async Task<IActionResult> Get(CancellationToken cancellationToken)
{
    var model = await _responseServices.Get(cancellationToken);
    return Ok(model);
}

And in ActionFilter OnExcecuted method as follow:

_request = context.HttpContext.Request.ReadAsString().Result;
_response = context.HttpContext.Response.ReadAsString().Result; //?

I'm trying to get the response in ReadAsString as an Extension method as follow:

public static async Task<string> ReadAsString(this HttpResponse response)
{
     var initialBody = response.Body;
     var buffer = new byte[Convert.ToInt32(response.ContentLength)];
     await response.Body.ReadAsync(buffer, 0, buffer.Length);
     var body = Encoding.UTF8.GetString(buffer);
     response.Body = initialBody;
     return body;
 }

But, there is no result!

How I can get the response in OnActionExcecuted?

Spiegelman answered 5/12, 2018 at 6:37 Comment(9)
You can get idea from #10592151Disposal
Thanks, I'm trying a lot of ways but all of the themes have no result of the response.Spiegelman
Why do you want to read the Response ? Although it's possible make it by reading the response, maybe there's much better way to achieve your goals if you tell us your intention.Fascinator
This might helps you #45500208Pleopod
I need to know which data provide to the client and log all of the results as a JSON @FascinatorSpiegelman
@SaeidMirzaei are those actions returning JsonResult ?Fascinator
@Fascinator I'm providing JSON result as default in Global Controller with [Produces("application/json")], So It's not my case. In Asp.Net Framework we can read response with actionExecutedContext.Response.Content.ReadAsStringAsync().Result; But Now, there is not Response.Content.Spiegelman
@SaeidMirzaei If you would like to read the response body, you could refer my answer here here. But I would not suggest that approach. The context.Result could be much more elegant.Fascinator
@Fascinator Thanks, for taking the time to try and help.Spiegelman
F
15

If you're logging for JSON result/ view result , you don't need to read the whole response stream. Simply serialize the context.Result:

public class MyFilterAttribute : ActionFilterAttribute
{
    private ILogger<MyFilterAttribute> logger;

    public MyFilterAttribute(ILogger<MyFilterAttribute> logger){
        this.logger = logger;
    }
    public override void OnActionExecuted(ActionExecutedContext context)
    {
        var result = context.Result;
        if (result is JsonResult json)
        {
            var x = json.Value;
            var status = json.StatusCode;
            this.logger.LogInformation(JsonConvert.SerializeObject(x));
        }
        if(result is ViewResult view){
            // I think it's better to log ViewData instead of the finally rendered template string
            var status = view.StatusCode;
            var x = view.ViewData;
            var name = view.ViewName;
            this.logger.LogInformation(JsonConvert.SerializeObject(x));
        }
        else{
            this.logger.LogInformation("...");
        }
    }
Fascinator answered 5/12, 2018 at 9:2 Comment(2)
This answer is significantly more efficient (code-wise anyway) than other common solutions I've seen that seem to universally recommend replacing and tracking the Response.Body's memory stream! Thanks!Ervin
It appears context.Result is always null in the filterAppealing
A
6

I know there is already an answer but I want to also add that the problem is the MVC pipeline has not populated the Response.Body when running an ActionFilter so you cannot access it. The Response.Body is populated by the MVC middleware.

If you want to read Response.Body then you need to create your own custom middleware to intercept the call when the Response object has been populated. There are numerous websites that can show you how to do this. One example is here.

As discussed in the other answer, if you want to do it in an ActionFilter you can use the context.Result to access the information.

Assign answered 5/12, 2018 at 9:48 Comment(0)
V
5

For logging whole request and response in the ASP.NET Core filter pipeline you can use Result filter attribute

    public class LogRequestResponseAttribute : TypeFilterAttribute
    {
        public LogRequestResponseAttribute() : base(typeof(LogRequestResponseImplementation)) { }

        private class LogRequestResponseImplementation : IAsyncResultFilter
        {
            public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
            {
                var requestHeadersText = CommonLoggingTools.SerializeHeaders(context.HttpContext.Request.Headers);
                Log.Information("requestHeaders: " + requestHeadersText);

                var requestBodyText = await CommonLoggingTools.FormatRequestBody(context.HttpContext.Request);
                Log.Information("requestBody: " + requestBodyText);

                await next();

                var responseHeadersText = CommonLoggingTools.SerializeHeaders(context.HttpContext.Response.Headers);
                Log.Information("responseHeaders: " + responseHeadersText);

                var responseBodyText = await CommonLoggingTools.FormatResponseBody(context.HttpContext.Response);
                Log.Information("responseBody: " + responseBodyText);
            }
        }
    }

In Startup.cs add

    app.UseMiddleware<ResponseRewindMiddleware>();

    services.AddScoped<LogRequestResponseAttribute>();

Somewhere add static class

    public static class CommonLoggingTools
    {
        public static async Task<string> FormatRequestBody(HttpRequest request)
        {
            //This line allows us to set the reader for the request back at the beginning of its stream.
            request.EnableRewind();

            //We now need to read the request stream.  First, we create a new byte[] with the same length as the request stream...
            var buffer = new byte[Convert.ToInt32(request.ContentLength)];

            //...Then we copy the entire request stream into the new buffer.
            await request.Body.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);

            //We convert the byte[] into a string using UTF8 encoding...
            var bodyAsText = Encoding.UTF8.GetString(buffer);

            //..and finally, assign the read body back to the request body, which is allowed because of EnableRewind()
            request.Body.Position = 0;

            return $"{request.Scheme} {request.Host}{request.Path} {request.QueryString} {bodyAsText}";
        }

        public static async Task<string> FormatResponseBody(HttpResponse response)
        {
            //We need to read the response stream from the beginning...
            response.Body.Seek(0, SeekOrigin.Begin);

            //...and copy it into a string
            string text = await new StreamReader(response.Body).ReadToEndAsync();

            //We need to reset the reader for the response so that the client can read it.
            response.Body.Seek(0, SeekOrigin.Begin);

            response.Body.Position = 0;

            //Return the string for the response, including the status code (e.g. 200, 404, 401, etc.)
            return $"{response.StatusCode}: {text}";
        }

        public static string SerializeHeaders(IHeaderDictionary headers)
        {
            var dict = new Dictionary<string, string>();

            foreach (var item in headers.ToList())
            {
                //if (item.Value != null)
                //{
                var header = string.Empty;
                foreach (var value in item.Value)
                {
                    header += value + " ";
                }

                // Trim the trailing space and add item to the dictionary
                header = header.TrimEnd(" ".ToCharArray());
                dict.Add(item.Key, header);
                //}
            }

            return JsonConvert.SerializeObject(dict, Formatting.Indented);
        }
    }

    public class ResponseRewindMiddleware {
        private readonly RequestDelegate next;

        public ResponseRewindMiddleware(RequestDelegate next) {
            this.next = next;
        }

        public async Task Invoke(HttpContext context) {

            Stream originalBody = context.Response.Body;

            try {
                using (var memStream = new MemoryStream()) {
                    context.Response.Body = memStream;

                    await next(context);

                    //memStream.Position = 0;
                    //string responseBody = new StreamReader(memStream).ReadToEnd();

                    memStream.Position = 0;
                    await memStream.CopyToAsync(originalBody);
                }

            } finally {
                context.Response.Body = originalBody;
            }

        } 

Vaginal answered 30/4, 2019 at 13:0 Comment(0)
C
1

You can also do:

string response = "Hello";
if (result is ObjectResult objectResult)
{
    var status = objectResult.StatusCode;
    var value = objectResult.Value;
    var stringResult = objectResult.ToString();
    responce = (JsonConvert.SerializeObject(value));
}

I used this in a .NET Core app.

Contrail answered 9/4, 2019 at 10:19 Comment(0)
D
0

Regarding @itminus answer, I was always getting null in the context.Response property. What I did was changing the interface that my filter implements from IAsyncActionFilter to IAsyncResultFilter.

Print of the context.Result inside the 'Watch' tab

Distinguishing answered 29/7, 2024 at 17:54 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.