在ASP.NET Core中添加响应头到中间件

63

我想向我的ASP.NET Core WebApi添加一个处理时间中间件,就像这样:

public class ProcessingTimeMiddleware  
{
    private readonly RequestDelegate _next;

    public ProcessingTimeMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        var watch = new Stopwatch();
        watch.Start();

        await _next(context);

        context.Response.Headers.Add("X-Processing-Time-Milliseconds", new[] { watch.ElapsedMilliseconds.ToString() });
    }
}

但这样做会抛出一个异常,提示

 System.InvalidOperationException: Headers are readonly, reponse has already started.

我该如何向响应中添加头信息?

6个回答

88

不要紧,代码在这里

    public async Task Invoke(HttpContext context)
    {
        var watch = new Stopwatch();
        watch.Start();

        //To add Headers AFTER everything you need to do this
        context.Response.OnStarting(state => {
            var httpContext = (HttpContext)state;
            httpContext.Response.Headers.Add("X-Response-Time-Milliseconds", new[] { watch.ElapsedMilliseconds.ToString() });

            return Task.CompletedTask;
        }, context);

        await _next(context);
    }

1
我还要补充一点,即await _next(context)应该是最后一个语句(在context.Response.Headers.Add之后)。这样更符合语义。 - Roman Pokrovskij
有没有 .NET 4 的等效版本或解决方法?我们仍然有一些使用 Server 2003 的客户。 - apc
1
你在 OnStarting 上使用状态重载的原因是什么? - Mark Lopez
@MarkLopez 因为你需要将对象(state)转换为HttpContext。其他重载没有这个,但我知道你的意思,可以使用context变量代替。 - Gillardo
1
我的意思是,为什么不让闭包捕获上下文变量以避免需要进行转换呢?尽管后来我意识到,API中提供了状态,不需要开发人员使用Lambda。 - Mark Lopez
如果我已经知道头部的值,是否有使用回调的理由?因为我发现一个地方使用了OnStarting回调,但是在那个时候就已经知道了头部的值。 - eddyP23

17

在响应正文内容被写入后,就不能再设置响应头了。一旦您将请求传递给下一个中间件并且它写入了响应,那么该中间件就无法再次设置响应头。

但是,有一种使用回调方法的解决方案可用。

Microsoft.AspNetCore.Http.HttpResponse 定义了 OnStarting 方法,它 添加了在响应头发送到客户端之前调用的委托。 您可以将此方法视为在开始编写响应之前调用的回调方法。

public class ResponseTimeMiddleware
    {
        private const string RESPONSE_HEADER_RESPONSE_TIME = "X-Response-Time-ms";

        private readonly RequestDelegate _next;

        public ResponseTimeMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task InvokeAsync(HttpContext context)
        {
            var watch = new Stopwatch();
            watch.Start();

            context.Response.OnStarting(() => 
            {
                watch.Stop();
                var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
                context.Response.Headers[RESPONSE_HEADER_RESPONSE_TIME] =  responseTimeForCompleteRequest.ToString(); 
                return Task.CompletedTask;
            });

            // Call the next delegate/middleware in the pipeline
            return this._next(context);
        }
    }

14

或者你也可以直接在 Startup.cs 文件的 Configure 方法中添加一个中间件。

app.Use(next => async context =>
{
    var stopWatch = new Stopwatch();
    stopWatch.Start();

    context.Response.OnStarting(() =>
    {
        stopWatch.Stop();
        context.Response.Headers.Add("X-ResponseTime-Ms", stopWatch.ElapsedMilliseconds.ToString());
        return Task.CompletedTask;
    });

    await next(context);
});

app.UseMvc();

1
这将会给我带来“无法设置OnStarting,因为响应已经开始”这个错误信息。 - cederlof
1
@cederlof 你需要将这个中间件放在其他中间件之前。 - maxisam

5

在相关问题上,不直接回答你的问题,现在有一个名为Server-Timing规范的标准头,可以提供时长等度量。这应该使您能够使用。

Server-Timing: processingTime;dur=12ms

您可以在https://www.w3.org/TR/server-timing/查找规范。

4
使用OnStarting方法的重载:
public async Task Invoke(HttpContext context)
{
    var watch = new Stopwatch();

    context.Response.OnStarting(() =>
    {
        watch.Stop();

        context
              .Response
              .Headers
              .Add("X-Processing-Time-Milliseconds",
                        new[] { watch.ElapsedMilliseconds.ToString() });

        return Task.CompletedTask;
    });

    watch.Start();

    await _next(context); 
}

0
在你的示例中,当执行到 context.Response.Headers.Add(...) 语句时,头信息已经被发送。
你可以尝试:
public async Task Invoke(HttpContext context)
{
    var watch = new Stopwatch();
    context.Response.OnSendingHeaders(x =>
    {
        watch.Stop();
        context.Response.Headers.Add("X-Processing-Time-Milliseconds", new[] { watch.ElapsedMilliseconds.ToString() });
    }, null);

    watch.Start();
    await _next(context);
    watch.Stop();
}

8
OnSendingHeaders 是从哪个软件包中导入的? - Roman Pokrovskij

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接