1

I have a Blazor server-side app where I use DynamicComponent on a page to create and add components at runtime. To do this I have the following:

A set of interfaces:

public interface IGetData
{
    Task GetDataAsync(string condition);
}

public interface IGetDataA : IGetData
{
    Task GetDataAsync(CriteriaA criteria);
}

public interface IGetDataB : IGetData
{
    Task GetDataAsync(CriteriaB criteria);
}

And then in the page (after creating the components):

public async Task RefreshAsync()
{
    var itemsA = items.Where(x => x.Ref.Instance is IGetDataA)
        .Select(x => x.Ref.Instance)
        .Cast<IGetDataA>();

    await Parallel.ForEachAsync(itemsA, async (item, token) =>
    {
        await item.GetDataAsync(criteriaA);
    });

    var itemsB = items.Where(x => x.Ref.Instance is IGetDataB)
        .Select(x => x.Ref.Instance)
        .Cast<IGetDataB>();

    await Parallel.ForEachAsync(itemsB, async (item, token) =>
    {
        await item.GetDataAsync(criteriaB);
    });
}

Is it possible to unify the above into just one cast and one loop?

UPDATE

I've made changes to improve the code. I've simplified the interfaces as follows:

public interface IGetData<T> 
{
    Task GetDataAsync(string condition);
    Task GetDataAsync(T data);
}

And the code:

bool IsGetData(object o) => o.GetType().GetInterfaces().Any(x =>
    x.IsGenericType &&
    x.GetGenericTypeDefinition() == typeof(IGetData<>));

var list = items.Where(x => IsGetData(x.Ref.Instance))
    .Select(x => x.Ref.Instance);

await Parallel.ForEachAsync(list, async (item, token) =>
{
    if (item is ItemA)
        await (item as ItemA).GetDataAsync(criteriaA);
    else if (item is ItemB)
        await (item as ItemB).GetDataAsync(criteriaB);
});

Now I have an extra helper method and also i need to use is and as. Is there a way to simplify the code further?

1
  • 2
    1) In your current code, you run ll the item.GetDataAsync(criteriaA) actions, then you run all the item.GetDataAsync(criteriaB) actions. Is this necessary, or could you interleave them? 2) IGetDataA and IGetDataB are both interfaces so theoretically a concrete type could implement both. Your current code will run both GetDataAsync(CriteriaA) and GetDataAsync(criteriaB). Do you need to retain that functionality? Commented Jun 14 at 5:38

4 Answers 4

2

Now I have an extra helper method and also i need to use is and as. Is there a way to simplify the code further?

Yes. The solution is your own realization that the interface can be generalized:

public interface IGetData<T> 
{
Task GetDataAsync(string condition);
Task GetDataAsync(T data);
}

This associates the type IGetData<T> with the method parameter type T data via the generic parameter T. This means that in your latest code, each line is specifying the type T twice.

The following is "case A", because

  • the cast is to ItemA aka IGetData<CriteriaA>,
  • the parameter value criteriaA is of type CriteriaA
if (item is ItemA)
    await (item as ItemA).GetDataAsync(criteriaA); 

To get rid of this redundancy, instead of trying to cast the callee item to the correct concrete type, keep that as generic and deduce the type from the parameter value that you have to pass in anyway. From you original "duplicated" code:

public async Task RefreshAsync()
{
   var itemsA = items.Where(x => x.Ref.Instance is IGetDataA)
       .Select(x => x.Ref.Instance)
       .Cast<IGetDataA>();

   await Parallel.ForEachAsync(itemsA, async (item, token) =>
   {
       await item.GetDataAsync(criteriaA);
   });

   var itemsB = items.Where(x => x.Ref.Instance is IGetDataB)
       .Select(x => x.Ref.Instance)
       .Cast<IGetDataB>();

   await Parallel.ForEachAsync(itemsB, async (item, token) =>
   {
       await item.GetDataAsync(criteriaB);
   });
}

realize that the code blocks differ in two places that can be refactored:

  • IGetDataA/IGetDataB - introduce a type parameter
  • criteriaA/criteriaB - introduce a regular method parameter (of the above parameter type)

You can then introduce a e.g. local function that can handle both cases.

public async Task RefreshAsync()
{
    await GetDataForAllItemsAsync(criteriaA);
    await GetDataForAllItemsAsync(criteriaB);

    async Task GetDataForAllItemsAsync<T>(T data)
    {
        var itemsB = items.Where(x => x.Ref.Instance is IGetData<T>)
                    .Select(x => x.Ref.Instance)
                    .Cast<IGetData<T>>();

        await Parallel.ForEachAsync(itemsB, async (item, token) =>
        {
            await item.GetDataAsync(data);
        });
    }
}

If either all your items only ever implement one IGetData<T> or if they implement multiple that are ok to be invoked in parallel, you could consider going further by making the entire class generic (if your other logic in that class allows that)

Sign up to request clarification or add additional context in comments.

Comments

2

You can use a pattern matching variable to merge the is and as, like item is ItemA a.

Furthermore, the helper function and Where seems extraneous. Just pass it through to Parallel.ForEachAsync and deall with it in the pattern check.

To avoid the setup costs of the task state machine, in this case you can safely elide the await.

var list = items.Select(x => x.Ref.Instance);

await Parallel.ForEachAsync(list, (item, token) =>
{
    if (item is ItemA a)
        return new ValueTask(a.GetDataAsync(criteriaA));
    else if (item is ItemB b)
        return new ValueTask(b.GetDataAsync(criteriaB));
    else
        return ValueTask.CompletedTask;
});

Or using the new switch expression, and a more concise expression lambda:

await Parallel.ForEachAsync(list, (item, token) =>
    item switch
    {
        ItemA a => new ValueTask(a.GetDataAsync(criteriaA)),
        ItemB b => new ValueTask(b.GetDataAsync(criteriaB)),
        _ => ValueTask.CompletedTask,
    });

Comments

0

If this is LINQ to Objects and not LINQ to DB, you can swap Where and Select and navigate to the instance only once:

var list = items
    .Select(x => x.Ref.Instance)
    .Where(i => IsGetData(i));

Comments

0

You could encapsulate casting and looping in separate method, for example such as:

public static class EnumerableExtensions
{
    public static async Task ForEachOfTypeAsync<T, TSelected>(
        this IEnumerable<T> enumerable, 
        Func<T, object> selector,
        Func<TSelected, Task> action)
    {
        var cast = enumerable
            .Select(x => selector(x))
            .Where(x => x is TSelected)
            .Cast<TSelected>();

        await Parallel.ForEachAsync(cast, async (item, token) =>
        {
            await action(item);
        });
    }
}

Then calling it would simplify the entire code just to

items.ForEachOfTypeAsync<IGetData, IGetDataA>(
    x => x.Ref.Instance, 
    x => x.GetDataAsync(criteriaA));

items.ForEachOfTypeAsync<IGetData, IGetDataB>(
    x => x.Ref.Instance,
    x => x.GetDataAsync(criteriaB));

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.