When invoking Single
or SingleOrDefault
on an IEnumerable<T>
, and it has more than one result, it throws InvalidOperationException
.
While the actual message of the exception is very descriptive, it is problematic to write a catch that will only handle the cases where the Single
/ SingleOrDefault
calls fail.
public virtual Fee GetFeeByPromoCode(string promoCode)
{
try
{
return _fees.SingleOrDefault(f => f.IsPromoCodeValid(promoCode));
}
catch (InvalidOperationException)
{
throw new TooManyFeesException();
}
}
In this scenario, if IsPromoCodeValid
also throws an InvalidOperationException
, then it becomes ambiguous as to what the catch is handling.
I could inspect the message of the exception, but I would like to avoid that as I find it dirty to handle code depending on a message of an exception.
My current alternative to the SingleOrDefault
looks like the following:
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();
}
However, this code is a lot less obvious than the code above, in addition, this generates a less efficient query (if using a linq-enabled ORM) than using SingleOrDefault
.
I could also do a Take(2)
with my second example to optimize it a bit, but this further obfuscates the intent of the code.
Is there a way to do this without writing my own extension for both IEnumerable
and IQueryable
?
SingleOrDefault<TSource>(IEnumerable<TSource>) Returns the only element of a sequence, or a default value if the sequence is empty; this method throws an exception if there is more than one element in the sequence.
C# SingleorDefault() Method The method returns a single specific element of a sequence. If the element is not present in the sequence, then the default value is returned.
For all simple-types, the default value is the value produced by a bit pattern of all zeros: For sbyte, byte, short, ushort, int, uint, long, and ulong, the default value is 0 .
May this solve the problem?
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();
}
}
I consider First() / Single() / SingleOrDefault() as a kind of Assert.
i.e. If you use them you don't want to catch the exception. Something is very wrong with your data and it should be handled as a critical error.
If multiple results is normal in your model, don't use exceptions to verify it.
From that perspective I don't think your Take(2) version is less obvious.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With