一:背景
1. 讲故事
前几天群里有一位敌人聊到,为什么我在 Action 中执行一句 Response.Write
之后,后续的 View 就不出现了,如果脑子中没有画面,那就上测试代码:
public class HomeController : Controller
{public IActionResult Index()
{Response.WriteAsync("hello world!");
return View();}
}
后果还是挺有意思的,大家都晓得,默认状况下会渲染 /Home/Index
对应的 view 页面,但这里被 Response.WriteAsync
插了一杠子,气的 view 都渲染不进去了,那接下来就来找一找 view 为啥这么怄气?
二:寻找假相
1. 从 Logger 动手
置信很多人都在用 aspnetcore 中的 logger 记录日志,为什么要首选这个 logger 呢?因为它在 web 框架 中是一等公民的存在,毕竟底层源码各处都嵌入着这玩意哈, 轻易找点代码:
internal abstract class ActionMethodExecutor
{private Task ResultNext<TFilter, TFilterAsync>(ref ResourceInvoker.State next, ref ResourceInvoker.Scope scope, [Nullable(2)] ref object state, ref bool isCompleted) where TFilter : class, IResultFilter where TFilterAsync : class, IAsyncResultFilter
{
ResourceInvoker.ResultExecutingContextSealed resultExecutingContext3 = this._resultExecutingContext;
this._diagnosticListener.BeforeOnResultExecuting(resultExecutingContext3, tfilter);
this._logger.BeforeExecutingMethodOnFilter(filterType, "OnResultExecuting", tfilter);
tfilter.OnResultExecuting(resultExecutingContext3);
this._diagnosticListener.AfterOnResultExecuting(resultExecutingContext3, tfilter);
this._logger.AfterExecutingMethodOnFilter(filterType, "OnResultExecuting", tfilter);
if (this._resultExecutingContext.Cancel)
{this._logger.ResultFilterShortCircuited(tfilter);
this._resultExecutedContext = new ResourceInvoker.ResultExecutedContextSealed(resultExecutingContext3, this._filters, resultExecutingContext3.Result, this._instance)
{Canceled = true};
goto IL_39E;
}
}
}
而且大家想想,这种写法特地奇葩,我想底层框架中的 logger 定会有所反馈,接下来在启动程序的时候采纳 WebApplication1
的模式启动,如下图:
启动后,在管制台上能够看到一堆报错信息:
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: E:\net5\WebApplication1\WebApplication1
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: Headers are read-only, response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)
at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_ContentType(String value)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ActionContext actionContext, IView view, ViewDataDictionary viewData, ITempDataDictionary tempData, String contentType, Nullable`1 statusCode)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor.ExecuteAsync(ActionContext context, ViewResult result)
at Microsoft.AspNetCore.Mvc.ViewResult.ExecuteResultAsync(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultAsync>g__Logged|21_0(ResourceInvoker invoker, IActionResult result)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
异样信息非常明显:Headers are read-only, response has already started
,大略就是说,header 是只读的,response 已是启动状态了,从调用堆栈的 ViewExecutor.ExecuteAsync
处可看出,代码筹备渲染 view,在 set_ContentType 处遭逢异样,完结了后续渲染流程。
接下来一起看下,为什么会触发这个异样???
三:调试源码寻找异样的起因
1. dnspy 调试
除了从异样堆栈中找到最早的异样代码处,这里还说一个小技巧,应用 ndspy 的 异样断点性能,在异样设置面板 定位 InvalidOperationException
异样即可。
接下来就能够让程序跑起来,当异样抛出时会主动断下来。
认真看一下图中的文字标注,还是很好了解的,接下来持续追一下:response.ContentType = contentType2;
外部都做了什么。
public override string ContentType
{
get
{return this.Headers[HeaderNames.ContentType];
}
set
{if (string.IsNullOrEmpty(value))
{this.HttpResponseFeature.Headers.Remove(HeaderNames.ContentType);
return;
}
this.HttpResponseFeature.Headers[HeaderNames.ContentType] = value;
}
}
能够看到 外部是给 this.HttpResponseFeature.Headers
赋值的,持续往下追:
从图中能够看到,最初的 HttpHeader._isReadOnly =true
导致异样的产生,罪魁祸首哈, 接下来钻研下这句 HttpHeader._isReadOnly=true
是何时被赋值的。
2. _isReadOnly=true 何时产生
这个问题就简略多了,必然是 Response.WriteAsync("hello world!");
造成了 _isReadOnly=true,在 HttpHeader 下有一个 SetReadOnly 办法用于对 _isReadOnly 字段的封装,代码如下:
internal abstract class HttpHeaders
{public void SetReadOnly()
{this._isReadOnly = true;}
}
接下来在该办法处下一个断点,持续调试,如下图:
从图中可看到,原来 Response.WriteAsync("hello world!")
是能够封闭 HttpHeaders 的,后续任何再对 HttpHeader 的操作都是有效的。。。
其实大家也能够想一想, 不同的 response,必定会有不同的 header,要想叠加的话这辈子都不可能的,只能让前面的报错,如下:
1. response:
HTTP/1.1 200 OK
Date: Mon, 19 Oct 2020 14:37:54 GMT
Server: Kestrel
Transfer-Encoding: chunked
c
hello world!
2. view:
HTTP/1.1 200 OK
Date: Mon, 19 Oct 2020 14:39:01 GMT
Content-Type: text/html; charset=utf-8
Server: Kestrel
Content-Length: 2239
四:总结
这篇就是对群聊天过程中抛出问题的集体探索,一家之言,不过挺有意思,大家也能够多用用调试工具寻找问题,证实问题,纸上得来终觉浅, 绝知此事要躬行,好了,心愿本篇对您有帮忙!
更多高质量干货:参见我的 GitHub: dotnetfly