Skip to content

Commit 4e0f4c2

Browse files
authored
fix: correct VS MSBuild (#29)
* test: add critical regression tests for build package behavior and model generation * feat: add compatibility layer for .NET Framework support This update introduces a compatibility layer for .NET Framework, allowing the project to utilize polyfills for APIs not available in .NET Framework 4.7.2. The changes include conditional compilation directives and the addition of helper methods to ensure compatibility across different target frameworks. * feat: enhance process execution with timeout handling and SQLite initialization - Added timeout handling for process execution to prevent indefinite waits. - Introduced SQLitePCL initialization for Microsoft.Data.Sqlite tests. - Updated project dependencies to include SQLitePCLRaw for SQLite support.
1 parent d14d787 commit 4e0f4c2

31 files changed

Lines changed: 1679 additions & 177 deletions
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#if NETFRAMEWORK
2+
using System.Collections.Generic;
3+
using System.Runtime.InteropServices;
4+
using System.Text;
5+
6+
namespace JD.Efcpt.Build.Tasks.Compatibility;
7+
8+
/// <summary>
9+
/// Provides polyfills for APIs not available in .NET Framework 4.7.2.
10+
/// </summary>
11+
internal static class NetFrameworkPolyfills
12+
{
13+
/// <summary>
14+
/// Throws ArgumentNullException if argument is null.
15+
/// Polyfill for ArgumentNullException.ThrowIfNull (introduced in .NET 6).
16+
/// </summary>
17+
public static void ThrowIfNull(object argument, string paramName = null)
18+
{
19+
if (argument is null)
20+
throw new ArgumentNullException(paramName);
21+
}
22+
23+
/// <summary>
24+
/// Throws ArgumentException if argument is null or whitespace.
25+
/// Polyfill for ArgumentException.ThrowIfNullOrWhiteSpace (introduced in .NET 7).
26+
/// </summary>
27+
public static void ThrowIfNullOrWhiteSpace(string argument, string paramName = null)
28+
{
29+
if (string.IsNullOrWhiteSpace(argument))
30+
throw new ArgumentException("Value cannot be null or whitespace.", paramName);
31+
}
32+
33+
/// <summary>
34+
/// Gets a relative path from one path to another.
35+
/// Polyfill for Path.GetRelativePath (introduced in .NET Standard 2.1).
36+
/// </summary>
37+
public static string GetRelativePath(string relativeTo, string path)
38+
{
39+
if (string.IsNullOrEmpty(relativeTo))
40+
throw new ArgumentNullException(nameof(relativeTo));
41+
if (string.IsNullOrEmpty(path))
42+
throw new ArgumentNullException(nameof(path));
43+
44+
relativeTo = Path.GetFullPath(relativeTo);
45+
path = Path.GetFullPath(path);
46+
47+
// Ensure relativeTo ends with directory separator
48+
if (!relativeTo.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
49+
!relativeTo.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
50+
{
51+
relativeTo += Path.DirectorySeparatorChar;
52+
}
53+
54+
var relativeToUri = new Uri(relativeTo);
55+
var pathUri = new Uri(path);
56+
57+
if (relativeToUri.Scheme != pathUri.Scheme)
58+
return path;
59+
60+
var relativeUri = relativeToUri.MakeRelativeUri(pathUri);
61+
var relativePath = Uri.UnescapeDataString(relativeUri.ToString());
62+
63+
if (string.Equals(pathUri.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
64+
{
65+
relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
66+
}
67+
68+
return relativePath;
69+
}
70+
71+
/// <summary>
72+
/// Converts byte array to hex string.
73+
/// Polyfill for Convert.ToHexString (introduced in .NET 5).
74+
/// </summary>
75+
public static string ToHexString(byte[] bytes)
76+
{
77+
var sb = new StringBuilder(bytes.Length * 2);
78+
foreach (var b in bytes)
79+
sb.Append(b.ToString("X2"));
80+
return sb.ToString();
81+
}
82+
}
83+
84+
/// <summary>
85+
/// Polyfill for OperatingSystem static methods (introduced in .NET 5).
86+
/// </summary>
87+
internal static class OperatingSystemPolyfill
88+
{
89+
public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
90+
public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
91+
public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
92+
}
93+
94+
/// <summary>
95+
/// Extension methods for KeyValuePair deconstruction (not available in .NET Framework).
96+
/// </summary>
97+
internal static class KeyValuePairExtensions
98+
{
99+
public static void Deconstruct<TKey, TValue>(
100+
this KeyValuePair<TKey, TValue> kvp,
101+
out TKey key,
102+
out TValue value)
103+
{
104+
key = kvp.Key;
105+
value = kvp.Value;
106+
}
107+
}
108+
109+
/// <summary>
110+
/// Extension methods for string operations not available in .NET Framework.
111+
/// </summary>
112+
internal static class StringPolyfillExtensions
113+
{
114+
/// <summary>
115+
/// Splits a string using StringSplitOptions.
116+
/// Polyfill for string.Split(char, StringSplitOptions) overload.
117+
/// </summary>
118+
public static string[] Split(this string str, char separator, StringSplitOptions options)
119+
{
120+
return str.Split(new[] { separator }, options);
121+
}
122+
}
123+
#endif

src/JD.Efcpt.Build.Tasks/ComputeFingerprint.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
using JD.Efcpt.Build.Tasks.Extensions;
55
using Microsoft.Build.Framework;
66
using Task = Microsoft.Build.Utilities.Task;
7+
#if NETFRAMEWORK
8+
using JD.Efcpt.Build.Tasks.Compatibility;
9+
#endif
710

811
namespace JD.Efcpt.Build.Tasks;
912

@@ -181,7 +184,11 @@ private bool ExecuteCore(TaskExecutionContext ctx)
181184
.Select(p => p.Replace('\u005C', '/'))
182185
.OrderBy(p => p, StringComparer.Ordinal)
183186
.Select(file => (
187+
#if NETFRAMEWORK
188+
rel: NetFrameworkPolyfills.GetRelativePath(TemplateDir, file).Replace('\u005C', '/'),
189+
#else
184190
rel: Path.GetRelativePath(TemplateDir, file).Replace('\u005C', '/'),
191+
#endif
185192
h: FileHash.HashFile(file)))
186193
.Aggregate(manifest, (builder, data)
187194
=> builder.Append("template/")
@@ -197,7 +204,11 @@ private bool ExecuteCore(TaskExecutionContext ctx)
197204
.Select(p => p.Replace('\u005C', '/'))
198205
.OrderBy(p => p, StringComparer.Ordinal)
199206
.Select(file => (
207+
#if NETFRAMEWORK
208+
rel: NetFrameworkPolyfills.GetRelativePath(GeneratedDir, file).Replace('\u005C', '/'),
209+
#else
200210
rel: Path.GetRelativePath(GeneratedDir, file).Replace('\u005C', '/'),
211+
#endif
201212
h: FileHash.HashFile(file)))
202213
.Aggregate(manifest, (builder, data)
203214
=> builder.Append("generated/")

src/JD.Efcpt.Build.Tasks/DacpacFingerprint.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ namespace JD.Efcpt.Build.Tasks;
2626
/// The implementation is based on the approach from ErikEJ/DacDeploySkip.
2727
/// </para>
2828
/// </remarks>
29+
#if NET7_0_OR_GREATER
2930
internal static partial class DacpacFingerprint
31+
#else
32+
internal static class DacpacFingerprint
33+
#endif
3034
{
3135
private const string ModelXmlEntry = "model.xml";
3236
private const string PreDeployEntry = "predeploy.sql";
@@ -144,7 +148,8 @@ private static byte[] ReadEntryBytes(ZipArchiveEntry entry)
144148
"AssemblySymbolsName" => AssemblySymbolsMetadataRegex(),
145149
_ => new Regex($"""(<Metadata\s+Name="{metadataName}"\s+Value=")([^"]+)(")""", RegexOptions.Compiled)
146150
};
147-
151+
152+
#if NET7_0_OR_GREATER
148153
/// <summary>
149154
/// Regex for matching Metadata elements with specific Name attributes.
150155
/// </summary>
@@ -153,5 +158,12 @@ private static byte[] ReadEntryBytes(ZipArchiveEntry entry)
153158

154159
[GeneratedRegex("""(<Metadata\s+Name="AssemblySymbolsName"\s+Value=")([^"]+)(")""", RegexOptions.Compiled)]
155160
private static partial Regex AssemblySymbolsMetadataRegex();
156-
161+
#else
162+
private static readonly Regex _fileNameMetadataRegex = new(@"(<Metadata\s+Name=""FileName""\s+Value="")([^""]+)("")", RegexOptions.Compiled);
163+
private static Regex FileNameMetadataRegex() => _fileNameMetadataRegex;
164+
165+
private static readonly Regex _assemblySymbolsMetadataRegex = new(@"(<Metadata\s+Name=""AssemblySymbolsName""\s+Value="")([^""]+)("")", RegexOptions.Compiled);
166+
private static Regex AssemblySymbolsMetadataRegex() => _assemblySymbolsMetadataRegex;
167+
#endif
168+
157169
}

src/JD.Efcpt.Build.Tasks/DbContextNameGenerator.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ namespace JD.Efcpt.Build.Tasks;
2525
/// </list>
2626
/// </para>
2727
/// </remarks>
28+
#if NET7_0_OR_GREATER
2829
public static partial class DbContextNameGenerator
30+
#else
31+
public static class DbContextNameGenerator
32+
#endif
2933
{
3034
private const string DefaultContextName = "MyDbContext";
3135
private const string ContextSuffix = "Context";
@@ -205,7 +209,7 @@ private static string HumanizeName(string rawName)
205209
return DefaultContextName;
206210

207211
// Handle dotted namespaces (e.g., "Org.Unit.SystemData" → "SystemData")
208-
var dotParts = rawName.Split('.', StringSplitOptions.RemoveEmptyEntries);
212+
var dotParts = rawName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
209213
var baseName = dotParts.Length > 0 ? dotParts[^1] : rawName;
210214

211215
// Remove digits at the end (common in DACPAC names like "MyDb20251225.dacpac")
@@ -327,6 +331,7 @@ private static string ToPascalCase(string input)
327331
return null;
328332
}
329333

334+
#if NET7_0_OR_GREATER
330335
[GeneratedRegex(@"[^a-zA-Z]", RegexOptions.Compiled)]
331336
private static partial Regex NonLetterRegex();
332337

@@ -341,4 +346,20 @@ private static string ToPascalCase(string input)
341346

342347
[GeneratedRegex(@"Data\s+Source\s*=\s*(?<name>[^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
343348
private static partial Regex DataSourceKeywordRegex();
349+
#else
350+
private static readonly Regex _nonLetterRegex = new(@"[^a-zA-Z]", RegexOptions.Compiled);
351+
private static Regex NonLetterRegex() => _nonLetterRegex;
352+
353+
private static readonly Regex _trailingDigitsRegex = new(@"\d+$", RegexOptions.Compiled);
354+
private static Regex TrailingDigitsRegex() => _trailingDigitsRegex;
355+
356+
private static readonly Regex _databaseKeywordRegex = new(@"(?:Database|Db)\s*=\s*(?<name>[^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
357+
private static Regex DatabaseKeywordRegex() => _databaseKeywordRegex;
358+
359+
private static readonly Regex _initialCatalogKeywordRegex = new(@"Initial\s+Catalog\s*=\s*(?<name>[^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
360+
private static Regex InitialCatalogKeywordRegex() => _initialCatalogKeywordRegex;
361+
362+
private static readonly Regex _dataSourceKeywordRegex = new(@"Data\s+Source\s*=\s*(?<name>[^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
363+
private static Regex DataSourceKeywordRegex() => _dataSourceKeywordRegex;
364+
#endif
344365
}

src/JD.Efcpt.Build.Tasks/EnsureDacpacBuilt.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
using Microsoft.Build.Framework;
44
using PatternKit.Behavioral.Strategy;
55
using Task = Microsoft.Build.Utilities.Task;
6+
#if NETFRAMEWORK
7+
using JD.Efcpt.Build.Tasks.Compatibility;
8+
#endif
69

710
namespace JD.Efcpt.Build.Tasks;
811

@@ -260,7 +263,7 @@ private void WriteFakeDacpac(BuildLog log, string sqlproj)
260263

261264
#region Helper Methods
262265

263-
private static readonly IReadOnlySet<string> ExcludedDirs = new HashSet<string>(
266+
private static readonly HashSet<string> ExcludedDirs = new HashSet<string>(
264267
["bin", "obj"],
265268
StringComparer.OrdinalIgnoreCase);
266269

@@ -286,7 +289,11 @@ private static DateTime LatestSourceWrite(string sqlproj)
286289

287290
private static bool IsUnderExcludedDir(string filePath, string root)
288291
{
292+
#if NETFRAMEWORK
293+
var relativePath = NetFrameworkPolyfills.GetRelativePath(root, filePath);
294+
#else
289295
var relativePath = Path.GetRelativePath(root, filePath);
296+
#endif
290297
var segments = relativePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
291298

292299
return segments.Any(segment => ExcludedDirs.Contains(segment));

src/JD.Efcpt.Build.Tasks/Extensions/DataRowExtensions.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System.Data;
2+
#if NETFRAMEWORK
3+
using JD.Efcpt.Build.Tasks.Compatibility;
4+
#endif
25

36
namespace JD.Efcpt.Build.Tasks.Extensions;
47

@@ -14,8 +17,12 @@ public static class DataRowExtensions
1417
/// </summary>
1518
public static string GetString(this DataRow row, string columnName)
1619
{
20+
#if NETFRAMEWORK
21+
NetFrameworkPolyfills.ThrowIfNull(row, nameof(row));
22+
#else
1723
ArgumentNullException.ThrowIfNull(row);
18-
24+
#endif
25+
1926
if (string.IsNullOrWhiteSpace(columnName)) throw new ArgumentException("Column name is required.", nameof(columnName));
2027

2128
if (!row.Table.Columns.Contains(columnName))

src/JD.Efcpt.Build.Tasks/FileSystemHelpers.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
#if NETFRAMEWORK
2+
using JD.Efcpt.Build.Tasks.Compatibility;
3+
#endif
4+
15
namespace JD.Efcpt.Build.Tasks;
26

37
/// <summary>
@@ -25,8 +29,13 @@ internal static class FileSystemHelpers
2529
/// </remarks>
2630
public static void CopyDirectory(string sourceDir, string destDir, bool overwrite = true)
2731
{
32+
#if NETFRAMEWORK
33+
NetFrameworkPolyfills.ThrowIfNull(sourceDir, nameof(sourceDir));
34+
NetFrameworkPolyfills.ThrowIfNull(destDir, nameof(destDir));
35+
#else
2836
ArgumentNullException.ThrowIfNull(sourceDir);
2937
ArgumentNullException.ThrowIfNull(destDir);
38+
#endif
3039

3140
if (!Directory.Exists(sourceDir))
3241
throw new DirectoryNotFoundException($"Source directory not found: {sourceDir}");
@@ -38,14 +47,22 @@ public static void CopyDirectory(string sourceDir, string destDir, bool overwrit
3847

3948
// Create all subdirectories first using LINQ projection for clarity
4049
var destDirs = Directory.EnumerateDirectories(sourceDir, "*", SearchOption.AllDirectories)
50+
#if NETFRAMEWORK
51+
.Select(dir => Path.Combine(destDir, NetFrameworkPolyfills.GetRelativePath(sourceDir, dir)));
52+
#else
4153
.Select(dir => Path.Combine(destDir, Path.GetRelativePath(sourceDir, dir)));
54+
#endif
4255

4356
foreach (var dir in destDirs)
4457
Directory.CreateDirectory(dir);
4558

4659
// Copy all files using LINQ projection for clarity
4760
var fileMappings = Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories)
61+
#if NETFRAMEWORK
62+
.Select(file => (Source: file, Dest: Path.Combine(destDir, NetFrameworkPolyfills.GetRelativePath(sourceDir, file))));
63+
#else
4864
.Select(file => (Source: file, Dest: Path.Combine(destDir, Path.GetRelativePath(sourceDir, file))));
65+
#endif
4966

5067
foreach (var (source, dest) in fileMappings)
5168
{

0 commit comments

Comments
 (0)