5

I have a database containing components with about 20 properties. To find out if an update is needed I want to check if all properties for the two objects, except DateCreated and Id, matches. If all matches no update, if not, update db.

Component comp_InApp = new Component()
{
    Id = null,
    Description = "Commponent",
    Price = 100,
    DateCreated = "2019-01-30",
    // Twenty more prop
};

Component comp_InDb = new Component()
{
    Id = 1,
    Description = "Component",
    Price = 100,
    DateCreated = "2019-01-01",
    // Twenty more prop
};

// Check if all properties match, except DateCreated and Id.
if (comp_InApp.Description == comp_InDb.Description &&
    comp_InApp.Price == comp_InDb.Price
    // Twenty more prop
    )
{
    // Everything up to date.
}
else
{
    // Update db.
}

This works, but it's not a very clean way with 20 properties. Is there a better way of achieiving the same result in a cleaner way?

meJustAndrew
  • 6,011
  • 8
  • 50
  • 76
  • You can use `Equals` and `GetHashCode` methods, or you can use an equality library that can make the comparison for you. – meJustAndrew Jan 30 '19 at 11:13
  • 2
    Possible duplicate of [Comparing object properties in c#](https://stackoverflow.com/questions/506096/comparing-object-properties-in-c-sharp) . check the answer with 64 upvotes. stuff like this is easily done with some libraries. – Andrei Dragotoniu Jan 30 '19 at 11:13
  • Don't use a string to store a `DateTime` – Tim Schmelter Jan 30 '19 at 11:14

4 Answers4

3

I am using DeepEqual when I don't want/don't have the time to write myself Equals and GetHashCode methods.

You can install it simply from NuGet with:

Install-Package DeepEqual

and use it like:

    if (comp_InApp.IsDeepEqual(comp_InDb))
    {
        // Everything up to date.
    }
    else
    {
        // Update db.
    }

But keep in mind that this will only work for your case when you want to explicitly compare objects, but not for the case when you want to remove an object form a List or cases like this, when Equals and GetHashCode are invoked.

meJustAndrew
  • 6,011
  • 8
  • 50
  • 76
2

One way, create a class that implements IEqualityComparer<Component> to encapsulate this logic and to avoid that you have modify the class Comparer itself(if you don't want this Equals logic all time). Then you can use it for a simple Equals of two instances of Component and even for all LINQ methods that accepts it as additional argument.

class ComponentComparer : IEqualityComparer<Component>
{
    public bool Equals(Component x, Component y)
    {
        if (object.ReferenceEquals(x, y)) return true;
        if (x == null || y == null) return false;
        return x.Price == y.Price && x.Description == y.Description;
    }

    public int GetHashCode(Component obj)
    {
        unchecked 
        {
            int hash = 17;
            hash = hash * 23 + obj.Price.GetHashCode();
            hash = hash * 23 + obj.Description?.GetHashCode() ?? 0;
            // ...
            return hash;
        }
    }
}

Your simple use-case:

var comparer = new ComponentComparer();
bool equal = comparer.Equals(comp_InApp, comp_InDb);

It works also if you have two collections and want to know the difference, for example:

IEnumerable<Component> missingInDb = inAppList.Except( inDbList, comparer );
Tim Schmelter
  • 450,073
  • 74
  • 686
  • 939
1

Here is a solution with Reflection:

    static bool AreTwoEqual(Component inApp, Component inDb)
    {
        string[] propertiesToExclude = new string[] { "DateCreated", "Id" };

        PropertyInfo[] propertyInfos = typeof(Component).GetProperties()
                                                 .Where(x => !propertiesToExclude.Contains(x.Name))
                                                 .ToArray();

        foreach (PropertyInfo propertyInfo in propertyInfos)
        {
            bool areSame = inApp.GetType().GetProperty(propertyInfo.Name).GetValue(inApp, null).Equals(inDb.GetType().GetProperty(propertyInfo.Name).GetValue(inDb, null));

            if (!areSame)
            {
                return false;
            }
        }

        return true;
    }

0

You can use a Reflection but it may slow your application. An alternative way of creating that comparator is to generate it with Linq Expressions. Try this code:

public static Expression<Func<T, T, bool>> CreateAreEqualExpression<T>(params string[] toExclude)
{
    var type = typeof(T);
    var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
        .Where(p => !toExclude.Contains(p.Name))
        .ToArray();

    var p1 = Expression.Parameter(type, "p1");
    var p2 = Expression.Parameter(type, "p2");

    Expression body = null;
    foreach (var property in props)
    {
        var pare = Expression.Equal(
            Expression.PropertyOrField(p1, property.Name),
            Expression.PropertyOrField(p2, property.Name)
        );

        body = body == null ? pare : Expression.AndAlso(body, pare);
    }

    if (body == null) // all properties are excluded
        body = Expression.Constant(true);

    var lambda = Expression.Lambda<Func<T, T, bool>>(body, p1, p2);
    return lambda;
}

it will generate an expression that looks like

(Component p1, Component p2) => ((p1.Description == p2.Description) && (p1.Price == p2.Price))

Usage is simple

var comporator = CreateAreEqualExpression<Component>("Id", "DateCreated")
    .Compile(); // save compiled comparator somewhere to use it again later
var areEqual = comporator(comp_InApp, comp_InDb);

EDIT: to make it more type safe you can exclude properties using lambdas

public static Expression<Func<T, T, bool>> CreateAreEqualExpression<T>(
  params Expression<Func<T, object>>[] toExclude)
{
    var exclude = toExclude
        .Select(e =>
        {
            // for properties that is value types (int, DateTime and so on)
            var name = ((e.Body as UnaryExpression)?.Operand as MemberExpression)?.Member.Name;
            if (name != null)
                return name;

            // for properties that is reference type
            return (e.Body as MemberExpression)?.Member.Name;
        })
        .Where(n => n != null)
        .Distinct()            
        .ToArray();

    var type = typeof(T);
    var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
        .Where(p => !exclude.Contains(p.Name))
        .ToArray();

    /* rest of code is unchanged */
}

Now when using it we have an IntelliSense support:

var comparator = CreateAreEqualExpression<Component>(
        c => c.Id,
        c => c.DateCreated)
    .Compile();
Aleks Andreev
  • 7,016
  • 8
  • 29
  • 37