programing

두 C # 개체 간의 속성 차이 찾기

copyandpastes 2021. 1. 15. 20:16
반응형

두 C # 개체 간의 속성 차이 찾기


내가 작업중인 프로젝트는 사용자가 이메일, 청구 주소 등을 변경할 때 간단한 감사 로깅이 필요합니다. 우리가 작업하는 개체는 WCF 서비스와 웹 서비스의 다른 소스에서 가져옵니다.

리플렉션을 사용하여 두 개의 다른 개체에 대한 속성 변경 사항을 찾기 위해 다음 방법을 구현했습니다. 이렇게하면 이전 값과 새 값과 함께 차이가있는 속성 목록이 생성됩니다.

public static IList GenerateAuditLogMessages(T originalObject, T changedObject)
{
    IList list = new List();
    string className = string.Concat("[", originalObject.GetType().Name, "] ");

    foreach (PropertyInfo property in originalObject.GetType().GetProperties())
    {
        Type comparable =
            property.PropertyType.GetInterface("System.IComparable");

        if (comparable != null)
        {
            string originalPropertyValue =
                property.GetValue(originalObject, null) as string;
            string newPropertyValue =
                property.GetValue(changedObject, null) as string;

            if (originalPropertyValue != newPropertyValue)
            {
                list.Add(string.Concat(className, property.Name,
                    " changed from '", originalPropertyValue,
                    "' to '", newPropertyValue, "'"));
            }
        }
    }

    return list;
}

"모든 숫자 형식 (예 : Int32 및 Double)이 String, Char 및 DateTime과 마찬가지로 IComparable을 구현하기 때문에 System.IComparable을 찾고 있습니다." 이것은 사용자 지정 클래스가 아닌 속성을 찾는 가장 좋은 방법 인 것 같습니다.

WCF 또는 웹 서비스 프록시 코드에 의해 생성 된 PropertyChanged 이벤트를 탭하는 것은 좋지만 내 감사 로그 (이전 및 새 값)에 대한 충분한 정보를 제공하지 않습니다.

이 작업을 수행하는 더 좋은 방법이 있는지에 대한 의견을 찾고 있습니다. 감사합니다!

@Aaronaught, 다음은 object.Equals 수행을 기반으로 긍정적 인 일치를 생성하는 몇 가지 예제 코드입니다.

Address address1 = new Address();
address1.StateProvince = new StateProvince();

Address address2 = new Address();
address2.StateProvince = new StateProvince();

IList list = Utility.GenerateAuditLogMessages(address1, address2);

"[주소] StateProvince가 'MyAccountService.StateProvince'에서 'MyAccountService.StateProvince'로 변경되었습니다."

StateProvince 클래스의 서로 다른 두 인스턴스이지만 속성 값은 동일합니다 (이 경우 모두 null). equals 메서드를 재정의하지 않습니다.


IComparable순서 비교를위한 것입니다. 하나를 사용 IEquatable하는 대신, 아니면 그냥 정적의 사용 System.Object.Equals방법을. 후자는 객체가 기본 유형이 아니지만을 재정 의하여 자체 동등성 비교를 정의하는 경우에도 작동하는 이점이 있습니다 Equals.

object originalValue = property.GetValue(originalObject, null);
object newValue = property.GetValue(changedObject, null);
if (!object.Equals(originalValue, newValue))
{
    string originalText = (originalValue != null) ?
        originalValue.ToString() : "[NULL]";
    string newText = (newText != null) ?
        newValue.ToString() : "[NULL]";
    // etc.
}

이것은 분명히 완벽하지는 않지만 제어하는 ​​클래스로만 수행하는 경우 항상 특정 요구에 맞게 작동하는지 확인할 수 있습니다.

객체를 비교하는 다른 방법 (예 : 체크섬, 직렬화 등)이 있지만 클래스가 일관되게 구현되지 않고 IPropertyChanged실제로 차이점을 알고 싶은 경우 가장 신뢰할 수 있습니다.


새로운 예제 코드 업데이트 :

Address address1 = new Address();
address1.StateProvince = new StateProvince();

Address address2 = new Address();
address2.StateProvince = new StateProvince();

IList list = Utility.GenerateAuditLogMessages(address1, address2);

사용하는 이유 object.Equals인스턴스가 실제로 동일하지 때문에 "히트"에 감사 방법의 결과입니다!

물론,이 StateProvince두 경우 모두 비어있을 수 있지만, address1그리고 address2여전히 비 - 널 (null) 값이 StateProvince속성을 각 인스턴스는 다르다. 따라서, address1address2다른 특성을 가지고있다.

이것을 뒤집어 보겠습니다.이 코드를 예로 들어 보겠습니다.

Address address1 = new Address("35 Elm St");
address1.StateProvince = new StateProvince("TX");

Address address2 = new Address("35 Elm St");
address2.StateProvince = new StateProvince("AZ");

이것들이 동등하다고 간주되어야합니까? 글쎄, 그들은 StateProvince구현하지 않기 때문에 당신의 방법을 사용할 것 IComparable입니다. 이것이 당신의 방법이 원래의 경우 두 개체가 동일하다고보고 한 유일한 이유입니다. StateProvince클래스가를 구현하지 않기 때문에 IComparable추적기는 해당 속성을 완전히 건너 뜁니다. 그러나이 두 주소는 분명히 같지 않습니다!

이것이 내가 원래 사용을 제안한 이유입니다 object.Equals. 왜냐하면 StateProvince더 나은 결과를 얻기 위해 메서드 에서 재정의 할 수 있기 때문입니다 .

public class StateProvince
{
    public string Code { get; set; }

    public override bool Equals(object obj)
    {
        if (obj == null)
            return false;

        StateProvince sp = obj as StateProvince;
        if (object.ReferenceEquals(sp, null))
            return false;

        return (sp.Code == Code);
    }

    public bool Equals(StateProvince sp)
    {
        if (object.ReferenceEquals(sp, null))
            return false;

        return (sp.Code == Code);
    }

    public override int GetHashCode()
    {
        return Code.GetHashCode();
    }

    public override string ToString()
    {
        return string.Format("Code: [{0}]", Code);
    }
}

이 작업을 완료하면 object.Equals코드가 완벽하게 작동합니다. 대신 순진 여부를 검사 address1하고 address2같은이 말 그대로 StateProvince참조를 실제로 의미 어떤지를 확인합니다.


다른 방법은 추적 코드를 확장하여 실제로 하위 개체로 내려가는 것입니다. 즉, 각 속성에 대해 Type.IsClass및 선택적으로 Type.IsInterface속성을 확인하고이면 true속성 자체에서 변경 추적 메서드를 재귀 적으로 호출하여 반환 된 감사 결과에 속성 이름을 접두사로 추가합니다. 그래서 당신은 StateProvinceCode.

위의 접근 방식도 가끔 사용하지만 Equals의미 적 동등성을 비교하려는 객체 (예 : 감사)를 ToString재정의하고 변경된 사항을 명확하게 하는 적절한 재정의를 제공하는 것이 더 쉽습니다 . 딥 네 스팅을 위해 확장되지는 않지만 그런 방식으로 감사하는 것은 드문 일이라고 생각합니다.

마지막 트릭은 IAuditable<T>매개 변수와 동일한 유형의 두 번째 인스턴스를 가져와 실제로 모든 차이점의 목록 (또는 열거 가능)을 반환하는 자체 인터페이스를 정의 하는 것입니다. object.Equals의 재정의 된 방법 과 유사 하지만 더 많은 정보를 제공합니다. 이것은 객체 그래프가 정말 복잡하고 Reflection 또는 Equals. 이를 위의 접근 방식과 결합 할 수 있습니다. 정말 당신이 할 일은 대신 할 IComparable당신을 위해 IAuditable와 인보 Audit은 그 인터페이스를 구현하는 경우 방법.


Codeplex 의이 프로젝트는 거의 모든 유형의 속성을 확인하고 필요에 따라 사용자 지정할 수 있습니다.


Microsoft의 Testapi 를보고 싶을 수 있습니다 . 심층 비교를 수행하는 개체 비교 API가 있습니다. 그것은 당신에게 과잉 일 수도 있지만 볼만한 가치가 있습니다.

var comparer = new ObjectComparer(new PublicPropertyObjectGraphFactory());
IEnumerable<ObjectComparisonMismatch> mismatches;
bool result = comparer.Compare(left, right, out mismatches);

foreach (var mismatch in mismatches)
{
    Console.Out.WriteLine("\t'{0}' = '{1}' and '{2}'='{3}' do not match. '{4}'",
        mismatch.LeftObjectNode.Name, mismatch.LeftObjectNode.ObjectValue,
        mismatch.RightObjectNode.Name, mismatch.RightObjectNode.ObjectValue,
        mismatch.MismatchType);
}

다음은 개체를 확장하고 같지 않은 속성 목록을 반환하는 짧은 LINQ 버전입니다.

사용법 : object.DetailedCompare (objectToCompare);

public static class ObjectExtensions
    {

        public static List<Variance> DetailedCompare<T>(this T val1, T val2)
        {
            var propertyInfo = val1.GetType().GetProperties();
            return propertyInfo.Select(f => new Variance
                {
                    Property = f.Name,
                    ValueA = f.GetValue(val1),
                    ValueB = f.GetValue(val2)
                })
                .Where(v => !v.ValueA.Equals(v.ValueB))
                .ToList();
        }

        public class Variance
        {
            public string Property { get; set; }
            public object ValueA { get; set; }
            public object ValueB { get; set; }
        }

    }

변경 가능한 속성 (누군가가 변경할 수있는 속성)에 GetHashCode를 구현하고 싶지는 않습니다. 즉, 비공개 설정자가 아닙니다.

이 시나리오를 상상해보십시오.

  1. GetHashCode ()를 "내부"또는 직접 (Hashtable) 사용하는 컬렉션에 개체의 인스턴스를 넣습니다.
  2. 그런 다음 누군가 GetHashCode () 구현에서 사용한 필드 / 속성의 값을 변경합니다.

Guess what...your object is permanently lost in the collection since the collection uses GetHashCode() to find it! You've effectively changed the hashcode value from what was originally placed in the collection. Probably not what you wanted.


Liviu Trifoi solution: Using CompareNETObjects library. GitHub - NuGet package - Tutorial.


I think this method is quite neat, it avoids repetition or adding anything to classes. What more are you looking for?

The only alternative would be to generate a state dictionary for the old and new objects, and write a comparison for them. The code for generating the state dictionary could reuse any serialisation you have for storing this data in the database.


The my way of Expression tree compile version. It should faster than PropertyInfo.GetValue.

static class ObjDiffCollector<T>
{
    private delegate DiffEntry DiffDelegate(T x, T y);

    private static readonly IReadOnlyDictionary<string, DiffDelegate> DicDiffDels;

    private static PropertyInfo PropertyOf<TClass, TProperty>(Expression<Func<TClass, TProperty>> selector)
        => (PropertyInfo)((MemberExpression)selector.Body).Member;

    static ObjDiffCollector()
    {
        var expParamX = Expression.Parameter(typeof(T), "x");
        var expParamY = Expression.Parameter(typeof(T), "y");

        var propDrName = PropertyOf((DiffEntry x) => x.Prop);
        var propDrValX = PropertyOf((DiffEntry x) => x.ValX);
        var propDrValY = PropertyOf((DiffEntry x) => x.ValY);

        var dic = new Dictionary<string, DiffDelegate>();

        var props = typeof(T).GetProperties();
        foreach (var info in props)
        {
            var expValX = Expression.MakeMemberAccess(expParamX, info);
            var expValY = Expression.MakeMemberAccess(expParamY, info);

            var expEq = Expression.Equal(expValX, expValY);

            var expNewEntry = Expression.New(typeof(DiffEntry));
            var expMemberInitEntry = Expression.MemberInit(expNewEntry,
                Expression.Bind(propDrName, Expression.Constant(info.Name)),
                Expression.Bind(propDrValX, Expression.Convert(expValX, typeof(object))),
                Expression.Bind(propDrValY, Expression.Convert(expValY, typeof(object)))
            );

            var expReturn = Expression.Condition(expEq
                , Expression.Convert(Expression.Constant(null), typeof(DiffEntry))
                , expMemberInitEntry);

            var expLambda = Expression.Lambda<DiffDelegate>(expReturn, expParamX, expParamY);

            var compiled = expLambda.Compile();

            dic[info.Name] = compiled;
        }

        DicDiffDels = dic;
    }

    public static DiffEntry[] Diff(T x, T y)
    {
        var list = new List<DiffEntry>(DicDiffDels.Count);
        foreach (var pair in DicDiffDels)
        {
            var r = pair.Value(x, y);
            if (r != null) list.Add(r);
        }
        return list.ToArray();
    }
}

class DiffEntry
{
    public string Prop { get; set; }
    public object ValX { get; set; }
    public object ValY { get; set; }
}

ReferenceURL : https://stackoverflow.com/questions/2387946/finding-property-differences-between-two-c-sharp-objects

반응형