在Single
上调用SingleOrDefault
或IEnumerable<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)
来优化它,但这进一步模糊了代码的意图。
如果没有为IEnumerable
和IQueryable
编写自己的扩展程序,有没有办法做到这一点?
答案 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
}
}