Humanizr/Humanizer · error · NotSupportedException

Custom strategy type

Error message

Custom strategy type '{GetType().FullName}' must explicitly implement IGrammaticalCaseTimeSpanHumanizeStrategy to support grammatical-case-aware durations.

What it means

Thrown by DefaultTimeSpanHumanizeStrategy's explicit IGrammaticalCaseTimeSpanHumanizeStrategy implementation when the runtime type is a subclass from a different assembly than Humanizer itself. This prevents a derived class from silently inheriting case support without owning the implementation, which could produce incorrect output.

Solutions

  1. Implement IGrammaticalCaseTimeSpanHumanizeStrategy explicitly on your subclass instead of inheriting it.
  2. Compose (wrap) a DefaultTimeSpanHumanizeStrategy rather than subclassing it.
  3. Use the built-in DefaultTimeSpanHumanizeStrategy directly.

Example fix

// before
public class LoggingStrategy : DefaultTimeSpanHumanizeStrategy { /* inherits case support */ }
Configurator.TimeSpanHumanizeStrategy = new LoggingStrategy();

// after
public class LoggingStrategy : DefaultTimeSpanHumanizeStrategy, IGrammaticalCaseTimeSpanHumanizeStrategy
{
    string IGrammaticalCaseTimeSpanHumanizeStrategy.Humanize(
        TimeSpan ts, int p, bool c, CultureInfo? cult,
        TimeUnit max, TimeUnit min, string? sep, bool sym, GrammaticalCase gc)
    {
        // log here
        return TimeSpanHumanizeExtensions.DefaultHumanizeWithCase(
            ts, p, c, cult, max, min, sep, sym, gc);
    }
}
Defensive patterns

Strategy: type-guard

Validate before calling

static bool IsSafeSubclass(Type strategyType) =>
    strategyType == typeof(DefaultTimeSpanHumanizeStrategy) ||
    strategyType.GetInterfaces().Contains(typeof(IGrammaticalCaseTimeSpanHumanizeStrategy)) &&
    strategyType.Assembly != typeof(DefaultTimeSpanHumanizeStrategy).Assembly;

Type guard

static bool StrategyOwnsCaseImpl(ITimeSpanHumanizeStrategy strategy) =>
    strategy.GetType() == typeof(DefaultTimeSpanHumanizeStrategy) ||
    strategy is IGrammaticalCaseTimeSpanHumanizeStrategy &&
    strategy.GetType().GetInterfaces().Contains(typeof(IGrammaticalCaseTimeSpanHumanizeStrategy));

Prevention

When it happens

Trigger: Creating a class that extends DefaultTimeSpanHumanizeStrategy in your own assembly, registering it via Configurator.TimeSpanHumanizeStrategy, and calling HumanizeWithCase. The assembly-equality check at line 75 detects the subclass.

Common situations: Developer subclasses DefaultTimeSpanHumanizeStrategy to add logging or caching and then calls case-aware APIs. The subclass inherits the explicit interface implementation, but Humanizer blocks this for safety.

Related errors


AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13). Data as JSON: /api/errors/3f7e2369986bb2cd. Report an issue: GitHub.

Appendix: source

Thrown at src/Humanizer/TimeSpanHumanizeStrategy/DefaultTimeSpanHumanizeStrategy.cs:77

            collectionSeparator,
            maxFractionalDigits,
            roundingMode,
            toSymbols);

    string IGrammaticalCaseTimeSpanHumanizeStrategy.Humanize(
        TimeSpan timeSpan,
        int precision,
        bool countEmptyUnits,
        CultureInfo? culture,
        TimeUnit maxUnit,
        TimeUnit minUnit,
        string? collectionSeparator,
        bool toSymbols,
        GrammaticalCase grammaticalCase)
    {
        if (GetType().Assembly != typeof(DefaultTimeSpanHumanizeStrategy).Assembly)
        {
            throw new NotSupportedException(
                $"Custom strategy type '{GetType().FullName}' must explicitly implement {nameof(IGrammaticalCaseTimeSpanHumanizeStrategy)} to support grammatical-case-aware durations.");
        }

        return TimeSpanHumanizeExtensions.DefaultHumanizeWithCase(
            timeSpan,
            precision,
            countEmptyUnits,
            culture,
            maxUnit,
            minUnit,
            collectionSeparator,
            toSymbols,
            grammaticalCase);
    }
}

View on GitHub (pinned to ffc2b77c0f)