Monday, August 6, 2007

Spring.NET - Introspection vs. Reflection. Use of Mono.Cecil library.

Thanks to Bruno Baia and Mark Pollack for pointing me to the Mono.Cecil introspection library. The focus of the previous post on this subject were limitations of System.Reflection library in relation to copying custom attributes from the original type to the target type.

High level inspection of Mono.Cecil showed that library looks very promising for resolving the custom attributes issue and far beyond this.

I only targeted some proof of concept for this time, and code to follow can be pretty naive at some places and only counts now with copying of type attributes from one type to another (not including methods, properties, parameters' attributes). Though it should be pretty straight forward to evolve to the fully fledged solution.

Proof of concept code:

Helper method signature is

void CopyAttributes(Type source, TypeBuilder target)

First, I tried to follow the current ReflectionUtils signature for this which would look like:

public static CustomAttributeBuilder CreateCustomAttribute(
            Type type, object[] ctorArgs, Attribute sourceAttribute)

Though I ran quite soon into the necessity to find the attribute passed in this method call via Cecil (which leads to the question of attributes equality). Realizing that that would take quite an effort to resolve, may still be error prone and there is no any real value in being this granular, I decided to go with the high-level signature presented above.  I can imagine some interceptor to be passed in as a parameter to influence the attributes copy process.

Find the TypeDefinition of the source type from which we want to copy attributes.

Mono.Cecil.TypeDefinition matchedType = GetTypeMatchFromAssembly(source, Mono.Cecil.AssemblyFactory.GetAssembly(source.Assembly.Location));
GetTypeMatchFromAssembly method simply iterates through the assembly and finds a type based onto the FullName match which for the given assembly should be unique.

Iterate through the collection of the type's custom attributes.

foreach (Mono.Cecil.CustomAttribute attribute in matchedType.CustomAttributes)

Create constructor parameter types and values arrays.

    object[] parameterValues = new object[attribute.Constructor.Parameters.Count];
    Type[] parameterTypes = new Type[attribute.Constructor.Parameters.Count];

    for (int i = 0; i < attribute.Constructor.Parameters.Count; i++)
    {
        parameterTypes[i] =
            Type.GetType(attribute.Constructor.Parameters[i].ParameterType.FullName);
        parameterValues[i] = attribute.ConstructorParameters[i];
    }

The abilities given by Cecil in this area are of great use for our task. If we compare with current workarounds present in Spring:

public static Type[] GetTypes(object[] args)
{
   ....
   paramsType[i] = (arg != null) ? args[i].GetType() : typeof(object);
  

This above line of code from ReflectionUtils is definitely a big workaround which may pick the wrong ctor actually. Though, Spring.NET is not utilizing this code path I believe, as array of objects would be always empty, due to other workarounds.

Create ConstructorInfo and set the custom attribute on a typeBuilder.

ConstructorInfo constructor = Type.GetType(
    attribute.Constructor.DeclaringType.FullName + "," +
    attribute.Constructor.DeclaringType.Scope, true).GetConstructor(
    parameterTypes);

target.SetCustomAttribute(new CustomAttributeBuilder(
    constructor, parameterValues));

The above code is absolutely precise about the constructor it is creating based onto parameter types and values. Which again is not achievable by means of pure Reflection in the current Spring.NET implementation.

Unit test output

In the unit test a type of TargetType is created, its typeBuilder is passed to the proof of concept method along with a source type.

TypeBuilder typeBuilder = CreateTestTargetTypeBuilder();

Type sourceType = typeof(TypeWithTestAttribute);
Type targetType = typeBuilder.CreateType();

Console.WriteLine(DumpMemberAttributes(sourceType));
Console.WriteLine(DumpMemberAttributes(targetType));

ReflectionUtils2.CopyAttributes(sourceType, typeBuilder);

targetType = typeBuilder.CreateType();

Console.WriteLine("*Target Type after copying attributes:");
Console.WriteLine(DumpMemberAttributes(targetType));

Which produces the following output:

*Dump of the custom attributes for member: Dsi.Mono.Cecil.TestTypes.B.TypeWithTestAttribute(TypeInfo)
**Attribute of type Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute

Properties:
    FirstGetterOnlyPropertyValue:99
    SecondGetterOnlyPropertyValue:-99
    ReadWriteProperty:ReadWriteProperty 2 set via class attribute ctor
    TypeId:Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Fields:
**Attribute of type System.Runtime.InteropServices.GuidAttribute
Properties:
    Value:263E3E90-9D06-4660-AD67-28D1BA5E0107
    TypeId:System.Runtime.InteropServices.GuidAttribute
Fields:
**Attribute of type Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Properties:
    ReadWriteProperty:ReadWriteProperty 2 set via class attribute ctor
    FirstGetterOnlyPropertyValue:99
    SecondGetterOnlyPropertyValue:-99
    TypeId:Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Fields:
**Attribute of type Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Properties:
    ReadWriteProperty:ReadWriteProperty 1 set via class attribute ctor
    FirstGetterOnlyPropertyValue:100
    SecondGetterOnlyPropertyValue:-100
    TypeId:Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Fields:
**Attribute of type System.SerializableAttribute
Properties:
    TypeId:System.SerializableAttribute
Fields:
*Dump of the custom attributes for member: TargetType(TypeInfo)
No Attributes present!

*Target Type after copying attributes:
*Dump of the custom attributes for member: TargetType(TypeInfo)

**Attribute of type Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Properties:
    ReadWriteProperty:null
    FirstGetterOnlyPropertyValue:99
    SecondGetterOnlyPropertyValue:-99
    TypeId:Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Fields:
**Attribute of type System.Runtime.InteropServices.GuidAttribute
Properties:
    Value:263E3E90-9D06-4660-AD67-28D1BA5E0107
    TypeId:System.Runtime.InteropServices.GuidAttribute
Fields:
**Attribute of type Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Properties:
    ReadWriteProperty:null
    FirstGetterOnlyPropertyValue:99
    SecondGetterOnlyPropertyValue:-99
    TypeId:Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Fields:
**Attribute of type Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Properties:
    ReadWriteProperty:null
    FirstGetterOnlyPropertyValue:100
    SecondGetterOnlyPropertyValue:-100
    TypeId:Dsi.Mono.Cecil.TestTypes.A.CustomTestAttribute
Fields:

Conclusion.

It seems that introspection and Mono.Cecil library would provide quite a benefit to Spring.NET if used. Areas of use exceed broadly the issue resolved by this proof of concept. As well as I believe one can expect much better performance in many areas from applying introspection as opposed to a Reflection.

For completeness, providing the test types used.

 

Test classes - custom attribute and class it is applied to.

using System;
using System.Collections.Generic;
using System.Text;

namespace Dsi.Mono.Cecil.TestTypes.A
{
    [AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
    public class CustomTestAttribute : Attribute
    {
        #region Fields
        private int firstGetterOnlyPropertyValue;
        private int secondGetterOnlyPropertyValue;
        private string readWriteProperty;
        #endregion

        #region Properties
        public int FirstGetterOnlyPropertyValue
        {
            get { return firstGetterOnlyPropertyValue; }
        }
        public int SecondGetterOnlyPropertyValue
        {
            get { return secondGetterOnlyPropertyValue; }
        }
        public string ReadWriteProperty
        {
            get { return readWriteProperty; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("ReadWriteProperty",
                        "Value can not be null!");
                }
                readWriteProperty = value;
            }
        }
        #endregion

        #region Constructors
        public CustomTestAttribute(int firstGetterOnlyProperty) : base()
        {
            this.firstGetterOnlyPropertyValue = firstGetterOnlyProperty;
        }
        public CustomTestAttribute(int firstGetterOnlyProperty, int secondGetterOnlyPropertyValue)
            : this(firstGetterOnlyProperty)
        {
            this.secondGetterOnlyPropertyValue = secondGetterOnlyPropertyValue;
        }
        public CustomTestAttribute(int firstGetterOnlyProperty, int secondGetterOnlyPropertyValue,
            string readWriteProperty)
            : this(firstGetterOnlyProperty, secondGetterOnlyPropertyValue)
        {
            this.readWriteProperty = readWriteProperty;

        }
        #endregion
    }
}
namespace Dsi.Mono.Cecil.TestTypes.B { [CustomTestAttribute(100, -100, ReadWriteProperty = "ReadWriteProperty 1 set via class attribute ctor")] [CustomTestAttribute(99, -99, ReadWriteProperty = "ReadWriteProperty 2 set via class attribute ctor")] [CustomTestAttribute(99, -99, ReadWriteProperty = "ReadWriteProperty 2 set via class attribute ctor")] [SerializableAttribute()] [GuidAttribute("263E3E90-9D06-4660-AD67-28D1BA5E0107")] public class TypeWithTestAttribute {} }

No comments: