Switch-less code : An Example in ASP.NET Core

It is common in ASP.NET Core application to store one or more configuration options in appsettings.json. These options are then read inside MVC controllers or Razor Pages page models. Depending on a particular configuration setting you want to execute certain piece of processing. Although it sounds quite simple and straightforward beginners often end up doing this in a not-so-good way. To that end this article discusses a possible approach that is more flexible and helps you reduce the amount of future changes.

Let's understand what we are trying to accomplish with a hypothetical example. Suppose you are building an ASP.NET Core application that stores data in some data store. You want to support multiple data stores and they should be configurable. For example, you want to specify in configuration file that you want to use SQL Server and accordingly the application should use data access code that is aimed at SQL Server. If you decide to change database to, say Cosmos DB, you will simply change it in the configuration file and the application now uses processing intended for Cosmos DB and so on.

Now let's implement this requirement (sans actual data access code) in a simple ASP.NET Core MVC application. Begin by creating a new MVC application and add the following configuration into its appsettings.json file.

{
  "AppSettings": {
    "DbType": "SqlServer"
  }
}

The AppSettings section stores just one key named DbType that takes a string value such as SqlServer, CosmosDB, and MongoDB.

Simple switch statement

Let's use the most simplistic way to read the configuration and execute some processing based on DbType value.

Go to HomeController and write this code:

public class HomeController : Controller
{
    private string dbType = "";

    public HomeController(IConfiguration config)
    {
        dbType = config.GetValue<string>("AppSettings:DbType");
    }
}

HomeController declares a string variable dbType that is assigned a value in the constructor. The constructor receives IConfiguration parameter and reads DbType key of AppSettings. The DbType value is assigned to dbType member variable.

Once you know dbType you can write a C# switch statement to test its value and execute code depending on a particular value. The following code shows this switch statement that goes inside Index() action:

switch(dbType)
{
    case "SqlServer":
        ViewBag.Message = "Using SQL Server";
        break;
    case "CosmosDB":
        ViewBag.Message = "Using Azure CosmosDB";
        break;
    case "MongoDB":
        ViewBag.Message = "Using MongoDB";
        break;
    default:
        ViewBag.Message = "No DbType found";
        break;
}

This is quite simple and straightforward. Isn't it?

If you output the Viewbag.Message on Index view like this:

<h1>@ViewBag.Message</h1>

then you would get this output:

Although this code is simple it introduce a few problems. Firstly, it uses string values for comparison in the switch statement. There could be errors related to character casing, white spaces etc. Secondly, it uses switch statement to test a value and execute certain processing. Suppose your application uses such switch statements at say 100 places in the code base. One day a new DbType gets added to the list (say, Azure Table Storage). Now which all places you need to make change? Firstly, configuration file and then each place where switch is used to test dbType (which means at 100 places in the code). This magnitude of change is obviously problematic.

Let's solve the first problem discussed above by replacing string dbType with an enumeration.

Switch statement that uses enum

Go ahead and define a new enumeration in your application as shown below:

public enum DbType
{
    SqlServer = 1,
    CosmosDB = 2,
    MongoDB = 3
}

Here, we have DbType enumeration that defines three options SqlServer, CosmosDB, and MongoDB. You can now read configuration and decide which enum value is specified there as shown below:

private DbType dbType;

public HomeController(IConfiguration config)
{
    this.dbType = Enum.Parse<DbType>
    (config.GetValue<string>("AppSettings:DbType"));
}

HomeController has dbType member of type DbType enumeration. Inside the constructor you use Parse<T>() method of Enum class to convert string value stored in the configuration to its equivalent enumeration value. Your switch statement inside Index() action will change as shown below:

switch (dbType)
{
    case DbType.SqlServer:
        ViewBag.Message = "Using SQL Server";
        break;
    case DbType.CosmosDB:
        ViewBag.Message = "Using Azure CosmosDB";
        break;
    case DbType.MongoDB:
        ViewBag.Message = "Using MongoDB";
        break;
    default:
        ViewBag.Message = "No DbType found";
        break;
}

This form of switch is better than the previous one because it uses enumeration values in various case blocks.

If a new DbType gets added to the system which all places you need to make change? Configuration file, DbType enumeration, and those 100 switch statements. Now, let's address the problem introduced by those 100 switch statements.

Avoid using switch statements in your code

By now you must have sensed that switch statement (same thing applies to if-else-if ladders) is creating problems for the extensibility and maintainability of the code. Since it is scattered throughout the code, even a small change to the options is going to cause good amount of rework throughout the code. So, our next step is to remove the use of switch statement altogether. Let's see how.

What we will do is to create a map that associates a DbType with a particular Action. This map can be a dictionary as shown below:

private Dictionary<DbType, Action> dbTypeLogic = 
new Dictionary<DbType, Action>();

Here, you declared a Dictionary that will store a set of DbType keys and associated Action values. To implement this change, constructor of HomeController tales this form:

public HomeController(IConfiguration config)
{
    this.dbType = Enum.Parse<DbType>
(config.GetValue<string>("AppSettings:DbType"));

    dbTypeLogic.Add(DbType.SqlServer, () => {
        ViewBag.Message = "Using SQL Server";
    });
    dbTypeLogic.Add(DbType.CosmosDB, () => {
        ViewBag.Message = "Using Azure CosmosDB";
    });
    dbTypeLogic.Add(DbType.MongoDB, () => {
        ViewBag.Message = "Using MongoDB";
    });
}

Above code stores the processing logic for each DbType enumeration value as an Action delegate.

Now you can remove the switch statement altogether and instead write the following in Index() action.

public IActionResult Index()
{
   dbTypeLogic[this.dbType]();
   return View();
}

Here, you invoke an Action from the Dictionary by passing DbType key value.

Now if a new DbType gets added which all places you need to make change? Configuration file, DbType enumeration, and dbTypeLogic dictionary. However, all the 100 places where you invoke an action from dbTypeLogic dictionary remain unchanged. So, change is now confined only to three well-known places.

This approach is much better than previous ones but still has one issue. Here, a DbType value is associated with one Action. What if there are more than one pieces of processing logic that you want to associate with a DbType value? An obvious solution is to define another Dictionary that holds DbType keys and another set of Action items. But soon this solution can also create problems if you keep creating different directories for each new set of processing logic. Let's remove that drawback in the final step discussed next.

Use objects that wrap behaviors for each DbType

Rather than maintaining a separate Dictionary for each set of Actions, you can wrap all the behaviors belonging to a DbType value in an object and then let the Dictionary return the require object to you. Let's see how this can be done.

Let's assume that there are two behaviors per DbType value, say GetMessage() and GetDetails(). The former method returns a string that indicates the database used. The later method returns database driver details such as .NET data provider for SQL Server, EF Core provider for Cosmos DB, and MongoDB .NET driver.

To represent these methods add an interface called IDbTypeLogic:

public interface IDbTypeLogic
{
    string GetMessage();
    string GetDetails();
}

Each DbType you wish to have will be represented by a class that implements this interface. In our example there are three DbTypes - SqlServer, CosmosDB, and MongoDB. So, there will be three classes, say SqlServerLogic, CosmosDBLogic, and MongoDBLogic. These classes are shown below:

public class SqlServerLogic:IDbTypeLogic
{
    public string GetMessage()
    {
        return "Using SQL Server";
    }
    public string GetDetails()
    {
        return ".NET provider for SQL Server";
    }
}

public class CosmosDBLogic : IDbTypeLogic
{
    public string GetMessage()
    {
        return "Using Azure CosmosDB";
    }
    public string GetDetails()
    {
        return "SQL API with EF Core CosmosDB 
Provider";
    }
}

public class MongoDBLogic : IDbTypeLogic
{
    public string GetMessage()
    {
        return "Using MongoDB";
    }
    public string GetDetails()
    {
        return "MongoDB .NET Driver";
    }

}

These classes are quite straightforward and hence not discussed in detail.

Next, change the Dictionary definition like this:

private Dictionary<DbType, Func<IDbTypeLogic>> 
dbTypeLogic = new Dictionary<DbType, Func<IDbTypeLogic>>();

As you can see, dbTypeLogic dictionary now takes DbType values as its keys and Func<IDbTypeLogic> as values. Difference between Action and Func is that Action can't return values whereas Func can return values.

Now change the constructor as shown below:

public HomeController(IConfiguration config)
{
   this.dbType = Enum.Parse<DbType>(
config.GetValue<string>("AppSettings:DbType"));

    dbTypeLogic.Add(DbType.SqlServer, () => {
        return new SqlServerLogic();
    });
    dbTypeLogic.Add(DbType.CosmosDB, () => {
        return new CosmosDBLogic();
    }); 
    dbTypeLogic.Add(DbType.MongoDB, () => {
        return new MongoDBLogic();
    });
}

Notice the Func values added to the dictionary. Basically they create and return an object of SqlServerLogic, CosmosDBLogic, and MongoDBLogic.

The calling code from Index() action will change to this:

public IActionResult Index()
{
    IDbTypeLogic obj = dbTypeLogic[this.dbType]();
    ViewBag.Message = obj.GetMessage();
    ViewBag.Details = obj.GetDetails();
    return View();
}

As you can see you retrieve a Func by passing dbType to the Dictionary. Invoking that function returns an implementation of IDbTypeLogic. You can then invoke the required behavior - GetMessage() or GetDetails().

A sample run of this code when configuration value is CosmosDB produces this output.

With this arrangement just one Dictionary is sufficient no matter how many behaviors are associated with a DbType value. When a new DbType gets added, you need to make change in the configuration file, DbType enumeration. Then you need to create a class that implements IDbTypeLogic interface and implement the two methods. Finally, you need to change the Dictionary to hold a new Func that returns an object of newly created class.

I hope this article gave you a glimpse of how object oriented design can help you write better code. Here is a quick question and exercise for beginners - In the code fragments discussed in this article we used polymorphism via interfaces, OCP, factory pattern, and strategy pattern. Can you identify where these are used in the code?

That's it for now! Keep coding!!


Bipin Joshi is an independent software consultant, trainer, author, and meditation teacher. He has been programming, meditating, and teaching for 24+ years. He conducts instructor-led online training courses in ASP.NET family of technologies for individuals and small groups. He is a published author and has authored or co-authored books for Apress and Wrox press. Having embraced the Yoga way of life he also teaches Ajapa Yoga to interested individuals. To know more about him click here.

Get article updates : Facebook  Twitter  LinkedIn

Posted On : 26 August 2019


Tags : ASP.NET ASP.NET Core MVC C# Visual Studio


Subscribe to our newsletter

Get monthly email updates about new articles, tutorials, code samples, and how-tos getting added to our knowledge base.

  

Receive Weekly Updates