Summary#
A ServiceHours row says when a team works: weekly windows in a zone, and closed days. The
workflow engine already walks it to decide when an SLA clock is due. These four calls ask the same walk directly:
ServiceHours.IsOpen(hours, at)— is the schedule open at that instant? The question "is she on shift?".ServiceHours.NextOpen(hours, at)— the first instant at or afteratwhen it is open:atitself when it already is, the next window's opening when not, andnullwhen nothing opens within forty years.ServiceHours.BusinessTime(hours, from, to)— how much open time lies between two instants.ServiceHours.After(hours, at, span)— the instantspanof open time afterat: BusinessTime's inverse, and exactly the walk the engine sets a deadline with.nullwhen the schedule does not open enough within the walk.
Signature#
bool ServiceHours.IsOpen(ServiceHours? hours, DateTime at);
DateTime? ServiceHours.NextOpen(ServiceHours? hours, DateTime at);
TimeSpan ServiceHours.BusinessTime(ServiceHours? hours, DateTime from, DateTime to);
DateTime? ServiceHours.After(ServiceHours? hours, DateTime at, TimeSpan span);Description#
One walk, two readers#
The engine computes a milestone's due time by walking the run's schedule; these calls load the same rows into the same
calendar and ask it. So "Rahul is off hours" and "this reply is due Monday 10:00" are two readings of one answer, and an
app that shows both cannot contradict itself. A window's local time is read in the schedule's Zone, daylight saving
included; a ServiceException with IsClosed closes its dates.
No schedule is around the clock#
hours may be null. That is the 24/7 schedule — the same meaning a workflow with no ServiceHours has — so
IsOpen(null, at) is true, BusinessTime(null, from, to) is to - from, and After(null, at, span) is at + span. A person who has not set their hours is
always available, rather than never.
A read, and a server one#
Each call reads the schedule's rows, so it runs on the server and is refused inside a query predicate (it is not a column). In a workflow body it is remembered like any other read, so a replay sees the answer the first pass saw.
Examples#
On shift, and when they start#
using Osysharp.ServiceHours;
partial entity ServiceHours { security { allow read, create when IsAuthenticated; } }
partial entity ServiceWindow { security { allow read, create when IsAuthenticated; } }
[Principal]
entity Agent {
[Required, MaxLength(100)] string Name;
ServiceHours? Hours; // their working hours, in their zone; none: around the clock
security { allow read, create, update when IsAuthenticated; }
}
bool OnShift(Agent a) { return ServiceHours.IsOpen(a.Hours, DateTime.UtcNow); }
string Starts(Agent a) {
var next = ServiceHours.NextOpen(a.Hours, DateTime.UtcNow);
return next == null ? "not scheduled" : $"{a.Name} starts at {next:HH:mm} UTC";
}
TimeSpan WorkedSince(Agent a, DateTime since) { return ServiceHours.BusinessTime(a.Hours, since, DateTime.UtcNow); }
// "Offered to her first — it falls through at…": fifteen minutes of HER working time from now, which on a Friday evening
// is Monday morning.
DateTime? FallsThrough(Agent a) { return ServiceHours.After(a.Hours, DateTime.UtcNow, TimeSpan.FromMinutes(15)); }See also#
- ServiceHours (SLA-accrual windows) — the schedule itself, and binding it to a workflow's clocks
- Assigned / Finished (milestones) — the promises that walk it
- Workflow.WorkByItem<T> (one row per item — the board read) — the due time a board shows, computed by the same walk