Pisząc jeden system, doszliśmy do wniosku w firmie, że trzeba zrobić testy integracyjne. Wiemy, że poszczególne części systemu same w sobie działają tak jak chcemy, albo dokładniej tak jak myślimy, że mają działać :)

Część systemu, którą chcieliśmy przetestować była odpowiedzialna za słanie wiadomości poprzez szynę, jej odbiór, następnie odpowiedni processing uzależniony od danych wejściowych i tych w bazie danych który kończy się wygenerowaniem odpowiednio dużej lub małej liczby rekordów w bazie danych.

Ze względu na to, że nasze wiadomości które ślemy są opakowane w zdarzenia które zachodzą w systemie, mamy osobą bibliotekę która udostępnia nam wszystkie możliwe eventy jakie mogą zajść oraz handlery do nich z wysłaniem wiadomości. Każdy event jest immutable. Raz stworzony ma tak pozostać do końca swego żywota. Co oznacza, że event ma odpowiedni konstruktor i własności z private set.

By móc zoptymalizować nasze testy oraz ograniczyć czas spędzony nad pisaniem aplikacji odpalającej testy postanowiłem wykorzystać z serializowane eventy do jsona i zapisane w odpowiednich plikach – każdy plik nazywa się tak samo jak event (no chyba, że jest ich więcej niż jeden, to wtedy dodatkowo dodajemy _X gdzie X to jakaś tam liczba). Zamysł był taki by te eventy deserializować w aplikacji testowej i następnie je odpalać – automatycznie, lub ręcznie.

Jednak natrafiliśmy oczywiście na zonk, z deserializacją obiektu który nie posiada konstruktora bezparametrowego. Jedna z możliwości kiedy korzysta się z Newtonsoft.Json do deserializacji umożliwia wykorzystanie prywatnego konstruktora bez parametrów w danej klasie. Wystarczy w ustawieniach podać:

var settings = new JsonSerializerSettings
{
    ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
};

To jednak wymaga zmiany wszystkich eventów tak by posiadały prywatny konstruktor. Nie chciałem tego robić

Drugim sposobem jest wykorzystanie ustawień, które działają wtedy kiedy nazwa parametru konstruktora jest taka sama jak nazwa własności czy też danych zapisanych w JSON. Wystarczy by do konstruktora dodać atrybut [JsonConstructor]. To znów wymaga zmiany w eventach a tego nie chciałem.

W końcu mamy możliwości sami określić jak obiekt będzie tworzony, przez jego stworzenie :)

To co musimy zrobić to przy ustawieniach podać implementację konwertera (można to zrobić za pomocą ustawień Converters lub przy wywoływaniu metody DeserializeObject). Ja wybrałem opcję przez ustawienia, ale o tym dlaczego, to zaraz.

var settings = new JsonSerializerSettings
{
    Converters = new []
    {
        new EventConverter<T>()
    }
};

Ok, ale trzeba nasz własny konwerter oprogramować. A skoro wiemy, że nie mamy domyślnego konstruktora, prywatnego konstruktora tylko konstruktor z parametrami, to możemy albo za pomocą refleksji próbować ustawić wszystkie wartości na domyślne albo wykorzystać metodę FormatterServices.GetUninitializedObject(type) która stworzy nam nowy obiekt, bez jego inicjalizacji (bez wywoływania żadnego konstruktora). Dzięki czemu nasz konwerter wygląda tak:

public class EventConverter<T> : CustomCreationConverter<T>
{
    public override T Create(Type objectType)
    {
        return (T)FormatterServices.GetUninitializedObject(objectType);
    }
}

To spowoduje, że Newtonsoft.Json będzie miał już instancję naszego obiektu. Teraz musi on móc go z deserializować, czyli przypisać własności. Tylko że u nas są same private set. Na szczęście można to obejść za pomocą ustawień biblioteki ustawiając ContractResolver w ustawieniach deserializacji:

var settings = new JsonSerializerSettings
{
    ContractResolver = new PrivateSetterContractResolver(),
    Converters = new []
    {
        new EventConverter<T>()
    }
};

Teraz wystarczy tylko implementacja resolvera (znalazłem to w sieci):

public class PrivateSetterContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);

        if (!prop.Writable)
        {
            var property = member as PropertyInfo;
            if (property != null)
            {
                var hasPrivateSetter = property.GetSetMethod(true) != null;
                prop.Writable = hasPrivateSetter;
            }
        }

        return prop;
    }
}

Która informuje wewnętrznie JSON.NET, że ta własność to tak naprawdę ma „dostępnego settera”.

Dzięki tym zabiegom podczas deserializacji nie tylko tworzymy obiekt który nie ma pustego konstruktora, ale także jesteśmy wstanie go w pełni zainicjalizować z poziomu JSON.NET. Skończenie aplikacji testującej to już była kwestia chwili. Teraz każdy może odpalić test, zmodyfikować json odpalić ponownie (albo exe, albo po prostu manualnie określony event) i zweryfikować czy aby na pewno cały system działa tak jak powinien wraz z ustawieniami MSMQ.

Te 20 linijek kodu, oszczędziły mnie mozolnych godzin pracy. I mam nadzieję że komuś się też przydadzą :)

PS.: to jest drugi raz w życiu kiedy korzystam z FormatterServices.GetUninitializedObject i drugi raz ratuje mi to trochę tyłek. Warto więc wiedzieć, że taka metoda istnieje :)

5 KOMENTARZE

  1. Super, szukałem kiedyś czego takiego. Ale to jednak hak jest: wszystko będzie działać dopóki nie przejdziecie na C# 6.0 i nie zaczniecie używać read-only auto-properties :)))

Comments are closed.