Czasami zdarza się, że muszę przeprowadzić refaktoryzację, w której Resharper nie może mi pomóc. W moim ostatnim poście opisałem, jak przydatne mogą być wyrażenia regularne przy takiej pracy: Refaktoryzacj przy pomocy wyrażeń regularnych w Visual Studio
Tym razem sprawa jest inna i prosta podmiana nie zadziała w tym przypadku.
Domyślna wartość dla właściwości
Natrafiłem na kod, w którym atrybut DefaultValue
był często używany, ale w nie tak, jak powinien. Atrybut DefaultValue
jest częścią platformy .Net Framework i jest używany przez generatory kodu w celu sprawdzenia, czy wartość domyślna właściwości jest taka sama jak wartość bieżąca i czy należy wygenerować dla tej właściwości kod. Nie chcę wchodzić w szczegóły, ale możesz zajrzeć do tego artykułu, aby uzyskać więcej informacji.
W moim przypadku musiałem zmienić wszystkie domyślne wartości na przypisania właściwości podczas tworzenia klasy. Problem polegał na tym, że ta klasa miała ponad 1000 takich właściwości, a zrobienie tego ręcznie nie tylko zajęłoby dużo czasu, ale mogłoby potencjalnie wprowadzić błędy.
Spójrzmy na przykładowy kod:
public class DefaultSettings { public DefaultSettings() { // assignments should be here } [DefaultValue(3)] public int LoginNumber { get; set; } [DefaultValue("PrimeHotel")] public string HotelName { get; set; } [DefaultValue("London")] public string Town { get; set; } [DefaultValue("Greenwod")] public string Street { get; set; } [DefaultValue("3")] public string HouseNumber { get; set; } [DefaultValue(RoomType.Standard)] public RoomType Type { get; set; } }
Tutaj przydaje się refleksja. Mógłbym łatwo zidentyfikować atrybuty właściwości dla danego typu, ale jak zmienić je w kod?
Napiszmy test jednostkowy! Test jednostkowy to mały fragment kodu, który może korzystać z niemal każdej klasy z testowanego projektu i co najważniejsze – można go łatwo uruchomić. 💪
[TestFixture] public class PropertyTest { [Test] public void Test1() { var prop = GetProperties(); Assert.True(true); } public string GetProperties() { var sb = new StringBuilder(); PropertyDescriptorCollection sourceObjectProperties = TypeDescriptor.GetProperties(typeof(DefaultSettings)); foreach (PropertyDescriptor sourceObjectProperty in sourceObjectProperties) { var attribute = (DefaultValueAttribute)sourceObjectProperty.Attributes[typeof(DefaultValueAttribute)]; if (attribute != null) { // produce a string } } return sb.ToString(); } }
To prosty kod, który uruchamia metodę GetProperties
. Metoda ta pobiera PropertyDescriptorCollection
która reprezentuje wszystkie właściwości w klasie DefaultSettings
. Następnie dla każdej z nich sprawdzamy, czy zawiera DefaultValueAttribute
. Teraz pozostaje tylko wygenerowanie kodu dla każdej właściwości przy użyciu klasy StringBuilder
. Domyślasz się jak? Jest to łatwiejsze niż może się wydawać.
Sprawdźmy jak kod będzie się różnił dla poszczególnych typów:
public string GetProperties() { var sb = new StringBuilder(); PropertyDescriptorCollection sourceObjectProperties = TypeDescriptor.GetProperties(typeof(DefaultSettings)); foreach (PropertyDescriptor sourceObjectProperty in sourceObjectProperties) { var attribute = (DefaultValueAttribute)sourceObjectProperty.Attributes[typeof(DefaultValueAttribute)]; if (attribute != null) { if (sourceObjectProperty.PropertyType.IsEnum) { sb.AppendLine($"{sourceObjectProperty.Name} = {sourceObjectProperty.PropertyType.FullName.Replace("+", ".")}.{attribute.Value};"); } else if (sourceObjectProperty.PropertyType.Name.Equals("string", StringComparison.OrdinalIgnoreCase)) { sb.AppendLine($"{sourceObjectProperty.Name} = \"{attribute.Value}\";"); } else { var value = attribute.Value == null ? "null" : attribute.Value; sb.AppendLine($"{sourceObjectProperty.Name} = {value};"); } } } return sb.ToString(); }
Dla typu enum, musimy wyświetlić przestrzeń nazw(namespace) oraz typ i jego wartość po kropce. Dla ciągów znaków, musimy wstawić apostrofy, a dla typu nullowalnego, wartością powinno być null. Dla innych musimy tylko przypisać wartość.
Jesteś ciekawy co z tego wyszło? 🤔
Powiedziałbym, że całkiem, całkiem:)
Kiedy wkleję go do mojego konstruktora, kod będzie wyglądał następująco:
public DefaultSettings() { LoginNumber = 3; HotelName = "PrimeHotel"; Town = "London"; Street = "Greenwod"; HouseNumber = "3"; Type = PrimeHotel.Web.Models.RoomType.Standard; }
I wiesz co? Działa!
Podsumowanie
Używanie refleksji nie jest najlepszą praktyką, ale jest ona bardzo potężnym narzędziem. W tym przykładzie pokazałem, jak jej użyć i wywołać ten kod w bardzo prosty sposób – jako test jednostkowy. Nie jest to kod, który bym commitował, ale w przypadku podejścia prób i błędów jest świetny. Jeśli natrafiłeś na takie ręczne zadanie w swoim kodzie, spróbuj je zautomatyzować 😎
Mam nadzieję, że nauczyłeś się czegoś dzisiaj i jeżeli tak, to świetnie! 🍺
Nie zapomnij zapisać się na newsletter, aby nie przegapić kolejnych postów. 📣