0

I've been experimenting with generics in C# to improve writing boilerplate code.

Recently, I wrote this method to repeat a paginated REST API call until the pages ran out. The method assumes the object obeys an IPaginated interface, which just has a required ID property.

This was my original implementation.


        /// <summary>
        /// Generic implementation of getting all object from a paginated API call.
        /// </summary>
        /// <param name="getTask">Delegate to the API calling method, points to the last object ID or null.</param>
        /// <param name="limit">The limit on # of elements.</param>
        /// <returns></returns>
        internal static async Task<Models.IPaginated[]> GetPaginated(Func<string, Task<Models.IPaginated[]>> getTask, int limit)
        {
            List<Models.IPaginated> values = new List<Models.IPaginated>();
            Models.IPaginated[] page = await getTask(null);
            values.AddRange(page);

            while (page.Length == limit)
            {
                var lastobj = page[limit - 1];
                page = await getTask(lastobj.ID);
                values.AddRange(page);
            }

            return values.ToArray();
        }

However, that cause a compiler error when I wrote out the delegate object. (Class A implements IPaginated.)


        public async Task<Models.ClassA[]> GetAll(string arg1, string arg2)
        {
            int limit = 100;
            Func<string, Task<Models.ClassA[]>> getTask = ((string lastobj) =>
            {
                return Get(arg1, arg2, limit, lastobj);
            });

            return await GetPaginated(getTask, limit);
        }

So, now I reworked the method to be a generic conditioned by IPaginated.

        /// <summary>
        /// Generic implementation of getting all object from a paginated API call.
        /// </summary>
        /// <param name="getTask">Delegate to the API calling method, points to the last object ID or null.</param>
        /// <param name="limit">The limit on # of elements.</param>
        /// <returns></returns>
        internal static async Task<T[]> GetPaginated<T>(Func<string, Task<T[]>> getTask, int limit)
            where T : Models.IPaginated
        {
            List<T> values = new List<T>();
            T[] page = await getTask(null);
            values.AddRange(page);

            while (page.Length == limit)
            {
                var lastobj = page[limit - 1];
                page = await getTask(lastobj.ID);
                values.AddRange(page);
            }

            return values.ToArray();
        }

which causes no compiler errors. But why? And how does T : IPaginated allow me to retrieve the .ID property without casting the object?

Ramneek Singh
  • 217
  • 3
  • 11
  • What is `Models.ClassA`? It should have the `Models.IPaginated` interface implemented – Jeroen van Langen Mar 06 '21 at 00:49
  • 1
    Because a `Task` is not the same as a `Task`. See duplicate for one of many already-existing questions addressing this. See also "generic type variance". – Peter Duniho Mar 06 '21 at 00:50
  • `ClassA` might be assignable to `IPaginated`, but [`Task` is _not_ assignable to `Task`](https://stackoverflow.com/questions/30996986/why-is-taskt-not-co-variant). The reason it works with a generic parameter is that `Task` will eventually translate to the concrete runtime type `Task` (rather than `Task`) once you pass `ClassA`-typed arguments to it. – Mathias R. Jessen Mar 06 '21 at 00:51
  • @MathiasR.Jessen Thanks, that makes sense; I had just assumed that covariance was universal between interfaces and classes. – Ramneek Singh Mar 06 '21 at 00:56
  • Nope, classes are _always_ invariant. – Mathias R. Jessen Mar 06 '21 at 00:57
  • `where T : Models.IPaginated` says that `T` can be ant type which implements ` Models.IPaginated` interface. So if `Models.IPaginated` has ID property then object of type `T` will ID property too. – Chetan Mar 06 '21 at 00:59

0 Answers0