Interface versus generic constraint with regard to structs

The other day I asked the following on Twitter:

Of course, with my minuscule follow count, I couldn’t really expect an answer (no, I’m not bitter, it’s not that I put any energy into growing that count), so I had to answer the question myself, or rather, confirm what I thought was correct. I wrote the following 2 methods:

public interface IPrimitive<T> { T Value { get; } }
readonly record struct UserId(string Value) : IPrimitive<string>;
readonly record struct ProductId(string Value) : IPrimitive<string>;

static string SwitchOnValue1<T>(T value) where T : IPrimitive<string> =>
  value switch
  {
    UserId { Value: "user-123" } => "It's you, Ralph",
    ProductId { Value: "product-123" } => "It's Ralph's product",
    _ => throw new NotSupportedException()
  };

static string SwitchOnValue2(IPrimitive<string> value) =>
  // identical to the first version

Notice the different argument type and the first version being generic while the second isn’t. With the wonderful BenchmarkDotNet, I set up two benchmarks that would call each version with the same structs…

[Benchmark]
public void CallingTheSwitch1()
{
  var result = SwitchOnValue1(new UserId("user-123"));
  result = SwitchOnValue1(new ProductId("product-123"));
}

[Benchmark]
public void CallingTheSwitch2()
{
  var result = SwitchOnValue2(new UserId("user-123"));
  result = SwitchOnValue2(new ProductId("product-123"));
}

Once the benachmark program did its job, I was rewarded with a great confirmation:

|            Method |      Mean |     Error |    StdDev |  Gen 0 | Allocated |
|------------------ |----------:|----------:|----------:|-------:|----------:|
| CallingTheSwitch1 |  4.977 ns | 0.1570 ns | 0.3172 ns |      - |         - |
| CallingTheSwitch2 | 20.872 ns | 0.4560 ns | 0.5068 ns | 0.0102 |      48 B |

The generic method will not do any boxing, T really is the struct and the fact that you used an interface for constraining the value is irrelevant. In the second case however, using the Interface requires a boxing operation of the struct with allocation and a significant performance penalty.