Fun with generics: mapping a generic instance from a non-generic reference

As already mentioned in my previous “Fun with generics” post, DDD can lead to generic concrete types that implement some non-generic abstract class or interface.

The reason could be that some domain constraint on concrete instances of a class is enforced through the type system by means of generic types with type constraints. At the same time, all those instances are manifestations of a common non-generic concept. This can be represented by some non-generic abstract class or interface which the concrete instances then implement.

This is all fine until we need to call some code that needs the generic type argument (like exporting or mapping), but only hold a reference to an instance of the non-generic concept type.

Let’s use the following example:

abstract class Unit { }

sealed class Quantity<TUnit> where TUnit : Unit
{
    public double Value { get; init; }
}

abstract class Range
{
    Range() { }

    public sealed class OfUnit<TUnit> : Range
        where TUnit : Unit
    {
        public Quantity<TUnit>? Min { get; init; }
        public Quantity<TUnit>? Max { get; init; }
    }
}

abstract class ExportedRange
{
    ExportedRange() { }

    public sealed class OfUnit<TUnit> : ExportedRange
        where TUnit : Unit
    {
        public Quantity<TUnit>? Min { get; init; }
        public Quantity<TUnit>? Max { get; init; }
    }
}

How would we map an instance of Range to anything that requires us to know its type arguments, like an instance of ExportedRange in this minimal example?
In other words, how to implement a function ExportedRange Export(Range range)?
Assume that Unit is open and extensible.
Thus, we cannot switch over all possible closed generic types.
We also do not want to resort to reflection and lose type safety.

Luckily, the answer is a bit simpler than in the aforementioned post.
We can use a visitor-like pattern again, but this time, we do not need to collect the generic types one by one because the instance already knows them all.
We only need to provide a way for the caller to have the instance call a provided generic function with its generic type arguments, like the MapWith below does.
Again, C# does not have open generic delegate types, so we must use an interface or abstract class.

abstract class Range
{
    // ...
    public abstract TOut MapWith<TOut>(IUnitMapper<TOut> mapper);

    public sealed class OfUnit<TUnit> : Range
        where TUnit: Unit
    {
        public override TOut MapWith<TOut>(IMapper<TOut> mapper) =>
            mapper.Invoke<TUnit>();
    }

    public interface IMapper<TOut>
    {
        TOut Map<TIn>() where TIn : Unit;
    }
}

Before I show the whole picture including the mapping itself, I will say that I dislike lumping this purely technical code together with the domain code.
Fortunately C# gives us partial classes, so that we can keep the technical mapping helper code separate from the “real” domain code.

Below you can see what the entire mapping would look like.
Note that aside from adding the partial modifiers, we did not touch the Range class.

// domain code goes here
abstract partial class Range
{
    Range() { }

    public sealed partial class OfUnit<TUnit> : Range
        where TUnit: Unit
    {
    }
}

// purely technical helper code goes here
abstract partial class Range
{
    public abstract TOut MapWith<TOut>(IMapper<TOut> mapper);

    public sealed partial class OfUnit<TUnit> : Range
       where TUnit : Unit
    {
        public override TOut MapWith<TOut>(IUnitMapper<TOut> mapper) =>
            mapper.Map(this);
    }

    public interface IMapper<TOut>
    {
        TOut Map<TUnit>(OfUnit<TUnit> range) where TUnit : Unit;
    }
}

static class Exporter
{
    public static ExportedRange Export(Range range) =>
        range.MapWith(ExportedRangeMapper.Instance);

    sealed class ExportedRangeMapper : Range.IMapper<ExportedRange>
    {
        public static readonly ExportedRangeMapper Instance = new();

        ExportedRange Range.IMapper<ExportedRange>.Map<TUnit>(Range.OfUnit<TUnit> range) =>
            new ExportedRange.OfUnit<TUnit>
            {
                Min = range.Min,
                Max = range.Max
            };
    }
}

Also note how we avoided repeating the type constraints of IMapper by implementing the interface explicitly.
That keeps the ownership of those constraints solely with the defining Range class itself and has a positive impact on readability and complexity of the pattern when multiple type arguments with many constraints are involved.

If we would like to call some method instead (without return value), we can add similar technical code and have the caller implement, say, an void ICommand.Invoke<TUnit>(Range.OfUnit<TUnit> range) in the same vein.

Both IMapper and ICommand are general enough to be a design pattern, i. e. we could write a source generator for them that generates the partial classes.

That way, we can reduce friction from technical difficulties and continue designing our types purely from a domain perspective.