比较c#中的对象属性

这就是我在许多其他类继承的类上提出的方法。 这个想法是,它允许简单地比较相同类型的对象的属性。

现在,这是行得通的 – 但为了提高我的代码质量,我想我会抛出审查。 如何更好/更有效率等?

/// <summary> /// Compare property values (as strings) /// </summary> /// <param name="obj"></param> /// <returns></returns> public bool PropertiesEqual(object comparisonObject) { Type sourceType = this.GetType(); Type destinationType = comparisonObject.GetType(); if (sourceType == destinationType) { PropertyInfo[] sourceProperties = sourceType.GetProperties(); foreach (PropertyInfo pi in sourceProperties) { if ((sourceType.GetProperty(pi.Name).GetValue(this, null) == null && destinationType.GetProperty(pi.Name).GetValue(comparisonObject, null) == null)) { // if both are null, don't try to compare (throws exception) } else if (!(sourceType.GetProperty(pi.Name).GetValue(this, null).ToString() == destinationType.GetProperty(pi.Name).GetValue(comparisonObject, null).ToString())) { // only need one property to be different to fail Equals. return false; } } } else { throw new ArgumentException("Comparison object must be of the same type.","comparisonObject"); } return true; } 

我正在寻找一些代码,可以做类似于编写单元测试的代码。 这是我最终使用的。

 public static bool PublicInstancePropertiesEqual<T>(T self, T to, params string[] ignore) where T : class { if (self != null && to != null) { Type type = typeof(T); List<string> ignoreList = new List<string>(ignore); foreach (System.Reflection.PropertyInfo pi in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)) { if (!ignoreList.Contains(pi.Name)) { object selfValue = type.GetProperty(pi.Name).GetValue(self, null); object toValue = type.GetProperty(pi.Name).GetValue(to, null); if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue))) { return false; } } } return true; } return self == to; } 

编辑:

与上面相同的代码,但使用LINQ和扩展方法:

 public static bool PublicInstancePropertiesEqual<T>(this T self, T to, params string[] ignore) where T : class { if (self != null && to != null) { var type = typeof(T); var ignoreList = new List<string>(ignore); var unequalProperties = from pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance) where !ignoreList.Contains(pi.Name) && pi.GetUnderlyingType().IsSimpleType() && pi.GetIndexParameters().Length == 0 let selfValue = type.GetProperty(pi.Name).GetValue(self, null) let toValue = type.GetProperty(pi.Name).GetValue(to, null) where selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)) select selfValue; return !unequalProperties.Any(); } return self == to; } public static class TypeExtensions { /// <summary> /// Determine whether a type is simple (String, Decimal, DateTime, etc) /// or complex (ie custom class with public properties and methods). /// </summary> /// <see cref="http://stackoverflow.com/questions/2442534/how-to-test-if-type-is-primitive"/> public static bool IsSimpleType( this Type type) { return type.IsValueType || type.IsPrimitive || new[] { typeof(String), typeof(Decimal), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), typeof(Guid) }.Contains(type) || (Convert.GetTypeCode(type) != TypeCode.Object); } public static Type GetUnderlyingType(this MemberInfo member) { switch (member.MemberType) { case MemberTypes.Event: return ((EventInfo)member).EventHandlerType; case MemberTypes.Field: return ((FieldInfo)member).FieldType; case MemberTypes.Method: return ((MethodInfo)member).ReturnType; case MemberTypes.Property: return ((PropertyInfo)member).PropertyType; default: throw new ArgumentException ( "Input MemberInfo must be if type EventInfo, FieldInfo, MethodInfo, or PropertyInfo" ); } } } 

更新:比较网络对象的最新版本位于GitHub ,有NuGet包和教程 。 它可以被称为像

 //This is the comparison class CompareLogic compareLogic = new CompareLogic(); ComparisonResult result = compareLogic.Compare(person1, person2); //These will be different, write out the differences if (!result.AreEqual) Console.WriteLine(result.DifferencesString); 

或者,如果您需要更改某些配置,请使用

 CompareLogic basicComparison = new CompareLogic() { Config = new ComparisonConfig() { MaxDifferences = propertyCount //add other configurations } }; 

完整的可配置参数列表在ComparisonConfig.cs中

原始答案:

我在你的代码中看到的限制:

  • 最大的一点是它不做深入的对象比较。

  • 如果属性是列表或包含列表作为元素(这可以是n级别的),则不通过元素比较来执行元素。

  • 它不考虑某些类型的属性不应该被比较(例如,用于过滤目的的Func属性,如PagedCollectionView类中的一个)。

  • 它没有跟踪哪些属性实际上是不同的(所以你可以显示你的断言)。

我今天正在寻找一些解决方案,为单位测试的目的,通过财产深入比较做财产,我最终使用: http : //comparenetobjects.codeplex.com 。

这是一个免费的图书馆,只有一个类,你可以简单地使用这样的:

 var compareObjects = new CompareObjects() { CompareChildren = true, //this turns deep compare one, otherwise it's shallow CompareFields = false, CompareReadOnly = true, ComparePrivateFields = false, ComparePrivateProperties = false, CompareProperties = true, MaxDifferences = 1, ElementsToIgnore = new List<string>() { "Filter" } }; Assert.IsTrue( compareObjects.Compare(objectA, objectB), compareObjects.DifferencesString ); 

此外,它可以轻松地重新编译为Silverlight。 只需将一个类复制到Silverlight项目中,并删除一行或两行代码,以便在私有成员比较中进行Silverlight中不可用的比较。

我认为最好遵循Override Object#Equals()的模式
为了更好的描述:阅读比尔·瓦格纳的有效的C# – 第9项我认为

 public override Equals(object obOther) { if (null == obOther) return false; if (object.ReferenceEquals(this, obOther) return true; if (this.GetType() != obOther.GetType()) return false; # private method to compare members. return CompareMembers(this, obOther as ThisClass); } 
  • 同样在检查相等性的方法中,您应该返回true或false。 要么他们是平等的或他们不是..而不是抛出一个异常,返回false。
  • 我会考虑重写Object#Equals。
  • 即使你必须考虑到这一点,使用反射比较属性应该是慢(我没有数字来支持)。 这是C#中valueType#Equals的默认行为,建议您为值类型覆盖Equals,并对性能进行明智的比较。 (之前,我速度阅读这个,因为你有一个自定义的属性对象的集合…我的坏。)

更新 – 2011年12月:

  • 当然,如果这个类型已经有一个生产Equals(),那么你需要另一种方法。
  • 如果您正在使用这种方法来比较不可变数据结构以用于测试目的,那么您不应该将Equals添加到生产类(有些人可能会通过查找Equals实现来对测试进行压缩,或者可能会阻止创建生产所需的Equals实现) 。

如果性能无关紧要,您可以序列化它们并比较结果:

 var serializer = new XmlSerializer(typeof(TheObjectType)); StringWriter serialized1 = new StringWriter(), serialized2 = new StringWriter(); serializer.Serialize(serialized1, obj1); serializer.Serialize(serialized2, obj2); bool areEqual = serialized1.ToString() == serialized2.ToString(); 

我认为大T的答案是相当不错的,但是深度比较失败了,所以我调整了一下:

 using System.Collections.Generic; using System.Reflection; /// <summary>Comparison class.</summary> public static class Compare { /// <summary>Compare the public instance properties. Uses deep comparison.</summary> /// <param name="self">The reference object.</param> /// <param name="to">The object to compare.</param> /// <param name="ignore">Ignore property with name.</param> /// <typeparam name="T">Type of objects.</typeparam> /// <returns><see cref="bool">True</see> if both objects are equal, else <see cref="bool">false</see>.</returns> public static bool PublicInstancePropertiesEqual<T>(T self, T to, params string[] ignore) where T : class { if (self != null && to != null) { var type = self.GetType(); var ignoreList = new List<string>(ignore); foreach (var pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (ignoreList.Contains(pi.Name)) { continue; } var selfValue = type.GetProperty(pi.Name).GetValue(self, null); var toValue = type.GetProperty(pi.Name).GetValue(to, null); if (pi.PropertyType.IsClass && !pi.PropertyType.Module.ScopeName.Equals("CommonLanguageRuntimeLibrary")) { // Check of "CommonLanguageRuntimeLibrary" is needed because string is also a class if (PublicInstancePropertiesEqual(selfValue, toValue, ignore)) { continue; } return false; } if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue))) { return false; } } return true; } return self == to; } } 

我会将下面一行添加到PublicInstancePropertiesEqual方法,以避免复制和粘贴错误:

 Assert.AreNotSame(self, to); 

你重写.ToString()在属性中的所有对象? 否则,第二个比较可能会返回null。

另外,在第二个比较中,从(A!= B)开始,在可读性方面,六个月/两年后,我正在围绕着!(A == B)的构造。 这条线本身很宽,如果你的显示器很宽,可以不用打印出来。 (挑剔)

是否所有的对象总是使用这样的代码将工作的属性? 会不会有一些内部的非属性数据可能不同于一个对象,但是所有暴露的数据都是一样的? 我正在考虑一些可能会随着时间而改变的数据,比如两个随机数字生成器碰巧在同一个点上碰到相同的数字,但是会产生两个不同的信息序列,或者只是没有暴露的数据通过财产界面。

确保对象不为空。

有obj1和obj2:

  if( obj1 == null ) { return false; } return obj1.Equals( obj2 ); 

如果只比较相同类型的对象或继承链中的下一个对象,为什么不将该参数指定为基类型而不是对象?

也对参数做空检查。

此外,我会使用'var'只是为了使代码更具可读性(如果它的C#3代码)

此外,如果对象具有引用类型作为属性,那么你只是在它们上调用ToString(),它并不真正比较值。 如果ToString没有被覆盖,那么它只是返回类型名称作为可能返回误报的字符串。

我建议的第一件事就是将实际的比较分开,这样它就更具可读性了(我还拿出了ToString() – 是否需要?):

 else { object originalProperty = sourceType.GetProperty(pi.Name).GetValue(this, null); object comparisonProperty = destinationType.GetProperty(pi.Name).GetValue(comparisonObject, null); if (originalProperty != comparisonProperty) return false; 

下一个建议是尽量减少反射的使用 – 这是非常缓慢的。 我的意思是, 真的很慢。 如果你要这样做,我会建议缓存属性引用。 我对Reflection API并不熟悉,所以如果这是一点点的话,只要调整它就可以编译:

 // elsewhere Dictionary<object, Property[]> lookupDictionary = new Dictionary<object, Property[]>; Property[] objectProperties = null; if (lookupDictionary.ContainsKey(sourceType)) { objectProperties = lookupProperties[sourceType]; } else { // build array of Property references PropertyInfo[] sourcePropertyInfos = sourceType.GetProperties(); Property[] sourceProperties = new Property[sourcePropertyInfos.length]; for (int i=0; i < sourcePropertyInfos.length; i++) { sourceProperties[i] = sourceType.GetProperty(pi.Name); } // add to cache objectProperties = sourceProperties; lookupDictionary[object] = sourceProperties; } // loop through and compare against the instances 

不过,我不得不说,我同意其他海报。 这闻起来很懒而且效率低下。 您应该实施IComparable而不是:-)。

这里修改一个将null = null等同处理

  private bool PublicInstancePropertiesEqual<T>(T self, T to, params string[] ignore) where T : class { if (self != null && to != null) { Type type = typeof(T); List<string> ignoreList = new List<string>(ignore); foreach (PropertyInfo pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (!ignoreList.Contains(pi.Name)) { object selfValue = type.GetProperty(pi.Name).GetValue(self, null); object toValue = type.GetProperty(pi.Name).GetValue(to, null); if (selfValue != null) { if (!selfValue.Equals(toValue)) return false; } else if (toValue != null) return false; } } return true; } return self == to; } 

我结束了这样做:

  public static string ToStringNullSafe(this object obj) { return obj != null ? obj.ToString() : String.Empty; } public static bool Compare<T>(T a, T b) { int count = a.GetType().GetProperties().Count(); string aa, bb; for (int i = 0; i < count; i++) { aa = a.GetType().GetProperties()[i].GetValue(a, null).ToStringNullSafe(); bb = b.GetType().GetProperties()[i].GetValue(b, null).ToStringNullSafe(); if (aa != bb) { return false; } } return true; } 

用法:

  if (Compare<ObjectType>(a, b)) 

更新

如果你想通过名称忽略一些属性:

  public static string ToStringNullSafe(this object obj) { return obj != null ? obj.ToString() : String.Empty; } public static bool Compare<T>(T a, T b, params string[] ignore) { int count = a.GetType().GetProperties().Count(); string aa, bb; for (int i = 0; i < count; i++) { aa = a.GetType().GetProperties()[i].GetValue(a, null).ToStringNullSafe(); bb = b.GetType().GetProperties()[i].GetValue(b, null).ToStringNullSafe(); if (aa != bb && ignore.Where(x => x == a.GetType().GetProperties()[i].Name).Count() == 0) { return false; } } return true; } 

用法:

  if (MyFunction.Compare<ObjType>(a, b, "Id","AnotherProp")) 

您可以通过每种类型只调用一次GetProperties来优化代码:

 public static string ToStringNullSafe(this object obj) { return obj != null ? obj.ToString() : String.Empty; } public static bool Compare<T>(T a, T b, params string[] ignore) { var aProps = a.GetType().GetProperties(); var bProps = b.GetType().GetProperties(); int count = aProps.Count(); string aa, bb; for (int i = 0; i < count; i++) { aa = aProps[i].GetValue(a, null).ToStringNullSafe(); bb = bProps[i].GetValue(b, null).ToStringNullSafe(); if (aa != bb && ignore.Where(x => x == aProps[i].Name).Count() == 0) { return false; } } return true; } 

即使对象不同,这也是有效的。 你可以自定义在公共事业类的方法,也许你想比较私人财产以及…

 using System; using System.Collections.Generic; using System.Linq; using System.Text; class ObjectA { public string PropertyA { get; set; } public string PropertyB { get; set; } public string PropertyC { get; set; } public DateTime PropertyD { get; set; } public string FieldA; public DateTime FieldB; } class ObjectB { public string PropertyA { get; set; } public string PropertyB { get; set; } public string PropertyC { get; set; } public DateTime PropertyD { get; set; } public string FieldA; public DateTime FieldB; } class Program { static void Main(string[] args) { // create two objects with same properties ObjectA a = new ObjectA() { PropertyA = "test", PropertyB = "test2", PropertyC = "test3" }; ObjectB b = new ObjectB() { PropertyA = "test", PropertyB = "test2", PropertyC = "test3" }; // add fields to those objects a.FieldA = "hello"; b.FieldA = "Something differnt"; if (a.ComparePropertiesTo(b)) { Console.WriteLine("objects have the same properties"); } else { Console.WriteLine("objects have diferent properties!"); } if (a.CompareFieldsTo(b)) { Console.WriteLine("objects have the same Fields"); } else { Console.WriteLine("objects have diferent Fields!"); } Console.Read(); } } public static class Utilities { public static bool ComparePropertiesTo(this Object a, Object b) { System.Reflection.PropertyInfo[] properties = a.GetType().GetProperties(); // get all the properties of object a foreach (var property in properties) { var propertyName = property.Name; var aValue = a.GetType().GetProperty(propertyName).GetValue(a, null); object bValue; try // try to get the same property from object b. maybe that property does // not exist! { bValue = b.GetType().GetProperty(propertyName).GetValue(b, null); } catch { return false; } if (aValue == null && bValue == null) continue; if (aValue == null && bValue != null) return false; if (aValue != null && bValue == null) return false; // if properties do not match return false if (aValue.GetHashCode() != bValue.GetHashCode()) { return false; } } return true; } public static bool CompareFieldsTo(this Object a, Object b) { System.Reflection.FieldInfo[] fields = a.GetType().GetFields(); // get all the properties of object a foreach (var field in fields) { var fieldName = field.Name; var aValue = a.GetType().GetField(fieldName).GetValue(a); object bValue; try // try to get the same property from object b. maybe that property does // not exist! { bValue = b.GetType().GetField(fieldName).GetValue(b); } catch { return false; } if (aValue == null && bValue == null) continue; if (aValue == null && bValue != null) return false; if (aValue != null && bValue == null) return false; // if properties do not match return false if (aValue.GetHashCode() != bValue.GetHashCode()) { return false; } } return true; } } 

Liviu上面的答案更新 – CompareObjects.DifferencesString已被弃用。

这在单元测试中运行良好:

 CompareLogic compareLogic = new CompareLogic(); ComparisonResult result = compareLogic.Compare(object1, object2); Assert.IsTrue(result.AreEqual); 

此方法将获得类的properties并比较每个property的值。 如果任何值不同,则return false ,否则return true

 public static bool Compare<T>(T Object1, T object2) { //Get the type of the object Type type = typeof(T); //return false if any of the object is false if (Object1 == null || object2 == null) return false; //Loop through each properties inside class and get values for the property from both the objects and compare foreach (System.Reflection.PropertyInfo property in type.GetProperties()) { if (property.Name != "ExtensionData") { string Object1Value = string.Empty; string Object2Value = string.Empty; if (type.GetProperty(property.Name).GetValue(Object1, null) != null) Object1Value = type.GetProperty(property.Name).GetValue(Object1, null).ToString(); if (type.GetProperty(property.Name).GetValue(object2, null) != null) Object2Value = type.GetProperty(property.Name).GetValue(object2, null).ToString(); if (Object1Value.Trim() != Object2Value.Trim()) { return false; } } } return true; } 

用法:

bool isEqual = Compare<Employee>(Object1, Object2)

为了扩展@nawfal:的答案,我使用它来测试单元测试中不同类型的对象,以比较相同的属性名称。 在我的情况下数据库实体和DTO。

在我的测试中这样使用

 Assert.IsTrue(resultDto.PublicInstancePropertiesEqual(expectedEntity)); public static bool PublicInstancePropertiesEqual<T, Z>(this T self, Z to, params string[] ignore) where T : class { if (self != null && to != null) { var type = typeof(T); var type2 = typeof(Z); var ignoreList = new List<string>(ignore); var unequalProperties = from pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance) where !ignoreList.Contains(pi.Name) let selfValue = type.GetProperty(pi.Name).GetValue(self, null) let toValue = type2.GetProperty(pi.Name).GetValue(to, null) where selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)) select selfValue; return !unequalProperties.Any(); } return self == null && to == null; } 

有时你不想比较所有公共属性,只想比较它们的子集,所以在这种情况下,你可以移动逻辑来比较所需的属性列表到抽象类

 public abstract class ValueObject<T> where T : ValueObject<T> { protected abstract IEnumerable<object> GetAttributesToIncludeInEqualityCheck(); public override bool Equals(object other) { return Equals(other as T); } public bool Equals(T other) { if (other == null) { return false; } return GetAttributesToIncludeInEqualityCheck() .SequenceEqual(other.GetAttributesToIncludeInEqualityCheck()); } public static bool operator ==(ValueObject<T> left, ValueObject<T> right) { return Equals(left, right); } public static bool operator !=(ValueObject<T> left, ValueObject<T> right) { return !(left == right); } public override int GetHashCode() { int hash = 17; foreach (var obj in this.GetAttributesToIncludeInEqualityCheck()) hash = hash * 31 + (obj == null ? 0 : obj.GetHashCode()); return hash; } } 

并稍后使用这个抽象类来比较这些对象

 public class Meters : ValueObject<Meters> { ... protected decimal DistanceInMeters { get; private set; } ... protected override IEnumerable<object> GetAttributesToIncludeInEqualityCheck() { return new List<Object> { DistanceInMeters }; } } 

我的解决方案灵感来自Aras Alenin上面的答案,我添加了一个级别的对象比较和自定义对象的比较结果。 我也有兴趣获得对象名称的属性名称:

  public static IEnumerable<ObjectPropertyChanged> GetPublicSimplePropertiesChanged<T>(this T previous, T proposedChange, string[] namesOfPropertiesToBeIgnored) where T : class { return GetPublicGenericPropertiesChanged(previous, proposedChange, namesOfPropertiesToBeIgnored, true, null, null); } public static IReadOnlyList<ObjectPropertyChanged> GetPublicGenericPropertiesChanged<T>(this T previous, T proposedChange, string[] namesOfPropertiesToBeIgnored) where T : class { return GetPublicGenericPropertiesChanged(previous, proposedChange, namesOfPropertiesToBeIgnored, false, null, null); } /// <summary> /// Gets the names of the public properties which values differs between first and second objects. /// Considers 'simple' properties AND for complex properties without index, get the simple properties of the children objects. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="previous">The previous object.</param> /// <param name="proposedChange">The second object which should be the new one.</param> /// <param name="namesOfPropertiesToBeIgnored">The names of the properties to be ignored.</param> /// <param name="simpleTypeOnly">if set to <c>true</c> consider simple types only.</param> /// <param name="parentTypeString">The parent type string. Meant only for recursive call with simpleTypeOnly set to <c>true</c>.</param> /// <param name="secondType">when calling recursively, the current type of T must be clearly defined here, as T will be more generic (using base class).</param> /// <returns> /// the names of the properties /// </returns> private static IReadOnlyList<ObjectPropertyChanged> GetPublicGenericPropertiesChanged<T>(this T previous, T proposedChange, string[] namesOfPropertiesToBeIgnored, bool simpleTypeOnly, string parentTypeString, Type secondType) where T : class { List<ObjectPropertyChanged> propertiesChanged = new List<ObjectPropertyChanged>(); if (previous != null && proposedChange != null) { var type = secondType == null ? typeof(T) : secondType; string typeStr = parentTypeString + type.Name + "."; var ignoreList = namesOfPropertiesToBeIgnored.CreateList(); IEnumerable<IEnumerable<ObjectPropertyChanged>> genericPropertiesChanged = from pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance) where !ignoreList.Contains(pi.Name) && pi.GetIndexParameters().Length == 0 && (!simpleTypeOnly || simpleTypeOnly && pi.PropertyType.IsSimpleType()) let firstValue = type.GetProperty(pi.Name).GetValue(previous, null) let secondValue = type.GetProperty(pi.Name).GetValue(proposedChange, null) where firstValue != secondValue && (firstValue == null || !firstValue.Equals(secondValue)) let subPropertiesChanged = simpleTypeOnly || pi.PropertyType.IsSimpleType() ? null : GetPublicGenericPropertiesChanged(firstValue, secondValue, namesOfPropertiesToBeIgnored, true, typeStr, pi.PropertyType) let objectPropertiesChanged = subPropertiesChanged != null && subPropertiesChanged.Count() > 0 ? subPropertiesChanged : (new ObjectPropertyChanged(proposedChange.ToString(), typeStr + pi.Name, firstValue.ToStringOrNull(), secondValue.ToStringOrNull())).CreateList() select objectPropertiesChanged; if (genericPropertiesChanged != null) { // get items from sub lists genericPropertiesChanged.ForEach(a => propertiesChanged.AddRange(a)); } } return propertiesChanged; } 

使用以下类来存储比较结果

 [System.Serializable] public class ObjectPropertyChanged { public ObjectPropertyChanged(string objectId, string propertyName, string previousValue, string changedValue) { ObjectId = objectId; PropertyName = propertyName; PreviousValue = previousValue; ProposedChangedValue = changedValue; } public string ObjectId { get; set; } public string PropertyName { get; set; } public string PreviousValue { get; set; } public string ProposedChangedValue { get; set; } } 

而一个样本单元测试:

  [TestMethod()] public void GetPublicGenericPropertiesChangedTest1() { // Define objects to test Function func1 = new Function { Id = 1, Description = "func1" }; Function func2 = new Function { Id = 2, Description = "func2" }; FunctionAssignment funcAss1 = new FunctionAssignment { Function = func1, Level = 1 }; FunctionAssignment funcAss2 = new FunctionAssignment { Function = func2, Level = 2 }; // Main test: read properties changed var propertiesChanged = Utils.GetPublicGenericPropertiesChanged(funcAss1, funcAss2, null); Assert.IsNotNull(propertiesChanged); Assert.IsTrue(propertiesChanged.Count == 3); Assert.IsTrue(propertiesChanged[0].PropertyName == "FunctionAssignment.Function.Description"); Assert.IsTrue(propertiesChanged[1].PropertyName == "FunctionAssignment.Function.Id"); Assert.IsTrue(propertiesChanged[2].PropertyName == "FunctionAssignment.Level"); }