How to extend class with an extra property

13,573

Solution 1

Your problem can relatively easily be solved by using Reflection.Emit and run-time code generation.

Suppose now you have the following class that you would like to extend.

public class Person
{
    public int Age { get; set; }
}

This class represents a person, and contains a property named Age to represent the person's age.

In your case, you would also like to add a Name property of type string to represent the person's name.

The simplest and most streamlined solution would then be to define the following interface.

public interface IPerson
{   
    string Name { get; set; }
    int Age { get; set; }
}

This interface, which will be used to extend your class, should contain all the old properties your current class contains, and the new ones you would like to add. The reason for this will become clear in a moment.

You can now use the following class definition to actually extend your class by creating a new type at runtime which will also make it derive from the above mentioned interface.

class DynamicExtension<T>
{
    public K ExtendWith<K>()
    { 
        var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Assembly"), AssemblyBuilderAccess.Run);
        var module = assembly.DefineDynamicModule("Module");
        var type = module.DefineType("Class", TypeAttributes.Public, typeof(T));

        type.AddInterfaceImplementation(typeof(K));

        foreach (var v in typeof(K).GetProperties())
        {
            var field = type.DefineField("_" + v.Name.ToLower(), v.PropertyType, FieldAttributes.Private);
            var property = type.DefineProperty(v.Name, PropertyAttributes.None, v.PropertyType, new Type[0]);
            var getter = type.DefineMethod("get_" + v.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual, v.PropertyType, new Type[0]);
            var setter = type.DefineMethod("set_" + v.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual, null, new Type[] { v.PropertyType });

            var getGenerator = getter.GetILGenerator();
            var setGenerator = setter.GetILGenerator();

            getGenerator.Emit(OpCodes.Ldarg_0);
            getGenerator.Emit(OpCodes.Ldfld, field);
            getGenerator.Emit(OpCodes.Ret);

            setGenerator.Emit(OpCodes.Ldarg_0);
            setGenerator.Emit(OpCodes.Ldarg_1);
            setGenerator.Emit(OpCodes.Stfld, field);
            setGenerator.Emit(OpCodes.Ret);

            property.SetGetMethod(getter);
            property.SetSetMethod(setter);

            type.DefineMethodOverride(getter, v.GetGetMethod());
            type.DefineMethodOverride(setter, v.GetSetMethod());
        }

        return (K)Activator.CreateInstance(type.CreateType());
    }
}

To actually use this class, simply execute the following lines of code.

class Program
{
    static void Main(string[] args)
    {
        var extended = new DynamicExtension<Person>().ExtendWith<IPerson>();

        extended.Age = 25;
        extended.Name = "Billy";

        Console.WriteLine(extended.Name + " is " + extended.Age);

        Console.Read();
    }
}

You can now see that the reason we used an interface to extend our newly created class is so that we can have a type-safe way of accessing its properties. If we simply returned an object type, we would be forced to access its properties by Reflection.

EDIT

The following modified version is now able to instantiate complex types located inside the interface, and implement the other simple ones.

The definition of the Person class stays the same, while the IPerson interface now becomes the following.

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

    Person Person { get; set; }
}

The DynamicExtension class definition now changes to the following.

class DynamicExtension<T>
{
    public T Extend()
    {
        var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Assembly"), AssemblyBuilderAccess.Run);
        var module = assembly.DefineDynamicModule("Module");
        var type = module.DefineType("Class", TypeAttributes.Public);

        type.AddInterfaceImplementation(typeof(T));

        foreach (var v in typeof(T).GetProperties())
        {
            var field = type.DefineField("_" + v.Name.ToLower(), v.PropertyType, FieldAttributes.Private);
            var property = type.DefineProperty(v.Name, PropertyAttributes.None, v.PropertyType, new Type[0]);
            var getter = type.DefineMethod("get_" + v.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual, v.PropertyType, new Type[0]);
            var setter = type.DefineMethod("set_" + v.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual, null, new Type[] { v.PropertyType });

            var getGenerator = getter.GetILGenerator();
            var setGenerator = setter.GetILGenerator();

            getGenerator.Emit(OpCodes.Ldarg_0);
            getGenerator.Emit(OpCodes.Ldfld, field);
            getGenerator.Emit(OpCodes.Ret);

            setGenerator.Emit(OpCodes.Ldarg_0);
            setGenerator.Emit(OpCodes.Ldarg_1);
            setGenerator.Emit(OpCodes.Stfld, field);
            setGenerator.Emit(OpCodes.Ret);

            property.SetGetMethod(getter);
            property.SetSetMethod(setter);

            type.DefineMethodOverride(getter, v.GetGetMethod());
            type.DefineMethodOverride(setter, v.GetSetMethod());
        }

        var instance = (T)Activator.CreateInstance(type.CreateType());

        foreach (var v in typeof(T).GetProperties().Where(x => x.PropertyType.GetConstructor(new Type[0]) != null))
        {
            instance.GetType()
                    .GetProperty(v.Name)
                    .SetValue(instance, Activator.CreateInstance(v.PropertyType), null);
        }

        return instance;
    }
}

We can now simply execute the following lines of code to get all the appropriate values.

class Program
{
    static void Main(string[] args)
    {
        var extended = new DynamicExtension<IPerson>().Extend();

        extended.Person.Age = 25;
        extended.Name = "Billy";

        Console.WriteLine(extended.Name + " is " + extended.Person.Age);

        Console.Read();
    }
}

Solution 2

as my comments were getting very verbose, I thought I'd add a new answer. this answer is completely Mario's work and thinking, only has my minor addition to exemplify what I'm trying to put across.

There are a few minor changes to mario's example that would make this work very well, namely, just changing the fact that the existing properties are added as the class object, rather than duplicating the entire class. Anyway, here's how this looks (only amended sections added, all else remains as per mario's answer):

public class Person
{
    public int Age { get; set; }
    public string FaveQuotation { get; set; }
}

for the IPerson interface, we add the actual Person class, rather than copying the properties:

public interface IPerson
{
    // extended property(s)
    string Name { get; set; }
    // base class to extend - tho we should try to overcome using this
    Person Person { get; set; }
}

This translates to an updated usage of:

static void Main(string[] args)
{
    var extended = new DynamicExtension<Person>().ExtendWith<IPerson>();

    var pocoPerson = new Person
    {
        Age = 25,
        FaveQuotation = "2B or not 2B, that is the pencil"
    };

    // the end game would be to be able to say: 
    // extended.Age = 25; extended.FaveQuotation = "etc";
    // rather than using the Person object along the lines below
    extended.Person = pocoPerson;
    extended.Name = "Billy";

    Console.WriteLine(extended.Name + " is " + extended.Person.Age 
        + " loves to say: '" + extended.Person.FaveQuotation + "'");

    Console.ReadKey();
}

Hope this helps the original OP, I know it made me think, tho the jury is still out as to whether the Person class should be flattened to the same level in the method as the new properties!! So in effect, using the line new DynamicExtension<Person>().ExtendWith<IPerson>(); SHOULD return a fully extended new object -intellisence included. Tough call - hmmm...

Solution 3

Without having access to the class definition, the best you could do is create a class which is derived from the target class. Unless the original is Sealed.

Share:
13,573

Related videos on Youtube

Ralf de Kleine
Author by

Ralf de Kleine

Sup?

Updated on June 04, 2022

Comments

  • Ralf de Kleine
    Ralf de Kleine about 2 years

    Suppose I've got a class named Foo.

    I cannot change the Foo class but I wan't to extend it with a property named Bar of type string.

    Also I've got a lot more classes like Foo so I'm interested in a 'generic' solution.

    I'm looking into ExpandoObject, dynamic and it gives me the result I'm asking for but I was wondering it it could be done without using dynamic...

    static void Main(string[] args)
    {
        var foo = new Foo() { Thing = "this" };
        var fooplus = Merge(foo, new { Bar = " and that" });
        Console.Write(string.Concat(fooplus.Thing, fooplus.Bar));
        Console.ReadKey();
    }
    
    public class Foo
    {
        public string Thing { get; set; }
    }
    
    public static dynamic Merge(object item1, object item2)
    {
        if (item1 == null || item2 == null)
        return item1 ?? item2 ?? new ExpandoObject();
    
        dynamic expando = new ExpandoObject();
        var result = expando as IDictionary<string, object>;
        foreach (System.Reflection.PropertyInfo fi in item1.GetType().GetProperties())
        {
            result[fi.Name] = fi.GetValue(item1, null);
        }
        foreach (System.Reflection.PropertyInfo fi in item2.GetType().GetProperties())
        {
            result[fi.Name] = fi.GetValue(item2, null);
        }
        return result;
    }
    
    • John Alexiou
      John Alexiou over 10 years
      Why not use an Extension method?
    • dcastro
      dcastro over 10 years
      @Liam, I assume that "I cannot change the Foo class" also means he can't mark it as partial.
    • Tim S.
      Tim S. over 10 years
      If Foo isn't sealed, you could extend it, e.g. public class FooBar : Foo { public string Bar { get; set; } }
    • dcastro
      dcastro over 10 years
      Well, I suppose you'll have to use your current approach then. Beware of the performance implications though.
  • Ralf de Kleine
    Ralf de Kleine over 10 years
    I'm quite excited by this code, only downside is that I need a interface which matches Person and the extra property I need. But I might get it to work. Interesting.
  • jim tollan
    jim tollan over 10 years
    +1 - whao- this looks very interesting and hints at some nice alternative uses. i like it very much. if you were able to refactor that to work in a similar way to the OP's merge (i.e. without the need to duplicate the base properties in the interface), that would be soo soo useful in lots of applications. might even have a bash at that refactor myself...
  • jim tollan
    jim tollan over 10 years
    ahh - inote that the final result is iperson extended!! hmmm
  • Mario Stopfer
    Mario Stopfer over 10 years
    @RalfdeKleine I can edit my answer to better fit your needs. What exactly do you want me to change? :D
  • Mario Stopfer
    Mario Stopfer over 10 years
    @jimtollan Exactly right. That is why I used the interface in the first place. So that we have a strongly typed result in the end, whose properties can be accessed in a type safe manner, and would be visible using intellisense.
  • jim tollan
    jim tollan over 10 years
    Mario - this keeps running thro my mind and something occurred to me. would it be possible to define the following: public class Person { public int Age { get; set; } } public interface IPerson { string Name { get; set; } Person Person { get; set; } } i.e. refactor your method to accept a full blown class object as a property, rather than copying the properties. then, you could refer to it in code as: extended.Person.Age = 25; . obviously, at present this doesn't work -but i'm sure it's tweakable to allow this.. go for it :)
  • jim tollan
    jim tollan over 10 years
    ran out of space. obviously, the remaining issue to tackle in your DynamicExtension<T> is the creation of a new class of type 'T', to cater for the class being passed at type level, rather than individual property level. this could of course be created when setting the properties -i.e. extended.Person = new Person(); but that's a bit lame and not particularly logical. throwing this challenge out to anyone that reads this!
  • Mario Stopfer
    Mario Stopfer over 10 years
    Jim, I have updated my answer to match what you had in mind. Could you just elaborate a bit more on how you want me to handle the Extend method, and how you think it should accept objects instead of types?
  • jim tollan
    jim tollan over 10 years
    hey mario -good job. ok, in the perfect world, i'd love to see your code SOMEHOW (don't ask me how), take the Person type that is part of IPerson and then when it is extended, return a brand new mixed type where the Person properties were flattened down onto the same level as the new IPerson properties. Basically, you'd now have a single new object with merged properties. I do btw understand the complxities of trying to get this to happen in design time with intellisense! but this would be a wonderful piece of engineering
  • jim tollan
    jim tollan over 10 years
    but just to add, this is an excellent advance on your initial answer, it wraps everything up really nicely
  • jim tollan
    jim tollan over 10 years
    in effect, the most PERFECT final version would be instantiated as such var extended = new DynamicExtension<Person>().ExtendWith<IPerson>();. In our IPerson interface, we'd ONLY have the new properties (no mention of Person). This call would then merge the properties found in Person and IPerson as a single type. This would mean that you could apply IPerson to a variety of other types i.e. var extended = new DynamicExtension<Monkey>().ExtendWith<IPerson>(); etc... as i said, i understand how almost impossible this is!! :-)
  • Mario Stopfer
    Mario Stopfer over 10 years
    Jim, it would certainly be possible for me to take both types and merge them into a single new one. But the resulting type that would be returned would be of type Object, therefore no intellisense. In order for us to use intellisense, we simply need to have the type, or its interface upfront.
  • jim tollan
    jim tollan over 10 years
    mario - yeah, this is too true. i think your update implementation is the closest to nailing the OP's requirement. I for one don't have a usecase for this yet, but it certainly caught my imagination. thanks for all the input -wish i could vote more than once!! :-) all the best for now..