I have a RecursionChecker class for this sort of thing. I hereby disclaim copyright on the code below.
It complains if it finds itself hitting the check for the target object too often. It's not a be-all-end-all; loops can cause false positives, for example. One could avoid that by having another call after the risky code, telling the checker that it can decrement its recurse call for the target object. It still would not be bulletproof.
To use it, I just call
public void DangerousMethod() {
RecursionChecker.Check(someTargetObjectThatWillBeTheSameIfWeReturnHereViaRecursion);
// recursion-risky code here.
}
Here is the RecursionChecker class:
/// <summary>If you use this class frequently from multiple threads, expect a lot of blocking. In that case,
/// might want to make this a non-static class and have an instance per thread.</summary>
public static class RecursionChecker
{
#if DEBUG
private static HashSet<ReentrancyInfo> ReentrancyNotes = new HashSet<ReentrancyInfo>();
private static object LockObject { get; set; } = new object();
private static void CleanUp(HashSet<ReentrancyInfo> notes) {
List<ReentrancyInfo> deadOrStale = notes.Where(info => info.IsDeadOrStale()).ToList();
foreach (ReentrancyInfo killMe in deadOrStale) {
notes.Remove(killMe);
}
}
#endif
public static void Check(object target, int maxOK = 10, int staleMilliseconds = 1000)
{
#if DEBUG
lock (LockObject) {
HashSet<ReentrancyInfo> notes = RecursionChecker.ReentrancyNotes;
foreach (ReentrancyInfo note in notes) {
if (note.HandlePotentiallyRentrantCall(target, maxOK)) {
break;
}
}
ReentrancyInfo newNote = new ReentrancyInfo(target, staleMilliseconds);
newNote.HandlePotentiallyRentrantCall(target, maxOK);
RecursionChecker.CleanUp(notes);
notes.Add(newNote);
}
#endif
}
}
helper classes below:
internal class ReentrancyInfo
{
public WeakReference<object> ReentrantObject { get; set;}
public object GetReentrantObject() {
return this.ReentrantObject?.TryGetTarget();
}
public DateTime LastCall { get; set;}
public int StaleMilliseconds { get; set;}
public int ReentrancyCount { get; set;}
public bool IsDeadOrStale() {
bool r = false;
if (this.LastCall.MillisecondsBeforeNow() > this.StaleMilliseconds) {
r = true;
} else if (this.GetReentrantObject() == null) {
r = true;
}
return r;
}
public ReentrancyInfo(object reentrantObject, int staleMilliseconds = 1000)
{
this.ReentrantObject = new WeakReference<object>(reentrantObject);
this.StaleMilliseconds = staleMilliseconds;
this.LastCall = DateTime.Now;
}
public bool HandlePotentiallyRentrantCall(object target, int maxOK) {
bool r = false;
object myTarget = this.GetReentrantObject();
if (target.DoesEqual(myTarget)) {
DateTime last = this.LastCall;
int ms = last.MillisecondsBeforeNow();
if (ms > this.StaleMilliseconds) {
this.ReentrancyCount = 1;
}
else {
if (this.ReentrancyCount == maxOK) {
throw new Exception("Probable infinite recursion");
}
this.ReentrancyCount++;
}
}
this.LastCall = DateTime.Now;
return r;
}
}
public static class DateTimeAdditions
{
public static int MillisecondsBeforeNow(this DateTime time) {
DateTime now = DateTime.Now;
TimeSpan elapsed = now.Subtract(time);
int r;
double totalMS = elapsed.TotalMilliseconds;
if (totalMS > int.MaxValue) {
r = int.MaxValue;
} else {
r = (int)totalMS;
}
return r;
}
}
public static class WeakReferenceAdditions {
/// <summary> returns null if target is not available. </summary>
public static TTarget TryGetTarget<TTarget> (this WeakReference<TTarget> reference) where TTarget: class
{
TTarget r = null;
if (reference != null) {
reference.TryGetTarget(out r);
}
return r;
}
}