November 24, 2024

Replace Task with IAsyncEnumerable

Hey there! In my latest codings, I’ve had an issue with returning an enumerable list of derived types, converted to a base type. This method needed to be async because it was calling an external service that retrieved the required data. Because an async function has to return Task<T> and Task is not contravariant, my situation didn’t work:


public class ProductDto
{
   public string Name { get; set; }

   public float Price { get; set; }
 
   public uint Quantity { get; set; }
}

public class BathroomProductDto : ProductDto
{
    public bool CleansLimescale { get; set; }
}

public class KitchenProductDto : ProductDto
{
    public short PowerLevel { get; set; }
}

public interface IProductService<TProduct> where TProduct : ProductDto
{
    Task<IEnumerable<TProduct>> ListProductsAsync();
}

public interface IProductCenter
{
    IEnumerable<IProductService<ProductDto>> GetServices();
}

The code is pretty simple and neat. Basically, we have a base ProductDto class and an interface which works with derived types. In the IProductCenter interface we want to be able to return all interfaces that work with products. So far so good, from the definition point of view. Let’s try to implement the IProductCenter interface (we will not implement the other ones for brevity):


public class ProductCenter : IProductCenter
    {
        public IEnumerable<IProductService<ProductDto>> GetServices()
        {
            yield return new KitchenProductService();
            yield return new BathroomProductService();
        }
    }

Now here problems start to arise because we cannot convert the implementing classes directly to IProductService<ProductDto> because IProductService is not declared as contravariant. The error says “Error CS0266 Cannot implicitly convert type ‘KitchenProductService’ to ‘IProductService’. An explicit conversion exists (are you missing a cast?)

No worries, we will change that at once, it’s just a single keyword required in the definition of the interface:


public class ProductCenter : IProductCenter
    {
        public IEnumerable<IProductService<ProductDto>> GetServices()
        {
            yield return new KitchenProductService();
            yield return new BathroomProductService();
        }
    }

Now we get another error in the definition of the ListProductsAsync method: “Invalid variance: The type parameter ‘TProduct’ must be invariantly valid on ‘IProductService.ListProductsAsync()’. ‘TProduct’ is covariant“. The reason why we get this error is that Task object is not contravariant hence we cannot return derived objects as the base type.

Rubbing your head around a solution right? 😊 Here’s the best solution I could think of and luckily it’s not that hard to implement. Instead of returning Task<IEnumerable<TProduct>> we can return IAsyncEnumerable<TProduct> because this type is contravariant and of course it can be awaited.


public class KitchenProductService : IProductService<KitchenProductDto>
    {
        public async IAsyncEnumerable<KitchenProductDto> ListProductsAsync()
        {
            // Async calls...
            yield return new KitchenProductDto
            {
                Name = "Domestos",
                PowerLevel = 1,
                Price = 3.44F,
                Quantity = 3
            };
        }
    }

As you can see in the implementation above, we can use yield to return enumerated objects. A method that returns an IAsyncEnumerable type needs to be marked async hence you need to have async calls in there. Then in your calling routine you can use an awaited foreach to loop the results:


await foreach (var _ in productService.ListAsync());

With that being said you can conclude that asynchronous programming model is awesome!

Thanks for reading, I hope you found this article useful and interesting. If you have any suggestions don’t hesitate to contact me. If you found my content useful please consider a small donation. Any support is greatly appreciated! Cheers  😉

afivan

Enthusiast adventurer, software developer with a high sense of creativity, discipline and achievement. I like to travel, I like music and outdoor sports. Because I have a broken ligament, I prefer safer activities like running or biking. In a couple of years, my ambition is to become a good technical lead with entrepreneurial mindset. From a personal point of view, I’d like to establish my own family, so I’ll have lots of things to do, there’s never time to get bored 😂

View all posts by afivan →