Search code examples
c#covariance

Covariant Expression<Func<TBase, bool>>


We have some common code. We'd like to have a method that accepts an Expression like below

public interface ISmallInterface
{
    // ...
}

public interface IExample<TBase>
{
    IQueryable<TBase> Where(Expression<Func<TBase, bool>> predicate);
}

public class SmallClass : ISmallInterface
{
    // ...
}

And from this we have some basic implementations

public abstract class AlphaBase<TBase> : IExample<TBase>
{
    public IQueryable<TBase> Where(Expression<Func<TBase, bool>> predicate)
    {
        // ...
    }
}

In our core logic we use this to build components. And then over here in this Gamma example, we'd like a method or property exposing an IExample<ISmallInterface>.

public class Beta : AlphaBase<SmallClass>
{
    // ...
}

public class Gamma
{
    public IExample<ISmallInterface> GetThing()
    {
        var b = new Beta();
        return b;
    }
}

However, this gives a compiler error.

Cannot implicitly convert type 'Beta' to 'IExample<ISmallInterface>'. An explicit conversion exists (are you missing a cast?)

Changing IExample to use a covariant type parameter fixes this conversion issue, but breaks the Where method.

public interface IExample<out TBase>
{
    IQueryable<TBase> Where(Expression<Func<TBase, bool>> predicate);
}

Gives compiler error

Invalid variance: The type parameter 'TBase' must be invariantly valid on 'IExample<TBase>.Where(Expression<Func<TBase, bool>>)'. 'TBase' is covariant.

In our case, we were able to work with just Func parameters.

public interface IExample<out TBase>
{
    IQueryable<TBase> Where(Func<TBase, bool> predicate);
}

public abstract class AlphaBase<TBase> : IExample<TBase>
{
    public IQueryable<TBase> Where(Func<TBase, bool> predicate)
    {
        throw new NotImplementedException();
    }
}

This compiles and runs. However, it would be convenient to work with Expression<Func<TBase, bool>>.

Is there some kind of work around to use Expression<Func<TBase, bool>> with a covariant type?

(dotnet core 2.2, if that matters, and I think C#7.3)


Solution

  • The solution is simple enough, and seems to follow "the Linq way."

    Just make an extension method with the correct signature and implement it there. There's no Where method in the interface now. Following the example above, code looks something like

    public interface IExample<out TBase>
    {
        // no Where, Count, or FirstOrDefault methods that accept Expression defined here
    
        // This is a lazy loading method and doesn't execute the query
        IQueryable<TBase> GetAll(ApplicationUser user);
    }
    
    public static class ExtensionMethods
    {
        public static IQueryable<TBase> Where<TBase>(
            this IExample<TBase> repository,
            ApplicationUser user,
            Expression<Func<TBase, bool>> predicate)
        {
            return repository.GetAll(user).Where(predicate);
        }
    
        public static int Count<TBase>(
            this IExample<TBase> repository,
            ApplicationUser user,
            Expression<Func<TBase, bool>> predicate)
        {
            return repository.GetAll(user).Count(predicate);
        }
    
        public static TBase FirstOrDefault<TBase>(
            this IExample<TBase> repository,
            ApplicationUser user,
            Expression<Func<TBase, bool>> predicate)
        {
            return repository.GetAll(user).FirstOrDefault(predicate);
        }
    }