2020年10月12日 星期一

使用 LoggerFactory 觀察 EF Core 送出的 SQL Statement

使用 LoggerFactory 觀察 EF Core 送出的 SQL Statement

根據 上一篇 EF Core 討論文章 使用 DbContextOptionsBuilder 來指定連線字串與觀察 EF Core 產生的 SQL 指令 ,透過 SQL Server Profiler 來觀察使用 EF Core 的程式,究竟產生了甚麼 SQL 指令到 SQL Server 上,現在,使用另外一種方式來觀察究竟執行了甚麼 SQL指令,在這裡需要使用 LoggerFactory 這個類別來做到這件事情。根據官方文件上提到的,這個類別的目的是 : 根據指定的提供者,產生 ILogger 類別的執行個體。

根據這份文件內容 .NET Core 與 ASP.NET Core 中的記錄 可以得知, [ILogger] 是用於提供支援記錄 API。

請按照底下的步驟來進行操作

建立練習專案

  • 打開 Visual Studio 2019
  • 點選 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗內,選擇 [主控台應用程式 (.NET Core)] 專案樣板
  • 在 [設定新的專案] 對話窗內,於 [專案名稱] 欄位內輸入 efLoggerFactory
  • 點選 [建立] 按鈕,以便開始建立這個專案

加入 Entity Framework Core 要使用到的 NuGet 套件

  • 滑鼠右擊專案內的 [相依性] 節點
  • 選擇 [管理 NuGet 套件]
  • 點選 [瀏覽] 標籤分頁頁次
  • 在 [搜尋] 文字輸入盒內,輸入 [Microsoft.EntityFrameworkCore.SqlServer]
  • 點選 [安裝] 按鈕以便安裝這個套件
  • 在 [搜尋] 文字輸入盒內,輸入 [Microsoft.EntityFrameworkCore.Tools]
  • 點選 [安裝] 按鈕以便安裝這個套件

使用反向工程來產生 Entity Framework 要用到的 Entity 模型相關類別

  • 切換到 [套件管理器主控台] 視窗

    若沒有看到 [套件管理器主控台] 視窗,點選功能表 [工具] > [NuGet 套件管理員] > [套件管理器主控台]

  • 在 [套件管理器主控台] 輸入底下內容

    因為都在同一個專案內,所以,這裡可以省略 StartupProject & Project 這兩個參數,因此,底下的指令會更為精簡

Scaffold-DbContext "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=School" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -f

請打開這個 [Program.cs] 檔案,完成底下的程式碼

using efLoggerFactory.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;

namespace efLoggerFactory
{
    class Program
    {
        public static readonly ILoggerFactory MyLoggerFactory
            = LoggerFactory.Create(builder =>
            {
                builder.AddConsole();
            });
        static void Main(string[] args)
        {
            string connectionString = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=School";
            DbContextOptions<SchoolContext> options = new DbContextOptionsBuilder<SchoolContext>()
                .UseLoggerFactory(MyLoggerFactory)
                .UseSqlServer(connectionString)
                .Options;
            using (var context = new SchoolContext(options))
            {
                Console.WriteLine($"取得 StudentGrade 第一筆紀錄");
                var aStudentGrade = context.StudentGrade.FirstOrDefault();
                Console.WriteLine($"更新成績為 4.99");
                aStudentGrade.Grade = 4.99m;
                context.SaveChanges();
            }
        }
    }
}

從上面的程式碼中,宣告了一個靜態的 ILoggerFactory 型別變數 , ILoggerFactory , 透過 [LoggerFactory.Create] 這個工廠方法,產生出一個物件,在呼叫這個工廠方法的時候,也宣告了使用螢幕 Console 作為輸出日誌的目的地,這裡可以參考 ConsoleLoggerExtensions.AddConsole 方法 文件。

接著,如同上一篇文章 使用 DbContextOptionsBuilder 來指定連線字串與觀察 EF Core 產生的 SQL 指令 做法,先建立一個 [DbContextOptions] 物件,不過,當該物件建立之後,會呼叫 [UseLoggerFactory] 這個方法,並且把 MyLoggerFactory 物件傳送進去,如此,便可以透過這個 [ILogger] 執行個體來觀察 EF Core 產生的 SQL 敘述。

執行結果如下

取得 StudentGrade 第一筆紀錄
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.7 initialized 'SchoolContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (29ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT TOP(1) [s].[EnrollmentID], [s].[CourseID], [s].[Grade], [s].[StudentID]
      FROM [StudentGrade] AS [s]
更新成績為 4.99
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (37ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (DbType = Decimal)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [StudentGrade] SET [Grade] = @p0
      WHERE [EnrollmentID] = @p1;
      SELECT @@ROWCOUNT;

觀察上面的輸出內容,可以但看到當 SchoolContext 進行初始化的時候,會使用 Microsoft.EntityFrameworkCore.SqlServer 這個提供者,接著便會執行底下的 SQL 指令,查詢出 StudentGrade 資料表內的紀錄

Executed DbCommand (29ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT TOP(1) [s].[EnrollmentID], [s].[CourseID], [s].[Grade], [s].[StudentID]
FROM [StudentGrade] AS [s]

最後執行底下的 SQL Command ,更新該筆紀錄

 Executed DbCommand (37ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (DbType = Decimal)], CommandType='Text', CommandTimeout='30']
 SET NOCOUNT ON;
 UPDATE [StudentGrade] SET [Grade] = @p0
 WHERE [EnrollmentID] = @p1; 

SELECT @@ROW 




2020年10月9日 星期五

使用 DbContextOptionsBuilder 來指定連線字串與觀察 EF Core 產生的 SQL 指令

使用 DbContextOptionsBuilder 來指定連線字串與觀察 EF Core 產生的 SQL 指令

接續 上一篇 EF Core 討論文章 DbContext 在多執行緒環境下的運作情況 ,有些時候,當要使用 DbContext 來建立一個可以存取資料庫的 EF Core 的物件,想要能夠自行指定連線字串,可以在該繼承 DbContext 的類別建構函式內,傳入要使用的連線字串,並且於 OnConfiguring 方法內,使用該傳入的連線字串做為要連線到資料庫的依據;不過,這裡使用另外一個方式,那就是使用 DbContextOptionsBuilder 型別,產生一個這個物件,以便指定要使用的連線字串。

請按照底下的步驟來進行操作

建立練習專案

  • 打開 Visual Studio 2019
  • 點選 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗內,選擇 [主控台應用程式 (.NET Core)] 專案樣板
  • 在 [設定新的專案] 對話窗內,於 [專案名稱] 欄位內輸入 efDbContextOptionsBuilder
  • 點選 [建立] 按鈕,以便開始建立這個專案

加入 Entity Framework Core 要使用到的 NuGet 套件

  • 滑鼠右擊專案內的 [相依性] 節點
  • 選擇 [管理 NuGet 套件]
  • 點選 [瀏覽] 標籤分頁頁次
  • 在 [搜尋] 文字輸入盒內,輸入 [Microsoft.EntityFrameworkCore.SqlServer]
  • 點選 [安裝] 按鈕以便安裝這個套件
  • 在 [搜尋] 文字輸入盒內,輸入 [Microsoft.EntityFrameworkCore.Tools]
  • 點選 [安裝] 按鈕以便安裝這個套件

使用反向工程來產生 Entity Framework 要用到的 Entity 模型相關類別

  • 切換到 [套件管理器主控台] 視窗

    若沒有看到 [套件管理器主控台] 視窗,點選功能表 [工具] > [NuGet 套件管理員] > [套件管理器主控台]

  • 在 [套件管理器主控台] 輸入底下內容

    因為都在同一個專案內,所以,這裡可以省略 StartupProject & Project 這兩個參數,因此,底下的指令會更為精簡

Scaffold-DbContext "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=School" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -f

現在 Entity Model 相關資料已經建立完成

請觀察 [SchoolContext.cs] 檔案,這裡的 SchoolContext 類別繼承了 DbContext 類別,不過,這裡有兩個建構式,一個是預設建構函式,也就是沒有任何參數的建構式,另外一個建構式可以接收 [DbContextOptions] 這個型別的物件;另外,在 [OnConfiguring] 覆寫方法內,將會檢查傳入的 [DbContextOptionsBuilder] 物件的 [IsConfigured] 屬性是否為 真,用來確認是否已經指定了連線字串,若沒有指定的話,將會在這裡使用預設的本機 localDB 資料庫之連線字串。

    public partial class SchoolContext : DbContext
    {
        public SchoolContext()
        {
        }
 
        public SchoolContext(DbContextOptions<SchoolContext> options)
            : base(options)
        {
        }
 
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.
                optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=School");
            }
        }
 
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
             . . .
            OnModelCreatingPartial(modelBuilder);
        }
 
        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }

因此,透過上面的解說,將會進行相關的程式開發與設計

請打開這個 [Program.cs] 檔案,完成底下的程式碼

static void Main(string[] args)
{
    string connectionString = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=School";
    DbContextOptions<SchoolContext> options = new DbContextOptionsBuilder<SchoolContext>()
        .UseSqlServer(connectionString)
        .Options;
    using (var context = new SchoolContext(options))
    {
        Console.WriteLine($"取得 StudentGrade 第一筆紀錄");
        var aStudentGrade = context.StudentGrade.FirstOrDefault();
        Console.WriteLine($"{aStudentGrade.StudentId} 學生的 {aStudentGrade.CourseId} 課程的成績為 {aStudentGrade.Grade}");
    }
}

從上面的程式碼中,可以看到首先建立了一個 connectionString 字串物件,該字串即是準備要用到的連線字串,緊接著建立一個 [DbContextOptions] 泛型型別物件,在該物件建立之後,便呼叫 [UseSqlServer] 方法,只是此次要 EF Core 使用指定的連線字串,連線到 SQL Server 服務上,最後,透過 [Options] 屬性,取得此次設定的相關內容。

如此,便可以建立 [SchoolContext] DbContext 物件,不過,需要在該建構函式內傳入剛剛產生的 [DbContextOptions] 物件,如此,一切準備工作都就緒了,便可以透過 [context] 變數來取得後端 SQL Server 內資料表的紀錄。

執行結果如下

取得 StudentGrade 第一筆紀錄
2 學生的 2021 課程的成績為 4.00

最後,要來觀察究竟 EF Core 產生了甚麼 SQL 指定到後端 SQL Server 內,此時,便可以透過 SSMS (SQL Server Management Studio) 這個工具來觀察,請打開此連結 SSMS ,下載與安裝這個工具。

安裝好之後,請找到 SQL Server Profilee 18 這個應用程式,並且打開與執行。

點選功能表 [檔案] > [新增追蹤] 選項

當 [連線至伺服器] 對話窗出現之後,請在 [伺服器名稱] 欄位內,輸入 (localdb)\. 內容

SQL Profiler

點選 [連線] 按鈕,連線到 SQL Server Express LocalDB 上

當出現 [連線屬性] 對話窗,點選 [執行按鈕]

SQL Profiler 連線屬性

現在可以重新執行剛剛建立的 [efDbContextOptionsBuilder] 專案;一旦執行完成之後,便可以看到底下的內容

SQL Profiler 連線屬性

從上面的 SQL Server Profiler 畫面中,可以看到此次將送出底下的 SQL指令到 SQL Server 內,而這個 SQL 指令是因為程式中執行了 context.StudentGrade.FirstOrDefault(); 敘述, EF Core 根據這個 C# 敘述所產生出來的 SQL 指令。

SELECT TOP(1) [s].[EnrollmentID], [s].[CourseID], [s].[Grade], [s].[StudentID] 

FROM [StudentGrade] AS [s] 




2020年10月8日 星期四

DbContext 在多執行緒環境下的運作情況

DbContext 在多執行緒環境下的運作情況

在上篇文章中 Console 專案與 EF Core 讀取已經存在的資料庫 ,可以知道如何在 Console 類型的專案內,加入 EF Core 套件,便可以透過 EF Core 框架所提供的功能,存取後端資料庫內的資料。在這篇文章中,將來探討論外一個問題,那就是需要在多執行緒的多工環境下,若使用同一個 DbContext 會發生甚麼問題。

請按照底下的步驟來進行操作

建立練習專案

  • 打開 Visual Studio 2019
  • 點選 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗內,選擇 [主控台應用程式 (.NET Core)] 專案樣板
  • 在 [設定新的專案] 對話窗內,於 [專案名稱] 欄位內輸入 efMultiThread
  • 點選 [建立] 按鈕,以便開始建立這個專案

加入 Entity Framework Core 要使用到的 NuGet 套件

  • 滑鼠右擊專案內的 [相依性] 節點
  • 選擇 [管理 NuGet 套件]
  • 點選 [瀏覽] 標籤分頁頁次
  • 在 [搜尋] 文字輸入盒內,輸入 [Microsoft.EntityFrameworkCore.SqlServer]
  • 點選 [安裝] 按鈕以便安裝這個套件
  • 在 [搜尋] 文字輸入盒內,輸入 [Microsoft.EntityFrameworkCore.Tools]
  • 點選 [安裝] 按鈕以便安裝這個套件

使用反向工程來產生 Entity Framework 要用到的 Entity 模型相關類別

  • 切換到 [套件管理器主控台] 視窗

    若沒有看到 [套件管理器主控台] 視窗,點選功能表 [工具] > [NuGet 套件管理員] > [套件管理器主控台]

  • 在 [套件管理器主控台] 輸入底下內容

    因為都在同一個專案內,所以,這裡可以省略 StartupProject & Project 這兩個參數,因此,底下的指令會更為精簡

Scaffold-DbContext "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=School" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -f

現在 Entity Model 相關資料已經建立完成

請打開這個 [Program.cs] 檔案,完成底下的程式碼

class Program
{
    static async Task Main(string[] args)
    {
        SchoolContext context = new SchoolContext();
 
        var task1 = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
               var people= await context.Person
                .AsNoTracking()
                .OrderBy(x => x.LastName).ToListAsync();
                foreach (var item in people)
                {
                    Console.Write($"{item.LastName} ");
                }
            }
        });
        var task2 = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                var courses = await context.Course
                 .AsNoTracking()
                 .OrderBy(x => x.Title).ToListAsync();
                foreach (var item in courses)
                {
                    Console.Write($"{item.Title} ");
                }
            }
        });
        await Task.WhenAll(task1, task2);
    }
}

從上面的程式碼中,可以看到首先建立了一個 SchoolContext 型別的 context 物件,這個物件就代表了遠端的資料庫,而接下來將會透過 Task.Run 方法 建立兩個非同步工作 物件,代表未來與承諾會完成這個非同步工作內指定的需求,在這兩個非同步工作內,分別會執行 100 次的迴圈,每次迴圈分別會對 Person & Course 資料表,讀取遠端資料庫內的所有紀錄回本機電腦內;基本上,看樣子的設計,似乎沒有甚麼衝突可以發生,因為,雖然有兩個非同步工作,這兩個非同步工作都是做查詢而已,而且是分別在不同資料表內做查詢,可是,當執行這個程式碼之後,便會發生底下的錯誤訊息:

System.InvalidOperationException: 'An attempt was made to use the context while it is being configured. A DbContext instance cannot be used inside OnConfiguring since it is still being configured at this point. This can happen if a second operation is started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.'

System.InvalidOperationException

通常,第一次看到這樣的錯誤訊息的時候,絕大多數的人都是很迷惘了,究竟發生了甚麼問題,可是仔細看這段訊息,會看到這樣的內容 Any instance members are not guaranteed to be thread safe. 這裡充分說明了 DbContext 的類別所產生單一物件,不能夠使用於多執行緒環境下,在微軟官方文件中,也有提到這樣的內容

Entity Framework Core does not support multiple parallel operations being run on the same DbContext instance. This includes both parallel execution of async queries and any explicit concurrent use from multiple threads. Therefore, always await async calls immediately, or use separate DbContext instances for operations that execute in parallel.

When EF Core detects an attempt to use a DbContext instance concurrently, you'll see an InvalidOperationException with a message like this:

A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext, however instance members are not guaranteed to be thread safe.

When concurrent access goes undetected, it can result in undefined behavior, application crashes and data corruption.

所以,這樣的用法就算僅作查詢或分別針對不同資料表做查詢,一樣會產生例外異常。

若想要修正這樣的問題,可以參考底下的作法,就可以避免掉執行緒不安全的問題。

static async Task Main(string[] args)
{
 
    var task1 = Task.Run(async () =>
    {
        using (SchoolContext context = new SchoolContext())
        {
            for (int i = 0; i < 100; i++)
            {
                var people = await context.Person
                 .AsNoTracking()
                 .OrderBy(x => x.LastName).ToListAsync();
                foreach (var item in people)
                {
                    Console.Write($"{item.LastName} ");
                }
            }
        }
    });
    var task2 = Task.Run(async () =>
    {
        using (SchoolContext context = new SchoolContext())
        {
            for (int i = 0; i < 100; i++)
            {
                var courses = await context.Course
                 .AsNoTracking()
                 .OrderBy(x => x.Title).ToListAsync();
                foreach (var item in courses)
                {
                    Console.Write($"{item.Title} ");
                }
            }
        }
    });
    await Task.WhenAll(task1, task2); 

} 




2020年10月2日 星期五

Memory Overflow 造成的原因

Memory Overflow 造成的原因

在前面的文章 Stack Overflow 造成的原因 ,討論到造成 Stack Overflow 的現象,在這裡,將會來研究另外一種 記憶體 Overflow 問題,通常當這個問題發生的時候,會看到 [記憶體不足] 這樣的訊息出現,而且,此時,這個處理程序就異常終止,並且會看到如下的畫面。

會發生這樣的現象,那是因為這個處理程序耗用了過多的 Heap 堆積內的記憶體空間,導致當其他程式碼需要產生執行個體的時候,因為沒有足夠的記憶體,而造成整個處理成緒崩潰。

現在,來了解發生這樣問題的真正原因。當在 .NET 系統底下,需要建立一個 參考 Reference 型別的物件,通常會使用 new 這個運算子,後面緊接著某個型別的建構式,此時, .NET CLR 將會從 Heap 堆積記憶體空間,配置一塊記憶體來作為儲存該執行個體內容之用;而當這個物件不被其他變數參考到的時候,便會被 GC 垃圾回收機制回收,接著是放掉這個物件所參考到的記憶體空間。

若系統不斷的要求超過位址空間所允許的記憶體,便會發生了記憶體不足的問題,除了不斷的要求建立一個新的物件,但是持續有變數參考到這個物件,便會造成記憶體不足的問題,這還會發生到 GC Generation 2 的時候,因為架構上的設計,導致記憶體空間破碎,雖然剩餘的記憶體空間還足夠,但無法真正的找到一塊連緒且符合要求的記憶體空間,這樣也會造成記憶體不足的問題。

在上面的說明中,可以看到對於 .NET 系統中,將會有兩種類型的記憶體儲存空間,一個是 Heap,這裡將會存放各種參考型別物件值,而且這裡可用的空間非常大,另外一種是 Stack,這裡的空間會使用當前執行緒所配置的空間來使用,而在 .NET 中,一個執行緒在建立的時候,預設將會擁有 1MB 的記憶體空間,任何在方法呼叫過程中,對於參數與區域變數,將會使用這裡的空間。

因此,底下的範例,將會設計兩個類別 [MyLargeClass] 與 [MySmallClass] ,這兩個類別都是參考型別,因此,當建立執行個體的時候,會從 Heap 中要求一塊記憶體空間來用;前者類別當產生物件的時候,將會佔據至少 86KB 的記憶體空間,因為該類別類宣告了一個長度為 86KB 的 byte 陣列,後者類別將會從 Heap 中要求至少 20KB 的記憶體空間。

所以,在 Main 方法內,將會建立一個超大迴圈,不斷地建立這兩個型別的物件,並且把取得的記憶體空間,儲存到 List 或者 List 集合型別內,也因為這樣,這些產生的物件並不會因為 GC 記憶體回收機制的運作,而把這些記憶體歸還給系統,經過不斷的執行,終於造成了記憶體不足的問題,也就會出現體底下的異常畫面。

在下圖,將可以看到這個測試程式碼執行時候,耗用記憶體空間的過程。

Memory Overflow

public class MyLargeClass
{
    // 這將會讓產生的執行個體會占用 > 85000 bytes 的記憶體大小
    public byte[] bytes = new byte[1024 * 86];
}
 
public class MySmallClass
{
    // 這將會讓產生的執行個體會占用 < 85000 bytes 的記憶體大小
    public byte[] bytes = new byte[1024 * 20];
}
class Program
{
    static void Main(string[] args)
    {
 
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
        //這裡將會模擬產生大量的大物件執行個體,耗用大量的堆積記憶體
        List<MyLargeClass> listLargeObject = new List<MyLargeClass>();
        for (int i = 0; i < 1000 * 1000; i++)
        {
            listLargeObject.Add(new MyLargeClass());
        }
 
        //這裡將會模擬產生大量的物件執行個體,耗用大量的堆積記憶體
        List<MySmallClass> listSmallObject = new List<MySmallClass>();
        for (int i = 0; i < 1000 * 1000; i++)
        {
            listSmallObject.Add(new MySmallClass());
        }
    }
}




 

2020年10月1日 星期四

Stack Overflow 造成的原因

Stack Overflow 造成的原因

在進行程式設計的時候,經常會聽到 記憶體不足 這樣的問題,其中,對於記憶體不足這樣的現象,會發生在兩種情況下,在這裡將會展示出如何過度使用 執行緒推疊 Stack Overflow。

當在設計類別中的方法時候,對於該方法所使用的參數或者區域變數,若這兩種物件的型別為 數值型別 Value Type,則該物件將會從堆疊結構中取得一個空間來作為儲存、存放的地方,然而,若是一個 參考型別 Reference Type ,則會從堆疊結構中取得一個該參考型別的位置儲存空間,並且要求 堆積 Heap 取得一塊空間,用來儲存該參考物件的相關執行個體內容。

在上面的說明中,可以看到對於 .NET 系統中,將會有兩種類型的記憶體儲存空間,一個是 Heap,這裡將會存放各種參考型別物件值,而且這裡可用的空間非常大,另外一種是 Stack,這裡的空間會使用當前執行緒所配置的空間來使用,而在 .NET 中,一個執行緒在建立的時候,預設將會擁有 1MB 的記憶體空間,任何在方法呼叫過程中,對於參數與區域變數,將會使用這裡的空間。

因此,底下的範例,將會設計一個方法 [NestRecursionMethod] ,這個方法將會自我遞迴呼叫,在這個方法內,將宣告五個 decimal 數值型別的區域變數,每個 decimal 物件,將會耗用 16 bytes的記憶體空間,所以,五個 decimal 物件,將會耗用 16*5 = 80 bytes 記憶體空間;當然在呼叫方法的過程中,還會有其他的資料會儲存在 Thread Stack 這個資料結構空間內。

在這個程式中,因為會反覆呼叫 [NestRecursionMethod] 方法,進而造成 Stack 記憶體空間不斷減少,一旦該 Stack 內沒有足夠的記憶體空間,將會造成 Stack 空間不足,也就是會拋出 Stack Overflow 的例外異常。

這裡是最後執行後的螢幕輸出結果

.
.
.
Level 3818
Level 3819
Level 3820
Level 3821
Level 3822
Level 3823
Level 3824
Level 3825
Level 3826
Stack overflow.

而在 Visual Studio 2019 內,將會看到這樣的例外異常提示畫面

這裡將會是該例外異常的詳細資訊

System.StackOverflowException
  HResult=0x800703E9
  Source=<無法評估例外狀況來源>
  StackTrace: 
<無法評估例外狀況堆疊追蹤>
class Program
{
    static void Main(string[] args)
    {
        int level = 1;
        Console.WriteLine("Hello World!");
        NestRecursionMethod(level);
    }
 
    private static void NestRecursionMethod(int level)
    {
        // 一個 decimal 使用 16 bytes 空間
        decimal decimal1=1;
        decimal decimal2= decimal1+1;
        decimal decimal3 = decimal2 + 1;
        decimal decimal4 = decimal3 + 1;
        decimal decimal5 = decimal4 + 1;
        Console.WriteLine($"Level {level}");
        NestRecursionMethod(++level);
    } 

}