Humanizr/Humanizer · error · NotSupportedException

Custom strategy type '{GetType().FullName}' must explicitly

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.

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)

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.