diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..2307ba4
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AltaSoft.Choice.Generator/Executor.cs b/src/AltaSoft.Choice.Generator/Executor.cs
index 924f829..985bab1 100644
--- a/src/AltaSoft.Choice.Generator/Executor.cs
+++ b/src/AltaSoft.Choice.Generator/Executor.cs
@@ -44,7 +44,16 @@ internal static void Execute(in ImmutableArray typesToGenerat
DeclaredAccessibility: Accessibility.Public
}).ToList();
- var sb = Process(typeSymbol, partialProperties);
+ // Get ordinary (non-partial) required properties
+ var ordinaryRequiredProperties = typeSymbol.GetMembersOfType().Where(x
+ => x is
+ {
+ IsStatic: false, IsWriteOnly: false, CanBeReferencedByName: true, IsPartialDefinition: false,
+ DeclaredAccessibility: Accessibility.Public,
+ IsRequired: true
+ }).ToList();
+
+ var sb = Process(typeSymbol, partialProperties, ordinaryRequiredProperties);
context.AddSource($"{typeSymbol.Name}.g.cs", sb.ToString());
}
@@ -55,7 +64,7 @@ internal static void Execute(in ImmutableArray typesToGenerat
}
}
- private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List properties)
+ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List properties, List ordinaryRequiredProperties)
{
var processedProperties = properties.ConvertAll(ProcessProperty);
var usingStatements = processedProperties.Select(x => x.Namespace).Concat(s_baseNamespaces);
@@ -67,6 +76,10 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List instance and sets its value using the specified {prop.TypeSymbol.GetCrefForType()}.");
- sb.AppendParamDescription("value", "The value to assign to the created choice instance.");
- sb.Append($"public static {typeFullName} CreateAs").Append(prop.Name).Append("(")
- .Append(prop.TypeName).Append(" value) => new () { ").Append(prop.Name).AppendLine(" = value };");
+ var requiredParamNames = new HashSet(
+ ordinaryRequiredProperties.Select(x => x.Name.ToCamelCase()),
+ StringComparer.Ordinal);
+
+ var choiceParamName = "value";
+ if (requiredParamNames.Contains(choiceParamName))
+ {
+ choiceParamName = "choiceValue";
+ var suffix = 1;
+ while (requiredParamNames.Contains(choiceParamName))
+ {
+ choiceParamName = $"choiceValue{suffix}";
+ suffix++;
+ }
+ }
+
+ // Add parameter descriptions for required ordinary properties
+ foreach (var reqProp in ordinaryRequiredProperties)
+ {
+ var paramName = reqProp.Name.ToCamelCase();
+ sb.AppendParamDescription(paramName, $"The value for the required property {reqProp.Name}.");
+ }
+
+ sb.AppendParamDescription(choiceParamName, "The value to assign to the created choice instance.");
+
+ // Build method signature with required property parameters
+ sb.Append($"public static {typeFullName} CreateAs").Append(prop.Name).Append("(");
+
+ // Add required ordinary properties as parameters first
+ for (var i = 0; i < ordinaryRequiredProperties.Count; i++)
+ {
+ var reqProp = ordinaryRequiredProperties[i];
+ var paramName = reqProp.Name.ToCamelCase();
+ var propType = reqProp.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ sb.Append(propType).Append(" ").Append(paramName).Append(", ");
+ }
+
+ // Add the choice property parameter
+ sb.Append(prop.TypeName).Append(" ").Append(choiceParamName).Append(") => new () { ");
+
+ // Initialize required ordinary properties
+ foreach (var reqProp in ordinaryRequiredProperties)
+ {
+ var paramName = reqProp.Name.ToCamelCase();
+ sb.Append(reqProp.Name).Append(" = ").Append(paramName).Append(", ");
+ }
+
+ // Initialize the choice property
+ sb.Append(prop.Name).Append(" = ").Append(choiceParamName).AppendLine(" };");
sb.NewLine();
}
@@ -191,8 +251,17 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List
/// Returns the C# keyword representation of a property's level,
/// optionally omitting the keyword for public if is true.
diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs
index 099ef2e..7052900 100644
--- a/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs
+++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs
@@ -226,6 +226,112 @@ public sealed partial class MixedAttributeChoice
});
}
+ [Fact]
+ public Task ChoiceTypeShouldGenerateWithRequiredOrdinaryProperties()
+ {
+ const string source =
+ """
+ using System;
+ using AltaSoft.Choice;
+
+ namespace TestNamespace
+ {
+ [Choice]
+ public sealed partial class OrderChoice
+ {
+ ///
+ /// Required ordinary property - Order ID
+ ///
+ public required string OrderId { get; set; }
+
+ ///
+ /// Required ordinary property - Customer Name
+ ///
+ public required string CustomerName { get; set; }
+
+ ///
+ /// Choice property - Express delivery
+ ///
+ public partial ExpressDelivery? Express { get; set; }
+
+ ///
+ /// Choice property - Standard delivery
+ ///
+ public partial StandardDelivery? Standard { get; set; }
+ }
+
+ public sealed class ExpressDelivery
+ {
+ public DateTime DeliveryDate { get; set; }
+ public decimal SurchargeAmount { get; set; }
+ }
+
+ public sealed class StandardDelivery
+ {
+ public int DeliveryDays { get; set; }
+ public decimal ShippingCost { get; set; }
+ }
+ }
+ """;
+
+ return TestHelper.Verify(source, (_, x, _) =>
+ {
+ Assert.Single(x);
+ });
+ }
+
+ [Fact]
+ public Task ChoiceTypeShouldGenerateWithRequiredPropertyNamedValue()
+ {
+ const string source =
+ """
+ using System;
+ using AltaSoft.Choice;
+
+ namespace TestNamespace
+ {
+ [Choice]
+ public sealed partial class ConfigChoice
+ {
+ ///
+ /// Required property named "Value" - tests parameter conflict resolution
+ ///
+ public required string Value { get; set; }
+
+ ///
+ /// Required property named "ChoiceValue" - tests fallback parameter conflict resolution
+ ///
+ public required string ChoiceValue { get; set; }
+
+ ///
+ /// Choice property - Option A
+ ///
+ public partial OptionA? OptionA { get; set; }
+
+ ///
+ /// Choice property - Option B
+ ///
+ public partial OptionB? OptionB { get; set; }
+ }
+
+ public sealed class OptionA
+ {
+ public string Data { get; set; }
+ }
+
+ public sealed class OptionB
+ {
+ public int Count { get; set; }
+ }
+ }
+ """;
+
+ return TestHelper.Verify(source, (_, x, _) =>
+ {
+ Assert.Single(x);
+ });
+ }
+
public static class TestHelper
{
internal static Task Verify(string source, Action, List, GeneratorDriver>? additionalChecks = null)
diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateAllMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateAllMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs
index 0b26c26..a09218c 100644
--- a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateAllMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs
+++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateAllMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs
@@ -24,6 +24,10 @@ namespace TestNamespace;
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
#pragma warning disable CS0628 // New protected member declared in sealed type
+#pragma warning disable CS0618 // Type or member is obsolete
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable IDE0290 // Use primary constructor
public sealed partial class Authorisation1Choice
{
diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateDocumentationCorrectly_ForArrayInChoice#ArrayInTypeChoice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateDocumentationCorrectly_ForArrayInChoice#ArrayInTypeChoice.g.verified.cs
index 909e2d9..660f141 100644
--- a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateDocumentationCorrectly_ForArrayInChoice#ArrayInTypeChoice.g.verified.cs
+++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateDocumentationCorrectly_ForArrayInChoice#ArrayInTypeChoice.g.verified.cs
@@ -24,6 +24,10 @@ namespace TestNamespace;
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
#pragma warning disable CS0628 // New protected member declared in sealed type
+#pragma warning disable CS0618 // Type or member is obsolete
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable IDE0290 // Use primary constructor
public sealed partial class ArrayInTypeChoice
{
diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement#MixedAttributeChoice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement#MixedAttributeChoice.g.verified.cs
index 8574cc6..98a7ce1 100644
--- a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement#MixedAttributeChoice.g.verified.cs
+++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement#MixedAttributeChoice.g.verified.cs
@@ -24,6 +24,10 @@ namespace TestNamespace;
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
#pragma warning disable CS0628 // New protected member declared in sealed type
+#pragma warning disable CS0618 // Type or member is obsolete
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable IDE0290 // Use primary constructor
public sealed partial class MixedAttributeChoice
{
diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredOrdinaryProperties#OrderChoice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredOrdinaryProperties#OrderChoice.g.verified.cs
new file mode 100644
index 0000000..8d8a29c
--- /dev/null
+++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredOrdinaryProperties#OrderChoice.g.verified.cs
@@ -0,0 +1,181 @@
+//HintName: OrderChoice.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by 'AltaSoft Choice.Generator'.
+// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+using TestNamespace;
+using AltaSoft.Choice;
+using System;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Xml;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace TestNamespace;
+
+#pragma warning disable CS8774 // Member must have a non-null value when exiting.
+#pragma warning disable CS0628 // New protected member declared in sealed type
+#pragma warning disable CS0618 // Type or member is obsolete
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable IDE0290 // Use primary constructor
+
+public sealed partial class OrderChoice
+{
+ ///
+ /// Constructor for Serialization/Deserialization
+ ///
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ public OrderChoice()
+ {
+ }
+
+ ///
+ /// Choice enum
+ ///
+ [JsonIgnore]
+ [XmlIgnore]
+ [ChoiceTypeProperty]
+ public ChoiceOf ChoiceType { get; private set; }
+
+ private TestNamespace.ExpressDelivery? _express;
+
+ ///
+ /// Choice property - Express delivery
+ ///
+ [DisallowNull]
+ [XmlElement("Express")]
+ [ChoiceProperty]
+ public partial TestNamespace.ExpressDelivery? Express
+ {
+ get => _express;
+ set
+ {
+ _express = value ?? throw new InvalidOperationException("Choice value cannot be null");
+ _standard = null;
+ ChoiceType = ChoiceOf.Express;
+ }
+ }
+
+ private TestNamespace.StandardDelivery? _standard;
+
+ ///
+ /// Choice property - Standard delivery
+ ///
+ [DisallowNull]
+ [XmlElement("Standard")]
+ [ChoiceProperty]
+ public partial TestNamespace.StandardDelivery? Standard
+ {
+ get => _standard;
+ set
+ {
+ _standard = value ?? throw new InvalidOperationException("Choice value cannot be null");
+ _express = null;
+ ChoiceType = ChoiceOf.Standard;
+ }
+ }
+
+
+ ///
+ /// Creates a new instance and sets its value using the specified .
+ ///
+ /// The value for the required property OrderId.
+ /// The value for the required property CustomerName.
+ /// The value to assign to the created choice instance.
+ public static TestNamespace.OrderChoice CreateAsExpress(string orderId, string customerName, TestNamespace.ExpressDelivery value) => new () { OrderId = orderId, CustomerName = customerName, Express = value };
+
+ ///
+ /// Creates a new instance and sets its value using the specified .
+ ///
+ /// The value for the required property OrderId.
+ /// The value for the required property CustomerName.
+ /// The value to assign to the created choice instance.
+ public static TestNamespace.OrderChoice CreateAsStandard(string orderId, string customerName, TestNamespace.StandardDelivery value) => new () { OrderId = orderId, CustomerName = customerName, Standard = value };
+
+ ///
+ /// Applies the appropriate function based on the current choice type
+ ///
+ /// The return type of the provided match functions
+ /// Function to invoke if the choice is a value
+ /// Function to invoke if the choice is a value
+ public TResult Match(
+ Func matchExpress,
+ Func matchStandard)
+ {
+ return ChoiceType switch
+ {
+ ChoiceOf.Express => matchExpress(Express!),
+ ChoiceOf.Standard => matchStandard(Standard!),
+ _ => throw new InvalidOperationException($"Invalid ChoiceType. '{ChoiceType}'")
+ };
+ }
+
+ ///
+ /// Applies the appropriate Action based on the current choice type
+ ///
+ /// Action to invoke if the choice is a value
+ /// Action to invoke if the choice is a value
+ public void Switch(
+ Action matchExpress,
+ Action matchStandard)
+ {
+ switch (ChoiceType)
+ {
+ case ChoiceOf.Express:
+ matchExpress(Express!);
+ return;
+
+ case ChoiceOf.Standard:
+ matchStandard(Standard!);
+ return;
+
+ default:
+ throw new XmlException($"Invalid ChoiceType. '{ChoiceType}'");
+ }
+ }
+
+
+ ///
+ /// Determines whether the property should be serialized.
+ ///
+ ///
+ /// true if is the active choice; otherwise, false.
+ ///
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ public bool ShouldSerializeExpress() => ChoiceType == ChoiceOf.Express;
+
+ ///
+ /// Determines whether the property should be serialized.
+ ///
+ ///
+ /// true if is the active choice; otherwise, false.
+ ///
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ public bool ShouldSerializeStandard() => ChoiceType == ChoiceOf.Standard;
+
+ ///
+ /// Choice enumeration
+ ///
+ [XmlType("ChoiceOf.OrderChoice")]
+ public enum ChoiceOf
+ {
+ ///
+ /// Choice property - Express delivery
+ ///
+ Express,
+ ///
+ /// Choice property - Standard delivery
+ ///
+ Standard,
+ }
+}
diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredPropertyNamedValue#ConfigChoice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredPropertyNamedValue#ConfigChoice.g.verified.cs
new file mode 100644
index 0000000..450cda4
--- /dev/null
+++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredPropertyNamedValue#ConfigChoice.g.verified.cs
@@ -0,0 +1,181 @@
+//HintName: ConfigChoice.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by 'AltaSoft Choice.Generator'.
+// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+using TestNamespace;
+using AltaSoft.Choice;
+using System;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Xml;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace TestNamespace;
+
+#pragma warning disable CS8774 // Member must have a non-null value when exiting.
+#pragma warning disable CS0628 // New protected member declared in sealed type
+#pragma warning disable CS0618 // Type or member is obsolete
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable IDE0290 // Use primary constructor
+
+public sealed partial class ConfigChoice
+{
+ ///
+ /// Constructor for Serialization/Deserialization
+ ///
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ public ConfigChoice()
+ {
+ }
+
+ ///
+ /// Choice enum
+ ///
+ [JsonIgnore]
+ [XmlIgnore]
+ [ChoiceTypeProperty]
+ public ChoiceOf ChoiceType { get; private set; }
+
+ private TestNamespace.OptionA? _optionA;
+
+ ///
+ /// Choice property - Option A
+ ///
+ [DisallowNull]
+ [XmlElement("OptionA")]
+ [ChoiceProperty]
+ public partial TestNamespace.OptionA? OptionA
+ {
+ get => _optionA;
+ set
+ {
+ _optionA = value ?? throw new InvalidOperationException("Choice value cannot be null");
+ _optionB = null;
+ ChoiceType = ChoiceOf.OptionA;
+ }
+ }
+
+ private TestNamespace.OptionB? _optionB;
+
+ ///
+ /// Choice property - Option B
+ ///
+ [DisallowNull]
+ [XmlElement("OptionB")]
+ [ChoiceProperty]
+ public partial TestNamespace.OptionB? OptionB
+ {
+ get => _optionB;
+ set
+ {
+ _optionB = value ?? throw new InvalidOperationException("Choice value cannot be null");
+ _optionA = null;
+ ChoiceType = ChoiceOf.OptionB;
+ }
+ }
+
+
+ ///
+ /// Creates a new instance and sets its value using the specified .
+ ///
+ /// The value for the required property Value.
+ /// The value for the required property ChoiceValue.
+ /// The value to assign to the created choice instance.
+ public static TestNamespace.ConfigChoice CreateAsOptionA(string value, string choiceValue, TestNamespace.OptionA choiceValue1) => new () { Value = value, ChoiceValue = choiceValue, OptionA = choiceValue1 };
+
+ ///
+ /// Creates a new instance and sets its value using the specified .
+ ///
+ /// The value for the required property Value.
+ /// The value for the required property ChoiceValue.
+ /// The value to assign to the created choice instance.
+ public static TestNamespace.ConfigChoice CreateAsOptionB(string value, string choiceValue, TestNamespace.OptionB choiceValue1) => new () { Value = value, ChoiceValue = choiceValue, OptionB = choiceValue1 };
+
+ ///
+ /// Applies the appropriate function based on the current choice type
+ ///
+ /// The return type of the provided match functions
+ /// Function to invoke if the choice is a value
+ /// Function to invoke if the choice is a value
+ public TResult Match(
+ Func matchOptionA,
+ Func matchOptionB)
+ {
+ return ChoiceType switch
+ {
+ ChoiceOf.OptionA => matchOptionA(OptionA!),
+ ChoiceOf.OptionB => matchOptionB(OptionB!),
+ _ => throw new InvalidOperationException($"Invalid ChoiceType. '{ChoiceType}'")
+ };
+ }
+
+ ///
+ /// Applies the appropriate Action based on the current choice type
+ ///
+ /// Action to invoke if the choice is a value
+ /// Action to invoke if the choice is a value
+ public void Switch(
+ Action matchOptionA,
+ Action matchOptionB)
+ {
+ switch (ChoiceType)
+ {
+ case ChoiceOf.OptionA:
+ matchOptionA(OptionA!);
+ return;
+
+ case ChoiceOf.OptionB:
+ matchOptionB(OptionB!);
+ return;
+
+ default:
+ throw new XmlException($"Invalid ChoiceType. '{ChoiceType}'");
+ }
+ }
+
+
+ ///
+ /// Determines whether the property should be serialized.
+ ///
+ ///
+ /// true if is the active choice; otherwise, false.
+ ///
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ public bool ShouldSerializeOptionA() => ChoiceType == ChoiceOf.OptionA;
+
+ ///
+ /// Determines whether the property should be serialized.
+ ///
+ ///
+ /// true if is the active choice; otherwise, false.
+ ///
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ public bool ShouldSerializeOptionB() => ChoiceType == ChoiceOf.OptionB;
+
+ ///
+ /// Choice enumeration
+ ///
+ [XmlType("ChoiceOf.ConfigChoice")]
+ public enum ChoiceOf
+ {
+ ///
+ /// Choice property - Option A
+ ///
+ OptionA,
+ ///
+ /// Choice property - Option B
+ ///
+ OptionB,
+ }
+}
diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithXmlTagNamespace#XmlNamespaceChoice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithXmlTagNamespace#XmlNamespaceChoice.g.verified.cs
index 268ab64..1f3017f 100644
--- a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithXmlTagNamespace#XmlNamespaceChoice.g.verified.cs
+++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithXmlTagNamespace#XmlNamespaceChoice.g.verified.cs
@@ -24,6 +24,10 @@ namespace TestNamespace;
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
#pragma warning disable CS0628 // New protected member declared in sealed type
+#pragma warning disable CS0618 // Type or member is obsolete
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable IDE0290 // Use primary constructor
public sealed partial class XmlNamespaceChoice
{
diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldNotGenerateImplicitMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldNotGenerateImplicitMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs
index 35cb294..f4c2e9d 100644
--- a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldNotGenerateImplicitMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs
+++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldNotGenerateImplicitMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs
@@ -24,6 +24,10 @@ namespace TestNamespace;
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
#pragma warning disable CS0628 // New protected member declared in sealed type
+#pragma warning disable CS0618 // Type or member is obsolete
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable IDE0290 // Use primary constructor
public sealed partial class Authorisation1Choice
{
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationJsonTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationJsonTests.cs
new file mode 100644
index 0000000..ae0c632
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationJsonTests.cs
@@ -0,0 +1,292 @@
+using System;
+using AltaSoft.ChoiceGenerator.Tests.TestHelpers;
+using AltaSoft.ChoiceGenerator.Tests.TestModels;
+using Xunit;
+
+namespace AltaSoft.ChoiceGenerator.Tests;
+
+///
+/// Tests for JSON serialization and deserialization of choice types
+///
+public class ChoiceSerializationJsonTests
+{
+ #region Serialization Tests
+
+ [Fact]
+ public void JsonSerialization_PaymentMethodWithCreditCard_ShouldProduceCorrectJson()
+ {
+ // Arrange
+ var cardPayment = new CreditCardPayment("4111111111111111", "John Doe", "12/25", "123");
+ var paymentMethod = PaymentMethod.CreateAsCreditCard(cardPayment);
+
+ // Act
+ var json = JsonSerializationHelper.SerializeToJson(paymentMethod);
+
+ // Assert
+ Assert.Contains("\"creditCard\"", json);
+ Assert.Contains("\"CardNumber\": \"4111111111111111\"", json);
+ Assert.Contains("\"CardHolderName\": \"John Doe\"", json);
+ Assert.DoesNotContain("bankTransfer", json);
+ Assert.DoesNotContain("payPal", json);
+ }
+
+ [Fact]
+ public void JsonSerialization_PaymentMethodWithBankTransfer_ShouldProduceCorrectJson()
+ {
+ // Arrange
+ var bankTransfer = new BankTransferPayment("123456789", "987654321", "Bank of America");
+ var paymentMethod = PaymentMethod.CreateAsBankTransfer(bankTransfer);
+
+ // Act
+ var json = JsonSerializationHelper.SerializeToJson(paymentMethod);
+
+ // Assert
+ Assert.Contains("\"bankTransfer\"", json);
+ Assert.Contains("\"AccountNumber\": \"123456789\"", json);
+ Assert.Contains("\"BankName\": \"Bank of America\"", json);
+ Assert.DoesNotContain("creditCard", json);
+ Assert.DoesNotContain("payPal", json);
+ }
+
+ [Fact]
+ public void JsonSerialization_ShippingOption_ShouldSerializeCorrectly()
+ {
+ // Arrange
+ var expressShipping = new ShippingDetails(15.99m, 2, "FedEx");
+ var shippingOption = ShippingOption.CreateAsExpress(expressShipping);
+
+ // Act
+ var json = JsonSerializationHelper.SerializeToJson(shippingOption);
+
+ // Assert
+ Assert.Contains("\"express\"", json);
+ Assert.Contains("\"Cost\": 15.99", json);
+ Assert.Contains("\"EstimatedDays\": 2", json);
+ Assert.Contains("\"Carrier\": \"FedEx\"", json);
+ }
+
+ [Fact]
+ public void JsonSerialization_SearchCriteriaWithKeyword_ShouldSerializeCorrectly()
+ {
+ // Arrange
+ var searchCriteria = SearchCriteria.CreateAsKeyword("laptop");
+
+ // Act
+ var json = JsonSerializationHelper.SerializeToJson(searchCriteria);
+
+ // Assert
+ Assert.Contains("\"keyword\": \"laptop\"", json);
+ Assert.DoesNotContain("categoryId", json);
+ Assert.DoesNotContain("dateRange", json);
+ Assert.DoesNotContain("priceRange", json);
+ }
+
+ [Fact]
+ public void JsonSerialization_NotificationChannel_ShouldSerializeEnum()
+ {
+ // Arrange
+ var channel = NotificationChannel.CreateAsChannel(NotificationChannelType.Email);
+
+ // Act
+ var json = JsonSerializationHelper.SerializeToJson(channel);
+
+ // Assert
+ Assert.Contains("\"channel\": \"Email\"", json);
+ }
+
+ #endregion
+
+ #region Deserialization Tests
+
+ [Fact]
+ public void JsonDeserialization_CreditCardPayment_ShouldDeserializeCorrectly()
+ {
+ // Arrange
+ const string json = """
+ {
+ "creditCard": {
+ "CardNumber": "4111111111111111",
+ "CardHolderName": "Jane Smith",
+ "ExpiryDate": "06/26",
+ "Cvv": "456"
+ }
+ }
+ """;
+
+ // Act
+ var paymentMethod = JsonSerializationHelper.DeserializeFromJson(json);
+
+ // Assert
+ Assert.NotNull(paymentMethod);
+ Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, paymentMethod.ChoiceType);
+ Assert.NotNull(paymentMethod.CreditCard);
+ Assert.Equal("4111111111111111", paymentMethod.CreditCard.CardNumber);
+ Assert.Equal("Jane Smith", paymentMethod.CreditCard.CardHolderName);
+ Assert.Null(paymentMethod.BankTransfer);
+ Assert.Null(paymentMethod.PayPal);
+ }
+
+ [Fact]
+ public void JsonDeserialization_PayPalPayment_ShouldDeserializeCorrectly()
+ {
+ // Arrange
+ const string json = """
+ {
+ "payPal": {
+ "Email": "test@example.com",
+ "TransactionId": "TXN-12345"
+ }
+ }
+ """;
+
+ // Act
+ var paymentMethod = JsonSerializationHelper.DeserializeFromJson(json);
+
+ // Assert
+ Assert.NotNull(paymentMethod);
+ Assert.Equal(PaymentMethod.ChoiceOf.PayPal, paymentMethod.ChoiceType);
+ Assert.NotNull(paymentMethod.PayPal);
+ Assert.Equal("test@example.com", paymentMethod.PayPal.Email);
+ Assert.Equal("TXN-12345", paymentMethod.PayPal.TransactionId);
+ }
+
+ [Fact]
+ public void JsonDeserialization_ShippingOption_ShouldDeserializeCorrectly()
+ {
+ // Arrange
+ const string json = """
+ {
+ "overnight": {
+ "Cost": 29.99,
+ "EstimatedDays": 1,
+ "Carrier": "DHL"
+ }
+ }
+ """;
+
+ // Act
+ var shippingOption = JsonSerializationHelper.DeserializeFromJson(json);
+
+ // Assert
+ Assert.NotNull(shippingOption);
+ Assert.Equal(ShippingOption.ChoiceOf.Overnight, shippingOption.ChoiceType);
+ Assert.NotNull(shippingOption.Overnight);
+ Assert.Equal(29.99m, shippingOption.Overnight.Cost);
+ Assert.Equal(1, shippingOption.Overnight.EstimatedDays);
+ }
+
+ [Fact]
+ public void JsonDeserialization_SearchCriteriaWithCategoryId_ShouldDeserializeCorrectly()
+ {
+ // Arrange
+ const string json = """
+ {
+ "categoryId": 42
+ }
+ """;
+
+ // Act
+ var searchCriteria = JsonSerializationHelper.DeserializeFromJson(json);
+
+ // Assert
+ Assert.NotNull(searchCriteria);
+ Assert.Equal(SearchCriteria.ChoiceOf.CategoryId, searchCriteria.ChoiceType);
+ Assert.Equal(42, searchCriteria.CategoryId);
+ }
+
+ [Fact]
+ public void JsonDeserialization_NotificationChannel_ShouldDeserializeEnum()
+ {
+ // Arrange
+ const string json = """
+ {
+ "channel": "SMS"
+ }
+ """;
+
+ // Act
+ var channel = JsonSerializationHelper.DeserializeFromJson(json);
+
+ // Assert
+ Assert.NotNull(channel);
+ Assert.Equal(NotificationChannelType.SMS, channel.Channel);
+ }
+
+ #endregion
+
+ #region Round-Trip Tests
+
+ [Fact]
+ public void JsonRoundTrip_PaymentMethod_ShouldPreserveData()
+ {
+ // Arrange
+ var original = PaymentMethod.CreateAsCreditCard(
+ new CreditCardPayment("4111111111111111", "Alice Johnson", "12/25", "123")
+ );
+
+ // Act
+ var restored = JsonSerializationHelper.RoundTrip(original);
+
+ // Assert
+ Assert.NotNull(restored);
+ Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, restored.ChoiceType);
+ Assert.Equal("Alice Johnson", restored.CreditCard?.CardHolderName);
+ Assert.Equal("4111111111111111", restored.CreditCard?.CardNumber);
+ }
+
+ [Fact]
+ public void JsonRoundTrip_ShippingOption_ShouldPreserveData()
+ {
+ // Arrange
+ var original = ShippingOption.CreateAsStandard(
+ new ShippingDetails(5.99m, 7, "USPS")
+ );
+
+ // Act
+ var restored = JsonSerializationHelper.RoundTrip(original);
+
+ // Assert
+ Assert.NotNull(restored);
+ Assert.Equal(ShippingOption.ChoiceOf.Standard, restored.ChoiceType);
+ Assert.Equal(5.99m, restored.Standard?.Cost);
+ Assert.Equal(7, restored.Standard?.EstimatedDays);
+ }
+
+ [Fact]
+ public void JsonRoundTrip_SearchCriteria_WithPriceRange_ShouldPreserveStruct()
+ {
+ // Arrange
+ var original = SearchCriteria.CreateAsPriceRange(new PriceRange(100m, 500m));
+
+ // Act
+ var restored = JsonSerializationHelper.RoundTrip(original);
+
+ // Assert
+ Assert.NotNull(restored);
+ Assert.Equal(SearchCriteria.ChoiceOf.PriceRange, restored.ChoiceType);
+ Assert.NotNull(restored.PriceRange);
+ Assert.Equal(100m, restored.PriceRange.Value.MinPrice);
+ Assert.Equal(500m, restored.PriceRange.Value.MaxPrice);
+ }
+
+ [Fact]
+ public void JsonRoundTrip_SearchCriteria_WithDateRange_ShouldPreserveDates()
+ {
+ // Arrange
+ var startDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ var endDate = new DateTime(2024, 12, 31, 23, 59, 59, DateTimeKind.Utc);
+ var original = SearchCriteria.CreateAsDateRange(new DateRange(startDate, endDate));
+
+ // Act
+ var restored = JsonSerializationHelper.RoundTrip(original);
+
+ // Assert
+ Assert.NotNull(restored);
+ Assert.Equal(SearchCriteria.ChoiceOf.DateRange, restored.ChoiceType);
+ Assert.NotNull(restored.DateRange);
+ Assert.Equal(startDate, restored.DateRange.StartDate);
+ Assert.Equal(endDate, restored.DateRange.EndDate);
+ }
+
+ #endregion
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationXmlTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationXmlTests.cs
new file mode 100644
index 0000000..e2a8556
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationXmlTests.cs
@@ -0,0 +1,332 @@
+using System;
+using AltaSoft.ChoiceGenerator.Tests.TestHelpers;
+using AltaSoft.ChoiceGenerator.Tests.TestModels;
+using Xunit;
+
+namespace AltaSoft.ChoiceGenerator.Tests;
+
+///
+/// Tests for XML serialization and deserialization of choice types
+///
+public class ChoiceSerializationXmlTests
+{
+ #region Serialization Tests
+
+ [Fact]
+ public void XmlSerialization_PaymentMethodWithCreditCard_ShouldProduceCorrectXml()
+ {
+ // Arrange
+ var cardPayment = new CreditCardPayment("4111111111111111", "John Doe", "12/25", "123");
+ var paymentMethod = PaymentMethod.CreateAsCreditCard(cardPayment);
+
+ // Act
+ var xml = XmlSerializationHelper.SerializeToXml(paymentMethod);
+
+ // Assert
+ Assert.Contains("", xml);
+ Assert.Contains("4111111111111111", xml);
+ Assert.Contains("John Doe", xml);
+ Assert.DoesNotContain("", xml);
+ Assert.Contains("123456789", xml);
+ Assert.Contains("Bank of America", xml);
+ Assert.DoesNotContain("", xml);
+ Assert.Contains("15.99", xml);
+ Assert.Contains("2", xml);
+ Assert.Contains("FedEx", xml);
+ Assert.DoesNotContain("laptop", xml);
+ Assert.DoesNotContain("Push", xml);
+ }
+
+ [Fact]
+ public void XmlSerialization_ShouldNotIncludeInactiveChoices()
+ {
+ // Arrange
+ var payPal = new PayPalPayment("test@example.com", "TXN-999");
+ var paymentMethod = PaymentMethod.CreateAsPayPal(payPal);
+
+ // Act
+ var xml = XmlSerializationHelper.SerializeToXml(paymentMethod);
+
+ // Assert
+ Assert.Contains("", xml);
+ // Inactive choices should not be serialized (no xsi:nil elements)
+ Assert.DoesNotContain("nil=", xml);
+ Assert.DoesNotContain("
+
+ 4111111111111111
+ Jane Smith
+ 06/26
+ 456
+
+
+ """;
+
+ // Act
+ var paymentMethod = XmlSerializationHelper.DeserializeFromXml(xml);
+
+ // Assert
+ Assert.NotNull(paymentMethod);
+ Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, paymentMethod.ChoiceType);
+ Assert.NotNull(paymentMethod.CreditCard);
+ Assert.Equal("4111111111111111", paymentMethod.CreditCard.CardNumber);
+ Assert.Equal("Jane Smith", paymentMethod.CreditCard.CardHolderName);
+ Assert.Null(paymentMethod.BankTransfer);
+ Assert.Null(paymentMethod.PayPal);
+ }
+
+ [Fact]
+ public void XmlDeserialization_PayPalPayment_ShouldDeserializeCorrectly()
+ {
+ // Arrange
+ const string xml = """
+
+
+ user@paypal.com
+ TXN-ABC123
+
+
+ """;
+
+ // Act
+ var paymentMethod = XmlSerializationHelper.DeserializeFromXml(xml);
+
+ // Assert
+ Assert.NotNull(paymentMethod);
+ Assert.Equal(PaymentMethod.ChoiceOf.PayPal, paymentMethod.ChoiceType);
+ Assert.NotNull(paymentMethod.PayPal);
+ Assert.Equal("user@paypal.com", paymentMethod.PayPal.Email);
+ Assert.Equal("TXN-ABC123", paymentMethod.PayPal.TransactionId);
+ }
+
+ [Fact]
+ public void XmlDeserialization_ShippingOption_ShouldDeserializeCorrectly()
+ {
+ // Arrange
+ const string xml = """
+
+
+ 29.99
+ 1
+ DHL
+
+
+ """;
+
+ // Act
+ var shippingOption = XmlSerializationHelper.DeserializeFromXml(xml);
+
+ // Assert
+ Assert.NotNull(shippingOption);
+ Assert.Equal(ShippingOption.ChoiceOf.Overnight, shippingOption.ChoiceType);
+ Assert.NotNull(shippingOption.Overnight);
+ Assert.Equal(29.99m, shippingOption.Overnight.Cost);
+ Assert.Equal(1, shippingOption.Overnight.EstimatedDays);
+ Assert.Equal("DHL", shippingOption.Overnight.Carrier);
+ }
+
+ [Fact]
+ public void XmlDeserialization_SearchCriteriaWithCategoryId_ShouldDeserializeCorrectly()
+ {
+ // Arrange
+ const string xml = """
+
+ 42
+
+ """;
+
+ // Act
+ var searchCriteria = XmlSerializationHelper.DeserializeFromXml(xml);
+
+ // Assert
+ Assert.NotNull(searchCriteria);
+ Assert.Equal(SearchCriteria.ChoiceOf.CategoryId, searchCriteria.ChoiceType);
+ Assert.Equal(42, searchCriteria.CategoryId);
+ }
+
+ [Fact]
+ public void XmlDeserialization_NotificationChannel_ShouldDeserializeEnum()
+ {
+ // Arrange
+ const string xml = """
+
+ Email
+
+ """;
+
+ // Act
+ var channel = XmlSerializationHelper.DeserializeFromXml(xml);
+
+ // Assert
+ Assert.NotNull(channel);
+ Assert.Equal(NotificationChannelType.Email, channel.Channel);
+ }
+
+ #endregion
+
+ #region Round-Trip Tests
+
+ [Fact]
+ public void XmlRoundTrip_PaymentMethod_ShouldPreserveData()
+ {
+ // Arrange
+ var original = PaymentMethod.CreateAsCreditCard(
+ new CreditCardPayment("4111111111111111", "Alice Johnson", "12/25", "123")
+ );
+
+ // Act
+ var restored = XmlSerializationHelper.RoundTrip(original);
+
+ // Assert
+ Assert.NotNull(restored);
+ Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, restored.ChoiceType);
+ Assert.Equal("Alice Johnson", restored.CreditCard?.CardHolderName);
+ Assert.Equal("4111111111111111", restored.CreditCard?.CardNumber);
+ }
+
+ [Fact]
+ public void XmlRoundTrip_ShippingOption_ShouldPreserveData()
+ {
+ // Arrange
+ var original = ShippingOption.CreateAsStandard(
+ new ShippingDetails(5.99m, 7, "USPS")
+ );
+
+ // Act
+ var restored = XmlSerializationHelper.RoundTrip(original);
+
+ // Assert
+ Assert.NotNull(restored);
+ Assert.Equal(ShippingOption.ChoiceOf.Standard, restored.ChoiceType);
+ Assert.Equal(5.99m, restored.Standard?.Cost);
+ Assert.Equal(7, restored.Standard?.EstimatedDays);
+ Assert.Equal("USPS", restored.Standard?.Carrier);
+ }
+
+ [Fact]
+ public void XmlRoundTrip_SearchCriteria_WithPriceRange_ShouldPreserveStruct()
+ {
+ // Arrange
+ var original = SearchCriteria.CreateAsPriceRange(new PriceRange(100m, 500m));
+
+ // Act
+ var restored = XmlSerializationHelper.RoundTrip(original);
+
+ // Assert
+ Assert.NotNull(restored);
+ Assert.Equal(SearchCriteria.ChoiceOf.PriceRange, restored.ChoiceType);
+ Assert.NotNull(restored.PriceRange);
+ Assert.Equal(100m, restored.PriceRange.Value.MinPrice);
+ Assert.Equal(500m, restored.PriceRange.Value.MaxPrice);
+ }
+
+ [Fact]
+ public void XmlRoundTrip_SearchCriteria_WithDateRange_ShouldPreserveDates()
+ {
+ // Arrange
+ var startDate = new DateTime(2024, 1, 1);
+ var endDate = new DateTime(2024, 12, 31);
+ var original = SearchCriteria.CreateAsDateRange(new DateRange(startDate, endDate));
+
+ // Act
+ var restored = XmlSerializationHelper.RoundTrip(original);
+
+ // Assert
+ Assert.NotNull(restored);
+ Assert.Equal(SearchCriteria.ChoiceOf.DateRange, restored.ChoiceType);
+ Assert.NotNull(restored.DateRange);
+ Assert.Equal(startDate, restored.DateRange.StartDate.Date);
+ Assert.Equal(endDate, restored.DateRange.EndDate.Date);
+ }
+
+ [Fact]
+ public void XmlRoundTrip_MultipleDifferentChoices_ShouldAllWork()
+ {
+ // Arrange
+ var payments = new[]
+ {
+ PaymentMethod.CreateAsCreditCard(new CreditCardPayment("4111", "Alice", "12/25", "123")),
+ PaymentMethod.CreateAsPayPal(new PayPalPayment("bob@test.com")),
+ PaymentMethod.CreateAsBankTransfer(new BankTransferPayment("123", "456", "Chase"))
+ };
+
+ // Act & Assert
+ foreach (var original in payments)
+ {
+ var restored = XmlSerializationHelper.RoundTrip(original);
+ Assert.NotNull(restored);
+ Assert.Equal(original.ChoiceType, restored.ChoiceType);
+ }
+ }
+
+ #endregion
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeCreationTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeCreationTests.cs
new file mode 100644
index 0000000..f20c1dc
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeCreationTests.cs
@@ -0,0 +1,220 @@
+using AltaSoft.ChoiceGenerator.Tests.TestModels;
+using Xunit;
+
+namespace AltaSoft.ChoiceGenerator.Tests;
+
+///
+/// Tests for choice type creation using factory methods and implicit operators
+///
+public class ChoiceTypeCreationTests
+{
+ #region Factory Method Tests
+
+ [Fact]
+ public void CreateAsCreditCard_WithValidData_ShouldCreateCorrectChoice()
+ {
+ // Arrange
+ var cardPayment = new CreditCardPayment("4111111111111111", "John Doe", "12/25", "123");
+
+ // Act
+ var paymentMethod = PaymentMethod.CreateAsCreditCard(cardPayment);
+
+ // Assert
+ Assert.NotNull(paymentMethod);
+ Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, paymentMethod.ChoiceType);
+ Assert.NotNull(paymentMethod.CreditCard);
+ Assert.Equal("4111111111111111", paymentMethod.CreditCard.CardNumber);
+ Assert.Equal("John Doe", paymentMethod.CreditCard.CardHolderName);
+ Assert.Null(paymentMethod.BankTransfer);
+ Assert.Null(paymentMethod.PayPal);
+ }
+
+ [Fact]
+ public void CreateAsBankTransfer_WithValidData_ShouldCreateCorrectChoice()
+ {
+ // Arrange
+ var bankTransfer = new BankTransferPayment("123456789", "987654321", "Bank of America");
+
+ // Act
+ var paymentMethod = PaymentMethod.CreateAsBankTransfer(bankTransfer);
+
+ // Assert
+ Assert.NotNull(paymentMethod);
+ Assert.Equal(PaymentMethod.ChoiceOf.BankTransfer, paymentMethod.ChoiceType);
+ Assert.NotNull(paymentMethod.BankTransfer);
+ Assert.Equal("123456789", paymentMethod.BankTransfer.AccountNumber);
+ Assert.Null(paymentMethod.CreditCard);
+ Assert.Null(paymentMethod.PayPal);
+ }
+
+ [Fact]
+ public void CreateAsPayPal_WithValidData_ShouldCreateCorrectChoice()
+ {
+ // Arrange
+ var payPal = new PayPalPayment("john.doe@example.com", "TXN-12345");
+
+ // Act
+ var paymentMethod = PaymentMethod.CreateAsPayPal(payPal);
+
+ // Assert
+ Assert.NotNull(paymentMethod);
+ Assert.Equal(PaymentMethod.ChoiceOf.PayPal, paymentMethod.ChoiceType);
+ Assert.NotNull(paymentMethod.PayPal);
+ Assert.Equal("john.doe@example.com", paymentMethod.PayPal.Email);
+ Assert.Null(paymentMethod.CreditCard);
+ Assert.Null(paymentMethod.BankTransfer);
+ }
+
+ [Fact]
+ public void CreateAsStandard_WithShippingDetails_ShouldCreateCorrectChoice()
+ {
+ // Arrange
+ var shipping = new ShippingDetails(5.99m, 7, "USPS");
+
+ // Act
+ var shippingOption = ShippingOption.CreateAsStandard(shipping);
+
+ // Assert
+ Assert.NotNull(shippingOption);
+ Assert.Equal(ShippingOption.ChoiceOf.Standard, shippingOption.ChoiceType);
+ Assert.NotNull(shippingOption.Standard);
+ Assert.Equal(5.99m, shippingOption.Standard.Cost);
+ Assert.Equal(7, shippingOption.Standard.EstimatedDays);
+ Assert.Null(shippingOption.Express);
+ Assert.Null(shippingOption.Overnight);
+ }
+
+ [Fact]
+ public void CreateAsKeyword_WithString_ShouldCreateCorrectChoice()
+ {
+ // Arrange
+ const string keyword = "laptop";
+
+ // Act
+ var searchCriteria = SearchCriteria.CreateAsKeyword(keyword);
+
+ // Assert
+ Assert.NotNull(searchCriteria);
+ Assert.Equal(SearchCriteria.ChoiceOf.Keyword, searchCriteria.ChoiceType);
+ Assert.Equal("laptop", searchCriteria.Keyword);
+ Assert.Null(searchCriteria.CategoryId);
+ Assert.Null(searchCriteria.DateRange);
+ Assert.Null(searchCriteria.PriceRange);
+ }
+
+ [Fact]
+ public void CreateAsCategoryId_WithInt_ShouldCreateCorrectChoice()
+ {
+ // Arrange
+ const int categoryId = 42;
+
+ // Act
+ var searchCriteria = SearchCriteria.CreateAsCategoryId(categoryId);
+
+ // Assert
+ Assert.NotNull(searchCriteria);
+ Assert.Equal(SearchCriteria.ChoiceOf.CategoryId, searchCriteria.ChoiceType);
+ Assert.Equal(42, searchCriteria.CategoryId);
+ Assert.Null(searchCriteria.Keyword);
+ Assert.Null(searchCriteria.DateRange);
+ Assert.Null(searchCriteria.PriceRange);
+ }
+
+ #endregion
+
+ #region Implicit Operator Tests
+
+ [Fact]
+ public void ImplicitOperator_FromCreditCardPayment_ShouldCreateChoice()
+ {
+ // Arrange
+ var cardPayment = new CreditCardPayment("4111111111111111", "Jane Smith", "06/26", "456");
+
+ // Act
+ PaymentMethod paymentMethod = cardPayment;
+
+ // Assert
+ Assert.NotNull(paymentMethod);
+ Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, paymentMethod.ChoiceType);
+ Assert.NotNull(paymentMethod.CreditCard);
+ Assert.Equal("Jane Smith", paymentMethod.CreditCard.CardHolderName);
+ }
+
+ [Fact]
+ public void ImplicitOperator_FromBankTransferPayment_ShouldCreateChoice()
+ {
+ // Arrange
+ var bankTransfer = new BankTransferPayment("987654321", "123456789", "Chase Bank");
+
+ // Act
+ PaymentMethod paymentMethod = bankTransfer;
+
+ // Assert
+ Assert.NotNull(paymentMethod);
+ Assert.Equal(PaymentMethod.ChoiceOf.BankTransfer, paymentMethod.ChoiceType);
+ Assert.NotNull(paymentMethod.BankTransfer);
+ Assert.Equal("Chase Bank", paymentMethod.BankTransfer.BankName);
+ }
+
+ [Fact]
+ public void ImplicitOperator_FromString_ShouldCreateSearchCriteria()
+ {
+ // Arrange
+ const string keyword = "smartphone";
+
+ // Act
+ SearchCriteria criteria = keyword;
+
+ // Assert
+ Assert.NotNull(criteria);
+ Assert.Equal(SearchCriteria.ChoiceOf.Keyword, criteria.ChoiceType);
+ Assert.Equal("smartphone", criteria.Keyword);
+ }
+
+ [Fact]
+ public void ImplicitOperator_FromInt_ShouldCreateSearchCriteria()
+ {
+ // Arrange
+ const int categoryId = 100;
+
+ // Act
+ SearchCriteria criteria = categoryId;
+
+ // Assert
+ Assert.NotNull(criteria);
+ Assert.Equal(SearchCriteria.ChoiceOf.CategoryId, criteria.ChoiceType);
+ Assert.Equal(100, criteria.CategoryId);
+ }
+
+ #endregion
+
+ #region Single Property Choice Tests
+
+ [Fact]
+ public void NotificationChannel_SingleProperty_ShouldCreateCorrectly()
+ {
+ // Arrange & Act
+ var channel = NotificationChannel.CreateAsChannel(NotificationChannelType.Email);
+
+ // Assert
+ Assert.NotNull(channel);
+ Assert.Equal(NotificationChannel.ChoiceOf.Channel, channel.ChoiceType);
+ Assert.Equal(NotificationChannelType.Email, channel.Channel);
+ }
+
+ [Fact]
+ public void NotificationChannel_ImplicitOperator_ShouldWork()
+ {
+ // Arrange
+ const NotificationChannelType type = NotificationChannelType.SMS;
+
+ // Act
+ NotificationChannel channel = type;
+
+ // Assert
+ Assert.NotNull(channel);
+ Assert.Equal(NotificationChannelType.SMS, channel.Channel);
+ }
+
+ #endregion
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeSwitchMatchTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeSwitchMatchTests.cs
new file mode 100644
index 0000000..ece6ca2
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeSwitchMatchTests.cs
@@ -0,0 +1,262 @@
+using System;
+using System.Linq;
+using AltaSoft.ChoiceGenerator.Tests.TestModels;
+using Xunit;
+
+namespace AltaSoft.ChoiceGenerator.Tests;
+
+///
+/// Tests for Switch and Match pattern matching on choice types
+///
+public class ChoiceTypeSwitchMatchTests
+{
+ #region Match Pattern Tests
+
+ [Fact]
+ public void Match_WithCreditCardPayment_ShouldExecuteCorrectBranch()
+ {
+ // Arrange
+ var cardPayment = new CreditCardPayment("4111111111111111", "Alice", "12/25", "123");
+ var paymentMethod = PaymentMethod.CreateAsCreditCard(cardPayment);
+
+ // Act
+ var result = paymentMethod.Match(
+ creditCard => $"Card: {creditCard.CardHolderName}",
+ bankTransfer => $"Bank: {bankTransfer.BankName}",
+ payPal => $"PayPal: {payPal.Email}"
+ );
+
+ // Assert
+ Assert.Equal("Card: Alice", result);
+ }
+
+ [Fact]
+ public void Match_WithBankTransferPayment_ShouldExecuteCorrectBranch()
+ {
+ // Arrange
+ var bankTransfer = new BankTransferPayment("123456", "654321", "Wells Fargo");
+ var paymentMethod = PaymentMethod.CreateAsBankTransfer(bankTransfer);
+
+ // Act
+ var result = paymentMethod.Match(
+ creditCard => $"Card: {creditCard.CardHolderName}",
+ bankTransfer => $"Bank: {bankTransfer.BankName}",
+ payPal => $"PayPal: {payPal.Email}"
+ );
+
+ // Assert
+ Assert.Equal("Bank: Wells Fargo", result);
+ }
+
+ [Fact]
+ public void Match_WithPayPalPayment_ShouldExecuteCorrectBranch()
+ {
+ // Arrange
+ var payPal = new PayPalPayment("bob@example.com", "TXN-789");
+ var paymentMethod = PaymentMethod.CreateAsPayPal(payPal);
+
+ // Act
+ var result = paymentMethod.Match(
+ creditCard => $"Card: {creditCard.CardHolderName}",
+ bankTransfer => $"Bank: {bankTransfer.BankName}",
+ payPal => $"PayPal: {payPal.Email}"
+ );
+
+ // Assert
+ Assert.Equal("PayPal: bob@example.com", result);
+ }
+
+ [Fact]
+ public void Match_WithShippingOptions_ShouldCalculateTotalCost()
+ {
+ // Arrange
+ var expressShipping = new ShippingDetails(15.99m, 2, "FedEx");
+ var shippingOption = ShippingOption.CreateAsExpress(expressShipping);
+
+ // Act
+ var totalCost = shippingOption.Match(
+ standard => standard.Cost,
+ express => express.Cost * 1.1m, // 10% processing fee for express
+ overnight => overnight.Cost * 1.2m // 20% processing fee for overnight
+ );
+
+ // Assert
+ Assert.Equal(17.589m, totalCost);
+ }
+
+ [Fact]
+ public void Match_WithSearchCriteria_ShouldFormatQuery()
+ {
+ // Arrange
+ var searchCriteria = SearchCriteria.CreateAsKeyword("laptop");
+
+ // Act
+ var query = searchCriteria.Match(
+ keyword => $"keyword={keyword}",
+ categoryId => $"category={categoryId}",
+ dateRange => $"from={dateRange.StartDate:yyyy-MM-dd}&to={dateRange.EndDate:yyyy-MM-dd}",
+ priceRange => $"min={priceRange.MinPrice}&max={priceRange.MaxPrice}"
+ );
+
+ // Assert
+ Assert.Equal("keyword=laptop", query);
+ }
+
+ #endregion
+
+ #region Switch Pattern Tests
+
+ [Fact]
+ public void Switch_WithCreditCardPayment_ShouldExecuteCorrectAction()
+ {
+ // Arrange
+ var cardPayment = new CreditCardPayment("4111111111111111", "Charlie", "12/25", "123");
+ var paymentMethod = PaymentMethod.CreateAsCreditCard(cardPayment);
+ var processedType = string.Empty;
+
+ // Act
+ paymentMethod.Switch(
+ creditCard => processedType = "credit_card",
+ bankTransfer => processedType = "bank_transfer",
+ payPal => processedType = "paypal"
+ );
+
+ // Assert
+ Assert.Equal("credit_card", processedType);
+ }
+
+ [Fact]
+ public void Switch_WithBankTransferPayment_ShouldExecuteCorrectAction()
+ {
+ // Arrange
+ var bankTransfer = new BankTransferPayment("999888", "777666", "Bank of America");
+ var paymentMethod = PaymentMethod.CreateAsBankTransfer(bankTransfer);
+ var processedType = string.Empty;
+
+ // Act
+ paymentMethod.Switch(
+ creditCard => processedType = "credit_card",
+ bankTransfer => processedType = "bank_transfer",
+ payPal => processedType = "paypal"
+ );
+
+ // Assert
+ Assert.Equal("bank_transfer", processedType);
+ }
+
+ [Fact]
+ public void Switch_WithPayPalPayment_ShouldExecuteCorrectAction()
+ {
+ // Arrange
+ var payPal = new PayPalPayment("dana@test.com");
+ var paymentMethod = PaymentMethod.CreateAsPayPal(payPal);
+ var processedType = string.Empty;
+
+ // Act
+ paymentMethod.Switch(
+ creditCard => processedType = "credit_card",
+ bankTransfer => processedType = "bank_transfer",
+ payPal => processedType = "paypal"
+ );
+
+ // Assert
+ Assert.Equal("paypal", processedType);
+ }
+
+ [Fact]
+ public void Switch_WithShippingOptions_ShouldAccumulateData()
+ {
+ // Arrange
+ var overnightShipping = new ShippingDetails(29.99m, 1, "DHL");
+ var shippingOption = ShippingOption.CreateAsOvernight(overnightShipping);
+ var deliveryInfo = new { Days = 0, Cost = 0m, Type = "" };
+
+ // Act
+ shippingOption.Switch(
+ standard => deliveryInfo = new { Days = standard.EstimatedDays, Cost = standard.Cost, Type = "Standard" },
+ express => deliveryInfo = new { Days = express.EstimatedDays, Cost = express.Cost, Type = "Express" },
+ overnight => deliveryInfo = new { Days = overnight.EstimatedDays, Cost = overnight.Cost, Type = "Overnight" }
+ );
+
+ // Assert
+ Assert.Equal(1, deliveryInfo.Days);
+ Assert.Equal(29.99m, deliveryInfo.Cost);
+ Assert.Equal("Overnight", deliveryInfo.Type);
+ }
+
+ [Fact]
+ public void Switch_WithSearchCriteria_ShouldModifyExternalState()
+ {
+ // Arrange
+ var priceRange = new PriceRange(100m, 500m);
+ var searchCriteria = SearchCriteria.CreateAsPriceRange(priceRange);
+ var filterApplied = false;
+ var filterType = "";
+
+ // Act
+ searchCriteria.Switch(
+ keyword => { filterApplied = true; filterType = "text"; },
+ categoryId => { filterApplied = true; filterType = "category"; },
+ dateRange => { filterApplied = true; filterType = "date"; },
+ priceRange => { filterApplied = true; filterType = "price"; }
+ );
+
+ // Assert
+ Assert.True(filterApplied);
+ Assert.Equal("price", filterType);
+ }
+
+ #endregion
+
+ #region Complex Scenarios
+
+ [Fact]
+ public void Match_ChainedWithSwitch_ShouldWorkCorrectly()
+ {
+ // Arrange
+ var standardShipping = new ShippingDetails(5.99m, 7, "USPS");
+ var shippingOption = ShippingOption.CreateAsStandard(standardShipping);
+
+ // Act - Use Match to calculate discount
+ var discount = shippingOption.Match(
+ standard => 0m,
+ express => 2m,
+ overnight => 5m
+ );
+
+ // Switch to apply discount
+ var finalCost = 0m;
+ shippingOption.Switch(
+ standard => finalCost = standard.Cost - discount,
+ express => finalCost = express.Cost - discount,
+ overnight => finalCost = overnight.Cost - discount
+ );
+
+ // Assert
+ Assert.Equal(5.99m, finalCost);
+ }
+
+ [Fact]
+ public void Match_UsedInLinqQuery_ShouldWork()
+ {
+ // Arrange
+ var payments = new[]
+ {
+ PaymentMethod.CreateAsCreditCard(new CreditCardPayment("4111", "Alice", "12/25", "123")),
+ PaymentMethod.CreateAsPayPal(new PayPalPayment("bob@test.com")),
+ PaymentMethod.CreateAsBankTransfer(new BankTransferPayment("123", "456", "Chase"))
+ };
+
+ // Act
+ var paymentTypes = payments.Select(p => p.Match(
+ cc => "Card",
+ bt => "Transfer",
+ pp => "PayPal"
+ )).ToArray();
+
+ // Assert
+ Assert.Equal(new[] { "Card", "PayPal", "Transfer" }, paymentTypes);
+ }
+
+ #endregion
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/MixedPropertiesChoiceTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/MixedPropertiesChoiceTests.cs
new file mode 100644
index 0000000..ee89a20
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/MixedPropertiesChoiceTests.cs
@@ -0,0 +1,560 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Serialization;
+using AltaSoft.Choice;
+using Xunit;
+
+namespace AltaSoft.ChoiceGenerator.Tests;
+
+#region Test Model Definitions
+
+///
+/// Choice type with one ordinary property and two choice properties
+///
+[Choice]
+public sealed partial class OrderChoice
+{
+ ///
+ /// Ordinary property - Order ID (not part of the choice)
+ ///
+ [XmlElement("OrderId")]
+ public string? OrderId { get; set; }
+
+ ///
+ /// Choice property - Express delivery
+ ///
+ [XmlTag("Express")]
+ public partial ExpressDelivery? Express { get; set; }
+
+ ///
+ /// Choice property - Standard delivery
+ ///
+ [XmlTag("Standard")]
+ public partial StandardDelivery? Standard { get; set; }
+}
+
+public sealed class ExpressDelivery
+{
+ public DateTime DeliveryDate { get; set; }
+ public decimal SurchargeAmount { get; set; }
+
+ public ExpressDelivery() { }
+ public ExpressDelivery(DateTime deliveryDate, decimal surchargeAmount)
+ {
+ DeliveryDate = deliveryDate;
+ SurchargeAmount = surchargeAmount;
+ }
+}
+
+public sealed class StandardDelivery
+{
+ public int DeliveryDays { get; set; }
+ public decimal ShippingCost { get; set; }
+
+ public StandardDelivery() { }
+ public StandardDelivery(int deliveryDays, decimal shippingCost)
+ {
+ DeliveryDays = deliveryDays;
+ ShippingCost = shippingCost;
+ }
+}
+
+///
+/// Choice type with multiple ordinary properties and choice properties
+///
+[Choice]
+public sealed partial class PaymentRequest
+{
+ ///
+ /// Ordinary property - Transaction ID
+ ///
+ [XmlElement("TxnId")]
+ public string? TransactionId { get; set; }
+
+ ///
+ /// Ordinary property - Amount
+ ///
+ [XmlElement("Amt")]
+ public decimal Amount { get; set; }
+
+ ///
+ /// Ordinary property - Currency
+ ///
+ [XmlElement("Ccy")]
+ public string? Currency { get; set; }
+
+ ///
+ /// Choice property - Card payment
+ ///
+ [XmlTag("Card")]
+ public partial CardPayment? Card { get; set; }
+
+ ///
+ /// Choice property - Bank transfer
+ ///
+ [XmlTag("BankTrf")]
+ public partial BankTransfer? BankTransfer { get; set; }
+
+ ///
+ /// Choice property - Cash payment
+ ///
+ [XmlTag("Cash")]
+ public partial CashPayment? Cash { get; set; }
+}
+
+public sealed class CardPayment
+{
+ public string? CardNumber { get; set; }
+ public string? ExpiryDate { get; set; }
+
+ public CardPayment() { }
+ public CardPayment(string cardNumber, string expiryDate)
+ {
+ CardNumber = cardNumber;
+ ExpiryDate = expiryDate;
+ }
+}
+
+public sealed class BankTransfer
+{
+ public string? IBAN { get; set; }
+ public string? BIC { get; set; }
+
+ public BankTransfer() { }
+ public BankTransfer(string iban, string bic)
+ {
+ IBAN = iban;
+ BIC = bic;
+ }
+}
+
+public sealed class CashPayment
+{
+ public string? ReceiptNumber { get; set; }
+ public DateTime ReceivedDate { get; set; }
+
+ public CashPayment() { }
+ public CashPayment(string receiptNumber, DateTime receivedDate)
+ {
+ ReceiptNumber = receiptNumber;
+ ReceivedDate = receivedDate;
+ }
+}
+
+#endregion
+
+///
+/// Tests for Choice types that contain both Choice properties (partial) and ordinary properties
+///
+public class MixedPropertiesChoiceTests
+{
+ private static readonly XmlWriterSettings s_xmlWriterSettings = new()
+ {
+ OmitXmlDeclaration = true,
+ Indent = true
+ };
+
+ [Fact]
+ public void MixedProperties_OrdinaryPropertyShouldNotAffectChoice()
+ {
+ var order = new OrderChoice
+ {
+ OrderId = "ORD-12345",
+ Express = new ExpressDelivery(new DateTime(2024, 12, 25), 25.00m)
+ };
+
+ Assert.Equal("ORD-12345", order.OrderId);
+ Assert.Equal(OrderChoice.ChoiceOf.Express, order.ChoiceType);
+ Assert.NotNull(order.Express);
+ Assert.Null(order.Standard);
+ }
+
+ [Fact]
+ public void MixedProperties_ChangingChoiceShouldNotAffectOrdinaryProperty()
+ {
+ var order = new OrderChoice
+ {
+ OrderId = "ORD-99999",
+ Express = new ExpressDelivery(new DateTime(2024, 12, 25), 25.00m)
+ };
+
+ // Change the choice
+ order.Standard = new StandardDelivery(5, 10.00m);
+
+ // Ordinary property should remain unchanged
+ Assert.Equal("ORD-99999", order.OrderId);
+ Assert.Equal(OrderChoice.ChoiceOf.Standard, order.ChoiceType);
+ Assert.Null(order.Express);
+ Assert.NotNull(order.Standard);
+ }
+
+ [Fact]
+ public void MixedProperties_MultipleOrdinaryProperties_ShouldNotAffectChoice()
+ {
+ var payment = new PaymentRequest
+ {
+ TransactionId = "TXN-001",
+ Amount = 150.50m,
+ Currency = "USD",
+ Card = new CardPayment("4111111111111111", "12/25")
+ };
+
+ Assert.Equal("TXN-001", payment.TransactionId);
+ Assert.Equal(150.50m, payment.Amount);
+ Assert.Equal("USD", payment.Currency);
+ Assert.Equal(PaymentRequest.ChoiceOf.Card, payment.ChoiceType);
+ Assert.NotNull(payment.Card);
+ Assert.Null(payment.BankTransfer);
+ Assert.Null(payment.Cash);
+ }
+
+ [Fact]
+ public void MixedProperties_SwitchChoice_OrdinaryPropertiesRemainIntact()
+ {
+ var payment = new PaymentRequest
+ {
+ TransactionId = "TXN-002",
+ Amount = 500.00m,
+ Currency = "EUR",
+ Card = new CardPayment("5500000000000004", "06/26")
+ };
+
+ // Switch to BankTransfer
+ payment.BankTransfer = new BankTransfer("GB82WEST12345698765432", "WESTGB22");
+
+ // Ordinary properties should remain unchanged
+ Assert.Equal("TXN-002", payment.TransactionId);
+ Assert.Equal(500.00m, payment.Amount);
+ Assert.Equal("EUR", payment.Currency);
+ Assert.Equal(PaymentRequest.ChoiceOf.BankTransfer, payment.ChoiceType);
+ Assert.Null(payment.Card);
+ Assert.NotNull(payment.BankTransfer);
+ Assert.Null(payment.Cash);
+
+ // Switch to Cash
+ payment.Cash = new CashPayment("RCPT-999", new DateTime(2024, 12, 20));
+
+ // Ordinary properties should still remain unchanged
+ Assert.Equal("TXN-002", payment.TransactionId);
+ Assert.Equal(500.00m, payment.Amount);
+ Assert.Equal("EUR", payment.Currency);
+ Assert.Equal(PaymentRequest.ChoiceOf.Cash, payment.ChoiceType);
+ Assert.Null(payment.Card);
+ Assert.Null(payment.BankTransfer);
+ Assert.NotNull(payment.Cash);
+ }
+
+ #region Factory Method Tests
+
+ [Fact]
+ public void MixedProperties_CreateAsFactory_ShouldSetChoiceOnly()
+ {
+ var order = OrderChoice.CreateAsExpress(
+ new ExpressDelivery(new DateTime(2024, 12, 31), 30.00m)
+ );
+
+ Assert.Null(order.OrderId); // Ordinary property not set by factory
+ Assert.Equal(OrderChoice.ChoiceOf.Express, order.ChoiceType);
+ Assert.NotNull(order.Express);
+ Assert.Equal(30.00m, order.Express.SurchargeAmount);
+ }
+
+ [Fact]
+ public void MixedProperties_CreateAsFactory_ThenSetOrdinaryProperties()
+ {
+ var payment = PaymentRequest.CreateAsCard(
+ new CardPayment("4012888888881881", "03/27")
+ );
+
+ // Set ordinary properties after creation
+ payment.TransactionId = "TXN-FACTORY";
+ payment.Amount = 299.99m;
+ payment.Currency = "GBP";
+
+ Assert.Equal("TXN-FACTORY", payment.TransactionId);
+ Assert.Equal(299.99m, payment.Amount);
+ Assert.Equal("GBP", payment.Currency);
+ Assert.Equal(PaymentRequest.ChoiceOf.Card, payment.ChoiceType);
+ Assert.NotNull(payment.Card);
+ }
+
+ #endregion
+
+ #region Match and Switch Tests
+
+ [Fact]
+ public void MixedProperties_Match_ShouldAccessChoiceAndOrdinaryProperties()
+ {
+ var payment = new PaymentRequest
+ {
+ TransactionId = "TXN-MATCH",
+ Amount = 100.00m,
+ Currency = "USD",
+ Card = new CardPayment("4111111111111111", "01/26")
+ };
+
+ var result = payment.Match(
+ matchCard: card => $"Card payment: {card.CardNumber} for {payment.Amount} {payment.Currency}",
+ matchBankTransfer: bank => $"Bank transfer to {bank.IBAN}",
+ matchCash: cash => $"Cash payment receipt {cash.ReceiptNumber}"
+ );
+
+ Assert.Equal("Card payment: 4111111111111111 for 100.00 USD", result);
+ }
+
+ [Fact]
+ public void MixedProperties_Switch_ShouldAccessChoiceAndOrdinaryProperties()
+ {
+ var payment = new PaymentRequest
+ {
+ TransactionId = "TXN-SWITCH",
+ Amount = 250.00m,
+ Currency = "EUR",
+ BankTransfer = new BankTransfer("DE89370400440532013000", "COBADEFFXXX")
+ };
+
+ string output = string.Empty;
+
+ payment.Switch(
+ matchCard: card => output = $"Processing card {payment.TransactionId}",
+ matchBankTransfer: bank => output = $"Processing bank transfer {payment.TransactionId} to {bank.IBAN}",
+ matchCash: cash => output = $"Processing cash {payment.TransactionId}"
+ );
+
+ Assert.Equal("Processing bank transfer TXN-SWITCH to DE89370400440532013000", output);
+ }
+
+ #endregion
+
+ #region XML Serialization Tests
+
+ [Fact]
+ public void MixedProperties_XmlSerialization_OrderChoice_ShouldRoundTrip()
+ {
+ var order = new OrderChoice
+ {
+ OrderId = "ORD-XML-001",
+ Express = new ExpressDelivery(new DateTime(2024, 12, 25), 25.00m)
+ };
+
+ var serializer = new XmlSerializer(typeof(OrderChoice));
+
+ // Serialize
+ using var sw = new StringWriter();
+ using var writer = XmlWriter.Create(sw, s_xmlWriterSettings);
+ serializer.Serialize(writer, order, XmlNamespaceHelper.EmptyNamespace);
+
+ var xml = sw.ToString();
+
+ // Verify structure
+ Assert.Contains("ORD-XML-001", xml);
+ Assert.Contains("", xml);
+ Assert.DoesNotContain("TXN-XML-001", xml);
+ Assert.Contains("150.00", xml);
+ Assert.Contains("USD", xml);
+ Assert.Contains("", xml);
+ Assert.DoesNotContain("TXN-XML-002", xml);
+ Assert.Contains("500.00", xml);
+ Assert.Contains("EUR", xml);
+ Assert.Contains("", xml);
+ Assert.DoesNotContain("", xml);
+ Assert.DoesNotContain("TXN-NO-CHOICE", xml);
+ Assert.Contains("99.99", xml);
+ Assert.Contains("GBP", xml);
+ Assert.DoesNotContain("", xml);
+ Assert.DoesNotContain("", xml);
+ Assert.DoesNotContain("", xml);
+ Assert.DoesNotContain("xsi:nil", xml);
+ }
+
+ #endregion
+
+ #region Edge Cases
+
+ [Fact]
+ public void MixedProperties_ModifyOrdinaryPropertyMultipleTimes_ChoiceShouldRemainStable()
+ {
+ var order = new OrderChoice
+ {
+ OrderId = "ORD-001",
+ Express = new ExpressDelivery(new DateTime(2024, 12, 25), 25.00m)
+ };
+
+ // Modify ordinary property multiple times
+ order.OrderId = "ORD-002";
+ order.OrderId = "ORD-003";
+ order.OrderId = "ORD-004";
+
+ Assert.Equal("ORD-004", order.OrderId);
+ Assert.Equal(OrderChoice.ChoiceOf.Express, order.ChoiceType);
+ Assert.NotNull(order.Express);
+ }
+
+ [Fact]
+ public void MixedProperties_SetOrdinaryPropertyToNull_ChoiceShouldRemainUnaffected()
+ {
+ var order = new OrderChoice
+ {
+ OrderId = "ORD-NULL",
+ Standard = new StandardDelivery(3, 5.00m)
+ };
+
+ order.OrderId = null;
+
+ Assert.Null(order.OrderId);
+ Assert.Equal(OrderChoice.ChoiceOf.Standard, order.ChoiceType);
+ Assert.NotNull(order.Standard);
+ }
+
+ [Fact]
+ public void MixedProperties_ComplexScenario_AllPropertiesModified()
+ {
+ var payment = new PaymentRequest
+ {
+ TransactionId = "INIT",
+ Amount = 100m,
+ Currency = "USD",
+ Card = new CardPayment("1234", "01/25")
+ };
+
+ // Modify ordinary properties
+ payment.TransactionId = "UPDATED-1";
+ payment.Amount = 200m;
+
+ // Switch choice
+ payment.BankTransfer = new BankTransfer("IBAN123", "BIC456");
+
+ // Modify ordinary properties again
+ payment.Currency = "EUR";
+ payment.TransactionId = "UPDATED-2";
+
+ // Switch choice again
+ payment.Cash = new CashPayment("RCPT-001", DateTime.Now);
+
+ // Modify ordinary properties one more time
+ payment.Amount = 300m;
+
+ // Verify final state
+ Assert.Equal("UPDATED-2", payment.TransactionId);
+ Assert.Equal(300m, payment.Amount);
+ Assert.Equal("EUR", payment.Currency);
+ Assert.Equal(PaymentRequest.ChoiceOf.Cash, payment.ChoiceType);
+ Assert.Null(payment.Card);
+ Assert.Null(payment.BankTransfer);
+ Assert.NotNull(payment.Cash);
+ }
+
+ #endregion
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ObsoleteTypeChoiceTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ObsoleteTypeChoiceTests.cs
new file mode 100644
index 0000000..26fc03b
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/ObsoleteTypeChoiceTests.cs
@@ -0,0 +1,85 @@
+using AltaSoft.Choice;
+using System;
+using Xunit;
+
+namespace AltaSoft.ChoiceGenerator.Tests;
+
+///
+/// Choice type using obsolete types - verifies warning suppression
+///
+[Choice]
+public sealed partial class PaymentMethodChoice
+{
+ ///
+ /// Legacy payment method - obsolete but still supported
+ ///
+ public partial LegacyPayment? Legacy { get; set; }
+
+ ///
+ /// Modern payment method
+ ///
+ public partial ModernPayment? Modern { get; set; }
+}
+
+///
+/// Obsolete payment type - still used in some legacy systems
+///
+[Obsolete("Use ModernPayment instead", false)]
+public sealed class LegacyPayment
+{
+ public string CardNumber { get; set; } = string.Empty;
+ public LegacyPayment() { }
+}
+
+///
+/// Modern payment type
+///
+public sealed class ModernPayment
+{
+ public string Token { get; set; } = string.Empty;
+ public ModernPayment() { }
+}
+
+///
+/// Tests for Choice types using obsolete types - verifies no warnings in generated code
+///
+public class ObsoleteTypeChoiceTests
+{
+ [Fact]
+ public void ObsoleteType_CanBeUsedInChoice_WithoutWarnings()
+ {
+ // The generated code should have CS0618 suppressed, so using obsolete types works
+ var payment = PaymentMethodChoice.CreateAsLegacy(new LegacyPayment { CardNumber = "1234" });
+
+ Assert.Equal(PaymentMethodChoice.ChoiceOf.Legacy, payment.ChoiceType);
+ Assert.NotNull(payment.Legacy);
+ Assert.Equal("1234", payment.Legacy.CardNumber);
+ }
+
+ [Fact]
+ public void ObsoleteType_Switch_ShouldWork()
+ {
+ var payment = PaymentMethodChoice.CreateAsLegacy(new LegacyPayment { CardNumber = "5678" });
+
+ var result = string.Empty;
+ payment.Switch(
+ legacy => result = $"Legacy: {legacy.CardNumber}",
+ modern => result = $"Modern: {modern.Token}"
+ );
+
+ Assert.Equal("Legacy: 5678", result);
+ }
+
+ [Fact]
+ public void ObsoleteType_Match_ShouldWork()
+ {
+ var payment = PaymentMethodChoice.CreateAsLegacy(new LegacyPayment { CardNumber = "9012" });
+
+ var result = payment.Match(
+ legacy => $"Legacy: {legacy.CardNumber}",
+ modern => $"Modern: {modern.Token}"
+ );
+
+ Assert.Equal("Legacy: 9012", result);
+ }
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/RequiredPropertiesChoiceTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/RequiredPropertiesChoiceTests.cs
new file mode 100644
index 0000000..02c9673
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/RequiredPropertiesChoiceTests.cs
@@ -0,0 +1,126 @@
+using AltaSoft.Choice;
+using Xunit;
+
+namespace AltaSoft.ChoiceGenerator.Tests;
+
+///
+/// Choice type with required ordinary properties - demonstrates generator enhancement
+///
+[Choice]
+public sealed partial class OrderWithRequiredProperties
+{
+ ///
+ /// Required ordinary property - Order ID
+ ///
+ public required string OrderId { get; set; }
+
+ ///
+ /// Required ordinary property - Customer Name
+ ///
+ public required string CustomerName { get; set; }
+
+ ///
+ /// Choice property - Express delivery
+ ///
+ public partial ExpressDelivery? Express { get; set; }
+
+ ///
+ /// Choice property - Standard delivery
+ ///
+ public partial StandardDelivery? Standard { get; set; }
+}
+
+///
+/// Tests for Choice types with required ordinary properties
+///
+public class RequiredPropertiesChoiceTests
+{
+ [Fact]
+ public void RequiredProperties_CreateAsExpress_ShouldRequireOrdinaryProperties()
+ {
+ // This should work after the fix - note camelCase parameter names
+ var order = OrderWithRequiredProperties.CreateAsExpress(
+ "ORD-001", // orderId
+ "John Doe", // customerName
+ new ExpressDelivery(new System.DateTime(2024, 12, 25), 25.00m) // express (value)
+ );
+
+ Assert.Equal("ORD-001", order.OrderId);
+ Assert.Equal("John Doe", order.CustomerName);
+ Assert.Equal(OrderWithRequiredProperties.ChoiceOf.Express, order.ChoiceType);
+ Assert.NotNull(order.Express);
+ }
+
+ [Fact]
+ public void RequiredProperties_CreateAsStandard_ShouldRequireOrdinaryProperties()
+ {
+ // This should work after the fix - note camelCase parameter names
+ var order = OrderWithRequiredProperties.CreateAsStandard(
+ "ORD-002", // orderId
+ "Jane Smith", // customerName
+ new StandardDelivery(5, 10.00m) // standard (value)
+ );
+
+ Assert.Equal("ORD-002", order.OrderId);
+ Assert.Equal("Jane Smith", order.CustomerName);
+ Assert.Equal(OrderWithRequiredProperties.ChoiceOf.Standard, order.ChoiceType);
+ Assert.NotNull(order.Standard);
+ }
+
+ [Fact]
+ public void RequiredPropertyNamedValue_ShouldNotConflictWithChoiceParameter()
+ {
+ // This tests the edge case where required ordinary properties would collide
+ // with both the default choice parameter name and its first fallback.
+ var config = ConfigWithValueProperty.CreateAsOptionA(
+ "CONFIG-123", // value (required property)
+ "CONFIG-ALT", // choiceValue (required property)
+ new OptionA("Option A Data") // choiceValue1 (choice parameter - uniquely renamed)
+ );
+
+ Assert.Equal("CONFIG-123", config.Value);
+ Assert.Equal("CONFIG-ALT", config.ChoiceValue);
+ Assert.Equal(ConfigWithValueProperty.ChoiceOf.OptionA, config.ChoiceType);
+ Assert.NotNull(config.OptionA);
+ Assert.Equal("Option A Data", config.OptionA.Data);
+ }
+}
+
+///
+/// Choice type with required properties named "Value" and "ChoiceValue" to test parameter conflict resolution
+///
+[Choice]
+public sealed partial class ConfigWithValueProperty
+{
+ ///
+ /// Required property named "Value" - this would conflict with the default choice parameter name
+ ///
+ public required string Value { get; set; }
+
+ ///
+ /// Required property named "ChoiceValue" - this would conflict with the first fallback choice parameter name
+ ///
+ public required string ChoiceValue { get; set; }
+
+ ///
+ /// Choice property - Option A
+ ///
+ public partial OptionA? OptionA { get; set; }
+
+ ///
+ /// Choice property - Option B
+ ///
+ public partial OptionB? OptionB { get; set; }
+}
+
+public sealed class OptionA
+{
+ public string Data { get; set; }
+ public OptionA(string data) => Data = data;
+}
+
+public sealed class OptionB
+{
+ public int Count { get; set; }
+ public OptionB(int count) => Count = count;
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/JsonSerializationHelper.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/JsonSerializationHelper.cs
new file mode 100644
index 0000000..5d2f141
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/JsonSerializationHelper.cs
@@ -0,0 +1,52 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace AltaSoft.ChoiceGenerator.Tests.TestHelpers;
+
+///
+/// Helper methods for JSON serialization in tests
+///
+public static class JsonSerializationHelper
+{
+ private static readonly JsonSerializerOptions s_options = new()
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ ///
+ /// Serializes an object to JSON string
+ ///
+ public static string SerializeToJson(T obj)
+ {
+ return JsonSerializer.Serialize(obj, s_options);
+ }
+
+ ///
+ /// Deserializes an object from JSON string
+ ///
+ public static T? DeserializeFromJson(string json)
+ {
+ return JsonSerializer.Deserialize(json, s_options);
+ }
+
+ ///
+ /// Performs a round-trip serialization test: object -> JSON -> object
+ ///
+ public static T? RoundTrip(T original)
+ {
+ var json = SerializeToJson(original);
+ return DeserializeFromJson(json);
+ }
+
+ ///
+ /// Normalizes JSON string for comparison (removes formatting differences)
+ ///
+ public static string NormalizeJson(string json)
+ {
+ // Parse and re-serialize to normalize formatting
+ using var doc = JsonDocument.Parse(json);
+ return JsonSerializer.Serialize(doc.RootElement, s_options);
+ }
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/XmlSerializationHelper.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/XmlSerializationHelper.cs
new file mode 100644
index 0000000..666da87
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/XmlSerializationHelper.cs
@@ -0,0 +1,67 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Xml;
+using System.Xml.Serialization;
+using AltaSoft.Choice;
+
+namespace AltaSoft.ChoiceGenerator.Tests.TestHelpers;
+
+///
+/// Helper methods for XML serialization in tests
+///
+public static class XmlSerializationHelper
+{
+ private static readonly XmlWriterSettings s_writerSettings = new()
+ {
+ OmitXmlDeclaration = true,
+ Indent = true,
+ Encoding = Encoding.UTF8
+ };
+
+ ///
+ /// Serializes an object to XML string
+ ///
+ public static string SerializeToXml(T obj) where T : class
+ {
+ var serializer = new XmlSerializer(typeof(T));
+ using var sw = new StringWriter();
+ using var writer = XmlWriter.Create(sw, s_writerSettings);
+
+ serializer.Serialize(writer, obj, XmlNamespaceHelper.EmptyNamespace);
+
+ return sw.ToString();
+ }
+
+ ///
+ /// Deserializes an object from XML string
+ ///
+ public static T DeserializeFromXml(string xml) where T : class
+ {
+ var serializer = new XmlSerializer(typeof(T));
+ using var reader = new StringReader(xml);
+
+ var result = serializer.Deserialize(reader) as T;
+ if (result == null)
+ throw new InvalidOperationException($"Failed to deserialize XML to type {typeof(T).Name}");
+
+ return result;
+ }
+
+ ///
+ /// Performs a round-trip serialization test: object -> XML -> object
+ ///
+ public static T RoundTrip(T original) where T : class
+ {
+ var xml = SerializeToXml(original);
+ return DeserializeFromXml(xml);
+ }
+
+ ///
+ /// Normalizes XML string for comparison (removes formatting differences)
+ ///
+ public static string NormalizeXml(string xml)
+ {
+ return xml.Trim().Replace("\r\n", "\n");
+ }
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/NotificationChannel.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/NotificationChannel.cs
new file mode 100644
index 0000000..17772f0
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/NotificationChannel.cs
@@ -0,0 +1,29 @@
+using System.Text.Json.Serialization;
+using AltaSoft.Choice;
+
+namespace AltaSoft.ChoiceGenerator.Tests.TestModels;
+
+///
+/// Represents a notification delivery channel (single property choice for testing)
+///
+[Choice]
+public sealed partial class NotificationChannel
+{
+ ///
+ /// The notification channel identifier
+ ///
+ [XmlTag("Channel")]
+ [JsonPropertyName("channel")]
+ public required partial NotificationChannelType Channel { get; set; }
+}
+
+///
+/// Types of notification channels
+///
+public enum NotificationChannelType
+{
+ Email,
+ SMS,
+ Push,
+ InApp
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/PaymentMethod.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/PaymentMethod.cs
new file mode 100644
index 0000000..8e857f0
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/PaymentMethod.cs
@@ -0,0 +1,89 @@
+using System.Text.Json.Serialization;
+using AltaSoft.Choice;
+
+namespace AltaSoft.ChoiceGenerator.Tests.TestModels;
+
+///
+/// Represents a payment method choice for e-commerce scenarios
+///
+[Choice]
+public sealed partial class PaymentMethod
+{
+ ///
+ /// Payment via credit or debit card
+ ///
+ [XmlTag("CreditCard")]
+ [JsonPropertyName("creditCard")]
+ public partial CreditCardPayment? CreditCard { get; set; }
+
+ ///
+ /// Payment via bank transfer
+ ///
+ [XmlTag("BankTransfer")]
+ [JsonPropertyName("bankTransfer")]
+ public partial BankTransferPayment? BankTransfer { get; set; }
+
+ ///
+ /// Payment via PayPal
+ ///
+ [XmlTag("PayPal")]
+ [JsonPropertyName("payPal")]
+ public partial PayPalPayment? PayPal { get; set; }
+}
+
+///
+/// Credit card payment details
+///
+public sealed class CreditCardPayment
+{
+ public string CardNumber { get; set; } = string.Empty;
+ public string CardHolderName { get; set; } = string.Empty;
+ public string ExpiryDate { get; set; } = string.Empty;
+ public string Cvv { get; set; } = string.Empty;
+
+ public CreditCardPayment() { }
+
+ public CreditCardPayment(string cardNumber, string cardHolderName, string expiryDate, string cvv)
+ {
+ CardNumber = cardNumber;
+ CardHolderName = cardHolderName;
+ ExpiryDate = expiryDate;
+ Cvv = cvv;
+ }
+}
+
+///
+/// Bank transfer payment details
+///
+public sealed class BankTransferPayment
+{
+ public string AccountNumber { get; set; } = string.Empty;
+ public string RoutingNumber { get; set; } = string.Empty;
+ public string BankName { get; set; } = string.Empty;
+
+ public BankTransferPayment() { }
+
+ public BankTransferPayment(string accountNumber, string routingNumber, string bankName)
+ {
+ AccountNumber = accountNumber;
+ RoutingNumber = routingNumber;
+ BankName = bankName;
+ }
+}
+
+///
+/// PayPal payment details
+///
+public sealed class PayPalPayment
+{
+ public string Email { get; set; } = string.Empty;
+ public string TransactionId { get; set; } = string.Empty;
+
+ public PayPalPayment() { }
+
+ public PayPalPayment(string email, string transactionId = "")
+ {
+ Email = email;
+ TransactionId = transactionId;
+ }
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/README.md b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/README.md
new file mode 100644
index 0000000..acb91f0
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/README.md
@@ -0,0 +1,142 @@
+# Test Models
+
+Realistic domain models for testing the AltaSoft Choice Generator.
+
+## Overview
+
+These models represent real-world scenarios to make tests more professional, maintainable, and understandable. They replace generic test types like `TwoValueTypeChoice` and `SinglePropertyChoice` with domain-specific models.
+
+## Models
+
+### PaymentMethod
+
+E-commerce payment choice supporting multiple payment types.
+
+**Choice Properties:**
+- `CreditCard` (CreditCardPayment) - Credit/debit card payment
+- `BankTransfer` (BankTransferPayment) - Direct bank transfer
+- `PayPal` (PayPalPayment) - PayPal payment
+
+**Helper Classes:**
+- `CreditCardPayment` - CardNumber, CardHolderName, ExpiryDate, Cvv
+- `BankTransferPayment` - AccountNumber, RoutingNumber, BankName
+- `PayPalPayment` - Email, TransactionId
+
+**Usage:**
+```csharp
+var card = new CreditCardPayment("4111111111111111", "John Doe", "12/25", "123");
+var payment = PaymentMethod.CreateAsCreditCard(card);
+
+payment.Match(
+ creditCard => ProcessCard(creditCard),
+ bankTransfer => ProcessBankTransfer(bankTransfer),
+ payPal => ProcessPayPal(payPal)
+);
+```
+
+**Tests:** Creation, Switch/Match, JSON serialization, XML serialization
+
+---
+
+### ShippingOption
+
+Shipping method choice for e-commerce orders.
+
+**Choice Properties:**
+- `Standard` (ShippingDetails) - Standard shipping
+- `Express` (ShippingDetails) - Express shipping
+- `Overnight` (ShippingDetails) - Overnight shipping
+
+**Helper Classes:**
+- `ShippingDetails` - Cost, EstimatedDays, Carrier
+
+**Usage:**
+```csharp
+var express = new ShippingDetails(15.99m, 2, "FedEx");
+var shipping = ShippingOption.CreateAsExpress(express);
+
+var totalCost = shipping.Match(
+ standard => standard.Cost,
+ express => express.Cost,
+ overnight => overnight.Cost
+);
+```
+
+**Tests:** Creation, Switch/Match with cost calculation, JSON/XML round-trips
+
+---
+
+### SearchCriteria
+
+Product catalog search choice supporting different search types.
+
+**Choice Properties:**
+- `Keyword` (string) - Text search
+- `CategoryId` (int) - Category filter
+- `DateRange` (DateRange class) - Date range filter
+- `PriceRange` (PriceRange struct) - Price range filter
+
+**Helper Classes:**
+- `DateRange` - StartDate, EndDate
+- `PriceRange` (struct) - MinPrice, MaxPrice
+
+**Usage:**
+```csharp
+var search = SearchCriteria.CreateAsKeyword("laptop");
+// OR
+var search = SearchCriteria.CreateAsCategoryId(42);
+// OR
+var search = SearchCriteria.CreateAsDateRange(new DateRange(start, end));
+
+var query = search.Match(
+ keyword => $"WHERE Name LIKE '%{keyword}%'",
+ categoryId => $"WHERE CategoryId = {categoryId}",
+ dateRange => $"WHERE Date BETWEEN '{dateRange.StartDate}' AND '{dateRange.EndDate}'",
+ priceRange => $"WHERE Price BETWEEN {priceRange.MinPrice} AND {priceRange.MaxPrice}"
+);
+```
+
+**Tests:** Mixed value types (string, int, class, struct), LINQ integration, JSON/XML serialization
+
+---
+
+### NotificationChannel
+
+Simple single-property choice for notification delivery.
+
+**Choice Properties:**
+- `Channel` (NotificationChannelType enum, required) - Email, SMS, Push, InApp
+
+**Usage:**
+```csharp
+var channel = NotificationChannel.CreateAsChannel(NotificationChannelType.Email);
+
+// Implicit conversion
+NotificationChannel smsChannel = NotificationChannelType.SMS;
+```
+
+**Tests:** Single-property choice, enum handling, implicit operators, required property
+
+---
+
+## Design Principles
+
+✅ **Realistic** - Models represent actual domain concepts (payments, shipping, search)
+✅ **Varied** - Different patterns (multi-choice, single-property, mixed types, structs)
+✅ **Documented** - XML doc comments on all types and properties
+✅ **Professional** - Follows C# naming conventions and patterns
+
+## JSON/XML Attributes
+
+Models use appropriate attributes for serialization control:
+- `[JsonPropertyName("camelCase")]` for JSON
+- `[XmlTag("PascalCase")]` for XML
+- Both respect Choice Generator conventions
+
+## Test Coverage
+
+These models are used across multiple test classes:
+- **ChoiceTypeCreationTests** - Factory methods and implicit operators
+- **ChoiceTypeSwitchMatchTests** - Match/Switch behavior
+- **ChoiceSerializationJsonTests** - JSON serialization round-trips
+- **ChoiceSerializationXmlTests** - XML serialization round-trips
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/SearchCriteria.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/SearchCriteria.cs
new file mode 100644
index 0000000..8fddc84
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/SearchCriteria.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Text.Json.Serialization;
+using AltaSoft.Choice;
+
+namespace AltaSoft.ChoiceGenerator.Tests.TestModels;
+
+///
+/// Represents search criteria for a product catalog
+/// Tests choices with different value types
+///
+[Choice]
+public sealed partial class SearchCriteria
+{
+ ///
+ /// Search by keyword (string)
+ ///
+ [XmlTag("Keyword")]
+ [JsonPropertyName("keyword")]
+ public partial string? Keyword { get; set; }
+
+ ///
+ /// Search by category ID (int)
+ ///
+ [XmlTag("CategoryId")]
+ [JsonPropertyName("categoryId")]
+ public partial int? CategoryId { get; set; }
+
+ ///
+ /// Search by date range
+ ///
+ [XmlTag("DateRange")]
+ [JsonPropertyName("dateRange")]
+ public partial DateRange? DateRange { get; set; }
+
+ ///
+ /// Search by price range
+ ///
+ [XmlTag("PriceRange")]
+ [JsonPropertyName("priceRange")]
+ public partial PriceRange? PriceRange { get; set; }
+}
+
+///
+/// Date range for filtering
+///
+public sealed class DateRange
+{
+ public DateTime StartDate { get; set; }
+ public DateTime EndDate { get; set; }
+
+ public DateRange() { }
+
+ public DateRange(DateTime startDate, DateTime endDate)
+ {
+ StartDate = startDate;
+ EndDate = endDate;
+ }
+}
+
+///
+/// Price range for filtering (struct to test value type behavior)
+///
+public struct PriceRange
+{
+ public decimal MinPrice { get; set; }
+ public decimal MaxPrice { get; set; }
+
+ public PriceRange(decimal minPrice, decimal maxPrice)
+ {
+ MinPrice = minPrice;
+ MaxPrice = maxPrice;
+ }
+}
diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/ShippingOption.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/ShippingOption.cs
new file mode 100644
index 0000000..144710e
--- /dev/null
+++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/ShippingOption.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Text.Json.Serialization;
+using AltaSoft.Choice;
+
+namespace AltaSoft.ChoiceGenerator.Tests.TestModels;
+
+///
+/// Represents shipping options for order delivery
+/// Tests multiple choices of the same type (ShippingDetails)
+///
+[Choice]
+public sealed partial class ShippingOption
+{
+ ///
+ /// Standard shipping (5-7 business days)
+ ///
+ [XmlTag("Standard")]
+ [JsonPropertyName("standard")]
+ public partial ShippingDetails? Standard { get; set; }
+
+ ///
+ /// Express shipping (2-3 business days)
+ ///
+ [XmlTag("Express")]
+ [JsonPropertyName("express")]
+ public partial ShippingDetails? Express { get; set; }
+
+ ///
+ /// Overnight shipping (next business day)
+ ///
+ [XmlTag("Overnight")]
+ [JsonPropertyName("overnight")]
+ public partial ShippingDetails? Overnight { get; set; }
+}
+
+///
+/// Shipping details including cost and estimated delivery
+///
+public sealed class ShippingDetails
+{
+ public decimal Cost { get; set; }
+ public int EstimatedDays { get; set; }
+ public string Carrier { get; set; } = string.Empty;
+
+ public ShippingDetails() { }
+
+ public ShippingDetails(decimal cost, int estimatedDays, string carrier)
+ {
+ Cost = cost;
+ EstimatedDays = estimatedDays;
+ Carrier = carrier;
+ }
+}