diff --git a/CapyKit/CapyEvent.cs b/CapyKit/CapyEvent.cs index 8745bfe..f088a9a 100644 --- a/CapyKit/CapyEvent.cs +++ b/CapyKit/CapyEvent.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using CapyKit.Attributes; using CapyKit.Extensions; namespace CapyKit @@ -14,7 +15,7 @@ namespace CapyKit /// /// Because consumers of CapyKit may have varied ways of handling logging, the provides /// a way for subscribers to recieve events for various "events" within the library. These can be thought of as - /// a logging solution for CapyKit. + /// a logging solution for CapyKit. Consumers are free to treat these events however they see fit. /// public static class CapyEventReporter { @@ -25,6 +26,10 @@ namespace CapyKit /// private static Dictionary> subscribers = new Dictionary>(); + /// A hash set storing unique identifiers for events intended to only be emitted once. + /// + private static HashSet uniqueIdentifiers = new HashSet(); + #endregion #region Methods @@ -73,14 +78,17 @@ namespace CapyKit /// Emits an event with the given severity level, message, and method name. /// /// In order to allow for efficient calling member access via - /// , it is suggested that is defined explicitly for formatted messages. + /// , + /// it is suggested that is defined explicitly for formatted messages. /// /// The severity level of the event. - /// The message describing the reason for the event. - /// - /// (Optional) The name of the method where the event was raised. String formatting for + /// + /// The message describing the reason for the event. String formatting for /// is accepted. /// + /// + /// (Optional) The name of the method where the event was raised. + /// /// /// A variable-length parameters list containing arguments for formatting the message. /// @@ -88,6 +96,7 @@ namespace CapyKit /// CapyEventReporter.EmitEvent(EventLevel.Error, "Could not find the description for {0}.", /// args: new[] { enumeration }); /// + /// internal static void EmitEvent(EventLevel eventLevel, string message, [CallerMemberName] string method = null, params object[] args) { if (!subscribers.ContainsKey(eventLevel)) @@ -105,6 +114,40 @@ namespace CapyKit } } + /// + /// Emits an event with the given severity level, message, unique identifier, and method name one + /// time. + /// + /// + /// This method is similar to + /// , but requires a unique identifier (such as a ) to prevent duplicate + /// emissions. + /// + /// The severity level of the event. + /// + /// The message describing the reason for the event. String formatting for + /// is accepted. + /// + /// A unique identifier for the event emission. + /// + /// (Optional) The name of the method where the event was raised. + /// + /// + /// A variable-length parameters list containing arguments for formatting the message. + /// + /// + /// + internal static void EmitEventOnce(EventLevel eventLevel, string message, string uniqueIdentifier, [CallerMemberName] string method = null, params object[] args) + { + if(uniqueIdentifiers.Contains(uniqueIdentifier)) + { + return; + } + + uniqueIdentifiers.Add(uniqueIdentifier); + EmitEvent(eventLevel, message, method: method, args: args); + } + #endregion } @@ -162,12 +205,19 @@ namespace CapyKit public enum EventLevel { /// Represents a critical error that requires immediate attention. + [EnumerationDescription("Represents a critical error that requires immediate attention.")] Critical = 0, /// Represents an error that prevents the normal execution of the application. + [EnumerationDescription("Represents an error that prevents the normal execution of the application.")] Error = 1, + /// Represents a warning indicating a non-critical issue that should be addressed. + [EnumerationDescription("Represents a warning indicating a non-critical issue that should be addressed.")] + Warning = 2, /// Represents informational messages that provide useful context to the consumer. - Information = 2, + [EnumerationDescription("Represents informational messages that provide useful context to the consumer.")] + Information = 3, /// Represents detailed messages that are typically used for debugging purposes. - Debug = 3 + [EnumerationDescription("Represents detailed messages that are typically used for debugging purposes.")] + Debug = 4 } } diff --git a/CapyKit/Extensions/EnumerationExtensions.cs b/CapyKit/Extensions/EnumerationExtensions.cs index f7afccf..1fc4308 100644 --- a/CapyKit/Extensions/EnumerationExtensions.cs +++ b/CapyKit/Extensions/EnumerationExtensions.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; namespace CapyKit.Extensions { + /// Provides static extentions methods for providing additional functionality for types. public static class EnumerationExtensions { #region Methods diff --git a/CapyKit/Extensions/LINQExtentions.cs b/CapyKit/Extensions/LINQExtentions.cs new file mode 100644 index 0000000..3a20094 --- /dev/null +++ b/CapyKit/Extensions/LINQExtentions.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace CapyKit.Extensions +{ + /// Provides static extension methods for performing common LINQ operations on and collections. + public static class LINQExtensions + { + /// + /// Filters out items matching a from the collection. + /// + /// Generic type parameter. + /// The source to act on. + /// The predicate. + /// + /// An enumerator that allows foreach to be used to process remove in this collection. + /// + public static IEnumerable Filter(this IEnumerable source, Func predicate) + { + return source.Where(item => !predicate(item)); + } + + /// + /// Filters out items matching a from the collection. + /// + /// Generic type parameter. + /// The source to act on. + /// The predicate. + /// + /// An enumerator that allows foreach to be used to process remove in this collection. + /// + public static IQueryable Filter(this IQueryable source, System.Linq.Expressions.Expression> predicate) + { + if(predicate.Parameters.Count > 1) + { + CapyEventReporter.EmitEvent(EventLevel.Warning, "More than one parameter was found in the predicate, which could result in unexpected behavior."); + } + + var parameter = predicate.Parameters.FirstOrDefault(); + var negatedPredicate = Expression.Not(predicate); + var lamda = Expression.Lambda>(negatedPredicate, parameter); + + return source.Where(lamda); + } + + /// + /// Get a page of items from a collection, skipping pages of + /// items per page. + /// + /// This method uses natural numbering starting at page 1. + /// + /// Thrown when is less than 1 or if + /// is less than + /// 1. + /// + /// Generic type parameter. + /// The source to act on. + /// The page number to retrieve. + /// Number of items per page. + /// + /// An enumerator that allows foreach to be used to process page in this collection. + /// + public static IEnumerable Page(this IEnumerable source, int pageNumber, int pageSize) + { + if (pageNumber < 1) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "pageNumber is out of range. [{0}]", args: new[] { pageNumber }); + throw new ArgumentOutOfRangeException("pageNumber"); + } + + if (pageSize < 1) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "pageSize is out of range. [{0}]", args: new[] { pageSize }); + throw new ArgumentOutOfRangeException("pageSize"); + } + + return source.Skip((pageNumber - 1) * pageSize).Take(pageSize); + } + + /// + /// Get a page of items from a collection, skipping pages of + /// items per page. + /// + /// This method uses natural numbering starting at page 1. + /// + /// Thrown when is less than 1 or if + /// is less than + /// 1. + /// + /// Generic type parameter. + /// The source to act on. + /// The page number to retrieve. + /// . + /// + /// An enumerator that allows foreach to be used to process page in this collection. + /// + public static IQueryable Page(this IQueryable source, int pageNumber, int pageSize) + { + if (pageNumber < 1) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "pageNumber is out of range. [{0}]", args: new[] { pageNumber }); + throw new ArgumentOutOfRangeException("pageNumber"); + } + + if (pageSize < 1) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "pageSize is out of range. [{0}]", args: new[] { pageSize }); + throw new ArgumentOutOfRangeException("pageSize"); + } + + return source.Skip((pageNumber - 1) * pageSize).Take(pageSize); + } + + /// + /// The number of pages of size in the given collection. + /// + /// + /// Thrown when is less than 1. + /// + /// Generic type parameter. + /// The source to act on. + /// Size of the page. + /// An int. + public static int PageCount(this IEnumerable source, int pageSize) + { + if (pageSize < 1) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "pageSize is out of range. [{0}]", args: new[] { pageSize }); + throw new ArgumentOutOfRangeException("pageSize"); + } + + var ceiling = Math.Ceiling(Convert.ToDouble(source.Count()) / pageSize); + return Convert.ToInt32(ceiling); + } + + /// + /// The number of pages of size in the given collection. + /// + /// + /// Thrown when is less than 1. + /// + /// Generic type parameter. + /// The source to act on. + /// Size of the page. + /// An int. + public static int PageCount(this IQueryable source, int pageSize) + { + if (pageSize < 1) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "pageSize is out of range. [{0}]", args: new[] { pageSize }); + throw new ArgumentOutOfRangeException("pageSize"); + } + + var ceiling = Math.Ceiling(Convert.ToDouble(source.Count()) / pageSize); + return Convert.ToInt32(ceiling); + } + + /// An IQueryable<T> extension method that left outer join. + /// Generic type parameter. + /// Generic type parameter. + /// Type of the key. + /// Type of the r. + /// The source to act on. + /// The inner. + /// The outer selector. + /// The inner selector. + /// The result selector. + /// (Optional) The default generator. + /// An IQueryable<R> + public static IQueryable LeftOuterJoin(this IQueryable source, IQueryable inner, Expression> outerSelector, Expression> innerSelector, Func, R> resultSelector, Func defaultGenerator = null) + { + Func, R> resultOrDefaultSelector = (i, o) => + { + if (defaultGenerator == null) + { + defaultGenerator = (t) => default(U); + } + + if (!o.Any()) + { + return resultSelector(i, new[] { defaultGenerator(i) }); + } + + return resultSelector(i, o); + }; + + return source.LeftOuterJoin(inner, outerSelector, innerSelector, (a, b) => resultSelector(a, b)); + } + + /// An IQueryable<T> extension method that left outer join. + /// Generic type parameter. + /// Generic type parameter. + /// Type of the key. + /// Type of the r. + /// The source to act on. + /// The inner. + /// The outer selector. + /// The inner selector. + /// The result selector. + /// An IQueryable<R> + private static IQueryable LeftOuterJoin(this IQueryable source, IQueryable inner, Expression> outerSelector, Expression> innerSelector, Expression, R>> resultSelector) + { + return source.GroupJoin(inner, outerSelector, innerSelector, resultSelector); + } + + /// An IEnumable<T> extension method that left outer join. + /// Generic type parameter. + /// Generic type parameter. + /// Type of the key. + /// Type of the r. + /// The source to act on. + /// The inner. + /// The outer selector. + /// The inner selector. + /// The result selector. + /// (Optional) The default generator. + /// + /// An enumerator that allows foreach to be used to process left outter join in this collection. + /// + public static IEnumerable LeftOuterJoin(this IEnumerable source, IEnumerable inner, Func outerSelector, Func innerSelector, Func, R> resultSelector, Func defaultGenerator = null) + { + var combined = source.GroupJoin(inner, outerSelector, innerSelector, (i, o) => new { inner = i, outer = o }); + + if (defaultGenerator == null) + { + defaultGenerator = (t) => default(U); + } + + return combined.Select(anon => + { + if (!anon.outer.Any()) + { + return resultSelector(anon.inner, new[] { defaultGenerator(anon.inner) }); + } + + return resultSelector(anon.inner, anon.outer); + }); + } + + /// + /// Enumerates distinct items in this collection as defined by the key . + /// + /// Generic type parameter of the parent object. + /// Generic type parameter property value. + /// The items to act on. + /// The property. + /// + /// An enumerator that allows foreach to be used to process distinct items in this collection. + /// + public static IEnumerable Distinct(this IEnumerable items, Func property) + { + var propertyComparer = new PropertyComparer(property); + return items.Distinct(propertyComparer); + } + } +} diff --git a/CapyKit/Extensions/StringExtensions.cs b/CapyKit/Extensions/StringExtensions.cs new file mode 100644 index 0000000..57b4bbf --- /dev/null +++ b/CapyKit/Extensions/StringExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CapyKit.Extensions +{ + /// Provides static extentions methods for providing additional functionality for types. + public static class StringExtensions + { + #region Members + + // + + #endregion Members + + #region Methods + + /// Replaces a null or empty string with a specified replacement string. + /// The original string. + /// The replacement string. + /// + /// The original string if not null or empty, otherwise the replacement string. + /// + /// + public static string IfNullOrEmpty(this string value, string replacement) + { + if (string.IsNullOrEmpty(value)) + { + return replacement; + } + + return value; + } + + /// Replaces a null or whitespace string with a specified replacement string. + /// The original string. + /// The replacement string. + /// + /// The original string if not null or whitespace, otherwise the replacement string. + /// + /// + public static string IfNullOrWhiteSpace(this string value, string replacement) + { + if (string.IsNullOrWhiteSpace(value)) + { + return replacement; + } + + return value; + } + + #endregion Methods + } +} diff --git a/CapyKit/Helpers/CompressionHelper.cs b/CapyKit/Helpers/CompressionHelper.cs new file mode 100644 index 0000000..b995d67 --- /dev/null +++ b/CapyKit/Helpers/CompressionHelper.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CapyKit.Helpers +{ + /// A class that contains methods for managing data compression. + public static class CompressionHelper + { + #region Members + + // + + #endregion Members + + #region Methods + + /// Compresses a given object using the gzip algorithm. + /// The object. + /// A byte array representing the compressed object in gzip format. + public static byte[] Compress(object obj) + { + var bytes = SerializationHelper.SerializeToBytes(obj); + + try + { + using (var inputStream = new MemoryStream(bytes)) + using (var outputStream = new MemoryStream()) + { + using (var gzipStream = new GZipStream(outputStream, CompressionMode.Compress)) + { + inputStream.Position = 0; + inputStream.CopyTo(gzipStream); + gzipStream.Flush(); + } + return outputStream.ToArray(); + } + } + catch (Exception ex) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "Could not compress the object."); + throw; + } + } + + /// Compresses a given object to a string using base64 encoding of gzip format. + /// The object. + /// A string in base64 encoding. + public static string CompressToString(object obj) + { + var bytes = Compress(obj); + return Convert.ToBase64String(bytes); + } + + /// Decompresses a given base64 encoded string of gzip format. + /// Generic type parameter. + /// The base64 encoded gzip string. + /// A typed object. + public static T Decompress(string encodedString) + { + var bytes = Convert.FromBase64String(encodedString); + return Decompress(bytes); + } + + /// Decompresses a given compressed gzip byte stream. + /// Generic type parameter. + /// The compressed byte stream. + /// A typed object. + public static T Decompress(byte[] byteStream) + { + try + { + using (var inputStream = new MemoryStream(byteStream)) + using (var outputStream = new MemoryStream()) + { + using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress)) + { + gzipStream.CopyTo(outputStream); + } + var bytes = outputStream.ToArray(); + return SerializationHelper.Deserialize(bytes); + } + } + catch (Exception ex) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "Could not decompress the deflated object."); + throw; + } + } + + /// Decompresses the given base64 string in gzip format. + /// The compressed string. + /// A decomressed string. + public static string DecompressToString(string compressed) + { + var bytes = Convert.FromBase64String(compressed); + + return Decompress(bytes); + } + + #endregion Methods + } +} diff --git a/CapyKit/Helpers/SerializationHelper.cs b/CapyKit/Helpers/SerializationHelper.cs new file mode 100644 index 0000000..b1bb378 --- /dev/null +++ b/CapyKit/Helpers/SerializationHelper.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace CapyKit.Helpers +{ + public static class SerializationHelper + { + #region Members + + // + + #endregion Members + + #region Methods + + /// Serializes an object to a byte array. + /// The object. + /// A JSON encoded string. + public static byte[] SerializeToBytes(object obj) + { + return JsonSerializer.SerializeToUtf8Bytes(obj); + } + + /// Serializes an object to a JSON encoded string. + /// The object. + /// A JSON encoded string. + public static string SerializeToString(object obj) + { + return JsonSerializer.Serialize(obj); + } + + /// Deserializes an object to a given type. + /// Thrown when the format of the byte array is incorrect. + /// Generic type parameter. + /// The byte array representing a object. + /// A object. + public static T Deserialize(byte[] bytes) + { + var stream = new MemoryStream(bytes); + + return Deserialize(stream); + } + + /// Deserializes an object to a given type. + /// + /// Thrown when the format of an input is incorrect. + /// + /// Generic type parameter. + /// The steam. + /// A object. + public static T Deserialize(Stream stream) + { + try + { + var obj = JsonSerializer.Deserialize(stream); + + if(obj == null) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "The deserialized object was null."); + throw new ArgumentNullException(nameof(stream)); + } + + return (T)obj; + } + catch (JsonException ex) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "JSON formatting error detected during deserialization of byte array for {0}.", args: new[] { typeof(T).Name }); + throw new FormatException(string.Format("JSON formatting error detected during deserialization of byte array for {0}.", typeof(T).Name), ex); + } + catch (Exception ex) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "Could not deserialize object of type {0}.", args: new[] { typeof(T).Name }); + throw; + } + } + + /// Deserializes a JSON encoded string to the given . + /// Generic type parameter. + /// The JSON encoded string representing a object. + /// A object. + public static T Deserialize(string str) + { + if (typeof(T) == typeof(string)) + { + return (T)Convert.ChangeType(str, typeof(T)); + } + else if(string.IsNullOrWhiteSpace(str)) + { + CapyEventReporter.EmitEvent(EventLevel.Error, "Could not deserialize an empty string."); + return default(T); + } + + return JsonSerializer.Deserialize(str); + } + + #endregion Methods + } +} diff --git a/CapyKit/PropertyComparer.cs b/CapyKit/PropertyComparer.cs new file mode 100644 index 0000000..580cd34 --- /dev/null +++ b/CapyKit/PropertyComparer.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CapyKit +{ + /// + /// A object comparer that can accept a lambda expression to compare properties. + /// + /// Generic type parameter of the parent object. + /// Generic type parameter of the property value. + /// + /// using System; + /// using System.Collections.Generic; + /// using System.Linq; + /// + /// class Program + /// { + /// static void Main(string[] args) + /// { + /// var people = new List<Person> + /// { + /// new Person { Name = "Alice", Age = 30 }, new Person { Name = "Bob", Age = 30 }, new + /// Person { Name = "Charlie", Age = 35 } + /// }; + /// + /// var comparer = new PropertyComparer<Person, int>(p => p.Age); + /// var distinctPeople = people.Distinct(comparer).ToList(); + /// + /// foreach (var person in distinctPeople) + /// { + /// Console.WriteLine($"{person.Name} - {person.Age}"); + /// } + /// } + /// } + /// + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + public class PropertyComparer : IEqualityComparer + { + /// The expression to retrieve the property. + private Func expression; + + /// Constructor. + /// The expression. + public PropertyComparer(Func expression) + { + this.expression = expression; + } + + /// Determines whether the specified properties are equal. + /// The first object of type to compare. + /// The second object of type to compare. + /// + /// if the specified objects are equal; otherwise, + /// . + /// + public bool Equals(T x, T y) + { + var left = expression.Invoke(x); + var right = expression.Invoke(y); + + if (left == null && right == null) + { + return true; + } + if (left == null ^ right == null) + { + return false; + } + else + { + return left.Equals(right); + } + } + + /// Returns a hash code for the specified object. + /// + /// The for which a hash code is to be returned. + /// + /// A hash code for the specified object. + /// + /// ### + /// The type of is a reference type and + /// is + /// . + /// + public int GetHashCode(T obj) + { + var property = expression(obj); + + return (property == null) ? 0 : property.GetHashCode(); + } + } +}