Category Archives: Coding

调试ASP.NET Core 源代码

今天在Stackoverflow上面看到一个Routing相关的问题, 看起来和想的不一样, 于是想调试下代码看看里面的细节, 结果发现非常方便,所以分享下。

我最开始的想法是从Github上面下载源码,然后Attach to Process.

先关掉Just My Code这个开关。PS: 没需要不要关这个开关,会让调试变慢。

于是开始动手,本地dotnet new webapidotnet build xx.sln, dotnet xx.dll

从github下载源码,https://github.com/aspnet/Mvc.git,打开后切换至对应版本的release分支。

后面就直接attach 到 dotnet.exe

此时断点是不亮的,如提示,没有加载运行所需的PDB文件: 'dotnet.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\2.1.3\Microsoft.AspNetCore.dll'. Cannot find or open the PDB file., 我们可以去巨硬的symbol server下载, 在Visual Studio中,Debug->Windows->Modules,找到要调试的DLL, 右键Load Symbols即可。

这样就断点亮起来,就可以继续调试了。

后面发现更方便的办法就是直接在Module里面Load symbols,如果本地没有源码,VS会提示是否要下载源码,不过如果要完整调试的话,需要下载好多次的感觉,还是把代码仓库搬下来省事。

DotNet Framework 源码阅读记录

Dotnet Core最近发布了2.1版本,相信也更加趋于稳定了。 虽然我接触.NET是从3.5+开始,不过这次可以从1.0开始见证它的发展了。 最近闲的时候阅读了一部分源码,虽然之前也看过不少,不过没有记录。 现在有了博客,就记录一点。

主要看的是下面几个仓库里面的内容:

Void 是个结构体(Struct)

Public struct Void{}

AggregateException

AggregateException 是伴随着async/await引入的新的类型, 有个Flatten方法, 可以将内部异常展开(貌似是BFS算法). 在调用时,将内部异常保存在一个ReadOnlyCollection中,不可改变. –> See how stack trace generated

DBNull

DbNull.Value is an instance of DbNull type. ToString() => string.Empty
Inheriated from Iconvertible, all other convertation will throw exception. InvalidCastException

DateTime

使用大量的缓存
* Days per 100 years/400years
* 里面存储了dates to 1601/1899/1970/10000,因为对应了不同的纪元 -> https://en.wikipedia.org/wiki/Epoch_(reference_date)
* Ticks per ms/s/Minute/Hour/Day
* s_daysToMonth365/s_daysToMonth366

字典

Dictionay – hash with chaining
HashTable – hash using open addressing

Overflow检测

在数字一部分, 有一段很精妙的overflow检测方式, 在做算法题的时候会用到。

        public static int Abs(int value)
        {
            if (value < 0)
            {
                value = -value;
                if (value < 0)
                {
                    ThrowAbsOverflow();
                }
            }
            return value;
        }

以及几个有意思的常量, 对于自己设计框架有帮助。

        public const double NegativeInfinity = (double)-1.0 / (double)(0.0);
        public const double PositiveInfinity = (double)1.0 / (double)(0.0);
        public const double NaN = (double)0.0 / (double)0.0;

Span

Span<T> https://msdn.microsoft.com/en-us/magazine/mt814808.aspx

DebuggerTypeProxy

在设计框架里面的类型时, 可以使用DebuggerTypeProxy在debugger里面显示的内容。

命令行语法说明怎么读

在阅读Git帮助文档时,经常会看到长长的语法说明,看起来有点晕,于是查询了这类怎么读。

$ man man

有一部分内容可以参考:
The following conventions apply to the SYNOPSIS section and can be used as a guide in other sections.

bold text type exactly as shown.
italic text replace with appropriate argument.
[-abc] any or all arguments within [ ] are optional.
-a|-b options delimited by | cannot be used together.
argument … argument is repeatable.
[expression] … entire expression within [ ] is repeatable.

另外Git book里面的参数是<>扩起来的。

 

rethrow exception and keeps original StackTrace

Expceiton rethrow

最近在改一个项目的bug,反射调用一段代码出错,结果在Log里面看不到完整的错误. 原因是Log里面记录的是抛出的Exception,而不是内部Exception. 当我修改为抛出内部Exception时,发现记录下来Exception的StackTrace是在抛出Exception的地方,而不是引发Exception的地方, 即Exception的StackTrace被修改了.

为了重现这个问题,添加一个dll

    class Processor
    {
        public string Runner(DateTime dateTime)
        {
            if (dateTime.Year > 2058)
                return "Hell! It's about time!";

            throw new Exception("Hold on..");
        }
    }

调用代码

class PluginRunner
    {
        public void ExecuteCore()
        {
            Assembly assembly = Assembly.LoadFrom("PluginTest.dll");

            object obj = assembly.CreateInstance("PluginTest.Processor");
            MethodInfo m = obj.GetType().GetMethod("Runner");

            if (m != null)
            {
                object[] methodParams = new object[] { DateTime.Now };
                var result = m.Invoke(obj, methodParams);
            }
        }
    }
        static void Main(string[] args)
        {
            PluginRunner runner = new PluginRunner();
            try
            {
                runner.ExecuteCore();
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"exception stacktrace:" + Environment.NewLine + ex.StackTrace + Environment.NewLine);
                throw;
            }

        }

由于调用方的异常抛出,此时, 记录异常为m.Invoke的堆栈,结果如下:

exception stacktrace:
at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
at Canary.PluginRunner.ExecuteCore() in xxx\Canary\PluginRunner.cs:line 23
at Canary.Program.Main(String[] args) in xxx\Canary\Program.cs:line 19

随后,尝试抛出InnerException

                try
                {
                    var result = m.Invoke(obj, methodParams);
                }
                catch (Exception ex)
                {
                    if (ex.InnerException != null)
                    {
                        // ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
                        throw ex.InnerException;
                    }
                    throw ex;
                }

得到的是Invoke调用方的异常,结果如下:

exception stacktrace:
   at Canary.PluginRunner.ExecuteCore() in xxx\Canary\PluginRunner.cs:line 33
   at Canary.Program.Main(String[] args) in xxx\Canary\Program.cs:line 19

但是在断点出查看时at PluginTest.Processor.Runner(DateTime dateTime),异常在抛出后被修改了.

解决办法:
第一种是使用.NET 4.5中引入的ExceptionDispatchInfo,如官方文档所说,这种方式会保留原始堆栈.这个class的引入应该初衷是为了配合Task调用的AggregateException.
The ExceptionDispatchInfo object stores the stack trace information and Watson information that the exception contains at the point where it is captured. The exception can be thrown at another time and possibly on another thread by calling the ExceptionDispatchInfo.Throw method. The exception is thrown as if it had flowed from the point where it was captured to the point where the Throw method is called.
代码如下:

ExceptionDispatchInfo.Capture(ex.InnerException).Throw();

此时异常如下,原始异常被追加上去了:

exception stacktrace:
   at PluginTest.Processor.Runner(DateTime dateTime)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Canary.PluginRunner.ExecuteCore() in xxx\Canary\PluginRunner.cs:line 32
   at Canary.Program.Main(String[] args) in xxx\Canary\Program.cs:line 19

SO大佬在2010年指出这是一个Windows CLR的限制:
This is a well known limitation in the Windows version of the CLR. It uses Windows' built-in support for exception handling (SEH). Problem is, it is stack frame based and a method has only one stack frame. You can easily solve the problem by moving the inner try/catch block into another helper method, thus creating another stack frame. Another consequence of this limitation is that the JIT compiler won't inline any method that contains a try statement.

Edit: 在复习CLR Via C#一书时,书中也提到了Windows系统的这一限制。更专业的说法是Windows重置了异常堆栈的起点, 直接throw不影响CLR对异常起点的认知。

那么Linux下呢, 我在WSL下看了看是确实没有这个问题.

➜  CanaryNetCore git:(master) ✗ dotnet run

Unhandled Exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Exception: Hold on..
   at PluginTestCore.Processor.Runner(DateTime dateTime)
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at CanaryNetCore.PluginRunner.ExecuteCore() in /mnt/c/DevLab/Canary/CanaryNetCore/PluginRunner.cs:line 33
   at CanaryNetCore.Program.Main(String[] args) in /mnt/c/DevLab/Canary/CanaryNetCore/Program.cs:line 18

➜  CanaryNetCore git:(master) ✗ cat cat PluginRunner.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading.Tasks;

namespace CanaryNetCore
{
    class PluginRunner
    {
        public void ExecuteCore()
        {
            Assembly assembly = Assembly.LoadFrom("PluginTestCore.dll");

            object obj = assembly.CreateInstance("PluginTestCore.Processor");
            MethodInfo m = obj.GetType().GetMethod("Runner");

            if (m != null)
            {
                object[] methodParams = new object[] { DateTime.Now };
                try
                {
                    var result = m.Invoke(obj, methodParams);
                }
                catch (Exception ex)
                {
                    //if (ex.InnerException != null)
                    //{
                    //    ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
                    //}
                    throw;
                }

            }
        }
    }
}

第二种方法是直接throw,在外面处理InnerException, 由于我不想修改外部代码, 所以使用了第一种方法.
The recommended way to re-throw an exception is to simply use the throw statement in C# and the Throw statement in Visual Basic without including an expression. This ensures that all call stack information is preserved when the exception is propagated to the caller.

参考:
1. https://stackoverflow.com/questions/57383/in-c-how-can-i-rethrow-innerexception-without-losing-stack-trace
2. https://msdn.microsoft.com/en-us/library/system.runtime.exceptionservices.exceptiondispatchinfo%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396
3. https://msdn.microsoft.com/en-us/library/system.exception(v=vs.110).aspx

记一次生产环境事故

新的项目采用了ASPNETCORE,其中依赖注入使用了AutoFac,通过JSON配置文件实现注入. 在配置生存周期的时候,将部分类型设置为SingleInstance. 其中有个发邮件的类,代码大概是下面的.

Class EmailService{
..
ctor(){
mailMessage = new MailMessage();
}

public void SendAsync(string To,...){
mailMessage.ToList.Add(To);
}
..
}

单例的对象里面的mailMessage.ToList在并发的情况下,在controller注入实例化后下次实例化之前,可能被多次添加收件人, 出现了一些用户收到多封的情况.

一些教训:
1. 不要写和修改自己不理解的代码
2. 代码提示很重要, 用代码配置可能比写配置文件更清晰,避免犯错
3. CodeReview很重要
4. 测试很重要, 要加入UT
5. Log要写, 便于评估影响
6. 多看多学