.NET Core/5+에서 동적 컴파일한 C# 코드를 (Breakpoint도 활용하며) 디버깅하는 방법 - #line 지시자
지난 글에서,
.NET Core/5+에서 C# 코드를 동적으로 컴파일/사용하는 방법
; https://www.sysnet.pe.kr/2/0/12809
C# 소스 코드를 동적으로 컴파일 및 사용하는 방법을 다뤘는데요, 사실 이에 대한 디버깅도 가능합니다. 어떻게 할 수 있는지 한번 다뤄볼까요? ^^
지난 글의 소스 코드를 활용해 바꿔볼 텐데요, 우선 C# 코드를 별도의 파일로 빼내야 합니다. 테스트를 위해 "dynamic_code.txt"라는 파일로 동적 코드를 빼내고,
string codeToCompile = File.ReadAllText("dynamic_code.txt");
다음과 같이, 해당 파일은 "Build Action: None", "Copy to Output Directory: Copy if newer" 옵션을 설정해 줍니다. (위의 경우에는 "Build Action" 설정은 필요 없지만, 원래 C# 코드를 담은 파일의 확장자를 .cs로 주는 것이 일반적이기 때문에 그런 때를 위해 명시하는 것이 좋습니다.)
그리고, 디버깅이 가능하려면 debug 모드로 빌드해야 하므로 옵션을 조정하고,
CSharpCompilationOptions options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary
, optimizationLevel: OptimizationLevel.Debug);
동적 빌드된 어셈블리의 바이너리뿐만 아니라 PDB 정보까지도 출력하도록 바꾸고, 사용할 때도 Symbol 정보를 로드하도록 바꿉니다.
using (var symbolsStream = new MemoryStream())
using (var ms = new MemoryStream())
{
EmitResult result = compilation.Emit(ms, pdbStream: symbolsStream);
if (result.Success)
{
ms.Seek(0, SeekOrigin.Begin);
symbolsStream?.Seek(0, SeekOrigin.Begin);
Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms, symbolsStream);
var type = assembly.GetType("MyType");
var instance = assembly.CreateInstance("MyType");
var meth = type.GetMember("Print").First() as MethodInfo;
meth.Invoke(instance, new[] { "World" });
}
}
혹은 새로운 옵션인 embedded를 사용해,
닷넷 응용 프로그램을 위한 PDB 옵션 - full, pdbonly, portable, embedded
; https://www.sysnet.pe.kr/2/0/12554
다음과 같이 코드를 좀 더 간략하게 만들 수 있습니다.
using (var ms = new MemoryStream())
{
var emitOptions = new EmitOptions(debugInformationFormat: DebugInformationFormat.Embedded);
EmitResult result = compilation.Emit(ms,
options: emitOptions);
if (result.Success)
{
ms.Seek(0, SeekOrigin.Begin);
Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
var type = assembly.GetType("MyType");
var instance = assembly.CreateInstance("MyType");
var meth = type.GetMember("Print").First() as MethodInfo;
meth.Invoke(instance, new[] { "World" });
}
}
기본적인 준비는 여기까지가 끝입니다.
자, 이제 마지막 남은 것이 하나 있는데요, 바로 비주얼 스튜디오에게 동적으로 컴파일한 MyType.Print 메서드가 불렸을 때 화면에 어떤 소스 코드 파일을 로드해 보여줄 것인지를 결정하는 것입니다.
그리고 바로 그런 역할을 하는 것이 #line 지시자입니다. 따라서 동적 소스 코드를 포함한 dynamic_code.txt 파일에 다음의 정보를 포함하고,
1
2 #line 3 "dynamic_code.txt"
3
4 using System;
5 using System.Text;
6
7 public class MyType
8 {
9 public void Print(object obj)
10 {
11 StringBuilder sb = new StringBuilder();
12 sb.Append(DateTime.Now);
13
14 Console.WriteLine("Hello: " + obj + " : " + sb.ToString());
15 }
16 }
빌드하면, 이제 C# 컴파일러는 해당 소스 코드가 "dynamic_code.txt" 파일에 있으며 기준점으로 3번째 라인에 매핑시켜야 한다는 것을 알 수 있습니다.
따라서, 이제 위의 소스 코드에 Breakpoint를 걸 수도 있고 실제로 디버그 모드로 실행하면 다음과 같이 BP도 잡히고 심지어 Watch 창에서 변숫값도 확인할 수 있습니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
한 가지 가정을 해볼까요? 만약 제가
Razor 템플릿 개발자이고 다음과 같은 코드 양식을 사용했을 때,
@page
@{
var name = string.Empty;
if (Request.HasFormContentType)
{
name = Request.Form["name"];
}
}
<div style="margin-top:30px;">
<form method="post">
<div>
Name: <input name="name" />
</div>
<div>
<input type="submit" />
</div>
</form>
</div>
<div>
@if (!string.IsNullOrEmpty(name))
{
<p>Hello @name!</p>
}
</div>
어떻게 저 razor 파일 내에 있는 C# 코드에 BP를 걸게 만들 수 있었을까요? ^^ 그렇습니다. 자동 생성된 코드에서 저 파일을 지정하도록 하면 되는 것입니다.
// sample.cshtml.g.cs
#line 3 "sample.cshtml" // 위의 파일 이름이 sample.cshtml라고 가정
using System;
using System.Text;
// razor 파일을 C#으로 다루는 코드
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]