Numeric Ids

April 3, 2026 ยท View on GitHub

ScrubNumericIds

Opt in scrubbing of numeric properties ending in Id or ID. Each unique numeric value gets a stable counter based replacement, similar to Guid and Date scrubbing.

The counter is scoped per property name. For properties named Id, the declaring type name is used as the scope (e.g. Customer_1). For properties like CustomerId or OrderId, the full property name is the scope (e.g. CustomerId_1, OrderId_1). This ensures stable output regardless of the actual numeric values, which is particularly useful when working with auto-incrementing database ids.

Fluent

var target = new
{
    Id = 123,
    UserId = 456,
    userID = 789,
    Name = "Test",
    Count = 10
};
return Verify(target)
    .ScrubNumericIds();

snippet source | anchor

Results in the following:

{
  Id: Id_1,
  UserId: UserId_1,
  userID: userID_1,
  Name: Test,
  Count: 10
}

snippet source | anchor

Instance

var target = new
{
    Id = 123,
    UserId = 456,
    Name = "Test"
};
var settings = new VerifySettings();
settings.ScrubNumericIds();
return Verify(target, settings);

snippet source | anchor

Globally

VerifierSettings.ScrubNumericIds();

snippet source | anchor

Parent-child relationships

When verifying object graphs with parent-child relationships, each id property gets its own counter scope. Properties named Id use the declaring type as the scope, while foreign key properties like CustomerId and OrderId use the property name.

public class Customer
{
    public int Id;
    public string? Name;
    public List<Order> Orders = [];
}

public class Order
{
    public int Id;
    public int CustomerId;
    public List<OrderItem> Items = [];
}

public class OrderItem
{
    public long Id;
    public int OrderId;
    public int ProductId;
    public int Quantity;
}

[Fact]
public Task ScrubNumericIdsNamedType()
{
    var target = new List<Customer>
    {
        new()
        {
            Id = 1023,
            Name = "Alice",
            Orders =
            [
                new()
                {
                    Id = 5001,
                    CustomerId = 1023,
                    Items =
                    [
                        new()
                        {
                            Id = 90_001,
                            OrderId = 5001,
                            ProductId = 7,
                            Quantity = 2
                        },
                        new()
                        {
                            Id = 90_002,
                            OrderId = 5001,
                            ProductId = 12,
                            Quantity = 1
                        }
                    ]
                }
            ]
        },
        new()
        {
            Id = 1099,
            Name = "Bob",
            Orders =
            [
                new()
                {
                    Id = 5002,
                    CustomerId = 1099,
                    Items =
                    [
                        new()
                        {
                            Id = 90_003,
                            OrderId = 5002,
                            ProductId = 7,
                            Quantity = 5
                        }
                    ]
                }
            ]
        }
    };
    return Verify(target)
        .ScrubNumericIds();
}

snippet source | anchor

Results in the following:

[
  {
    Id: Customer_1,
    Name: Alice,
    Orders: [
      {
        Id: Order_1,
        CustomerId: CustomerId_1,
        Items: [
          {
            Id: OrderItem_1,
            OrderId: OrderId_1,
            ProductId: ProductId_1,
            Quantity: 2
          },
          {
            Id: OrderItem_2,
            OrderId: OrderId_1,
            ProductId: ProductId_2,
            Quantity: 1
          }
        ]
      }
    ]
  },
  {
    Id: Customer_2,
    Name: Bob,
    Orders: [
      {
        Id: Order_2,
        CustomerId: CustomerId_2,
        Items: [
          {
            Id: OrderItem_3,
            OrderId: OrderId_2,
            ProductId: ProductId_1,
            Quantity: 5
          }
        ]
      }
    ]
  }
]

snippet source | anchor

Note:

  • Id on Customer produces Customer_1, Customer_2
  • Id on Order produces Order_1, Order_2
  • Id on OrderItem produces OrderItem_1, OrderItem_2, OrderItem_3
  • ProductId is scoped independently, so the same product (id 7) is ProductId_1 in both orders
  • Quantity is not scrubbed since it does not end in Id

ScrubMembers approach

For more targeted control, ScrubMembers can be used to check the DeclaringType and the name of the member.

public class NumericIdSample
{
    public class Target : IHasId
    {
        public required int Id { get; init; }
        public required string Name { get; init; }
    }

    [ModuleInitializer]
    public static void Init() =>
        VerifierSettings.ScrubMembers(
            _ => typeof(IHasId).IsAssignableFrom(_.DeclaringType) &&
                 _.Name == "Id");

    [Fact]
    public Task Test()
    {
        var target = new Target
        {
            Id = Random.Shared.Next(),
            Name = "The Name"
        };
        return Verify(target);
    }

    public interface IHasId
    {
        public int Id { get; init; }
    }
}

snippet source | anchor

Produces

{
  Id: {Scrubbed},
  Name: The Name
}

snippet source | anchor