Típus létrehozása futásidőben – RuntimeTypeFactory

“Azok a programozók, akik a régen “szabványos”, úgynevezett procedurális programozáson nevelkedtek egy évtizede megtanulták az objektumorientált programozást. Mert meg kellett.” – graffity 2004-ből.

Aztán, amikor a Java 1.1 beköszöntött és hozta a java.lang.reflect névteret, akkor néztünk nagyokat, hogy mi értelme van egy osztályt felderíteni futásidőben? Pláne, amikor programozás közben ott vannak a property-k és metódusok. Aztán csak-csak hasznát vettük, de a címben említett megoldásra még rémálmunkban sem gondoltunk volna. De a Symbol LAB összefogott és megoldotta ezt is…

1. Keretrendszer

Adott a keretrendszerünk, amiben sok helyen használjuk a reflection-t. Például listáink szűrőablakai nem léteznek önállóan, hanem szűrőosztályokat definiálunk (közös ősből interfésszel). Az osztályok feltérképezésével pedig létre tudjuk hozni a szűrőablakokat. Ha találunk egy property-t, aminek DateInterval a típusa, akkor kikerül két dátumválasztó, amelyek a property-be írnak, onnan olvasnak (DataBinding). A property-k attributumokkal rendelkeznek, amik magyar nevet adnak a mezőknek.
Szép, kidolgozott technológia, meg is számolom… (2 perc eltelik itt) … a Symbol Ügyvitelben jelenleg 103 szűrőobjektum van.

2. Probléma

A feni technológiát kényelmes használni, nem is akarunk letérni az útról, de egyedi fejlesztéseink (SyX) esetén külső fájlból jönnek az osztályok, amelyek nem az 1-es pontban említett ősosztályból származnak. Mi tévők legyünk? Az ügyfél várja a megoldást, ki kell valamit találni…

3. (Egyelőre még nem elégséges) Megoldás

Fejlesztési vezetőnk pénteken már említett valamit, de csak hétfőn állt elő az ötlettel. Ha fel lehet térképezni egy osztályt, akkor miért ne CSINÁLNÁNK egyet, csak úgy futásidőben. Még jó, hogy van Google, mert volt honnan információt meríteni, de hideg zuhanyként ért minket a találatok listája.
Létre lehet hozni típust futásidőben, de a következő lépéseket kell végiggondolni:

  1. Minden property-nek van egy lokális változója
  2. Kell, hogy legyen egy GET és egy SET metódus, amely a property értékét olvassa és írja a lokális változóba/változóból.
  3. A GET és SET metódusok törzse, lényegi része nem C#-ban írandó, hanem a köztes MSIL/IL nyelven, ami a .NET-ben megvalósított Assembly nyelv, azaz szinte gépi kód.

Itt egy kis szomorúság jött, mert ilyet nem oktatnak az egyetemen, nem hétköznapi a felhasználási módja és nincs róla még “MSIL 21 nap alatt” könyv sem.
Viszont a fejlesztési vezetőnk ellentmondást nem tűrve a megoldás útjára lépett és saját maga valósította meg. Ilyenkor kis csendet szokott kérni, de ez most 4 órán át tartott. Megszületett egy már működő megoldás. De ez még nem volt elég jó a felhasználásra.

4. Jó megoldás

További 2 órába telt, hogy megoldjuk a problmát, ugyanis az újonnan létrehozott osztálynak van egy őse, amelynek van egy két paraméteres konstruktora. MSIL nyelven kellett megoldani a base konstruktor hívását és a paraméterek átadását.

5. Összefoglalás

Keretrendszerünk már ezt is tudja, általános osztályt csináltunk belőle, amelyet alant közzéteszünk. Hogy izgalmasabb legyen, egy hibát rejtettünk el benne a 32-33. sor környékén. Aki a hibát kijavítja, +1 pontot kap önéletrajza leadásakor a HR osztálytól. 🙂

using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection.Emit;
using System.Reflection;
namespace SymbolTech.BaseProject.FrameWork.Common
{
    public class RuntimeTypeFactory
    {
        private TypeBuilder tb;
        public RuntimeTypeFactory(string assemblyname, string typename, Type baseclass)
        {
            AssemblyName assname = new AssemblyName(assemblyname);
            AssemblyBuilder assbuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assname, AssemblyBuilderAccess.RunAndSave);
            ModuleBuilder mb = assbuilder.DefineDynamicModule(assname.Name, assname.Name + ".dll");
            tb = mb.DefineType(typename, TypeAttributes.Public, baseclass);
            //Override base constructors
            if (baseclass != null)
                foreach (ConstructorInfo ci in baseclass.GetConstructors())
                {
                    List<Type> types = new List<Type>();
                    foreach (ParameterInfo pari in ci.GetParameters())
                        types.Add(pari.ParameterType);
                    ConstructorBuilder ctor = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, types.ToArray());
                    ILGenerator ctorIL = ctor.GetILGenerator();
                    for (byte i = 0; i < types.Count; i++)
                        ctorIL.Emit(OpCodes.Ldarg_0, i);
                    ctorIL.Emit(OpCodes.Call, ci);
                    ctorIL.Emit(OpCodes.Ret);
                }
        }
        public static CustomAttributeBuilder CreateCustomAttributeItem(Type attributetype, Type[] constructorparametertypes, object[] constructorparameters)
        {
            return new CustomAttributeBuilder(attributetype.GetConstructor(constructorparametertypes), constructorparameters);
        }
        public void AddProperty(string name, Type type)
        {
            AddProperty(name, type, null);
        }
        public void AddProperty(string name, Type type, params CustomAttributeBuilder[] customattributes)
        {
            if (String.IsNullOrEmpty(name) || type == null)
                return;
            PropertyBuilder newprop = tb.DefineProperty(name, System.Reflection.PropertyAttributes.HasDefault, type, null);
            if (customattributes != null)
                foreach (CustomAttributeBuilder cab in customattributes)
                    if (cab != null)
                        newprop.SetCustomAttribute(cab);
            FieldBuilder newfield = tb.DefineField(name.ToLower(), type, FieldAttributes.Private);
            MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
            MethodBuilder methodget = tb.DefineMethod(String.Format("get_{0}", name), getSetAttr, type, Type.EmptyTypes);
            ILGenerator methodgetil = methodget.GetILGenerator();
            methodgetil.Emit(OpCodes.Ldarg_0);
            methodgetil.Emit(OpCodes.Ldfld, newfield);
            methodgetil.Emit(OpCodes.Ret);
            MethodBuilder methodset = tb.DefineMethod(String.Format("set_{0}", name), getSetAttr, null, new Type[] { type });
            ILGenerator methodsetil = methodset.GetILGenerator();
            methodsetil.Emit(OpCodes.Ldarg_0);
            methodsetil.Emit(OpCodes.Ldarg_1);
            methodsetil.Emit(OpCodes.Stfld, newfield);
            methodsetil.Emit(OpCodes.Ret);
            newprop.SetGetMethod(methodget);
            newprop.SetSetMethod(methodset);
        }
        public Type CreateType()
        {
            return tb.CreateType();
        }
    }
}