paint-brush
How to Stop String.GetHashCode() in .NET C# from Driving You Crazyby@ahmedtarekhasan
176 reads

How to Stop String.GetHashCode() in .NET C# from Driving You Crazy

by Ahmed Tarek HasanJanuary 23rd, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

String.GetHashCode() in.NET C# is not guaranteed to be stable. Hash codes for identical strings can differ across .NET implementations and .NET platforms. This implies that two subsequent runs of the same program may return different hash codes. Here is how to overcome this in a good way.

People Mentioned

Mention Thumbnail
featured image - How to Stop String.GetHashCode() in .NET C# from Driving You Crazy
Ahmed Tarek Hasan HackerNoon profile picture

Know when to depend on String.GetHashCode() in .NET C#, and when not.


The Story

I was working on a side project of a tool to help me with my daily work. In one module, I needed to keep track of some transactions which could be executed between the tool runs. In other words, when I close and open the tool, these transactions should be there.


For storage, I used a SQLite database where I save a hash-code of an entity representing my Transaction class. I was depending on this hash-code as at some point the tool would generate a new hash-code and compare it to the one already saved in the database.


So, after setting everything in place, I started the tool, give it a try, yes… it is working. I close the tool, do some stuff, then try testing another thing, now it is not working as it should!!


I kept trying to understand what is going on and finally I got it. The hash-code comparison is not working fine. During the same run session of the tool, the comparison is working fine. However, when I close and open the tool, the newly generated hash-code -to the same exact transaction-is not the same as the one saved in the database.


I searched the internet and found that this is true. According to Microsoft:


The hash code itself is not guaranteed to be stable. Hash codes for identical strings can differ across .NET implementations, across .NET versions, and across .NET platforms (such as 32-bit and 64-bit) for a single version of .NET. In some cases, they can even differ by application domain. This implies that two subsequent runs of the same program may return different hash codes.


As a result, hash codes should never be used outside of the application domain in which they were created, they should never be used as key fields in a collection, and they should never be persisted.


Finally, don’t use the hash code instead of a value returned by a cryptographic hashing function if you need a cryptographically strong hash. For cryptographic hashes, use a class derived from the System.Security.Cryptography.HashAlgorithm or System.Security.Cryptography.KeyedHashAlgorithm class.


For more information about hash codes, see Object.GetHashCode.


Therefore, I am now sharing this with you and I am going to tell you how to overcome this in a good way.


Photo by Brett Jordan on Unsplash

Let’s Give It a Try

Create a Console Application with the following code.


class Program
{
    static void Main(string[] args)
    {
        var employee = new Employee("Ahmed");

        Console.WriteLine(employee.GetHashCode());
        Console.ReadLine();
    }
}

public sealed class Employee : IEquatable<Employee>
{
    public string Name { get; }

    public Employee(string name)
    {
        Name = name;
    }

    public override int GetHashCode()
    {
        return (Name != null ? Name.GetHashCode() : 0);
    }

    public bool Equals(Employee other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;

        return Name == other.Name;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;

        return Equals((Employee)obj);
    }

    public static bool operator ==(Employee left, Employee right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Employee left, Employee right)
    {
        return !Equals(left, right);
    }
}


A simple class Employee with a simple implementation of IEquatable<Employee> interface.


Just notice how the GetHashCode method is implemented.


When running the application, this is what we get:


Image by Ahmed Tarek

Now, stop the application, and run it again. This is what we get:


Image by Ahmed Tarek


See, both results are not the same.


Photo by Volkan Olmez on Unsplash

The Right Way to Fix This


ArrayEqualityComparer<T>


using System.Collections.Generic;

namespace StringGetHashCode
{
    public sealed class ArrayEqualityComparer<T> : IEqualityComparer<T[]>
    {
        private static readonly EqualityComparer<T> ElementComparer
            = EqualityComparer<T>.Default;

        public static ArrayEqualityComparer<T> Default => new();

        public bool Equals(T[] x, T[] y)
        {
            if (x == y)
            {
                return true;
            }

            if (x == null || y == null)
            {
                return false;
            }

            if (x.Length != y.Length)
            {
                return false;
            }

            for (var i = 0; i < x.Length; i++)
            {
                if (!ElementComparer.Equals(x[i], y[i]))
                {
                    return false;
                }
            }

            return true;
        }

        public int GetHashCode(T[] obj)
        {
            unchecked
            {
                var hash = 17;

                foreach (var element in obj)
                {
                    hash = hash * 31 + ElementComparer.GetHashCode(element);
                }

                return hash;
            }
        }
    }
}


StringExtensions


using System.Security.Cryptography;
using System.Text;

namespace StringGetHashCode
{
    public static class StringExtensions
    {
        public static int GetReliableHashCode(
            this string str,
            HashAlgorithm hashingAlgorithm) =>
            ArrayEqualityComparer<byte>.Default.GetHashCode(
                hashingAlgorithm.ComputeHash(Encoding.ASCII.GetBytes(str)));
    }
}


FixedEmployee


class Program
{
    static void Main(string[] args)
    {
        var fixedEmployee = new FixedEmployee("Ahmed");

        Console.WriteLine(fixedEmployee.GetHashCode());
        Console.ReadLine();
    }
}

public sealed class FixedEmployee : IEquatable<FixedEmployee>
{
    public string Name { get; }

    public FixedEmployee(string name)
    {
        Name = name;
    }

    public override int GetHashCode()
    {
        var hashCode = -1587353937;

        using (var ha = new System.Security.Cryptography.SHA1Managed())
        {
            hashCode = hashCode * -1521134295 + Name.GetReliableHashCode(ha);
        }

        return hashCode;
    }

    public bool Equals(FixedEmployee other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;

        return Name == other.Name;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;

        return Equals((FixedEmployee)obj);
    }

    public static bool operator ==(FixedEmployee left, FixedEmployee right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(FixedEmployee left, FixedEmployee right)
    {
        return !Equals(left, right);
    }
}


When running the application, this is what we get:


Image by Ahmed Tarek


Now, stop the application, and run it again. This is what we get:


Image by Ahmed Tarek


See, both results are the same.


Photo by MIO ITO on Unsplash

Summary

If you need the result of String.GetHashCode() to be persistent between your application run sessions, you will need to follow this way or another similar way. Otherwise, you will end up with a new result every time you stop and run the application.


That’s it, hope you found reading this article as interesting as I found writing it.


Also Published Here