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();
Results in the following:
{
Id: Id_1,
UserId: UserId_1,
userID: userID_1,
Name: Test,
Count: 10
}
Instance
var target = new
{
Id = 123,
UserId = 456,
Name = "Test"
};
var settings = new VerifySettings();
settings.ScrubNumericIds();
return Verify(target, settings);
Globally
VerifierSettings.ScrubNumericIds();
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();
}
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
}
]
}
]
}
]
Note:
IdonCustomerproducesCustomer_1,Customer_2IdonOrderproducesOrder_1,Order_2IdonOrderItemproducesOrderItem_1,OrderItem_2,OrderItem_3ProductIdis scoped independently, so the same product (id 7) isProductId_1in both ordersQuantityis not scrubbed since it does not end inId
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; }
}
}
Produces
{
Id: {Scrubbed},
Name: The Name
}