Search code examples
c#genericsinheritancepolymorphism

Hide generic type arguments behind concrete type results in CS0535


I've a rather academic question: why is it technically not possible to use the following code?

// abstractions
public record MappingRequest<TContract, TEntity>(
    TContract Contract, TEntity Entity);

public interface IMapper<TResult, TContract, TEntity>
{
    TResult Map(MappingRequest<TContract, TEntity> request);
}

// more readable abstraction
public interface ILocationMapper
    : IMapper<LocationResult
    , LocationContract, LocationEntity>
{
}

// concrete
public record LocationResult;
public record LocationContract;
public record LocationEntity;

public record LocationRequest(LocationContract Contract, LocationEntity Entity)
    : MappingRequest<LocationContract
    , LocationEntity>(Contract, Entity);

public class LocationMapper : ILocationMapper
{
    public LocationResult Map(LocationRequest request)
    {
        return new LocationResult();
    }
}

This gives me the following compiler error:

Error CS0535 : 'LocationMapper' does not implement interface member 'IMapper<LocationResult, LocationContract, LocationEntity>.Map(MappingRequest<LocationContract, LocationEntity>)'

Why can't the compiler not "infer" (not entirely sure whether this is the correct term here) that LocationRequest inherits from MappingRequest?


Solution

  • Your LocationMapper.Map method currently accepts a LocationRequest, but any implementation of IMapper<LocationResult, LocationContract, LocationEntity> must implement a Map method that accepts any MappingRequest<TContract, TEntity>, not just LocationRequest.

    Your code is simple, but imagine LocationRequest had a property Foo:

    public record LocationRequest(LocationContract Contract, LocationEntity Entity)
        : MappingRequest<LocationContract, LocationEntity>(Contract, Entity)
    {
        public string Foo { get; }
    }
    

    And your method tried to use that property:

    public class LocationMapper : ILocationMapper
    {
        public LocationResult Map(LocationRequest request)
        {
            Console.WriteLine(request.Foo);
            return new LocationResult();
        }
    }
    

    Now, if you try to call that as an IMapper<LocationResult, LocationContract, LocationEntity>, with another MappingRequest<TContract, TEntity>, we have a problem, because there is no Foo property.

    e.g.

    var request = new MappingRequest<TContract, TEntity>(default, default);
    IMapper<LocationResult, LocationContract, LocationEntity> mapper
        = new LocationMapper();
    mapper.Map(request); // We don't have any Foo to use !!
    

    The problem is that LocationRequest isn't a simplified alias for MappingRequest<LocationContract, LocationEntity>, it's a completely different type altogether.

    If verbosity is a problem for you, then you can alias your type like this:

    using LocationRequest = MappingRequest<LocationContract, LocationEntity>;