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.
Plik z kodem źródłowym (w formacie programu LINQPad): ObjectHasher.linq.
Aż się prosi o podzielenie na 2 klasy i refactoring :).
Ps. dlaczego nie przeciązacie GetHasha w konkretnych klasach ?:)
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).