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