在生产环境中使用Fluent Asserts增强默认.NET LINQ较差的异常



当标准.NET异常与堆栈竞争或等额外信息基本上没有意义时,我的问题标题会带来一般问题

序列包含多个匹配元素

我太懒了,每次在Single之前都不写if-else语句,这就是为什么一段时间前我开始在代码中使用这样的资产(如FluentAsserts(构造

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

因此,在执行Single之前,代码会失败,出现更详细的异常,甚至在异常中包含项目。我不想发明这样的轮子,我想使用一些现成的生产断言库,这样我就可以编写更可读的代码,比如

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

就像FluentAssessments一样,但这个库是为测试而开发的,而不是为生产而开发的。

有关于生产解决方案的建议吗?

也许是相关问题:FluentAssessments应该在生产代码中使用吗?SingleOrDefault异常处理

由于我在互联网上找不到什么东西,也没有从人们那里得到一些解决方案,我受FluentAssers的启发发明了我的"轮子"。

免责声明:未在生产上进行测试,但通过一些本地测试和性能测量运行。

背后的想法是使代码/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();

我们可能会看到,可以使用相同的异常消息模式,所以(对我来说(显而易见的解决方案是将其封装到一些可重用的通用代码中,并封装这种冗长的代码。所以这个例子看起来像

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

流畅的断言解决方案允许

  • 转储项目(之前必须枚举顺序(
  • 自定义消息的延迟加载(通过传递Func而不是字符串(
  • 在不调用Single/First方法的情况下验证假设(if-else的完全替换((毕竟调用Verify方法.IfEmpty((.Sthrows和/或.IfMoreThanOne((.Strows(
  • 处理IfAny案件

此处提供代码(和单元测试(https://gist.github.com/svonidze/4477529162a138c101e3c022070e9fe3我会强调的主要逻辑

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

最新更新