C# 使用Single和SingleOrDefault抛出更简洁的异常

C# 使用Single和SingleOrDefault抛出更简洁的异常,c#,.net,linq,exception,design-patterns,C#,.net,Linq,Exception,Design Patterns,在IEnumerable上调用Single或SingleOrDefault,并且它有多个结果时,会抛出InvalidOperationException 虽然异常的实际消息非常具有描述性,但编写一个catch只处理Single/SingleOrDefault调用失败的情况是有问题的 public virtual Fee GetFeeByPromoCode(string promoCode) { try { return _fees.SingleOrDefault(

IEnumerable
上调用
Single
SingleOrDefault
,并且它有多个结果时,会抛出
InvalidOperationException

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

public virtual Fee GetFeeByPromoCode(string promoCode)
{
    try
    {
        return _fees.SingleOrDefault(f => f.IsPromoCodeValid(promoCode));
    }
    catch (InvalidOperationException)
    {
        throw new TooManyFeesException();
    }
}
在这种情况下,如果
IsPromoCodeValid
也抛出
invalidoOperationException
,则捕获正在处理的内容变得不明确

我可以检查异常消息,但我希望避免这种情况,因为我发现根据异常消息处理代码很麻烦

我当前的
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();
}
但是,与上面的代码相比,此代码不太明显。此外,与使用
SingleOrDefault
相比,此代码生成的查询效率较低(如果使用启用linq的ORM)

我还可以对第二个示例进行
Take(2)
优化,但这进一步混淆了代码的意图


有没有一种方法可以做到这一点,而不必为
IEnumerable
IQueryable
编写自己的扩展名?

这样可以解决问题吗

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

无效操作异常
相当普遍。任何被访问的属性(甚至堆栈中更深的属性)都可能引发此异常。因此,一种方法是滚动您自己的异常和扩展方法。例如:

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. */ }
另一种方法是使用
TryGet…
-模式,但它不是很干净。即使没有匹配的元素,
TryGetSingle
也会返回true。你可以用EnUM(有效/无效)替换布尔,但是我将把它留给读者看它是否可读。

< P>我把FiST()/Sunle()/SunLeReOffer()视为一种断言。 i、 如果你使用它们,你不想捕捉异常。您的数据存在严重错误,应将其视为严重错误处理

如果模型中有多个结果是正常的,则不要使用异常来验证它


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

我只是想能够修改异常消息,我使用带有普通字符串参数的扩展来完成这一点(请参阅下面的第一个扩展方法)

你的解决方案我使用了一个通用的扩展,使它很好和干净。我传递了要作为泛型类型抛出的异常类型(请参见下面的第二个扩展方法)

我反编译了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);
    }
}
公共静态类linqextensions
{        
//扩展方法1:仅更改“多个”异常的消息
公共静态TSource SingleOrDefault(此IEnumerable源,字符串moreThanOneMatchMessage=“MoreThanOneMatch”)
{
if(source==null)
{
抛出新的ArgumentNullException(“源”);
}
IList list=源作为IList;
如果(列表!=null)
{
开关(list.Count)
{
案例0:
返回默认值(TSource);
案例1:
返回列表[0];
}
}
其他的
{
使用(IEnumerator enumerator=source.GetEnumerator())
{
如果(!enumerator.MoveNext())
{
返回默认值(TSource);
}
TSource current=枚举数.current;
如果(!enumerator.MoveNext())
{
回流;
}
}
}
//我在下面的代码中更改了这一行,并将多条匹配消息作为参数
//它是:throw Error.MoreThanOneElement();换句话说,它是throw new invalidoOperationException(“MoreThanOneMatch”);
抛出新的InvalidOperationException(不止一条匹配消息);
}
//扩展方法2:更改要引发的异常类型和消息
公共静态TSource SingleOrDefault(此IEnumerable源,字符串noElementsMessage=“NoElements”,字符串moreThanOneMatchMessage=“MoreThanOneMatch”)
其中TMoreThanOnceExceptionType:Exception
{
if(source==null)
{
抛出新的ArgumentNullException(“源”);
}
IList list=源作为IList;
如果(列表!=null)
{
开关(list.Count)
{
案例0:
返回默认值(TSource);
案例1:
返回列表[0];
}
}
其他的
{
使用(IEnumerator enumerator=source.GetEnumerator())
{
如果(!enumerator.MoveNext())
{
返回默认值(TSource);
}
TSource current=枚举数.current;
如果(!enumerator.MoveNext())
{
回流;
}
}
}
//将下面的此行更改为引发动态异常类型。
//它是:throw Error.MoreThanOneElement();换句话说,它是throw new invalidoOperationException(“MoreThanOneMatch”);
//是的,有些人认为激活器会减慢代码的速度,如果您使用DI框架并注册您的异常类型,则不应如此
throw(TMoreThanOnceExceptionType)Activator.CreateInstance(typeof(TMoreThanOnceExceptionType),morethaneMatchMessage);
}
}
好的,下面是使用代码:

    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
        }
    }
publicstaticvoidtestmethod(字符串代码)
{
列表代码=新列表();
//添加促销代码
    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
        }
    }