-1

My question title brings general problems when standard .NET exceptions are mostly meaningless with stacktrace or extra information like

Sequence contains more than one matching element

I am too lazy to write if-else statements every time before Single that is why some time ago I started to use such asset (like in FluentAsserts) constructions in code

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

so the code fails before Single is executed with more verbose exception and even with including items in the exception. I do not want to invent the wheel with this and want to use some ready-for-production Assertion library so I could write more readable code like

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();

like FluentAssertions does, but this library is developed for testing not for production.

Any recommendation for ready-for-production solution?

Perhaps related questions: Should FluentAssertions be used in production code? SingleOrDefault exception handling

svonidze
  • 151
  • 8
  • When you are using just `Single()` you expect that for sure that collection has only one item. If you expect collection to have multiple values or be empty, then you do such validation usually without throwing an exception, because this is **expected** behaviour. – Fabio Apr 18 '20 at 05:18
  • On other hand to write such extension methods will take less time than writing this question ;) – Fabio Apr 18 '20 at 05:19
  • "If you expect collection to have multiple values or be empty, then you do such validation usually without throwing an exception." It depends. Usually throwing of exception is the only way to break processing, and it is ok in .Net world. In general case I just want to write Single and get verbose error if violates. I do not want to write if-else statements in most cases. Unfortunately the standard exceptions are useless without stacktrace in the real/production world. – svonidze Apr 18 '20 at 06:07
  • In production you usually display message such "Oops, ..." and log original exception with stack trace. – Fabio Apr 18 '20 at 06:07

1 Answers1

-1

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();
    }
}

svonidze
  • 151
  • 8