Так как мне не удалось найти что-то в inte rnet и получить некоторые решения от людей, я изобрел свое «колесо», вдохновленное FluentAsserts.
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: не тестировалось на производстве но выполните некоторые локальные тесты и измерения производительности.
Идея состоит в том, чтобы сделать исключения, которые код / LINQ генерирует более многословно
// may throw
// Sequence contains more than one matching element
// or
// Sequence contains no matching element
var single = source.Single(x => x.Value == someEnumValue);
В этом случае только трассировка стека может помочь идентифицировать строка, когда это произошло, но трассировка стека могла бы быть потеряна или перезаписана, если исключение прошло через несколько уровней обслуживания (например, WCF). Обычно вы бы делали исключения более подробными, как это:
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();
. Мы могли бы видеть, что можно использовать те же шаблоны сообщений об исключениях, поэтому очевидным решением (для меня) было заключить их в некий повторно используемый обобщенный код * 1034. * код и инкапсулировать это многословие. Таким образом, этот пример может выглядеть следующим образом:
// 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);
Свободное решение для подтверждения позволяет
- выгрузить элементы (последовательность должна быть перечислена ранее)
- отложенная загрузка для пользовательских сообщения (путем передачи Fun c вместо строки)
- проверка предположений (полная замена для if-else) без вызова методов Single / First (вызовите метод Verify вместо всех .IfEmpty (). Выдает и / или .IfMoreThanOne (). Броски)
- обработка случаев IfAny
Код (и модульные тесты) доступен здесь https://gist.github.com/svonidze/4477529162a138c101e3c022070e9fe3 Однако, я бы выделил основной logi c
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();
}
}