Conversione a c# + threads

This commit is contained in:
Maddo Scientisto 2021-02-25 11:14:44 +01:00
commit d133917283
24 changed files with 2649 additions and 642 deletions

27
MaddoShared/FileData.cs Normal file
View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaddoShared
{
public class FileData
{
/// <summary>
/// Il file originale
/// </summary>
public FileInfo File { get; set; }
/// <summary>
/// La cartella di destinazione
/// </summary>
public DirectoryInfo Directory { get; set; }
public FileData(FileInfo newFile, DirectoryInfo newDirectory)
{
this.File = newFile;
this.Directory = newDirectory;
}
}
}

View file

@ -0,0 +1,116 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaddoShared
{
public class FileHelperOptions
{
public bool SeparateFiles { get; set; }
public NumerazioneType NumerationType { get; set; }
public int CounterSize { get; set; }
public string Suffix { get; set; }
public int FilesPerFolder { get; set; }
}
public enum NumerazioneType
{
Progressiva,
Files
}
public class FileHelperSharp
{
public List<FileData> GetFilesRecursive(DirectoryInfo root, DirectoryInfo destRoot, string filter, FileHelperOptions options)
{
ConcurrentDictionary<FileInfo, DirectoryInfo> dirSourceDest = new ConcurrentDictionary<FileInfo, DirectoryInfo>();
List<FileInfo> result = new List<FileInfo>();
// Dim stack As New Stack(Of DirectoryInfo)
Stack<KeyValuePair<DirectoryInfo, DirectoryInfo>> stack = new Stack<KeyValuePair<DirectoryInfo, DirectoryInfo>>();
KeyValuePair<DirectoryInfo, DirectoryInfo> pair = new KeyValuePair<DirectoryInfo, DirectoryInfo>();
// stack.Push(root)
stack.Push(new KeyValuePair<DirectoryInfo, DirectoryInfo>(root, destRoot));
while ((stack.Count > 0))
{
KeyValuePair<DirectoryInfo, DirectoryInfo> curDirKV = stack.Pop();
// curDirKP = stack.Pop()
DirectoryInfo dir = curDirKV.Key;
DirectoryInfo dDir = curDirKV.Value;
try
{
// result.AddRange(dir.GetFiles(filter, SearchOption.TopDirectoryOnly))
// dividere file qui
if (options.FilesPerFolder > 0 & options.SeparateFiles)
AppendDictionaryConcurrent(dirSourceDest, DividiFilesInDirConcurrent(dir, dDir, options, filter));
else
AppendDictionaryConcurrent(dirSourceDest, DividiFilesInDirConcurrent(dir, dDir, options, filter));
foreach (DirectoryInfo subDirectory in dir.GetDirectories())
stack.Push(new KeyValuePair<DirectoryInfo, DirectoryInfo>(subDirectory, new DirectoryInfo(Path.Combine(dDir.FullName, subDirectory.Name))));
}
catch (Exception ex)
{
// TODO ERROR
}
}
List<FileData> resultData = new List<FileData>();
resultData.AddRange(from p in dirSourceDest
select new FileData(p.Key, p.Value));
return resultData;
}
public ConcurrentDictionary<FileInfo, DirectoryInfo> AppendDictionaryConcurrent(ConcurrentDictionary<FileInfo, DirectoryInfo> dictA, ConcurrentDictionary<FileInfo, DirectoryInfo> dictB)
{
foreach (KeyValuePair<FileInfo, DirectoryInfo> pair in dictB)
dictA.TryAdd(pair.Key, pair.Value);
return dictA;
}
private ConcurrentDictionary<FileInfo, DirectoryInfo> DividiFilesInDirConcurrent(DirectoryInfo dir, DirectoryInfo dirDest, FileHelperOptions options, string filter)
{
//int filesCount = dir.GetFiles(Filter).Length;
int contaFilePerDir = 0;
int contaDirPerDir = 0;
string tempText;// = string.Empty;
ConcurrentDictionary<FileInfo, DirectoryInfo> foldersDict = new ConcurrentDictionary<FileInfo, DirectoryInfo>();
DirectoryInfo destDir;
destDir = new DirectoryInfo(Path.Combine(dirDest.FullName));
foreach (FileInfo file in dir.GetFiles(filter))
{
contaFilePerDir += 1;
if (contaFilePerDir == (contaDirPerDir * options.FilesPerFolder) + 1)
{
contaDirPerDir += 1;
tempText = options.NumerationType == NumerazioneType.Progressiva ? contaDirPerDir.ToString() : (contaDirPerDir * options.FilesPerFolder).ToString();
int i;
for (i = 1; i <= (options.CounterSize - tempText.Length); i++)
tempText = "0" + tempText;
destDir = new DirectoryInfo(Path.Combine(dirDest.FullName, options.Suffix + tempText));
}
if (!destDir.Exists)
destDir.Create();
foldersDict.TryAdd(file, destDir);
}
return foldersDict;
}
}
}

View file

@ -0,0 +1,200 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CatalogVbLib;
using Dasync.Collections;
namespace MaddoShared
{
public class ImageCreationStuff
{
public class Options
{
public bool AggiornaSottodirectory { get; set; }
public bool CreaSottocartelle { get; set; }
public int FilePerCartella { get; set; }
public string SuffissoCartelle { get; set; }
public int CifreContatore { get; set; }
public NumerazioneType NumerazioneType { get; set; }
public string SourcePath { get; set; }
public string DestinationPath { get; set; }
public int MaxThreads { get; set; }
public int ChunksSize { get; set; }
public bool LinearExecution { get; set; }
}
public async Task<string> CreaCatalogoParallel(Options options)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
// todo immagini counter
//todo set label
await CreaImmaginiParallel(options);
// todo set finito label
stopwatch.Stop();
return $"{stopwatch.Elapsed.Hours}h {stopwatch.Elapsed.Minutes}m ${stopwatch.Elapsed.Seconds}s ({stopwatch.Elapsed.TotalSeconds}s)";
}
public async Task CreaImmaginiParallel(Options options)
{
var dataToProcess = new List<FileData>();
if (options.AggiornaSottodirectory && options.CreaSottocartelle)
{
var helper = new FileHelperSharp();
dataToProcess = helper.GetFilesRecursive(new DirectoryInfo(options.SourcePath), new DirectoryInfo(options.DestinationPath),
"*.jpg", new FileHelperOptions()
{
FilesPerFolder = options.FilePerCartella,
Suffix = options.SuffissoCartelle,
CounterSize = options.CifreContatore,
NumerationType = options.NumerazioneType
});
}
else if (!options.CreaSottocartelle)
{
var files = Directory.EnumerateFiles(options.SourcePath, "*.jpg",
options.AggiornaSottodirectory
? SearchOption.AllDirectories
: SearchOption.TopDirectoryOnly);
dataToProcess = files.Select(x =>
{
var fInfo = new FileInfo(x);
var filePath = fInfo.DirectoryName;
var trimmedSourcePath = options.SourcePath.TrimEnd('\\');
var newFilePath = fInfo.FullName.Replace(trimmedSourcePath, "").TrimStart('\\');
newFilePath = Path.Combine(options.DestinationPath, newFilePath);
var destFolderPath = new FileInfo(newFilePath).DirectoryName;
var destFolderInfo = new DirectoryInfo(destFolderPath);
destFolderInfo.EnsureDirectoryExists();
return new FileData(fInfo, new DirectoryInfo(new FileInfo(newFilePath).DirectoryName));
}).ToList();
//// TODO
//dataToProcess =
// (from f in Directory.EnumerateFiles(options.SourcePath, "*.jpg",
// options.AggiornaSottodirectory
// ? SearchOption.AllDirectories
// : SearchOption.TopDirectoryOnly)
// select new FileData(new FileInfo(f),
// new DirectoryInfo(options.DestinationPath.PathCombine(
// new FileInfo(f).DirectoryName.Replace(options.SourcePath.TrimEnd(new char[] {'\\'}), "")
// )
// )
// )
// )
// .ToList();
}
var threads = options.MaxThreads == 0 ? Environment.ProcessorCount * 2 : options.MaxThreads;
var scheduler = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, threads)
.ConcurrentScheduler;
//var allTasks = new List<Task>();
var test = from d in dataToProcess
select Task.Factory.StartNew(async () =>
{
await new ImageCreatorSharp(d.File, d.Directory).CreaImmagineThread(d.File.Name);
//var imgC = new ImageCreatorSharp(d.File, d.Directory);
//imgC.CreaImmagineThread(d.File.Name);
//imgC = null;
}, CancellationToken.None, TaskCreationOptions.None, scheduler);
if (options.LinearExecution)
{
foreach (var task in test)
{
await task;
}
}
else
{
if (options.ChunksSize == 0)
{
var opts = new ParallelOptions() { MaxDegreeOfParallelism = threads };
await dataToProcess.ParallelForEachAsync(async fileData =>
{
await new ImageCreatorSharp(fileData.File, fileData.Directory).CreaImmagineThread(fileData.File.Name);
}, maxDegreeOfParallelism: threads);
//var throttler = new SemaphoreSlim(initialCount: threads);
//foreach (var fileData in dataToProcess)
//{
// await throttler.WaitAsync();
// allTasks.Add(Task.Factory.StartNew(() => {
// try
// {
// new ImageCreatorSharp(fileData.File, fileData.Directory).CreaImmagineThread(
// fileData.File.Name);
// }
// finally
// {
// throttler.Release();
// }
// }, CancellationToken.None, TaskCreationOptions.None, scheduler));
//}
//await Task.WhenAll(test);
}
else
{
var asdf = SplitList(dataToProcess.ToList(), dataToProcess.Count() / options.ChunksSize).ToList();
//var sadf = asdf[0];
//var sadf1 = asdf[1];
foreach (var sdaf in asdf)
{
await sdaf.ParallelForEachAsync(async fileData =>
{
await new ImageCreatorSharp(fileData.File, fileData.Directory).CreaImmagineThread(fileData.File.Name);
}, maxDegreeOfParallelism: threads);
}
//foreach (var chunk in asdf)
//{
// await Task.WhenAll(chunk);
// GC.Collect();
// //GC.WaitForPendingFinalizers();
// //GC.Collect();
//}
}
}
//foreach (var task in test)
//{
// await task;
//}
}
public static IEnumerable<List<T>> SplitList<T>(List<T> bigList, int nSize = 3)
{
for (int i = 0; i < bigList.Count; i += nSize)
{
yield return bigList.GetRange(i, Math.Min(nSize, bigList.Count - i));
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{AEBFE9E3-277C-4A7B-8448-145D1B11998B}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MaddoShared</RootNamespace>
<AssemblyName>MaddoShared</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="AsyncEnumerable, Version=4.0.2.0, Culture=neutral, PublicKeyToken=0426b068161bd1d1, processorArchitecture=MSIL">
<HintPath>..\packages\AsyncEnumerator.4.0.2\lib\net461\AsyncEnumerable.dll</HintPath>
</Reference>
<Reference Include="Ben.Demystifier, Version=0.3.0.0, Culture=neutral, PublicKeyToken=a6d206e05440431a, processorArchitecture=MSIL">
<HintPath>..\packages\Ben.Demystifier.0.3.0\lib\net45\Ben.Demystifier.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=5.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.5.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
</Reference>
<Reference Include="SixLabors.ImageSharp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d998eea7b14cab13, processorArchitecture=MSIL">
<HintPath>..\packages\SixLabors.ImageSharp.1.0.3\lib\net472\SixLabors.ImageSharp.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Collections.Immutable.5.0.0\lib\net461\System.Collections.Immutable.dll</HintPath>
</Reference>
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Metadata, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Reflection.Metadata.5.0.0\lib\net461\System.Reflection.Metadata.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="Z.ExtensionMethods, Version=2.1.1.0, Culture=neutral, PublicKeyToken=59b66d028979105b, processorArchitecture=MSIL">
<HintPath>..\packages\Z.ExtensionMethods.2.1.1\lib\net45\Z.ExtensionMethods.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="ImageCreationStuff.cs" />
<Compile Include="ImageCreatorSharp.cs" />
<Compile Include="ThreadingHelper.cs" />
<Compile Include="FileData.cs" />
<Compile Include="FileHelperSharp.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CatalogVbLib\CatalogVbLib.vbproj">
<Project>{44465926-240d-473f-90b8-786ba4384406}</Project>
<Name>CatalogVbLib</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View file

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MaddoShared")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MaddoShared")]
[assembly: AssemblyCopyright("Copyright © 2021")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("aebfe9e3-277c-4a7b-8448-145d1b11998b")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View file

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MaddoShared
{
public static class ThreadingHelper
{
/// <summary>
/// Starts the given tasks and waits for them to complete. This will run, at most, the specified number of tasks in parallel.
/// <para>NOTE: If one of the given tasks has already been started, an exception will be thrown.</para>
/// </summary>
/// <param name="tasksToRun">The tasks to run.</param>
/// <param name="maxTasksToRunInParallel">The maximum number of tasks to run in parallel.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public static void StartAndWaitAllThrottled(IEnumerable<Task> tasksToRun, int maxTasksToRunInParallel, CancellationToken cancellationToken = new CancellationToken())
{
StartAndWaitAllThrottled(tasksToRun, maxTasksToRunInParallel, -1, cancellationToken);
}
/// <summary>
/// Starts the given tasks and waits for them to complete. This will run, at most, the specified number of tasks in parallel.
/// <para>NOTE: If one of the given tasks has already been started, an exception will be thrown.</para>
/// </summary>
/// <param name="tasksToRun">The tasks to run.</param>
/// <param name="maxTasksToRunInParallel">The maximum number of tasks to run in parallel.</param>
/// <param name="timeoutInMilliseconds">The maximum milliseconds we should allow the max tasks to run in parallel before allowing another task to start. Specify -1 to wait indefinitely.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public static void StartAndWaitAllThrottled(IEnumerable<Task> tasksToRun, int maxTasksToRunInParallel, int timeoutInMilliseconds, CancellationToken cancellationToken = new CancellationToken())
{
// Convert to a list of tasks so that we don&#39;t enumerate over it multiple times needlessly.
var tasks = tasksToRun.ToList();
using (var throttler = new SemaphoreSlim(maxTasksToRunInParallel))
{
var postTaskTasks = new List<Task>();
// Have each task notify the throttler when it completes so that it decrements the number of tasks currently running.
tasks.ForEach(t => postTaskTasks.Add(t.ContinueWith(tsk => throttler.Release())));
// Start running each task.
foreach (var task in tasks)
{
// Increment the number of tasks currently running and wait if too many are running.
throttler.Wait(timeoutInMilliseconds, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
task.Start();
}
// Wait for all of the provided tasks to complete.
// We wait on the list of "post" tasks instead of the original tasks, otherwise there is a potential race condition where the throttler&#39;s using block is exited before some Tasks have had their "post" action completed, which references the throttler, resulting in an exception due to accessing a disposed object.
Task.WaitAll(postTaskTasks.ToArray(), cancellationToken);
}
}
}
}

19
MaddoShared/app.config Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.2.0.1" newVersion="4.2.0.1" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="AsyncEnumerator" version="4.0.2" targetFramework="net472" />
<package id="Ben.Demystifier" version="0.3.0" targetFramework="net472" />
<package id="Microsoft.Bcl.AsyncInterfaces" version="5.0.0" targetFramework="net472" />
<package id="SixLabors.ImageSharp" version="1.0.3" targetFramework="net472" />
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
<package id="System.Collections.Immutable" version="5.0.0" targetFramework="net472" />
<package id="System.Memory" version="4.5.4" targetFramework="net472" />
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
<package id="System.Reflection.Metadata" version="5.0.0" targetFramework="net472" />
<package id="System.Runtime.CompilerServices.Unsafe" version="5.0.0" targetFramework="net472" />
<package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net472" />
<package id="Z.ExtensionMethods" version="2.1.1" targetFramework="net472" />
</packages>