Skip to content

Commit 4232365

Browse files
authored
Merge pull request #510 from dadhi/copilot/use-unsafeaccess-net8-and-net10
Use UnsafeAccessor (NET8+) and UnsafeAccessorType (NET10+) for non-public IL generator internals
2 parents e27a161 + c0a8a45 commit 4232365

10 files changed

Lines changed: 199 additions & 29 deletions

File tree

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ jobs:
3232
- name: Build
3333
run: dotnet build -c:Release
3434

35+
- name: Tests - net10.0 (Latest)
36+
run: dotnet run --no-build -c:Release -f:net10.0 --project test/FastExpressionCompiler.TestsRunner/FastExpressionCompiler.TestsRunner.csproj
37+
3538
- name: Tests - net9.0 (Latest)
3639
run: dotnet run --no-build -c:Release -f:net9.0 --project test/FastExpressionCompiler.TestsRunner/FastExpressionCompiler.TestsRunner.csproj
3740

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
1010
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
1111

12-
<NoWarn>IDE0251;IDE0079;IDE0047;NETSDK1212</NoWarn>
12+
<NoWarn>IDE0251;IDE0079;IDE0047;NETSDK1212;NU1510</NoWarn>
1313

1414
<!-- When set, reducec number of the TargetPlatforms to speedup a local Dev -->
1515
<DevMode>false</DevMode>

build.bat

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ echo:## Finished: RESTORE and BUILD
1515

1616
echo:
1717
echo:## Starting: TESTS...
18+
echo:
19+
echo:running on .NET 10.0 (Latest)
20+
dotnet run --no-build -f:net10.0 -c:Release --project test/FastExpressionCompiler.TestsRunner
21+
if %ERRORLEVEL% neq 0 goto :error
22+
1823
echo:
1924
echo:running on .NET 9.0 (Latest)
2025
dotnet run --no-build -f:net9.0 -c:Release --project test/FastExpressionCompiler.TestsRunner

src/FastExpressionCompiler.LightExpression/FastExpressionCompiler.LightExpression.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0</TargetFrameworks>
4-
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0</TargetFrameworks>
3+
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
4+
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0;net10.0</TargetFrameworks>
55

66
<VersionPrefix>5.4.1</VersionPrefix>
77
<VersionSuffix></VersionSuffix>

src/FastExpressionCompiler/FastExpressionCompiler.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9050,6 +9050,15 @@ public virtual LocalBuilder DeclareLocal(Type localType, bool pinned)
90509050
return localBuilder;
90519051
}
90529052
*/
9053+
#if NET10_0_OR_GREATER
9054+
// In .NET 10+, use UnsafeAccessorType to directly access RuntimeILGenerator's private fields
9055+
// without the need for reflection-based DynamicMethod generation at startup
9056+
GetNextLocalVarLocation = static (il, t) =>
9057+
{
9058+
GetMLocalSignature(il).AddArgument(t, false);
9059+
return PostInc(ref GetMLocalCount(il));
9060+
};
9061+
#else
90539062
// Let's try to acquire the more efficient less allocating method
90549063
var m_localSignatureField = DynamicILGeneratorType.GetField("m_localSignature", instanceNonPublic);
90559064
if (m_localSignatureField == null)
@@ -9095,6 +9104,7 @@ public virtual LocalBuilder DeclareLocal(Type localType, bool pinned)
90959104

90969105
ExpressionCompiler.FreePooledParamTypes(paramTypes);
90979106
endOfGetNextVar:;
9107+
#endif
90989108
}
90999109

91009110
// Restore the demit
@@ -9220,6 +9230,43 @@ public override void Emit(OpCode opcode, MethodInfo meth, int paramCount)
92209230
m_length += 4;
92219231
}
92229232
*/
9233+
9234+
#if NET10_0_OR_GREATER
9235+
// UnsafeAccessorType methods for accessing private fields of the non-public RuntimeILGenerator class.
9236+
// RuntimeILGenerator is the internal base class of DynamicILGenerator that holds the core IL generation state.
9237+
// Using UnsafeAccessorType avoids reflection at call time and is compatible with AOT compilation.
9238+
9239+
/// <summary>Gets a ref to the <c>m_localSignature</c> field of the ILGenerator (declared in the internal RuntimeILGenerator).</summary>
9240+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_localSignature")]
9241+
internal static extern ref SignatureHelper GetMLocalSignature(
9242+
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);
9243+
9244+
/// <summary>Gets a ref to the <c>m_localCount</c> field of the ILGenerator (declared in the internal RuntimeILGenerator).</summary>
9245+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_localCount")]
9246+
internal static extern ref int GetMLocalCount(
9247+
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);
9248+
9249+
/// <summary>Gets a ref to the <c>m_length</c> field of the ILGenerator (declared in the internal RuntimeILGenerator).</summary>
9250+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_length")]
9251+
internal static extern ref int GetMLength(
9252+
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);
9253+
9254+
/// <summary>Gets a ref to the <c>m_ILStream</c> field of the ILGenerator (declared in the internal RuntimeILGenerator).</summary>
9255+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_ILStream")]
9256+
internal static extern ref byte[] GetMILStream(
9257+
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);
9258+
9259+
/// <summary>Calls the internal <c>UpdateStackSize</c> method on the ILGenerator (declared in the internal RuntimeILGenerator).</summary>
9260+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "UpdateStackSize")]
9261+
internal static extern void UpdateStackSize(
9262+
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il,
9263+
OpCode opcode, int stackchange);
9264+
9265+
/// <summary>Gets a ref to the <c>m_tokens</c> field on a DynamicScope instance (internal type).</summary>
9266+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_tokens")]
9267+
internal static extern ref System.Collections.Generic.List<object> GetMTokens(
9268+
[UnsafeAccessorType("System.Reflection.Emit.DynamicScope")] object scope);
9269+
#endif
92239270
}
92249271

92259272
[RequiresUnreferencedCode(Trimming.Message)]

src/FastExpressionCompiler/FastExpressionCompiler.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0</TargetFrameworks>
4-
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0</TargetFrameworks>
3+
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
4+
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0;net10.0</TargetFrameworks>
55

66
<VersionPrefix>5.4.1</VersionPrefix>
77
<VersionSuffix></VersionSuffix>

test/FastExpressionCompiler.IssueTests/EmitHacksTest.cs

Lines changed: 134 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ public class EmitHacksTest : ITest
1515
public int Run()
1616
{
1717
DynamicMethod_Emit_Hack();
18-
// DynamicMethod_Emit_Newobj();
19-
// DynamicMethod_Hack_Emit_Newobj();
18+
#if NET10_0_OR_GREATER
19+
DynamicMethod_Emit_Hack_Net10();
20+
return 4;
21+
#else
2022
return 3;
23+
#endif
2124
}
2225

2326
public void DynamicMethod_Emit_Hack()
@@ -27,18 +30,27 @@ public void DynamicMethod_Emit_Hack()
2730
Asserts.AreEqual(42, a);
2831
}
2932

30-
static Type ilType = typeof(ILGenerator).Assembly.GetType("System.Reflection.Emit.DynamicILGenerator");
31-
static FieldInfo mScopeField = ilType.GetField("m_scope", BindingFlags.Instance | BindingFlags.NonPublic);
33+
static readonly Type ilType = typeof(ILGenerator).Assembly.GetType("System.Reflection.Emit.DynamicILGenerator");
3234

33-
static Type scopeType = ilType.Assembly.GetType("System.Reflection.Emit.DynamicScope");
34-
static FieldInfo mTokensField = scopeType.GetField("m_tokens", BindingFlags.Instance | BindingFlags.NonPublic);
35+
// m_scope field is on DynamicILGenerator (internal class) - accessed via reflection since
36+
// the field type DynamicScope is also internal (UnsafeAccessorType can't return non-public types).
37+
static readonly FieldInfo mScopeField = ilType?.GetField("m_scope", BindingFlags.Instance | BindingFlags.NonPublic);
3538

36-
static FieldInfo mLengthField = typeof(ILGenerator).GetField("m_length", BindingFlags.Instance | BindingFlags.NonPublic);
37-
static FieldInfo mILStreamField = typeof(ILGenerator).GetField("m_ILStream", BindingFlags.Instance | BindingFlags.NonPublic);
38-
static MethodInfo updateStackSize = typeof(ILGenerator).GetMethod("UpdateStackSize", BindingFlags.Instance | BindingFlags.NonPublic);
39+
static readonly Type scopeType = ilType?.Assembly.GetType("System.Reflection.Emit.DynamicScope");
40+
static readonly FieldInfo mTokensField = scopeType?.GetField("m_tokens", BindingFlags.Instance | BindingFlags.NonPublic);
41+
42+
// m_length, m_ILStream, and UpdateStackSize are on RuntimeILGenerator (the internal base class of DynamicILGenerator),
43+
// NOT on the public ILGenerator class. Look up the fields on the correct type.
44+
static readonly Type runtimeILGenType = ilType?.BaseType; // System.Reflection.Emit.RuntimeILGenerator
45+
static readonly FieldInfo mLengthField = runtimeILGenType?.GetField("m_length", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
46+
static readonly FieldInfo mILStreamField = runtimeILGenType?.GetField("m_ILStream", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
47+
static readonly MethodInfo updateStackSizeMethod = runtimeILGenType?.GetMethod("UpdateStackSize", BindingFlags.Instance | BindingFlags.NonPublic);
3948

4049
private static Func<ILGenerator, IList<object>> GetScopeTokens()
4150
{
51+
if (mScopeField == null || mTokensField == null)
52+
return null;
53+
4254
var dynMethod = new DynamicMethod(string.Empty,
4355
typeof(IList<object>), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(ILGenerator) },
4456
typeof(ExpressionCompiler), skipVisibility: true);
@@ -57,6 +69,8 @@ private static Func<ILGenerator, IList<object>> GetScopeTokens()
5769

5870
private static GetFieldRefDelegate<TFieldHolder, TField> CreateFieldAccessor<TFieldHolder, TField>(FieldInfo field)
5971
{
72+
if (field == null) return null;
73+
6074
var dynMethod = new DynamicMethod(string.Empty,
6175
typeof(TField).MakeByRefType(), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(TFieldHolder) },
6276
typeof(TFieldHolder), skipVisibility: true);
@@ -69,14 +83,34 @@ private static GetFieldRefDelegate<TFieldHolder, TField> CreateFieldAccessor<TFi
6983
return (GetFieldRefDelegate<TFieldHolder, TField>)dynMethod.CreateDelegate(typeof(GetFieldRefDelegate<TFieldHolder, TField>));
7084
}
7185

72-
static GetFieldRefDelegate<ILGenerator, int> mLengthFieldAccessor = CreateFieldAccessor<ILGenerator, int>(mLengthField);
73-
static GetFieldRefDelegate<ILGenerator, byte[]> mILStreamAccessor = CreateFieldAccessor<ILGenerator, byte[]>(mILStreamField);
86+
static readonly GetFieldRefDelegate<ILGenerator, int> mLengthFieldAccessor = CreateFieldAccessor<ILGenerator, int>(mLengthField);
87+
static readonly GetFieldRefDelegate<ILGenerator, byte[]> mILStreamAccessor = CreateFieldAccessor<ILGenerator, byte[]>(mILStreamField);
88+
89+
static readonly Action<ILGenerator, OpCode, int> updateStackSizeDelegate = GetUpdateStackSizeDelegate();
7490

75-
static Action<ILGenerator, OpCode, int> updateStackSizeDelegate =
76-
(Action<ILGenerator, OpCode, int>)Delegate.CreateDelegate(typeof(Action<ILGenerator, OpCode, int>), null, updateStackSize);
91+
private static Action<ILGenerator, OpCode, int> GetUpdateStackSizeDelegate()
92+
{
93+
if (updateStackSizeMethod == null) return null;
94+
// Cannot use Delegate.CreateDelegate with a method from a non-public declaring type (RuntimeILGenerator).
95+
// Instead, wrap the call in a DynamicMethod with skipVisibility: true.
96+
var dynMethod = new DynamicMethod(string.Empty,
97+
typeof(void), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(ILGenerator), typeof(OpCode), typeof(int) },
98+
typeof(ExpressionCompiler), skipVisibility: true);
99+
var il = dynMethod.GetILGenerator();
100+
il.Emit(OpCodes.Ldarg_1); // ILGenerator (runtime type: DynamicILGenerator : RuntimeILGenerator)
101+
il.Emit(OpCodes.Ldarg_2); // OpCode
102+
il.Emit(OpCodes.Ldarg_3); // int stackchange
103+
il.Emit(OpCodes.Call, updateStackSizeMethod);
104+
il.Emit(OpCodes.Ret);
105+
return (Action<ILGenerator, OpCode, int>)dynMethod.CreateDelegate(
106+
typeof(Action<ILGenerator, OpCode, int>), ExpressionCompiler.EmptyArrayClosure);
107+
}
77108

78109
public static Func<int, int> Get_DynamicMethod_Emit_Hack()
79110
{
111+
if (mLengthFieldAccessor == null || mILStreamAccessor == null || updateStackSizeDelegate == null || getScopeTokens == null)
112+
return null;
113+
80114
var meth = MethodStatic1Arg;
81115
var paramCount = 1;
82116

@@ -128,6 +162,87 @@ public static Func<int, int> Get_DynamicMethod_Emit_Hack()
128162
return (Func<int, int>)dynMethod.CreateDelegate(typeof(Func<int, int>), ExpressionCompiler.EmptyArrayClosure);
129163
}
130164

165+
#if NET10_0_OR_GREATER
166+
// In .NET 10+, use UnsafeAccessorType to access the private fields of non-public types directly,
167+
// without the DynamicMethod-based delegation used in earlier .NET versions.
168+
// RuntimeILGenerator is the internal base class of DynamicILGenerator that holds the IL stream state.
169+
170+
/// <summary>Directly accesses m_length on RuntimeILGenerator via UnsafeAccessorType (NET10+).</summary>
171+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_length")]
172+
private static extern ref int GetMLength_Net10(
173+
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);
174+
175+
/// <summary>Directly accesses m_ILStream on RuntimeILGenerator via UnsafeAccessorType (NET10+).</summary>
176+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_ILStream")]
177+
private static extern ref byte[] GetMILStream_Net10(
178+
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il);
179+
180+
/// <summary>Directly calls UpdateStackSize on RuntimeILGenerator via UnsafeAccessorType (NET10+).</summary>
181+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "UpdateStackSize")]
182+
private static extern void UpdateStackSize_Net10(
183+
[UnsafeAccessorType("System.Reflection.Emit.RuntimeILGenerator")] object il,
184+
OpCode opcode, int stackchange);
185+
186+
/// <summary>Directly accesses m_tokens on DynamicScope via UnsafeAccessorType (NET10+).</summary>
187+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "m_tokens")]
188+
private static extern ref List<object> GetMTokens_Net10(
189+
[UnsafeAccessorType("System.Reflection.Emit.DynamicScope")] object scope);
190+
191+
public void DynamicMethod_Emit_Hack_Net10()
192+
{
193+
var f = Get_DynamicMethod_Emit_Hack_Net10();
194+
var a = f(41);
195+
Asserts.AreEqual(42, a);
196+
}
197+
198+
/// <summary>
199+
/// Demonstrates using UnsafeAccessorType (NET10+) to directly access private fields
200+
/// of non-public types (RuntimeILGenerator, DynamicScope) for fast IL emission.
201+
/// Replaces the DynamicMethod-based delegation approach used in earlier .NET versions.
202+
/// </summary>
203+
public static Func<int, int> Get_DynamicMethod_Emit_Hack_Net10()
204+
{
205+
var meth = MethodStatic1Arg;
206+
var paramCount = 1;
207+
208+
var dynMethod = new DynamicMethod(string.Empty,
209+
typeof(int), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(int) },
210+
typeof(ExpressionCompiler),
211+
skipVisibility: true);
212+
213+
var il = dynMethod.GetILGenerator(16);
214+
215+
// Use UnsafeAccessorType to get refs to the internal IL stream fields directly
216+
ref var mLength = ref GetMLength_Net10(il);
217+
ref var mILStream = ref GetMILStream_Net10(il);
218+
219+
// il.Emit(OpCodes.Ldarg_1);
220+
mILStream[mLength++] = (byte)OpCodes.Ldarg_1.Value;
221+
UpdateStackSize_Net10(il, OpCodes.Ldarg_1, 1);
222+
223+
// il.Emit(OpCodes.Call, meth);
224+
mILStream[mLength++] = (byte)OpCodes.Call.Value;
225+
UpdateStackSize_Net10(il, OpCodes.Call, CalcStackChange(meth, paramCount));
226+
227+
// Access m_scope via reflection (DynamicILGenerator.m_scope returns DynamicScope which is a non-public type,
228+
// so UnsafeAccessorType cannot currently be used for the return value).
229+
// Then use UnsafeAccessorType to access m_tokens on the DynamicScope instance directly.
230+
if (mScopeField == null) return null;
231+
var scope = mScopeField.GetValue(il);
232+
ref var mTokens = ref GetMTokens_Net10(scope);
233+
mTokens.Add(meth.MethodHandle);
234+
var token = mTokens.Count - 1 | (int)0x06000000; // MetadataTokenType.MethodDef
235+
BinaryPrimitives.WriteInt32LittleEndian(mILStream.AsSpan(mLength), token);
236+
mLength += 4;
237+
238+
// il.Emit(OpCodes.Ret);
239+
mILStream[mLength++] = (byte)OpCodes.Ret.Value;
240+
UpdateStackSize_Net10(il, OpCodes.Ret, 0);
241+
242+
return (Func<int, int>)dynMethod.CreateDelegate(typeof(Func<int, int>), ExpressionCompiler.EmptyArrayClosure);
243+
}
244+
#endif
245+
131246
[MethodImpl(MethodImplOptions.AggressiveInlining)]
132247
private static int CalcStackChange(MethodInfo meth, int paramCount)
133248
{
@@ -213,15 +328,15 @@ public static Func<A> Get_DynamicMethod_Hack_Emit_Newobj()
213328
// m_tokens.Add(rtConstructor.MethodHandle);
214329
// var tk = m_tokens.Count - 1 | (int)MetadataTokenType.MethodDef;
215330

216-
var mScopeField = ilType.GetField("m_scope", BindingFlags.Instance | BindingFlags.NonPublic);
217-
if (mScopeField == null)
331+
var scopeField = ilType.GetField("m_scope", BindingFlags.Instance | BindingFlags.NonPublic);
332+
if (scopeField == null)
218333
return null;
219-
var mScope = mScopeField.GetValue(il);
334+
var mScope = scopeField.GetValue(il);
220335

221-
var mTokensField = mScope.GetType().GetField("m_tokens", BindingFlags.Instance | BindingFlags.NonPublic);
222-
if (mTokensField == null)
336+
var tokensField = mScope.GetType().GetField("m_tokens", BindingFlags.Instance | BindingFlags.NonPublic);
337+
if (tokensField == null)
223338
return null;
224-
var mTokens = mTokensField.GetValue(mScope);
339+
var mTokens = tokensField.GetValue(mScope);
225340

226341

227342
il.Emit(OpCodes.Ret);

test/FastExpressionCompiler.IssueTests/FastExpressionCompiler.IssueTests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;net6.0;net8.0;net9.0</TargetFrameworks>
4-
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0</TargetFrameworks>
3+
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net472;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
4+
<TargetFrameworks Condition="'$(DevMode)' == 'true'">net472;net9.0;net10.0</TargetFrameworks>
55
</PropertyGroup>
66

77
<ItemGroup>

test/FastExpressionCompiler.TestsRunner/FastExpressionCompiler.TestsRunner.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net9.0;net8.0;net6.0</TargetFrameworks>
4-
<TargetFrameworks Condition="'$(DevMode)' == 'true' OR '$(Configuration)' == 'Debug'">net9.0</TargetFrameworks>
3+
<TargetFrameworks Condition="'$(DevMode)' != 'true'">net9.0;net8.0;net6.0;net10.0</TargetFrameworks>
4+
<TargetFrameworks Condition="'$(DevMode)' == 'true' OR '$(Configuration)' == 'Debug'">net9.0;net10.0</TargetFrameworks>
55

66
<OutputType>Exe</OutputType>
77
<IsTestProject>false</IsTestProject>

0 commit comments

Comments
 (0)