Dynamiczne tworzenie właściwości (properties)

Na początek geneza powstania problemu. Otóż jest wiele starszych systemów, które komunikują się z bazą danych za pomocą procedur. Otrzymują w ten sposób informacje na przykład w postaci obiektu DataSet czy DataTable, a następnie na nich wykonywane są różne operacje.
Mój problem, a raczej pytanie, powstało, gdy któryś raz z kolei brałem pierwszy wiersz z otrzymanych wyników, a następnie tworzyłem z nich konkretny tekst za pomocą string.Format()“Czy nie można tego przyśpieszyć?”. W ten sposób powstał pomysł by wykorzystać metodę rozszerzeniową FormatWith(). Trudność zagadnienia polega na tym, iż ta metoda wykorzystuje właściwości (properties) przesłanego do niej obiektu. Jak wiadomo, właściwości są statycznymi elementami klasy, a ja potrzebowałem tworzyć je dynamicznie – każda kolumna to property z wartością wiersza. “Czy jest to możliwe do osiągnięcia?”. Wtedy przypomniało mi się, iż czytałem kiedyś ciekawy artykuł na codeguru.pl, w którym Paweł Gorczyński zaprezentował jak zaimplementować samomodyfikujący się program. Korzystając z wspaniałego źródła, którym jest MSDN, stworzyłem statyczną klasę zawierającą metodę rozszerzającą dla DataRow, która przedstawia się następująco:

public static class ExtensionMethod
{
    static CodeCompileUnit compileUnit;
    static CodeNamespace myNamespace;
    static CSharpCodeProvider csc;
    static CompilerParameters parameters;
    static int counter;

    static ExtensionMethod()
    {
        counter = 0;
        compileUnit = new CodeCompileUnit();
        myNamespace = new CodeNamespace("DynamicProperties");
        myNamespace.Imports.Add(new CodeNamespaceImport("System"));
        compileUnit.Namespaces.Add(myNamespace);

        csc = new Microsoft.CSharp.CSharpCodeProvider(new Dictionary<string, string>() 
        	{ { "CompilerVersion", "v2.0" } });
        parameters = new CompilerParameters(new[] { "System.dll" }, "DynamicProperties.dll", true) 
            { GenerateExecutable = false, GenerateInMemory = true };
    }

    public static object ToObjectWithProperties(this System.Data.DataRow row)
    {
        CodeTypeDeclaration myClass = new CodeTypeDeclaration("DynamicClass" + (++counter));
        myNamespace.Types.Add(myClass);

        foreach (System.Data.DataColumn item in row.Table.Columns)
        {
            string fieldName = string.Format("_{0}", item.ColumnName);
            myClass.Members.Add(new CodeMemberField(typeof(string), fieldName));

            var prop = new CodeMemberProperty();
            prop.Name = item.ColumnName;
            prop.Type = new CodeTypeReference(typeof(string));
            prop.Attributes = MemberAttributes.Public;
            prop.GetStatements.Add(new CodeMethodReturnStatement(
            	new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), fieldName)));
            prop.SetStatements.Add(new CodeAssignStatement(
            	new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), fieldName),
                new CodePropertySetValueReferenceExpression()));

            myClass.Members.Add(prop);
        }

        CompilerResults results = csc.CompileAssemblyFromDom(parameters, new[] { compileUnit });
        results.Errors.Cast<System.CodeDom.Compiler.CompilerError>()
        	.ToList().ForEach(error => Console.WriteLine(error.ErrorText));

        var myType = results.CompiledAssembly.GetType(string.Format("{0}.{1}", 
        	myNamespace.Name, myClass.Name), true, true);
        object myObject = Activator.CreateInstance(myType);

        foreach (System.Data.DataColumn item in row.Table.Columns)
        {
            myType.GetProperty(item.ColumnName).SetValue(myObject, row[item.ColumnName], null);
        }

        return myObject;
    }
}

Pokrótce opisując powyższy kod, poczynając od konstruktora, posiada on elementy, które są stałe – nie wymagają każdorazowej definicji. Zawiera jednostkę przechowującą tworzone elementy kodu, namespace z zaimportowaną przestrzenią System oraz kompilator CSharp z odpowiednimi parametrami.
Sama metoda zawiera definicję dynamicznie tworzonej klasy, którą dodajemy do ówcześnie stworzonej w konstruktorze przestrzeni. Następnie w pętli tworzone są pola prywatne i odwołujące się do nich właściwości. Później całość jest kompilowana i jeśli zawiera błędy, konsola wypisze je na ekran. W następnej kolejności tworzony jest typ z wyników kompilacji oraz obiekt tegoż typu. Obiektowi zostają nadane wartości oraz finalnie zostaje on zwrócony.
Nadmienię, iż stworzona przestrzeń nazw posiada wszystkie klasy ponumerowane w kolejności wywoływania metody.

Na sam koniec mój przykład wykorzystania:

DataTable dt = DataBaseConnector.GetExampleData();
var obj = dt.Rows[0].ToObjectWithProperties();
foreach (var item in obj.GetType().GetProperties())
{
    Console.WriteLine("{0}: {1}", item.Name, item.GetValue(obj, null));
}
Console.WriteLine();
Console.WriteLine("Imię: {FirstName}, Nazwisko: {LastName}".FormatWith(obj));

Wszelakie detale odnośnie zagadnienia znaleźć można na wspomnianej wcześniej stronie MSDN, przestrzeni System.CodeDom oraz Microsoft.CSharp.

Promuj

4 thoughts on “Dynamiczne tworzenie właściwości (properties)

    • Ad 1. Mechanizm tworzenia jest dowolny – możemy próbować parsować np na DateTime i jeśli test przejdzie stworzyć właściwość typu DataTime. Mój przypadek nie wymaga właściwości innego typu.
      Ad 2. Tym sposobem definiujesz klasę tak jakbyś to robił w kodzie z palca, a co za tym idzie również musisz zdefiniować blok TryCachFinally.

  1. Hym…
    Wygląda mi to na przekombinowane rozwiązanie. Fakt używałem CSharpCodeProvider, ale takie było wymaganie do aplikacji (użytkownik miał możliwość pisania własnych klas z edytora dostępnego w aplikacji).
    Skoro to jest aplikacja bazodanowa to takie dynamiczne tworzenie typów nie zabije wydajności?

Skomentuj Łukasz K. Anuluj pisanie odpowiedzi

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *