AXMIM AXMIM - 23 days ago 5
C# Question

Ensure deferred execution will be executed only once or else

I ran into a weird issue and I'm wondering what I should do about it.

I have this class that return a

IEnumerable<MyClass>
and it is a deferred execution. Right now, there are two possible consumers. One of them sorts the result.

See the following example :

public class SomeClass
{
public IEnumerable<MyClass> GetMyStuff(Param givenParam)
{
double culmulativeSum = 0;
return myStuff.Where(...)
.OrderBy(...)
.TakeWhile( o =>
{
bool returnValue = culmulativeSum < givenParam.Maximum;
culmulativeSum += o.SomeNumericValue;
return returnValue;
};
}
}


Consumers call the deferred execution only once, but if they were to call it more than that, the result would be wrong as the
culmulativeSum
wouldn't be reset. I've found the issue by inadvertence with unit testing.

The easiest way for me to fix the issue would be to just add
.ToArray()
and get rid of the deferred execution at the cost of a little bit of overhead.

I could also add unit test in consumers class to ensure they call it only once, but that wouldn't prevent any new consumer coded in the future from this potential issue.

Another thing that came to my mind was to make subsequent execution throw.
Something like

return myStuff.Where(...)
.OrderBy(...)
.TakeWhile(...)
.ThrowIfExecutedMoreThan(1);


Obviously this doesn't exist.
Would it be a good idea to implement such thing and how would you do it?

Otherwise, if there is a big pink elephant that I don't see, pointing it out will be appreciated. (I feel there is one because this question is about a very basic scenario :| )

EDIT :



Here is a bad consumer usage example :

public class ConsumerClass
{
public void WhatEverMethod()
{
SomeClass some = new SomeClass();
var stuffs = some.GetMyStuff(param);
var nb = stuffs.Count(); //first deferred execution
var firstOne = stuff.First(); //second deferred execution with the culmulativeSum not reset
}
}

Answer

You can solve the incorrect result issue by simply turning your method into iterator:

double culmulativeSum = 0;
var query = myStuff.Where(...)
       .OrderBy(...)
       .TakeWhile(...);
foreach (var item in query) yield return item;

It can be encapsulated in a simple extension method:

public static class Iterators
{
    public static IEnumerable<T> Lazy<T>(Func<IEnumerable<T>> source)
    {
        foreach (var item in source())
            yield return item;
    }
}

Then all you need to do in such scenarios is to surround the original method body with Iterators.Lazy call, e.g.:

return Iterators.Lazy(() =>
{
    double culmulativeSum = 0;
    return myStuff.Where(...)
           .OrderBy(...)
           .TakeWhile(...);
});
Comments