利用Mermaid產出例外錯誤流程圖
前言
最近研究繪製流程圖時,無意間發現了「Mermaid」,Mermaid是由JavaScript為基礎建構的開源軟體,其最主的特色是利用簡易的文字語法來繪製各種不同類型的圖示:Flowchart 流程圖、Sequence Diagram 循序圖、Class Diagram 類別圖、State Diagram 狀態圖、Entity Relationship Diagram 實體關係圖、User Journey 用戶旅程圖、Gantt 甘特圖、Pie Chart 圓餅圖、Git Graph 版控圖
網站也提供線上操作展示範例
下面文章環境是選擇使用C# asp.net core MVC,例外錯誤訊息利用Flowchart(流程圖)產出視覺化的圖示。
Flowchart 文字語法
flowchart TB
    c1-->a2
    subgraph one
    a1-->a2
    end
    subgraph two
    b1-->b2
    end
    subgraph three
    c1-->c2
    end

由文件說明及範例參考先觀察出語法如何撰寫
這邊先簡單寫一個會沒有檢查時間物件null而導致出現例外錯誤的範例
public class HelpObj
{
	public static void TestTime()
	{
		DateTime? dt = null;
		string testError = dt.ToStrForTime();
	}
}
public static class DateTimeExtensions
{
	public static string ToStrForTime(this DateTime? dateTime)
	{
		return dateTime.Value.ToString("yyyy MM dd");
	}
}其例外錯誤的StackTrace就會是
at System.Nullable`1.get_Value() at Common.Util.Extensions.DateTimeExtensions.ToStrForTime(Nullable`1 dateTime) in ...\Common\Util\Extensions\DateTimeExtensions.cs:line 38 at Common.Util.HelpObj.TestTime() in ..\CoreMVC\Common\Util\HelpObj.cs:line 21 at CoreMVC.Controllers.HomeController.Privacy() in ..\CoreMVC\CoreMVC\Controllers\HomeController.cs:line 34
這些錯誤訊息中
System.Nullable`1.get_Value()
Common.Util.Extensions.DateTimeExtensions.ToStrForTime(Nullable`1 dateTime)
Common.Util.HelpObj.TestTime()
CoreMVC.Controllers.HomeController.Privacy()
這四段就是我們要轉換為Flowchart語法的關鍵
將錯誤訊息分兩個部分
1.程式執行流程
從訊息中可得到 Privacy() > TestTime() > ToStrForTime() > get_Value()
2.物件所在位置
注意第二三行的有共同專案及目錄名稱
將錯誤訊息套到Flowchart語法中
Privacy(Privacy: 34):Privacy()是物件所在位置,()內為顯示文字,這邊顯示方法及程式行數
graph LR
	ToStrForTime-->get_Value
	TestTime-->ToStrForTime
	Privacy-->TestTime
	subgraph System
	subgraph Nullable`1
	get_Value
	end
	end
	subgraph Common
	
	subgraph Util
	subgraph Extensions
	subgraph DeTimeExtensions
	ToStrForTime(ToStrForTime: 38)
	end
	end
	end
	subgraph HelpObj
	TestTime(TestTime: 21)
	end
	
	end
	subgraph CoreMVC
	subgraph Controllers
	subgraph HomeController
	Privacy(Privacy: 34)
	end
	end
	end
有了目標,有可以開發model、及轉換方法
轉換暫存model
public class ErrorDiagram
{
	private List<ErrorDiagram> _child;
	public List<ErrorDiagram> Child 
	{
		get 
		{
			if (_child == null)
			{
				_child = new List<ErrorDiagram>();
			}
			return _child;
		}
	}
	/// <summary>
	/// Sub name
	/// </summary>
	public string SubGraph { get; set; }
	/// <summary>
	/// function name
	/// </summary>
	public string FunctionName { get; set; }
	/// <summary>
	/// error line
	/// </summary>
	public string LineNum { get; set; }
	/// <summary>
	/// 是否為function
	/// </summary>
	/// <returns></returns>
	public bool IsFunction()
	{
		return FunctionName != null;
	}
	/// <summary>
	/// view function & error linenum
	/// </summary>
	/// <returns></returns>
	public string FunctionView()
	{
		if (IsFunction() && string.IsNullOrEmpty(LineNum) != true)
		{
			return string.Format("{0}({0}:{1})", FunctionName, LineNum);
		}
		else if (IsFunction())
		{
			return FunctionName;
		}
		return string.Empty;
	}
	/// <summary>
	/// 是否有Sub
	/// </summary>
	/// <returns></returns>
	public bool HasSubChild()
	{
		if (Child != null)
		{
			return Child.Count > 0;
		}
		return false;
	}
	/// <summary>
	/// get mermaid flowchart code
	/// </summary>
	/// <returns></returns>
	public string GetDiagramCode()
	{
		// 
		if (IsFunction())
		{
			return FunctionView();
		}
		if (HasSubChild() && Child.Count == 1)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine($"subgraph {SubGraph}");
			stringBuilder.AppendLine(Child.FirstOrDefault().GetDiagramCode());
			stringBuilder.AppendLine("end");
			return stringBuilder.ToString();
		}
		else if (HasSubChild())
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine($"subgraph {SubGraph}");
			foreach (var item in Child)
			{
				stringBuilder.AppendLine(item.GetDiagramCode());
			}
			stringBuilder.AppendLine("end");
			return stringBuilder.ToString();
		}
		return "";
	}
}
轉換主要流程code
/// <summary>
/// 轉換為流程圖code
/// </summary>
/// <param name="StackTrace"></param>
/// <returns></returns>
public static string GetFlowChart(string stackTrace)
{
	StringReader strReader = new StringReader(stackTrace);
	string strline = strReader.ReadLine();
	List<ErrorDiagram> errorDiagrams = new List<ErrorDiagram>();
	List<string> functionName = new List<string>();
	while (strline != null)
	{
		var strfirstLast = strline.Split(" in ");
		string strfirst = strfirstLast.FirstOrDefault().Replace("at","").Trim();
		var strLastLineNum = strfirstLast.Length > 1 ? strfirstLast.LastOrDefault().Split(":line").LastOrDefault() : "";
		var subDiagrams = strfirst.Split(".").ToList();
		functionName.Add(subDiagrams.LastOrDefault().Split("(").FirstOrDefault());
		BuildErrorDiagrams(ref errorDiagrams, subDiagrams, strLastLineNum);
		strline = strReader.ReadLine();
	}
	StringBuilder stringBuilder = new StringBuilder();
	stringBuilder.AppendLine("graph LR");
	// flow
	for (int i = 0; i < functionName.Count-1; i++)
	{
		int parentIndex = i + 1;
		if (parentIndex < functionName.Count)
		{
			stringBuilder.AppendLine($"{functionName[parentIndex]}-->{functionName[i]}");
		}
	}
	// object site
	foreach (var item in errorDiagrams)
	{
		stringBuilder.AppendLine(item.GetDiagramCode());
	}
	
	string result = stringBuilder.ToString();
	return result;
}
建置各層訊息流程方法
private static void BuildErrorDiagrams(ref List<ErrorDiagram> errorDiagrams, List<string> subDiagrams,string lineNum) 
{
	//建置 Diagrams物件
	ErrorDiagram errorDiagramParent = null;
	foreach (var item in subDiagrams)
	{
		if (errorDiagramParent == null)
		{
			// first
			errorDiagramParent = errorDiagrams.Find(x => x.SubGraph == item);
			// check has
			if (errorDiagramParent == null)
			{
				errorDiagramParent = new ErrorDiagram()
				{
					SubGraph = item,
				};
				errorDiagrams.Add(errorDiagramParent);
			}
		}
		else if (item == subDiagrams.LastOrDefault())
		{
			// function
			errorDiagramParent.Child.Add(new ErrorDiagram()
			{
				FunctionName = item.Split("(").FirstOrDefault(),
				LineNum = lineNum,
			});
		}
		else
		{
			var errorDiagramChild = errorDiagramParent.Child.Find(x => x.SubGraph == item);
			// check has
			if (errorDiagramChild == null)
			{
				var errorDiagramNew = new ErrorDiagram()
				{
					SubGraph = item,
				};
				errorDiagramParent.Child.Add(errorDiagramNew);
				errorDiagramParent = errorDiagramNew;
			}
		}
	}
}
設置在執行Privacy頁面出錯時,將錯誤訊息導致DiagramError頁面
public class HomeController : Controller
{
	...
	
	public IActionResult Privacy()
	{
		try
		{
			//HelpObj.Test();
			HelpObj.TestTime();
		}
		catch (Exception ex)
		{
			TempData["error"] = JsonConvert.SerializeObject(new DiagramErrorModel
			{
				Message = ex.Message,
				StackTrace = ex.StackTrace
			});
			return RedirectToAction("DiagramError");
		}
		
		return View();
	}
	public IActionResult DiagramError()
	{
		DiagramErrorModel diagramErrorModel = new DiagramErrorModel();
		object temp = TempData["error"];
		if (temp != null)
		{
			diagramErrorModel = JsonConvert.DeserializeObject<DiagramErrorModel>(temp.ToString());
		}
		var diagramError = new DiagramErrorViewModel()
		{
			Message = diagramErrorModel.Message,
			DiagramCode = HelpMermaid.GetFlowChart(diagramErrorModel.StackTrace),
			StackTrace = diagramErrorModel.StackTrace
		};
		return View(diagramError);
	}
}
Error Model
public class DiagramErrorModel
{
	public string Message { get; set; }
	public string StackTrace { get; set; }
}
public class DiagramErrorViewModel
{
	public string Message { get; set; }
	public string DiagramCode { get; set; }
	public string StackTrace { get; set; }
}
參考文件加入script參考

顯示頁error view 加入script參考
@model DiagramErrorViewModel
<h1>錯誤流程圖DiagramError</h1>
<p>@Model.Message</p>
<div class="mermaid">
    @Model.DiagramCode
</div>
<div>
    @Model.StackTrace
</div>
@section Scripts{
    <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
    <script>
        mermaid.initialize({ start"font-size: 12pt;">
}
...點選Privacy

產出錯誤流程圖

結論
這次用例外錯誤範例來呈現Flowchart對於開發者來說,唯一比較明顯的幫助,大概程式是否有用錯方法或用錯誤物件,從Flowchart會比原來錯誤訊息來的更明確一些。
或許這個範例成效不是那麼明顯,但目前能想到的Mermaid可應用範圍,是那些不容易從文字訊息了解內容,如果可以透過轉換成簡單的圖示,是不是就可以幫助使用者更快速地了解系統產出的文字內容。
 
						 
								 
								 
								 
								 
								 
								 
								 
								 
								 
								 
								 
								