diff --git a/GalaxyCheck.sln b/GalaxyCheck.sln
index d638ca75..adc6cb1d 100644
--- a/GalaxyCheck.sln
+++ b/GalaxyCheck.sln
@@ -27,6 +27,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GalaxyCheck.Xunit.CodeAnaly
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GalaxyCheck.Tests.V3", "src\GalaxyCheck.Tests.V3\GalaxyCheck.Tests.V3.csproj", "{0C715CD7-6439-49FB-BB80-3D80D24AFE9F}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{AAADE7B3-18E7-42F9-BFA8-5D8F97AE0088}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GalaxyCheck.Samples.EverythingIsSerializable", "samples\GalaxyCheck.Samples.EverythingIsSerializable\GalaxyCheck.Samples.EverythingIsSerializable.csproj", "{9D0FC288-5DFC-4659-A157-1E5DC4CDC137}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -69,6 +73,10 @@ Global
{0C715CD7-6439-49FB-BB80-3D80D24AFE9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C715CD7-6439-49FB-BB80-3D80D24AFE9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C715CD7-6439-49FB-BB80-3D80D24AFE9F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9D0FC288-5DFC-4659-A157-1E5DC4CDC137}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -76,4 +84,7 @@ Global
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {15E48146-1EC3-4A95-8482-8E21408F43E4}
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {9D0FC288-5DFC-4659-A157-1E5DC4CDC137} = {AAADE7B3-18E7-42F9-BFA8-5D8F97AE0088}
+ EndGlobalSection
EndGlobal
diff --git a/samples/GalaxyCheck.Samples.EverythingIsSerializable/Events.cs b/samples/GalaxyCheck.Samples.EverythingIsSerializable/Events.cs
new file mode 100644
index 00000000..9c20d44b
--- /dev/null
+++ b/samples/GalaxyCheck.Samples.EverythingIsSerializable/Events.cs
@@ -0,0 +1,24 @@
+namespace GalaxyCheck.Samples.EverythingIsSerializable;
+
+public static class Events
+{
+ public interface IEvent
+ {
+ string Type { get; }
+ }
+
+ public record InvoiceCreatedEvent(string SupplierId, string CustomerId, string InvoiceId) : IEvent
+ {
+ public string Type => "InvoiceCreatedEvent";
+ }
+
+ public record InvoiceUpdatedEvent(string SupplierId, string InvoiceId) : IEvent
+ {
+ public string Type => "InvoiceUpdatedEvent";
+ }
+
+ public record InvoicePaidEvent(string SupplierId, string InvoiceId, decimal PaidAmount) : IEvent
+ {
+ public string Type => "InvoicePaidEvent";
+ }
+}
diff --git a/samples/GalaxyCheck.Samples.EverythingIsSerializable/GalaxyCheck.Samples.EverythingIsSerializable.csproj b/samples/GalaxyCheck.Samples.EverythingIsSerializable/GalaxyCheck.Samples.EverythingIsSerializable.csproj
new file mode 100644
index 00000000..cdb4ce82
--- /dev/null
+++ b/samples/GalaxyCheck.Samples.EverythingIsSerializable/GalaxyCheck.Samples.EverythingIsSerializable.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/samples/GalaxyCheck.Samples.EverythingIsSerializable/Tests.cs b/samples/GalaxyCheck.Samples.EverythingIsSerializable/Tests.cs
new file mode 100644
index 00000000..e41973b5
--- /dev/null
+++ b/samples/GalaxyCheck.Samples.EverythingIsSerializable/Tests.cs
@@ -0,0 +1,86 @@
+using System.Text.Json;
+using FluentAssertions;
+using Xunit;
+using static GalaxyCheck.Samples.EverythingIsSerializable.Events;
+
+namespace GalaxyCheck.Samples.EverythingIsSerializable;
+
+/**
+ * Demonstrates how to pass "control data" through to properties using Xunit's DataAttributes, and when you might want to do that instead of
+ * generating the value in the property itself.
+ *
+ *
+ * Scenario:
+ *
+ * I have an event-driven system. Commands are sent into the system, and command handlers may emit events. Events are then handled by a separate
+ * worker process, so events need to be serializable so they can be pushed into a queue service. It's not practical to write an end-to-end test
+ * covering every event - we have over 100 events right now, and we're adding more all the time. Because of this, sometimes we push new events that
+ * haven't had their serialization exercised end-to-end. Worse still, sometimes we modify an existing event and completely break a workflow in
+ * production!
+ *
+ * I want to make sure that all events are serializable. And when we add a new event, I want that to be covered by a test without needing to
+ * remember to do it.
+ *
+ * I can use a property for that!
+ */
+public class Tests
+{
+ private static readonly IEnumerable _eventTypes =
+ typeof(IEvent).Assembly.GetTypes().Where(it => it.IsAssignableTo(typeof(IEvent)) && it.IsClass); // Get all event types by reflection
+
+ /**
+ * This is how properties are normally written. All variable values are generated, which means that any test-case generated by the property might
+ * receive any of our events.
+ *
+ * This is fine for most properties. But in this case, we actually want a property foreach event - as the serialization of each event is a
+ * distinct behaviour that we want to ensure is well-tested.
+ *
+ * Properties generate 100 tests by default, and the input to the property varies randomly. If we had more than 100 events, then it'd be
+ * impossible for a single run of the test suite to test each event. Also, each event would not be exercised all that thoroughly.
+ */
+ [Property]
+ public Property AllEventsAreSerializable()
+ {
+ var gen =
+ from eventType in Gen.Element(_eventTypes) // Generate a random event type
+ from ev in Gen.Create(eventType) // Then, generate a random instance of that event type
+ select ev;
+
+ return Property.ForAll(gen, event0 =>
+ {
+ var event1 = SerializeDeserialize(event0);
+ event1.Should().BeEquivalentTo(event0);
+ });
+ }
+
+ public static TheoryData EventTypesTheoryData => _eventTypes.ToTheoryData();
+
+ /**
+ * Alternatively, we can use "control data" to express our tests.
+ *
+ * This treats the "event type" as a control variable, and we can create a property foreach event type. Each property then generates 100 tests (by
+ * default, but we mightn't need that many now!). This is similar to the relationship between Facts and Theories in Xunit - adding control data
+ * (using MemberData, or InlineData) will multiply a property by each control value.
+ *
+ * As a bonus effect, each property that is created by this method are treated as individual "test-cases" in your test explorer. This means that
+ * you can easily run a single test for a single event type, and you can see which event types are covered by your test suite.
+ */
+ [Property]
+ [MemberData(nameof(EventTypesTheoryData))]
+ public Property EventIsSerializable(Type eventType)
+ {
+ var gen = Gen.Create(eventType);
+
+ return Property.ForAll(gen, event0 =>
+ {
+ var event1 = SerializeDeserialize(event0);
+ event1.Should().BeEquivalentTo(event0);
+ });
+ }
+
+ private static object? SerializeDeserialize(object value)
+ {
+ var str = JsonSerializer.Serialize(value);
+ return JsonSerializer.Deserialize(str, value.GetType());
+ }
+}
diff --git a/samples/GalaxyCheck.Samples.EverythingIsSerializable/XunitExtensions.cs b/samples/GalaxyCheck.Samples.EverythingIsSerializable/XunitExtensions.cs
new file mode 100644
index 00000000..7bedb55c
--- /dev/null
+++ b/samples/GalaxyCheck.Samples.EverythingIsSerializable/XunitExtensions.cs
@@ -0,0 +1,18 @@
+using Xunit;
+
+namespace GalaxyCheck.Samples.EverythingIsSerializable;
+
+public static class XunitExtensions
+{
+ public static TheoryData ToTheoryData(this IEnumerable enumerable)
+ {
+ var theoryData = new TheoryData();
+
+ foreach (var item in enumerable)
+ {
+ theoryData.Add(item);
+ }
+
+ return theoryData;
+ }
+}