Search code examples
c#.netienumerableiqueryablecode-contracts

Can I do Contract.Ensures on IQueryable and IEnumerable?


Lets see this code:

public IQueryable<Category> GetAllActive()
{
    Contract.Ensures(Contract.Result<IQueryable<Category>>() != null);
    return dataSource.GetCategories(T => T.IsActive);
}

There is a small question. Is it okay with code contracts write this:

public IQueryable<Category> GetAllActive()
{
    Contract.Ensures(Contract.Result<IQueryable<Category>>() != null);
    Contract.Ensures(Contract.Result<IQueryable<Category>>().All(T => T.IsActive));
    return dataSource.GetCategories(T => T.IsActive);
}

Or not?

Will such a thing produce the unnecessary sequence enumeration, which is highly undesireble?


Solution

  • Assuming you're using the binary rewriter and enforcing contracts at runtime, you should not do this.

    When you use Contract.Ensures like so:

    Contract.Ensures(Contract.Result<T>() <operation>);
    return expression;
    

    It's transformed and the operation is lifted into something like the following:

    T expression = <expression>;
    
    // Perform checks on expression.
    if (!(expression <operation>) <throw appropriate exception>;
    
    // Return value.
    return expression;
    

    In this case, it means that your code unwinds to:

    IQueryable<Category> temp = dataSource.GetCategories(T => T.IsActive);
    
    // Perform checks.
    if (!(temp != null)) throw new Exception();
    if (!temp.All(T => T.IsActive)) throw new Exception();
    
    // Return values.
    return temp;
    

    In this case, your IQueryable<Category> will be iterated through and will cause another request to be sent to the underlying data store.

    Depending on the operation, you might not notice it, but the query is definitely going to be executed twice and that's bad for performance.

    For things of this nature, you should check at the point of consumption of the IQueryable<T> whether or not you have any elements (you can do it with a boolean flag that is set as you foreach through it, or when you materialize it).

    However, if you are not running the binary rewriter on your compiled assemblies, then Contract.Result<T> contracts of this nature cannot be enforced; in this case, it's a noop and probably shouldn't be there, as it's not doing anything.