Adding Extensions and Helpers

This commit is contained in:
Jordan Wages 2024-04-12 23:27:42 -05:00
parent cbbe897d15
commit 6cdd805be4
7 changed files with 686 additions and 7 deletions

View file

@ -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
/// <remarks>
/// Because consumers of CapyKit may have varied ways of handling logging, the <see cref="CapyEventReporter"/> 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.
/// </remarks>
public static class CapyEventReporter
{
@ -25,6 +26,10 @@ namespace CapyKit
/// </summary>
private static Dictionary<EventLevel, List<(CapyEventHandler Handler, string origin)>> subscribers = new Dictionary<EventLevel, List<(CapyEventHandler Handler, string origin)>>();
/// <summary> A hash set storing unique identifiers for events intended to only be emitted once. </summary>
/// <seealso cref="EmitEventOnce(EventLevel, string, string, string, object[])"/>
private static HashSet<string> uniqueIdentifiers = new HashSet<string>();
#endregion
#region Methods
@ -73,14 +78,17 @@ namespace CapyKit
/// <summary> Emits an event with the given severity level, message, and method name. </summary>
/// <remarks>
/// In order to allow for efficient calling member access via <see cref="CallerMemberNameAttribute"/>
/// , it is suggested that <paramref name="args"/> is defined explicitly for formatted messages.
/// ,
/// it is suggested that <paramref name="args"/> is defined explicitly for formatted messages.
/// </remarks>
/// <param name="eventLevel"> The severity level of the event. </param>
/// <param name="message"> The message describing the reason for the event. </param>
/// <param name="method">
/// (Optional) The name of the method where the event was raised. String formatting for <paramref name="args"/>
/// <param name="message">
/// The message describing the reason for the event. String formatting for <paramref name="args"/>
/// is accepted.
/// </param>
/// <param name="method">
/// (Optional) The name of the method where the event was raised.
/// </param>
/// <param name="args">
/// A variable-length parameters list containing arguments for formatting the message.
/// </param>
@ -88,6 +96,7 @@ namespace CapyKit
/// CapyEventReporter.EmitEvent(EventLevel.Error, "Could not find the description for {0}.",
/// args: new[] { enumeration });
/// </example>
/// <seealso cref="CallerMemberNameAttribute"/>
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
}
}
/// <summary>
/// Emits an event with the given severity level, message, unique identifier, and method name one
/// time.
/// </summary>
/// <remarks>
/// This method is similar to <see cref="EmitEvent(EventLevel, string, string, string, object[])"/>
/// , but requires a unique identifier (such as a <see cref="Guid"/>) to prevent duplicate
/// emissions.
/// </remarks>
/// <param name="eventLevel"> The severity level of the event. </param>
/// <param name="message">
/// The message describing the reason for the event. String formatting for <paramref name="args"/>
/// is accepted.
/// </param>
/// <param name="uniqueIdentifier"> A unique identifier for the event emission. </param>
/// <param name="method">
/// (Optional) The name of the method where the event was raised.
/// </param>
/// <param name="args">
/// A variable-length parameters list containing arguments for formatting the message.
/// </param>
/// <seealso cref="CallerMemberNameAttribute"/>
/// <seealso cref="Guid"/>
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
{
/// <summary> Represents a critical error that requires immediate attention. </summary>
[EnumerationDescription("Represents a critical error that requires immediate attention.")]
Critical = 0,
/// <summary> Represents an error that prevents the normal execution of the application. </summary>
[EnumerationDescription("Represents an error that prevents the normal execution of the application.")]
Error = 1,
/// <summary> Represents a warning indicating a non-critical issue that should be addressed. </summary>
[EnumerationDescription("Represents a warning indicating a non-critical issue that should be addressed.")]
Warning = 2,
/// <summary> Represents informational messages that provide useful context to the consumer. </summary>
Information = 2,
[EnumerationDescription("Represents informational messages that provide useful context to the consumer.")]
Information = 3,
/// <summary> Represents detailed messages that are typically used for debugging purposes. </summary>
Debug = 3
[EnumerationDescription("Represents detailed messages that are typically used for debugging purposes.")]
Debug = 4
}
}

View file

@ -9,6 +9,7 @@ using System.Threading.Tasks;
namespace CapyKit.Extensions
{
/// <summary> Provides static extentions methods for providing additional functionality for <see cref="Enum"/> types. </summary>
public static class EnumerationExtensions
{
#region Methods

View file

@ -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
{
/// <summary> Provides static extension methods for performing common LINQ operations on <see cref="IEnumerable{T}"/> and <see cref="IQueryable{T}"/> collections. </summary>
public static class LINQExtensions
{
/// <summary>
/// Filters out items matching a <paramref name="predicate"/> from the collection.
/// </summary>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="source"> The source to act on. </param>
/// <param name="predicate"> The predicate. </param>
/// <returns>
/// An enumerator that allows foreach to be used to process remove in this collection.
/// </returns>
public static IEnumerable<T> Filter<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
return source.Where(item => !predicate(item));
}
/// <summary>
/// Filters out items matching a <paramref name="predicate"/> from the collection.
/// </summary>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="source"> The source to act on. </param>
/// <param name="predicate"> The predicate. </param>
/// <returns>
/// An enumerator that allows foreach to be used to process remove in this collection.
/// </returns>
public static IQueryable<T> Filter<T>(this IQueryable<T> source, System.Linq.Expressions.Expression<Func<T, bool>> 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<Func<T, bool>>(negatedPredicate, parameter);
return source.Where(lamda);
}
/// <summary>
/// Get a page of items from a collection, skipping <paramref name="pageNumber"/> pages of
/// <paramref name="pageSize"/> items per page.
/// </summary>
/// <remarks> This method uses natural numbering starting at page 1. </remarks>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="pageNumber"/> is less than <c>1</c> or if
/// <paramref name="pageSize"/> is less than
/// <c>1</c>.
/// </exception>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="source"> The source to act on. </param>
/// <param name="pageNumber"> The page number to retrieve. </param>
/// <param name="pageSize"> Number of items per page. </param>
/// <returns>
/// An enumerator that allows foreach to be used to process page in this collection.
/// </returns>
public static IEnumerable<T> Page<T>(this IEnumerable<T> 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);
}
/// <summary>
/// Get a page of items from a collection, skipping <paramref name="pageNumber"/> pages of
/// <paramref name="pageSize"/> items per page.
/// </summary>
/// <remarks> This method uses natural numbering starting at page 1. </remarks>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="pageNumber"/> is less than <c>1</c> or if
/// <paramref name="pageSize"/> is less than
/// <c>1</c>.
/// </exception>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="source"> The source to act on. </param>
/// <param name="pageNumber"> The page number to retrieve. </param>
/// <param name="pageSize"> . </param>
/// <returns>
/// An enumerator that allows foreach to be used to process page in this collection.
/// </returns>
public static IQueryable<T> Page<T>(this IQueryable<T> 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);
}
/// <summary>
/// The number of pages of <paramref name="pageSize"/> size in the given collection.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="pageSize"/> is less than <c>1</c>.
/// </exception>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="source"> The source to act on. </param>
/// <param name="pageSize"> Size of the page. </param>
/// <returns> An int. </returns>
public static int PageCount<T>(this IEnumerable<T> 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);
}
/// <summary>
/// The number of pages of <paramref name="pageSize"/> size in the given collection.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="pageSize"/> is less than <c>1</c>.
/// </exception>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="source"> The source to act on. </param>
/// <param name="pageSize"> Size of the page. </param>
/// <returns> An int. </returns>
public static int PageCount<T>(this IQueryable<T> 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);
}
/// <summary> An IQueryable&lt;T&gt; extension method that left outer join. </summary>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <typeparam name="U"> Generic type parameter. </typeparam>
/// <typeparam name="TKey"> Type of the key. </typeparam>
/// <typeparam name="R"> Type of the r. </typeparam>
/// <param name="source"> The source to act on. </param>
/// <param name="inner"> The inner. </param>
/// <param name="outerSelector"> The outer selector. </param>
/// <param name="innerSelector"> The inner selector. </param>
/// <param name="resultSelector"> The result selector. </param>
/// <param name="defaultGenerator"> (Optional) The default generator. </param>
/// <returns> An IQueryable&lt;R&gt; </returns>
public static IQueryable<R> LeftOuterJoin<T, U, TKey, R>(this IQueryable<T> source, IQueryable<U> inner, Expression<Func<T, TKey>> outerSelector, Expression<Func<U, TKey>> innerSelector, Func<T, IEnumerable<U>, R> resultSelector, Func<T, U> defaultGenerator = null)
{
Func<T, IEnumerable<U>, 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));
}
/// <summary> An IQueryable&lt;T&gt; extension method that left outer join. </summary>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <typeparam name="U"> Generic type parameter. </typeparam>
/// <typeparam name="TKey"> Type of the key. </typeparam>
/// <typeparam name="R"> Type of the r. </typeparam>
/// <param name="source"> The source to act on. </param>
/// <param name="inner"> The inner. </param>
/// <param name="outerSelector"> The outer selector. </param>
/// <param name="innerSelector"> The inner selector. </param>
/// <param name="resultSelector"> The result selector. </param>
/// <returns> An IQueryable&lt;R&gt; </returns>
private static IQueryable<R> LeftOuterJoin<T, U, TKey, R>(this IQueryable<T> source, IQueryable<U> inner, Expression<Func<T, TKey>> outerSelector, Expression<Func<U, TKey>> innerSelector, Expression<Func<T, IEnumerable<U>, R>> resultSelector)
{
return source.GroupJoin(inner, outerSelector, innerSelector, resultSelector);
}
/// <summary> An IEnumable&lt;T&gt; extension method that left outer join. </summary>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <typeparam name="U"> Generic type parameter. </typeparam>
/// <typeparam name="TKey"> Type of the key. </typeparam>
/// <typeparam name="R"> Type of the r. </typeparam>
/// <param name="source"> The source to act on. </param>
/// <param name="inner"> The inner. </param>
/// <param name="outerSelector"> The outer selector. </param>
/// <param name="innerSelector"> The inner selector. </param>
/// <param name="resultSelector"> The result selector. </param>
/// <param name="defaultGenerator"> (Optional) The default generator. </param>
/// <returns>
/// An enumerator that allows foreach to be used to process left outter join in this collection.
/// </returns>
public static IEnumerable<R> LeftOuterJoin<T, U, TKey, R>(this IEnumerable<T> source, IEnumerable<U> inner, Func<T, TKey> outerSelector, Func<U, TKey> innerSelector, Func<T, IEnumerable<U>, R> resultSelector, Func<T, U> 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);
});
}
/// <summary>
/// Enumerates distinct items in this collection as defined by the key <paramref name="property"/>.
/// </summary>
/// <typeparam name="T"> Generic type parameter of the parent object. </typeparam>
/// <typeparam name="U"> Generic type parameter property value. </typeparam>
/// <param name="items"> The items to act on. </param>
/// <param name="property"> The property. </param>
/// <returns>
/// An enumerator that allows foreach to be used to process distinct items in this collection.
/// </returns>
public static IEnumerable<T> Distinct<T, U>(this IEnumerable<T> items, Func<T, U> property)
{
var propertyComparer = new PropertyComparer<T, U>(property);
return items.Distinct(propertyComparer);
}
}
}

View file

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CapyKit.Extensions
{
/// <summary> Provides static extentions methods for providing additional functionality for <see cref="string"/> types. </summary>
public static class StringExtensions
{
#region Members
//
#endregion Members
#region Methods
/// <summary> Replaces a null or empty string with a specified replacement string. </summary>
/// <param name="value"> The original string. </param>
/// <param name="replacement"> The replacement string. </param>
/// <returns>
/// The original string if not null or empty, otherwise the replacement string.
/// </returns>
/// <seealso cref="string.IsNullOrEmpty(string?)"/>
public static string IfNullOrEmpty(this string value, string replacement)
{
if (string.IsNullOrEmpty(value))
{
return replacement;
}
return value;
}
/// <summary> Replaces a null or whitespace string with a specified replacement string. </summary>
/// <param name="value"> The original string. </param>
/// <param name="replacement"> The replacement string. </param>
/// <returns>
/// The original string if not null or whitespace, otherwise the replacement string.
/// </returns>
/// <seealso cref="string.IsNullOrWhiteSpace(string?)"/>
public static string IfNullOrWhiteSpace(this string value, string replacement)
{
if (string.IsNullOrWhiteSpace(value))
{
return replacement;
}
return value;
}
#endregion Methods
}
}

View file

@ -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
{
/// <summary> A class that contains methods for managing data compression. </summary>
public static class CompressionHelper
{
#region Members
//
#endregion Members
#region Methods
/// <summary> Compresses a given object using the <c>gzip</c> algorithm. </summary>
/// <param name="obj"> The object. </param>
/// <returns> A byte array representing the compressed object in <c>gzip</c> format. </returns>
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;
}
}
/// <summary> Compresses a given object to a string using <c>base64</c> encoding of <c>gzip</c> format. </summary>
/// <param name="obj"> The object. </param>
/// <returns> A string in <c>base64</c> encoding. </returns>
public static string CompressToString(object obj)
{
var bytes = Compress(obj);
return Convert.ToBase64String(bytes);
}
/// <summary> Decompresses a given <c>base64</c> encoded string of <c>gzip</c> format. </summary>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="encodedString"> The <c>base64</c> encoded <c>gzip</c> string. </param>
/// <returns> A <typeparamref name="T"/> typed object. </returns>
public static T Decompress<T>(string encodedString)
{
var bytes = Convert.FromBase64String(encodedString);
return Decompress<T>(bytes);
}
/// <summary> Decompresses a given compressed <c>gzip</c> byte stream. </summary>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="byteStream"> The compressed byte stream. </param>
/// <returns> A <typeparamref name="T"/> typed object. </returns>
public static T Decompress<T>(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<T>(bytes);
}
}
catch (Exception ex)
{
CapyEventReporter.EmitEvent(EventLevel.Error, "Could not decompress the deflated object.");
throw;
}
}
/// <summary> Decompresses the given <c>base64</c> string in <c>gzip</c> format. </summary>
/// <param name="compressed"> The compressed string. </param>
/// <returns> A decomressed string. </returns>
public static string DecompressToString(string compressed)
{
var bytes = Convert.FromBase64String(compressed);
return Decompress<string>(bytes);
}
#endregion Methods
}
}

View file

@ -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
/// <summary> Serializes an object to a byte array. </summary>
/// <param name="obj"> The object. </param>
/// <returns> A <c>JSON</c> encoded string. </returns>
public static byte[] SerializeToBytes(object obj)
{
return JsonSerializer.SerializeToUtf8Bytes(obj);
}
/// <summary> Serializes an object to a <c>JSON</c> encoded string. </summary>
/// <param name="obj"> The object. </param>
/// <returns> A <c>JSON</c> encoded string. </returns>
public static string SerializeToString(object obj)
{
return JsonSerializer.Serialize(obj);
}
/// <summary> Deserializes an object to a given <typeparamref name="T"/> type. </summary>
/// <exception cref="FormatException"> Thrown when the format of the byte array is incorrect. </exception>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="bytes"> The byte array representing a <typeparamref name="T"/> object. </param>
/// <returns> A <typeparamref name="T"/> object. </returns>
public static T Deserialize<T>(byte[] bytes)
{
var stream = new MemoryStream(bytes);
return Deserialize<T>(stream);
}
/// <summary> Deserializes an object to a given <typeparamref name="T"/> type. </summary>
/// <exception cref="FormatException">
/// Thrown when the format of an input is incorrect.
/// </exception>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="stream"> The steam. </param>
/// <returns> A <typeparamref name="T"/> object. </returns>
public static T Deserialize<T>(Stream stream)
{
try
{
var obj = JsonSerializer.Deserialize<T>(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;
}
}
/// <summary> Deserializes a <c>JSON</c> encoded string to the given <typeparamref name="T"/>. </summary>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="str"> The <c>JSON</c> encoded string representing a <typeparamref name="T"/> object. </param>
/// <returns> A <typeparamref name="T"/> object. </returns>
public static T Deserialize<T>(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<T>(str);
}
#endregion Methods
}
}

101
CapyKit/PropertyComparer.cs Normal file
View file

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CapyKit
{
/// <summary>
/// A object comparer that can accept a lambda expression to compare properties.
/// </summary>
/// <typeparam name="T"> Generic type parameter of the parent object. </typeparam>
/// <typeparam name="U"> Generic type parameter of the property value. </typeparam>
/// <example>
/// using System;
/// using System.Collections.Generic;
/// using System.Linq;
///
/// class Program
/// {
/// static void Main(string[] args)
/// {
/// var people = new List&lt;Person&gt;
/// {
/// new Person { Name = "Alice", Age = 30 }, new Person { Name = "Bob", Age = 30 }, new
/// Person { Name = "Charlie", Age = 35 }
/// };
///
/// var comparer = new PropertyComparer&lt;Person, int&gt;(p =&gt; 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; }
/// }
/// </example>
public class PropertyComparer<T, U> : IEqualityComparer<T>
{
/// <summary> The expression to retrieve the property. </summary>
private Func<T, U> expression;
/// <summary> Constructor. </summary>
/// <param name="expression"> The expression. </param>
public PropertyComparer(Func<T, U> expression)
{
this.expression = expression;
}
/// <summary> Determines whether the specified properties are equal. </summary>
/// <param name="x"> The first object of type <typeparamref name="T"/> to compare. </param>
/// <param name="y"> The second object of type <typeparamref name="T" /> to compare. </param>
/// <returns>
/// <see langword="true" /> if the specified objects are equal; otherwise,
/// <see langword="false" />.
/// </returns>
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);
}
}
/// <summary> Returns a hash code for the specified object. </summary>
/// <param name="obj">
/// The <see cref="T:System.Object" /> for which a hash code is to be returned.
/// </param>
/// <returns> A hash code for the specified object. </returns>
///
/// ### <exception cref="T:System.ArgumentNullException">
/// The type of <paramref name="obj" /> is a reference type and
/// <paramref name="obj" /> is
/// <see langword="null" />.
/// </exception>
public int GetHashCode(T obj)
{
var property = expression(obj);
return (property == null) ? 0 : property.GetHashCode();
}
}
}