سؤال

My biggest concern with exposing an IQueryable in my business logic is that it could throw an Entity Framework exception in my business logic. I consider that a problem because my business layer either needs to know that I'm using Entity Framework -or- I I have to catch a very generic exception.

Instead, I'd like to create an IQueryable that captures Entity Framework exceptions and converts them to my data layer exception types.

Ultimately, I want my code to look like this:

public IQueryable<Customer> GetCustomers()
{
    var customers = from customer in dbContext.Customers
                    where customer.IsActive
                    select customer;
    return customers.WrapErrors(ex => new DataLayerException("oops", ex);
}

Clients would then be able to add additional LINQ clauses. If an error occurs (the database goes down), the original exception will be wrapped with the DataLayerException.

هل كانت مفيدة؟

المحلول

The problem with @Moho's answer is that it replaces the underlying IQueryable. When you simply wrap the IQueryable, it effects the final Expression that is generated. If you immediately wrap the ISet<T>, it will break calls to Include. Furthermore, it can effect how/when other operations occur. So the solution is actually a bit more involved.

In searching for the solution, I came across this blog: http://blogs.msdn.com/b/alexj/archive/2010/03/01/tip-55-how-to-extend-an-iqueryable-by-wrapping-it.aspx. Unfortunately, this example is a little bit broken, but it was easy to fix (and improve). Below, I post the code I ended up writing.

The first class is an abstract base class that makes it possible to create different types of IQueryable wrappers. LINQ uses IQueryProviders to convert LINQ expressions into executable code. I created an IQueryProvider that simply passes calls to the underlying provider, making it essentially invisible.

public abstract class InterceptingProvider : IQueryProvider
{
    private readonly IQueryProvider provider;

    protected InterceptingProvider(IQueryProvider provider)
    {
        this.provider = provider;
    }

    public virtual IEnumerator<TElement> ExecuteQuery<TElement>(Expression expression)
    {
        IQueryable<TElement> query = provider.CreateQuery<TElement>(expression);
        IEnumerator<TElement> enumerator = query.GetEnumerator();
        return enumerator;
    }

    public virtual IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        IQueryable<TElement> queryable = provider.CreateQuery<TElement>(expression);
        return new InterceptingQuery<TElement>(queryable, this);
    }

    public virtual IQueryable CreateQuery(Expression expression)
    {
        IQueryable queryable = provider.CreateQuery(expression);
        Type elementType = queryable.ElementType;
        Type queryType = typeof(InterceptingQuery<>).MakeGenericType(elementType);
        return (IQueryable)Activator.CreateInstance(queryType, queryable, this);
    }

    public virtual TResult Execute<TResult>(Expression expression)
    {
        return provider.Execute<TResult>(expression);
    }

    public virtual object Execute(Expression expression)
    {
        return provider.Execute(expression);
    }
}

Then I created a class to wrap the actual IQuerable. This class sends any calls to the provider. This way calls to Where, Select, etc. get passed to the underlying provider.

internal class InterceptingQuery<TElement> : IQueryable<TElement>
{
    private readonly IQueryable queryable;
    private readonly InterceptingProvider provider;

    public InterceptingQuery(IQueryable queryable, InterceptingProvider provider)
    {
        this.queryable = queryable;
        this.provider = provider;
    }

    public IQueryable<TElement> Include(string path)
    {
        return new InterceptingQuery<TElement>(queryable.Include(path), provider);
    }

    public IEnumerator<TElement> GetEnumerator()
    {
        Expression expression = queryable.Expression;
        return provider.ExecuteQuery<TElement>(expression);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public Type ElementType
    {
        get { return typeof(TElement); }
    }

    public Expression Expression
    {
        get { return queryable.Expression; }
    }

    public IQueryProvider Provider
    {
        get { return provider; }
    }
}

Notice that this class implements a method called Include. This allows the System.Data.Entity.QueryableExtensions.Include methods to work against the wrapper.

At this point, we just need a subclass of InterceptingProvider that can actually wrap the exceptions that get thrown.

internal class WrappedProvider<TException> : InterceptingProvider
    where TException : Exception
{
    private readonly Func<TException, Exception> wrapper;

    internal WrappedProvider(IQueryProvider provider, Func<TException, Exception> wrapper)
        : base(provider)
    {
        this.wrapper = wrapper;
    }

    public override IEnumerator<TElement> ExecuteQuery<TElement>(Expression expression)
    {
        return Check(() => wrapEnumerator<TElement>(expression), wrapper);
    }

    private IEnumerator<TElement> wrapEnumerator<TElement>(Expression expression)
    {
        IEnumerator<TElement> enumerator = base.ExecuteQuery<TElement>(expression);
        return new WrappedEnumerator<TElement>(enumerator, wrapper);
    }

    public override TResult Execute<TResult>(Expression expression)
    {
        return Check(() => base.Execute<TResult>(expression), wrapper);
    }

    public override object Execute(Expression expression)
    {
        return Check(() => base.Execute(expression), wrapper);
    }

    internal static TResult Check<TResult>(Func<TResult> action, Func<TException, Exception> wrapper)
    {
        try
        {
            return action();
        }
        catch (TException exception)
        {
            throw wrapper(exception);
        }
    }

    private class WrappedEnumerator<TElement> : IEnumerator<TElement>
    {
        private readonly IEnumerator<TElement> enumerator;
        private readonly Func<TException, Exception> wrapper;

        public WrappedEnumerator(IEnumerator<TElement> enumerator, Func<TException, Exception> wrapper)
        {
            this.enumerator = enumerator;
            this.wrapper = wrapper;
        }

        public TElement Current
        {
            get { return enumerator.Current; }
        }

        public void Dispose()
        {
            enumerator.Dispose();
        }

        object IEnumerator.Current
        {
            get { return Current; }
        }

        public bool MoveNext()
        {
            return WrappedProvider<TException>.Check(enumerator.MoveNext, wrapper);
        }

        public void Reset()
        {
            enumerator.Reset();
        }
    }
}

Here I am just overriding the ExecuteQuery and Execute methods. In the case of Execute, the underlying provider is executed immediately and I catch and wrap any exceptions. As for ExecuteQuery, I create an implementation of IEnumerator that wraps exceptions as @Moho suggested.

The only thing missing is the code to actually create the WrappedProvider. I created a simple extension method.

public static class QueryWrappers
{
    public static IQueryable<TElement> Handle<TElement, TException>(this IQueryable<TElement> source, Func<TException, Exception> wrapper)
        where TException : Exception
    {
        return WrappedProvider<TException>.Check(() => handle(source, wrapper), wrapper);
    }

    private static IQueryable<TElement> handle<TElement, TException>(IQueryable<TElement> source, Func<TException, Exception> wrapper)
        where TException : Exception
    {
        var provider = new WrappedProvider<TException>(source.Provider, wrapper);
        return provider.CreateQuery<TElement>(source.Expression);
    }
}

I tested this code in a handful of scenarios to see if I could break something: SQL Server turned off; Single on a table with multiple records; Include-ing a non-existent table; etc. It appeared to work in every case with no unwanted side-effects.

Since the InterceptingProvider class is abstract, it can be used to create other types of invisible IQueryProviders. You can recreate the code in AlexJ's blog with very little work.

The nice thing is that I don't feel weary about exposing IQuerable from my data layer anymore. Now the business layer can mess with the IQueryable all it wants and there's no risk of violating encapsulation because of an Entity Framework exception escaping.

The only thing I like to do is make sure the exception gets wrapped with a message indicating what operation failed; e.g., "An error occurred. Could not retrieve the requested user." I like to wrap the IQueryable in the data layer, but I don't know what the business logic will do to it until later. So I make the business logic responsible for telling the data layer what its intentions are. Passing an error message string to the data layer, just in case, is a bit of a pain, but this is still better than defining a distinct repository method for every possible query and rewriting the same error handling logic 100 times over.

نصائح أخرى

Create a wrapper class that wraps the IQueryable<T> and implements its own IEnumerator<T>, allowing you to convert thrown exceptions on calls that may throw exceptions (such as MoveNext() in my example, which should cover most if not all of your concerns). Example:

class Program
{
    static void Main( string[] args )
    {
        using( var context = new TestContext() )
        {
            for( int i = 0; i < 2; ++i )
            {
                IQueryable<EntityA> query = context.EntityAs.Include( "NoSuchProperty" );

                if( i == 1)
                {
                    query = query.WrapErrors( ex => new ExceptionWrapper( "Test 123", ex ) );
                }

                try
                {
                    var list = query.ToList();
                }
                catch( Exception ex )
                {
                    Console.WriteLine( ex.GetType() );
                    //Console.WriteLine( ex );
                }
            }
        }

        Console.ReadKey();
    }
}

public static class ExtensionMethods
{
    public static IQueryable<T> WrapErrors<T>( this IQueryable<T> query, Func<Exception, Exception> exceptionConversion )
    {
        return new QueryWrapper<T>( query, exceptionConversion );
    }
}

public class QueryWrapper<T> : IQueryable<T>
{
    private IQueryable<T> _query;
    private Func<Exception, Exception> _exceptionConversion;

    public QueryWrapper( IQueryable<T> query, Func<Exception, Exception> exceptionConversion )
    {
        if( null == query )
        {
            throw new ArgumentNullException( "query" );
        }

        _query = query;
        _exceptionConversion = exceptionConversion;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new QueryWrapperEnumerator( _query, _exceptionConversion );
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    public Type ElementType
    {
        get 
        {
            return _query.ElementType;
        }
    }

    public System.Linq.Expressions.Expression Expression
    {
        get 
        {
            return _query.Expression;
        }
    }

    public IQueryProvider Provider
    {
        get 
        {
            return _query.Provider;
        }
    }

    public class QueryWrapperEnumerator : IEnumerator<T>
    {
        IEnumerator<T> _enumerator;
        public Func<Exception, Exception> _exceptionConversion;

        public QueryWrapperEnumerator( IQueryable<T> query, Func<Exception, Exception> exceptionConversion )
        {
            if( null == query )
            {
                throw new ArgumentNullException( "query" );
            }

            _enumerator = query.GetEnumerator();
            _exceptionConversion = exceptionConversion;
        }

        public T Current
        {
            get
            {
                return _enumerator.Current;
            }
        }

        public void Dispose()
        {
            _enumerator.Dispose();
        }

        object System.Collections.IEnumerator.Current
        {
            get 
            {
                return _enumerator.Current;
            }
        }

        public bool MoveNext()
        {
            try
            {
                return _enumerator.MoveNext();
            }
            catch( Exception ex )
            {
                if( null == _exceptionConversion )
                {
                    throw;
                }

                throw _exceptionConversion.Invoke( ex );
            }
        }

        public void Reset()
        {
            _enumerator.Reset();
        }
    }
}

public class EntityA
{
    [System.ComponentModel.DataAnnotations.Schema.DatabaseGenerated( System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.None )]
    public int Id { get; set; }
    public string Name { get; set; }
}

public class TestContext : DbContext
{
    public DbSet<EntityA> EntityAs { get; set; }

    public TestContext()
    {
        Database.SetInitializer( new DropCreateDatabaseAlways<TestContext>() );
    }
}

public class DropCreateDatabaseAlwaysInitializer<T> : DropCreateDatabaseAlways<T> where T : DbContext
{
    protected override void Seed( T context )
    {
    }
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top