Calculate fingerprint for an object I need to store serialized objects in a database and I want to avoid duplicates. In order to be able to tell wheter an object is already stored, I have to compare them somehow. My idea was to calculate a hash for some of its properties that I consider as equatable and store it in a separate column.



The algorithm is very simple: I implemented it with a builder that allows me to define a hash function and how each property should be formatted. E.g. strings may be case insensitive, floats may be rounded or date-time may be stripped of its millisecond part etc. This transformation is done by each expression. It then serializes each value into bytes and adds them to the buffer. Finally the hash-function calculates the hash-value. Sorting the expressions by member-name should prevent bugs caused by simply changing their order.

public class FingerprintBuilder&lt;T&gt;
{
    private readonly List&lt;(string MemberName, Func&lt;T, object&gt; Fingerprint)&gt; _fingerprints;

    public FingerprintBuilder()
    {
        _fingerprints = new List&lt;(string Expression, Func&lt;T, object&gt; Fingerprint)&gt;();
    }

    public static FingerprintBuilder&lt;T&gt; Empty =&gt; new FingerprintBuilder&lt;T&gt;();

    public FingerprintBuilder&lt;T&gt; For&lt;TProperty&gt;(Expression&lt;Func&lt;T, TProperty&gt;&gt; memberExpression, Expression&lt;Func&lt;TProperty, TProperty&gt;&gt; fingerprint)
    {
        var getProperty = memberExpression.Compile();
        var getFingerprint = fingerprint.Compile();
        _fingerprints.Add((
            ((MemberExpression)memberExpression.Body).Member.Name, 
            (Func&lt;T, object&gt;)(obj =&gt; getFingerprint(getProperty(obj)))
        ));
        return this;
    }

    public Func&lt;T, byte[]&gt; Build(Func&lt;byte[], byte[]&gt; computeHash)
    {
        return obj =&gt;
        {
            var buffer = new List&lt;byte&gt;();

            var binaryFormatter = new BinaryFormatter();
            foreach (var t in _fingerprints.OrderBy(e =&gt; e.MemberName, StringComparer.OrdinalIgnoreCase))
            {
                var fingerprint = t.Fingerprint(obj);
                using (var memory = new MemoryStream())
                {
                    binaryFormatter.Serialize(memory, fingerprint);
                    buffer.AddRange(memory.ToArray());
                }
            }

            return computeHash(buffer.ToArray());
        };
    }
}


In this example I also use two helper extensions. One for calculating the hash and the other one to get its string representation:

public static byte[] ComputeSHA1(this byte[] source)
{
    if (source == null) throw new ArgumentNullException(nameof(source));

    using (var sha1 = new SHA1Managed())
    {
        return sha1.ComputeHash(source);
    }
}

public static string ToHexString(this byte[] source)
{
    if (source == null) throw new ArgumentNullException(nameof(source));

    return
        source
            .Aggregate(new StringBuilder(), (current, next) =&gt; current.Append(next.ToString("X2")))
            .ToString();
}


Example

Testing it with these examples:

var p1 = new Person { FirstName = "John", LastName = "Doe", Age = 20 };
var p2 = new Person { FirstName = "John", LastName = "DOE", Age = 30 };
var p3 = new Person { FirstName = "Johny", LastName = "DOE", Age = 30 };

var personFingerprint = 
    FingerprintBuilder&lt;Person&gt;.Empty
        .For(p =&gt; p.LastName, x =&gt; x.ToUpperInvariant().Trim())
        .For(p =&gt; p.FirstName, x =&gt; x.ToUpperInvariant().Trim())
        .Build(SHA1.ComputeSHA1);

personFingerprint(p1).ToHexString().Dump();
personFingerprint(p2).ToHexString().Dump();
personFingerprint(p3).ToHexString().Dump();


produces these three results:

D46E5C869CDBA5775D7F8C9AF6BC497D5EEA76C4
D46E5C869CDBA5775D7F8C9AF6BC497D5EEA76C4
F450C1CE6B1A2041D8D7D0FEAA31AF7C5C214D81




What do you think of this algorithm? Does it have any weaknesses or can anything else be improved?
It looks alright to me, although it seems odd having a young man of age 20 to equal an "elderly" man of age 30 :-).

I have 3 minor things:

1) One MemoryStream can be used to serialize all expressions:

  public Func&lt;T, byte[]&gt; Build(Func&lt;byte[], byte[]&gt; computeHash)
  {
    return obj =&gt;
    {
      var binaryFormatter = new BinaryFormatter();
      using (var memory = new MemoryStream())
      {
        foreach (var t in _fingerprints.OrderBy(e =&gt; e.MemberName, StringComparer.OrdinalIgnoreCase))
        {
          var fingerprint = t.Fingerprint(obj);
          {
            binaryFormatter.Serialize(memory, fingerprint);
          }
        }

        return computeHash(memory.ToArray());
      }
    };
  }




2) The ToHexString() can be simplified like this:

  public static string ToHexString(this byte[] source)
  {
    if (source == null) throw new ArgumentNullException(nameof(source));

    return string.Join("", source.Select(ch =&gt; ch.ToString("X2")));
  }




3) FingerprintBuilder.Empty is an strange name. I would call it Builder or avoid the static instantiater.