Search code examples
.netasp.net-coreodataasp.net-core-webapiiqueryable

How do I ensure an error status code for errors when returning an IQueryable from a Web API method?


I am getting started with OData through the Microsoft.AspNetCore.OData package. The success cases are working, but I noticed that when an IQueryable<T> is successfully returned, but it fails to execute, I do not get a decent error message. Instead, I get a truncated result:

{"@odata.context":"https://localhost:44300/odata/$metadata#Documents","value":[

There is nothing special in my controller:

public class DocumentsController : ODataController
{
    Db.Context Context { get; }

    public DocumentsController(Db.Context context)
    {
        Context = context;
    }

    [EnableQuery]
    public ActionResult<IQueryable<Document>> Get()
    {
        return Context.Documents;
    }
}

The problem is not OData-specific: a plain ControllerBase-derived controller returning an IQueryable<Document> behaves the same way (except with a truncated result of only [). However, the context of OData may rule out some possible solutions.

I understand why this is happening: sending the serialised result has already started; there is no way to go back in time to un-send it and send an error response instead.

I also understand that it is not a real option to prevent in the general case: when sending many items, it's possible that all but the last would be sent without any problems and some exception could be thrown on the last item, so preventing it in the general case would require buffering the complete result.

However, it should be possible to handle at least the cases where an exception is thrown even before the IQueryable<T>'s returns its first result. This would cover the most severe errors where a good exception message is very helpful in debugging: database offline, database schema does not match expectations, etc.

How do I do so?


Solution

  • I can create a custom IEnumerable<T> implementation which wraps another IEnumerable<T>, and immediately calls .GetEnumerator().MoveNext() upon construction, taking care to save the enumerator and MoveNext() response. Then, when my GetEnumerator() is called for the first time, I return a wrapper around the saved enumerator. When its MoveNext() is called for the first time, I return the saved boolean. Other uses just forward to wrapped enumerable and wrapped enumerator. I will spare the code, as the implementation is almost trivial.

    The tricky part is finding the right point to wrap the queryable. It needs to be after any query modifiers in the URL have been applied. There are two main options here:

    1. Forget about EnableQueryAttribute. Instead, as Ihar Yakimush answered, I can take a ODataQueryOptions<Document> parameter and apply the query modifiers directly. Since I'm still in control after the query has been modified, I can then wrap it in my custom enumerable.

    2. Subclass EnableQueryAttribute and override its OnResultExecuting(ResultExecutingContext context) method. Then, I can check if context.Result is ObjectResult, and if so, if objectResult.Value is IEnumerable<T>. If both are true, then I can wrap the result.

      A variant of this approach is to put OnResultExecuting in a separate ActionFilterAttribute derivative, but it makes more sense to me to have them in the same class.

    In both cases, I do have to take care of the situation where I do not get an IQuerable<Document>: I may get an IQueryable<SomeWrapperAround<Document>> instead. Handling that is easy enough. dynamic seemed like it would have made it exceptionally easy: just create object Wrap(object o) => o and object Wrap<T>(IQueryable<T> queryable) => new EnumerableWrapper<T>(queryable) overloads and let the runtime figure out which method to call, but unfortunately SomeWrapperAround can be an OData-internal class, which dynamic doesn't like. It's still not too difficult with manual reflection.