diff --git a/docs/dates.md b/docs/dates.md
index fb458fb98c..a3b17e3c59 100644
--- a/docs/dates.md
+++ b/docs/dates.md
@@ -29,7 +29,7 @@ var target = new DateTimeTarget
await Verify(target);
```
-snippet source | anchor
+snippet source | anchor
Results in the following:
@@ -70,7 +70,7 @@ settings.DontScrubDateTimes();
return Verify(target, settings);
```
-snippet source | anchor
+snippet source | anchor
@@ -87,7 +87,7 @@ var target = new
return Verify(target)
.DontScrubDateTimes();
```
-snippet source | anchor
+snippet source | anchor
@@ -100,7 +100,7 @@ return Verify(target)
public static void ModuleInitializer() =>
VerifierSettings.DontScrubDateTimes();
```
-snippet source | anchor
+snippet source | anchor
@@ -124,7 +124,7 @@ settings.DisableDateCounting();
return Verify(target, settings);
```
-snippet source | anchor
+snippet source | anchor
@@ -141,7 +141,7 @@ var target = new
return Verify(target)
.DisableDateCounting();
```
-snippet source | anchor
+snippet source | anchor
@@ -154,7 +154,7 @@ return Verify(target)
public static void ModuleInitializer() =>
VerifierSettings.DisableDateCounting();
```
-snippet source | anchor
+snippet source | anchor
@@ -201,7 +201,7 @@ public Task ScrubInlineDateTimesInstance()
settings);
}
```
-snippet source | anchor
+snippet source | anchor
@@ -215,7 +215,7 @@ public Task ScrubInlineDateTimesFluent() =>
Verify("content 2020-10-20 content")
.ScrubInlineDateTimes("yyyy-MM-dd");
```
-snippet source | anchor
+snippet source | anchor
@@ -231,7 +231,7 @@ public static class ModuleInitializer
VerifierSettings.ScrubInlineDateTimes("yyyy-MM-dd");
}
```
-snippet source | anchor
+snippet source | anchor
@@ -252,7 +252,7 @@ settings.AddNamedDateTime(new(2030, 1, 2), "instanceNamedDateTime");
settings.AddNamedDateTimeOffset(new DateTime(2030, 1, 2), "instanceNamedTimeOffset");
await Verify(target, settings);
```
-snippet source | anchor
+snippet source | anchor
@@ -267,7 +267,7 @@ await Verify(target)
.AddNamedDateTime(new(2030, 1, 2), "instanceNamedDateTime")
.AddNamedDateTimeOffset(new DateTime(2030, 1, 2), "instanceNamedTimeOffset");
```
-snippet source | anchor
+snippet source | anchor
@@ -285,7 +285,7 @@ public static void NamedDatesAndTimesGlobal()
VerifierSettings.AddNamedDateTimeOffset(new(new(2030, 1, 1)), "namedDateTimeOffset");
}
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/guids.md b/docs/guids.md
index b3e05eb398..526743de76 100644
--- a/docs/guids.md
+++ b/docs/guids.md
@@ -23,7 +23,7 @@ var target = new GuidTarget
await Verify(target);
```
-snippet source | anchor
+snippet source | anchor
Results in the following:
@@ -79,7 +79,7 @@ await Verify(target)
```cs
VerifierSettings.DontScrubGuids();
```
-snippet source | anchor
+snippet source | anchor
@@ -103,7 +103,7 @@ public Task ScrubInlineGuidsInstance()
settings);
}
```
-snippet source | anchor
+snippet source | anchor
@@ -117,7 +117,7 @@ public Task ScrubInlineGuidsFluent() =>
Verify("content 651ad409-fc30-4b12-a47e-616d3f953e4c content")
.ScrubInlineGuids();
```
-snippet source | anchor
+snippet source | anchor
@@ -133,7 +133,7 @@ public static class ModuleInitializer
VerifierSettings.ScrubInlineGuids();
}
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/mdsource/numeric-ids.source.md b/docs/mdsource/numeric-ids.source.md
index 44b6848fe9..4bcdf4b48d 100644
--- a/docs/mdsource/numeric-ids.source.md
+++ b/docs/mdsource/numeric-ids.source.md
@@ -1,9 +1,57 @@
# Numeric Ids
-Numbers are not scrubbed. Sometimes it is helpful to scrub numeric Ids. This can be done using `ScrubMembers` and checking the DeclaringType and the name of the member.
+
+## 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](guids.md) and [Date](dates.md) 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
+
+snippet: ScrubNumericIdsFluent
+
+Results in the following:
+
+snippet: SerializationTests.ScrubNumericIdsFluent.verified.txt
+
+
+### Instance
+
+snippet: ScrubNumericIdsInstance
+
+
+### Globally
+
+snippet: ScrubNumericIdsGlobal
+
+
+### 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.
+
+snippet: ScrubNumericIdsRelationships
+
+Results in the following:
+
+snippet: SerializationTests.ScrubNumericIdsNamedType.verified.txt
+
+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.
snippet: NumericIdSample
Produces
-snippet: NumericIdSample.Test.verified.txt
\ No newline at end of file
+snippet: NumericIdSample.Test.verified.txt
diff --git a/docs/members-throw.md b/docs/members-throw.md
index 820cf8c72b..1324480da5 100644
--- a/docs/members-throw.md
+++ b/docs/members-throw.md
@@ -35,7 +35,7 @@ public Task CustomExceptionPropFluent()
.IgnoreMembersThatThrow();
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -45,7 +45,7 @@ Or globally:
```cs
VerifierSettings.IgnoreMembersThatThrow();
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -82,7 +82,7 @@ public Task ExceptionMessagePropFluent()
.IgnoreMembersThatThrow(_ => _.Message == "Ignore");
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -92,7 +92,7 @@ Or globally:
```cs
VerifierSettings.IgnoreMembersThatThrow(_ => _.Message == "Ignore");
```
-snippet source | anchor
+snippet source | anchor
Result:
diff --git a/docs/named-tuples.md b/docs/named-tuples.md
index 252c06df19..2e36544341 100644
--- a/docs/named-tuples.md
+++ b/docs/named-tuples.md
@@ -19,7 +19,7 @@ Given a method that returns a named tuple:
static (bool Member1, string Member2, string Member3) MethodWithNamedTuple() =>
(true, "A", "B");
```
-snippet source | anchor
+snippet source | anchor
Can be verified:
@@ -29,7 +29,7 @@ Can be verified:
```cs
await VerifyTuple(() => MethodWithNamedTuple());
```
-snippet source | anchor
+snippet source | anchor
Resulting in:
diff --git a/docs/numeric-ids.md b/docs/numeric-ids.md
index 92ab4cd10a..700e1c67af 100644
--- a/docs/numeric-ids.md
+++ b/docs/numeric-ids.md
@@ -7,7 +7,240 @@ To change this file edit the source file and then run MarkdownSnippets.
# Numeric Ids
-Numbers are not scrubbed. Sometimes it is helpful to scrub numeric Ids. This can be done using `ScrubMembers` and checking the DeclaringType and the name of the member.
+
+## 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](guids.md) and [Date](dates.md) 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
+
+
+
+```cs
+var target = new
+{
+ Id = 123,
+ UserId = 456,
+ userID = 789,
+ Name = "Test",
+ Count = 10
+};
+return Verify(target)
+ .ScrubNumericIds();
+```
+snippet source | anchor
+
+
+Results in the following:
+
+
+
+```txt
+{
+ Id: Id_1,
+ UserId: UserId_1,
+ userID: userID_1,
+ Name: Test,
+ Count: 10
+}
+```
+snippet source | anchor
+
+
+
+### Instance
+
+
+
+```cs
+var target = new
+{
+ Id = 123,
+ UserId = 456,
+ Name = "Test"
+};
+var settings = new VerifySettings();
+settings.ScrubNumericIds();
+return Verify(target, settings);
+```
+snippet source | anchor
+
+
+
+### Globally
+
+
+
+```cs
+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.
+
+
+
+```cs
+public class Customer
+{
+ public int Id;
+ public string? Name;
+ public List Orders = [];
+}
+
+public class Order
+{
+ public int Id;
+ public int CustomerId;
+ public List Items = [];
+}
+
+public class OrderItem
+{
+ public long Id;
+ public int OrderId;
+ public int ProductId;
+ public int Quantity;
+}
+
+[Fact]
+public Task ScrubNumericIdsNamedType()
+{
+ var target = new List
+ {
+ 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:
+
+
+
+```txt
+[
+ {
+ 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.
@@ -31,7 +264,7 @@ public class NumericIdSample
{
var target = new Target
{
- Id = new Random().Next(),
+ Id = Random.Shared.Next(),
Name = "The Name"
};
return Verify(target);
diff --git a/docs/obsolete-members.md b/docs/obsolete-members.md
index 016093211f..c42a33f815 100644
--- a/docs/obsolete-members.md
+++ b/docs/obsolete-members.md
@@ -31,7 +31,7 @@ public Task WithObsoleteProp()
return Verify(target);
}
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -79,7 +79,7 @@ public Task WithObsoletePropIncludedFluent()
.IncludeObsoletes();
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -89,7 +89,7 @@ Or globally:
```cs
VerifierSettings.IncludeObsoletes();
```
-snippet source | anchor
+snippet source | anchor
Result:
diff --git a/docs/scrubbers.md b/docs/scrubbers.md
index beab920dd0..9ea9c2573a 100644
--- a/docs/scrubbers.md
+++ b/docs/scrubbers.md
@@ -59,7 +59,7 @@ For example remove lines containing `text`:
```cs
verifySettings.ScrubLines(line => line.Contains("text"));
```
-snippet source | anchor
+snippet source | anchor
@@ -74,7 +74,7 @@ For example remove lines containing `text1` or `text2`
```cs
verifySettings.ScrubLinesContaining("text1", "text2");
```
-snippet source | anchor
+snippet source | anchor
Case insensitive by default (`StringComparison.OrdinalIgnoreCase`).
@@ -86,7 +86,7 @@ Case insensitive by default (`StringComparison.OrdinalIgnoreCase`).
```cs
verifySettings.ScrubLinesContaining(StringComparison.Ordinal, "text1", "text2");
```
-snippet source | anchor
+snippet source | anchor
@@ -101,7 +101,7 @@ For example converts lines to upper case:
```cs
verifySettings.ScrubLinesWithReplace(line => line.ToUpper());
```
-snippet source | anchor
+snippet source | anchor
@@ -114,7 +114,7 @@ Replaces `Environment.MachineName` with `TheMachineName`.
```cs
verifySettings.ScrubMachineName();
```
-snippet source | anchor
+snippet source | anchor
@@ -127,7 +127,7 @@ Replaces `Environment.UserName` with `TheUserName`.
```cs
verifySettings.ScrubUserName();
```
-snippet source | anchor
+snippet source | anchor
diff --git a/docs/serializer-settings.md b/docs/serializer-settings.md
index 6346c6c0d1..ecf1e3a376 100644
--- a/docs/serializer-settings.md
+++ b/docs/serializer-settings.md
@@ -132,7 +132,7 @@ var settings = new JsonSerializerSettings
DefaultValueHandling = DefaultValueHandling.Ignore
};
```
-snippet source | anchor
+snippet source | anchor
@@ -204,7 +204,7 @@ To disable this behavior globally use:
```cs
VerifierSettings.DontIgnoreEmptyCollections();
```
-snippet source | anchor
+snippet source | anchor
@@ -495,7 +495,7 @@ public Task ScopedSerializerFluent()
.AddExtraSettings(_ => _.TypeNameHandling = TypeNameHandling.All);
}
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -623,7 +623,7 @@ public Task IgnoreTypeFluent()
.IgnoreMembersWithType();
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -633,7 +633,7 @@ Or globally:
```cs
VerifierSettings.IgnoreMembersWithType();
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -770,7 +770,7 @@ public Task ScrubTypeFluent()
.ScrubMembersWithType();
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -780,7 +780,7 @@ Or globally:
```cs
VerifierSettings.ScrubMembersWithType();
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -859,7 +859,7 @@ public Task AddIgnoreInstanceFluent()
.IgnoreInstance(_ => _.Property == "Ignore");
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -869,7 +869,7 @@ Or globally:
```cs
VerifierSettings.IgnoreInstance(_ => _.Property == "Ignore");
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -931,7 +931,7 @@ public Task AddScrubInstanceFluent()
.ScrubInstance(_ => _.Property == "Ignore");
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -941,7 +941,7 @@ Or globally:
```cs
VerifierSettings.ScrubInstance(_ => _.Property == "Ignore");
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -1004,7 +1004,7 @@ public Task IgnoreMemberByExpressionFluent()
_ => _.PropertyThatThrows);
}
```
-snippet source | anchor
+snippet source | anchor
Or globally
@@ -1019,7 +1019,7 @@ VerifierSettings.IgnoreMembers(
_ => _.GetOnlyProperty,
_ => _.PropertyThatThrows);
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -1079,7 +1079,7 @@ public Task ScrubMemberByExpressionFluent()
_ => _.PropertyThatThrows);
}
```
-snippet source | anchor
+snippet source | anchor
Or globally
@@ -1094,7 +1094,7 @@ VerifierSettings.ScrubMembers(
_ => _.GetOnlyProperty,
_ => _.PropertyThatThrows);
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -1173,7 +1173,7 @@ public Task IgnoreMemberByNameFluent()
.IgnoreMember(_ => _.PropertyThatThrows);
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -1193,7 +1193,7 @@ VerifierSettings.IgnoreMember("Field");
// For a specific type with expression
VerifierSettings.IgnoreMember(_ => _.PropertyThatThrows);
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -1268,7 +1268,7 @@ public Task ScrubMemberByNameFluent()
.ScrubMember(_ => _.PropertyThatThrows);
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -1288,7 +1288,7 @@ VerifierSettings.ScrubMember("Field");
// For a specific type with expression
VerifierSettings.ScrubMember(_ => _.PropertyThatThrows);
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -1381,7 +1381,7 @@ public Task IgnoreDictionaryByPredicate()
return Verify(target, settings);
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -1393,7 +1393,7 @@ VerifierSettings.IgnoreMembers(
_=>_.DeclaringType == typeof(TargetClass) &&
_.Name == "Proprty");
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -1482,7 +1482,7 @@ public Task ScrubDictionaryByPredicate()
return Verify(target, settings);
}
```
-snippet source | anchor
+snippet source | anchor
Or globally:
@@ -1494,7 +1494,7 @@ VerifierSettings.ScrubMembers(
_=>_.DeclaringType == typeof(TargetClass) &&
_.Name == "Proprty");
```
-snippet source | anchor
+snippet source | anchor
Result:
@@ -1548,7 +1548,7 @@ public Task MemberConverterByExpression()
return Verify(input);
}
```
-snippet source | anchor
+snippet source | anchor
diff --git a/src/StrictJsonTests/SerializationTests.ScrubNumericIdsFluent.verified.json b/src/StrictJsonTests/SerializationTests.ScrubNumericIdsFluent.verified.json
new file mode 100644
index 0000000000..8477d5f819
--- /dev/null
+++ b/src/StrictJsonTests/SerializationTests.ScrubNumericIdsFluent.verified.json
@@ -0,0 +1,7 @@
+{
+ "Id": "Id_1",
+ "UserId": "UserId_1",
+ "userID": "userID_1",
+ "Name": "Test",
+ "Count": 10
+}
\ No newline at end of file
diff --git a/src/StrictJsonTests/SerializationTests.ScrubNumericIdsInstance.verified.json b/src/StrictJsonTests/SerializationTests.ScrubNumericIdsInstance.verified.json
new file mode 100644
index 0000000000..a53f5be4a5
--- /dev/null
+++ b/src/StrictJsonTests/SerializationTests.ScrubNumericIdsInstance.verified.json
@@ -0,0 +1,5 @@
+{
+ "Id": "Id_1",
+ "UserId": "UserId_1",
+ "Name": "Test"
+}
\ No newline at end of file
diff --git a/src/StrictJsonTests/SerializationTests.ScrubNumericIdsLong.verified.json b/src/StrictJsonTests/SerializationTests.ScrubNumericIdsLong.verified.json
new file mode 100644
index 0000000000..c0ce827928
--- /dev/null
+++ b/src/StrictJsonTests/SerializationTests.ScrubNumericIdsLong.verified.json
@@ -0,0 +1,4 @@
+{
+ "Id": "Id_1",
+ "Name": "Test"
+}
\ No newline at end of file
diff --git a/src/StrictJsonTests/SerializationTests.ScrubNumericIdsNamedType.verified.json b/src/StrictJsonTests/SerializationTests.ScrubNumericIdsNamedType.verified.json
new file mode 100644
index 0000000000..79fe14a86f
--- /dev/null
+++ b/src/StrictJsonTests/SerializationTests.ScrubNumericIdsNamedType.verified.json
@@ -0,0 +1,44 @@
+[
+ {
+ "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
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/src/StrictJsonTests/SerializationTests.ScrubNumericIdsSameValues.verified.json b/src/StrictJsonTests/SerializationTests.ScrubNumericIdsSameValues.verified.json
new file mode 100644
index 0000000000..9f549a89db
--- /dev/null
+++ b/src/StrictJsonTests/SerializationTests.ScrubNumericIdsSameValues.verified.json
@@ -0,0 +1,5 @@
+{
+ "OrderId": "OrderId_1",
+ "ParentId": "ParentId_1",
+ "OtherId": "OtherId_1"
+}
\ No newline at end of file
diff --git a/src/Verify.Tests/Naming/CounterBuilder.cs b/src/Verify.Tests/Naming/CounterBuilder.cs
index c535824bb6..9b84ad7555 100644
--- a/src/Verify.Tests/Naming/CounterBuilder.cs
+++ b/src/Verify.Tests/Naming/CounterBuilder.cs
@@ -5,6 +5,7 @@ public static Counter Empty() =>
true,
true,
true,
+ false,
#if NET6_0_OR_GREATER
[],
[],
diff --git a/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsFluent.verified.txt b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsFluent.verified.txt
new file mode 100644
index 0000000000..321fdfb12b
--- /dev/null
+++ b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsFluent.verified.txt
@@ -0,0 +1,7 @@
+{
+ Id: Id_1,
+ UserId: UserId_1,
+ userID: userID_1,
+ Name: Test,
+ Count: 10
+}
\ No newline at end of file
diff --git a/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsInstance.verified.txt b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsInstance.verified.txt
new file mode 100644
index 0000000000..ca66501037
--- /dev/null
+++ b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsInstance.verified.txt
@@ -0,0 +1,5 @@
+{
+ Id: Id_1,
+ UserId: UserId_1,
+ Name: Test
+}
\ No newline at end of file
diff --git a/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsLong.verified.txt b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsLong.verified.txt
new file mode 100644
index 0000000000..66f0e183c5
--- /dev/null
+++ b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsLong.verified.txt
@@ -0,0 +1,4 @@
+{
+ Id: Id_1,
+ Name: Test
+}
\ No newline at end of file
diff --git a/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsNamedType.verified.txt b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsNamedType.verified.txt
new file mode 100644
index 0000000000..52a86651dd
--- /dev/null
+++ b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsNamedType.verified.txt
@@ -0,0 +1,44 @@
+[
+ {
+ 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
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsSameValues.verified.txt b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsSameValues.verified.txt
new file mode 100644
index 0000000000..7386722fc0
--- /dev/null
+++ b/src/Verify.Tests/Serialization/SerializationTests.ScrubNumericIdsSameValues.verified.txt
@@ -0,0 +1,5 @@
+{
+ OrderId: OrderId_1,
+ ParentId: ParentId_1,
+ OtherId: OtherId_1
+}
\ No newline at end of file
diff --git a/src/Verify.Tests/Serialization/SerializationTests.cs b/src/Verify.Tests/Serialization/SerializationTests.cs
index 3a6bfc81db..c8c5439adb 100644
--- a/src/Verify.Tests/Serialization/SerializationTests.cs
+++ b/src/Verify.Tests/Serialization/SerializationTests.cs
@@ -732,6 +732,166 @@ public Task GuidScrubbingDisabledNested() =>
})
.DontScrubGuids();
+ [Fact]
+ public Task ScrubNumericIdsFluent()
+ {
+ #region ScrubNumericIdsFluent
+
+ var target = new
+ {
+ Id = 123,
+ UserId = 456,
+ userID = 789,
+ Name = "Test",
+ Count = 10
+ };
+ return Verify(target)
+ .ScrubNumericIds();
+
+ #endregion
+ }
+
+ [Fact]
+ public Task ScrubNumericIdsInstance()
+ {
+ #region ScrubNumericIdsInstance
+
+ var target = new
+ {
+ Id = 123,
+ UserId = 456,
+ Name = "Test"
+ };
+ var settings = new VerifySettings();
+ settings.ScrubNumericIds();
+ return Verify(target, settings);
+
+ #endregion
+ }
+
+ // ReSharper disable once UnusedMember.Local
+ static void ScrubNumericIdsGlobal() =>
+
+ #region ScrubNumericIdsGlobal
+
+ VerifierSettings.ScrubNumericIds();
+
+ #endregion
+
+ [Fact]
+ public Task ScrubNumericIdsSameValues()
+ {
+ var target = new
+ {
+ OrderId = 42,
+ ParentId = 42,
+ OtherId = 99
+ };
+ return Verify(target)
+ .ScrubNumericIds();
+ }
+
+ [Fact]
+ public Task ScrubNumericIdsLong()
+ {
+ var target = new
+ {
+ Id = 999999999999L,
+ Name = "Test"
+ };
+ return Verify(target)
+ .ScrubNumericIds();
+ }
+
+ #region ScrubNumericIdsRelationships
+
+ public class Customer
+ {
+ public int Id;
+ public string? Name;
+ public List Orders = [];
+ }
+
+ public class Order
+ {
+ public int Id;
+ public int CustomerId;
+ public List Items = [];
+ }
+
+ public class OrderItem
+ {
+ public long Id;
+ public int OrderId;
+ public int ProductId;
+ public int Quantity;
+ }
+
+ [Fact]
+ public Task ScrubNumericIdsNamedType()
+ {
+ var target = new List
+ {
+ 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();
+ }
+
+ #endregion
+
[Fact]
public Task ScrubberWithBadNewLine() =>
Verify("a")
diff --git a/src/Verify.Tests/Snippets/NumericIdSample.cs b/src/Verify.Tests/Snippets/NumericIdSample.cs
index 77532664d9..98ba5e76ff 100644
--- a/src/Verify.Tests/Snippets/NumericIdSample.cs
+++ b/src/Verify.Tests/Snippets/NumericIdSample.cs
@@ -19,7 +19,7 @@ public Task Test()
{
var target = new Target
{
- Id = new Random().Next(),
+ Id = Random.Shared.Next(),
Name = "The Name"
};
return Verify(target);
@@ -31,4 +31,4 @@ public interface IHasId
}
}
-#endregion
\ No newline at end of file
+#endregion
diff --git a/src/Verify.Tests/Wizard/WizardGen.cs b/src/Verify.Tests/Wizard/WizardGen.cs
index 1a4294b116..16b1ce8fd6 100644
--- a/src/Verify.Tests/Wizard/WizardGen.cs
+++ b/src/Verify.Tests/Wizard/WizardGen.cs
@@ -35,7 +35,7 @@ public async Task Run()
File.Delete(sourceFile);
await WriteLf(sourceFile, builder);
var process = Process.Start("mdsnippets", repoRoot);
- await process!.WaitForExitAsync();
+ await process.WaitForExitAsync();
}
static Task WriteLf(string path, StringBuilder builder) =>
diff --git a/src/Verify/Counter.cs b/src/Verify/Counter.cs
index 956acdbf7a..1a2c2ce34e 100644
--- a/src/Verify/Counter.cs
+++ b/src/Verify/Counter.cs
@@ -13,6 +13,7 @@ public partial class Counter :
public bool DateCounting { get; }
public bool ScrubDateTimes { get; }
public bool ScrubGuids { get; }
+ public bool ScrubNumericIds { get; }
static AsyncLocal local = new();
internal bool TryGetNamed(object value, [NotNullWhen(true)] out string? result)
@@ -103,6 +104,7 @@ public Counter(
bool dateCounting,
bool scrubDateTimes,
bool scrubGuids,
+ bool scrubNumericIds,
#if NET6_0_OR_GREATER
Dictionary namedDates,
Dictionary