Udostępnij za pośrednictwem


Is IQueryable poisoning your interfaces?

Thanks indirectly to a comment on my previous post, today I read ‘IQueryable is tight coupling’ (disclaimer: his words). I feel like it contains an interesting mixture of truth and panic, and makes a fine discussion topic.

The main interesting truth he mentions is: nobody implements IQueryable fully!

Yes! If you’ve used an ORM, you’ve probably seen it happen many times. You’ve also maybe not seen it happen lately, because you’ve either trained yourself to think in terms of the feature set you know will work, or trained yourself to think in terms of the SQL that will be generated and then work backwards from there. Or just given up on ORMs completely because you think the abstraction is too leaky.

OK now that the truth is mostly done, let the panicking begin!

Panic attack! “Using IQueryable in your interfaces violates the liskov substitutability principle”

Does this seem accurate? What does the principle state?

’If an type A is a subtype of B then objects of type A may be used in stead of objects of type B’ is the headline summary. [Sideline philosophical question: if type B is an interface, do objects of type B even exist?] [Note: what interface is violating the principle here? He’s not saying IQueryable itself violates the interface (how can an interface violate itself?). He’s saying if you use IQueryable in your interface, you’re going to be violating the principle by accident.]

Suppose you’re writing a new interface, there are three ways to use IQueryable in your interface.

a) Provide IQueryable

Ploeh: You did know that query providers are evil because they don’t really allow arbitrary queries, right? LSP violated!

b) Consume IQueryable

void DoStuff (IQueryable dataSource); How can this be evil? Is it bad because you’re encouraging people to implement yet more broken query providers, and violate LSP?!

finally c) Transform IQueryable

IQueryable ModifiedDataSource(IQueryable realDataSource); Ploeh didn’t discuss this one but I feel it’s an important special case.

 

OK, now the panicking has started, I just want to say 'Don’t Panic! (yet)’

The value of LINQ in terms of using it in your interface is not that it makes a promise that ‘you can write any expression’. The beauty is its promise ‘you can write a variety of expressions in normal C#’’ . Or, in other words, you can write more expressive code.

Seriously.

I mean, you are constantly faced with a choice between writing different pieces of code on the provider side, and a corresponding choice of different pieces of code on the consumer side too.

IQueryable world

IQueryable<Package> GetPackages();
var topPackages = GetPackages().Where(p => p.Author.Name == userId)

[or perhaps
IQueryable<Package> GetPackagesByAuthor();
var topPackages = GetPackagesByAuthor().Take(50);]

Non-IQueryable world

IEnumerable<Package> GetPackagesByAuthor(string userId, int count top = 50);
var topPackages = GetPackagesByAuthor(userId, 50);

Which of these interfaces is more expressive? Easy - The IQueryable one is more expressive. If you choose the IQueryable interface, your consuming code can literally do way more stuff, without having to rewrite the interface or the implementation of GetPackages. If you want to change the non-iqueryable GetPackagesByAuthor to return a different result set instead of Packages, good luck!

Which of these interfaces is more fragile? If you want to change the business logic consuming GetPackagesByAuthor to get even packages which have already been deleted/unlisted … oh dear! That’s a breaking interface change.

What are we really scared of here?

Diversion: Language designer diversion

How would a language designer interpret LSP?  Well, LSP is also a guideline on how your type checker might work. I would suggest an alternative LSP phrasing aimed at the language designer: ‘When objects follow a protocol where they are substitutable for each other at runtime, your type system should allow you to model that with an appropriate common base type, so that programmers can write type expressions such that type safety checking is satisfiable.

Yes, LSP really only makes sense when you’re talking about a statically type checked language.

Second diversion: an interesting special case: transforming IQueryable?

What about the case where we write something that transforms IQueryables like IQueryable SelectVersionsForPackages(IQueryable packages);

Logically, we are doing a perfect implementation of IQueryable! That is, we will provide you a perfect implementation of IQueryable in our output, as long as the implementation of the input IQueryable was perfect, and supports arbitrary expressions.

Returning to the debate

I think the LSP argument is really this:

When you do IQueryable, this is bad because you are postponing compile time type safety checking to runtime. Yes, this is the actual consequence of the IQueryable fib .

I think my position is this:

When you do IQueryables, yes, you are fibbing to the type checker, but it is a fib of great convenience because, it lets every part in your system speak the same interoperable language if IQueryable while keeping the type checker happy, and losing no expressiveness.

Let’s see what happens if we scale up the type-checking argument to look at the largest systems that are violators of the LSP – the ORMs themselves: what could any ORM have realistically done instead of implement IQueryable, that would have given you type errors, instead of runtime errors. ?
-They could have written their own new interface to use instead of IQueryable.
-It would have been called IEntityFrameworkQuery or something.
-It would not allow you to pass it Expression<Func<T>> etc – because it doesn’t really support arbitrary expressions. In fact it would probably require you either to manually build the expression trees, using code, in order that you can build type-checked expression trees that are not dangerous Expression<T>.

Only…
It wouldn’t really be an ORM any more then, would it? :-/

And also since you had to use a custom interface, it’s highly unlikely that your code written to use it can interoperate with any other ORM…

[So, just in case you are in doubt, let me point out the answer to my blog title is ‘No!’]

Comments

  • Anonymous
    September 16, 2014
    The ideal fantasy solution to this problem I think would be to extend the language to support "restricted expression types" which only allow an explicitly defined subset of the language, typechecked at compile time.

  • Anonymous
    September 17, 2014
    Great post, but there is one thing where I'd like to object. If you'd do your own interface, like "IEntityFrameworkQuery" or something, you could as well add a Visual Studio plugin that actually extends the static type checker e.g. using Roslyn and then you could raise a compile-time error when there are expressions that are not supported. This is of course a bit like changing the rules and also introducing some problems like the coupled evolution of the framework and plugin but I think it would be useful.