Skip to content

Instantly share code, notes, and snippets.

@cobysy
Last active October 18, 2025 17:12
Show Gist options
  • Save cobysy/178fa7029e1bc2bc2a297e80c2c30c72 to your computer and use it in GitHub Desktop.
Save cobysy/178fa7029e1bc2bc2a297e80c2c30c72 to your computer and use it in GitHub Desktop.
// HashUtils to compute Hash on a complex document
var doc = new Document
{
Title = "Hash Test",
Links =
[
new() { Url = "https://b.com", Text = "B" },
new() { Url = "https://a.com", Text = "A" }
]
};
string hashOrderMatters = HashUtils.CalculateHash(
doc,
HashSelector<Document>.For(d => d.Title),
HashSelector<Document>.ForEach(d => d.Links, l => l.Url, sort: false)
);
public static class HashUtils
{
public static string CalculateHash<T>(
T obj,
params HashSelector<T>[] selectors
)
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
if (selectors == null || selectors.Length == 0)
{
throw new ArgumentException("At least one selector must be specified.");
}
var sb = new StringBuilder();
foreach (var selector in selectors)
{
var value = selector.GetValue(obj);
sb.Append(SerializeValue(value));
sb.Append("|");
}
using var sha = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
var hashBytes = sha.ComputeHash(bytes);
return BitConverter.ToString(hashBytes).Replace(
"-",
"").ToLowerInvariant();
}
private static string SerializeValue(
object? value)
{
if (value == null)
{
return string.Empty;
}
var t = value.GetType();
if (t.IsPrimitive || t.IsEnum || t == typeof(string) || t == typeof(decimal))
{
return value.ToString() ?? string.Empty;
}
if (t == typeof(DateTime))
{
return ((DateTime)value).ToUniversalTime().ToString("O");
}
if (value is IEnumerable enumerable && !(value is string))
{
var items = enumerable.Cast<object?>().Select(SerializeValue);
return "[" + string.Join(
",",
items) + "]";
}
// complex type: serialize recursively
var props = t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.OrderBy(p => p.Name);
var nested = props
.Select(p => $"{p.Name}:{SerializeValue(p.GetValue(value))}");
return "{" + string.Join(
",",
nested) + "}";
}
public abstract class HashSelector<T>
{
public abstract object? GetValue(
T obj);
public static HashSelector<T> For(
Expression<Func<T, object>> expr)
{
return new SimpleHashSelector<T>(expr);
}
public static HashSelector<T> ForEach<TItem>(
Expression<Func<T, IEnumerable<TItem>>> collectionExpr,
Func<TItem, object?> selector,
bool sort = false
)
{
return new CollectionHashSelector<T, TItem>(
collectionExpr,
selector,
sort);
}
}
private sealed class SimpleHashSelector<T>(Expression<Func<T, object>> expr) : HashSelector<T>
{
private readonly Func<T, object?> _getter = expr.Compile();
public override object? GetValue(
T obj)
{
try
{
return this._getter(obj);
}
catch
{
return null;
}
}
}
private sealed class CollectionHashSelector<T, TItem>(
Expression<Func<T, IEnumerable<TItem>>> getter,
Func<TItem, object?> selector,
bool sort)
: HashSelector<T>
{
private readonly Func<T, IEnumerable<TItem>?> _getter = getter.Compile();
public override object? GetValue(
T obj)
{
try
{
var items = this._getter(obj);
if (items == null)
{
return null;
}
var projected = items.Select(selector).Where(v => v != null);
if (sort)
{
projected = projected.OrderBy(
v => v?.ToString(),
StringComparer.Ordinal);
}
return projected.ToArray();
}
catch
{
return null;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment