Custom Functions
Reusable C# your calculations can call — what to put in a function library, and the caching behaviour to know about.
Because Infohub calculations are compiled C#, you can write your own methods and call them from any expression. A function library is the right home for any logic that appears in more than a couple of calculations, and it turns unreadable expressions into readable ones.
When a function earns its place#
Create one when logic is repeated, fiddly, or likely to change.
- Repeated. The zero guard appears in every ratio in the model. As a function it appears once.
- Fiddly. Multi-branch state logic, unit conversions with offsets, calendar arithmetic — all easier to get right once and reuse than to retype.
- Likely to change. A costing rule or a conversion factor that will be revised is far safer in one function than scattered across two hundred expressions.
Do not create one for a single simple expression used once. A function that wraps a + b adds indirection and no value.
Writing one#
A function library is a named collection of static C# methods. Methods should be public static, take and return nullable types where a value may be absent, and avoid throwing.
The one every site should have:
public static double? SafeDivide(double? numerator, double? denominator)
{
if (!denominator.HasValue || denominator.Value == 0) return 0;
return (numerator ?? 0) / denominator.Value;
}
A percentage of target, bounded so a chart axis stays sensible:
public static double? PercentOfTarget(double? actual, double? target, bool cap = true)
{
if (!target.HasValue || target.Value == 0) return null;
var pct = (actual ?? 0) / target.Value * 100;
return cap ? Math.Min(pct, 100) : pct;
}
Spreading a period figure across the days in that period, without assuming a month length:
public static double? PerDay(double? periodValue, double daysInPeriod)
{
if (daysInPeriod <= 0) return null;
return (periodValue ?? 0) / daysInPeriod;
}
Called as PerDay(Measure_2100.Value, Period.Duration.TotalDays).
Reading a model attribute safely, which otherwise clutters every expression that uses one:
public static double AttrAsDouble(ModelAttributeCollection attrs, string key, double fallback = 0)
{
if (attrs == null || !attrs.ContainsKey(key)) return fallback;
return double.TryParse(attrs[key].Value, out var v) ? v : fallback;
}
Calling them#
Function library namespaces are imported into the generated calculation code, so methods are called unqualified:
SafeDivide(Measure_1043.Value, Measure_1044.Value)
PercentOfTarget(Measure_2000.Value, Measure_2001.Value)
System, System.Linq, and System.Collections.Generic are available inside your functions as well as in expressions.
Caching: an edit affects everything at once#
Infohub compiles calculations into assemblies and caches them, keyed on the expression and its dependencies. It also tracks when a function was last edited: when a function library changes, cached calculation assemblies are invalidated and rebuilt.
Two consequences worth planning around.
A function edit changes every calculation that calls it. That is the point — but it means correcting SafeDivide to return null rather than zero will change the behaviour of every ratio in the model at once. Know the blast radius before editing a widely used function.
Behaviour changes going forward, not retrospectively. Already-calculated intervals keep their old values until recalculated. After a meaningful function change, recalculate the affected range or the model will hold a mix of old and new logic.
Rename with care. Renaming a function breaks every expression calling it, and those expressions fail at compile time. If you must rename, find and update the callers in the same change. The Config CLI can list calculation expressions, which makes finding them practical.
Guidelines that keep libraries useful#
Return null for "unknown", not a sentinel. Returning -1 or -999 to mean "no answer" pollutes every average and chart downstream. double? exists for this.
Never throw. An exception fails the whole calculation for that interval, and the message a user sees will not be helpful. Validate inputs and return null.
Keep them pure. A function should depend only on its arguments. Reading ambient state or writing anywhere makes calculations non-reproducible and very hard to debug.
Name for intent, not mechanics. SafeDivide reads better than DivideWithZeroCheck. PercentOfTarget reads better than Calc2.
Document the units. A function converting between units should say which way round it goes, in its name or a comment. KgToTonnes is unambiguous; Convert is not.
Group by domain. Separate libraries for general maths helpers, unit conversions, and site-specific business rules. It makes ownership clear and limits the blast radius of a change.
Testing before rollout#
There is no unit test harness for function libraries inside Infohub, so test deliberately:
- Write the function.
- Point one calculated measure at it, on a measure whose correct answer you can verify by hand.
- Check the value for several periods, including a period where the awkward case applies — zero denominator, missing input, month boundary.
- Only then use it widely.
The cost of skipping this is a function used in two hundred expressions with a bug in its edge case.
A note on complexity#
Functions make it possible to put a great deal of logic into the model, which is not always wise. A three-hundred-line state machine in a function library is invisible on a dashboard, hard to review, and known only to whoever wrote it.
Prefer several intermediate measures over one function that does everything: intermediate measures are inspectable, chartable, and self-documenting. Reach for a function when the logic genuinely is a reusable calculation, not when it is really a modelling structure in disguise.