0Pricing
C# Academy · Lesson

Domain modeling with discriminated unions (records) — C# 6 emulation

Model a discriminated union in C# 6 using a tag enum and small classes, create factory methods, and handle variants via switch with a default throw.

Domain modeling with discriminated unions (records) — C# 6 emulation is a free C# Academy lesson on CoddyKit — lesson 3 of 3. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 3 lessons in the course, and your progress syncs across the web and the CoddyKit app.

DU idea in C# 6

Aim: Build a small discriminated union in C# 6.

  • Tag enum for the active case
  • Small case classes for data
  • Factory methods to create valid cases
  • Switch + default throw to handle all variants

Tag + factories

Use a tag enum and factory methods to construct only valid cases; fields for other cases stay unused.

using System;

public enum PaymentKind { Cash, Card, Wire }

public sealed class Payment
{
  public PaymentKind Kind;

  // per-case data (only one is meaningful per instance)
  public string CardLast4; // for Card
  public string Iban;      // for Wire

  private Payment() { }

  // factories keep invariants
  public static Payment Cash()
  {
    Payment p = new Payment();
    p.Kind = PaymentKind.Cash;
    return p;
  }

  public static Payment Card(string last4)
  {
    if (string.IsNullOrEmpty(last4) || last4.Length != 4)
      throw new ArgumentException("last4 must be 4 digits", "last4");
    Payment p = new Payment();
    p.Kind = PaymentKind.Card;
    p.CardLast4 = last4;
    return p;
  }

  public static Payment Wire(string iban)
  {
    if (string.IsNullOrEmpty(iban))
      throw new ArgumentException("iban must not be empty", "iban");
    Payment p = new Payment();
    p.Kind = PaymentKind.Wire;
    p.Iban = iban;
    return p;
  }
}

public class Program
{
  public static void Main(string[] args)
  {
    Payment a = Payment.Cash();
    Payment b = Payment.Card("1234");
    Payment c = Payment.Wire("TR00BANKIBAN");
    Console.WriteLine(a.Kind + ", " + b.Kind + ", " + c.Kind);
  }
}

Handling variants

Classic switch over the tag gives clear handling. A default throw exposes unhandled variants during tests.

using System;

public enum PaymentKind { Cash, Card, Wire }

public sealed class Payment
{
  public PaymentKind Kind;
  public string CardLast4;
  public string Iban;

  public static Payment Cash()
  {
    Payment p = new Payment(); p.Kind = PaymentKind.Cash; return p;
  }
  public static Payment Card(string last4)
  {
    if (string.IsNullOrEmpty(last4) || last4.Length != 4) throw new ArgumentException("last4");
    Payment p = new Payment(); p.Kind = PaymentKind.Card; p.CardLast4 = last4; return p;
  }
  public static Payment Wire(string iban)
  {
    if (string.IsNullOrEmpty(iban)) throw new ArgumentException("iban");
    Payment p = new Payment(); p.Kind = PaymentKind.Wire; p.Iban = iban; return p;
  }
}

public static class Exhaustive
{
  public static Exception Unhandled(object x)
  {
    return new InvalidOperationException("Unhandled case: " + x);
  }
}

public class Program
{
  static string Describe(Payment p)
  {
    switch (p.Kind)
    {
      case PaymentKind.Cash: return "Cash";
      case PaymentKind.Card: return "Card ****" + p.CardLast4;
      case PaymentKind.Wire: return "Wire " + p.Iban;
      default: throw Exhaustive.Unhandled(p.Kind);
    }
  }

  public static void Main(string[] args)
  {
    Console.WriteLine(Describe(Payment.Card("9876")));
  }
}

Match helper (visitor-ish)

A small Match helper centralizes the switch and returns a value, keeping call sites clean.

using System;

public enum PaymentKind { Cash, Card, Wire }

public sealed class Payment
{
  public PaymentKind Kind;
  public string CardLast4;
  public string Iban;

  public static Payment Cash() { Payment p = new Payment(); p.Kind = PaymentKind.Cash; return p; }
  public static Payment Card(string last4) { Payment p = new Payment(); p.Kind = PaymentKind.Card; p.CardLast4 = last4; return p; }
  public static Payment Wire(string iban) { Payment p = new Payment(); p.Kind = PaymentKind.Wire; p.Iban = iban; return p; }
}

public static class PaymentMatch
{
  public static T Match<T>(
    Payment p,
    Func<T> onCash,
    Func<string, T> onCard,
    Func<string, T> onWire)
  {
    switch (p.Kind)
    {
      case PaymentKind.Cash: return onCash();
      case PaymentKind.Card: return onCard(p.CardLast4);
      case PaymentKind.Wire: return onWire(p.Iban);
      default: throw new InvalidOperationException("Unhandled: " + p.Kind);
    }
  }
}

public class Program
{
  public static void Main(string[] args)
  {
    Payment p = Payment.Card("1234");
    string summary = PaymentMatch.Match(
      p,
      delegate { return "Cash"; },
      delegate(string last4) { return "Card ****" + last4; },
      delegate(string iban) { return "Wire " + iban; }
    );
    Console.WriteLine(summary);
  }
}

Invariants via factories

Factories enforce invariants: each case initializes only its meaningful fields; invalid inputs are rejected early.

using System;

public enum ResultKind { Ok, Error }

public sealed class Result
{
  public ResultKind Kind;
  public string Message; // only for Error
  public int Value;      // only for Ok

  private Result() { }

  public static Result Ok(int value)
  {
    Result r = new Result();
    r.Kind = ResultKind.Ok;
    r.Value = value;
    r.Message = null;
    return r;
  }

  public static Result Error(string message)
  {
    if (string.IsNullOrEmpty(message)) throw new ArgumentException("message");
    Result r = new Result();
    r.Kind = ResultKind.Error;
    r.Message = message;
    r.Value = 0;
    return r;
  }
}

public class Program
{
  static string Show(Result r)
  {
    switch (r.Kind)
    {
      case ResultKind.Ok: return "OK: " + r.Value;
      case ResultKind.Error: return "ERROR: " + r.Message;
      default: throw new InvalidOperationException("Unhandled: " + r.Kind);
    }
  }

  public static void Main(string[] args)
  {
    Console.WriteLine(Show(Result.Ok(5)));
    Console.WriteLine(Show(Result.Error("Boom")));
  }
}

Tips & trade-offs

Tips:

  • Prefer small, focused cases and factories.
  • Handle variants in one place (switch helper) to keep code DRY.
  • Keep a default throw to reveal missing cases in tests.

Trade-offs: More boilerplate than modern records, but clear and C# 6-compatible.

DU emulation approach

Quick check: In C# 6, what is a simple way to emulate a discriminated union?

Recap

Recap: Model variants with a tag enum, build valid cases via factories, and centralize handling in a switch (default throws) for runtime exhaustiveness.

Frequently asked questions

Is the “Domain modeling with discriminated unions (records) — C# 6 emulation” lesson free?

Yes — the full text of “Domain modeling with discriminated unions (records) — C# 6 emulation” is free to read here on the web, and the C# Academy course includes 3 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.

What will I learn in “Domain modeling with discriminated unions (records) — C# 6 emulation”?

Model a discriminated union in C# 6 using a tag enum and small classes, create factory methods, and handle variants via switch with a default throw. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C# Academy?

No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 3, so you can start here or from the beginning and move at your own pace.

How long does the “Domain modeling with discriminated unions (records) — C# 6 emulation” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C# Academy lesson?

Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. is patterns; relational & logical patterns (C# 6 emulation)
  2. switch expressions; exhaustive checks with when (C# 6 emulation)
  3. Domain modeling with discriminated unions (records) — C# 6 emulation
← Back to C# Academy