可以包装例外

时间:2013-12-17 00:36:33

标签: linq entity-framework exception-handling iqueryable

我在业务逻辑中暴露IQueryable的最大问题是它可能会在我的业务逻辑中抛出实体框架异常。我认为这是一个问题,因为我的业务层需要知道我正在使用实体框架 - 或 - 我必须捕获一个非常通用的异常。

相反,我想创建一个IQueryable来捕获实体框架异常并将它们转换为我的数据层异常类型。

最终,我希望我的代码看起来像这样:

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

然后客户端可以添加其他LINQ子句。如果发生错误(数据库关闭),原始异常将被DataLayerException包装。

2 个答案:

答案 0 :(得分:2)

@ Moho回答的问题是它取代了潜在的IQueryable。当您简单地包裹IQueryable时,它会影响生成的最终Expression。如果您立即打包ISet<T>,则会中断对Include的调用。此外,它可以影响其他操作发生的方式/时间。所以解决方案实际上更复杂一些。

在搜索解决方案时,我遇到了这个博客:http://blogs.msdn.com/b/alexj/archive/2010/03/01/tip-55-how-to-extend-an-iqueryable-by-wrapping-it.aspx。不幸的是,这个例子有点破,但很容易修复(和改进)。下面,我发布了我最后编写的代码。

第一个类是一个抽象基类,可以创建不同类型的IQueryable包装器。 LINQ使用IQueryProvider将LINQ表达式转换为可执行代码。我创建了一个IQueryProvider,它只是将调用传递给底层提供者,使其基本上不可见。

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);
    }
}

然后我创建了一个包装实际IQuerable的类。此类将任何调用发送给提供程序。这种方式调用WhereSelect等等,传递给底层提供程序。

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; }
    }
}

请注意,此类实现了一个名为Include的方法。这允许System.Data.Entity.QueryableExtensions.Include方法对包装器起作用。

此时,我们只需要一个InterceptingProvider的子类,它实际上可以包装抛出的异常。

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();
        }
    }
}

这里我只是重写了ExecuteQueryExecute方法。在Execute的情况下,底层提供程序立即执行,我捕获并包装任何异常。至于ExecuteQuery,我创建了一个IEnumerator的实现,它包含了@Moho建议的异常。

唯一缺少的是实际创建WrappedProvider的代码。我创建了一个简单的扩展方法。

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);
    }
}

我在一些场景中测试了这段代码,看看我是否可以破解:SQL Server关闭; Single在包含多个记录的表上; Include - 一张不存在的牌桌;它似乎在每种情况下都有效,没有不必要的副作用。

由于InterceptingProvider类是抽象的,因此可以用它来创建其他类型的不可见IQueryProvider。您可以在很少的工作中重新创建AlexJ博客中的代码。

好消息是,我不再厌倦从我的数据层中暴露IQuerable。现在,业务层可能会混淆IQueryable它想要的所有内容,并且由于实体框架异常转义,不存在违反封装的风险。

我唯一要做的就是确保异常被包装,并显示一条消息,指出哪些操作失败;例如,“发生错误。无法检索请求的用户”。我喜欢将IQueryable包装在数据层中,但我不知道业务逻辑将在以后做什么。所以我让业务逻辑负责告诉数据层它的意图是什么。将错误消息字符串传递给数据层,以防万一,这有点痛苦,但这仍然比为每个可能的查询定义一个不同的存储库方法并重写相同的错误处理逻辑100次更好。

答案 1 :(得分:1)

创建一个包装类,它包装IQueryable<T>并实现自己的IEnumerator<T>,允许您转换可能引发异常的调用的抛出异常(例如我的示例中的MoveNext(),应该涵盖大部分(如果不是全部)您的担忧)。例如:

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 )
    {
    }
}