Czy można serializować interfejsy C#? Można!

Ostatnio chciałem serializować i deserializować liste gdzie typem listy był interfejs (coś takiego: List<IPerson>). Chciałem do tego wykorzystać XmlSerializer-a. Okazało się, że przy użyciu tej klasy nie można serializować listy interfesjów do XML-a. Już chciałem szukać dlaczego nie można, ale miałem trochę czasu i stwierdziłem, że tym razem zrobimy to w trudny sposób. Poniżej prezentuję jak można serializować i deserializować listę interfejsów.

Po przeczytaniu tego postu dowiesz się:

  • Dlaczego nie można serializować interfejsów?
  • Jak tworzyć własne typy danych w trakcie działania aplikacji tzw. dynamiczne tworzenie typów.
  • Jak stworzyć właściwości, metody i pola w ILAsm?

Jest sporo kodu więc cały kod jest dostępny w kompilującej się bibliotece na githubie.

https://github.com/przemekwa/InterfaceToXML

Dodatkowo dostępne są testy, dzięki którym można sobie posprawdzać jak co działa.

Kontekst

Mamy takie obiekty w programie:

    public interface IPerson
    {
        string FirstName { get; set; }
    }

    public class Person : IPerson
    {
        public string FirstName
        {
            get; set;
        }
    }

Dodatkowo mamy listę:

var lista = new List<IPerson>();

lista.Add(new Person { FirstName="Przemek" });
lista.Add(new Person { FirstName="Jola" });

Problem do rozwiązania

Jeżeli będziemy chcieli tą listę serializować używając klasy XmlSerializer dostaniemy błąd.

xml_serializacja_blad

Dlaczego nie można serializować interfejsów?

To proste. Interfejs nie jest czymś, co przechowuje konkretne wartości. To tylko „umowa” na to, co dany obiekt ma mieć, więc trudno z „umowy” wyciągnąć konkretny obiekt.

Co prawda przy samej serializacji to nie jest takie problematyczne, ponieważ jeśli lista nie jest pusta, to zawiera konkretne obiekty implementujące interfejs, więc jest z czego brać wartości.

Generyczny kod serilizacji interfejsu poniżej:

public static class XMLInterfaceSerialization
    {
        public static void Serialize<T>(IEnumerable<T> list, XmlWriter xml)
        {
            var interfaceType = list.AsQueryable().ElementType;

            if (!interfaceType.IsInterface)
            {
                throw new ArgumentException("Generic arguments is not a interface");
            }

            var interfaceName = interfaceType.Name;

            xml.WriteStartElement(interfaceName+"Root");

            foreach (var obj in list)
            {
                xml.WriteStartElement(interfaceName);

                foreach (var p in obj.GetType().GetInterface(interfaceName).GetProperties())
                {
                    xml.WriteElementString(p.Name, p.GetValue(obj, null).ToString());
                }

                xml.WriteEndElement();
            }

            xml.Close();
        }

Wszystko powinno być jasne. Z ciekawszych linijek to:

 var interfaceType = list.AsQueryable().ElementType;

Tak można pobrać typ elementów danej listy. Są jeszcze inne sposoby ale ten był najprostszy.

Kolejna ciekawa linijka:

foreach (var p in obj.GetType().GetInterface(interfaceName).GetProperties())
                {
                    xml.WriteElementString(p.Name, p.GetValue(obj, null).ToString());
                }

Tutaj iterujemy po wszystkich właściwościach w interfejsie i tworzymy elementy XML dla każdej z nich. Jest to najprostsze podejście i nie zakłada typów listowych, jednak do celów edukacyjnych jak najbardziej się nadaje.

Deserilizacja

Tutaj pojawia się problem. Jeśli będziemy czytać plik z danymi i będziemy znać interfejs, to brakuje nam obiektu, który ten interfejs implementuje. Zakładam, że podczas deserializacji nie mamy obiektu, który implementuje interfejs.

Dlatego utworzymy typ implementujący interfejs już w trakcie działania programu.

Tworzenie własnego typu opiera się na założeniu, że najpierw stworzymy bibliotekę, w niej stworzymy moduł i w tym module stworzymy nowy typ. Typ będzie posiadał właściwości zgodne z potrzebnym interfejsem,  co oznacza, że musimy stworzyć metody get i set do każdej z właściwości. Cała metoda poniżej. W dalszej części postu opisze jej działanie.

 public static IEnumerable<object> Deserialize(Type interfaceType, string xmlFileName)
        {
            if (!interfaceType.IsInterface)
            {
                throw new ArgumentException("Generic arguments is not a interface");
            }

            var aName = new AssemblyName("temp");

            var appDomain = Thread.GetDomain();

            var aBuilder = appDomain.DefineDynamicAssembly(aName, AssemblyBuilderAccess.RunAndSave);

            var mBuilder = aBuilder.DefineDynamicModule(aName.Name, aName + ".dll");

            TypeBuilder tBuilder = mBuilder.DefineType("TempClass", TypeAttributes.Class | TypeAttributes.Public);

            tBuilder.AddInterfaceImplementation(interfaceType);

            foreach (var propertyInfo in interfaceType.GetProperties())
            {
                PropertyBuilder property = tBuilder.DefineProperty(propertyInfo.Name, PropertyAttributes.HasDefault, CallingConventions.Any, propertyInfo.PropertyType, null);
                MethodBuilder getMethod = tBuilder.DefineMethod("get_" + propertyInfo.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual, propertyInfo.PropertyType, Type.EmptyTypes);
                FieldBuilder privateFiled = tBuilder.DefineField("_" + propertyInfo.Name.ToLower(), propertyInfo.PropertyType, FieldAttributes.Private);

                ILGenerator methodIlGetGenerator = getMethod.GetILGenerator();

                methodIlGetGenerator.Emit(OpCodes.Ldarg_0);
                methodIlGetGenerator.Emit(OpCodes.Ldfld, privateFiled);
                methodIlGetGenerator.Emit(OpCodes.Ret);

                MethodBuilder setMethod = tBuilder.DefineMethod("set_" + propertyInfo.Name,
                MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null,
                new[] { propertyInfo.PropertyType });

                ILGenerator methodSetGenerator = setMethod.GetILGenerator();

                methodSetGenerator.Emit(OpCodes.Ldarg_0);
                methodSetGenerator.Emit(OpCodes.Ldarg_1);
                methodSetGenerator.Emit(OpCodes.Stfld, privateFiled);
                methodSetGenerator.Emit(OpCodes.Ret);

                property.SetSetMethod(setMethod);
                property.SetGetMethod(getMethod);
            }

            Type retval = tBuilder.CreateType();

            var xml = new XmlDocument();

            xml.Load(xmlFileName);

            var result = new List<object>();

            var xmlNodeList = xml.SelectNodes("//" + interfaceType.Name);

            if (xmlNodeList == null) return result;

            foreach (XmlNode x in xmlNodeList)
            {
                dynamic t = Activator.CreateInstance(retval);

                foreach (XmlNode c in x.ChildNodes)
                {
                    foreach (var pro in t.GetType().GetInterface(interfaceType.Name).GetProperties())
                    {
                        if (pro.Name == c.Name)
                        {
                            pro.SetValue(t, c.InnerText, null);
                        }
                    }
                }

                result.Add(t);
            }
            return result;
        }

Najważniejszą częścią prócz dodania do naszego typu implementacji interfejsu jest budowanie właściwości, które są w interfejsie.

W kodzie iterujemy po właściwościach w interfejsie i dla każdej z nich tworzymy właściwość w tworzonym typie.

Tworzenie pola

FieldBuilder privateFiled = tBuilder.DefineField("_" + propertyInfo.Name.ToLower(), propertyInfo.PropertyType, FieldAttributes.Private);

Opisanie wszystkich parametrów mocno wykracza poza ten post więc nie będe się wgłębiał. Krótko mówiąc tworzymy prywatne pole. Nazwa pola nie ma znaczenia.

Tworzenie właściwości

PropertyBuilder property = tBuilder.DefineProperty(propertyInfo.Name, PropertyAttributes.HasDefault, CallingConventions.Any, propertyInfo.PropertyType, null);

Krótko mówiąc podajmy nazwę właściwości i typ dla niej.

Tworzenie metody GET

MethodBuilder getMethod = tBuilder.DefineMethod("get_" + propertyInfo.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual, propertyInfo.PropertyType, Type.EmptyTypes);

Tworzymy metodę dla get-a. Robimy to ponieważ typ, który tworzymy, jest generowany dynamicznie, przez co nie ma kiedy go skompilować do kodu pośredniego(IL).

Budując własny typ tworzymy w kodzie pośrednim (IL), dlatego też auto-właściwości dostępne od C# 3.0 (get;set;) nie są już auto ; )

Ważnym parametrem jest tutaj MethodAttributes.Virtual, który po prostu wskazuje, że metoda jest virtualna.
Dodatkowo ważna jest również nazwa. Koniecznie musi zaczynać się od słowa get_NazwaPola inaczej interfejs nie znajdzie metody get i wystąpi wyjątek – typ nie implementuje interfejsu.

Tworzenie metody SET

MethodBuilder setMethod = tBuilder.DefineMethod("set_" + propertyInfo.Name,
                MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null,
                new[] { propertyInfo.PropertyType });

Stworzenie metody set jest podobne do tworzenia metody get z tą różnicą, że zmienia się nazwa metody.

Emitowanie IL-a

Wyżej stworzyliśmy, tak jak by strukturę naszego obiektu. Teraz musimy dodać kod do metod, które stworzyliśmy. Aby to zrobić musimy wyemitować konkretne instrukcje dla IL-a.

Teraz czas na zejście poziom niżej w sztuce programowania. Jak wiemy biblioteki w C# i w VB kompilują się do tzw. kodu pośredniego (IL). IL to pośredni kod, który jest wykonywany przez CLR. CLR zamienia go na kod maszynowy z bezpośrednimi instrukcjami dla CPU. IL ma strukturę binarną, jednak jest możliwość dekompilacji IL-a  do postaci czytelnej dla człowieka jest to tzw. ILAsm (IL Assembler). Do podglądania kodu IL używam ILSpy.

Ciało metody GET

Celem jest napisanie instrukcji IL, które zwrócą wartość pola prywatnego.

ILGenerator custNameGetIL = getMethod.GetILGenerator();
                custNameGetIL.Emit(OpCodes.Ldarg_0);
                custNameGetIL.Emit(OpCodes.Ldfld, privateFiled);
                custNameGetIL.Emit(OpCodes.Ret);

Dla metody get pobieramy generator IL. Dzięki, któremu napiszemy kod dla IL-a. Metoda Emit zajmuje się tworzeniem instrukcji dla IL.

  • (OpCodes.Ldarg_0) – instrukcja ta ładuje na stos instancję metody get. Czyli takie this.
  • (OpCodes.Ldfld, privateFiled) – instrukcja ta szuka referencji do obiektu privateFiled w obiekcie, który jest załadowany na stos.
  • (OpCodes.Ret) – kończy metodę i zwraca jeśli jest coś do zwrócenia na stos.

Ciało metody SET

Celem jest ustawienie wartości pola prywatnego

ILGenerator custNameSetIL = setMethod.GetILGenerator();

                custNameSetIL.Emit(OpCodes.Ldarg_0);
                custNameSetIL.Emit(OpCodes.Ldarg_1);
                custNameSetIL.Emit(OpCodes.Stfld, privateFiled);
                custNameSetIL.Emit(OpCodes.Ret);

Część instrukcji jest podobna jak przy get

  • (OpCodes.Ldarg_0) – ładujemy instancję metody set.
  • (OpCodes.Ldarg_1) – ładujemy wartość argumentu „value”. Czyli wartość obiektu, który przesyłamy do metody set.
  • (OpCodes.Stfld, privateFiled) – instrukcja podmienia wartość z pola privateFiled na ta która jest na stosie. Na ta którą w poprzedniej instrukcji włożyliśmy na stos.
  • (OpCodes.Ret) – kończy metodę i zwraca jeśli jest coś do zwrócenia na stos.

Przypisanie metod GET i SET:

Ostatnią rzeczą jest przypisanie metod get i set do właściwości:

  property.SetGetMethod(getMethod);
  property.SetSetMethod(setMethod);

Nazwy mówią same za siebie. Do właściwości przypisujemy metodę set i get.

Teraz tworzymy już gotowy typ:

Type newType= tBuilder.CreateType();

Voilà!

Stworzyliśmy nowy typ danych implementujący interfejs w czasie działania programu.

Deserializacja XML-a

Ten kod już jest prosty i zajmuje się wczytaniem elementów z pliku i stworzeniem obiektu dla każdego elementu.

var xml = new XmlDocument();

            xml.Load(xmlFileName);

            var result = new List<object>();

            var xmlNodeList = xml.SelectNodes("//" + interfaceType.Name);

            if (xmlNodeList == null) return result;

            foreach (XmlNode x in xmlNodeList)
            {
                dynamic t = Activator.CreateInstance(newType);

                foreach (XmlNode c in x.ChildNodes)
                {
                    foreach (var pro in t.GetType().GetInterface(interfaceType.Name).GetProperties())
                    {
                        if (pro.Name == c.Name)
                        {
                            pro.SetValue(t, c.InnerText, null);
                        }
                    }
                }

                result.Add(t);
            }
            return result;
        }

Najważniejsza linijka to:
[code lang="csharp"]
dynamic t = Activator.CreateInstance(retval);

Słowo kluczowe dynamic jest tutaj bardzo potrzebne. Podczas dynamicznego tworzenia obiektów nie możemy w kodzie dokładnie określić typu dla obiektu. Activator.CreateInstance zwraca po prostu object i nie mamy możliwości użycia słowa kluczowego as aby potraktować ten obiekt jako instancję interfejsu. Dzięki dynamic kod się kompiluje i kompilator ufa nam, że wiemy, że to będzie jakiś obiekt.

Opisałem dwie metody statyczne, które pozwalają na serializację i deserializację list, gdzie elementem listy jest interfejs. W moim podejściu pokazałem jak stworzyć typy dynamiczne.
Nie jest to jedyny sposób na podejście do tematu serializacji interfejsów, jednak moje podejście jest mocno edukacyjne.

Jeśli macie jakieś uwagi, to zapraszam do komentowania.

2 komentarze do “Czy można serializować interfejsy C#? Można!

  1. Zamiast samemu emitować ILa można skorzystać na przykład z Castle.DynamicProxy. Jedna linijka kodu i obiekcik implementujący zadany interfejs gotowy. :)

Możliwość komentowania została wyłączona.