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();
+ }
+ }
+}