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() + .Select(s => s.To()); + } + + public static TSource FirstOfType(this IEnumerable source) + { + return source + .OfType() + .FirstOrDefault(); + } + + public static bool Matches(this ICollection source, ICollection items, IEqualityComparer comparer = null) where T : IComparable + { + return source.Count == items.Count + && !source.Except(items, comparer).Any() + && !items.Except(source, comparer).Any(); + } + + public static IEnumerable Insert(this IEnumerable items, int index, T item) + { + var count = 0; + + foreach (var i in items) + { + if (count == index) + { + yield return item; + } + + yield return i; + + count++; + } + } + + public static Dictionary ToDictionary(this IEnumerable> source, IEqualityComparer comparer = null) => source.ToDictionary(k => k.Key, v => v.Value, comparer); + + #region Replace + + /// + /// Replaces an item at index with replacement + /// + /// + /// + /// Index for item getting replaced + /// Replacement item + /// + public static IEnumerable Replace(this IEnumerable items, int index, T replacement) => items.Replace(index, t => replacement); + + /// + /// Replaces an item at index with replacement (lambda) + /// + /// + /// + /// Index for item getting replaced + /// Replacement lambda + /// + public static IEnumerable Replace(this IEnumerable items, int index, Func replacement) + { + var count = 0; + + foreach (var i in items) + { + if (count == index) + { + yield return replacement(i); + } + else + { + yield return i; + } + + count++; + } + } + + #endregion + + public static IEnumerable RandomTake(this ICollection collection, int take) => RandomTake(collection, collection.Count, take); + + public static IEnumerable RandomTake(this IEnumerable items, int collectionCount, int take) + { + var rand = new Random(); + var needed = take; + var available = collectionCount; + + foreach (var i in items) + { + if (needed == 0) + { + yield break; + } + + if (rand.NextDouble() < (double)needed / available) + { + yield return i; + needed--; + } + + available--; + } + } + + public static T RandomFirstOrDefault(this IEnumerable items) => RandomShuffle(items).FirstOrDefault(); + + public static T RandomFirstOrDefault(this IEnumerable items, Func predicate) => RandomShuffle(items).Where(predicate).FirstOrDefault(); + + public static void Swap(this IList list, int indexX, int indexY) + { + if (list == null) + { + throw new ArgumentNullException(nameof(list)); + } + + if (indexX < 0 || indexX >= list.Count) + { + throw new ArgumentOutOfRangeException(nameof(indexX)); + } + if (indexY < 0 || indexY >= list.Count) + { + throw new ArgumentOutOfRangeException(nameof(indexY)); + } + + var tmp = list[indexX]; + list[indexX] = list[indexY]; + list[indexY] = tmp; + } + + /// Knuth-Fisher-Yates shuffle algorithm, see http://www.codinghorror.com/blog/2007/12/the-danger-of-naivete.html + /// + public static ICollection RandomShuffle(this IEnumerable collection) + { + var rnd = new Random(); + var list = collection.ToList(); + + for (var i = list.Count - 1; i > 0; i--) + { + var n = rnd.Next(i + 1); + list.Swap(i, n); + } + + return list; + } + + public static int IndexOf(this IEnumerable items, T value, IEqualityComparer comparer) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + if (comparer == null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + var index = 0; + foreach (var item in items) + { + if (comparer.Equals(item, value)) + { + return index; + } + + index++; + } + + return -1; + } + + public static int IndexOf(this IEnumerable items, T value) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + return items.IndexOf(value, EqualityComparer.Default); + } + + /// + /// Determines whether a sequence has any elements. Null collections return false. + /// + /// + /// + /// + /// + public static bool AnyAndNotNull(this IEnumerable items, Func predicate = null) + { + if (items == null) + { + return false; + } + + return predicate != null ? items.Any(predicate) : items.Any(); + } + + /// + /// Determines whether a sequence is null or is an empty set. + /// + /// + /// + /// + /// + public static bool NoneOrNull(this IEnumerable items, Func predicate = null) => !items.AnyAndNotNull(predicate); + + /// + /// If list has one item, display singular string. Otherwise, display plural string. + /// + /// + /// + /// Word to be used if not 1 item count + /// Word to be used if only 1 item count + /// + public static string IfPlural(this IEnumerable list, string plural, string singular) + { + var items = list.ToList(); + + return items.AnyAndNotNull() && items.Count() == 1 ? singular : plural; + } + + /// + /// Executes an inline conditional statement if the sequence contains at least one element + /// + /// + /// + /// + /// + /// + /// + public static TResult IfAny(this IEnumerable list, Func, TResult> anyResult, Func, TResult> noneResult) + { + if (list.AnyAndNotNull()) + { + return anyResult(list); + } + + return noneResult(list); + } + + #region OrderBy + + public static IOrderedEnumerable OrderBy(this IEnumerable source, Func keySelector, bool ascending) + { + return ascending + ? source.OrderBy(keySelector) + : source.OrderByDescending(keySelector); + } + + public static IEnumerable OrderBy(this IEnumerable source, string key) => source.OrderBy(key, true); + + public static IEnumerable OrderByDescending(this IEnumerable source, string key) => source.OrderBy(key, false); + + public static IEnumerable OrderBy(this IEnumerable source, string key, bool ascending) + { + // i => i.SomeProperty + // "i" is a parameter + // "i.SomeProperty" is the body of the expression + var param = Expression.Parameter(typeof(T), "i"); + var body = Expression.Property(param, key); + + // create expression using body and parameters + var expression = Expression.Lambda>(body, param).Compile(); + + return source.OrderBy(expression, ascending); + } + + #endregion + + #region Sort + + public static IEnumerable Sort(this IEnumerable items) where T : IComparable => items.OrderBy(i => i); + + public static IEnumerable SortDescending(this IEnumerable items) where T : IComparable => items.OrderByDescending(i => i); + + #endregion + + /// + /// Returns an empty collection if collection is null + /// + /// + /// + /// + public static IEnumerable EmptyIfNull(this IEnumerable items) => items ?? Enumerable.Empty(); + + /// + /// Retrieves form data in a key/value pair collection + /// + /// + /// + public static IEnumerable> GetParameters(this NameValueCollection collection) + { + foreach (string k in collection.Keys) + { + yield return new KeyValuePair(k, collection[k]); + } + } + } +} \ No newline at end of file diff --git a/BinaryDad.Extensions/Extensions/DataTableExtensions.cs b/BinaryDad.Extensions/Extensions/DataTableExtensions.cs new file mode 100644 index 0000000..f4e6608 --- /dev/null +++ b/BinaryDad.Extensions/Extensions/DataTableExtensions.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Data; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace BinaryDad.Extensions +{ + public static class DataTableExtensions + { + /// + /// Maps table data to a collection of objects with matching properties. Either or may be used to bind columns to properties. + /// + /// The type to convert each row to + /// + /// Action used to modify each object with additional data from row. This is applied after binding. + /// + public static IList ToList(this DataTable table, Action afterRowBinding = null) + { + #region Null checks + + if (table == null) + { + return null; + } + + #endregion + + return table.Rows + .Cast() + .ToList(afterRowBinding); + } + + /// + /// Maps a collection of rows to a collection of objects with matching properties. Either or may be used to bind columns to properties. + /// + /// The type to convert each row to + /// + /// Action used to modify each object with additional data from row. This is applied after binding. + /// + public static IList ToList(this IEnumerable rows, Action afterRowBinding = null) + { + #region Null checks + + if (rows == null) + { + return null; + } + + #endregion + + var type = typeof(T); + + // Prevent deferred execution: http://stackoverflow.com/questions/3628425/ienumerable-vs-list-what-to-use-how-do-they-work + return rows + .Select(row => + { + // map the row to the object + var instance = (T)row.To(type); + + // invoke any modifiers + afterRowBinding?.Invoke(instance, row); + + return instance; + }) + .ToList(); + } + + /// + /// Maps the first of a to an object type. Either or may be used to bind columns to properties. + /// + /// + /// + /// + public static T To(this DataTable table) => table.FirstRow().To(); + + /// + /// Maps a to an object type. Either or may be used to bind columns to properties. + /// + /// The type to convert the to + /// + /// + public static T To(this DataRow row) => (T)row.To(typeof(T)); + + /// + /// Maps a to an object type. Either or may be used to bind columns to properties. + /// + /// + /// The type to convert the to + /// + public static object To(this DataRow row, Type type) + { + #region Null checks + + if (row == null) + { + throw new ArgumentNullException(nameof(row)); + } + + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + #endregion + + var properties = type.GetProperties(); + + if (type.IsValueType || type == typeof(string)) + { + // If the object is a value/string type, we can only pull one item, so assume it's the first in the row + return row[0].To(type); + } + + var instance = Activator.CreateInstance(type); + + /* 01/09/13 - SJK: Class properties can be decorated with our custom DataRowField attribute. This allows us to + * declare a mapping between an object and a database column that are named differently. If the attribute is + * present on a property, we use its name for the customProperties collection. Otherwise, we use the name + * of the property as we assume the name on the database column is the same. + */ + properties.ForEach(p => RecursiveSetValue(p, row, instance)); + + return instance; + } + + /// + /// Returns a dictionary of key (column header) and value (cell value) pairs + /// + /// + /// + public static IDictionary ToDictionary(this DataRow row) + { + return row.Table.Columns + .Cast() + .ToDictionary(c => c.ColumnName, c => row[c]); + } + + /// + /// Converts a data table to a CSV string + /// + /// + /// + public static string ToCsv(this DataTable table) + { + using (var stream = new MemoryStream()) + { + table.ToCsv(stream); + + return Encoding.UTF8.GetString(stream.ToArray()); + } + } + + /// + /// Converts data table to a CSV string and writes to a file + /// + /// + /// + public static void ToCsv(this DataTable table, string filePath) + { + using (var file = File.OpenWrite(filePath)) + { + table.ToCsv(file); + } + } + + /// + /// Converts data table to a CSV string and writes to a stream + /// + /// + /// + public static void ToCsv(this DataTable table, Stream stream) + { + var encoding = new UTF8Encoding(false, true); + + // leave the underlying stream open (uses default parameters) + using (var sw = new StreamWriter(stream, encoding, 1024, true)) + { + #region Write Columns + + var columnNames = table.Columns.Cast() + .Select(column => column.ColumnName) + .ToArray(); + + sw.WriteLine(string.Join(",", columnNames)); + + #endregion + + #region Write Rows + + foreach (DataRow row in table.Rows) + { + var fields = row.ItemArray + .Select(field => QuoteValue(field.ToString())) + .ToArray(); + + sw.WriteLine(string.Join(",", fields)); + } + + #endregion + + sw.Close(); + } + } + + /// + /// Performs an inline replacement of a value + /// + /// + /// + /// Allows for update/modification of the existing value + public static void SetField(this DataRow row, string columnName, Func value) => row[columnName] = value(row[columnName]); + + /// + /// Iterates through a collection of + /// + /// + /// + public static void ForEach(this DataRowCollection rows, Action predicate) + { + #region Null checks + + if (rows == null) + { + return; + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + #endregion + + rows + .Cast() + .ForEach(predicate); + } + + /// + /// Returns whether the is not null and has rows + /// + /// + /// + public static bool HasRows(this DataTable data) + { + if (data?.Rows == null) + { + return false; + } + + return data.Rows.Count > 0; + } + + /// + /// Returns the first row of a data table, or null if there are no rows + /// + /// + /// + public static DataRow FirstRow(this DataTable data) + { + if (data.HasRows()) + { + return data.Rows[0]; + } + + return null; + } + + /// + /// Projects of a into a new form + /// + /// + /// + /// + /// + public static IEnumerable Select(this DataTable data, Func selector) => data.Rows.Cast().Select(selector); + + #region Private Methods + + private static void RecursiveSetValue(PropertyInfo property, DataRow row, object instance, int maxDepth = 5, int currentDepth = 0) + { + #region Max Depth + + //Ensure we prevent too complex objects. This could be a sign of poor design or a condition prompting infinite recursion. + if (currentDepth > maxDepth) + { + throw new MaxRecursionException($"The currentDepth {currentDepth} may not exceed the maxDepth {maxDepth} for recursion. Check the complexity of the object and its implementation of the DataRowPopulate attribute."); + } + + #endregion + + try + { + // ignore the property if specified + if (property.HasCustomAttribute()) + { + return; + } + + // if we found DataRowPopulate attributes, we go a level deeper + if (property.HasCustomAttribute()) + { + currentDepth++; + + // get all properties on this type and continue if we have any + var subProperties = property.PropertyType.GetProperties(); + + if (subProperties.AnyAndNotNull()) + { + // since we have properties, we attempt to create an item (e.g. Address) based on the type of the current p + var subItem = Activator.CreateInstance(Type.GetType(property.PropertyType.AssemblyQualifiedName)); + + // attempt to set each value on subItem (e.g. populating Street1, Street2 on Address). + subProperties.ForEach(p2 => RecursiveSetValue(p2, row, subItem, maxDepth, currentDepth)); + + // now that subItem (instance of Address) has been populated, we in turn need to populate the current p (Address) on item. + property.SetValue(instance, subItem, null); + } + else + { + property.SetValue(row, instance); + } + } + else + { + property.SetValue(row, instance); + } + } + catch (MaxRecursionException) + { + throw; + } + catch (Exception ex) + { + throw new Exception($"Property {property.Name} cannot be set from row.", ex); + } + } + + private static string QuoteValue(string value) => string.Concat("\"", value.Replace("\"", "\"\""), "\""); + + #endregion + } +} \ No newline at end of file diff --git a/BinaryDad.Extensions/Extensions/DateTimeExtensions.cs b/BinaryDad.Extensions/Extensions/DateTimeExtensions.cs new file mode 100644 index 0000000..9f0b423 --- /dev/null +++ b/BinaryDad.Extensions/Extensions/DateTimeExtensions.cs @@ -0,0 +1,48 @@ +using System; + +namespace BinaryDad.Extensions +{ + public static class DateTimeExtensions + { + public static bool IsWeekDay(this DateTime date) + { + //default to false + var returnValue = false; + + switch (date.DayOfWeek) + { + case DayOfWeek.Monday: + case DayOfWeek.Tuesday: + case DayOfWeek.Wednesday: + case DayOfWeek.Thursday: + case DayOfWeek.Friday: + returnValue = true; + break; + default: + break; + + } + + return returnValue; + } + + public static bool IsWeekEnd(this DateTime date) + { + //default to false + var returnValue = false; + + switch (date.DayOfWeek) + { + case DayOfWeek.Saturday: + case DayOfWeek.Sunday: + + returnValue = true; + break; + default: + break; + } + + return returnValue; + } + } +} \ No newline at end of file diff --git a/BinaryDad.Extensions/Extensions/DirectoryExtensions.cs b/BinaryDad.Extensions/Extensions/DirectoryExtensions.cs new file mode 100644 index 0000000..d3ee415 --- /dev/null +++ b/BinaryDad.Extensions/Extensions/DirectoryExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace BinaryDad.Extensions +{ + public static class DirectoryExtensions + { + public static IEnumerable GetFilesByExtensions(this DirectoryInfo directory, params string[] extensions) + { + if (extensions == null) + { + throw new ArgumentNullException(nameof(extensions)); + } + + return directory + .EnumerateFiles() + .Where(f => extensions.Contains(f.Extension, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/BinaryDad.Extensions/Extensions/EnumExtensions.cs b/BinaryDad.Extensions/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..8baf645 --- /dev/null +++ b/BinaryDad.Extensions/Extensions/EnumExtensions.cs @@ -0,0 +1,49 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace BinaryDad.Extensions +{ + // NOTE: RJP - This extension may be generic enough to be used outside of just enums. Something to think about later. + public static class EnumExtensions + { + /// + /// Retrieves a descriptive attribute associated with the enum field using DescriptionAttribute + /// + /// + /// + public static string GetDescription(this Enum value) + { + #region DescriptionAttribute + + var description = value.GetCustomAttribute(); + + if (description != null) + { + return description.Description; + } + + #endregion + + // default + return value.ToString(); + } + + /// + /// Retrieves a custom attribute of a specified type that is applied to a specified member + /// + /// + /// + /// + public static T GetCustomAttribute(this Enum value) where T : Attribute + { + var member = value + .GetType() + .GetMember(value.ToString()) + .FirstOrDefault(); + + return member?.GetCustomAttribute() ?? default; + } + } +} \ No newline at end of file diff --git a/BinaryDad.Extensions/Extensions/GenericExtensions.cs b/BinaryDad.Extensions/Extensions/GenericExtensions.cs new file mode 100644 index 0000000..6cbe4b6 --- /dev/null +++ b/BinaryDad.Extensions/Extensions/GenericExtensions.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BinaryDad.Extensions +{ + public static class GenericExtensions + { + /// + /// Returns true if the value is between the lower and upper range. This is inclusive in its comparison. + /// + /// + /// + /// + /// + /// + public static bool Between(this T value, T lower, T upper) where T : IComparable + { + return Comparer.Default.Compare(value, lower) >= 0 + && Comparer.Default.Compare(value, upper) <= 0; + } + + #region IfNotNull + + /// + /// Executes an inline statement if the source value is not null + /// + /// + /// + /// + /// + /// + /// + public static TResult IfNotNull(this TSource source, Func value, TResult defaultValue) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (source != null) + { + return value(source); + } + + return defaultValue == null ? default : defaultValue; + } + + /// + /// Executes an inline statement if the source value is not null + /// + /// + /// + /// + /// + /// + public static TResult IfNotNull(this TSource source, Func value) => source.IfNotNull(value, default(TResult)); + + /// + /// Executes an inline statement if the source value is not null + /// + /// + /// + /// + /// + /// + /// + public static TResult IfNotNull(this TSource source, TResult value, TResult defaultValue) => source.IfNotNull(o => value, defaultValue); + + /// + /// Executes an inline statement if the source value is not null + /// + /// + /// + /// + /// + /// + public static TResult IfNotNull(this TSource source, TResult value) => source.IfNotNull(o => value, default); + + #endregion + + #region If + + /// + /// Executes an inline conditional statement, allowing for an evaluation for true or false + /// + /// + /// + /// + /// + /// + /// + /// + public static TResult If(this TSource source, Func condition, Func trueResult, Func falseResult = null) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (condition == null) + { + throw new ArgumentNullException(nameof(condition)); + } + + return source.If(condition(source), trueResult, falseResult); + } + + /// + /// Executes an inline conditional statement, allowing for an evaluation for true or false + /// + /// + /// + /// + /// + /// + /// + /// + public static TResult If(this TSource source, bool condition, Func trueResult, Func falseResult = null) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (trueResult == null) + { + throw new ArgumentNullException(nameof(trueResult)); + } + + if (condition) + { + return trueResult(source); + } + + if (falseResult != null) + { + return falseResult(source); + } + + return default; + } + + #endregion + + #region In + + /// + /// Returns whether a value is in a particular sequence of items + /// + /// + /// + /// The collection of items the value may belong to + /// + public static bool In(this T value, params T[] items) where T : IComparable => items.AnyAndNotNull(t => t.Equals(value)); + + /// + /// Returns whether a value is in a particular sequence of items + /// + /// + /// + /// The collection of items the value may belong to + /// + public static bool In(this T value, IEnumerable items) where T : IComparable => In(value, items.ToArray()); + + #endregion + + #region With + + /// + /// Performs an action on an object and returns that instance + /// + /// + /// + /// + /// + public static T With(this T value, Action action) + { + if (action == null) + { + // could throw null arg exception but "action" is the whole point of the method + throw new InvalidOperationException("No action specified in With or is null. Please omit use of this method."); + } + + action(value); + + return value; + } + + #endregion + } +} \ No newline at end of file diff --git a/BinaryDad.Extensions/Extensions/NumericExtensions.cs b/BinaryDad.Extensions/Extensions/NumericExtensions.cs new file mode 100644 index 0000000..c0fbc79 --- /dev/null +++ b/BinaryDad.Extensions/Extensions/NumericExtensions.cs @@ -0,0 +1,44 @@ +namespace BinaryDad.Extensions +{ + public static class NumericExtensions + { + public static string ToCurrency(this decimal currency, bool alwaysShowCents = false) + { + // only hide the cents if value is an even amount (i.e., has no cents LOL) + if (!alwaysShowCents && (int)currency == currency) + { + return ((int)currency).ToString("C0"); + } + + return currency.ToString("C"); + } + + public static string WithOrdinal(this int value) + { + if (value <= 0) + { + return value.ToString(); + } + + switch (value % 100) + { + case 11: + case 12: + case 13: + return $"{value}th"; + } + + switch (value % 10) + { + case 1: + return $"{value}st"; + case 2: + return $"{value}nd"; + case 3: + return $"{value}rd"; + default: + return $"{value}th"; + } + } + } +} diff --git a/BinaryDad.Extensions/Extensions/ObjectExtensions.cs b/BinaryDad.Extensions/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..572fd24 --- /dev/null +++ b/BinaryDad.Extensions/Extensions/ObjectExtensions.cs @@ -0,0 +1,158 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BinaryDad.Extensions +{ + public static class ObjectExtensions + { + /// + /// Target is the object wanted to be merged to, if source has the value and target does not, copy source value to target + /// + /// + /// + /// + + public static void CoalesceValues(this T target, T source) + { + var t = typeof(T); + + var properties = t.GetProperties().Where(prop => prop.CanRead && prop.CanWrite); + + foreach (var prop in properties) + { + var valueT = prop.GetValue(target, null); + var valueS = prop.GetValue(source, null); + + if (valueT == null) + { + if (valueS != null) + { + prop.SetValue(target, valueS, null); + } + } + + } + + } + + #region To + + /// + /// Casts or converts a value to type T + /// + /// + /// + /// + public static T To(this object value) + { + if (value is T castValue) + { + return castValue; + } + + return (T)value.To(typeof(T)); + } + + /// + /// Casts or converts a value to a specified type + /// + /// + /// + /// + public static object To(this object value, Type type) + { + #region Check if null + + // if value is null and is value type, return "default" value + // otherwise, return null + if (value == null || value == DBNull.Value) + { + if (type.IsValueType) + { + return Activator.CreateInstance(type); + } + + return null; + } + + #endregion + + #region Check if empty string + + if (type.IsValueType && value is string stringValue && stringValue == string.Empty) + { + return Activator.CreateInstance(type); + } + + #endregion + + var convertType = Nullable.GetUnderlyingType(type) ?? type; + + #region Check if Enum + + if (convertType.IsEnum) + { + return value.ToString().ToEnum(convertType); + } + + #endregion + + #region Attempt convert using TypeConverter.ConvertFrom + + var converter = TypeDescriptor.GetConverter(type); + + if (converter.CanConvertFrom(value.GetType())) + { + return converter.ConvertFrom(value); + } + + #endregion + + #region Convert using ChangeType + + return Convert.ChangeType(value, convertType); + + #endregion + } + + #endregion + + /// + /// Serializes an object to a JSON string. Wraps + /// + /// + /// + public static string Serialize(this object value) => JsonConvert.SerializeObject(value); + + /// + /// Returns a dictionary of properties and values for an object or an empty dictionary if value is null or no properties (e.g., value types and string) + /// + /// + /// + public static IDictionary GetPropertyValues(this object value) + { + // check for null + if (value == null) + { + return null; + } + + var valueType = value.GetType(); + + // check for value types, and string + if (valueType.IsValueType || valueType == typeof(string)) + { + return null; + } + + return valueType + .GetProperties() + .EmptyIfNull() + .Select(p => new KeyValuePair(p.Name, p.GetValue(value, null))) + .ToDictionary(k => k.Key, k => k.Value); + } + } +} \ No newline at end of file diff --git a/BinaryDad.Extensions/Extensions/ReflectionExtensions.cs b/BinaryDad.Extensions/Extensions/ReflectionExtensions.cs new file mode 100644 index 0000000..156cbbb --- /dev/null +++ b/BinaryDad.Extensions/Extensions/ReflectionExtensions.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Data; +using System.Linq; +using System.Reflection; + +namespace BinaryDad.Extensions +{ + public static class ReflectionExtensions + { + #region Public + + /// + /// Determines whether a property has a specific + /// + /// + /// + /// + public static bool HasCustomAttribute(this PropertyInfo property) where T : Attribute + { + return property.GetCustomAttributes(typeof(T), false).Any(); + } + + /// + /// Sets the value of a property using the matched column from the data row, using or for binding. + /// + /// + /// + /// + public static void SetValue(this PropertyInfo property, DataRow row, object instance) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + if (row == null) + { + return; + } + + // get the column name from the row + var matchedColumnName = property.GetDataColumnName(row); + + //Set the value if we matched a column. If we don't have a match, there's simply no way to set a value. + if (matchedColumnName != null) + { + // raw value from the row, matching on column name + var tableValue = row[matchedColumnName]; + + // if DBNull, set as null + var value = tableValue == DBNull.Value ? null : tableValue; + + // if the value is null, we know to just set as null + if (value == null) + { + property.SetValue(instance, null, null); + } + else + { + try + { + property.SetValue(instance, value.To(property.PropertyType), null); + } + catch (Exception ex) + { + // encapsulate more detail about why the conversion failed + throw new DataPropertyConversionException(instance, property, value, ex); + } + } + } + } + + #endregion + + #region Internal + + /// + /// Retrieves a list of available property binding aliases using . + /// + /// + /// + internal static IEnumerable GetDataColumnNames(this PropertyInfo property) + { + #region Null checks + + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + #endregion + + var dataRowFieldNames = new List(); + + var attributes = property + .GetCustomAttributes(true); + + var columnAttribute = attributes.FirstOfType(); + + if (columnAttribute != null) + { + dataRowFieldNames.Add(columnAttribute.Name); + } + + // add the property's name at the end, so it's the last in the lookup + dataRowFieldNames.Add(property.Name); + + return dataRowFieldNames; + } + + /// + /// Retrieves a list of available property binding aliases using + /// + /// + /// + internal static IEnumerable GetEnumAliases(this FieldInfo field) + { + #region Null checks + + if (field == null) + { + throw new ArgumentNullException(nameof(field)); + } + + #endregion + + var aliasNames = new List(); + + var enumAlias = field + .GetCustomAttributes(true) + .FirstOfType(); + + if (enumAlias != null) + { + aliasNames.AddRange(enumAlias.EnumAliasNames); + } + + aliasNames.Add(field.Name); + + return aliasNames; + } + + /// + /// Retrieves the first matched column name from the data row. Uses or . + /// + /// + /// + /// + internal static string GetDataColumnName(this PropertyInfo property, DataRow row) + { + return property.GetDataColumnName(row.Table.Columns); + } + + /// + /// Retrieves the first matched column name from the collection of data columns. Uses or . + /// + /// + /// + /// + internal static string GetDataColumnName(this PropertyInfo property, DataColumnCollection columns) + { + //Check whether we have any columns with the data row field names. This should not be null if the property + //was configured correctly with PropertyAliasAttribute. + return property + .GetDataColumnNames() + .FirstOrDefault(f => columns.Contains(f)); + } + + #endregion + } +} \ No newline at end of file diff --git a/BinaryDad.Extensions/Extensions/StreamExtensions.cs b/BinaryDad.Extensions/Extensions/StreamExtensions.cs new file mode 100644 index 0000000..a3c811d --- /dev/null +++ b/BinaryDad.Extensions/Extensions/StreamExtensions.cs @@ -0,0 +1,28 @@ +using System.IO; +using System.Threading.Tasks; + +namespace BinaryDad.Extensions +{ + public static class StreamExtensions + { + public static byte[] GetBytes(this Stream stream) + { + using (var memoryStream = new MemoryStream()) + { + stream.CopyTo(memoryStream); + + return memoryStream.ToArray(); + } + } + + public static async Task GetBytesAsync(this Stream stream) + { + using (var memoryStream = new MemoryStream()) + { + await stream.CopyToAsync(memoryStream); + + return memoryStream.ToArray(); + } + } + } +} diff --git a/BinaryDad.Extensions/Extensions/StringExtensions.cs b/BinaryDad.Extensions/Extensions/StringExtensions.cs new file mode 100644 index 0000000..b1e0b1d --- /dev/null +++ b/BinaryDad.Extensions/Extensions/StringExtensions.cs @@ -0,0 +1,357 @@ +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace BinaryDad.Extensions +{ + public static class StringExtensions + { + /// + /// Searches a list of strings with a delimiter for a match on specified value. + /// + /// The the delimited string to search + /// The value to match against delimited values + /// The string separator + /// True if a match is found, otherwise false + /// + public static bool ContainsMatch(this string s, string value, char separator = ';', bool isCaseSensitive = false) + { + if (s == null) + { + return false; + } + + if (isCaseSensitive) + { + return s.SafeSplit(separator).Any(item => value.Trim().Contains(item.Trim())); + } + + return s.SafeSplit(separator).Any(item => value.ToLower().Trim().Contains(item.ToLower().Trim())); + } + + public static bool Contains(this string source, string value, StringComparison comparisonType) => source.IndexOf(value, comparisonType) >= 0; + + /// + /// Retrieves the last length of characters + /// + /// + /// + /// + public static string GetLast(this string source, int length) + { + return length >= source.Length + ? source + : source.Substring(source.Length - length); + } + + /// + /// Parse a datetime string using DateTime.TryParse, returning null if it cannot be parsed. + /// + /// + /// + public static DateTime? ParseDateTime(this string value) + { + if (DateTime.TryParse(value, out var date)) + { + return date; + } + + return null; + } + + /// + /// Validate date string + /// + /// + /// + public static bool IsDate(this string date) + { + try + { + var dt = DateTime.Parse(date); + return true; + } + catch + { + return false; + } + } + + + /// + /// Checks whether the string is an email. + /// + /// + /// + public static bool IsEmail(this string value) + { + if (value.IsNullOrWhiteSpace()) + { + return false; + } + + try + { + return Regex.IsMatch(value, + @"^(?("")("".+?(? + /// Wrapper for String.IsNullOrEmpty(value) + /// + /// + /// + public static bool IsNullOrEmpty(this string value) => string.IsNullOrEmpty(value); + + /// + /// Wrapper for String.IsNullOrWhiteSpace(value) + /// + /// + /// + public static bool IsNullOrWhiteSpace(this string value) => string.IsNullOrWhiteSpace(value); + + /// + /// Allows for a replacement string if the value is null + /// + /// + /// + /// + public static string ValueIfNull(this string obj, string value) + { + if (obj == null) + { + return value; + } + + return obj; + } + + /// + /// Returns true if the string is not null and has a value + /// + /// + /// + public static bool IsNotEmpty(this string value) => !value.IsNullOrWhiteSpace(); + + /// + /// Returns whether a value is in a particular sequence of strings + /// + /// + /// + /// The collection of items the value may belong to + /// + public static bool In(this string value, StringComparison comparisonType, params string[] items) => items.AnyAndNotNull(t => t.Equals(value, comparisonType)); + + /// + /// Checks whether the entire string is numeric. This purely evaluates numbers, not currency (decimals/thousand-separators). + /// + /// + /// True, if entire string is numeric; otherwise, false. + public static bool IsNumeric(this string value) => decimal.TryParse(value, out _); + + /// + /// Returns whether a string matches a regular expression pattern + /// + /// The string to check + /// The regex pattern + /// + /// + public static bool Like(this string value, string regex, bool ignoreCase = true) => Regex.IsMatch(value, regex, ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None); + + public static string[] Split(this string text, params string[] separator) => text.Split(separator, StringSplitOptions.None); + + public static string[] SafeSplit(this string s, char separator = ';') + { + if (s == null) + { + return new string[] { }; + } + + return (from sr in s.Split(separator) + let tr = sr.Trim() + where !string.IsNullOrEmpty(tr) + select tr).ToArray(); + } + + public static string NullIfWhiteSpace(this string text) => text.If(s => s.IsNullOrWhiteSpace(), s => null, s => s); + + #region ToEnum + + /// + /// Casts or converts a string value to type T + /// + /// + /// + /// + public static T ToEnum(this string value) where T : Enum + { + var type = typeof(T); + var parsedEnum = value.ToEnum(type); + + if (parsedEnum == null) + { + throw new ApplicationException($"Could not cast value {value} to type {type.FullName}"); + } + + return (T)parsedEnum; + } + + /// + /// Casts or converts a string value to a type + /// + /// + /// + /// + public static object ToEnum(this string value, Type type) + { + #region Constraints + + if (value == null) + { + return null; + } + + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (!type.IsEnum) + { + throw new ArgumentException("Destination conversion type must be an Enum", nameof(type)); + } + + #endregion + + #region Helpers + + bool isDefined(string s) + { + return int.TryParse(s, out var num) + ? Enum.IsDefined(type, num) + : Enum.IsDefined(type, s); + } + + #endregion + + // Try to parse normally (works for int values and literal strings matching enum field name) + // Calling the isDefined wrapper handles checks for ints or string values, as we can't use TryParse + if (isDefined(value)) + { + return Enum.Parse(type, value); + } + else // If no match, dig up enum alias attributes for a match + { + return type + .GetFields() + .FirstOrDefault(field => field.GetEnumAliases().Any(a => a.Equals(value, StringComparison.OrdinalIgnoreCase))) + ?.GetValue(null); + } + } + + #endregion + + public static string NullableTrim(this string value) => value?.Trim(); + + public static string ToPhoneNumber(this string value) => Regex.Replace(value, @"^\d?(\d{3})(\d{3})(\d{4})$", "($1) $2-$3"); + + public static string MaskSsn(this string value) + { + if (!value.IsNullOrWhiteSpace()) + { + var match = Regex.Match(value, @"\d{3}.?\d{2}.?(\d{4})"); + + if (match.Success) + { + return $"XXX-XX-{match.Groups[1].Value}"; + } + } + + return null; + } + + /// + /// Truncates a string to a maximum length value + /// + /// + /// + /// Indicates whether to include whole words when truncating (i.e., whether to truncate in the middle of a word) + /// Indicates whether to include an ellipsis (...) if truncating occurs. + /// + public static string Truncate(this string text, int limit, bool useWholeWords = false, bool useEllipsis = false) + { + var output = text; + + if (!text.IsNullOrWhiteSpace() && output.Length > limit && limit > 0) + { + output = output.Substring(0, limit); + + // include whole words when truncating (don't truncate in middle of a word) + if (useWholeWords && text.Substring(output.Length, 1) != " ") + { + var lastSpaceIndex = output.LastIndexOf(" ", StringComparison.OrdinalIgnoreCase); + + if (lastSpaceIndex != -1) + { + output = output.Substring(0, lastSpaceIndex); + } + } + + if (useEllipsis) + { + output += "..."; + } + } + + return output; + } + + /// + /// Deserializes text to a type. Wraps . + /// + /// + /// + /// + public static T Deserialize(this string text) => JsonConvert.DeserializeObject(text); + + /// + /// Deserializes text to a type. Wraps . + /// + /// + /// + /// + public static object Deserialize(this string text, Type type) => JsonConvert.DeserializeObject(text, type); + + /// + /// Encodes a plaintext string into Base-64 + /// + /// + /// + public static string Base64Encode(this string plainText) + { + var bytes = System.Text.Encoding.UTF8.GetBytes(plainText); + + return Convert.ToBase64String(bytes); + } + + /// + /// Decodes a Base-64 encoded string into plaintext + /// + /// + /// + public static string Base64Decode(this string encodedText) + { + var bytes = Convert.FromBase64String(encodedText); + + return System.Text.Encoding.UTF8.GetString(bytes); + } + } +} \ No newline at end of file diff --git a/BinaryDad.Extensions/Extensions/TypeExtensions.cs b/BinaryDad.Extensions/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..270bb56 --- /dev/null +++ b/BinaryDad.Extensions/Extensions/TypeExtensions.cs @@ -0,0 +1,17 @@ +using System; + +namespace BinaryDad.Extensions +{ + public static class TypeExtensions + { + public static bool IsNullable(this Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + } +} \ No newline at end of file diff --git a/BinaryDad.Extensions/Extensions/XmlExtensions.cs b/BinaryDad.Extensions/Extensions/XmlExtensions.cs new file mode 100644 index 0000000..e7faa0b --- /dev/null +++ b/BinaryDad.Extensions/Extensions/XmlExtensions.cs @@ -0,0 +1,57 @@ +using System.Xml; + +namespace BinaryDad.Extensions +{ + public static class XmlExtensions + { + /// + /// Null-safe retrieval of a collection. Returns null if node is null. + /// + /// + /// + /// + /// + public static T GetAttributeValue(this XmlNode node, string attribute) + { + var selectedAttribute = node?.Attributes?[attribute]; + + if (selectedAttribute != null) + { + return selectedAttribute.Value.To(); + } + + return default; + } + + /// + /// Null-safe retrieval of a collection. Returns null if node is null. + /// + /// + /// + /// + public static string GetAttributeValue(this XmlNode node, string attribute) => node.GetAttributeValue(attribute); + + /// + /// Null-safe retrieval of a . Returns null if node is null. + /// + /// + /// + /// + public static T GetInnerText(this XmlNode node) + { + if (node?.InnerText != null) + { + return node.InnerText.To(); + } + + return default; + } + + /// + /// Null-safe retrieval of a . Returns null if node is null. + /// + /// + /// + public static string GetInnerText(this XmlNode node) => node.GetInnerText(); + } +} diff --git a/BinaryDad.Extensions/RestUtility.cs b/BinaryDad.Extensions/RestUtility.cs new file mode 100644 index 0000000..3ac2fe2 --- /dev/null +++ b/BinaryDad.Extensions/RestUtility.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; + +namespace BinaryDad.Extensions +{ + public class RestUtility + { + public const int DefaultTimeout = 5000; + public const int DefaultSleepDelay = 3000; + public const int DefaultRetries = 3; + + #region Get + + /// + /// Invokes a GET request with optional headers + /// + /// + /// + /// + /// Timeout of the request in milliseconds + /// + public static T Get(string url, Dictionary additionalHeaders = null, int timeoutMs = DefaultTimeout, int sleepDelayMs = DefaultSleepDelay, int retriesAllowed = DefaultRetries) + { + return Send(url, HttpMethod.Get, typeof(T), null, additionalHeaders, timeoutMs, sleepDelayMs, retriesAllowed).To(); + } + + /// + /// Invokes a GET request with optional headers + /// + /// + /// + /// Timeout of the request in milliseconds + /// + public static string Get(string url, Dictionary additionalHeaders = null, int timeoutMs = DefaultTimeout, int sleepDelayMs = DefaultSleepDelay, int retriesAllowed = DefaultRetries) + { + // response type is always a string if no returnObjectType is used + return Send(url, HttpMethod.Get, null, null, additionalHeaders, timeoutMs, sleepDelayMs, retriesAllowed) as string; + } + + /// + /// Invokes a GET request with optional headers + /// + /// + /// + /// + /// Timeout of the request in milliseconds + /// + /// + /// + public static object Get(string url, Type returnObjectType, Dictionary additionalHeaders = null, int timeoutMs = DefaultTimeout, int sleepDelayMs = DefaultSleepDelay, int retriesAllowed = DefaultRetries) + { + return Send(url, HttpMethod.Get, returnObjectType, null, additionalHeaders, timeoutMs, sleepDelayMs, retriesAllowed); + } + + #endregion + + #region Post + + /// + /// Invokes a POST request with optional headers + /// + /// + /// + /// + /// + /// Timeout of the request in milliseconds + /// + public static T Post(string url, object body, Dictionary additionalHeaders = null, int timeoutMs = DefaultTimeout, int sleepDelayMs = DefaultSleepDelay, int retriesAllowed = DefaultRetries) + { + return Send(url, HttpMethod.Post, typeof(T), body, additionalHeaders, timeoutMs, sleepDelayMs, retriesAllowed).To(); + } + + /// + /// Invokes a POST request with optional headers + /// + /// + /// + /// + /// Timeout of the request in milliseconds + /// + public static string Post(string url, object body, Dictionary additionalHeaders = null, int timeoutMs = DefaultTimeout, int sleepDelayMs = DefaultSleepDelay, int retriesAllowed = DefaultRetries) + { + // response type is always a string if no returnObjectType is used + return Send(url, HttpMethod.Post, null, body, additionalHeaders, timeoutMs, sleepDelayMs, retriesAllowed) as string; + } + + /// + /// Invokes a POST request with optional headers + /// + /// + /// + /// + /// + /// Timeout of the request in milliseconds + /// + /// + /// + public static object Post(string url, Type returnObjectType, object body, Dictionary additionalHeaders, int timeoutMs = DefaultTimeout, int sleepDelayMs = DefaultSleepDelay, int retries = DefaultRetries) + { + return Send(url, HttpMethod.Post, returnObjectType, body, additionalHeaders, timeoutMs, sleepDelayMs, retries); + } + + #endregion + + #region Send + + /// + /// Invokes a request with custom method/verb and optional headers + /// + /// + /// + /// + /// + /// + /// Timeout of the request in milliseconds + /// + public static T Send(string url, HttpMethod method, object body = null, Dictionary additionalHeaders = null, int timeoutMs = DefaultTimeout, int sleepDelayMs = DefaultSleepDelay, int retriesAllowed = DefaultRetries) + { + return Send(url, method, typeof(T), body, additionalHeaders, timeoutMs, sleepDelayMs, retriesAllowed).To(); + } + + /// + /// Invokes a request with custom method/verb and optional headers + /// + /// + /// + /// + /// + /// Timeout of the request in milliseconds + /// + public static string Send(string url, HttpMethod method, object body = null, Dictionary additionalHeaders = null, int timeoutMs = DefaultTimeout, int sleepDelayMs = DefaultSleepDelay, int retriesAllowed = DefaultRetries) + { + return Send(url, method, null, body, additionalHeaders, timeoutMs, sleepDelayMs, retriesAllowed) as string; + } + + /// + /// Invokes a request with custom method/verb and optional headers + /// + /// + /// + /// + /// + /// + /// Timeout of the request in milliseconds + /// + /// + /// + public static object Send(string url, HttpMethod method, Type returnObjectType, object body = null, Dictionary additionalHeaders = null, int timeoutMs = DefaultTimeout, int sleepDelayMs = DefaultSleepDelay, int retriesAllowed = DefaultRetries) + { + var serializedResponse = string.Empty; + var success = false; + + for (var attempts = 1; attempts <= retriesAllowed && !success; attempts++) + { + var request = WebRequest.CreateHttp(url); + + request.Method = method.ToString(); + request.Timeout = timeoutMs; + request.ContentType = "application/json"; + request.Accept = "application/json, text/javascript, *; q=0.01"; // Accept is a reserved header, so you must modify it rather than add + + // add additional headers + if (additionalHeaders != null) + { + foreach (var key in additionalHeaders.Keys) + { + if (additionalHeaders[key] != null) + { + request.Headers.Add(key, additionalHeaders[key]); + } + else + { + request.Headers.Add(key); + } + } + } + + try + { + if (body != null) + { + var serializedBody = body.Serialize(); + var bytes = System.Text.Encoding.GetEncoding("iso-8859-1").GetBytes(serializedBody); + + request.ContentLength = bytes.Length; + + using (var writeStream = request.GetRequestStream()) + { + writeStream.Write(bytes, 0, bytes.Length); + } + } + else if (method == HttpMethod.Post) // POST requires a content length, set to 0 for null body + { + request.ContentLength = 0; + } + + using (var response = (HttpWebResponse)request.GetResponse()) + { + if (response.StatusCode < HttpStatusCode.BadRequest) + { + // Success + using (var responseStream = response.GetResponseStream()) + { + if (responseStream != null) + { + using (var reader = new StreamReader(responseStream)) + { + serializedResponse = reader.ReadToEnd(); + } + } + } + } + } + + success = true; + } + catch + { + // only throw after we have reached our retry limit + if (attempts >= retriesAllowed) + { + throw; + } + } + + // if post failed, pause before another attempt + if (!success) + { + Thread.Sleep(sleepDelayMs); + } + else + { + break; + } + } + + if (success && returnObjectType != null) + { + return serializedResponse.Deserialize(returnObjectType); + } + else + { + return serializedResponse; + } + } + + #endregion + } +} diff --git a/BinaryDad.Extensions/UrlUtility.cs b/BinaryDad.Extensions/UrlUtility.cs new file mode 100644 index 0000000..d0e8aef --- /dev/null +++ b/BinaryDad.Extensions/UrlUtility.cs @@ -0,0 +1,34 @@ +using System; + +namespace BinaryDad.Extensions +{ + public static class UrlUtility + { + /// + /// Combines a base URL with a relative URL + /// + /// + /// + /// + public static string Combine(string baseUrl, string relativeUrl) + { + // ensure domain ends with trailing slash + if (!baseUrl.EndsWith("/")) + { + baseUrl = $"{baseUrl}/"; + } + + var baseUri = new Uri(baseUrl); + + if (!baseUri.IsAbsoluteUri) + { + throw new ArgumentException("Base URL must be absolute", nameof(baseUrl)); + } + + // ensure relative path does not have a prefixed slash + relativeUrl = relativeUrl.TrimStart('/'); + + return new Uri(baseUri, relativeUrl).OriginalString; + } + } +}