ObjectHasher, czyli jak wyliczyć hash z obiektu

System nad którym obecnie pracuję wykonuje wiele zapytań do zewnętrznych systemów. Każde zapytanie generuje nie tylko opóźnienie w działaniu aplikacji, ale również dodatkowe koszty. Postanowiliśmy zaimplementować rodzaj cache’u po naszej stronie. Do tego celu potrzebowaliśmy obliczać hash z modelu który przychodził do systemu. Niniejsza notka pokazuje przykładową implementację komponentu do wyliczania hash’a z obiektu.

Obliczanie hash’a

Metod obliczających hash z dowolnego ciągu znaków jest bardzo dużo. Weźmy przykładową znalezioną w internecie.

string Hash(string input)
{
    var sb = new StringBuilder();
    using (var hash = MD5.Create())
    {
        byte[] result = hash.ComputeHash(Encoding.UTF8.GetBytes(input));
        foreach (byte b in result)
        {
            sb.Append(b.ToString("x2"));
        }
    }
    return sb.ToString();
}

Natomiast naszym ciągiem znaków będą sklejone ze sobą wartości właściwości modelu. Do ich uzyskania posłużę się refleksją.

string ExtractValuesFromProperties(object obj)
{
    var sb = new StringBuilder();
    var properties = obj.GetType()
        .GetProperties(BindingFlags.Public | BindingFlags.Instance);
    foreach (var property in properties)
    {
        if (property.PropertyType.FullName.StartsWith("MyProjectNamespace"))
        {
            sb.Append(ExtractValuesFromProperties(property.GetValue(obj)));
        }
        else
        {
            sb.Append($"#{property.GetValue(obj)}");
        }
    }
    return sb.ToString();
}

W powyższym kodzie najpierw tworzę StringBuildera, którego zadaniem będzie łączyć ze sobą wartości właściwości. Następnie wyciągam wszystkie publiczne właściwości. Iterując po każdej z nich sprawdzam czy jej typ pochodzi z mojego projektu. Jeśli typ właściwości jest z projektu, to wywołuję rekurencyjnie metodę ExtractValuesFromProperties() z jej wartością. Ma to na celu zebranie wartości z niżej położonych złożonych właściwości, które mogą być instancjami klas. Jeśli natomiast typ nie pochodzi z projektu, to wyciągam tylko wartość. Na koniec metoda zwracam sklejone ze sobą wartości wszystkich właściwości jako jeden ciąg znaków.

Ignorowanie właściwości

Załóżmy, że model może zawierać właściwości, które chcemy pominąć w procesie obliczania hash’a. Aby tego dokonać, można zdefiniować atrybut, który posłuży jako znacznik.

[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class HashIgnoreAttribute : Attribute
{
        
}

W tym momencie, podczas iteracji dodatkowo musimy sprawdzić czy powyższy atrybut jest zadeklarowany nad właściwością. Jeśli tak, to pomijamy wartość tej właściwości w wyniku.

// ...
if (property.PropertyType.FullName.StartsWith("MyProjectNamespace"))
{
    sb.Append(ExtractValuesFromProperties(property.GetValue(obj)));
}
else if (property.GetCustomAttribute<HashIgnoreAttribute>() == null)
{
    sb.Append($"#{property.GetValue(obj)}");
}
// ...

Ostateczna implementacja komponentu

Metoda wyliczająca, zamiast przyjmować ciąg znaków, przyjmuje dowolny obiekt. Z obiektu wyciągane są wartości publicznych właściwości żeby z nich na koniec wyliczyć hash.

public class ObjectHasher
{
    public static string Hash(object obj)
    {
        var sb = new StringBuilder();
        using (var hash = MD5.Create())
        {
            string values = ExtractValuesFromProperties(obj);
            byte[] result = hash.ComputeHash(Encoding.UTF8.GetBytes(values));
            foreach (byte b in result)
            {
                sb.Append(b.ToString("x2"));
            }
        }
        return sb.ToString();
    }

    private static string ExtractValuesFromProperties(object obj)
    {
        var sb = new StringBuilder();
        var properties = obj.GetType()
            .GetProperties(BindingFlags.Public | BindingFlags.Instance);
        foreach (var property in properties)
        {
            if (property.PropertyType.FullName.StartsWith("MyProjectNamespace"))
            {
                sb.Append(ExtractValuesFromProperties(property.GetValue(obj)));
            }
            else if (property.GetCustomAttribute<HashIgnoreAttribute>() == null)
            {
                sb.Append($"#{property.GetValue(obj)}");
            }
        }
        return sb.ToString();
    }
}

Poniższy zrzut ekranu prezentuje przykład wykorzystania.

ObjectHasher in LINQPad

Plik z kodem źródłowym (w formacie programu LINQPad): ObjectHasher.linq.

Podbij ↑

2 thoughts on “ObjectHasher, czyli jak wyliczyć hash z obiektu

    • mudzio,
      Wielkie dzięki za komentarz, dobrze posłuchać zdania innych osób.
      Wiesz co, gdybyśmy potrzebowali gdzieś jeszcze skorzystać z ExtractValuesFromProperties(), to zapewne tak by się to odbyło – dwie klasy. Natomiast na razie nie było takiej potrzeby, toteż jest jak jest, jest jak widać.
      Przeciążanie GetHashCode() jest jakąś opcją, aczkolwiek dość pracochłonną (dodatkowa robota w każdym modelu).

Skomentuj mudzio Anuluj pisanie odpowiedzi

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *