Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Globalization;
using Hangfire.Common;
using Hangfire.States;

namespace Madev.Utils.Infrastructure.Hangfire.Attributes;

public class AllowedExecutionTimeRangeAttribute : JobFilterAttribute, IElectStateFilter
{
private readonly TimeSpan _timeFrom;
private readonly TimeSpan _timeTo;

/// <summary>
/// Allows to run tasks only at a time interval
/// </summary>
/// <param name="timeFrom">Time from (HH:mm:ss)</param>
/// <param name="timeTo">Time to (HH:mm:ss)</param>
public AllowedExecutionTimeRangeAttribute(string timeFrom, string timeTo)
{
_timeFrom = DateTime.ParseExact(timeFrom, "HH:mm:ss", CultureInfo.InvariantCulture).TimeOfDay;
_timeTo = DateTime.ParseExact(timeTo, "HH:mm:ss", CultureInfo.InvariantCulture).TimeOfDay;
}

public void OnStateElection(ElectStateContext context)
{
if (context.CurrentState != EnqueuedState.StateName) return;

var state = context.Connection.GetStateData(context.BackgroundJob.Id);
if (state == null) return; // just in case

var dateTimeNow = GetTimeInCentralEuropeStandardTime(DateTime.Now);

if (JobIsAllowedInActualTime(dateTimeNow) == false)
{
context.CandidateState = new FailedState(new ArgumentOutOfRangeException($"It is not allowed to perform the task at {dateTimeNow}"))
{
Reason = $"It is not allowed to perform the task at {dateTimeNow}"
};
}
}

public bool JobIsAllowedInActualTime(TimeSpan now)
{
if (_timeFrom == _timeTo)
throw new Exception("Duration cannot be 0h0m0s");

// if range is over midnight (from: 23:00:00 to: 01:00:00 duration: 2h)
if (_timeFrom > _timeTo)
{
if ((now >= _timeFrom) || (now <= _timeTo))
{
return true;
}
}

// if range is over day (from: 01:00:00 to: 23:00:00 duration: 22h)
if (_timeFrom < _timeTo)
{
if ((now >= _timeFrom) && (now <= _timeTo))
{
return true;
}
}

return false;
}

public TimeSpan GetTimeInCentralEuropeStandardTime(DateTime dateTime)
{
TimeZoneInfo infotime = TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time");
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded timezone "Central Europe Standard Time" makes this attribute unusable for applications running in other timezones. Consider accepting the timezone as a constructor parameter with a default value, allowing consumers to specify their timezone requirements.

Copilot uses AI. Check for mistakes.

return TimeZoneInfo.ConvertTimeBySystemTimeZoneId(dateTime, infotime.Id).TimeOfDay;
Comment on lines +67 to +71
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TimeZoneInfo.FindSystemTimeZoneById call can throw TimeZoneNotFoundException if "Central Europe Standard Time" is not found on the system. This is particularly problematic on non-Windows systems where timezone identifiers differ (e.g., Linux uses "Europe/Prague" or "Europe/Berlin" instead). Consider using TimeZoneInfo.TryFindSystemTimeZoneById or providing a fallback mechanism.

Suggested change
public TimeSpan GetTimeInCentralEuropeStandardTime(DateTime dateTime)
{
TimeZoneInfo infotime = TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time");
return TimeZoneInfo.ConvertTimeBySystemTimeZoneId(dateTime, infotime.Id).TimeOfDay;
private static readonly string[] CentralEuropeTimeZoneIds =
{
"Central Europe Standard Time", // Windows
"Europe/Prague", // IANA (example CET/CEST)
"Europe/Berlin" // IANA (alternative CET/CEST)
};
public TimeSpan GetTimeInCentralEuropeStandardTime(DateTime dateTime)
{
TimeZoneInfo infoTime = null;
foreach (var timeZoneId in CentralEuropeTimeZoneIds)
{
try
{
infoTime = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
break;
}
catch (TimeZoneNotFoundException)
{
// Try next candidate ID
}
catch (InvalidTimeZoneException)
{
// Try next candidate ID
}
}
if (infoTime == null)
{
infoTime = TimeZoneInfo.Local;
}
return TimeZoneInfo.ConvertTime(dateTime, infoTime).TimeOfDay;

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand All @@ -15,7 +15,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Hangfire.Core" Version="1.8.22" />
<PackageReference Include="Hangfire.Core" Version="1.8.23" />
</ItemGroup>

</Project>
Loading