4

what I am having difficulties understanding is : what should I do in the build method?

It is our intent to implement a framework that has a method that generates a new instance of a class of domain in which their properties and methods have added functionalities. The Following example presents a case of the creation of a object of domain Stock where the parameters passed to build will be pass as arguments to the constructor of stock in it's instantiation.


Enhancer.Build<Stock>("Apple", "Dow Jones");


The added functionalities are specified by custom attributes that mark the virtual methods and properties, as the following example shows. In the comments it is said what we intend the marked methods and properties to start verifying in the returned object from the function Build of Enhancer. Every custom attribute in the example should have a common base type - EnhancedAtributte - with an abstract method Check(object[] args) that receives the arguments from the marked method.

class Stock
 {
 public Stock(string name, string index) { ... }
 [NonNull]
 public virtual string Market { get; set; } // set will launch an exception if values are null
 [Min(73)]
 public virtual long Quote { get; set; } // set will launch an exception if values are < 73
 [Min(0.325)]
 public virtual double Rate { get; set; } // set will launch an exception if values are < 0.325
 [Accept("Jenny", "Lily", "Valery")]
 public virtual string Trader{get; set; } // set will launch an exception if values are different than Jenny, Lily or Valery 
 [Max(58)]
 public virtual int Price { get; set; } // set will launch an exception if values are > 58
 // it will launch exception if the state of this or any of the parameters has been altered by the execution of a marked method
 // -- BuildInterest
 [NoEffects]
 public double BuildInterest(Portfolio port, Store st) { ... }
 }
  • Implementation strategies:
    • The project Enhancer must use the System.Reflection.Emit API.
      • The solution must not be compromised with the given examples.
      • A function Build(params object[] args) should return a new instance of a new class derived from T, let's call it T’, that redefines the virtual methods of T.
      • The new class T’ is created dynamically with the API resource System.Reflection.Emit.
      • For each M method in T that is marked ( Custom Attribute), it should be created a new definition of the method M’ in class T’. The method M’ should call the base method M and should do the specified by the custom attribute.
MigthyMoron
  • 119
  • 9
  • 1
    Can you link to where you read this, and update the question to be more clear? I assume it's just that `T'` extends `T`. – zzzzBov Feb 11 '18 at 16:02
  • I think you can make dynamic type with a base class using `System.Reflection.Emit.TypeBuilder` – derloopkat Feb 11 '18 at 16:12
  • First off, strong typisation is your friend. Do not fight it, embrace it. Without it, we had the same issues as PHP and Javascript: http://www.sandraandwoo.com/2015/12/24/0747-melodys-guide-to-programming-languages/ You want the compiler to check what you write. Dynamic and object are at best "nessesary evils". Things we use when we interact with some weakly typed code or WebAPI. – Christopher Feb 11 '18 at 16:16
  • Does the `Check` method do all the validation work, e.g., does it throw an exception on an invalid value? – Mike Strobel Feb 11 '18 at 16:46
  • @MikeStrobel to my understanding what the `Check` methods does is verify if the condition of the attribute is valid. In other words, if you try to set the property `price` higher than 58 it launches a custom exception, otherwise it will do nothing and let you set the value as you wish. So it does do all the validation work – MigthyMoron Feb 11 '18 at 17:09

1 Answers1

1

I'd start by verifying that T is neither sealed nor abstract. That should be sufficient to make sure it's (1) a class; and (2) capable of being extended.

Next, iterate over typeof(T).GetProperties() to find any 'enhanced' properties where CanWrite is true and property.GetCustomAttributes<EnhancedAtributte>().Any() reports true. If there aren't any matching properties, you can just instantiate T directly.

Since the property values are validated by the attribute instances themselves, you'll want to cache the attributes somewhere, lest you incur an expensive lookup on each property change. You should aim to generate a class that looks something like this:

public class __Enhanced__Stock__ {
    private static readonly EnhancedAttribute[] __Price__Attributes__;

    static __Enhanced__Stock__() {
        __Price__Attributes__ = typeof(Stock).GetProperty("Price")
                                            .GetCustomAttributes<EnhancedAtributte>()
                                            .ToArray();
    }

    public override int Price {
        get => base.Price;
        set {
            for (int i = 0, n = __Price__Attributes__.Length; i < n; i++) 
                __Price__Attributes__[i].Check(new Object[] { (Object)value });
            }
            base.Price = value;
        }
    }
}

A TypeBuilder can be created from a ModuleBuilder, which is created from an AssemblyBuilder. For the latter two, you can just keep a static instance around.

You'll need to use TypeBuilder.DefineField to define a static field to use as an attribute cache for each property (or use a single EnhancedAttribute[][] indexed by property). In either case, you'll have to define a class constructor (see TypeBuilder.DefineTypeInitializer) to initialize the cache. You'll have to write the IL yourself using MethodBuilder.GetILGenerator().

For each enhanced property you found, you'll need to define a new property with the same name (see TypeBuilder.DefineProperty), and emit a separate get and set accessor for each (see TypeBuilder.DefineMethod). Each accessor will need to be bound to the property, which can be accomplished via PropertyBuilder.SetGetMethod and SetSetMethod. You'll also have to make the accessors override the accessors in T, which you can do via TypeBuilder.DefineMethodOverride. You can see some hints on overriding properties in this question.

The code for the get accessor override will be simple: you need only delegate to the base property. The set accessor is more complicated, because you'll need to loop over the attribute cache for the property and call each attribute's Check method. Again, you'll need to emit the IL manually, which includes figuring out how to emit a simple for loop. Alternatively, since you already know the number of attributes for each property, you could just write a manually-unrolled loop. Regardless, for each call to Check, remember that you'll need to initialize a new object[] containing into which you must copy your value parameter.

Once you've declared the attribute cache field(s), the type initializer, the properties, and their accessors, you're essentially finished. You can 'bake' the derived type by calling CreateType() on your TypeBuilder.


Partial Solution

I felt like writing some code, so here's a solution that should handle the attribute-based property validation. It's not entirely clear to me how the attributes on other methods should work, but this should provide a good starting point nonetheless.

public class Enhancer
{
    private static readonly ModuleBuilder ModuleBuilder;

    static Enhancer()
    {
        var b = AssemblyBuilder.DefineDynamicAssembly(
            new AssemblyName(Guid.NewGuid().ToString()),
            AssemblyBuilderAccess.Run);

        ModuleBuilder = b.DefineDynamicModule($"{b.GetName().Name}.Module");
    }

    private const BindingFlags InstanceFlags = BindingFlags.Instance |
                                               BindingFlags.Public |
                                               BindingFlags.NonPublic;

    private const FieldAttributes CacheFlags = FieldAttributes.Private |
                                               FieldAttributes.Static |
                                               FieldAttributes.InitOnly;

    private const TypeAttributes TypeFlags = TypeAttributes.Public |
                                             TypeAttributes.Sealed |
                                             TypeAttributes.BeforeFieldInit |
                                             TypeAttributes.AutoLayout |
                                             TypeAttributes.AnsiClass;

    private static IEnumerable<PropertyInfo> FindEnhancedProperties(Type t)
    {
        foreach (var p in t.GetProperties(InstanceFlags))
        {
            if (p.CanWrite &&
                p.GetSetMethod(true).IsVirtual &&
                p.IsDefined(typeof(EnhancedAttribute)))
            {
                yield return p;
            }
        }
    }

    public static EnhancedAttribute[] FindEnhancedAttributes(PropertyInfo p)
    {
        return p.GetCustomAttributes<EnhancedAttribute>().ToArray();
    }

    private static readonly MethodInfo CheckMethod =
        typeof(EnhancedAttribute).GetMethod(
            nameof(EnhancedAttribute.Check),
            new[] { typeof(object[]) });

    private static readonly MethodInfo GetTypeFromHandleMethod =
        typeof(Type).GetMethod(
            nameof(Type.GetTypeFromHandle),
            new[] { typeof(RuntimeTypeHandle) });

    private static readonly MethodInfo GetPropertyMethod =
        typeof(Type).GetMethod(
            nameof(Type.GetProperty),
            new[] { typeof(string), typeof(BindingFlags) });

    private static readonly MethodInfo FindEnhancedAttributesMethod =
        typeof(Enhancer).GetMethod(
            nameof(FindEnhancedAttributes),
            new[] { typeof(PropertyInfo) });

    private readonly Type _base;
    private readonly TypeBuilder _enhanced;
    private readonly PropertyInfo[] _properties;
    private readonly FieldBuilder[] _attributeCaches;
    private readonly MethodBuilder[] _propertySetters;

    private static readonly Dictionary<Type, Type> Cache = new Dictionary<Type, Type>();

    public static T Build<T>(params object[] args) where T : class
    {
        Type type;

        lock (Cache)
        {
            if (!Cache.TryGetValue(typeof(T), out type))
                Cache[typeof(T)] = type = new Enhancer(typeof(T)).Enhance();
        }

        return (T)Activator.CreateInstance(type, args);
    }

    private Enhancer(Type t)
    {
        if (t?.IsSealed != false || t.IsInterface)
        {
            throw new ArgumentException(
                "Type must be a non-sealed, non-abstract class type.");
        }

        _base = t;
        _enhanced = ModuleBuilder.DefineType($"<Enhanced>{t.FullName}", TypeFlags, t);
        _properties = FindEnhancedProperties(t).ToArray();

        _attributeCaches = _properties.Select(
            p => _enhanced.DefineField(
                $"__{p.Name}Attributes",
                typeof(EnhancedAttribute[]),
                CacheFlags)).ToArray();

        _propertySetters = new MethodBuilder[_properties.Length];
    }

    private Type Enhance()
    {
        GenerateTypeInitializer();

        for (int i = 0, n = _properties.Length; i < n; i++)
            EnhanceProperty(i);

        GenerateConstructors();

        return _enhanced.CreateType();
    }

    private void GenerateConstructors()
    {
        var baseCtors = _base.GetConstructors(InstanceFlags);

        foreach (var baseCtor in baseCtors)
        {
            if (baseCtor.IsPrivate)
                continue;

            var parameters = baseCtor.GetParameters();

            var ctor = _enhanced.DefineConstructor(
                baseCtor.Attributes,
                baseCtor.CallingConvention,
                parameters.Select(p => p.ParameterType).ToArray());

            var il = ctor.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);

            for (int i = 0; i < parameters.Length; i++)
                il.Emit(OpCodes.Ldarg, i + 1);

            il.Emit(OpCodes.Call, baseCtor);
            il.Emit(OpCodes.Ret);
        }
    }

    private void GenerateTypeInitializer()
    {
        var typeInit = _enhanced.DefineTypeInitializer();
        var il = typeInit.GetILGenerator();

        for (int i = 0, n = _properties.Length; i < n; i++)
        {
            var p = _properties[i];

            il.Emit(OpCodes.Ldtoken, _base);
            il.EmitCall(OpCodes.Call, GetTypeFromHandleMethod, null);
            il.Emit(OpCodes.Ldstr, p.Name);   
            il.Emit(OpCodes.Ldc_I4_S, (int)InstanceFlags);
            il.EmitCall(OpCodes.Call, GetPropertyMethod, null);
            il.EmitCall(OpCodes.Call, FindEnhancedAttributesMethod, null);
            il.Emit(OpCodes.Stsfld, _attributeCaches[i]);
        }

        il.Emit(OpCodes.Ret);
    }

    private void EnhanceProperty(int index)
    {
        var p = _properties[index];

        var property = _enhanced.DefineProperty(
            p.Name,
            p.Attributes,
            p.PropertyType,
            null);

        var baseSet = p.GetSetMethod(true);

        var set = _enhanced.DefineMethod(
            baseSet.Name,
            baseSet.Attributes & ~MethodAttributes.NewSlot | MethodAttributes.Final,
            baseSet.CallingConvention,
            baseSet.ReturnType,
            new[] { p.PropertyType });

        property.SetSetMethod(set);

        _enhanced.DefineMethodOverride(set, baseSet);

        var il = set.GetILGenerator();
        var attributeCount = p.GetCustomAttributes<EnhancedAttribute>().Count();

        for (int j = 0; j < attributeCount; j++)
        {
            il.Emit(OpCodes.Ldsfld, _attributeCaches[index]);
            il.Emit(OpCodes.Ldc_I4, j);
            il.Emit(OpCodes.Ldelem_Ref, j);
            il.Emit(OpCodes.Ldc_I4_1);
            il.Emit(OpCodes.Newarr, typeof(object));
            il.Emit(OpCodes.Dup);
            il.Emit(OpCodes.Ldc_I4_0);
            il.Emit(OpCodes.Ldarg_1);

            if (p.PropertyType.IsValueType)
                il.Emit(OpCodes.Box, p.PropertyType);

            il.Emit(OpCodes.Stelem_Ref);
            il.EmitCall(OpCodes.Callvirt, CheckMethod, null);
        }

        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.EmitCall(OpCodes.Call, baseSet, null);
        il.Emit(OpCodes.Ret);

        _propertySetters[index] = set;
    }
}
Mike Strobel
  • 25,075
  • 57
  • 69
  • I need to create a propertyBuilder per property? In my build class how should I return my type? I was doing something like this: return (T)Activator.CreateInstance(newType, arguments); But somehow I am having some expceptions. – MigthyMoron Feb 17 '18 at 12:28
  • Yes, one `PropertyBuilder` per property. If you do not declare a constructor yourself, you should be able to use `Activator.CreateInstance(yourType)` where `yourType = yourTypeBuilder.CreateType()`. It is not unusual to run into exceptions when emitting IL by hand—if you don't know IL very well, it can be difficult to get it right. I suggest using `AssemblyBuilder.Save()` to save your generated code to a `.dll` file, and then open it up in a good decompiler (e.g., Reflector or dotPeek) to see what the decompiled code looks like. That might give you some hints as to what went wrong. – Mike Strobel Feb 17 '18 at 19:52
  • 1
    Actually, better yet, write a class that mimics what the generated class should look like. Run _that_ through a decompiler, and see what the IL looks like. Then, essentially all you'll need to do is duplicate it. But make sure your example class has a few different property types—at least one value type and one reference type, as the IL will differ a bit when you call `Check` (value types will need to be boxed). – Mike Strobel Feb 17 '18 at 20:04
  • See my updated answer; I went ahead and provided a reference implementation for you. – Mike Strobel Feb 17 '18 at 22:11
  • thank you for the help, that actualy helped me a lot! – MigthyMoron Feb 20 '18 at 11:05