Benchmarking two different approaches for collection validation
Tags: net5, benchmarkdotnet, validation, collections
Published: 2021-05-09 14:36:51

I recently came up in a situation at work where I needed to validate a collection of permission's ids (`providedIds`) based on a valid collection of permission's ids (`validIds`). If I find at least one invalid Id, the whole collection fails the validation.

So I used LINQ `Except` extension to check what exists on my `providedIds` that doens't exist in my `validIds`. If there are any, my `providedIds` are not valid.

```csharp
public bool ExceptApproach(IEnumerable<int> permissionIds, IEnumerable<int> validPermissionsIds)
{
    var invalidPermissions = permissionIds.Except(validPermissionsIds);

    return !invalidPermissions.Any();
}
```

But then it occurred to me that all I need to know is if there are a single invalid id in my collection. So I tryed a different approach:

```csharp
public bool AnyApproach(IEnumerable<int> permissionIds, IEnumerable<int> validPermissionsIds)
{
    var atLeastOneInvalidPermission = permissionIds.Any(id => !validPermissionsIds.Contains(id));

    return !atLeastOneInvalidPermission;
}
```

But, is it really better? To answear that question I searched a way to benchmark it and decided to use: [benchmarkdotnet](https://benchmarkdotnet.org/). And I came up with this:

```ini

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.928 (2004/?/20H1)
Intel Core i5-9400F CPU 2.90GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical cores
.NET Core SDK=5.0.101
  [Host]     : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
  DefaultJob : .NET Core 5.0.1 (CoreCLR 5.0.120.57516, CoreFX 5.0.120.57516), X64 RyuJIT
```

| Method         | permissionIds | validPermissionsIds |        Mean |     Error |    StdDev |
| -------------- | ------------- | ------------------- | ----------: | --------: | --------: |
| ExceptApproach | [1,2,3]       | [1,2,3]             |   199.40 ns |  1.181 ns |  1.104 ns |
| ExceptApproach | [1,2,3]       | [2,3,4]             |   162.11 ns |  2.729 ns |  2.553 ns |
| ExceptApproach | Range(1, 100) | Range(1, 100)       | 3,787.92 ns | 59.032 ns | 55.219 ns |
| ExceptApproach | Range(1, 100) | Range(2, 101)       | 2,242.24 ns |  7.806 ns |  6.920 ns |
| AnyApproach    | [1,2,3]       | [1,2,3]             |    83.33 ns |  0.294 ns |  0.260 ns |
| AnyApproach    | [1,2,3]       | [2,3,4]             |    41.22 ns |  0.479 ns |  0.425 ns |
| AnyApproach    | Range(1, 100) | Range(1, 100)       | 2,398.31 ns | 22.912 ns | 21.432 ns |
| AnyApproach    | Range(1, 100) | Range(2, 101)       |    57.68 ns |  0.770 ns |  0.721 ns |

The `AnyApproach` is better than the `ExceptApproach` for small collections. When the collection
[1,2,3] is valid, the `AnyApproach` was about 50% faster. And when the collection is invalid [2,3,4]
the `AnyApproach` was way better (because the first element was already invalid)

For larger collections, the `ExceptApproach` and `AnyApproach` were more close: 3,787.92 ns x 2,398.31 ns and yet, the `AnyApproach` was faster (for a valid collection). For a invalid collection, the `AnyApproach` was significantly faster (again, because the first element was already invalid)

In conclusion, I think using the `AnyApproach` is better than the `ExceptApproach`. I know I should have included more cenarios like: "what if only the last permission Id is invalid?" or "what if only the element at the center of the collection is invalid?", but for the situation I have at work, this is enough to go on.

\*For those wandering, I edited the columns permissionIds and validPermissionsIds because I didn't find a way to include custom names for the values I used. Here's the [full code](https://gist.github.com/Titowisk/5ff2a1c91c4aa506302e9aa781312c65).