使用Single和SingleOrDefault可以抛出更简洁的异常

时间:2014-09-29 20:39:52

标签: c# .net linq exception design-patterns

Single上调用SingleOrDefaultIEnumerable<T>时,如果结果不止一个,则会抛出InvalidOperationException

虽然异常的实际消息非常具有描述性,但编写一个只能处理Single / SingleOrDefault调用失败的情况的catch会有问题。

public virtual Fee GetFeeByPromoCode(string promoCode)
{
    try
    {
        return _fees.SingleOrDefault(f => f.IsPromoCodeValid(promoCode));
    }
    catch (InvalidOperationException)
    {
        throw new TooManyFeesException();
    }
}

在这种情况下,如果IsPromoCodeValid也抛出InvalidOperationException,那么捕获的内容就会变得模糊不清。

可以检查异常的消息,但我想避免这种情况,因为我发现根据异常消息处理代码很脏。

我目前替代SingleOrDefault的内容如下:

public virtual Fee GetFeeByPromoCode(string promoCode)
{
    var fees = _fees.Where(f => f.IsPromoCodeValid(promoCode)).ToList();

    if (fees.Count > 1)
    {
        throw new InvalidFeeSetupException();
    }

    return fees.FirstOrDefault();
}

然而,这段代码远不如上面的代码那么明显,此外,这会产生效率较低的查询(如果使用启用linq的ORM)而不是使用SingleOrDefault

我还可以用我的第二个例子做Take(2)来优化它,但这进一步模糊了代码的意图。

如果没有为IEnumerableIQueryable编写自己的扩展程序,有没有办法做到这一点?

4 个答案:

答案 0 :(得分:2)

这可以解决问题吗?

public virtual Fee GetFeeByPromoCode(string promoCode)
{
    try
    {
        return _fees.SingleOrDefault(f =>
            {
                try
                {
                    return f.IsPromoCodeValid(promoCode);
                }
                catch(InvalidOperationException)
                {
                    throw new PromoCodeException();
                }
            });
    }
    catch (InvalidOperationException)
    {
        throw new TooManyFeesException();
    }
}

答案 1 :(得分:2)

我认为First()/ Single()/ SingleOrDefault()是一种断言。

即。如果您使用它们,则不希望捕获异常。您的数据出现问题,应将其作为严重错误处理。

如果模型中有多个结果是正常的,请不要使用例外来验证它。

从这个角度来看,我认为你的Take(2)版本并不那么明显。

答案 2 :(得分:1)

InvalidOperationException相当普遍。访问的任何属性(甚至堆栈中更深的属性)都可能抛出此异常。因此,一种方法是推出自己的异常和扩展方法。例如:

static class EnumerableExtensions
{
    public static TSource ExactlyOneOrZero<TSource>(
        this IEnumerable<TSource> source,
        Func<TSource, bool> predicate)
    {
        if (source == null) { throw new ArgumentNullException("source"); }
        if (predicate == null) { throw new ArgumentNullException("predicate"); }

        IEnumerable<TSource> matchingItems = source.Where(predicate);
        IReadOnlyList<TSource> limitedMatchingItems = matchingItems.Take(2).ToList();

        int matchedItemCount = limitedMatchingItems.Count;

        switch (matchedItemCount)
        {
            case 0: return default(TSource);
            case 1: return limitedMatchingItems[0]; // Or Single() 
            default: throw new TooManyMatchesException();
        }
    }
}

class TooManyMatchesException : Exception { /* Don't forget to implement this properly. */ }

这使您可以保持原始代码清洁:

    public virtual Fee GetFeeByPromoCode(string promoCode)
    {
        try
        {
            return _fees.ExactlyOneOrZero(f => f.IsPromoCodeValid(promoCode));
        }
        catch (TooManyMatchesException)
        {
            throw new TooManyFeesException();
        }
    }

另一种方法是使用TryGet... - 模式,但它不是很干净。即使没有匹配的元素,TryGetSingle也会返回true。您可以用枚举(有效/无效)替换布尔值,但我会将它留给读者,无论这是否可读。

答案 3 :(得分:0)

我只是想能够修改Exception消息,所以我使用了带有普通字符串参数的扩展名(请参见下面的第一个扩展方法)

您的解决方案我使用了一个通用扩展名,使它更美观。我将异常类型传递为泛型类型(请参见下面的第二个扩展方法)。

我反编译了System.Linq.Enumerable库,并复制了它们的代码,只是修改了要抛出的异常消息和异常类型,我们开始了!

新的linq扩展名:

  public static class LinqExtentions
  {        
    // Extension method 1 : Just to change the message for the "more than one" exception
    public static TSource SingleOrDefault<TSource>(this IEnumerable<TSource> source, string moreThanOneMatchMessage = "MoreThanOneMatch")
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }

        IList<TSource> list = source as IList<TSource>;
        if (list != null)
        {
            switch (list.Count)
            {
                case 0:
                    return default(TSource);
                case 1:
                    return list[0];
            }
        }
        else
        {
            using (IEnumerator<TSource> enumerator = source.GetEnumerator())
            {
                if (!enumerator.MoveNext())
                {
                    return default(TSource);
                }
                TSource current = enumerator.Current;
                if (!enumerator.MoveNext())
                {
                    return current;
                }
            }
        }
        // I Changed this line below from their code - moreThanOneMatchMessage as parameter
        // It was : throw Error.MoreThanOneElement(); in other words it was throw new InvalidOperationException("MoreThanOneMatch");
        throw new InvalidOperationException(moreThanOneMatchMessage);
    }

    // Extension method 2 : Change the Exception Type to be thrown and the message
    public static TSource SingleOrDefault<TSource, TMoreThanOnceExceptionType>(this IEnumerable<TSource> source, string noElementsMessage = "NoElements", string moreThanOneMatchMessage = "MoreThanOneMatch")
       where TMoreThanOnceExceptionType : Exception
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }

        IList<TSource> list = source as IList<TSource>;
        if (list != null)
        {
            switch (list.Count)
            {
                case 0:
                    return default(TSource);
                case 1:
                    return list[0];
            }
        }
        else
        {
            using (IEnumerator<TSource> enumerator = source.GetEnumerator())
            {
                if (!enumerator.MoveNext())
                {
                    return default(TSource);
                }
                TSource current = enumerator.Current;
                if (!enumerator.MoveNext())
                {
                    return current;
                }
            }
        }
        // Changes this line below to throw dynamic exception type.
        // It was : throw Error.MoreThanOneElement(); in other words it was throw new InvalidOperationException("MoreThanOneMatch");
        // Yes some believe the Activator can slow down code, If you use a DI Framework and register your exception type this should not be the case
        throw (TMoreThanOnceExceptionType)Activator.CreateInstance(typeof(TMoreThanOnceExceptionType), moreThanOneMatchMessage);
    }
}

好的,这是用法代码:

    public static void TestMethod(string promoCode)
    {
        List<Fee> promoCodes = new List<Fee>();
        // Add promo codes in for example

        // moreThanOneMatchMessage = "Duplicate Promo codes detected" and retur null for no codes
        promoCodes
            .Where(f => f.IsPromoCodeValid(promoCode))
            .SingleOrDefault(moreThanOneMatchMessage: "Duplicate Promo codes detected");

        // OR noElementsMessage = "There is no Promotion codes configured" and moreThanOneMatchMessage = "Duplicate Promo codes!!"
        // This extention was not included in the code section, wanted to keep response small however show this extention, same concept as SingleOrDefault extention
        promoCodes
            .Where(f => f.IsPromoCodeValid(promoCode))
            .Single(moreThanOneMatchMessage: "Duplicate Promo codes!!", noElementsMessage: "There is no Promotion codes configured");

        try
        {
            // Lets Customeze the exception type, TooManyFeesException thrown with a message "Duplicate Promo codes!!"
            // AND if there are no items a InvlaidArgumentException with message "There is no Promotion codes configured"
            promoCodes
                    .Where(f => f.IsPromoCodeValid(promoCode))
                    .SingleOrDefault<Fee, TooManyFeesException>("There is no Promotion codes configured", "Duplicate Promo codes!!");
        }
        catch (TooManyFeesException tmte)
        {
            //catching you specific exception here
        }
    }