Sometimes I need to get a single thing from a collection but I’m not very happy with the Single
extension. It does not differentiate between empty and more then one element and throws the same InvalidOperationException
in both cases. I find it’s an important disadvantage and it helps a lot if you know excactly why and exception was thrown.
I want to replace it with my own extension that I currently name Single2
as throwing two different exceptions.
public static T Single2<T>(this IEnumerable<T> source) { var count = 0; var single = default(T); using (var enumerator = source.GetEnumerator()) { while (enumerator.MoveNext() && count++ < 1) { single = enumerator.Current; } } switch ((SearchResult)count) { case SearchResult.NotFound: throw new EmptySequenceException(); case SearchResult.SingleMatch: return single; default: throw new MoreThenOneElementException(); } } public enum SearchResult { NotFound = 0, SingleMatch = 1, ManyMatches = 2, }
The exceptions are very simple:
public class EmptySequenceException : Exception { } public class MoreThenOneElementException : Exception { }
That’s all. What do you think of this new helper? Is there still room for improvement?
Example
This is one of the methods that already uses the new Single2
. It’s from my ResourceReader
and it searches for an embedded resource in an assembly.
public static string FindName<T>([NotNull] this IResourceReader resources, [NotNull] Expression<Func<string, bool>> predicateExpression) { if (resources == null) throw new ArgumentNullException(nameof(resources)); if (predicateExpression == null) throw new ArgumentNullException(nameof(predicateExpression)); var predicate = predicateExpression.Compile(); try { return resources .GetResourceNames(typeof(T).Assembly) .Where(predicate) .Single2(); } catch (EmptySequenceException innerException) { throw DynamicException.Factory.CreateDynamicException( $ "ResourceNotFound{nameof(Exception)}", $ "Expression {predicateExpression.ToString().QuoteWith("'")} does not match any resource in the {typeof(T).Assembly.GetName().Name.QuoteWith("'")} assembly.", innerException); } catch (MoreThenOneElementException innerException) { throw DynamicException.Factory.CreateDynamicException( $ "MoreThenOneResourceFound{nameof(Exception)}", $ "Expression {predicateExpression.ToString().QuoteWith("'")} matches more then one resource in the {typeof(T).Assembly.GetName().Name.QuoteWith("'")} assembly.", innerException); } }