Share via


Switch statement C#

Introduction

A look at different ways to use the switch statement from conventional to new new invocations ranging from declaration pattern in C# 7 to switch expressions introduced in C# 8 with and without pattern matching for C# 9.

Prerequisites

  • Microsoft Visual Studio 2019 or higher
  • .NET Core 5 Framework installed
  • For the Entity Framework Core code sample, Microsoft SQL-Server (minimum Express Edition)

Code samples

In some cases code samples can not simply be copied and pasted into code but by cloning the GitHub code these partial samples are fully functional unlike many code samples found on the web.

Switch vs if statement

Both switch and if statements are both valid ways to control code flow while after five branches for an if should move to switches. In code examples presented, in many cases only two or three branches/arms of switch statements are used for demonstration purposes which break the five plus condition rule. 

Shiny new features

Developers inherently want to try and new features which does not mean these new features should be used because they are new like with new incarnations of the switch statement. 

Basics

Keep with easy to following examples, using a if-else to determine if a integer is in specific ranges using a language extension method for keeping business logic clean.

public static  string Determination(this int  sender)
{
    var result = "";
     
    if (sender < 0)
    {
        result = "Less than or equal to 0";
    }else if  (sender > 0 && sender <= 10)
    {
        result = "More than 0 but less than or equal to 10";
    }
    else
    {
        result = "More than 10";
    }
 
    return result;
}

Usage

Console.WriteLine(2.Determination());

Now let's move from if-else to conventional switch.

public static  string Determination(this int  sender)
{
    var result = "";
 
    switch (sender)
    {
        case < 0:
            result = "Less than or equal to 0";
            break;
        case > 0 and <= 10:
            result = "More than 0 but less than or equal to 10";
            break;
        default:
            result = "More than 10";
            break;
    }
 
    return result;
}

Usage remains the same as if-else. The last example can be cleaned up with C# 8 and higher with switch expressions as presented below. Much less code although when sharing code with others in or in a team with less experience with more switch arms this may be harder to read. Also, the original developer may have issues figuring out how this extension method functions. 

public static  string Determination(this int  sender) => sender switch
    {
        < 0 => "Less than or equal to 0",
        > 0 and <= 10 => "More than 0 but less than or equal to 10",
        _ => "More than 10"
    };

Same action for different values

With conventional switch statements to handle different values we place two cases together. For example, in the following code sample if CostCenter is 042 or 043 the same address is returned.

public static  string Conventional()
{
    string userAddress;
 
    switch (EnvironmentData.CostCenter)
    {
        case "040":
            userAddress = "P O BOX 14135 * SALEM, OR  97309-5068\r\n(877) 345 - 3484 or Fax(503) 947 - 1335\r\n";
            break;
        case "042":
        case "043":
            userAddress = "875 Union Street NE * SALEM, OR  97311\r\n(503) 947-1669 or Fax (503) 947-1668\r\n";
            break;
        case "044":
            userAddress = "P O BOX 14130 * SALEM, OR  97309-5046\r\n(503) 947-1995 or Fax (503) 947-1811\r\n";
            break;
        case "045":
            userAddress = "PO BOX 14518 * Salem, Oregon  97309\r\n(800) 436-6191 or Fax (877) 353-7700\r\n";
            break;
        case "200":
            userAddress = "PO Box 14135 * Salem, Oregon  97309 5068\r\n(503) 292-2057 or (877) 345-3484 (in Oregon)\r\n" + 
                          "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878";
            break;
        case "300":
            userAddress = "PO Box 14135 * Salem, Oregon  97309 5068\r\n(877) 345-3484 or Fax to (866) 345-1878";
            break;
        case "700":
            userAddress = "PO Box 14135 * Salem, Oregon  97309 5068\r\n(541) 388-6207 or (877) 345-3484 (in Oregon)\r\n" + 
                          "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878";
            break;
        case "990":
            userAddress = "PO Box 14135 * Salem, Oregon  97309 5068\r\n(503) 292-2057 or (877) 345-3484 (in Oregon)\r\n" + 
                          "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878";
            break;
        default:
            userAddress = "875 Union Street NE\r\nSalem, OR  97311\r\n(800) 237-3710, Fax to (866) 345-1878";
            break;
    }
 
    EnvironmentData.UserAddress = userAddress;
 
    return userAddress;
     
}

In the above code sample the last two lines can be optimized by removing the return statement and simply set UserAddress directly.

public static  void ExpressionBodiedMember()
{
    EnvironmentData.UserAddress = EnvironmentData.CostCenter switch
    {
        var value when value.InCondition("043","044") => 
            "875 Union Street NE * SALEM, OR  97311\r\n(503) 947-1669 or Fax (503) 947-1668\r\n",
            "040" => "P O BOX 14135 * SALEM, OR  97309-5068\r\n(877) 345 - 3484 or Fax(503) 947 - 1335\r\n",
            "042" => "875 Union Street NE * SALEM, OR  97311\r\n(503) 947-1669 or Fax (503) 947-1668\r\n",
            "045" => "PO BOX 14518 * Salem, Oregon  97309\r\n(800) 436-6191 or Fax (877) 353-7700\r\n",
            "200" => "PO Box 14135 * Salem, Oregon  97309 5068\r\n(503) 292-2057 or (877) 345-3484 (in Oregon)\r\n" +
                    "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878",
            "300" => "PO Box 14135 * Salem, Oregon  97309 5068\r\n(877) 345-3484 or Fax to (866) 345-1878",
            "700" => "PO Box 14135 * Salem, Oregon  97309 5068\r\n(541) 388-6207 or (877) 345-3484 (in Oregon)\r\n" +
                    "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878",
            "990" => "PO Box 14135 * Salem, Oregon  97309 5068\r\n(503) 292-2057 or (877) 345-3484 (in Oregon)\r\n" +
                    "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878",
        _ => 
            "875 Union Street NE\r\nSalem, OR  97311\r\n(800) 237-3710, Fax to (866) 345-1878"
    };
 
}

Although a return is not need there is still a variable, let's optimize down to setting EnvironmentData.UserAddress using a void method.

public static  void ExpressionBodiedMember() {
     
    EnvironmentData.UserAddress = EnvironmentData.CostCenter switch
    {
        "040" => "P O BOX 14135 * SALEM, OR  97309-5068\r\n(877) 345 - 3484 or Fax(503) 947 - 1335\r\n",
        "042" or "043"  => "875 Union Street NE * SALEM, OR  97311\r\n(503) 947-1669 or Fax (503) 947-1668\r\n",
        "044" => "P O BOX 14130 * SALEM, OR  97309-5046\r\n(503) 947-1995 or Fax (503) 947-1811\r\n",
        "045" => "PO BOX 14518 * Salem, Oregon  97309\r\n(800) 436-6191 or Fax (877) 353-7700\r\n",
        "200" => "PO Box 14135 * Salem, Oregon  97309 5068\r\n(503) 292-2057 or (877) 345-3484 (in Oregon)\r\n" + 
                 "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878",
        "300" => "PO Box 14135 * Salem, Oregon  97309 5068\r\n(877) 345-3484 or Fax to (866) 345-1878",
        "700" => "PO Box 14135 * Salem, Oregon  97309 5068\r\n(541) 388-6207 or (877) 345-3484 (in Oregon)\r\n" + 
                 "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878",
        "990" => "PO Box 14135 * Salem, Oregon  97309 5068\r\n(503) 292-2057 or (877) 345-3484 (in Oregon)\r\n" + 
                 "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878",
        _ => "875 Union Street NE\r\nSalem, OR  97311\r\n(800) 237-3710, Fax to (866) 345-1878"
    };
 
}

A variation on the above as each developer tends to prefer a particular style. 

public static  void ExpressionBodiedMember()
{
 
    EnvironmentData.UserAddress = 
        EnvironmentData.CostCenter is  "040" ? "P O BOX 14135 * SALEM, OR  97309-5068\r\n(877) 345 - 3484 or Fax(503) 947 - 1335\r\n" :
        EnvironmentData.CostCenter is  "042" or "043" ? "875 Union Street NE * SALEM, OR  97311\r\n(503) 947-1669 or Fax (503) 947-1668\r\n" :
        EnvironmentData.CostCenter is  "044" ? "P O BOX 14130 * SALEM, OR  97309-5046\r\n(503) 947-1995 or Fax (503) 947-1811\r\n" :
        EnvironmentData.CostCenter is  "045" ? "PO BOX 14518 * Salem, Oregon  97309\r\n(800) 436-6191 or Fax (877) 353-7700\r\n" :
        EnvironmentData.CostCenter is  "200" ? "PO Box 14135 * Salem, Oregon  97309 5068\r\n(503) 292-2057 or (877) 345-3484 (in Oregon)\r\n" +
                                              "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878" :
        EnvironmentData.CostCenter is  "300" ? "PO Box 14135 * Salem, Oregon  97309 5068\r\n(877) 345-3484 or Fax to (866) 345-1878" :
        EnvironmentData.CostCenter is  "700" ? "PO Box 14135 * Salem, Oregon  97309 5068\r\n(541) 388-6207 or (877) 345-3484 (in Oregon)\r\n" +
                                              "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878" :
        EnvironmentData.CostCenter is  "990" ? "PO Box 14135 * Salem, Oregon  97309 5068\r\n(503) 292-2057 or (877) 345-3484 (in Oregon)\r\n" +
                                              "(877) 345-3484 (outside Oregon) or Fax to (866) 345-1878" : "875 Union Street NE\r\nSalem, OR  97311\r\n(800) 237-3710, Fax to (866) 345-1878";
 
}

A consideration is always keeping on top of the values/codes (in this case) might change which is a good reason to not placed copies of switches into business logic.

Switch with methods

As with conventional switch statements passing actions to methods, the same can be done with switch expressions. In the following example an action is performed based on a customer contact type.

Contact types are defined in an enum.

[GeneratedCode("TextTemplatingFileGenerator", "10")]
public enum  ContactType
{
    AccountingManager = 1,
    AssistantSalesAgent = 2,
    AssistantSalesRepresentative = 3,
    MarketingAssistant = 4,
    MarketingManager = 5,
    OrderAdministrator = 6,
    Owner = 7,
    OwnerMarketingAssistant = 8,
    SalesAgent = 9,
    SalesAssociate = 10,
    SalesManager = 11,
    SalesRepresentative = 12,
    VicePresidentSales = 13
}

Then imagine a customer action is needed dependent on their contact type.

  • In the code the action for most arms of the switch are one line do nothing methods while one does more which means there is not limitations on what can be done with the switch.
  • Both Func and Actions can be used.
    public static  Action ContactTypeAction(ContactType contactType) => contactType switch
    {
        ContactType.AccountingManager => AccountingManagerMethod,
        ContactType.AssistantSalesAgent => AssistantSalesAgentMethod, 
        ContactType.AssistantSalesRepresentative => AssistantSalesRepresentativeMethod,
        ContactType.MarketingAssistant => MarketingAssistantMethod,  
        ContactType.MarketingManager => MarketingManagerMethod,
        ContactType.OrderAdministrator => OrderAdmin, 
        ContactType.OwnerMarketingAssistant => OwnerMarketingAssistantMethod,
        ContactType.Owner => OwnerMethod, 
        ContactType.SalesAgent => SalesAgentMethod,
        ContactType.SalesAssociate => SalesAssociateMethod,
        ContactType.SalesManager => SalesManagerMethod, 
        ContactType.SalesRepresentative  => SalesRepresentativeMethod,
        ContactType.VicePresidentSales => VicePresidentMethod,
        _ => ContactTypeDefaultMethod
    };
 
    private static  void AccountingManagerMethod() => WriteLine("Accounting manager method");
    private static  void AssistantSalesRepresentativeMethod() => WriteLine("Assistant Sales Representative method");
    private static  void AssistantSalesAgentMethod() => WriteLine("Assistant sales agent method");
    private static  void ContactTypeDefaultMethod() => WriteLine($"Unknown contact type");
    private static  void SalesAgentMethod() => WriteLine("Sales Agent method");
    private static  void SalesRepresentativeMethod() => WriteLine("Sales Representative method");
    private static  void MarketingManagerMethod() => WriteLine("Marketing Manager method");
    /// <summary>
    /// Let's do some more work than the others
    /// </summary>
    private static  void OwnerMethod()
    {
 
        var fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Owner.txt");
        File.WriteAllText(fileName, "An owner");
         
        WriteLine("Owner method");
         
    }
    private static  void OrderAdmin() => WriteLine("Order Administrator method");
    private static  void OwnerMarketingAssistantMethod() => WriteLine("Owner Marketing Assistant method");
    private static  void SalesAssociateMethod() => WriteLine("Sales Associate method");
    private static  void VicePresidentMethod() => WriteLine("VP method");
    private static  void SalesManagerMethod() => WriteLine("Sales Manager method");
    private static  void MarketingAssistantMethod() => WriteLine("Marketing assistant method");
 
    /// <summary>
    /// Working with Func
    /// </summary>
    public static  Func<ContactType, string> ContactTypeFunc = contactType =>
    {
        return contactType switch
        {
            ContactType.AccountingManager => "AM",
            ContactType.AssistantSalesAgent => "ASG",
            ContactType.AssistantSalesRepresentative => "ADR",
            _ => "Unknown"
        };
    };
}

Default pass through

With conventional switches there is a default arm, with expressions a discard can be used via _ =>. If there are many places were there is a common default, create a method e.g.

public static  bool DefaultSwitch() => false;

Example usage

public static  class Example
{
    public static  bool Demo(int item, string folder) => item switch
    {
        1 => Operations.DoSomething1(folder),
        2 => Operations.DoSomething2(folder),
        _ => Operations.DefaultSwitch()
    };
}

Practical sample 1

At one point or another the will be a need to start an external process which is done with Process.Start. In the following code conventional if, switch and switch expressions are shown with conditions on file extension.

Which one is right is dependent on the developer although keep in mind that a if should not be used with many else if's.

public class  ProcessCodeSample
{
    private static  readonly string  BasePath = 
        Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Processed");
     
    public static  void ExpressionSwitch(string filename, string extension)
    {
 
        var process = new  Process
        {
            StartInfo =
            {
                FileName = extension switch
                {
                    "docx" => "WINWORD.EXE",
                    "doc" => "WINWORD.EXE",
                    "xlsx" => "EXCEL.EXE",
                    "xls" => "EXCEL.EXE",
                    "pdf" => "AcroRd32.exe",
                    _ => throw  new NotImplementedException("Unknown file type")
                }
            }
        };
 
 
        if (!File.Exists(Path.Combine(BasePath, filename)))  return;
 
        process.StartInfo.Arguments = Path.Combine(BasePath, filename);
        process.Start();
 
    }
    public static  void SwitchConventional(string filename, string extension)
    {
 
        var process = new  Process();
 
        switch (extension)
        {
            case "docx":
            case "doc":
                process.StartInfo.FileName = "WINWORD.EXE";
                break;
            case "xlsx":
            case "xls":
                process.StartInfo.FileName = "EXCEL.EXE";
                break;
            case "pdf":
                process.StartInfo.FileName = "AcroRd32.exe";
                break;
            default:
                throw new  NotImplementedException("Unknown file type");
        }
 
        if (!File.Exists(Path.Combine(BasePath, filename)))  return;
 
        process.StartInfo.Arguments = Path.Combine(BasePath, filename);
        process.Start();
 
    }
    public static  void IfConventional(string filename, string extension)
    {
 
        var process = new  Process();
 
        if (extension == "docx" || extension == "doc")
        {
            process.StartInfo.FileName = "WINWORD.EXE";
        }
        else if  (extension == "xlsx"  || extension == "xls")
        {
            process.StartInfo.FileName = "EXCEL.EXE";
        }
        else if  (extension == "pdf")
        {
            process.StartInfo.FileName = "AcroRd32.exe";
        }
        else
            throw new  NotImplementedException("Unknown file type");
 
        if (!File.Exists(Path.Combine(BasePath, filename)))  return;
 
        process.StartInfo.Arguments = Path.Combine(BasePath, filename);
        process.Start();
 
    }
}

Practical sample 2

Suppose there is a need to cycle through various states, similar to a traffic light. The same pattern shown prior can be used.

public enum  TrafficLightState
{
    Red,
    Yellow,
    Green
};
 
public static  TrafficLightState GetNextLight(TrafficLightState currentLight) => currentLight  switch
{
    TrafficLightState.Red => TrafficLightState.Yellow,
    TrafficLightState.Yellow => TrafficLightState.Green,
    TrafficLightState.Green => TrafficLightState.Red,
    _ => throw  new InvalidTrafficLightState()
};
 
public static  void TrafficCodeSample()
{
    TrafficLightState originalLightState = TrafficLightState.Yellow;
    TrafficLightState currentTrafficLightState = GetNextLight(originalLightState);
    Console.WriteLine(currentTrafficLightState.ToString());
}

Practical sample 3

Suppose various aspects of an application needs to be setup for user roles? Keeping things simple, there are two roles.

public enum  ApplicationRoles
{
    User,
    Admin
}

Back to expressions, depending on the role an action is performed.

private static  void ExpressionBodiedMember1_enum(ApplicationRoles caseSwitch) =>
    (
        caseSwitch switch
        {
            ApplicationRoles.User => (Action)Operations.Case1,
            ApplicationRoles.Admin => Operations.Case2,
            _ => throw  new ArgumentOutOfRangeException("Dude, unknown member for case")
        })
    ();

Practical sample 4

Developers use log files often and tend to write everything to one file while another approach is to have several log files to separate between general logging, exceptions etc.

A conventional method to accomplish this.

public static  void WriteConventional(Exception exception, ExceptionLogType exceptionLogType = ExceptionLogType.General)
{
    var fileName = "";
 
    switch (exceptionLogType)
    {
        case ExceptionLogType.Post:
            fileName = "PostUnhandledException.txt";
            break;
        case ExceptionLogType.General:
            fileName = "GeneralUnhandledException.txt";
            break;
        case ExceptionLogType.Data:
            fileName = "DataUnhandledException.txt";
            break;
        case ExceptionLogType.Unknown:
            fileName = "UnknownUnhandledException.txt";
            break;
        default: throw  new NotImplementedException();
    }
 
 
    Debug.WriteLine($"Conventional: {fileName}");
 
}

Or with C# 8 and higher, much cleaner and easy to read.

public static  void WriteByExpression(Exception exception, ExceptionLogType exceptionLogType = ExceptionLogType.General)
{
    var fileName = exceptionLogType switch
    {
        ExceptionLogType.General => "GeneralUnhandledException.txt",
        ExceptionLogType.Data => "DataUnhandledException.txt",
        ExceptionLogType.Unknown => "UnknownUnhandledException.txt",
        ExceptionLogType.Post => "PostUnhandledException.txt",
        _ => throw  new NotImplementedException()
    };
 
    Debug.WriteLine($"C# 8: {fileName}");
 
}

Practical sample Entity Framework Core 5

All of the prior examples can be used with Entity Framework. In the following sample a switch is used to check a numeric grade and set a letter which in turn is passed to the user interface via an event.

#nullable enable
public class  SchoolOperations
{
    public delegate  void OnIteratePersonGrades(PersonGrades personData);
    public static  event OnIteratePersonGrades? OnIteratePersonGradesEvent;
 
    /// <summary>
    /// Get students in a course by course identifier, better solution
    /// then in StudentsForCourse as there is only one condition
    /// </summary>
    /// <param name="courseIdentifier"></param>
    public static  List<StudentEntity> GradesForPeople(int courseIdentifier = 2021)
    {
         
        using var context = new SchoolContext();
         
        List<StudentEntity> studentEntities = context
            .StudentGrade
            .Include(studentEntity => studentEntity.Student)
            .Select(StudentGrade.Projection)
            .Where(studentEntity => studentEntity.CourseID == courseIdentifier)
            .ToList();
 
        foreach (var entity in studentEntities)
        {
            var letterGrade = entity.Grade.Value switch
            {
                >= 1.00m and <= 2.00m => "F",
                2.50m => "C",
                3.00m => "B",
                3.50m => "A",
                4.00m => "A+",
                _ => "unknown",
            };
 
            OnIteratePersonGradesEvent?.Invoke(new PersonGrades()
            {
                PersonID = entity.PersonID,
                FirstName = entity.FirstName,
                LastName = entity.LastName,
                Grade = entity.Grade,
                GradeLetter = letterGrade
            });
 
        }
 
        return studentEntities;
         
    }
     
}

Special note

When inspected the code, projections are used and with that two partial classes are used. Since the main classes are in the Models folder and the partial classes for projections are in a Classes folder to make partial work the partial classes under Classes folder must have the same namespace as under the models folder.

StudentGrade under Classes folder

namespace Switches.Models
{
    public partial  class StudentGrade
    {
 
        public static  Expression<Func<StudentGrade, StudentEntity>> Projection
        {
            get
            {
                return (student) => new StudentEntity()
                {
                    PersonID = student.StudentID,
                    CourseID = student.CourseID,
                    FirstName = student.Student.FirstName,
                    LastName = student.Student.LastName,
                    Grade = student.Grade
                };
            }
        }
 
    }
}

Main StudentGrade under models folder

namespace Switches.Models
{
    public partial  class StudentGrade
    {
        public int  EnrollmentID { get; set; }
        public int  CourseID { get; set; }
        public int  StudentID { get; set; }
        public decimal? Grade { get; set; }
 
        public virtual  Person Student { get; set; }
    }
}

 In a windows form (could be WPF for instance also)

public partial  class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
         
        SchoolOperations.OnIteratePersonGradesEvent += SchoolOperationsOnOnIteratePersonGradesEvent;
    }
 
    private void  SchoolOperationsOnOnIteratePersonGradesEvent(PersonGrades pData)
    {
        if (pData.Grade is null) return;
         
        var item = new  ListViewItem(new[]
        {
            pData.PersonID.ToString(), 
            pData.FullName, 
            pData.Grade.Value.ToString(CultureInfo.CurrentCulture), 
            pData.GradeLetter
        });
 
        listView1.Items.Add(item);
    }
 
 
    private void  StudentGradesButton_Click(object sender, EventArgs e)
    {
        listView1.Items.Clear();
        SchoolOperations.GradesForPeople(2021);
        listView1.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
    }
}

Summary

Conventional switch and new variations of the switch statement for C# 8 and higher have been presented. When direction to use depends on developer comfort and experience both at time of coding and when maintenance is needed. This goes the same for working with if statements keeping the branches to five or less.

Tip

If using Jet brains Resharper, this Visual Studio extension will provide refactoring options to convert between conventional switches to expressions and expressions to conventional switches.

See also

Visual C# Resources on the TechNet Wiki

Microsoft documentation

switch reference
if-else reference

External resources

Welcome to C# 9
C# 8 Switch Expressions with Pattern Matching

Source code

Using Git run the following in a batch file

mkdir code
cd code
git init
git remote add -f origin https://github.com/karenpayneoregon/csharp-features
git sparse-checkout init --cone
git sparse-checkout add ConsoleHelpers
git sparse-checkout add SwitchExpressions_basics
git sparse-checkout add Switches_efcore
git pull origin master
:clean-up
del .gitattributes
del .gitignore
del .yml
del .editorconfig
del *.md
del *.sln

Or clone the following GitHub repository which contain more code than contained in this article.

https://github.com/karenpayneoregon/csharp-features