Niedawno przedstawiłem TokenContext, klasę pomocniczą upraszczającą wyławianie kontekstu z masy logów tworzonych w aplikacji wielowątkowej, gdzie wiele operacji przeplata się nawzajem.
Regularnie jednak zdarza się, że interesuje nas nie tyle jedna szczególna operacje co cały cykl życia konkretnego obiektu. Kumpel, z którym pracuję nad "głównym" projektem, zaproponował stosowanie takiej, bardzo prostej, logicznej i przydatnej, konwencji:
1: _log.Debug("User#{0} did something", user.Id);
Albo:
1: _log.Debug("Order#{0} processed", order.Id);
Oczywiście to uproszczony przykład, ale wiadomo o co chodzi. Dzięki temu przetwarzając logi w poszukiwaniu wszystkich informacji o użytkowniku ID=125 wyfiltruję linijki z tekstem "User#125" i mam czego potrzebowałem.
Ostatnio w mniejszym projekcie postanowiłem przetestować jak można by to uprościć jeszcze bardziej, żeby nie klepać ręcznie nazwy typu i hasha. Rozwiązanie przychodzi do głowy samo, a obrazują je te banalne testy:
1: public class ObjectLogInfoTests 2: { 3: public class WithId 4: { 5: public virtual int Id { get; set; } 6: } 7: 8: public class WithoutId 9: { 10: public virtual int SomeOtherProperty { get; set; } 11: } 12: 13: [Fact] 14: public void should_return_type_name_with_hash_and_id_if_class_has_Id_property() 15: { 16: var o = new WithId { Id = 8 }; 17: 18: string logInfo = o.LogInfo(); 19: 20: Assert.Equal("WithId#8", logInfo); 21: } 22: 23: [Fact] 24: public void should_return_type_name_if_class_does_not_have_Id_property() 25: { 26: var o = new WithoutId(); 27: 28: string logInfo = o.LogInfo(); 29: 30: Assert.Equal("WithoutId", logInfo); 31: } 32: }
Klasa implementująca te założenia jest jeszcze "banalniejsza":
1: public static class ObjectLoggingExtensions 2: { 3: public static string LogInfo(this object @this) 4: { 5: Type type = @this.GetType(); 6: var idProperty = type.GetProperty("Id"); 7: object idValue = null; 8: if (idProperty != null) 9: { 10: idValue = idProperty.GetValue(@this, null); 11: } 12: if (idValue == null) 13: { 14: return type.Name; 15: } 16: else 17: { 18: return "{0}#{1}".FormatWith(type.Name, idValue); 19: } 20: } 21: }
Dzięki temu podany na początku przykład wygląda teraz tak:
1: _log.Debug("{0} did something", user.LogInfo());
Oczywiście narzuca to na system konwencje nazywania właściwości "Id", jednak w tym konkretnym przypadku nie był to problem. Bez wielkiego nakładu pracy można by rozszerzyć tą klasę aby dodatkowo szukała na przykład właściwości o nazwie "[TypeName]Id".
Początkowo przez chwilę zastanowiłem się nad tym, czy wykorzystanie refleksji jest w tym przypadku słuszne ze względów wydajnościowych, jednak… nie oszukujmy się, przy kontakcie z bazą danych takie coś nie ma absolutnie żadnego znaczenia. Testowo puściłem wykonanie podobnej instrukcji w pętli milion razy i zajęło to około sekundy.
Tak czy siak rozwiązanie się póki co sprawdza (chociaż nie pokrywa wszystkich scenariuszy w których loguję informację o obiektach).
Coraz bardziej przekonuję się do tego, że zbudowanie odpowiednich pomocników wokół konceptu "logowania" jest istotną częścią systemu. Logowanie powinno być bezbolesne, momentalne i naturalne. Im mniej klawiszy trzeba wcisnąć do uzyskania informacji w logu – tym lepiej.