fastest way to compare 2 objects excluding a few p

2020-05-06 21:35发布

问题:

I have a site where users upload data into it and I only want to update data where properties have been changed. So I am comparing 2 objects of the same type for changes and I need to exclude a few properties such as ModifiedOn which is a date.

Here is my code thus far using reflection:

 private bool hasChanges(object OldObject, object newObject)
        {
            var oldprops = (from p in OldObject.GetType().GetProperties() select p).ToList();
            var newprops = (from p in newObject.GetType().GetProperties() select p).ToList();
            bool isChanged = false;
            foreach (PropertyInfo i in oldprops)
            {
                if (checkColumnNames(i.Name))
                {
                    var newInfo = (from x in newprops where x.Name == i.Name select x).Single();
                    var oldVal = i.GetValue(OldObject, null);
                    var newVal = newInfo.GetValue(newObject, null);

                    if (newVal == null || oldVal == null)
                    {
                        if (newVal == null && oldVal != null)
                        {
                            isChanged = true;
                            return true;
                        }
                        if (oldVal == null && newVal != null)
                        {
                            isChanged = true;
                              return true;
                        }
                    }
                    else
                    {
                        if (!newVal.Equals(oldVal))
                        {
                            isChanged = true;
                         return true;
                        }
                    }
                }
            }

            return isChanged;
        }

I ignore certain columns with this method:

private bool checkColumnNames(string colName)
        {
            if (
                colName.ToLower() == "productid" ||
                colName.ToLower() == "customerid" ||
                colName.ToLower() == "shiptoid" ||
                colName.ToLower() == "parentchildid" ||
                colName.ToLower() == "categoryitemid" ||
                 colName.ToLower() == "volumepricingid" ||
                colName.ToLower() == "tagid" ||
                colName.ToLower() == "specialprice" ||
                colName.ToLower() == "productsmodifierid" ||
                colName.ToLower() == "modifierlistitemid" ||
                colName.ToLower() == "modifierlistid" ||
                colName.ToLower() == "categoryitemid" ||
                colName.ToLower() == "createdon" ||
                colName.ToLower() == "createdby" ||
                colName.ToLower() == "modifiedon" ||
                colName.ToLower() == "modifiedby" ||
                colName.ToLower() == "deletedon" ||
                colName.ToLower() == "deletedby" ||
                colName.ToLower() == "appendproductmodifiers" ||
                colName.ToLower() == "introdate" ||
                colName.ToLower() == "id" ||
                colName.ToLower() == "discontinued" ||
                colName.ToLower() == "stagingcategories"
                )
                return false;

            return true;
        }

This has been working very well except for now I have users comparing 50,000+ items in a single upload which is taking a really long time.

Is there a faster way to accomplish this?

回答1:

It would certainly be faster if you just used reflection to create and compile a method using the above logic. This should be much faster than reflecting on each object.



回答2:

Compile and cache your code using expression trees or dynamic methods. You will probably see a 10-100x performance improvement. Your original reflection code to retrieve properties and you can use it as a basis for creating a compiled version.

Example

Here is a snippet of code I use in a framework for reading all of the properties of an object to track state changes. In this scenario, I do not know any of the property names of the object. All of the property values are placed into a StringBuilder.

I've simplified this from my original code; it still compiles, but you may need to tweak it.

private static DynamicMethod CreateChangeTrackingReaderIL( Type type, Type[] types )
{
    var method = new DynamicMethod( string.Empty, typeof( string ), new[] { type } );

    ILGenerator il = method.GetILGenerator();
    LocalBuilder lbInstance = il.DeclareLocal( type );

    // place the input parameter of the function onto the evaluation stack
    il.Emit( OpCodes.Ldarg_0 ); 

    // store the input value
    il.Emit( OpCodes.Stloc, lbInstance ); 

    // declare a StringBuilder
    il.Emit( OpCodes.Newobj, typeof( StringBuilder ).GetConstructor( Type.EmptyTypes ) ); 

    foreach( Type t in types )
    {
        // any logic to retrieve properties can go here...
        List<PropertyInfo> properties = __Properties.GetTrackableProperties( t );

        foreach( PropertyInfo pi in properties )
        {
            MethodInfo mi = pi.GetGetMethod();

            if( null == mi )
            {
                continue;
            }

            il.Emit( OpCodes.Ldloc, lbInstance ); // bring the stored reference onto the eval stack
            il.Emit( OpCodes.Callvirt, mi ); // call the appropriate getter method

            if( pi.PropertyType.IsValueType )
            {
                il.Emit( OpCodes.Box, pi.PropertyType ); // box the return value if necessary
            }

            // append it to the StringBuilder
            il.Emit( OpCodes.Callvirt, typeof( StringBuilder ).GetMethod( "Append", new Type[] { typeof( object ) } ) ); 
        }
    }

    // call ToString() on the StringBuilder
    il.Emit( OpCodes.Callvirt, typeof( StringBuilder ).GetMethod( "ToString", Type.EmptyTypes ) ); 

    // return the last value on the eval stack (output of ToString())
    il.Emit( OpCodes.Ret ); 

    return method;
}

Note that if you are not familiar with IL generation, most people find expression trees much easier to work with. Either approach has a similar result.



回答3:

Are the objects guaranteed to have the same type? Even if not, you could check them, and if they are the same type, send them to this method:

private bool hasChanges(object OldObject, object newObject) 
    { 
        var props = OldObject.GetType().GetProperties();
        foreach (PropertyInfo i in props) 
        { 
            if (checkColumnNames(i.Name)) 
            { 
                var oldVal = i.GetValue(OldObject, null); 
                var newVal = i.GetValue(newObject, null); 

                if (newVal == null) 
                {
                    if (oldVal != null) 
                    { 
                        return true; 
                    }
                }
                else if (oldVal == null)
                {
                    return true;
                }
                else if (!newVal.Equals(oldVal)) 
                { 
                    return true; 
                } 
            } 
        } 
        return false; 
    } 

This is only slightly more efficient than your method. As Tim Medora and PinnyM noted, it would be quicker still to emit code dynamically and cache the result, which would mean that you take the reflection hit only once, rather than once per object.

Note also that according to Best Practices for Using Strings in the .NET Framework, you should be using ToUpper rather than ToLower for your string comparisons, but you should be using String.Equals(string, string, StringComparison) rather than converting the case yourself. That will have one advantage, at least: Equals returns false if the strings are different lengths, so you skip the case conversion. That will save a bit of time as well.

Another thought:

If the objects are of different types, you can still improve your algorithm by joining the properties collections rather than using the repeated linear search you have:

private bool hasChanges(object OldObject, object newObject)  
    {  
        var oldprops = OldObject.GetType().GetProperties();  
        var newprops = newObject.GetType().GetProperties();
        var joinedProps = from oldProp in oldprops
                          join newProp in newProps
                              on oldProp.Name equals newProp.Name
                          select new { oldProp, newProp }
        foreach (var pair in joinedProps)  
        {  
            if (checkColumnNames(pair.oldProp.Name))  
            {  
                var oldVal = pair.oldProp.GetValue(OldObject, null);  
                var newVal = pair.newProp.GetValue(newObject, null);  

//etcetera

Final thought (inspired by Tim Schmelter's comment):

Override object.Equals on your classes, and use

private bool HasChanges(object o1, object o2) { return !o1.Equals(o2); }

Sample class:

class SomeClass
{
    public string SomeString { get; set; }
    public int SomeInt { get; set; }
    public DateTime SomeDateTime { get; set; }
    public bool Equals(object other)
    {
        SomeClass other1 = other as SomeClass;

        if (other1 != null)
            return other1.SomeInt.Equals(SomeInt)
                && other1.SomeDateTime.Equals(SomeDateTime)
                && other1.SomeString.Equals(SomeString); //or whatever string equality check you prefer

        //possibly check for other types here, if necessary

        return false;
    }
}


标签: c# asp.net