.NET Core MVC中Telemetry Initializer读取请求体遇已释放对象异常
解决.NET Core MVC中Application Insights Telemetry Initializer读取请求体时的ObjectDisposedException问题
这个问题我之前在项目里碰到过,核心原因是**.NET Core和.NET Framework在请求流处理上的差异**,再加上Telemetry Initializer的执行时机特性导致的:
- .NET Core为了优化性能,请求体流默认是单次读取后即被释放,而.NET Framework的Web API中请求流默认支持多次读取;
- RequestTelemetry的初始化器默认是在**请求结束后(即Action执行完成)**才会被调用,这时候Action已经读取过请求体流,流已经被释放,自然会抛出已释放对象的异常。
下面给你两种可靠的解决思路:
方法一:提前缓存请求体(推荐)
通过自定义中间件在请求管道的早期阶段启用请求体重放并缓存内容,这样后续无论是Action还是Telemetry Initializer都能读取到内容,而且不会触发流已释放的问题。
步骤1:创建请求体缓存中间件
因为你用的是.NET Core 2.0,要使用EnableRewind()来启用流重放:
public class RequestBodyCacheMiddleware { private readonly RequestDelegate _next; public RequestBodyCacheMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { // 启用请求流重放,允许多次读取 context.Request.EnableRewind(); // 读取请求体内容并缓存到HttpContext.Items中 if (context.Request.Body.CanRead && !string.IsNullOrEmpty(context.Request.ContentType) && context.Request.ContentType.Contains("application/json")) // 可根据需求过滤请求类型 { using var streamReader = new StreamReader(context.Request.Body, leaveOpen: true); var bodyContent = await streamReader.ReadToEndAsync(); // 重置流指针到开头,保证后续Action能正常读取 context.Request.Body.Position = 0; // 将内容存入HttpContext,供Telemetry Initializer使用 context.Items["CachedRequestBody"] = bodyContent; } await _next(context); } } // 扩展方法方便注册中间件 public static class RequestBodyCacheMiddlewareExtensions { public static IApplicationBuilder UseRequestBodyCache(this IApplicationBuilder app) { return app.UseMiddleware<RequestBodyCacheMiddleware>(); } }
步骤2:注册中间件到请求管道
在Startup.cs的Configure方法中,将这个中间件放在路由中间件之前(确保在Action执行前完成缓存):
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ...其他中间件(比如异常处理、静态文件等) // 注册请求体缓存中间件,一定要放在UseRouting之前 app.UseRequestBodyCache(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); }
步骤3:修改Telemetry Initializer读取缓存内容
不再直接读取请求流,而是从HttpContext.Items中取已缓存的内容:
public class RequestBodyTelemetryInitializer : ITelemetryInitializer { private readonly IHttpContextAccessor _httpContextAccessor; public RequestBodyTelemetryInitializer(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public void Initialize(ITelemetry telemetry) { if (!(telemetry is RequestTelemetry requestTelemetry)) return; var httpContext = _httpContextAccessor.HttpContext; if (httpContext?.Items.TryGetValue("CachedRequestBody", out var cachedBody) == true && cachedBody is string bodyContent) { // 将请求体内容添加到遥测属性中,注意避免添加过大内容影响性能 requestTelemetry.Properties.Add("RequestBody", bodyContent); } } }
步骤4:确保注册必要服务
在Startup.cs的ConfigureServices中,注册Telemetry Initializer和IHttpContextAccessor:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); // 注册HttpContextAccessor services.AddHttpContextAccessor(); // 注册自定义Telemetry Initializer services.AddSingleton<ITelemetryInitializer, RequestBodyTelemetryInitializer>(); // 注册Application Insights services.AddApplicationInsightsTelemetry(); }
方法二:理解Telemetry Initializer的执行时机(为什么不能提前执行)
其实Telemetry Initializer的设计初衷是在遥测数据即将被发送前进行修改,而RequestTelemetry是在整个请求生命周期结束后才会被构造和初始化,所以它的执行时机必然是在Action之后。因此没有办法直接让它在Action前执行,方法一是最可靠的解决方案。
注意事项
- 不要缓存过大的请求体(比如上传文件的请求),可以在中间件中通过
context.Request.ContentLength判断大小,只缓存小内容的请求; - 针对敏感数据(比如密码、令牌),一定要在缓存前进行脱敏处理,避免泄露到Application Insights中。
内容的提问来源于stack exchange,提问作者Jithesh Balakrishnan
相关产品推荐
相关产品推荐

