Select Page

Validation refactoring should preserve the result a caller sees. This example converts an ordinary if validator into a pattern-based switch expression, then checks both against the same expected results.

It reports the first problem only. It does not collect every error, authorize a booking, check room availability, or enforce a real hotel’s policy. The small contract is deliberate: required name, 1–30 nights, positive guest count and capacity, supported room kind, kitchen requirements, and enough capacity.

Run the complete example on .NET 10

The example was compiled using .NET SDK 10.0.401, target net10.0, C# 14, nullable analysis enabled, implicit usings disabled, and warnings treated as errors. These particular language features require C# 9 or later: switch expressions/property patterns arrived in C# 8; records and relational/logical patterns arrived in C# 9. This tutorial uses a tested .NET 10 setup rather than claiming older SDK compatibility.

Create a console project, replace Program.cs with the complete program below, and run it:

dotnet new console -n PatternValidation --framework net10.0
cd PatternValidation
dotnet run

For an exact SDK match, put this global.json in the project directory:

{"sdk":{"version":"10.0.401","rollForward":"disable"}}

Use these project settings in PatternValidation.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14.0</LangVersion>
    <Nullable>enable</Nullable>
    <ImplicitUsings>disable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

Before and after: the same ordered rules

WithIfs is a fair baseline: it uses simple comparisons and needs no cast. WithPatterns groups each failing shape into an arm. Pick the form your team finds easier to maintain; this example makes no speed or allocation claim.

using System;

public enum RoomKind { Standard, Suite }

public sealed record Booking(
    string? GuestName, int Nights, int Guests, RoomKind Room,
    int Capacity, bool HasKitchen, bool NeedsKitchen);

public static class Validator
{
    public static string? WithIfs(Booking? booking)
    {
        if (booking is null) return "Request required.";
        if (string.IsNullOrWhiteSpace(booking.GuestName)) return "Name required.";
        if (booking.Nights <= 0) return "Stay must be at least one night.";
        if (booking.Nights > 30) return "Stay exceeds 30 nights.";
        if (booking.Guests <= 0) return "At least one guest required.";
        if (booking.Room != RoomKind.Standard && booking.Room != RoomKind.Suite)
            return "Unsupported room kind.";
        if (booking.Capacity <= 0) return "Room capacity must be positive.";
        if (booking.NeedsKitchen &&
            (booking.Room == RoomKind.Standard || !booking.HasKitchen))
            return "Selected room has no kitchen.";
        if (booking.Guests > booking.Capacity) return "Room capacity exceeded.";
        return null;
    }

    public static string? WithPatterns(Booking? booking) => booking switch
    {
        null => "Request required.",
        { GuestName: var name } when string.IsNullOrWhiteSpace(name)
            => "Name required.",
        { Nights: <= 0 } => "Stay must be at least one night.",
        { Nights: > 30 } => "Stay exceeds 30 nights.",
        { Guests: <= 0 } => "At least one guest required.",
        { Room: not (RoomKind.Standard or RoomKind.Suite) }
            => "Unsupported room kind.",
        { Capacity: <= 0 } => "Room capacity must be positive.",
        { NeedsKitchen: true, Room: RoomKind.Standard }
            => "Selected room has no kitchen.",
        { NeedsKitchen: true, HasKitchen: false }
            => "Selected room has no kitchen.",
        { Guests: var guests, Capacity: var capacity } when guests > capacity
            => "Room capacity exceeded.",
        _ => null
    };
}

public static class ArticleDemo
{
    public static void Main()
    {
        var valid = new Booking("Ada", 1, 2, RoomKind.Suite, 2, true, true);
        Console.WriteLine(Validator.WithPatterns(valid) ?? "Accepted.");
        Console.WriteLine(Validator.WithPatterns(valid with { Nights = 31 }));
    }
}

Expected output:

Accepted.
Stay exceeds 30 nights.

Read the branches as validation rules

{ Nights: <= 0 } checks a property against a constant boundary. { Room: not (RoomKind.Standard or RoomKind.Suite) } rejects enum values outside the two supported kinds. An enum can contain an unnamed numeric value, so this branch matters even though the enum declaration lists only two names.

A property pattern requires a non-null input; the first arm handles a missing booking. The name arm uses string.IsNullOrWhiteSpace to reject null, empty, and whitespace-only names. The final capacity comparison uses a when guard because it compares two input values, rather than a value and a constant. See Microsoft’s property-pattern and relational-pattern references.

The earliest matching arm supplies the result. If both name and nights are invalid, the name error wins. Changing the order changes the contract; the compiler does not decide which business rule should win. Microsoft documents arm selection and guards in the switch-expression reference.

Here _ => null means the preceding rules found no problem. It also handles all remaining inputs, so it cannot warn you about a business rule you forgot. The model is sealed, unsupported room kinds have an explicit rejection, and tests cover the defined policy. Adding a room kind or policy requires changing both validators and their tests. An accepted result is not permission to book.

Test the contract, including the uncomfortable cases

The published program built with zero warnings and errors. Separate fixtures checked 24 expected-result cases and compared both validators on 4,800 combinations of names, nights, guests, room kinds, capacities, and kitchen flags. Finite checks give useful evidence for these inputs; they do not prove every future rule correct.

Input change from the valid booking Expected first result
Null request Request required.
Null, empty, or whitespace-only name Name required.
Nights −1 or 0 Stay must be at least one night.
Nights 1 or 30 No problem
Nights 31 Stay exceeds 30 nights.
Room kind cast from 999 Unsupported room kind.
Standard room with a kitchen requirement Selected room has no kitchen.
Two guests, capacity two No problem
Three guests, capacity two Room capacity exceeded.
Empty name and zero nights Name required.

To adapt this validator, change one policy at a time, add an expected-result case, and rerun the before/after comparison. If the interface needs every error at once, design and test a separate collection-returning validator; replacing first-match control flow with a few independent checks does not automatically preserve the rules.

Next steps

Continue through the C# learning paths, or explore the C# packages directory once your application needs libraries. For language details, read the patterns reference, language-version configuration, and C# version history. Source and fixture checks were recorded on 4 October 2026.

Share This