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

Posted

技术标签:

【中文标题】在生产环境中使用 Fluent Asserts 增强默认的 .NET LINQ 不良异常?【英文标题】:Enhance default .NET LINQ poor exceptions with Fluent Asserts on production? 【发布时间】:2020-07-31 02:00:59 【问题描述】:

当标准 .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();

与 FluentAssertions 类似,但该库是为测试而非生产而开发的。

对可用于生产的解决方案有什么建议吗?

也许相关的问题: Should FluentAssertions be used in production code? SingleOrDefault exception handling

【问题讨论】:

当您只使用Single() 时,您肯定希望该集合只有一项。如果您希望集合有多个值或为空,那么您通常会在不引发异常的情况下进行此类验证,因为这是预期行为。 另一方面,写这样的扩展方法比写这个问题要花更少的时间;) “如果您希望集合有多个值或为空,那么您通常会进行此类验证而不会引发异常。”这取决于。通常抛出异常是中断处理的唯一方法,在 .Net 世界中是可以的。在一般情况下,我只想写 Single 并在违反时得到详细的错误。在大多数情况下,我不想编写 if-else 语句。不幸的是,在现实/生产世界中,如果没有堆栈跟踪,标准异常将毫无用处。 在生产中,您通常会显示诸如“糟糕,...”之类的消息,并使用堆栈跟踪记录原始异常。 【参考方案1】:

由于我没有设法在互联网上找到一些东西并从人们那里得到一些解决方案,我发明了受 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();

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

// 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)(在所有 .IfEmpty().Throws 和/或 .IfMoreThanOne().Throws 之后调用 Verify 方法) 处理 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();
    


【讨论】:

以上是关于在生产环境中使用 Fluent Asserts 增强默认的 .NET LINQ 不良异常?的主要内容,如果未能解决你的问题,请参考以下文章

fluent的UDF环境变量设置问题

为什么在NUnit Android Test Runner中Asserts等于0(零)?

如何与多个用户环境中运行的PBS

如何读取android asserts下面已经存在的sqlite数据库

在Fluent中加载UDF出现不能加载的情况,如何解决?

anysis中fluent 与 VS2015 编译 环境配置