Korzeniem wyrażenia jest zawsze obiekt LambdaExpression, którego Body jest wywołaniem metody lub konstruktora, których argumenty będą poddane sprawdzeniu. Wywołanie metody powoduje odłożenie na drzewie obiektu MethodCallExpression, zaś sama metoda i informaje o niej (MethodInfo) jest dostępna przez property Method. Wywołanie konstruktora (np. new Service(...)) odkłada na drzewie obiekt NewExpression, umieszczając informacje o konstruktorze (ConstructorInfo) w property Constructor.
Argumenty przekazane do wywołania metody lub konstruktora dostępne są poprzez property Arguments zarówno w MethodCallExpression jak i NewExpression.
Tyle wstępu i rozróżniania przypadków - przejdźmy do najciekawszej, choć, jak się okaże, banalnej części - sprawdzania argumentów.
W tej chwili interesują nas wyłącznie argumenty przekazane do funkcji/konstruktora. Każdy argument jest typu Expression - co jest dosyć naturalne, bo w końcu możemy naszą funkcję wywołać na wiele sposobów:
public void Function(string arg1, User arg2, int arg3) { this.CheckArg(x => x.Function(arg1, new User(), 5)); this.CheckArg(x => x.Function(arg1, arg2, arg3)) ... };
Pierwsze wywołanie spowoduje utworzenie następujących argumentów:
- MemberExpression
- NexExpression
- ConstantExpression
Jeśli przyjrzymy się tym obiektom, to okazuje się, że tylko ConstantExpression posiada property Value - a jak odczytać pozostałe? W zasadzie należałoby przejść po całym drzewie za każdym razem patrząc na to, jakiego typu jest wyrażenie i odpowiednie przetwarzanie:
- NewExpression wiemy jak przetworzyć - wywołać konstruktor tak jak przy refleksji
- MemberExpression zawiera w sobie Expression, które może być dowolnego typu z podanych powyżej (i nie tylko)
- ConstantExpression prawie od razu daje wartość
Expression.Lambda(expression).Compile().DynamicInvoke();
Iterując po wszystkich argumentach bez problemu już sprawdzimy czy dany argument nie jest nullem.
Pozostaje kwestia wyświetlania błędów - jeśli po prostu rzucimy wyjątek, to na stacktrace będziemy w funkcji przetwarzającej wyrażenia lambda, a nie w funkcji, której argumenty sprawdzamy (chociaż ta też się pojawi, ale głębiej). Tego nie przeskoczymy - możemy jednak podać ładny komunikat o błędzie, w którym będzie podana nazwa funkcji - a to już wystarczy do łatwego i szybkiego zorientowania się o co chodzi, w przypadku wystąpienia błędu.
Na początku tej części wspomniałem, że informacje o metodzie znajdują się w property Method klasy MethodCallExpression, dzięki czemu otrzymujemy MethodInfo. Zwykły ToString() daje nam dosyć dobrą nazwę funkcji, więc proponuję ją zostawić.
A co w przypadku konstruktora? Spróbujmy Constructor.ToString() - przykładowy wynik:
Void .ctor(System.String, Int32, Some.User)
Hm.. nie za dobrze - skąd w runtime po otrzymaniu błędu będziemy wiedzieć którego obiektu to konstruktor? Na szczęście NewExpression (a właściwie to klasa bazowa Expression) posiada property Type (czyli typ obiektu reprezentowanego przez wyrażenie), z którego możemy odczytać FullName:
newExpression.Type.FullName + " " + newExpression.Constructorco daje w wyniku:
Some.HelperWithConstructor Void .ctor(System.String, Int32, Some.User)
Wydaje mi się, że jest to akceptowalne.
Przy rzucaniu ArgumentNullException należałoby podać również nazwę argumentu, który był nullem - wystarczy wywołać expression.Member.Name.
W tej chwili kod nam bardzo ładnie działa - jest dokonywana pełna walidacja nieobecności nulla, wyjątek jest komunikatywny zarówno pod względem nazw argumentów jak i treści, a co najlepsze - sprawdzanie jest dokonywane z pełną kontrolą typów na poziomie kompilatora - jeśli dodamy nowy argument do funkcji bez rozszerzenia walidacji, to kompilator wyrzuci błąd. Z kolei samo rozszerzenie walidacji jest banalne - po prostu dopisujemy kolejny argument do wywołania.
Dociekliwy czytelnik zauważy, że na początku rozmawialiśmy o zastąpieniu np. takiej konstrukcji:
if (string.IsNullOrEmpty(actionName)) { throw new ArgumentNullException("actionName"); }a kod, który napisaliśmy sprawdzi wyłącznie obecność null - w przypadku stringów przepuści wartość string.Empty (lub po prostu ""). Co więc należy zrobić? Oczywiście dopisać warunek specyficzny dla stringów - wiemy jak pobrać typ argumentu (jak i funkcji :)), więc poprzez zwykłe sprawdzenie typów, możemy dodatkowo sprawdzać:
if (argument.Type == typeof(String)) { if (string.IsNullOrEmpty(value as string)) { throw ... } }
Brak komentarzy:
Prześlij komentarz