Since I have not managed to find something on internet and got some solutions from people I invented my "wheel" inspired by FluentAsserts.
DISCLAIMER: not tested on production but run through some local testing and performance measurements.
The idea behind is to make exceptions which the code/LINQ throws more verbose
// may throw
// Sequence contains more than one matching element
// or
// Sequence contains no matching element
var single = source.Single(x => x.Value == someEnumValue);
In this case only stack trace may help to identify the line when it happened, but stack trace might be lost or overwritten if exception walked through few service (like WCF) layers.
Usually you would make the exceptions more verbose like this
more
var array = source.Where(x => x.Value == someEnumValue ).ToArray();
if(array.Lenght == 0)
{
throw new CustomException($"Sequence of type \"{nameof(SomeType)}\" contains no matching element with the search criteria {nameof(SomeType.Value)}={someEnumValue }")
}
if(array.Lenght > 1)
{
throw new CustomException($"Sequence of type \"{nameof(SomeType)}\" contains more than one matching element with the search criteria {nameof(SomeType.Value)}={searchValue}")
}
var single = array.Single();
we might see that the same exception message patterns can be used so the obvious solution (for me) was to wrap this into some reusable generic code and encapsulate this verbosity. So this example could look like
// throw generic but verbose InvalidOperationException like
// Sequence of type SomeType contains no matching element with the search criteria Value=SomeEnum.Value
var single = source
.AllowVerboseException()
.WithSearchParams(someEnumValue)
.Single(x => x.Value == someEnumValue);
// or CustomException
var single = source
.AllowVerboseException()
.WithSearchParams(someEnumValue)
.IfEmpty().Throws<CustomException>()
.IfMoreThanOne().Throws<CustomException>()
.Single(x => x.Value == someEnumValue);
// or CustomException with custom messages
var single = source
.AllowVerboseException()
.WithSearchParams(someEnumValue)
.IfEmpty().Throws<CustomException>("Found nothing in the source for " + someEnumValue)
.IfMoreThanOne().Throws<CustomException>("Found more than one in the source for " + someEnumValue)
.Single(x => x.Value == someEnumValue);
The fluent assertion solution allows
- dumping items (the sequence has to be enumerated before)
- lazy loading for custom messages (by passing Func instead of string)
- verifying assumptions (full replacements for if-else) without calling Single/First methods (call Verify method instead after all .IfEmpty().Throws and/or .IfMoreThanOne().Throws)
- handling IfAny cases
The code (and unit tests) is available here https://gist.github.com/svonidze/4477529162a138c101e3c022070e9fe3 hovewer I would highlight the main logic
private const int MoreThanOne = 2;
...
public T SingleOrDefault(Func<T, bool> predicate = null)
{
if (predicate != null)
this.sequence = this.sequence.Where(predicate);
return this.Get(Only.Single | Only.Default);
}
...
private T Get(Only only)
{
// the main trip and probably performance downgrade
// the logic takes first 2 elements to then check IfMoreThanOne
// it might be critical in DB queries but might be not
var items = this.sequence.Take(MoreThanOne).ToList();
switch (items.Count)
{
case 1:
case MoreThanOne when only.HasFlag(Only.First):
var first = items.First();
this.Dispose();
return first;
case 0 when only.HasFlag(Only.Default):
this.Dispose();
return default(T);
}
if (this.ifEmptyExceptionFunc == null) this.ifEmptyExceptionFunc = DefaultExceptionFunc;
if (this.ifMoreThanOneExceptionFunc == null) this.ifMoreThanOneExceptionFunc = DefaultExceptionFunc;
this.Verify(() => items.Count);
throw new NotSupportedException("Should not reach this code");
}
private void Verify(Func<int> getItemCount)
{
var itemCount = getItemCount.InitLazy();
ExceptionFunc exceptionFunc = null;
string message = null;
if (this.ifEmptyExceptionFunc != null && itemCount.Value == 0)
{
message = Messages.Elements.NoOne;
exceptionFunc = this.ifEmptyExceptionFunc;
}
else if (this.ifMoreThanOneExceptionFunc != null && itemCount.Value > 1)
{
message = Messages.Elements.MoreThanOne;
exceptionFunc = this.ifMoreThanOneExceptionFunc;
}
else if (this.ifAnyExceptionFunc != null && itemCount.Value > 0)
{
message = Messages.Elements.Some;
exceptionFunc = this.ifAnyExceptionFunc;
}
if (exceptionFunc == null)
return;
message = string.Format(Messages.BeginningFormat, this.typeNameOverride ?? typeof(T).
this.searchCriteria = this.searchCriteria ?? this.searchCriteriaFunc?.Invoke();
if (!string.IsNullOrWhiteSpace(this.searchCriteria))
{
message += $" with the search criteria {this.searchCriteria}";
}
if (this.dumpItemFunc != null)
{
message += ". Items: " + this.dumpItemFunc();
}
try
{
throw exceptionFunc(message);
}
finally
{
this.Dispose();
}
}