Constraint-Based Type Inference in C#/F#

Asked by At

I've been trying to get this something like this static extension working for a while now:

public static class MixedRepositoryExtensions {
    public static Task<TEntity> FindBySelectorAsync<TRepository, TEntity, TSelector>(
        this TRepository repository,
        TSelector selector)
        where TRepository : IReadableRepository<TEntity>, IListableRepository<TEntity>
        where TEntity : class, ISearchableEntity<TSelector>
        => repository.Entities.SingleOrDefaultAsync(x => x.Matches(selector));
}

To my understanding, however, C#, by design, does not include generic constraints as part of its inference process, resulting in the following CS0411 error when trying to call it:

The type arguments for method 'MixedRepositoryExtensions.FindBySelectorAsync(TRepository, TSelector)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

Example calling method (where ProjectRepository extends both IReadableRepository<Project> and IListableRepository<Project> and project extends ISearchableEntity<int>):

await (new ProjectRepository()).FindBySelectorAsync(0);

I have thought about defining them explicitly on all callers, however, this method is not ideal as it would be used in a lot of places and with a lot of long-named types.

I have also considered inheriting the two interfaces into one interface like so:

IReadableAndListableRepository<TEntity> : 
    IReadableRepository<TEntity>,
    IListableRepository<TEntity>

However, as I would have more than just this one extension using more that just this one combination, I found that this would lead to interface explosion (if that's a thing?). For instance, this would be another:

IUpdatableAndListableRepository<TEntity :
    IUpdatableRepository<TEntity>,
    IListableRepository<TEntity>

I found a hint here from Eric Lippert that using F# may be able to help (as I was getting desperate):

Generics: Why can't the compiler infer the type arguments in this case?

I played around a bit with F# but found little documentation about constraining types to multiple interfaces (or any specific interfaces for that matter) and couldn't get past a few errors. Here's the last attempt I tried. I realize that the method does not return the same value, I was just trying, for now, to get the constraints to play nicely. Sorry if this is badly done, it's my first time playing with F#.

[<Extension>]
type MixedRepositoryExtensions() =
    [<Extension>]
    static member inline FindBySelectorAsync<'TSelector, 'TEntity when 'TEntity: not struct and 'TEntity:> ISearchableEntity<'TSelector>>(repository: 'TRepository when 'TRepository:> IReadableRepository<'TEntity> and 'TRepository:> IListableRepository<'TEntity>, selector: 'TSelector) = repository;

However, this implementation results in the following errors, both referring to the line on which FindBySelectorAsync is defined:

FS0331: The implicit instantiation of a generic construct at or near this point could not be resolved because it could resolve to multiple unrelated types, e.g. 'IListableRepository <'TEntity>' and 'IReadableRepository <'TEntity>'. Consider using type annotations to resolve the ambiguity

FS0071: Type constraint mismatch when applying the default type 'IReadableRepository<'TEntity>' for a type inference variable. The type 'IReadableRepository<'TEntity>' is not compatible with the type 'IListableRepository<'TEntity>' Consider adding further type constraints

So, I guess my questions are:

  1. Is there a design pattern you can think of in C# that would allow me utilize this method without introducing interface explosion?
  2. If not, can F# more advanced inference solve this for me? Or am I barking up the wrong tree?
  3. If F# can solve this for me, what should the method signature look like (because I know mine isn't right, but I couldn't find a good example of something like this online)?
  4. Since this F# method will be used by C#, would it use F#'s inference, or C#'s inference?
  5. Am I overlooking some huge reason why this would be a bad idea? I've been known to have bad ideas before, and I'm not opposed to being told not to try this.

Interfaces

As requested, here are the primary interfaces that were used in the examples:

public interface IRepository<TEntity>
    where TEntity : class {
}

public interface IReadableRepository<TEntity> :
    IRepository<TEntity>
    where TEntity : class {
    #region Read
    Task<TEntity> FindAsync(TEntity entity);
    #endregion
}

public interface IListableRepository<TEntity> :
    IRepository<TEntity>
    where TEntity : class {
    #region Read
    IQueryable<TEntity> Entities { get; }
    #endregion
}

public interface ISearchableEntity<TSelector> {
    bool Matches(TSelector selector);
}

Solution

A great thank you to Zoran Horvat below. This solution builds upon his idea and wouldn't be possible without it. I simply abstracted it a bit further for my purposes and moved the FixTypes method into the extension methods. Here's the final solution I came to:

public interface IMixedRepository<TRepository, TEntity>
    where TRepository: IRepository<TEntity>
    where TEntity : class { }

public static class MixedRepositoryExtensions {
    public static TRepository AsMixedRepository<TRepository, TEntity>(
        this IMixedRepository<TRepository, TEntity> repository)
        where TRepository : IMixedRepository<TRepository, TEntity>, IRepository<TEntity>
        where TEntity : class
        => (TRepository)repository;
}

public static Task<TEntity> FindBySelectorAsync<TRepository, TEntity, TSelector>(
        this IMixedRepository<TRepository, TEntity> repository,
        TSelector selector)
        where TRepository : 
            IMixedRepository<TRepository, TEntity>, 
            IReadableRepository<TEntity>, 
            IListableRepository<TEntity>
        where TEntity : class, ISearchableEntity<TSelector>
        => repository.AsMixedRepository().Entities.SingleAsync(selector);

public class ProjectRepository :
    IMixedRepository<IProjectRepository, Project>,
    IReadableRepository<Project>,
    IListableRepository<Project>
{ ... }

Finally, the extension method method can be called via:

await (new ProjectRepository())
    .FindBySelectorAsync(0);

This solution, however, lacks some static typing since it uses a down-casting. If you down-cast as mixed repository as a repository it does not implement, this will throw an exception. And due to the further limitations on circular constraint dependencies, it is possible to break this at runtime. For a fully static typed version, see Zoran's answer below.


Alternative Solution

Another solution based on Zoran's answer that does enforce static typing:

public interface IMixedRepository<TRepository, TEntity>
    where TRepository: IRepository<TEntity>
    where TEntity : class {
    TRepository Mixed { get; }
}

public static class MixedRepositoryExtensions {
    public static TRepository AsMixedRepository<TRepository, TEntity>(
        this IMixedRepository<TRepository, TEntity> repository)
        where TRepository : IMixedRepository<TRepository, TEntity>, IRepository<TEntity>
        where TEntity : class
        => repository.Mixed;
}

public static Task<TEntity> FindBySelectorAsync<TRepository, TEntity, TSelector>(
    this IMixedRepository<TRepository, TEntity> repository,
    TSelector selector)
    where TRepository : 
        IMixedRepository<TRepository, TEntity>, 
        IReadableRepository<TEntity>, 
        IListableRepository<TEntity>
    where TEntity : class, ISearchableEntity<TSelector>
    => repository.AsMixedRepository().Entities.SingleAsync(selector);

public class ProjectRepository :
    IMixedRepository<IProjectRepository, Project>,
    IReadableRepository<Project>,
    IListableRepository<Project>
{ 
    IProjectRepository IMixedRepository<IProjectRepository, Project>.Mixed { get => this; }
    ... 
}

This can be called the same way. The only difference is that you have to implement it in each repository.. Not that much of a pain though.

2 Answers

1
Zoran Horvat On Best Solutions

I suspect that the problem occurs because TEntity is only defined indirectly, or transitively, so to say. For the compiler, the only way to figure out what TEntity is would be to inspect TRepository in depth. However, C# compiler doesn't inspect types in depth, but only observes their immediate signature.

I believe that by removing TRepository from the equation, all your troubles will go away:

public static class MixedRepositoryExtensions {
    public static Task<TEntity> FindBySelectorAsync<TEntity, TSelector>(
        this IReadableAndListableRepository<TEntity> repository,
        TSelector selector)
        where TEntity : class, ISearchableEntity<TSelector>
        => repository.Entities.SingleOrDefaultAsync(x => x.Matches(selector));
}

When you apply this method to a concrete object implementing the repository interface, its own generic type parameter will be used to infer the signature of the FindBySelectorAsync method.

If the problem is in having ability to specify the list of constraints for the repository in several non-equal extension methods, then I think that the .NET platform is the limitation, rather than C# itself. Since F# also compiles into byte code, generic types in F# would fall under the same constraints as those of C#.

I couldn't find dynamic solution, the one which resolves all the types on the fly. However, there is one trick which retains full static typing capabilities, but requires each concrete repository to add one additional property getter. This property cannot be inherited or attached as an extension, because it would differ in return type in each concrete type. Here is the code which demonstrates this idea (property is simply called FixTypes):

public class EntityHolder<TTarget, TEntity>
{
    public TTarget Target { get; }

    public EntityHolder(TTarget target)
    {
        Target = target;
    }
}

public class PersonsRepository
    : IRepository<Person>, IReadableRepository<Person>,
      IListableRepository<Person>
{
    public IQueryable<Person> Entities { get; } = ...

    // This is the added property getter
    public EntityHolder<PersonsRepository, Person> FixTypes =>
        new EntityHolder<PersonsRepository, Person>(this);
}

public static class MixedRepositoryExtensions 
{
    // Note that method is attached to EntityHolder, not a repository
    public static Task<TEntity> FindBySelectorAsync<TRepository, TEntity, TSelector>(
        this EntityHolder<TRepository, TEntity> repository, TSelector selector)
        where TRepository : IReadableRepository<TEntity>, IListableRepository<TEntity>
        where TEntity : class, ISearchableEntity<TSelector>
        => repository.Target.Entities.SingleOrDefaultAsync(x => x.Matches(selector));
        // Note that Target must be added before accessing Entities
}

A repository with the FixTypes property getter defined can be consumed in usual way, but extension method is only defined on the result of its FixTypes property:

new PersonsRepository().FixTypes.FindBySelectorAsync(ageSelector);
0
Olivier Jacot-Descombes On

Isn't this repository structure over-designed? Either a repository is read-only or it is read write.

public interface IReadOnlyRepository<TEntity>
    where TEntity : class
{
    Task<TEntity> FindAsync(TEntity entity);
    IQueryable<TEntity> Entities { get; }
    // etc.
}

// The read-write version inherits from the read-only interface.
public interface IRepository<TEntity> : IReadOnlyRepository<TEntity>
    where TEntity : class
{
    void Update(TEntity entity);
    void Insert(TEntity entity);
    // etc.
}

Also, you could get rid of TSelector by changing the design to

public interface ISelector<TEntity>
    where TEntity : class
{
    bool Matches(TEntity entity);
}

Now, only one type parameter is required

public static class MixedRepositoryExtensions {
    public static Task<TEntity> FindBySelectorAsync<TEntity>(
        this IReadOnlyRepository<TEntity> repository,
        ISelector<TEntity> selector
    ) where TEntity : class
        => repository.Entities.SingleOrDefaultAsync(x => selector.Matches(x));
}