Улучшение по умолчанию. NET LINQ плохие исключения с Fluent Asserts на производстве? - PullRequest
0 голосов
/ 17 апреля 2020

Заголовок моего вопроса в общих чертах вызывает общие проблемы. NET исключения в большинстве случаев бессмысленны с помощью stacktrace или дополнительной информации, такой как

Последовательность содержит более одного соответствующего элемента

Мне лень писать операторы if-else каждый раз перед Single, поэтому когда-то go я начал использовать такие активы (как в FluentAsserts) в коде

var singleItem = itemCollection
  .Where(i => i.Id = id)
  .ToArray()
  .ThrowIfEmpty<Item>(searchCriteria: id))
  .ThrowIfMoreThanOne<Item>(searchCriteria: id, dumpItems: true))
  .Single();

, поэтому код завершается ошибкой before Single выполняется с более подробным исключением и даже с включением элементов в исключение. Я не хочу изобретать колесо с этим и хочу использовать некоторую готовую к использованию библиотеку Assertion, чтобы я мог написать более читаемый код, такой как

var singleItem = itemCollection
  .Where(i => i.Id = id)
  .ToArray()
  .Should().BeNotEmpty().And().HasMoreThanOneElement().For(searchCriteria: id)
  //.Otherwise().Throw<MyCustomException>("maybe with some custom message")
  .Single();

, как это делает FluentAssertions, но эта библиотека разработана для тестирования не для производства.

Любая рекомендация для готового к использованию решения?

Возможно, связанные вопросы: Следует ли использовать FluentAssertions в производственном коде? Обработка исключений SingleOrDefault

1 Ответ

0 голосов
/ 06 мая 2020

Так как мне не удалось найти что-то в 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();
    }
}

...