public void Register(RecurringJobInfo recurringJobInfo)
        {
            if (recurringJobInfo == null)
            {
                throw new ArgumentNullException(nameof(recurringJobInfo));
            }

            Register(recurringJobInfo.RecurringJobId, recurringJobInfo.Method, recurringJobInfo.Cron, recurringJobInfo.TimeZone, recurringJobInfo.Queue);
        }
        /// <summary>
        /// Register RecurringJob via <see cref="RecurringJobInfo"/>.
        /// </summary>
        /// <param name="recurringJobInfo"><see cref="RecurringJob"/> info.</param>
        public void Register(RecurringJobInfo recurringJobInfo)
        {
            if (recurringJobInfo == null)
            {
                throw new ArgumentNullException(nameof(recurringJobInfo));
            }

            Register(recurringJobInfo.RecurringJobId, recurringJobInfo.Method, recurringJobInfo.Cron, recurringJobInfo.TimeZone, recurringJobInfo.Queue);

            using (var storage = new RecurringJobInfoStorage())
            {
                storage.SetJobData(recurringJobInfo);
            }
        }
        /// <summary>   Adds an or update to 'args'. </summary>
        /// <exception cref="ArgumentNullException">    Thrown when one or more required arguments are
        ///                                             null. </exception>
        /// <typeparam name="T">    Generic type parameter. </typeparam>
        /// <param name="expression">   The expression. </param>
        /// <param name="args">         The arguments. </param>
        public static void AddOrUpdate <T>(Expression <Action <T> > expression, object args)
        {
            if (expression == null)
            {
                throw new ArgumentNullException(nameof(expression));
            }

            foreach (var method in typeof(T).GetTypeInfo().DeclaredMethods)
            {
                if (!method.IsDefined(typeof(RecurringJobAttribute), false))
                {
                    continue;
                }

                var attribute = method.GetCustomAttribute <RecurringJobAttribute>(false);

                if (attribute == null)
                {
                    continue;
                }

                if (string.IsNullOrWhiteSpace(attribute.RecurringJobId))
                {
                    attribute.RecurringJobId = method.GetRecurringJobId();
                }

                if (!attribute.Enabled)
                {
                    RecurringJob.RemoveIfExists(attribute.RecurringJobId);
                    continue;
                }

                var recurringJobInfo = new RecurringJobInfo
                {
                    RecurringJobId = attribute.RecurringJobId,
                    Enable         = true,
                    Method         = expression.GetMethodInfo(),
                    JobData        = args.ToDictionary(),
                    Cron           = attribute.Cron,
                    TimeZone       = string.IsNullOrEmpty(attribute.TimeZone) ? TimeZoneInfo.Utc : TimeZoneInfo.FindSystemTimeZoneById(attribute.TimeZone),
                    Queue          = attribute.Queue ?? EnqueuedState.DefaultQueue
                };
                AddOrUpdate(recurringJobInfo);
            }
        }
        /// <summary>
        /// Sets <see cref="RecurringJobInfo"/> to storage which associated with <see cref="RecurringJob"/>.
        /// </summary>
        /// <param name="recurringJobInfo">The specified identifier of the RecurringJob.</param>
        public void SetJobData(RecurringJobInfo recurringJobInfo)
        {
            if (recurringJobInfo == null)
            {
                throw new ArgumentNullException(nameof(recurringJobInfo));
            }

            if (recurringJobInfo.JobData == null || recurringJobInfo.JobData.Count == 0)
            {
                return;
            }

            using (_connection.AcquireDistributedLock($"recurringjobextensions-jobdata:{recurringJobInfo.RecurringJobId}", LockTimeout))
            {
                var changedFields = new Dictionary <string, string>
                {
                    [nameof(RecurringJobInfo.Enable)]  = SerializationHelper.Serialize(recurringJobInfo.Enable),
                    [nameof(RecurringJobInfo.JobData)] = SerializationHelper.Serialize(recurringJobInfo.JobData)
                };

                _connection.SetRangeInHash($"recurring-job:{recurringJobInfo.RecurringJobId}", changedFields);
            }
        }