2

Suppose I have an abstract base class in which I want a CreateCopy method:

public abstract class BaseClass
{
    ///base stuff

    public BaseClass CreateCopy() //or public object, if necessary   
    {
        //?????
    }
}

Assume that all derived classes have a parameterless constructor and property fields (that can be) marked with some kind of attribute:

public class DerivedClass : BaseClass
{
    [CopiableProperty]
    public string Property1 {get; private set;}

    [CopiableProperty]
    public int Property2 {get; private set;}

    //no need to copy
    public int Property3 {get; private set;}

    //parameterless constructor
    public DerivedClass() { }
}

Is it possible with this structure to write the body of CreateCopy() in a way that I can create new instances of the derived objects with the correct CopiableProperty fields?


Naturally I could make a public abstract BaseClass CreateCopy() and force each derived class to care for its own copy, but due to the size and amount of the derived classes, this would bring too much extra effort.

Daniel Möller
  • 84,878
  • 18
  • 192
  • 214
  • have you thought about using an intermediate class between the base and the derived class? – Alan Deep Jul 18 '18 at 12:41
  • Yes it is possible, iterate through the properties, filter for your attribute and copy the values of the properties left. – thehennyy Jul 18 '18 at 12:45
  • 2
    Perhaps you could use a Json serializer. You can decorate the properties with JsonIgnore. Then your base CreateCopy could return a serialized payload of the derived class. You can then take that payload and de-serailize into a copy of the class? I think :) – Wheels73 Jul 18 '18 at 12:48
  • 1
    Have you considered using serialization? You can use attributes to control which properties should be serialized or not. See https://www.newtonsoft.com/json/help/html/SerializationAttributes.htm#JsonIgnoreAttribute and also https://stackoverflow.com/a/15788750/558486 – Rui Jarimba Jul 18 '18 at 12:48

3 Answers3

4

A fairly simple approach could be using generics and reflection:

public abstract class BaseClass
{
    // restrict to children of BaseClass
    public T CreateCopy<T>() where T: BaseClass, new()
    {
        var copy = new T();

        // get properties that you actually care about
        var properties = typeof(T).GetProperties()
            .Where(x => x.GetCustomAttribute<CopiablePropertyAttribute>() != null);

        foreach (var property in properties)
        {
            // set the value to the copy from the instance that called this method
            property.SetValue(copy, property.GetValue(this));
        }

        return copy;
    }
}

public class DerivedClass : BaseClass
{
    [CopiableProperty]
    public string Property1 { get; set; }

    [CopiableProperty]
    public int Property2 { get; set; }

    public int Property3 { get; set; }

    public override string ToString()
    {
        return $"{Property1} - {Property2} - {Property3}";
    }
}

static void Main(string[] args)
{
    var original = new DerivedClass
    {
        Property1 = "Hello",
        Property2 = 123,
        Property3 = 500
    };

    var copy = original.CreateCopy<DerivedClass>();

    Console.WriteLine(original);
    Console.WriteLine(copy);

    Console.ReadLine();
}

This would print:

Hello - 123 - 500
Hello - 123 - 0

Another approach would be to take advantage of a serialization library, if you don't mind the dependency:

public abstract class BaseClass
{
    public BaseClass CreateCopy()
    {
        string serialized = JsonConvert.SerializeObject(this);

        var actualType = GetType();

        return JsonConvert.DeserializeObject(serialized, actualType) as BaseClass;
    }
}

public class DerivedClass : BaseClass
{
    public string Property1 { get; set; }

    public int Property2 { get; set; }

    [JsonIgnore]
    public int Property3 { get; set; }

    //parameterless constructor
    public DerivedClass() { }

    public override string ToString()
    {
        return $"{Property1} - {Property2} - {Property3}";
    }
}
Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
  • Great thing with Generics. Thanks for the very complete answer. --- A "bug" that is easy worked around: if the target property belongs to the base class, it throws an exception when setting. (It would be complex if I had more than one level of inheritance, but here I just use the base class property without reflection). – Daniel Möller Jul 18 '18 at 13:37
  • @DanielMöller Huh, I knew I had to test this a little more. Glad you found it useful though! – Camilo Terevinto Jul 18 '18 at 13:39
  • Maybe using `BindingFlags.DeclaredOnly` should solve it, but then I'd have to go through each type in the hierarchy up to the base type. – Daniel Möller Jul 18 '18 at 13:47
2

My solution uses serialization/deserialization, using JSON.NET nuget package.

No need for a method in the base class, you can use an extension method instead (adapted from this answer):

using Newtonsoft.Json;

public static class ObjectExtensions
{
    public static T Clone<T>(this T source)
    {
        var serialized = JsonConvert.SerializeObject(source);
        var clone = JsonConvert.DeserializeObject<T>(serialized);

        return clone;
    }
}

And then use attributes to control which properties should be copied or not - example:

using Newtonsoft.Json;

public class DerivedClass : BaseClass
{
    public string Property1 { get; set; }

    public int Property2 { get; set; }

    [JsonIgnore]
    public int Property3 { get; set; }
}

Using the code:

var obj1 = new DerivedClass
{
    Property1 = "Abc",
    Property2 = 999,
    Property3 = 123
};

DerivedClass clone = obj1.Clone();

Result - as you can see, Property3 has the default value in the cloned object:

Clone results

Rui Jarimba
  • 11,166
  • 11
  • 56
  • 86
1

Iterate through all properties in type and check your attribute with GetCustomAttributes. See code:

public BaseClass CreateCopy()
{
    var type = GetType();
    var result = Activator.CreateInstance(type);

    foreach (var propertyInfo in type.GetProperties())
    {
        var skipThisProperty = !propertyInfo.GetCustomAttributes(
                typeof(CopiablePropertyAttribute), false)
            .Any();

        if (skipThisProperty)
            continue;

        var value = propertyInfo.GetValue(this, null);
        propertyInfo.SetValue(result, value, null);
    }

    return (BaseClass) result;
}

Please pay attention to null parameter in GetValue and SetValue. If your property is indexer, you need to pass a correct value as last argument

Aleks Andreev
  • 7,016
  • 8
  • 29
  • 37