65.9K
CodeProject 正在变化。 阅读更多。
Home

Expression API 食谱

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (46投票s)

2013 年 9 月 10 日

CPOL

16分钟阅读

viewsIcon

90169

downloadIcon

482

一个 Expression API 操作指南。

在此处下载演示代码

引言 

本文在很多方面都有些奇怪,因为它没有一个真正意义上的最终产品,它更多的是一篇“我该如何做 X / 食谱”类型的文章。那么,这些食谱是什么,它将教会读者什么呢?

嗯,我将先讲一个小故事(别担心,它是有目的的)。很久以前,我收到一封来自美国一个家伙的电子邮件,他正在为 Windows 8 创建一个 MVVM 框架。我一直与这家伙保持联系(他确实是个家伙,如果我能说服他上传一张照片,你就会明白我的意思),我们谈到了他 MVVM 框架中的 IOC 容器,它类似于 Windows 8 的 MEF。

浏览 Ian 的代码,我立刻就明白,Ian 真的(我是说真的)非常了解如何在 .NET 中使用 Expression API。这个命名空间一直让我很感兴趣,所以我与 Ian 讨论了撰写一篇联合文章的优点,其中我们将有效地提出一些使用 Expression API 来解决的场景。

Ian 表示,如果我愿意负责文章撰写方面的工作,他非常乐意为我提出的任何场景编写代码。这似乎是一种非常公平的方式,所以我们就这么做了。

现在你可能会问,Expression API 有什么酷之处?嗯,我喜欢的一点是,你真的可以用它来编写整个程序,而另一件你一次又一次看到的事情是,创建动态构建的已编译 Lambda 表达式,这些表达式会编译成委托,因此与反射相比,性能极高。这就是 Expression API 有用之处(至少我们是这么认为的)。

这实际上就是本文的全部内容,我们有一系列我提出的场景(希望它们是好的场景),由 Ian 编写了代码。示例范围从简单的属性获取/设置到一些相当复杂的示例,我们在其中展示了如何创建 If-Then-Else 表达式,以及如何根据对象的属性值计算对象的 HashCode。

以下是我们将在文中涵盖的场景列表:

  更改跟踪器 此示例将向您展示如何使用 Expression API 来监视任意对象以进行更改通知,或监视任意集合以进行更改通知。对象可能深埋在树状结构中。
  转换 此示例将向您展示如何使用 Expression.ConvertExpression.TypeAs API 来执行转换和强制类型转换操作。
  HashCompute 此示例将向您展示如何创建一个表达式,该表达式能够通过检查源输入对象的属性来生成动态哈希码。
  IfThenElse 此示例将向您展示如何使用 Expression.IfThenElse 表达式 API 创建条件表达式树逻辑。
  MethodCall 此示例将向您展示如何调用没有参数的对象方法,还将展示一个更复杂的示例,其中被调用的方法需要参数。
  PropertyGetSet 此示例将向您展示如何获取/设置对象的属性,还将展示一个更复杂的示例,其中我们展示了如何获取/设置对象属性层次结构中深处嵌套属性的值。
  WhereClause 此示例将向您展示如何构建一个动态的 WHERE 子句,该子句可以使用 Where(...) 扩展方法应用于 IEnumerable<T>


我们希望这些示例能够帮助您了解如何使用 Expression API 来完成您可能会遇到的更常见的编程任务。

 

演示代码

在本节中,您将找到所有示例。这些部分与实际的演示代码项目之间几乎存在一对一的映射关系。


更改跟踪器

这是所有示例中最复杂的一个,因为它实际提供的功能最多,但反过来,实际的 Expression API 使用并不像这里找到的其他一些示例那么复杂。我们选择从这个示例开始的原因是,我们真诚地认为它是一段有用的代码(前提是您对“德米特法则”持保留态度)。内容很多,请耐心阅读,这是一次(希望)值得的旅程。

我们认为开始学习这个示例的最佳方式是分点说明我们想要创建的内容,以下是我们在这篇文章开始之前 Ian 和我共同确定的原始列表:

  • 跟踪器应允许同时跟踪多个对象。
  • 跟踪器应 yield 返回一个 IObservable<bool> 用于 IsDirty 属性(我们最初打算使用 Rx),或者引发一个总体 IsDirty 事件供跟踪器用户使用。最终我们选择了一个简单的事件,因为我们不想给非 Rx 用户增加额外的 DLL 和额外的学习负担。
  • 如果跟踪的值被设置回其原始值,则跟踪器应被视为“未脏”。这显然只适用于简单属性,例如 Int, double, string 等。
  • 应有一种方法可以将整个跟踪器标记为“未脏”,以便更改跟踪能够有效地重置回其原始状态。
  • 应有一种更改跟踪器接口,允许通过提供一个可以用于提取属性的表达式来跟踪新对象的更改。请求跟踪的对象将指向一个通过使用 INotifyPropertyChanged 接口支持更改跟踪的对象。
  • 应有一种更改跟踪器接口,允许通过提供一个可以用于提取属性的表达式来跟踪列表对象(用于添加/删除)。请求跟踪的对象将指向一个通过使用 INotifyCollectionChanged 接口支持更改跟踪的对象。
  • 应有一种更改跟踪器接口,允许跟踪列表对象(用于更改)。为此提供的表达式不仅可以用于提取属性,还可以提供一个 Predicate<T>,在考虑跟踪列表项对跟踪器的总体脏状态做出贡献之前,必须满足该谓词。
这就是更改跟踪器的简要说明,基于此,Ian 和我为更改跟踪器设计了以下接口。
public class IsDiryChangedArgs : EventArgs
{
	/// <summary>
	/// Default constructor
	/// </summary>
	/// <param name="isDirty">the new value for is dirty</param>
	public IsDiryChangedArgs(bool isDirty)
	{
	IsDirty = isDirty;
	}

	/// <summary>
	/// The new is dirty value
	/// </summary>
	public bool IsDirty { get; private set; }
}




public interface ITracker : IDisposable
{
	/// <summary>
	/// Track all properties on the specified object
	/// </summary>
	/// <typeparam name="T"></typeparam>
	/// <param name="objectToTrack"></param>
	void TrackObject<T>(T objectToTrack);

	/// <summary>
	/// Track a specified property on an object or a child object
	/// </summary>
	/// <typeparam name="T"></typeparam>
	/// <typeparam name="TProp"></typeparam>
	/// <param name="objectToTrack"></param>
	/// <param name="trackingExpression"></param>
	void TrackObject<T, TProp>(T objectToTrack, Expression<Func<T, TProp>> trackingExpression);

	/// <summary>
	/// True if any of the objects being tracked is dirty
	/// </summary>
	bool IsDirty { get; }

	/// <summary>
	/// Mark all components in the tracker as clean
	/// </summary>
	void MarkAsClean();

	/// <summary>
	/// List of component trackers associated with this tracker
	/// </summary>
	/// <returns></returns>
	IEnumerable<IComponentTracker> ComponentTrackers();

	/// <summary>
	/// Event is raised when the IsDirty flag is changed
	/// </summary>
	event EventHandler<IsDiryChangedArgs> IsDirtyChanged;
}
我们认为学习如何使用它的最佳方法是查看测试,所以让我们从提供的测试中看一两个例子。

如果我们考虑以下测试文件及其关系:



点击图片放大

 

使用测试进行演示

然后检查一些测试用例,应该会更容易理解一些。

简单的测试用例

这些测试用例涵盖了如何为要跟踪的非列表类型属性添加跟踪器。从这些测试中,您应该可以看到如何跟踪一个对象,甚至是一个深埋在属性树中的对象。(但请记住,被跟踪的对象必须实现 INotifyPropertyChanged)。

[TestClass]
public class SimpleTrackingTests
{
	[TestMethod]
	public void TrackFirstLayer()
	{
		bool eventCalled = false;
		Child1 child1 = new Child1{ TestInt = 10};
		Tracker tracker = new Tracker();

		Assert.IsFalse(tracker.IsDirty);
		tracker.IsDirtyChanged += (sender, args) => eventCalled = true;

		tracker.TrackObject(child1, x => x.TestInt);
		Assert.IsFalse(tracker.IsDirty);

		child1.TestInt = 5;
		Assert.IsTrue(tracker.IsDirty);
		Assert.IsTrue(eventCalled);

		eventCalled = false;
		child1.TestInt = 10;
		Assert.IsFalse(tracker.IsDirty); 
		Assert.IsTrue(eventCalled);
	}

	[TestMethod]
	public void TrackSecondLayer()
	{
		bool eventCalled = false;
		ChildA childA = new ChildA
			{
	            Child1 = new Child1
	              { 
	                  TestInt = 10
	              }
            };


		Tracker tracker = new Tracker();
		tracker.IsDirtyChanged += (sender, args) => eventCalled = true;

		Assert.IsFalse(tracker.IsDirty);
		tracker.TrackObject(childA, x => x.Child1.TestInt);
		Assert.IsFalse(tracker.IsDirty);
		childA.Child1.TestInt = 5;
		Assert.IsTrue(tracker.IsDirty);
		Assert.IsTrue(eventCalled);

		eventCalled = false;
		childA.Child1.TestInt = 10;

		Assert.IsFalse(tracker.IsDirty); 
		Assert.IsTrue(eventCalled);
	}




	[TestMethod]
	public void TrackThirdLayer()
	{
		bool eventCalled = false;
		RootObject rootObject = 
			new RootObject
		    {
			   ChildA = new ChildA
			   {
				    Child1 = new Child1
				    {
				       TestInt = 5
				    }
			    }
		    };


		Tracker tracker = new Tracker();
		tracker.IsDirtyChanged += (sender, args) => eventCalled = true;
		Assert.IsFalse(tracker.IsDirty);

		tracker.TrackObject(rootObject,x => x.ChildA.Child1.TestInt);
		Assert.IsFalse(tracker.IsDirty);

		rootObject.ChildA.Child1.TestInt = 10;
		Assert.IsTrue(tracker.IsDirty);
		Assert.IsTrue(eventCalled);


		eventCalled = false;
		rootObject.ChildA.Child1.TestInt = 5;
		Assert.IsFalse(tracker.IsDirty);
		Assert.IsTrue(eventCalled);
	}

	[TestMethod]
	public void MarkAsClean()
	{
		bool eventCalled = false;
		ChildA childA = new ChildA
		{
			Child1 = new Child1 { TestInt = 10 }
		};

		Tracker tracker = new Tracker();
		tracker.IsDirtyChanged += (sender, args) => eventCalled = true;
		Assert.IsFalse(tracker.IsDirty);
		tracker.TrackObject(childA, x => x.Child1.TestInt);
		Assert.IsFalse(tracker.IsDirty);

		childA.Child1.TestInt = 5;
		Assert.IsTrue(tracker.IsDirty);
		Assert.IsTrue(eventCalled);

		tracker.MarkAsClean();
		Assert.IsFalse(tracker.IsDirty);
		eventCalled = false;
		childA.Child1.TestInt = 10;

		Assert.IsTrue(tracker.IsDirty);
		Assert.IsTrue(eventCalled);

		childA.Child1.TestInt = 5;

		Assert.IsFalse(tracker.IsDirty);
		Assert.IsTrue(eventCalled);
	}
}

列表测试用例

这些测试用例涵盖了如何为要跟踪的列表类型属性添加跟踪器。从这些测试中,您应该可以看到如何为以下场景跟踪列表:

  • 简单的添加/删除(被跟踪的项目必须实现 INotifyCollectionChanged
  • 跟踪一个满足特定 Predicate<T> 的列表
  • 跟踪嵌套列表
[TestClass]
public class ListTrackingTests
{
	[TestMethod]
	public void TrackAllTest()
	{
		ObservableCollection<Child1> newList = new ObservableCollection<Child1>
			{
				new Child1 { TestInt = 5 },
				new Child1 { TestInt = 10 }
			};

		Tracker tracker = new Tracker();
		tracker.TrackObject(newList);

		Assert.IsFalse(tracker.IsDirty);
		newList[0].TestInt = 10;

		Assert.IsTrue(tracker.IsDirty);
		newList[0].TestInt = 5;
		Assert.IsFalse(tracker.IsDirty);
	}




	[TestMethod]
	public void FilterListTest()
	{
		ObservableCollection<Child1> testList = new ObservableCollection<Child1>();
		ListObject listContainer = new ListObject
		{
			Child1List = testList
		};

		Tracker tracker = new Tracker();

		tracker.TrackObject(listContainer, 
			c => c.Child1List.TrackList(x => x.TestInt > 0));

		testList.Add(new Child1 { TestInt = 0, TestString = "Start" });
		Assert.IsFalse(tracker.IsDirty);

		testList.Add(new Child1 { TestInt = 1, TestString = "Start" });

		Assert.IsTrue(tracker.IsDirty);
	}




	[TestMethod]
	public void NestedFilterListTest()
	{
		ObservableCollection<Child1> testList = new ObservableCollection<Child1>();
		ListObject listContainer = new ListObject
			{
				Child1List = testList
			};

		Tracker tracker = new Tracker();
		tracker.TrackObject(listContainer, 
			c => c.Child1List.TrackList(x => x.TestInt > 0).TestString);

		testList.Add(new Child1 { TestInt = 0, TestString = "Start" });
		Assert.IsFalse(tracker.IsDirty);
		testList.Add(new Child1 { TestInt = 1, TestString = "Start" });
		Assert.IsTrue(tracker.IsDirty);
	}




	[TestMethod]
	public void NestedListTest()
	{
		ObservableCollection<Child1> testList = new ObservableCollection<Child1>();
		ListObject listContainer = new ListObject
		{
			Child1List = testList
		};

		testList.Add(new Child1 { TestString = "Start" });
		Tracker tracker = new Tracker();

		tracker.TrackObject(listContainer, 
			c => c.Child1List.TrackList().TestString); 
			
		Assert.IsFalse(tracker.IsDirty);
		testList[0].TestString = "Hello";

		Assert.IsTrue(tracker.IsDirty);

		testList[0].TestString = "Start";
		Assert.IsFalse(tracker.IsDirty);
	}
}

那么跟踪器是如何工作的呢?

很快就显而易见的一件事是,我们需要不同类型的跟踪器。所以我们设计了三种,可以在下图中看到:

其中 IComponentTracker 接口看起来是这样的:

/// <summary>
/// IComponentTracker is the interface all component trackers implements.
/// </summary>
public interface IComponentTracker : IDisposable
{
	/// <summary>
	/// The component being tracker
	/// </summary>
	object TrackedComponent { get; }

	/// <summary>
	/// True if the component or one of it's children is dirty
	/// </summary>
	bool IsDirty { get; }

	/// <summary>
	/// mark the component and all it's children as clean
	/// </summary>
	void MarkAsClean();

	/// <summary>
	/// A list of child trackers
	/// </summary>
	/// <returns></returns>
	IEnumerable<IComponentTracker> ChildTrackers();

	/// <summary>
	/// Event that is raised when the is dirty flag is changed
	/// </summary>
	event EventHandler<IsDiryChangedArgs> IsDirtyChanged;
}

我们不会详述这些类的每个方面,而是侧重于每个类最显著的要点。然而,在进入每种类型的组件跟踪器之前,让我们先看一下所有 3 种组件跟踪器共用的代码,以便构建 Expression 以获取要跟踪的属性的 getter。

以下是相关的代码,它实际上构建了针对对象属性名的查找委托:

/// <summary>
/// ComponentTrackerHelper is used create property accessor and new component trackers. 
/// </summary>
public class ComponentTrackerHelper
{
	private readonly Dictionary<string, Func<object, object>> accessMethods = new Dictionary<string, Func<object, object>>();

	/// <summary>
	/// Access a named property on an object
	/// </summary>
	/// <param name="target"></param>
	/// <param name="propertyName"></param>
	/// <returns></returns>
	public object AccessProperty(object target, string propertyName)
	{
		Func<object, object> accessDelegate = GetPropertyAccessor(target, propertyName);

		if (accessDelegate != null)
		{
			return accessDelegate(target);
		}
		throw new Exception(string.Format("Could not access property {0} on {1}", propertyName, target.GetType().FullName));
	}

	/// <summary>
	/// Creates a new property accessor function
	/// </summary>
	/// <param name="target"></param>
	/// <param name="propertyName"></param>
	/// <returns></returns>
	public Func<object, object> GetPropertyAccessor(object target, string propertyName)
	{
		string fullPropertyName = target.GetType().FullName + "|" + propertyName;
		Func<object, object> accessDelegate;

		if (!accessMethods.TryGetValue(fullPropertyName, out accessDelegate))
		{
			PropertyInfo propertyInfo = target.GetType().GetProperty(propertyName);
			// create the parameter for the instance parameter of type object
			ParameterExpression inParameter = Expression.Parameter(typeof(object), "objectParam");
			// cast the instance to it's real type
			Expression castExpression = Expression.Convert(inParameter, target.GetType());
			// create the property access expression
			Expression propertyAccessExpression =
				Expression.Property(castExpression, propertyInfo);
			// cast the property access expression to an object
			Expression returnCastExpression = Expression.Convert(propertyAccessExpression, typeof(object));
			accessDelegate = Expression.Lambda<Func<object, object>>(returnCastExpression, inParameter).Compile();
			accessMethods.Add(fullPropertyName, accessDelegate);
		}
		return accessDelegate;
	}

	/// <summary>
	/// Creates a new component tracker based on the provided TrackerInfo
	/// </summary>
	/// <param name="target"></param>
	/// <param name="path"></param>
	/// <returns></returns>
	public IComponentTracker CreateTracker(object target, TrackerInfo path)
	{
		switch (path.TrackerType)
		{
			case ComponentTrackerType.Node:
				return new NodeComponentTracker(this, target, path);
			case ComponentTrackerType.Leaf:
				return new LeafComponentTracker(this, target, path);
			case ComponentTrackerType.List:
				return new ListComponentTracker(this, target as IEnumerable, path);
		}
		throw new Exception("Unknown tracker type: " + path.TrackerType);
	}
}

现在来看不同类型的组件跟踪器。

LeafComponentTracker

  • 跟踪对象上的所有更改。
  • 对象必须实现 INotifyPropertyChanged
  • 我们将跟踪旧值和新值,这样我们就可以通过比较“原始”值和当前值来知道它是否真的脏了。这由 WeakReference 处理,以确保对象仍可被垃圾回收。
  • 如果恢复到原始值,还应支持“未脏”状态。

这是此类型跟踪器最相关的代码:

/// <summary>
/// Initialize the object, setting up property trackers for all relavent properties
/// </summary>
/// <param name="helper"></param>
/// <param name="objectToTrack"></param>
/// <param name="trackerInfo"></param>
private void Initialize(ComponentTrackerHelper helper, object objectToTrack, TrackerInfo trackerInfo)
{
	propertyTrackers = new Dictionary<string, PropertyTracker>();
	this.helper = helper;
	this.objectToTrack = objectToTrack;
	this.trackerInfo = trackerInfo;
	// listen to property change events
	if (objectToTrack is INotifyPropertyChanged)
	{
		((INotifyPropertyChanged)objectToTrack).PropertyChanged += OnPropertyChanged;
	}
	// loop through all public properties
	foreach (PropertyInfo propertyInfo in objectToTrack.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
	{
		// only monitor properties that are write able
		if (propertyInfo.CanWrite)
		{
			bool trackWeakly = !(propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof(string));
			// create a new property tracking object and a property accessor delegate
			PropertyTracker propertyTracker = new PropertyTracker(trackWeakly)
	
			// use the new property access delegate to retrieve the property from the object
			object origValue = propertyTracker.PropertyAccess(objectToTrack);

			// store the original value 
			propertyTracker.SetOriginalValue(origValue);

			// save the new property tracker into the dictionary by property name
			propertyTrackers[propertyInfo.Name] = propertyTracker;
		}
	}
}


/// <summary>
/// This is the property changed handler for the object being tracker
/// </summary>
/// <param name="sender"></param>
/// <param name="propertyChangedEventArgs"></param>
private void OnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
	PropertyTracker propertyTrackingInfo;
	// Get the property tracker associated with the property that changed
	if (propertyTrackers.TryGetValue(propertyChangedEventArgs.PropertyName, out propertyTrackingInfo))
	{
		bool dirtyChanged = false;
		bool hasOriginalValue;
		object originalValue = propertyTrackingInfo.GetOriginalValue(out hasOriginalValue);
		object newValue = propertyTrackingInfo.PropertyAccess(sender);

		if (newValue != null)
		{
			// Property is reverting back to original value so setting is dirty to false
			if (newValue.Equals(originalValue))
			{
				propertyTrackingInfo.IsDirty = false;
				dirtyChanged = true;
			}
			// else if the property is clean we need to dirty it up
			else if (!propertyTrackingInfo.IsDirty)
			{
				propertyTrackingInfo.IsDirty = true;
				dirtyChanged = true;
			}
		}
		else if (!hasOriginalValue)
		{
			// the original value was null and the new value is null so the property is now clean
			if (propertyTrackingInfo.IsDirty)
			{
				propertyTrackingInfo.IsDirty = false;
				dirtyChanged = true;
			}
		}
		else // the new value is null and we have an original value
		{
			// only set to true if the property is clean
			if (!propertyTrackingInfo.IsDirty)
			{
				propertyTrackingInfo.IsDirty = true;
				dirtyChanged = true;
			}
		}


		// only check for dirty properties if the property dirty flag changed
		if (dirtyChanged)
		{
			IsDirty = propertyTrackers.Values.Any(x => x.IsDirty);
		}
	}
}

 

ListComponentTracker

  • 跟踪 ObservableCollection(或任何其他实现 INotifyCollectionChanged 的集合)的更改,例如添加/删除/替换/重置。
  • 对象必须实现 INotifyCollectionChanged

这是最相关的代码:

/// <summary>
/// Initialize the tracker
/// </summary>
/// <param name="helper"></param>
/// <param name="objectToTrack"></param>
/// <param name="trackerInfo"></param>
private void Initialize(ComponentTrackerHelper helper, IEnumerable objectToTrack, TrackerInfo trackerInfo)
{
	componentTrackers = new Dictionary<object, IComponentTracker>();
	this.helper = helper;
	this.objectToTrack = objectToTrack;
	this.trackerInfo = trackerInfo;

	// add all children to be tracked
	AddChildren(objectToTrack);

	// if this list implements the collection changed event we need to listen to it
	if (objectToTrack is INotifyCollectionChanged)
	{
		((INotifyCollectionChanged)objectToTrack).CollectionChanged += OnCollectionChanged;
	}
}

/// <summary>
/// Handler for collection changed. It adds and removes trackers as the list changes
/// </summary>
/// <param name="sender"></param>
/// <param name="notifyCollectionChangedEventArgs"></param>
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
{
	bool fireChanged = false;
	bool changed = false;

	if (!IsDirty)
	{
		fireChanged = true;
	}

	switch (notifyCollectionChangedEventArgs.Action)
	{
		case NotifyCollectionChangedAction.Add:
			changed = AddChildren(notifyCollectionChangedEventArgs.NewItems);
			break;
		case NotifyCollectionChangedAction.Remove:
			changed = RemoveChildren(notifyCollectionChangedEventArgs.OldItems);
			break;
		case NotifyCollectionChangedAction.Replace:
			changed = RemoveChildren(notifyCollectionChangedEventArgs.OldItems);
			changed |= AddChildren(notifyCollectionChangedEventArgs.NewItems);
			break;
		case NotifyCollectionChangedAction.Reset:
			changed = RemoveChildren(componentTrackers.Values.ToArray());
			break;
	}

	if (changed)
	{
		isDirty = true;
	}

	if (isDirty && fireChanged && IsDirtyChanged != null)
	{
		IsDirtyChanged(this, new IsDiryChangedArgs(isDirty));
	}
}




/// <summary>
/// Handler for child tracker is dirty changing
/// </summary>
/// <param name="sender"></param>
/// <param name="eventArgs"></param>
private void ChildTrackerIsDirtyChanged(object sender, IsDiryChangedArgs eventArgs)
{
	// short circuit if we are already dirty
	if (isDirty)
	{
		return;
	}

	IComponentTracker childTracker = (IComponentTracker)sender;
	bool newValue = childTracker.IsDirty;
	bool fireChange = true;

	foreach (KeyValuePair<object, IComponentTracker> kvp in componentTrackers)
	{
		if (kvp.Key == sender)
		{
			continue;
		}

		if (kvp.Value.IsDirty != newValue)
		{
			fireChange = false;
			break;
		}
	}

	if (fireChange && IsDirtyChanged != null)
	{
		IsDirtyChanged(this, eventArgs);
	}
}


/// <summary>
/// Add children to be tracked
/// </summary>
/// <param name="childObjects"></param>
/// <returns></returns>
private bool AddChildren(IEnumerable childObjects)
{
	bool returnValue = false;
	IListComponentFilter listFilter = trackerInfo.ListFilter;

	// if we have child tracker info create a new tracker for the object
	if (trackerInfo.ChildTrackerInfo != null)
	{
		foreach (object child in childObjects)
		{
			// filter the object if a filter exists
			if (listFilter != null && !listFilter.FilterComponent(child))
			{
				continue;
			}

			IComponentTracker childTracker =
				helper.CreateTracker(child, trackerInfo.ChildTrackerInfo);
			childTracker.IsDirtyChanged += ChildTrackerIsDirtyChanged;
			componentTrackers.Add(child, childTracker);
			returnValue = true;
		}
	}
	else 
	{
		// Create a leaf tracker for each child in the list
		foreach (object child in childObjects)
		{
			if (listFilter != null && !listFilter.FilterComponent(child))
			{
				continue;
			}

			IComponentTracker childTracker =
				helper.CreateTracker(child, new TrackerInfo { TrackerType = ComponentTrackerType.Leaf });

			childTracker.IsDirtyChanged += ChildTrackerIsDirtyChanged;
			componentTrackers.Add(child, childTracker);
			returnValue = true;
		}
	}
	return returnValue;
}




/// <summary>
/// Remove child object that need to stop being tracked
/// </summary>
/// <param name="childObjects"></param>
/// <returns></returns>
private bool RemoveChildren(IEnumerable childObjects)
{
	bool returnValue = false;
	// foreach child object try and find a tracker and dispose it
	foreach (object child in childObjects)
	{
		IComponentTracker oldTracker;
		if (componentTrackers.TryGetValue(child, out oldTracker))
		{
			componentTrackers.Remove(child);
			oldTracker.IsDirtyChanged -= ChildTrackerIsDirtyChanged;
			oldTracker.Dispose();

			returnValue = true;
		}
	}




	return returnValue;
}

 

NodeComponentTracker

  • 跟踪对象上的特定属性。
    • 对象必须实现 INotifyCollectionChanged

以下是最相关的方法:

/// <summary>
/// Initialize the new tracker
/// </summary>
/// <param name="helper"></param>
/// <param name="objectToTrack"></param>
/// <param name="trackerInfo"></param>
private void Initialize(ComponentTrackerHelper helper, object objectToTrack, TrackerInfo trackerInfo)
{
	this.helper = helper;
	this.objectToTrack = objectToTrack;
	this.trackerInfo = trackerInfo;
	// calculate if we should track the property weakly
	bool trackWeakly = !(trackerInfo.PropertyType.IsValueType || trackerInfo.PropertyType == typeof(string));
	object currentValue = helper.AccessProperty(objectToTrack, trackerInfo.PropertyName);

	propertyTracker = new PropertyTracker(trackWeakly);
	propertyTracker.SetOriginalValue(currentValue);
	// if we have a current value and child tracker info start tracking
	if (currentValue != null && trackerInfo.ChildTrackerInfo != null)
	{
		childComponentTracker = helper.CreateTracker(currentValue, trackerInfo.ChildTrackerInfo);
		childComponentTracker.IsDirtyChanged += ChildComponentTrackerOnIsDirtyChanged;
	}

	INotifyPropertyChanged propertyChangeObject = objectToTrack as INotifyPropertyChanged;
	if (propertyChangeObject != null)
	{
		propertyChangeObject.PropertyChanged += PropertyChangedOnTrackedObject;
	}
}




/// <summary>
/// Handler for child component tracker changing is dirty
/// </summary>
/// <param name="sender"></param>
/// <param name="eventArgs"></param>
private void ChildComponentTrackerOnIsDirtyChanged(object sender, IsDiryChangedArgs eventArgs)
{
	IComponentTracker componentTracker = sender as IComponentTracker;
	if (!propertyTracker.IsDirty && IsDirtyChanged != null)
	{
		IsDirtyChanged(this, eventArgs);
	}
}

/// <summary>
/// The property change handler for the object being tracked
/// </summary>
/// <param name="sender"></param>
/// <param name="propertyChangedEventArgs"></param>
private void PropertyChangedOnTrackedObject(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
	if (childComponentTracker != null)
	{
		childComponentTracker.IsDirtyChanged -= ChildComponentTrackerOnIsDirtyChanged;
		childComponentTracker.Dispose();
		childComponentTracker = null;
	}

	bool hasOriginalValue;
	object originalValue = propertyTracker.GetOriginalValue(out hasOriginalValue);
	object newValue = helper.AccessProperty(objectToTrack, trackerInfo.PropertyName);

	if (newValue != null)
	{
		if (originalValue != null &&& newValue.Equals(originalValue))
		{
			IsDirty = false;
		}
		else
		{
			IsDirty = true;
		}
	}
	else
	{
		if (hasOriginalValue)
		{
			IsDirty = true;
		}
		else
		{
			IsDirty = false;
		}
	}

	// if there is a new value and we have a child tracker info we need to create a new tracker and listen to it
	if (newValue != null &&&& trackerInfo.ChildTrackerInfo != null)
	{
		childComponentTracker = helper.CreateTracker(newValue, trackerInfo.ChildTrackerInfo);
		childComponentTracker.IsDirtyChanged += ChildComponentTrackerOnIsDirtyChanged;
	}
}



现在我们知道这里有很多代码,可能很难理解,但请尝试玩一下单元测试,核心 API 其实非常简单,而且我们认为这个非常非常有用。

转换

Expression.Convert

这个简单的示例向您展示了如何使用 Cast(通过 Expression.Convert 实现),它接受一个对象并将其强制转换为 T 类型。

以下是相关代码

/// <summary>
/// Creates a new function that cast an object to a type T.
/// it will throw an exception if the cast is invalid
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static Func<object, T> CreateStaticCastStatement<T>()
{
	ParameterExpression inObject = Expression.Parameter(typeof(object));

	// static cast expression (T)inObject
	Expression convertExpression = Expression.Convert(inObject, typeof(T));
			
	return Expression.Lambda<Func<object,T>>(convertExpression,inObject).Compile();
}

我们可以这样使用它。如果转换调用失败,将抛出 Exception

Func<object,int> intStaticCastMethod = ConvertStatementCreater.CreateStaticCastStatement<int>();
int testValue = intStaticCastMethod(8);

Expression.TypeAs

我们也可以使用 Expression.TypeAs 在转换失败时返回 null,而不是像上一个示例那样抛出 Exception(对于无效转换)。

/// <summary>
/// Creates a new function that casts an object it will return null if the cast fails
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static Func<object, T> CreateAsCastStatement<T>()
{
	ParameterExpression inObject = Expression.Parameter(typeof(object));
			
	// cast using as statement   inObject as T
	Expression convertExpression = Expression.TypeAs(inObject, typeof(T));

	return Expression.Lambda<Func<object, T>>(convertExpression, inObject).Compile();
}

我们可以这样使用它:

Func<object, string> stringAsCastMethod = ConvertStatementCreater.CreateAsCastStatement<string>();

if (stringAsCastMethod("Hello") != "Hello")
{
	throw new Exception("Cast did not work");	
}

if (stringAsCastMethod(789) != null)
{
	throw new Exception("Should have been null");
}




哈希计算

在这个示例中,我们进行了一些有趣的尝试,展示了如何为任何对象创建一个通用的 GetHashCode 函数,这是通过检查传递给 GenericHashCalculator<T>T 对象类型的公共属性来实现的。对于每个属性值,都会设置一个新的 Expression.Property,然后进行 Expression.Call 调用以调用属性对象实例的 GetHashCode() 方法。结果将附加到一个持续进行的 StringBuilder 字符串连接中(其中单个属性 GetHashCode()int 值将传递给 StringBuilder.Append() 方法)。在迭代完所有公共属性后,将调用最终的 StringBuilder 字符串的 GetHashCode() 方法,这实际上就是该对象的总体哈希值。

所以,如果我们使用这个演示类:
public class Person
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public string EmailAddress { get; set; }
	public int Age { get; set; }
	public int NumberOfSiblings { get; set; }
	public int NumberOfChildren { get; set; }
}

我们可以构建一个单个属性的哈希值,该值将追加到持续进行的 StringBuilder 字符串值中(作为 int),这将用作最终的哈希值。

private Expression CreatePropertyHashExpression(
    PropertyInfo propertyInfo,
	ParameterExpression stringBuilderParameter,
	ParameterExpression objectToHashParameter)
{
	Expression returnExpression = null;
	Expression propertyExpression = Expression.Property(objectToHashParameter, propertyInfo);
	Expression propertyHashCodeExpression = Expression.Call(propertyExpression, getHashCodeMethod);
	Expression stringBuildAppendExpression = Expression.Call(
        stringBuilderParameter,
	stringBuilderAppendMethod,
	propertyHashCodeExpression);
			
    if (propertyInfo.PropertyType.IsValueType)
	{
	returnExpression = stringBuildAppendExpression;
	}
	else
	{
	Expression notEqualToNullExpress = Expression.NotEqual(propertyExpression, Expression.Constant(null));
	returnExpression = Expression.IfThen(notEqualToNullExpress, stringBuildAppendExpression);
	}
	return returnExpression;
}

这就是创建最终哈希值的部分,通过调用累加字符串值(来自用于创建单个属性部分哈希值的 StringBuilder)的 GetHashCode() 方法。

private void CreateHashDelegate()
{
	ParameterExpression objectToHashParameter = Expression.Parameter(typeof(T), "objectToHash");
	ParameterExpression stringBuilderParameter = Expression.Variable(typeof(StringBuilder), "stringBuilder");
	Expression assignAndCreateExpression = Expression.Assign(stringBuilderParameter,
		Expression.New(stringBuilderConstructor,
		Expression.Constant(typeof(T).FullName)));

	List<Expression> computeHashStatements = new List<Expression>();
	computeHashStatements.Add(assignAndCreateExpression);

	foreach (PropertyInfo propertyInfo in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public))
	{
		computeHashStatements.Add(
			CreatePropertyHashExpression(propertyInfo, 
                stringBuilderParameter, objectToHashParameter));
	}

	ParameterExpression tempStringVariable = Expression.Variable(typeof(string), "tempString");
	Expression toStringForBuilder = Expression.Call(stringBuilderParameter, stringBuildToStringMethod);
	Expression assignToStringToTempString = Expression.Assign(tempStringVariable, toStringForBuilder);
	Expression finalGetHashCodeFromString = Expression.Call(tempStringVariable, getHashCodeMethod);
	computeHashStatements.Add(assignToStringToTempString);
	computeHashStatements.Add(finalGetHashCodeFromString);
	BlockExpression finalBlock = BlockExpression.Block(typeof(int),
		new[] { stringBuilderParameter, tempStringVariable },
		computeHashStatements);

	hashDelegate = Expression.Lambda<Func<T, int>>(finalBlock, objectToHashParameter).Compile();
}

您可能这样使用这个 GenericHashCalculator<T> 类:

GenericHashCalculator<Person> hasher = new GenericHashCalculator<Person>();
Person testPerson1 = new Person();
Person testPerson2 = new Person();
int hashValue1 = hasher.Compute(testPerson1);
int hashValue2 = hasher.Compute(testPerson2);
testPerson1.FirstName = "Ian";
testPerson2.FirstName = "Ian";
int hashValue3 = hasher.Compute(testPerson1);
int hashValue4 = hasher.Compute(testPerson2);
if (hashValue1 != hashValue2)
{
	throw new Exception("Hashes need to match");
}
if (hashValue1 == hashValue3)
{
	throw new Exception("Hashes must not match");
}
if (hashValue3 != hashValue4)
{
	throw new Exception("Hashes need to match");
}
testPerson1.LastName = "Johnson";
testPerson2.LastName = "IsDementedExpressionMonster";
hashValue1 = hasher.Compute(testPerson1);
hashValue2 = hasher.Compute(testPerson2);
if (hashValue1 == hashValue2)
{
	throw new Exception("Hashes should not match");
}






If-Then-Else

在这个示例中,我们将研究如何使用 Expression 树来构建 If-Then-Else 流控制。

用伪代码来说,这就是我们试图做的:

.Block() {
  .If ($var1 > 0) {
    .Return returnLabel { "Positive" }
  } .Else {
     .If ($var1 < 0) {
         .Return returnLabel { "Negative" }
     } .Else {
         .Return returnLabel { "Zero" }
     }
  };
  .Label
      ""
  .LabelTarget returnLabel:

事实证明,确实有一个 Expression.IfThenElse API,所以将这段伪代码翻译成 Expression API 实际上并不难,这是相关的代码:

public static Func<int, string> CreatePositiveNegative()
{
	// Create the int parameter that is passed into the function
	ParameterExpression intParameter = Expression.Parameter(typeof(int));
	LabelTarget returnTarget = Expression.Label(typeof(string),"returnLabel");
	// create expression that tests the int parameter is > 0
	Expression testForPositive = Expression.GreaterThan(intParameter, Expression.Constant(0));
	// create expression that tests the int parameter is < 0
	Expression testForNegative = Expression.LessThan(intParameter, Expression.Constant(0));
	// Creates a statement that returns the string "Negative" if the int
	// parameter is less than 0 else it returns the string "Zero"
	Expression ifNegativeElseZeroExpression =
		Expression.IfThenElse(testForNegative, 
			Expression.Return(returnTarget, Expression.Constant(NegativeString)),
			Expression.Return(returnTarget, Expression.Constant(ZeroString)));
	// creates a statement that returns the string Positive if the parameter is 
	// greater than 0 else run the negative if/then/else expression
	Expression ifPositiveElseStatement =
		Expression.IfThenElse(testForPositive,
			Expression.Return(returnTarget, Expression.Constant(PositiveString)),
			ifNegativeElseZeroExpression);
	// creates a block that contains the if statement and a return label (GoTo statement)
	Expression expressionBody = Expression.Block(ifPositiveElseStatement,Expression.Label(returnTarget,Expression.Constant("")));

	return Expression.Lambda<Func<int, string>>(expressionBody, intParameter).Compile();
}

这是如何使用它的一个例子:

Func<int, string> intTestFunc = IfThenElseStatementCreater.CreatePositiveNegative();

if (intTestFunc(1) != IfThenElseStatementCreater.PositiveString)
{
	throw new Exception("Wrong value, should have been positive");
}

if (intTestFunc(-1) != IfThenElseStatementCreater.NegativeString)
{
	throw new Exception("Wrong value, should have been negative");
}

if (intTestFunc(0) != IfThenElseStatementCreater.ZeroString)
{
	throw new Exception("Wrong value, should have been zero");
}

当然,我们可以用更有意义的数据结构替换下面显示的常量字符串值。

public const string PositiveString = "Positive";
public const string NegativeString = "Negative";
public const string ZeroString = "Zero";




方法调用

方法是每个程序员日常编程中都必须调用的东西,所以可以想象,您可能偶尔需要创建一个委托来调用您想频繁调用的方法。回想一下,在本文开头,我说过您可以通过反射来完成很多事情,然而,使用反射会带来每次查找方法的开销,一个更好的选择是创建一个委托,我们可以在需要时使用它。好的,第一次将已编译的 Expression 树转换为委托(Func<TR>)仍然需要时间,但之后它应该和调用在编译时已知的委托一样快。

我们创建了两个示例,展示了如何构建方法调用委托 Expression 树,这两个示例都将使用这个对象来调用方法:

public class SomeBasicClass
{
    public string SomeMethod()
    {
       return "Hello World";
    }

    public string MoreComplexMethod(int param1, double param2, string param3)
    {
	return "Hello World " + param1 + " " + param2 + " " + param3;
    }
}

简单示例

简单情况相当直接,因为示例方法(SomeMethod)没有需要处理的参数,我们只需要获取 MethodInfo 并创建一个 Expression.Lambda 来调用正确的方法。以下是相关的代码:

protected void Initialize(MethodInfo methodInfo)
{
	Type declaringType = methodInfo.DeclaringType;

	// the input parameter to the function
	ParameterExpression inputObject = Expression.Parameter(typeof(object));

	// loval variable of type declaringType
	ParameterExpression tVariable = Expression.Variable(declaringType);

	// cast the input object to be declaring type
	Expression castExpression = Expression.Convert(inputObject, declaringType);

	// assign the cast value to the tVaraible variable
	Expression assignmentExpression = Expression.Assign(tVariable, castExpression);
			
	// call the specified method on tVariable
	Expression callExpression = Expression.Call(tVariable, methodInfo);

	// create a block statement that contains the body of the linq statement
	BlockExpression body = Expression.Block(new[] { tVariable }, assignmentExpression, callExpression);

	// create a new delegate that takes in an object and returns an object
	invokeMethod = Expression.Lambda<Func<object, object>>(body, inputObject).Compile();
}

复杂示例

复杂情况稍微棘手一些,因为我们需要处理为要调用的方法(MoreComplexMethod)创建一系列参数,因此代码量会多一些,但核心内容还是相似的,我们只是构建一个参数数组,这些参数将提供给最终的方法调用(Expression.Call)。

protected void Initialize(MethodInfo methodInfo)
{
	// set the number of parameters so we can do a sanity check when we call it.
	parameterCount = methodInfo.GetParameters().Count();

	Type declaringType = methodInfo.DeclaringType;

	// the parameter to call the method on
	ParameterExpression inputObject = Expression.Parameter(typeof(object), "inputObject");
	// the input parameters for the method
	ParameterExpression parameterArray = Expression.Parameter(typeof(object[]), "parameters");
	// loval variable of type declaringType
	ParameterExpression tVariable = Expression.Variable(declaringType);

	// keep a list of the variable we declare for use when we define the body
	List<ParameterExpression> variableList = new List<ParameterExpression> { tVariable };
	// cast the input object to be declaring type
	Expression castExpression = Expression.Convert(inputObject, declaringType);

	// assign the cast value to the tVaraible variable
	Expression assignmentExpression = Expression.Assign(tVariable, castExpression);

	List<Expression> bodyExpressions = new List<Expression> { assignmentExpression };

	Expression callExpression = null;
	if (parameterCount == 0)
	{
		// call the specified method on tVariable
		callExpression = Expression.Call(tVariable, methodInfo);
	}
	else // we need to build up the parameters
	{
		int parameterNum = 0;
		List<ParameterExpression> callArguements = new List<ParameterExpression>();
		// we need to convert each parameter in the array to match the method signature
		foreach (ParameterInfo parameterInfo in methodInfo.GetParameters())
		{
			// create new local variable of the same type as the paramete
			ParameterExpression newVariable = Expression.Variable(parameterInfo.ParameterType, "param" + parameterNum);
			// add it to the list of local variable we have
			callArguements.Add(newVariable);

			// create an expression to access the parameter array
			// using a constant indexer [parameterNum]
			Expression arrayAccess = Expression.ArrayAccess(parameterArray, Expression.Constant(parameterNum));
			// cast the array access expression to be the parameter type
			Expression castArrayValue = Expression.Convert(arrayAccess, parameterInfo.ParameterType);

			// assign the casted value to the local variable
			Expression variableAssign = Expression.Assign(newVariable, castArrayValue);
			// add the variable assignment expression to the list of expression that is the body
			bodyExpressions.Add(variableAssign);
			// move to the next parameter in the array
			parameterNum++;
		}
		// add all the new local variables to the variable list
		variableList.AddRange(callArguements);

		// call the method on tVaraible with the specified callArguement list
		callExpression = Expression.Call(tVariable, methodInfo, callArguements);
	}
	// add the call expression to the body
	bodyExpressions.Add(callExpression);

	// create a block statement that contains the body of the linq statement
	BlockExpression body = Expression.Block(variableList, bodyExpressions);




	// create a new delegate that takes in an object and object array and returns an object
	invokeMethod = Expression.Lambda<Func<object, object[], object>>(body, inputObject,parameterArray).Compile();
}




属性获取/设置

在对象上获取/设置属性值似乎是一个非常常见的需求。为了说明这一点,我们将通过一个简单的案例,然后再进入一个更复杂的案例。

简单示例

这是一个标准的属性获取/设置,我们将使用这个源对象:

public class BusinessObject
{
public ChildObjectA A { get; set; }
public int IntProp { get; set; }
public string StringProp { get; set; }
}

Getter

那么我们如何编写 getter 表达式呢?

/// <summary>
/// Creates a new Func that takes an instance object and returns the property value
/// 
/// (System.Object)((PropertyGetSetExample.BusinessObject)$objectParam).IntProp
/// </summary>
/// <param name="propertyInfo"></param>
/// <returns></returns>
private Func<object,object> CreateGetMethod(PropertyInfo propertyInfo)
{
	// create the parameter for the instance parameter of type object
	ParameterExpression inParameter = Expression.Parameter(typeof(object), "objectParam");


	// cast the instance to it's real type
	Expression castExpression = Expression.Convert(inParameter, objectType);


	// create the property access expression
	Expression propertyAccessExpression =
		Expression.Property(castExpression, propertyInfo);

	// cast the property access expression to an object
	Expression returnCastExpression = Expression.Convert(propertyAccessExpression, typeof(object));

	return Expression.Lambda<Func<object, object>>(returnCastExpression, inParameter).Compile();
}

我们可以像下面这样使用它。这大致相当于使用反射表达为:var x = (object)typeof(BusinessObject).GetProperty("IntProp").GetValue(bo, null);,但 Expression API 示例的区别在于,它将是一个已编译的 Func<object,object>,因此在再次使用时速度会非常快,而通过反射执行相同操作每次都会很慢。

BusinessObject bo = new BusinessObject { IntProp = 80 };
PropertyInfo propertyInfo = objectType.GetProperty("IntProp",BindingFlags.Public | BindingFlags.Instance);
var getMethod = CreateGetMethod(propertyInfo);
if ((int)getMethod(bo) != 80)
{
	throw new Exception("Value should have been 80");
}

Setter

那么setter呢,我们现在来看看吧。

/// <summary>
/// Creates a new Action that takes the instance object and the new value for the property and sets it
/// 
/// ((PropertyGetSetExample.BusinessObject)$objectParam).IntProp = (System.Int32)$newValue
/// </summary>
/// <param name="propertyInfo"></param>
/// <returns></returns>
private Action<object,object> CreateSetMethod(PropertyInfo propertyInfo)
{
	// define the first parameter to the action the object parameter
	ParameterExpression objectParameter = Expression.Parameter(typeof(object),"objectParam");




	// define the new value parameter for the action
	ParameterExpression newValueParameter = Expression.Parameter(typeof(object),"newValue");

	// cast the instance to it's proper type
	Expression castExpression = Expression.Convert(objectParameter, objectType);

	// cast the new value to be the correct property type
	Expression newValueCastExpression = Expression.Convert(newValueParameter, propertyInfo.PropertyType);

	// create a new property access expression and assign the cast new value to it
	Expression assignExpression = Expression.Assign(Expression.Property(castExpression, propertyInfo),
			                                        newValueCastExpression);

	return Expression.Lambda<Action<object, object>>(assignExpression, objectParameter, newValueParameter).Compile();
}

你可以这样使用它:

BusinessObject bo = new BusinessObject { IntProp = 80 };
PropertyInfo propertyInfo = objectType.GetProperty("IntProp",BindingFlags.Public | BindingFlags.Instance);
var setMethod = CreateSetMethod(propertyInfo);
setMethod(bo, 160);
if ((int)getMethod(bo) != 160)
{
	throw new Exception("Value should have been 160");
}

复杂示例

还有一个更复杂的示例,它使用这些演示对象,并允许我们深入对象的嵌套属性。

public class BusinessObject
{
public ChildObjectA A { get; set; }
public int IntProp { get; set; }
public string StringProp { get; set; }
}

public class ChildObjectA
{
public ChildObjectB B { get; set; }
}

public class ChildObjectB
{
public ChildObjectC C { get; set; }
}

public class ChildObjectC
{
public int IntProp { get; set; }
public string StringPro { get; set; }
}

毫无疑问,这个示例要复杂得多,因此我将只关注嵌套属性的 getter。大部分工作是找到一个 Expression,它可以将类似 "A.B.C.IntProp" 的东西转换成一个实际的 Expression 委托。可以想象,我们需要递归地检查由 "." 分隔的每个值,并创建一个代表我们试图构建的表达式属性树的那一部分的 Expression 部分。这部分是通过这段代码完成的:

// <summary>
/// Creates the linq expression the goes down the dotted property chain (A.B.C)
/// It will recursively work it's way down the chain
/// </summary>
/// <param name="valueParameter">the current object in the chain</param>
/// <param name="throwParameter">if thrown parameter</param>
/// <param name="objectType">object type</param>
/// <param name="propertyName">property name (a.b.c)</param>
/// <param name="createAction">function that will be called once the final property is reached. 
/// This the part that is different between get and set</param>
/// <returns></returns>
private static Expression CreateAccessPathCode(
        ParameterExpression valueParameter,
		ParameterExpression throwParameter,
		Type objectType,
		string propertyName,
		Func<Type, string, Expression, Expression> createAction)
{
	Expression returnExpression = null;
	int firstPeriod = propertyName.IndexOf('.');

	// there are more properties in the chain
	if (firstPeriod > 0)
	{
		string currentPropertyName = propertyName.Substring(0, firstPeriod);
		string theRest = propertyName.Substring(firstPeriod + 1);
		Type propertyOrFieldType = GetPropertyOrFieldType(objectType, currentPropertyName);
		// create local variable to hold the property we are about to inspect
		ParameterExpression newValue = Expression.Variable(propertyOrFieldType);

		// assign the property to the local variable
		Expression assignExpression = Expression.Assign(newValue,
			Expression.PropertyOrField(valueParameter, currentPropertyName));
	
		// recurse back up the chain passing the local variable in as the starting point
		// and theRest of the string for the property name
		Expression recurse =
			CreateAccessPathCode(newValue,throwParameter,
				propertyOrFieldType,
				theRest,
				createAction);

		// if the property type is not a value type we need to test it for null
		if (!propertyOrFieldType.GetTypeInfo().IsValueType)
		{
			// expression to create a new Exception object with the message provided
			Expression newExceptionExpression = Expression.New(
				exceptionConstructor,
				Expression.Constant(string.Format("Could not find property {1} on type {0}",
										objectType.FullName,
										currentPropertyName)));




			// create a statement that will throw a new exception if the throwParameter is true
			Expression throwIfMissingTrueExpression =
				Expression.IfThen(Expression.IsTrue(throwParameter),
					Expression.Throw(newExceptionExpression));

			// create if statement that tests the local variable for null
			Expression ifExpression =
				Expression.IfThenElse(
					Expression.NotEqual(newValue, Expression.Constant(null)),
						recurse, throwIfMissingTrueExpression);

			// create a new block containing the new local variable and if expression
			returnExpression =
				Expression.Block(new[] { newValue }, new[] { assignExpression, ifExpression });
		}
		else
		{
			// create a new block containing the recurse statement and local variable
			returnExpression =
				Expression.Block(new[] { newValue }, new[] { assignExpression, recurse });
		}
	}
	else
	{
		// execute create action and return it.
		returnExpression = createAction(objectType, propertyName, valueParameter);
	}
	return returnExpression;
}

下一步是实际的 getter。它看起来是这样的:

public static GetPropertyDelegate CreateGetPropertyDelegate(Type instanceType, string propertyName, Type indexType)
{
	// create input parameters for the method.
	ParameterExpression valueObjectParameter = Expression.Parameter(typeof(object), "valueObject");
	ParameterExpression indexParameter = Expression.Parameter(typeof(object), "index");
	ParameterExpression throwParameter = Expression.Parameter(typeof(bool), "throwIfMissing");

	// define a return variable at the base of 
	ParameterExpression returnValueExpression = Expression.Variable(typeof(object), "returnValue");

	// local variable that is the proper type
	ParameterExpression castValue = Expression.Variable(instanceType, "castValue");
	// assign the value object to the local castValue
	Expression castExpression =
		Expression.Assign(castValue, Expression.Convert(valueObjectParameter, instanceType));
	Expression accessBlock
		= CreateAccessPathCode(castValue,
			throwParameter,
			instanceType,
			propertyName,
			(type, name, expression) =>
			{
				Expression returnValue = null;
				if (indexType != null)
				{
					PropertyInfo propertyInfo = type.GetProperty(propertyName);
					if (propertyInfo != null)
					{
						// assign the property to the returnValue using the index
						returnValue = Expression.Assign(
							returnValueExpression,
							Expression.Convert(
								Expression.Property(expression,
								propertyInfo,
								Expression.Convert(indexParameter, indexType)),
								typeof(object)));
					}
					else
					{
						throw new Exception(
							string.Format("Could not find property {0} on type {1}",
								type.FullName,
								name));
					}
				}
				else
				{
				// assign the final property to the returnValue
					returnValue = Expression.Assign(
						returnValueExpression,
						Expression.Convert(
							Expression.PropertyOrField(expression, name), typeof(object)));
				}

				return returnValue;
			});

	// create body of method
	BlockExpression returnBlock =
		Expression.Block(new[] { castValue, returnValueExpression },
			castExpression,
			accessBlock,
			returnValueExpression);

	return Expression.Lambda<GetPropertyDelegate>(
		returnBlock, valueObjectParameter, indexParameter, throwParameter).
									Compile();
}

诚然,这段代码相当晦涩,但当你想到它在做什么时,它就相当酷了。它创建了一个委托,允许你针对对象的嵌套属性创建属性 getter 委托,如下所示:

BusinessObject bo = new BusinessObject
{
	A = new ChildObjectA
	{
		B = new ChildObjectB
		{
			C = new ChildObjectC { IntProp = 90 }
		}
	}
};var getValue =
	ComplexPropertyAccessor.CreateGetPropertyDelegate(typeof(BusinessObject), "A.B.C.IntProp", null);

//returnValue will be 90
var returnValue = getValue(bo, null, true);

可以看出,这段代码实际上调用了递归方法 CreateAccessPathCode,并传递了一个回调操作,该回调操作在递归结束时被调用,回调函数将接收最终对象 Type、最终属性名和最终值参数。然后,回调函数作为 Expression.Block 的一部分,该 Expression.Block 又作为最终输出,用于创建 LambdaExpression,最终返回给用户作为属性 getter 委托使用。





动态 Where

这个示例背后的想法很简单,也是一个非常常见的需求。我们有一个 IEnumerable<T>,其中 T 是某个对象,我们希望对其进行过滤。但是,我们希望动态构建过滤器。

假设 IEnumerable<T>Person 对象组成,所以我们有 IEnumerable<T>,其中 Person 的样子是这样的:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

如果我们不动态构建过滤器,我们可以简单地这样做:

var x = people.Where(x => x.FirstName="sam" && LastName.Contains("Bree"));

但是,如果我们想在运行时根据某些输入条件构建过滤器,例如让用户选择要在过滤器中使用的字段/方法,我们就需要创建一个 where 构建器。我们认为 where 部分可以考虑为一个单独的类,当我们构建整个动态 where 子句过滤器时,我们将组合多个 where 子句部分对象。

WhereClausePart 如下所示:

public enum CompareMethod
{
	Equal,
	GreaterThan,
	GreaterThanOrEqual,
	LessThan,
	LessThanOrEqual,
	Contains,
	StartsWith,
	EndsWith
}


/// <summary>
/// This class represents one part of a complex where statement
/// </summary>
public class WhereClausePart
{
	public WhereClausePart()
	{
			
	}

	public WhereClausePart(string propertyName, CompareMethod compareMethod, object compareValue)
	{
		PropertyName = propertyName;
		CompareMethod = compareMethod;
		CompareValue = compareValue;
	}

	public string PropertyName { get; set; }
	public object CompareValue { get; set; }
	public CompareMethod CompareMethod { get; set; }
}

然后,我们可以使用两个主要的辅助方法来构建一个复杂的动态 WHERE 过滤器:

  • CreateAndWhereClause:它有效地使用 ExpressionAPI Expression.AndAlso 将所有提供的 WhereClausePart 对象 AND 起来。
  • CreateOrWereClause:它有效地使用 ExpressionAPI Expression.OrElse 将所有提供的 WhereClausePart 对象 OR 起来。

以下代码的绝大部分都在做这件事。还有一些辅助方法可以根据 WhereClausePart 对象提供的比较类型来执行实际的比较。

public static class WhereClauseCreator
{
	private static readonly MethodInfo containsMethod;
	private static readonly MethodInfo endsWithMethod;
	private static readonly MethodInfo startsWithMethod;
		
	static WhereClauseCreator()
	{
		containsMethod = typeof(WhereClauseCreator).GetMethod("Contains",BindingFlags.NonPublic | BindingFlags.Static);
		endsWithMethod = typeof(WhereClauseCreator).GetMethod("EndsWith", BindingFlags.NonPublic | BindingFlags.Static);
		startsWithMethod = typeof(WhereClauseCreator).GetMethod("StartsWith", BindingFlags.NonPublic | BindingFlags.Static);
	}

	/// <summary>
	/// Creates a Func(T,bool) that can be used as part of a Where statement in Linq
	/// The comparission statements will be linked using an or operator
	/// </summary>
	/// <typeparam name="T"></typeparam>
	/// <param name="whereClause"></param>
	/// <returns></returns>
	public static Func<T, bool> CreateOrWhereClause<T>(IEnumerable<WhereClausePart> whereClause)
	{
		ParameterExpression inParameter = Expression.Parameter(typeof(T));
		Expression whereStatement = null;

		// foreach part of the where clause create the compare expression then or it with the current where statement
		foreach (WhereClausePart part in whereClause)
		{
			Expression propertyEqualStatement = CompareExpression<T>(inParameter, part);
			if (whereStatement == null)
			{
				whereStatement = propertyEqualStatement;
			}
			else
			{
				whereStatement = Expression.OrElse(whereStatement, propertyEqualStatement);
			}
		}

		if (whereStatement == null)
		{
			whereStatement = Expression.Constant(true);
		}
		return Expression.Lambda<Func<T, bool>>(whereStatement, inParameter).Compile();
	}

	public static Func<T, bool> CreateAndWhereClause<T>(IEnumerable<WhereClausePart> whereClause)
	{
		ParameterExpression inParameter = Expression.Parameter(typeof(T));
		Expression whereStatement = null;




		foreach (WhereClausePart part in whereClause)
		{
			Expression propertyEqualStatement = CompareExpression<T>(inParameter, part);
			if (whereStatement == null)
			{
				whereStatement = propertyEqualStatement;
			}
			else
			{
				whereStatement = Expression.AndAlso(whereStatement, propertyEqualStatement);
			}
		}
		if (whereStatement == null)
		{
			whereStatement = Expression.Constant(true);
		}
		return Expression.Lambda<Func<T, bool>>(whereStatement, inParameter).Compile();
	}

	/// <summary>
	/// Creates an expression where it compares a property to a particular value
	/// </summary>
	/// <typeparam name="T"></typeparam>
	/// <param name="inParameter"></param>
	/// <param name="part"></param>
	/// <returns></returns>
	private static Expression CompareExpression<T>(ParameterExpression inParameter, WhereClausePart part)
	{
		PropertyInfo propertyInfo = typeof(T).GetProperty(part.PropertyName);
		// based on the comparission method we change the Expression we return back
		switch (part.CompareMethod)
		{
			case CompareMethod.Equal:
				return Expression.Equal(Expression.Property(inParameter, propertyInfo), 
					Expression.Constant(part.CompareValue));
			case CompareMethod.GreaterThan:
				return Expression.GreaterThan(Expression.Property(inParameter, propertyInfo), 
					Expression.Constant(part.CompareValue));
			case CompareMethod.GreaterThanOrEqual: 
				return Expression.GreaterThanOrEqual(Expression.Property(inParameter, propertyInfo), 
					Expression.Constant(part.CompareValue));
			case CompareMethod.LessThan:
				return Expression.LessThan(Expression.Property(inParameter, propertyInfo), 
					Expression.Constant(part.CompareValue));
			case CompareMethod.LessThanOrEqual:
				return Expression.LessThanOrEqual(Expression.Property(inParameter, propertyInfo), 
					Expression.Constant(part.CompareValue));
			case CompareMethod.Contains:
				return Expression.Call(containsMethod,
					Expression.Property(inParameter, propertyInfo),
					Expression.Constant(part.CompareValue));
			case CompareMethod.StartsWith:
				return Expression.Call(startsWithMethod,
				Expression.Property(inParameter, propertyInfo),
					Expression.Constant(part.CompareValue));
			case CompareMethod.EndsWith:
				return Expression.Call(endsWithMethod,
					Expression.Property(inParameter, propertyInfo),
					Expression.Constant(part.CompareValue));

		}
		return null;
	}

	/// <summary>
	/// I've created a wrapper around Contains so that I don't need to check for null in the linq expression 
	/// </summary>
	/// <param name="testString"></param>
	/// <param name="containsValue"></param>
	/// <returns></returns>
	private static bool Contains(string testString, string containsValue)
	{
		if (testString != null)
		{
			return testString.Contains(containsValue);
		}
		return false;
	}


	/// <summary>
	/// I've created a wrapper around EndsWith so that I don't need to check for null in the linq expression 
	/// </summary>
	/// <param name="testString"></param>
	/// <param name="endsWithValue"></param>
	/// <returns></returns>
	private static bool EndsWith(string testString, string endsWithValue)
	{
		if (testString != null)
		{
			return testString.EndsWith(endsWithValue);
		}
		return false;
	}

	/// <summary>
	/// I've created a wrapper around StartsWith so that I don't need to check for null in the linq expression 
	/// </summary>
	/// <param name="testString"></param>
	/// <param name="startsWithValue"></param>
	/// <returns></returns>
	private static bool StartsWith(string testString, string startsWithValue)
	{
		if (testString != null)
		{
			return testString.StartsWith(startsWithValue);
		}

		return false;
	}
}


实际使用可能看起来像这样:

List<Person> persons = CreatePersonList();
Func<Person, bool> clause =
	WhereClauseCreator.CreateAndWhereClause<Person>(new[]
		{
			new WhereClausePart("LastName",CompareMethod.Equal, "Doe"),
 			new WhereClausePart("Age",CompareMethod.GreaterThan, 20) 
		});

if (persons.Where(clause).Count() != 4)
{
	throw new Exception("There should be only 4.");
}

Func<Person, bool> orClause =
	WhereClauseCreator.CreateOrWhereClause<Person>(new[]
		{
			new WhereClausePart("LastName",CompareMethod.Equal, "Johnson"),
 			new WhereClausePart("FirstName",CompareMethod.StartsWith, "J") 
		});

if (persons.Where(orClause).Count() != 8)
{
	throw new Exception("Result should be 8.");
}




 

 

就这些

总之,希望您喜欢这篇。我们都觉得这种 Expression API 食谱式的方法是学习 Expression API 的一种相当有用的方式。我个人赞赏 Ian 的 Expression 技能,它们远远胜过我,我们都希望这篇文章对您中的一些人有所帮助。我知道我在学习 Ian 的示例时也学到了很多东西。

一如既往,如果您喜欢这篇文章,请随时发表评论/投票。

 

© . All rights reserved.