C# Find properties shared by two types of the same

2019-07-08 10:55发布

问题:

I need to copy the value of properties from one class to another that are descendants of the same base type. Source and target object may be on different levels of the same inheritance branch, meaning one is derived from the other, or be descendant of different branches, meaning they share a common base type.

      A
  B1      B2
C1 C2      C3

From the structure above I may want to copy all properties from A to C1, C2 to C3, C3 to B1, etc. Basically any possible combination from the tree. Obviously I can only copy properties present in the source type, that must also be present in the target type.

Iterating the properties of the source type is easy as

var sourceProperties = source.GetType().GetProperties();

However how do I check which property is declared on the target type? Simply checking by name is not enough, as they might have different types. Also in the past I made bad experiences with duplicate properties using new.

Unfortunately C# or .NET has no built-in method to check if a type has a certain PropertyInfo like Type.HasProperty(PropertyInfo). The best I could come up with is to check if the property was declared by a shared base type.

public static void CopyProperties(object source, object target)
{
    var targetType = target.GetType();
    var sharedProperties =source.GetType().GetProperties()
        .Where(p => p.DeclaringType.IsAssignableFrom(targetType));

    foreach (var property in sharedProperties)
    {
        var value = property.GetValue(source);
        if (property.CanWrite)
            property.SetValue(target, value);
    }
}

Question: Is there a better solution?

回答1:

Here's a solution that doesn't require inheritance. It copies properties from one object (of one type) to another (of another type) as long as the names and types match.

You create an instance of once of these property copier objects for each pair of types you want to be able to copy from/to. The copier object is immutable once created, so it can be long-lived, static, used from many threads (after created), etc.

Here's the code for the PropertyCopier class. You need to specify the source and destination types when you create an object of this type.

public class PropertyCopier<TSource, TDest> where TSource : class where TDest : class
{
    private List<PropertyCopyPair> _propertiesToCopy = new List<PropertyCopyPair>();

    public PropertyCopier()
    {
        //get all the readable properties of the source type
        var sourceProps = new Dictionary<string, Tuple<Type, MethodInfo>>();
        foreach (var prop in typeof(TSource).GetProperties(BindingFlags.Instance | BindingFlags.Public))
        {
            if (prop.CanRead)
            {
                sourceProps.Add(prop.Name, new Tuple<Type, MethodInfo>(prop.PropertyType, prop.GetGetMethod()));
            }
        }

        //now walk though the writeable properties of the destination type
        //if there's a match by name and type, keep track of them.

        foreach (var prop in typeof(TDest).GetProperties(BindingFlags.Instance | BindingFlags.Public))
        {
            if (prop.CanWrite)
            {
                if (sourceProps.ContainsKey(prop.Name) && sourceProps[prop.Name].Item1 == prop.PropertyType)
                {
                    _propertiesToCopy.Add (new PropertyCopyPair(prop.Name, prop.PropertyType, sourceProps[prop.Name].Item2, prop.GetSetMethod()));
                }
            }
        }
    }

    public void Copy(TSource source, TDest dest)
    {
        foreach (var prop in _propertiesToCopy)
        {
            var val = prop.SourceReader.Invoke(source, null);
            prop.DestWriter.Invoke(dest, new []{val});
        }
    }
}

It relies on a helper class that looks like (this can get stripped down; the extra properties where to help with debugging (and might be useful to you)).

public class PropertyCopyPair
{
    public PropertyCopyPair(string name, Type theType, MethodInfo sourceReader, MethodInfo destWriter)
    {
        PropertyName = name;
        TheType = theType;
        SourceReader = sourceReader;
        DestWriter = destWriter;
    }

    public string PropertyName { get; set; }
    public Type TheType { get; set; }
    public MethodInfo SourceReader { get; set; }
    public MethodInfo DestWriter { get; set; }
}

I also created another real simple class for testing:

public class TestClass
{
    public string PropertyName { get; set; }
    public Type TheType { get; set; }
    public string Other { get; set; }
}

With all that in place, this code exercises the copier class:

 var copier = new PropertyCopier<PropertyCopyPair, TestClass>();
 var source = new PropertyCopyPair("bob", typeof(string), null, null);
 var dest = new TestClass {Other = "other", PropertyName = "PropertyName", TheType = this.GetType()};
 copier.Copy(source, dest);

When you run it, all the properties of the source that have a property in the destination that have the same name and type will get copied.

If you want to restrict the source and destination types to a common base class, you can do this:

public class PropertyCopierCommonBase<TSource, TDest, TBase> : PropertyCopier<TSource, TBase>
    where TBase : class where TSource : class, TBase where TDest : class, TBase
{  }

If you don't want two classes, just declare the original PropertyCopier class with the three type parameters above, and that set of generic constraints.