The problem with Monitor.Enter is that it will block indefinitely if the monitor is unavailable. I avoid writing production code that calls APIs which might block forever, because if you hit a deadlock scenario, you'll never recover. And even though you will normally want to design your code in such a way as to avoid deadlock, it's still best to be able to at least detect it if it should occur. (Ideally you want to be robust enough to recover from it too.)
The solution to this is to use Monitor.TryEnter instead - this allows you to pass in a timeout, specifying how long you're prepared to wait. The right timeout will depend on your application, but you will normally be able to pick a timeout which, if exceeded, is clear evidence of deadlock. Most of the time I use something on the order of 10 seconds - I normally try to hold locks for no more than a few statements, so if one has been unavailable for 10 seconds, then things are clearly very badly wrong.

using System;
using System.Threading;

// Thanks to Eric Gunnerson for recommending this be a struct rather
// than a class - avoids a heap allocation.
//
// (In Debug mode, we make it a class so that we can add a finalizer
// in order to detect when the object is not freed.)
//
// Thanks to Chance Gillespie and Jocelyn Coulmance for pointing out
// the bugs that then crept in when I changed it to use struct...
//
// http://www.interact-sw.co.uk/iangblog/2004/03/23/locking

#if DEBUG
public class TimedLock : IDisposable
#else
public struct TimedLock : IDisposable
#endif
{
    public static TimedLock Lock (object o)
    {
        return Lock (o, TimeSpan.FromSeconds (10));
    }

    public static TimedLock Lock (object o, TimeSpan timeout)
    {
        TimedLock tl = new TimedLock (o);
        if (!Monitor.TryEnter (o, timeout))
        {
#if DEBUG
            System.GC.SuppressFinalize(tl);
#endif
            throw new LockTimeoutException ();
        }

        return tl;
    }

    private object target;
    private TimedLock (object o)
    {
        target = o;
    }

    public void Dispose()
    {
        Monitor.Exit (target);

        // It's a bad error if someone forgets to call Dispose,
        // so in Debug builds, we put a finalizer in to detect
        // the error. If Dispose is called, we suppress the
        // finalizer.
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

#if DEBUG
    ~TimedLock()
    {
        // If this finalizer runs, someone somewhere failed to
        // call Dispose, which means we've failed to leave
        // a monitor!
        System.Diagnostics.Debug.Fail("Undisposed lock");
    }
#endif
}

public class LockTimeoutException : Exception
{
    public LockTimeoutException () : base("Timeout waiting for lock")
    {
    }
}

// Usage:
// ==========================================================================
// The TimedLock.Lock method can be called to lock an object. It returns an
// object that implements IDisposable. (As it happens, this is an instance of
// the TimedLock class, but it doesn't really matter what it is - a private
// nested class could also have been used.) Now we can write code like this:
using (TimedLock.Lock(obj))
{
    // ...
}