3
\$\begingroup\$

I have written a class library for creating timer-based bacground operations in .NET projects. The idea is to be able to create and manage (start/stop/resume/cancel) recurring background operations and also to be able to poll and/or react to changes in the background operation's state.

The library consists mainly of two things:

RecurringOperation model/class

  • Encapsulates and represents a single action/method which is executed in specified intervals.
  • Represents the state of the recurring operation, providing properties and events to bind to.
  • Supports XAML and MVVM pattern to bind UI controls to the state of the recurring operation.

RecurringOperationManager singleton class

  • Responsible for managing all RecurringOperation objects inside an application (starting/stopping/resuming).
  • Responsible for executing recurring operations in a thread safe way and makes sure that a single recurring operation does not "queue up" if the execution of a recurring operation takes longer than the specified interval for that operation.

I'd like your opinion on

  • The overall code quality and readability (any glaring bugs or mistakes)
  • The architecture and implemenation of the RecurringOperation and RecurringOperationManager classes

In order to get the general idea about the usage of the library, please take a look at the readme on the github page at https://github.com/TDMR87/Recurop

To see the library being used, take a look at this github page. The project creates a stopwatch program in WPF using this library. https://github.com/TDMR87/RecuropDemo


The implementation of the RecurringOperation class looks like this:

public class RecurringOperation : INotifyPropertyChanged
    {
        public RecurringOperation(string name)
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new InvalidOperationException(Constants.UnnamedOperationException);

            _name = name;
            IsExecuting = false;
            IsRecurring = false;
            IsNotRecurring = true;
            IsPaused = false;
            IsIdle = true;
            IsCancelled = false;
            IsNotCancelled = true;
            CallbackLock = new object();
            Status = RecurringOperationStatus.Idle;
            CanBeStarted = true;
        }
        private readonly string _name;

        /// <summary>
        /// Indicates whether the recurring operation can be started.
        /// </summary>
        public bool CanBeStarted
        {
            get => canBeStarted;
            internal set
            {
                canBeStarted = value;
                OnPropertyChanged();
            }
        }
        private bool canBeStarted;

        /// <summary>
        /// Indicates whether the recurring background operation is 
        /// currently executing it's specified action.
        /// </summary>
        public bool IsExecuting
        {
            get => isExecuting;
            internal set
            {
                isExecuting = value;
                OnPropertyChanged();
            }
        }
        private bool isExecuting;

        /// <summary>
        /// Indicates whether the recurring background operation is 
        /// currently in recurring state (and not cancelled, for example).
        /// </summary>
        public bool IsRecurring
        {
            get => isRecurring;
            internal set
            {
                isRecurring = value;
                OnPropertyChanged();
            }
        }
        private bool isRecurring;

        /// <summary>
        /// Indicates whether the recurring background operation is 
        /// currently in recurring state.
        /// </summary>
        public bool IsNotRecurring
        {
            get => isNotRecurring;
            internal set
            {
                isNotRecurring = value;
                OnPropertyChanged();
            }
        }
        private bool isNotRecurring;

        /// <summary>
        /// Indicates whether the recurring background operation is 
        /// currently in cancelled state.
        /// </summary>
        public bool IsCancelled
        {
            get => isCancelled;
            internal set
            {
                isCancelled = value;
                OnPropertyChanged();
            }
        }
        private bool isCancelled;

        /// <summary>
        /// Indicates whether the recurring background operation is 
        /// currently not in cancelled state.
        /// </summary>
        public bool IsNotCancelled
        {
            get => isNotCancelled;
            internal set
            {
                isNotCancelled = value;
                OnPropertyChanged();
            }
        }
        private bool isNotCancelled;

        /// <summary>
        /// Indicates whether the recurring operation is currently paused.
        /// </summary>
        public bool IsPaused
        {
            get => isPaused;
            internal set
            {
                isPaused = value;
                OnPropertyChanged();
            }
        }
        private bool isPaused;

        /// <summary>
        /// Indicates whether the recurring operation is idle. An idle state means that 
        /// the operation is not yet started or it has been aborted.
        /// </summary>
        public bool IsIdle
        {
            get => isIdle;
            set
            {
                isIdle = value;
                OnPropertyChanged();
            }
        }
        private bool isIdle;

        /// <summary>
        /// The start time of the latest background operation execution.
        /// </summary>
        public DateTime LastRunStart
        {
            get => lastRunStart;
            internal set
            {
                lastRunStart = value;
                OnPropertyChanged();
            }
        }
        private DateTime lastRunStart;

        /// <summary>
        /// The start time of the latest background operation execution.
        /// </summary>
        public DateTime LastRunFinish
        {
            get => lastRunFinish;
            internal set
            {
                lastRunFinish = value;
                OnPropertyChanged();
            }
        }
        private DateTime lastRunFinish;

        public RecurringOperationStatus Status
        {
            get => status;
            internal set
            {
                status = value;
                OnStatusChanged();
                OnPropertyChanged();
            }
        }
        private RecurringOperationStatus status;

        /// <summary>
        /// A lock object used to prevent overlapping threads from modifying
        /// the properties of the Background Operation object. Use with lock-statement.
        /// </summary>
        internal object CallbackLock { get; }

        /// <summary>
        /// When the Background Operation's action delegate throws an exception,
        /// the exception is accessible through this property. The next exception
        /// in the action delegate will override any previous value in this property.
        /// </summary>
        public Exception Exception
        {
            get => exception;
            internal set
            {
                exception = value;
                OnOperationFaulted();
            }
        }
        private Exception exception;

        /// <summary>
        /// The event handler is triggered whenever the status of
        /// the background operation changes.
        /// </summary>
        public event Action StatusChanged;

        /// <summary>
        /// The event handler is triggered when the
        /// Background Operation's action delegate throws an exception.
        /// </summary>
        public event Action<Exception> OperationFaulted;

        /// <summary>
        /// The event handler is triggered when properties of the Background Operation
        /// change value.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }

        protected void OnStatusChanged()
        {
            StatusChanged?.Invoke();
        }

        protected void OnOperationFaulted()
        {
            OperationFaulted?.Invoke(Exception);
        }

        /// <summary>
        /// Returns the identifying name of this background operation.
        /// </summary>
        /// <returns></returns>
        public string GetName()
        {
            if (string.IsNullOrWhiteSpace(_name))
            {
                throw new InvalidOperationException(Constants.UninitializedOperationException);
            }

            return _name;
        }

        /// <summary>
        /// Returns the identifying name of this background operation.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            if (string.IsNullOrWhiteSpace(_name))
            {
                throw new InvalidOperationException(Constants.UninitializedOperationException);
            }

            return _name;
        }
    }

And the RecurringOperationManager class looks like this:

public sealed class RecurringOperations
    {
        private static RecurringOperations instance = null;
        private static readonly object instanceLock = new object();
        private readonly List<Operation> recurringOperations;

        /// <summary>
        /// Private singleton constructor.
        /// </summary>
        private RecurringOperations()
        {
            recurringOperations = new List<Operation>();
        }

        /// <summary>
        /// Public singleton instance.
        /// </summary>
        public static RecurringOperations Manager
        {
            get
            {
                lock (instanceLock)
                {
                    if (instance == null) instance = new RecurringOperations();
                    return instance;
                }
            }
        }

        /// <summary>
        /// Resumes the execution of the specified recurring operation 
        /// if currently in Paused state. Resuming a cancelled operation
        /// throws an exception.
        /// </summary>
        /// <param name="operation"></param>
        /// <param name="interval"></param>
        /// <param name="action"></param>
        /// <param name="startImmediately"></param>
        public void ResumeRecurring(RecurringOperation operation, bool startImmediately = false)
        {
            // If an uninitialized background operation object was given as an argument
            if (operation == null || string.IsNullOrWhiteSpace(operation.GetName()))
                throw new InvalidOperationException(Constants.UninitializedOperationException);

            // If cancelled
            if (operation.Status == RecurringOperationStatus.Cancelled)
            {
                var exception = new InvalidOperationException(Constants.CancelledOperationException);

                operation.Exception = exception;

                throw exception;
            }

            // Search the Manager's collection of operations for an operation
            var recurringOperation =
                recurringOperations.Find(op => op.Name.Equals(operation.GetName()));

            // If the operation was found and is not recurring
            if (recurringOperation != null)
            {
                // Set background operation status
                operation.Status = RecurringOperationStatus.Idle;
                operation.IsIdle = true;
                operation.IsRecurring = true;
                operation.IsNotRecurring = false;
                operation.IsPaused = false;
                operation.IsExecuting = false;

                // Re-start the operation
                recurringOperation.Timer.Change(
                    startImmediately ? TimeSpan.Zero : recurringOperation.Interval, recurringOperation.Interval);
            }
        }

        /// <summary>
        /// Creates and/or starts a recurring operation.
        /// If an operation has already been started,
        /// an exception will be thrown.
        /// </summary>
        /// <param name="backgroundOperation"></param>
        /// <param name="interval"></param>
        /// <param name="action"></param>
        /// <param name="startImmediately"></param>
        public void StartRecurring(RecurringOperation backgroundOperation, TimeSpan interval, Action action, bool startImmediately = false)
        {
            // If an uninitialized background operation object was given as an argument
            if (backgroundOperation == null || string.IsNullOrWhiteSpace(backgroundOperation.GetName()))
                throw new InvalidOperationException(Constants.UninitializedOperationException);

            // Search the Manager's collection of operations for an operation
            // that corresponds with the background operation
            var recurringOperation =
                recurringOperations.Find(op => op.Name.Equals(backgroundOperation.GetName()));

            // If operation has already been registered
            if (recurringOperation != null)
            {
                throw new InvalidOperationException($"{Constants.RecurringOperationException}. " +
                                                    $"Operation name '{recurringOperation.Name}'.");
            }

            // Create a new recurring operation object
            var operation = new Operation
            {
                Name = backgroundOperation.GetName(),
                Interval = interval
            };

            // This is a local function here..
            // ..Create a callback function for the timer
            void TimerCallback(object state)
            {
                // To indicate if a thread has entered 
                // a block of code.
                bool lockAcquired = false;

                try
                {
                    // Try to acquire a lock.
                    // Sets the value of the lockAcquired, even if the method throws an exception, 
                    // so the value of the variable is a reliable way to test whether the lock has to be released.
                    Monitor.TryEnter(backgroundOperation.CallbackLock, ref lockAcquired);

                    // If lock acquired
                    if (lockAcquired)
                    {
                        // Set background operation status
                        backgroundOperation.LastRunStart = DateTime.Now;
                        backgroundOperation.IsExecuting = true;
                        backgroundOperation.IsIdle = false;
                        backgroundOperation.Status = RecurringOperationStatus.Executing;

                        try
                        {
                            action();
                        }
                        catch (Exception ex)
                        {
                            // Set reference to the catched exception in the background operations exception property
                            backgroundOperation.Exception = ex;
                        }
                        finally
                        {
                            // Set last run finish time
                            backgroundOperation.LastRunFinish = DateTime.Now;

                            // Set background operation state
                            backgroundOperation.IsExecuting = false;
                            backgroundOperation.IsIdle = true;

                            // If not cancelled
                            if (backgroundOperation.Status != RecurringOperationStatus.Cancelled)
                            {
                                // Set status to idle
                                backgroundOperation.Status = RecurringOperationStatus.Idle;
                            }
                        }
                    }
                }
                finally
                {
                    // If a thread acquired the lock
                    if (lockAcquired)
                    {
                        // Release the lock
                        Monitor.Exit(backgroundOperation.CallbackLock);
                    }
                }
            } // End local funtion

            // Create a timer that calls the action in specified intervals
            // and save the timer in the recurring operations Timer property
            operation.Timer = new Timer(
                new TimerCallback(TimerCallback), null, startImmediately ? TimeSpan.Zero : interval, interval);

            // Add the recurring operation to the Manager's collection
            recurringOperations.Add(operation);

            // Set status of the client-side background operation
            backgroundOperation.Status = RecurringOperationStatus.Idle;
            backgroundOperation.IsRecurring = true;
            backgroundOperation.IsNotRecurring = false;
            backgroundOperation.IsPaused = false;
            backgroundOperation.IsIdle = false;
            backgroundOperation.CanBeStarted = false;
        }

        /// <summary>
        /// Pauses the recurring execution of the operation.
        /// Execution can be continued with a call to ResumeRecurring().
        /// </summary>
        /// <param name="backgroundOperation"></param>
        public void PauseRecurring(RecurringOperation backgroundOperation)
        {
            // Search the Manager's collection of operations for an operation
            // that corresponds with the background operation
            var privateOperation =
                recurringOperations.Find(
                    operation => operation.Name.Equals(backgroundOperation.GetName()));

            // If found
            if (privateOperation != null)
            {
                // Pause the timer
                privateOperation.Timer.Change(Timeout.Infinite, Timeout.Infinite);

                // Set the status of the client-side background operation
                backgroundOperation.Status = RecurringOperationStatus.Idle;
                backgroundOperation.IsRecurring = false;
                backgroundOperation.IsNotRecurring = true;
                backgroundOperation.IsPaused = true;
                backgroundOperation.IsIdle = true;
                backgroundOperation.IsExecuting = false;
            }
        }

        /// <summary>
        /// Cancels the specified recurring operation. 
        /// Cancelled operations cannot be resumed, they must be restarted
        /// with a call to StartRecurring.
        /// </summary>
        /// <param name="backgroundOperation"></param>
        public void Cancel(RecurringOperation backgroundOperation)
        {
            // Search the Manager's collection of operations for an operation
            // that corresponds with the background operation
            var privateOperation =
                recurringOperations.Find(
                    operation => operation.Name.Equals(backgroundOperation.GetName()));

            // If found
            if (privateOperation != null)
            {
                // Dispose the timer and the operation object
                privateOperation.Timer.Dispose();
                recurringOperations.Remove(privateOperation);

                // Set the status of the client-side background operation
                backgroundOperation.Status = RecurringOperationStatus.Cancelled;
                backgroundOperation.IsRecurring = false;
                backgroundOperation.IsNotRecurring = true;
                backgroundOperation.IsPaused = false;
                backgroundOperation.IsExecuting = false;
                backgroundOperation.IsIdle = true;
                backgroundOperation.IsCancelled = true;
                backgroundOperation.IsNotCancelled = false;
                backgroundOperation.CanBeStarted = true;
            }
        }

        /// <summary>
        /// Private operation data structure.
        /// </summary>
        private class Operation
        {
            internal string Name { get; set; }
            internal Timer Timer { get; set; }
            internal TimeSpan Interval { get; set; }
        }
    }
\$\endgroup\$
7
  • \$\begingroup\$ Do you aware of existing System.Threading.Tasks.Task? Asynchronous Programming. Looks like inventing yet another wheel. :) In general I mean not a replacement your class with .NET one but it may power the solution making it 10x times smaller/simpler/faster. Also learn something about CancellationTokenSource in addition to above link. \$\endgroup\$
    – aepot
    Apr 24, 2021 at 21:00
  • \$\begingroup\$ I'm aware of Task and async/await. Do you have an example on how to create a recurring background job using Task and make sure it's thread safe, cancellable, re-startable and managed through a single entity? \$\endgroup\$
    – TDMR87
    Apr 25, 2021 at 13:55
  • \$\begingroup\$ I can't figure out the value of the solution without a reproducible test case. I see the code but don't know what's its special feature that can't be achieved using regular async approach to the development. \$\endgroup\$
    – aepot
    Apr 25, 2021 at 13:59
  • \$\begingroup\$ Also I must note, that you're able to bind XAML controls into the properties of my RecurringOperation object and make the UI react to the changes in the recurring operation's status. See my demo project where I created a WPF stopwatch program using my library. github.com/TDMR87/RecuropDemo \$\endgroup\$
    – TDMR87
    Apr 25, 2021 at 14:01
  • 1
    \$\begingroup\$ My message was just giving an idea how to power the solution with async/await+TAP features. Btw, the solution has not a bubble-sort-complexity. Afm, to rewrite it correctly with TAP can take a long time, the hour that I have isn't enough. Maybe later. For now I suggest you to edit the post adding links and some information from comments there. \$\endgroup\$
    – aepot
    Apr 25, 2021 at 15:05

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.