diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config
new file mode 100644
index 0000000..67f8ea0
--- /dev/null
+++ b/.nuget/NuGet.Config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets
new file mode 100644
index 0000000..3f8c37b
--- /dev/null
+++ b/.nuget/NuGet.targets
@@ -0,0 +1,144 @@
+
+
+
+ $(MSBuildProjectDirectory)\..\
+
+
+ false
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+ $([System.IO.Path]::Combine($(SolutionDir), ".nuget"))
+
+
+
+
+ $(SolutionDir).nuget
+
+
+
+ $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config
+ $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config
+
+
+
+ $(MSBuildProjectDirectory)\packages.config
+ $(PackagesProjectConfig)
+
+
+
+
+ $(NuGetToolsPath)\NuGet.exe
+ @(PackageSource)
+
+ "$(NuGetExePath)"
+ mono --runtime=v4.0.30319 "$(NuGetExePath)"
+
+ $(TargetDir.Trim('\\'))
+
+ -RequireConsent
+ -NonInteractive
+
+ "$(SolutionDir) "
+ "$(SolutionDir)"
+
+
+ $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)
+ $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols
+
+
+
+ RestorePackages;
+ $(BuildDependsOn);
+
+
+
+
+ $(BuildDependsOn);
+ BuildPackage;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BinaryDad.Extensions.sln b/BinaryDad.Extensions.sln
new file mode 100644
index 0000000..ad504bd
--- /dev/null
+++ b/BinaryDad.Extensions.sln
@@ -0,0 +1,34 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29123.88
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDad.Extensions", "BinaryDad.Extensions\BinaryDad.Extensions.csproj", "{2676B147-0E5A-4161-88B6-6EAFE814B769}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2676B147-0E5A-4161-88B6-6EAFE814B769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2676B147-0E5A-4161-88B6-6EAFE814B769}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2676B147-0E5A-4161-88B6-6EAFE814B769}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2676B147-0E5A-4161-88B6-6EAFE814B769}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {8B0AB60B-3317-435D-9945-0EE5492D5141}
+ EndGlobalSection
+ GlobalSection(TeamFoundationVersionControl) = preSolution
+ SccNumberOfProjects = 2
+ SccEnterpriseProvider = {4CA58AB2-18FA-4F8D-95D4-32DDF27D184C}
+ SccTeamFoundationServer = http://tfs.binarydad.com:8080/tfs/defaultcollection
+ SccLocalPath0 = .
+ SccProjectUniqueName1 = BinaryDad.Extensions\\BinaryDad.Extensions.csproj
+ SccProjectName1 = BinaryDad.Extensions
+ SccLocalPath1 = BinaryDad.Extensions
+ EndGlobalSection
+EndGlobal
diff --git a/BinaryDad.Extensions/Annotations/CommandFlagAttribute.cs b/BinaryDad.Extensions/Annotations/CommandFlagAttribute.cs
new file mode 100644
index 0000000..b8e7978
--- /dev/null
+++ b/BinaryDad.Extensions/Annotations/CommandFlagAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace BinaryDad.Extensions
+{
+ ///
+ /// Binds data from command line flags to attached property. In the example, [("context")] public string Context { get; set; }, a command of "command.exe -context Production" will set Context property equal to "Production".
+ ///
+ public class CommandFlagAttribute : Attribute
+ {
+ public string Flag { get; }
+
+ ///
+ /// The name of the flag, without the preceding dash (-)
+ ///
+ ///
+ public CommandFlagAttribute(string flag)
+ {
+ Flag = flag;
+ }
+ }
+}
diff --git a/BinaryDad.Extensions/Annotations/DataRowPopulateAttribute.cs b/BinaryDad.Extensions/Annotations/DataRowPopulateAttribute.cs
new file mode 100644
index 0000000..720f7dd
--- /dev/null
+++ b/BinaryDad.Extensions/Annotations/DataRowPopulateAttribute.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace BinaryDad.Extensions
+{
+ ///
+ /// Allows for a complex property to be populated via ToType().
+ ///
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+ public sealed class DataRowPopulateAttribute : Attribute { }
+}
\ No newline at end of file
diff --git a/BinaryDad.Extensions/Annotations/EnumAliasAttribute.cs b/BinaryDad.Extensions/Annotations/EnumAliasAttribute.cs
new file mode 100644
index 0000000..8d33435
--- /dev/null
+++ b/BinaryDad.Extensions/Annotations/EnumAliasAttribute.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+
+namespace BinaryDad.Extensions
+{
+ ///
+ /// Allows for additional metadata to be applied to Enum values
+ ///
+ [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
+ public sealed class EnumAliasAttribute : Attribute
+ {
+ public IEnumerable EnumAliasNames { get; private set; }
+
+ public EnumAliasAttribute(params string[] ids)
+ {
+ EnumAliasNames = ids;
+ }
+ }
+}
\ No newline at end of file
diff --git a/BinaryDad.Extensions/BinaryDad.Extensions.csproj b/BinaryDad.Extensions/BinaryDad.Extensions.csproj
new file mode 100644
index 0000000..57b8c67
--- /dev/null
+++ b/BinaryDad.Extensions/BinaryDad.Extensions.csproj
@@ -0,0 +1,22 @@
+
+
+
+ SAK
+ SAK
+ SAK
+ SAK
+
+
+
+ netstandard2.0
+ A bunch of common extensions
+ 7.3
+
+
+
+
+
+
+
+
+
diff --git a/BinaryDad.Extensions/BinaryDad.Extensions.nuspec b/BinaryDad.Extensions/BinaryDad.Extensions.nuspec
new file mode 100644
index 0000000..a16545f
--- /dev/null
+++ b/BinaryDad.Extensions/BinaryDad.Extensions.nuspec
@@ -0,0 +1,17 @@
+
+
+
+ BinaryDad.Extensions
+ 1
+ Ryan Peters
+ Ryan Peters
+ false
+
+ A set of common utilities and extension methods for .NET..
+
+ Check TFS history for changes since previous release.
+
+
+
+
+
\ No newline at end of file
diff --git a/BinaryDad.Extensions/CacheHelper.cs b/BinaryDad.Extensions/CacheHelper.cs
new file mode 100644
index 0000000..d162dd0
--- /dev/null
+++ b/BinaryDad.Extensions/CacheHelper.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Runtime.Caching;
+using System.Threading.Tasks;
+
+namespace BinaryDad.Extensions
+{
+ ///
+ /// In-memory cache helper wrapping the
+ ///
+ public static class CacheHelper
+ {
+ private const int DefaultCacheDuration = 10; // minutes
+
+ #region Add
+
+ ///
+ /// Adds an item to the cache for a duration (in minutes)
+ ///
+ ///
+ ///
+ /// The length of the cache duration in minutes
+ /// Indicates whether the cache duration is sliding or absolute
+ public static void Add(string key, object value, int cacheDuration = DefaultCacheDuration, bool isSliding = false) => Add(key, value, GetCacheItemPolicy(cacheDuration, isSliding));
+
+ ///
+ /// Adds an item to the cache with an absolute expiration
+ ///
+ ///
+ ///
+ ///
+ public static void Add(string key, object value, DateTime absoluteExpiration) => Add(key, value, GetCacheItemPolicy(absoluteExpiration));
+
+ #endregion
+
+ #region Get
+
+ ///
+ /// Retrieves an object from the cache, and if not found, retrieves and sets from a source delegate
+ ///
+ ///
+ ///
+ ///
+ /// Amount of time in minutes to persist the cache if loaded from source
+ /// Indicates whether the cache duration is sliding or absolute
+ ///
+ public static T Get(string key, Func source, int cacheDuration = DefaultCacheDuration, bool isSliding = false) => Get(key, source, GetCacheItemPolicy(cacheDuration, isSliding));
+
+ ///
+ /// Retrieves an object from the cache, and if not found, retrieves and sets from a source delegate
+ ///
+ ///
+ ///
+ ///
+ /// Amount of time in minutes to persist the cache if loaded from source
+ /// Indicates whether the cache duration is sliding or absolute
+ ///
+ public static Task GetAsync(string key, Func> source, int cacheDuration = DefaultCacheDuration, bool isSliding = false) => GetAsync(key, source, GetCacheItemPolicy(cacheDuration, isSliding));
+
+ ///
+ /// Retrieves an object from the cache, and if not found, retrieves and sets from a source delegate
+ ///
+ ///
+ ///
+ ///
+ /// The date and time when the cache will invalidate
+ ///
+ public static T Get(string key, Func source, DateTime absoluteExpiration) => Get(key, source, GetCacheItemPolicy(absoluteExpiration));
+
+ ///
+ /// Retrieves an object from the cache, and if not found, retrieves and sets from a source delegate
+ ///
+ ///
+ ///
+ ///
+ /// The date and time when the cache will invalidate
+ ///
+ public static Task GetAsync(string key, Func> source, DateTime absoluteExpiration) => GetAsync(key, source, GetCacheItemPolicy(absoluteExpiration));
+
+ ///
+ /// Retrieves an object from the cache
+ ///
+ ///
+ ///
+ ///
+ public static T Get(string key) => MemoryCache.Default[key].To();
+
+ #endregion
+
+ ///
+ /// Removes an item from the cache
+ ///
+ ///
+ public static void Remove(string key) => MemoryCache.Default.Remove(key);
+
+ ///
+ /// Returns true if the value exists in the cache
+ ///
+ ///
+ ///
+ public static bool Exists(string key) => MemoryCache.Default[key] != null;
+
+ #region Private Methods
+
+ ///
+ /// Adds an item to the cache with a custom
+ ///
+ ///
+ ///
+ ///
+ private static void Add(string key, object value, CacheItemPolicy cacheItemPolicy)
+ {
+ Remove(key);
+
+ MemoryCache.Default.Add(key, value, cacheItemPolicy);
+ }
+
+ ///
+ /// Retrieves an object from the cache, and if not found, retrieves and sets from a source delegate
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static T Get(string key, Func source, CacheItemPolicy cacheItemPolicy)
+ {
+ if (Exists(key))
+ {
+ return Get(key);
+ }
+
+ // if the value does not exist in the cache, automatically add it and return it
+ var value = source.Invoke();
+
+ Add(key, value, cacheItemPolicy);
+
+ return value;
+ }
+
+ ///
+ /// Retrieves an object from the cache, and if not found, retrieves and sets from a source delegate
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static async Task GetAsync(string key, Func> source, CacheItemPolicy cacheItemPolicy)
+ {
+ if (Exists(key))
+ {
+ return Get(key);
+ }
+
+ // if the value does not exist in the cache, automatically add it and return it
+ var value = await source.Invoke();
+
+ Add(key, value, cacheItemPolicy);
+
+ return value;
+ }
+
+ ///
+ /// Creates a given an absolute expiration date/time
+ ///
+ ///
+ ///
+ private static CacheItemPolicy GetCacheItemPolicy(DateTime absoluteExpiration)
+ {
+ return new CacheItemPolicy
+ {
+ AbsoluteExpiration = absoluteExpiration
+ };
+ }
+
+ ///
+ /// Creates a given a cache duration (in minutes) and if the cache expiration is sliding
+ ///
+ ///
+ ///
+ ///
+ private static CacheItemPolicy GetCacheItemPolicy(int cacheDuration, bool isSliding)
+ {
+ if (isSliding)
+ {
+ return new CacheItemPolicy
+ {
+ SlidingExpiration = new TimeSpan(0, cacheDuration, 0)
+ };
+ }
+
+ return GetCacheItemPolicy(DateTime.Now.AddMinutes(cacheDuration));
+ }
+
+ #endregion
+ }
+}
diff --git a/BinaryDad.Extensions/ConsoleHelper.cs b/BinaryDad.Extensions/ConsoleHelper.cs
new file mode 100644
index 0000000..74d21d2
--- /dev/null
+++ b/BinaryDad.Extensions/ConsoleHelper.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Reflection;
+
+namespace BinaryDad.Extensions
+{
+ ///
+ /// A set of utilities to assist with Console applications
+ ///
+ public static class ConsoleHelper
+ {
+ ///
+ /// Parses command argument flags in the format "-flag1 value -flag2 value2"
+ ///
+ ///
+ /// A raw string of arguments
+ ///
+ public static T ParseCommandFlags(string args) where T : new()
+ {
+ return ParseCommandFlags(args.Split(' '));
+ }
+
+ ///
+ /// Parses command argument flags in the format "command.exe -flag1 value -flag2 value2"
+ ///
+ ///
+ /// Collection of arguments, typically from Program.Main(string[] args)
+ ///
+ public static T ParseCommandFlags(string[] args) where T : new()
+ {
+ // the new parameter instance
+ var parameters = new T();
+
+ parameters
+ .GetType()
+ .GetProperties()
+ .EmptyIfNull()
+ .ForEach(p =>
+ {
+ var commandFlagAttribute = p.GetCustomAttribute(true);
+
+ if (commandFlagAttribute != null)
+ {
+ var valueFlagIndex = args.IndexOf($"-{commandFlagAttribute.Flag}", StringComparer.OrdinalIgnoreCase);
+ var valueIndex = valueFlagIndex + 1;
+
+ // find the argument value in the list, convert to the desired type, and set the value
+ if (valueFlagIndex >= 0 && args.Length > valueIndex)
+ {
+ p.SetValue(parameters, args[valueIndex].To(p.PropertyType));
+ }
+ }
+ });
+
+ return parameters;
+ }
+ }
+}
diff --git a/BinaryDad.Extensions/CryptoHelper.cs b/BinaryDad.Extensions/CryptoHelper.cs
new file mode 100644
index 0000000..86a1393
--- /dev/null
+++ b/BinaryDad.Extensions/CryptoHelper.cs
@@ -0,0 +1,183 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace BinaryDad.Extensions
+{
+ ///
+ /// Set of cryptography helpers using AES and Rijndael cipher
+ ///
+ public static class CryptoHelper
+ {
+ // This constant is used to determine the keysize of the encryption algorithm in bits.
+ // We divide this by 8 within the code below to get the equivalent number of bytes.
+ private const int DefaultKeySize = 256;
+
+ // This constant determines the number of iterations for the password bytes generation function.
+ private const int DerivationIterations = 1000;
+
+ #region Encrypt
+
+ ///
+ /// Encrypts a string with pass key using AES (Rijndael cipher)
+ ///
+ ///
+ ///
+ ///
+ public static string Encrypt(string plainText, string passPhrase)
+ {
+ return Encrypt(plainText, passPhrase, DefaultKeySize);
+ }
+
+ ///
+ /// Encrypts a string with pass key using AES (Rijndael cipher)
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static string Encrypt(string plainText, string passPhrase, int keySize)
+ {
+ ValidateKeySize(keySize);
+
+ // Salt and IV is randomly generated each time, but is preprended to encrypted cipher text
+ // so that the same Salt and IV values can be used when decrypting.
+ var saltStringBytes = GenerateBitsOfRandomEntropy(keySize);
+ var ivStringBytes = GenerateBitsOfRandomEntropy(keySize);
+ var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
+
+ using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
+ {
+ var keyBytes = password.GetBytes(keySize / 8);
+
+ using (var symmetricKey = new RijndaelManaged())
+ {
+ symmetricKey.BlockSize = keySize;
+ symmetricKey.Mode = CipherMode.CBC;
+ symmetricKey.Padding = PaddingMode.PKCS7;
+
+ using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes))
+ {
+ using (var memoryStream = new MemoryStream())
+ {
+ using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
+ {
+ cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
+ cryptoStream.FlushFinalBlock();
+
+ // Create the final bytes as a concatenation of the random salt bytes, the random iv bytes and the cipher bytes.
+ var cipherTextBytes = saltStringBytes;
+
+ cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray();
+ cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray();
+
+ memoryStream.Close();
+ cryptoStream.Close();
+
+ return Convert.ToBase64String(cipherTextBytes);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ #region Decrypt
+
+ ///
+ /// Decrypts a string with pass key using AES (Rijndael cipher)
+ ///
+ ///
+ ///
+ ///
+ public static string Decrypt(string cipherText, string passPhrase)
+ {
+ return Decrypt(cipherText, passPhrase, DefaultKeySize);
+ }
+
+ ///
+ /// Decrypts a string with pass key using AES (Rijndael cipher)
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static string Decrypt(string cipherText, string passPhrase, int keySize)
+ {
+ ValidateKeySize(keySize);
+
+ // Get the complete stream of bytes that represent:
+ // [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText]
+ var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText);
+
+ // Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes.
+ var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(keySize / 8).ToArray();
+
+ // Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes.
+ var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(keySize / 8).Take(keySize / 8).ToArray();
+
+ // Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string.
+ var cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip((keySize / 8) * 2).Take(cipherTextBytesWithSaltAndIv.Length - ((keySize / 8) * 2)).ToArray();
+
+ using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
+ {
+ var keyBytes = password.GetBytes(keySize / 8);
+
+ using (var symmetricKey = new RijndaelManaged())
+ {
+ symmetricKey.BlockSize = keySize;
+ symmetricKey.Mode = CipherMode.CBC;
+ symmetricKey.Padding = PaddingMode.PKCS7;
+
+ using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes))
+ {
+ using (var memoryStream = new MemoryStream(cipherTextBytes))
+ {
+ using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
+ {
+ var plainTextBytes = new byte[cipherTextBytes.Length];
+ var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
+
+ memoryStream.Close();
+ cryptoStream.Close();
+
+ return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static byte[] GenerateBitsOfRandomEntropy(int keySize)
+ {
+ var randomBytes = new byte[keySize / 8];
+
+ using (var rngCsp = new RNGCryptoServiceProvider())
+ {
+ // Fill the array with cryptographically secure random bytes.
+ rngCsp.GetBytes(randomBytes);
+ }
+
+ return randomBytes;
+ }
+
+ private static void ValidateKeySize(int keySize)
+ {
+ if (keySize % 8 != 0)
+ {
+ throw new ArgumentException("Key size must be a multiple of 8", nameof(keySize));
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/BinaryDad.Extensions/Exceptions/DataPropertyConversionException.cs b/BinaryDad.Extensions/Exceptions/DataPropertyConversionException.cs
new file mode 100644
index 0000000..2d3758f
--- /dev/null
+++ b/BinaryDad.Extensions/Exceptions/DataPropertyConversionException.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Reflection;
+
+namespace BinaryDad.Extensions
+{
+ [Serializable]
+ public class DataPropertyConversionException : Exception
+ {
+ public object Item { get; private set; }
+ public object Value { get; private set; }
+ public PropertyInfo Property { get; private set; }
+
+ ///
+ /// Represents an exception that occurs upon setting the value of a property on an object through type conversion
+ ///
+ /// The parent object containing the property
+ /// The property info instance of the property
+ /// The value being set
+ /// The original exception (assigned as inner)
+ public DataPropertyConversionException(object item, PropertyInfo property, object value, Exception ex)
+ : base($"Unable to assign value {value ?? "null"} ({value?.GetType().Name}) to property {item?.GetType().Name}.{property?.Name} ({property?.PropertyType.Name})", ex)
+ {
+ Value = value;
+ Item = item;
+ Property = property;
+ }
+ }
+}
\ No newline at end of file
diff --git a/BinaryDad.Extensions/Exceptions/MaxRecursionException.cs b/BinaryDad.Extensions/Exceptions/MaxRecursionException.cs
new file mode 100644
index 0000000..b428b2c
--- /dev/null
+++ b/BinaryDad.Extensions/Exceptions/MaxRecursionException.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace BinaryDad.Extensions
+{
+ [Serializable]
+ public class MaxRecursionException : Exception
+ {
+ public MaxRecursionException() { }
+
+ public MaxRecursionException(string message) : base(message) { }
+
+ public MaxRecursionException(string message, Exception innerException) : base(message, innerException) { }
+ }
+}
\ No newline at end of file
diff --git a/BinaryDad.Extensions/Extensions/CollectionExtensions.cs b/BinaryDad.Extensions/Extensions/CollectionExtensions.cs
new file mode 100644
index 0000000..024b6be
--- /dev/null
+++ b/BinaryDad.Extensions/Extensions/CollectionExtensions.cs
@@ -0,0 +1,715 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Data;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace BinaryDad.Extensions
+{
+ public static class CollectionExtensions
+ {
+ public static void AddReplace(this ICollection items, TItem item, Func matchingProperty) where TItem : class where TProperty : IComparable
+ {
+ var existing = items.FirstOrDefault(i => matchingProperty(i).Equals(matchingProperty(item)));
+
+ // if existing exists, replace existing
+ if (existing != null)
+ {
+ items.Remove(existing);
+ }
+
+ items.Add(item);
+ }
+
+ ///
+ /// Returns a distinct list of elements using the first-matched item on a specific property
+ ///
+ ///
+ ///
+ ///
+ /// Property of object to compare
+ ///
+ public static IEnumerable Distinct(this IEnumerable items, Func property) where TProperty : IComparable
+ {
+ return items
+ .GroupBy(property)
+ .Select(i => i.FirstOrDefault());
+ }
+
+ public static IEnumerable Concat(this IEnumerable items, T additionalValue)
+ {
+ if (items == null)
+ {
+ throw new ArgumentNullException(nameof(items));
+ }
+
+ return items.Concat(new[]
+ {
+ additionalValue
+ });
+ }
+
+ #region ToDataTable
+
+ ///
+ /// Converts a typed collection into a . This method excludes the column if is bound to a property.
+ ///
+ ///
+ ///
+ /// Specifies whether the data column should use the name from or , if bound to a property.
+ ///
+ ///
+ public static DataTable ToDataTable(this IEnumerable collection, bool useColumnAttributeName = false, string tableName = null) where T : class
+ {
+ using (var table = new DataTable(tableName))
+ {
+ #region Build Table Schema
+
+ var propertyInfo = typeof(T)
+ .GetProperties()
+ .Select(p => new
+ {
+ Property = p,
+
+ // set column name to be either the property name
+ // or, if specified, based on the attribute
+ ColumnName = useColumnAttributeName
+ ? p.GetDataColumnNames().FirstOrDefault()
+ : p.Name,
+
+ // include the column if [NotMapped] is NOT attached
+ IncludeColumn = !p.HasCustomAttribute()
+ })
+ .Where(p => p.IncludeColumn)
+ .ToList();
+
+ foreach (var info in propertyInfo)
+ {
+ var columnType = info.Property.PropertyType;
+
+ if (columnType.IsGenericType)
+ {
+ columnType = columnType.GetGenericArguments()[0];
+ }
+
+ table.Columns.Add(info.ColumnName, columnType);
+ }
+
+ #endregion
+
+ #region Populate the rows
+
+ foreach (var item in collection)
+ {
+ var row = table.NewRow();
+
+ foreach (var info in propertyInfo)
+ {
+ var value = info.Property.GetValue(item, null);
+
+ if (value != null)
+ {
+ row[info.ColumnName] = value;
+ }
+ }
+
+ table.Rows.Add(row);
+ }
+
+ #endregion
+
+ return table;
+ }
+ }
+
+ #endregion
+
+ #region Join
+
+ public static string Join(this IEnumerable items, string separator = "") where T : IComparable
+ {
+ if (items == null)
+ {
+ throw new ArgumentNullException(nameof(items));
+ }
+
+ if (separator == null)
+ {
+ throw new ArgumentNullException(nameof(separator));
+ }
+
+ return string.Join(separator, items.ToArray());
+ }
+
+ public static string Join(this IEnumerable items, Func selector, string separator = "") where TSelector : IComparable => Join(items.Select(selector), separator);
+
+ ///
+ /// Performs a simple join of list type T, returning items from source only
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IEnumerable Join(this IEnumerable outer, IEnumerable inner, Func key) => outer.Join(inner, key, key, (t, i) => t);
+
+ #endregion
+
+ #region Zip
+
+ public static IEnumerable Zip(this IEnumerable first, IEnumerable second, Func combine, bool symmetric)
+ {
+ if (first == null)
+ {
+ throw new ArgumentNullException(nameof(first));
+ }
+
+ if (second == null)
+ {
+ throw new ArgumentNullException(nameof(second));
+ }
+
+ if (combine == null)
+ {
+ throw new ArgumentNullException(nameof(combine));
+ }
+
+ var iter1 = first.GetEnumerator();
+ var iter2 = second.GetEnumerator();
+
+ var mn1 = iter1.MoveNext();
+ var mn2 = iter2.MoveNext();
+
+ while ((symmetric && mn1 && mn2) || (!symmetric && (mn1 || mn2)))
+ {
+ var c1 = default(TFirst);
+ var c2 = default(TSecond);
+
+ if (mn1)
+ {
+ c1 = iter1.Current;
+ mn1 = iter1.MoveNext();
+ }
+
+ if (mn2)
+ {
+ c2 = iter2.Current;
+ mn2 = iter2.MoveNext();
+ }
+
+ yield return combine(c1, c2);
+ }
+ }
+
+ #endregion
+
+ #region RemoveEmpty
+
+ public static IEnumerable RemoveEmpty(this IEnumerable list) where T : class
+ {
+ if (list == null)
+ {
+ throw new ArgumentNullException(nameof(list));
+ }
+
+ return list.Where(i => i != null);
+ }
+
+ #endregion
+
+ #region Apply
+
+ public static T Apply(this IEnumerable> functions, T input)
+ {
+ if (functions == null)
+ {
+ throw new ArgumentNullException(nameof(functions));
+ }
+
+ functions.ForEach(f => input = f(input));
+
+ return input;
+ }
+
+ #endregion
+
+ #region Where
+
+ public static IEnumerable Where(this IEnumerable list, IEnumerable> filters)
+ {
+ if (list == null)
+ {
+ throw new ArgumentNullException(nameof(list));
+ }
+
+ if (filters == null)
+ {
+ throw new ArgumentNullException(nameof(filters));
+ }
+
+ filters.ForEach(f => list = list.Where(f));
+
+ return list;
+ }
+
+ #endregion
+
+ #region ForEach
+
+ public static void ForEach(this IEnumerable list, Action action)
+ {
+ if (list == null)
+ {
+ return;
+ }
+
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ foreach (var i in list)
+ {
+ action(i);
+ }
+ }
+
+ #endregion
+
+ #region Traverse
+
+ public static IEnumerable Traverse(this IEnumerable source, Func> getYield, Func> getChildren)
+ {
+ if (source == null)
+ {
+ yield break;
+ }
+
+ if (getYield == null)
+ {
+ throw new ArgumentNullException(nameof(getYield));
+ }
+
+ if (getChildren == null)
+ {
+ throw new ArgumentNullException(nameof(getChildren));
+ }
+
+ foreach (var item in source)
+ {
+ var yields = getYield(item);
+
+ if (yields != null)
+ {
+ foreach (var yield in yields)
+ {
+ yield return yield;
+ }
+ }
+
+ var children = getChildren(item);
+
+ if (children != null)
+ {
+ foreach (var child in Traverse(children, getYield, getChildren))
+ {
+ yield return child;
+ }
+ }
+ }
+ }
+
+ public static IEnumerable Traverse(this IEnumerable source, Func> recurse)
+ {
+ if (source == null)
+ {
+ yield break;
+ }
+ if (recurse == null)
+ {
+ throw new ArgumentNullException(nameof(recurse));
+ }
+
+ foreach (var item in source)
+ {
+ yield return item;
+
+ var children = recurse(item);
+
+ if (children != null)
+ {
+ foreach (var child in Traverse(children, recurse))
+ {
+ yield return child;
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ #region JoinAction
+
+ ///
+ /// Performs an action on a joined set of lists of the same type
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void JoinAction(this IEnumerable left, IEnumerable right, Func key, Action assignment) => left.JoinAction(right, key, key, assignment);
+
+ ///
+ /// Performs an action on a joined set of lists of a different type
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void JoinAction(this IEnumerable left, IEnumerable right, Func leftKey, Func rightKey, Action assignment)
+ {
+ left
+ .Join(right, leftKey, rightKey, (l, r) => new { Left = l, Right = r })
+ .ForEach(j => assignment(j.Left, j.Right));
+ }
+
+ ///
+ /// Performs an action on a group joined set of lists of a different type
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void GroupJoinAction(this IEnumerable left, IEnumerable right, Func leftKey, Func rightKey, Action> assignment)
+ {
+ left
+ .GroupJoin(right, leftKey, rightKey, (l, r) => new { Left = l, Right = r })
+ .ForEach(j => assignment(j.Left, j.Right));
+ }
+
+ #endregion
+
+ ///
+ /// Returns a collection of items containing the items of a second collection
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IEnumerable Containing(this IEnumerable source, IEnumerable items, IEqualityComparer comparer = null) => source.Where(s => items.Contains(s, comparer));
+
+ public static bool Contains(this string[] source, string value, StringComparison comparison) => source.AnyAndNotNull(s => s.IndexOf(value, comparison) >= 0);
+
+ public static IEnumerable Convert(this IEnumerable source) where T : IConvertible
+ {
+ return source
+ .Cast