15

Given the followin snippet, i quite dont understand WHY what im going to achieve is not possible:

Interface:

public interface IEntityRepository<out T> : IRepository<IEntity> {

    void RequeryDataBase();

    IEnumerable<T> Search(string pattern);

    Task<IEnumerable<T>> SearchAsync(string pattern);

    SearchContext Context { get; }

    string BaseTableName { get; }
  }

In IRepository<IEntity> are just simple generic CRUD defined.

I get an Error on this line: Task<IEnumerable<T>> SearchAsync(string pattern);

Error:

method return type must be output safe. invalid variance: the type parameter T must be invariantly valid on Task

Please help me understand, why i cant use <out T> with a Task<T>

lokusking
  • 7,396
  • 13
  • 38
  • 57

5 Answers5

19

Task<T> is not covariant. Variance can only be applied to generic interfaces (and delegates, but that's not relevant here).

e.g. Task<IEnumerable<Dog>> cannot be assigned to Task<IEnumerable<Animal>>. Because of this, your interface cannot be marked as covariant either.

You might want to see this related question.

Community
  • 1
  • 1
Charles Mager
  • 25,735
  • 2
  • 35
  • 45
  • 22
    They can give all the explanations they want. I still feel like someone made the wrong call when introducing `Task`, `ITask` but no `ITask`. – Toxantron Jul 01 '16 at 08:30
6

When deciding on variance of a generic type argument in some generic interface, you have to take into account all uses of the generic type argument inside the interface. Each use may introduce some constraints regarding variance. This includes:

  • Uses as input argument to a method - disallows covariance
  • Uses as return value from a method - disallows contravariance
  • Uses as part of other generic derivation, such as Task<T> - such use may disallow covariance, disallow contravariance, or both

I was using negative logic to emphasize that all these cases are basically introducing constraints. After the analysis, you will know whether any element has disallowed covariance and then your argument might not be declared as out. Conversely, if any element has disallowed contravariance, your argument might not be declared as in.

In your particular case, the first Search method returns IEnumerable<T>, rendering contravariance non-applicable. But then, SearchAsync is returning Task<IEnumerable<T>>, and that usage introduces constraints that exist in the Task type - it is invariant, meaning that this time out is impossible as well.

The result is that your generic interface must be invariant on its generic argument type in order to satisfy signatures of all its methods.

Zoran Horvat
  • 10,924
  • 3
  • 31
  • 43
  • 2
    I don't fully understand why there is not ITask interface. It would help with those issues. – Toxantron Jul 01 '16 at 08:19
  • @Toxantron because someone has to _set_ the result, so it can't be covariant. What I still don't see is how all this applies here, since the type in question is still `IEnumerable` and not `T`. Why does the variance of `T` play any role in `Task>`? – René Vogt Jul 01 '16 at 08:22
  • 1
    @RenéVogt `Task` is invariant on `IEnumerable`, which transitively means that T must not vary, despite the fact that `IEnumerable` itself is covariant on `T`. The `Search` method returns plain `IEnumerable` and that doesn't constraint variance. With only the `Search` method, entire interface could be covariant on `T`. – Zoran Horvat Jul 01 '16 at 08:25
  • @RenéVogt sure it can. I design APIs like this all the time. Look at List and IEnumerable. You operate on Task but return ITask. – Toxantron Jul 01 '16 at 08:28
  • @Toxantron What is ITask? – Zoran Horvat Jul 01 '16 at 08:34
  • @ZoranHorvat Sadly non existent for some foggy reasons. And they argue it can't be changed now. I would expect someone creates `ITask` and whenever we come across an API that uses Task we switch it out. – Toxantron Jul 01 '16 at 08:57
1

If you can use .NET Core or .NET 5 and onwards the solution is to use IAsyncEnumerable<T> instead of Task<IEnumerable<T>>.

The covariant interface would look like:

public interface IEntityRepository<out T> : IRepository<IEntity> {

    void RequeryDataBase();

    IEnumerable<T> Search(string pattern);

    IAsyncEnumerable<T> SearchAsync(string pattern);

    SearchContext Context { get; }

    string BaseTableName { get; }
  }

More info about covariant async can be found here.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Lakedaimon
  • 1,784
  • 2
  • 11
  • 10
0

You can cheat and use

IObservable<out T>

which is almost the same as Task<T> but with a covariant type. It can always be converted to a task when you need it. You lose a little bit in the readability of the code because it is assumed ( and enforced ) with Task<T> that you only get one result but with IObservable<T> you can get many. However you can do

IObservable<T> o = ...
var foo = await o;

just the same as

Task<T> t = ...
var foo = await t;

Note that you'll need to include the System.Reactive.Linq namespace from the Rx library to make this work. It will add an extension method to IObservable<> that will make it awaitable.

IluTov
  • 6,807
  • 6
  • 41
  • 103
bradgonesurfing
  • 30,949
  • 17
  • 114
  • 217
0

As others have mentioned before, TResult in Task<TResult> is not covariant. What you can do however is create a new Task instance using ContinueWith:

var intTask = Task.FromResult(42);
var objectTask = intTask.ContinueWith(t => (object) t.Result);

await objectTask; // 42
IluTov
  • 6,807
  • 6
  • 41
  • 103