close icon
.NET

Five C# Features You Might Not Know

A quick tour about five C# features that even experienced developers might not know: from variable scopes to the top-level statements and others.

March 10, 2021

Even if you are an experienced C# developer, there are features you might not be aware of. In this article, you will explore five little-known C# features that allow you to accomplish things in a slightly different way than you are used to. These features allow you to declare local variables in an unusual way, create conditions with the switch command and the try/catch block, reduce the number of lines to set up a console program, define properties that can only be initialized, and access private variables of a class instance.

Let's get started.

Variable Scope and Braces

In C#, you can declare variables within code blocks surrounded by braces to define methods, conditional statements, loops, among others. For example, the braces define a variable scope within a method:

static void Main(string[] args)
{
  Console.Write("Enter your name: ");
  var name = Console.ReadLine();

  Console.WriteLine($"Hello, {name}!");
}

Also, you can define a variable scope with an if statement:

if (name.Trim() != "")
{
  var dayOfWeek = DateTime.Today.ToString("dddd");
  Console.WriteLine($"Happy {dayOfWeek}, {name}!");
}

And with a while statement as well:

while (name.Trim() != "")
{
  var dayOfWeek = DateTime.Today.ToString("dddd");
  Console.WriteLine($"Happy {dayOfWeek}, {name}!");

  Console.Write("Enter your name: ");
  name = Console.ReadLine();
}

Regardless of the context, the braces delimit the scope of the variable. Did you know that you can define a variable scope with braces even without a specific statement? Consider the following code:

using System;

namespace variable_scope
{
  class Program  {
    static void Main(string[] args)
    {
      Console.WriteLine("Hello World!");
      {
        var theVariable = "I'm the variable";
        Console.WriteLine(theVariable);
      }
    }
  }
}

Here, we have a code block delimited by braces inside the Main() method. The code block defines and initializes the variable theVariable and writes its value to the console. Notice that no if, while, for, or any other statement stands right before the braces. This program works as expected: it will write both strings as shown below:

Hello World!
I'm the variable

What happens if we add the following statement outside the code block, as in the following example?

using System;

namespace variable_scope
{
  class Program  {
    static void Main(string[] args)
    {
      Console.WriteLine("Hello World!");
      {
        var theVariable = "I'm the variable";
        Console.WriteLine(theVariable);
      }
      //๐Ÿ‘‡ new statement outside the code block
      Console.WriteLine(theVariable);
    }
  }
}

If you attempt to run this program, you will get the following error:

error CS0103: The name 'theVariable' does not exist in the current context

As you can see, the variable defined inside the code block is not accessible outside it.

In other words, you can create a variable scope by simply using braces anywhere in your code. This feature may be useful when you have a short sequence of statements that need to share a value through a variable. You create a code block on the fly, declare the variable, and use it. However, I suggest not to abuse it. Sometimes, it's a better idea to create a method and invoke it. It may be more readable than a code block.

The when keyword

Another little-known C# feature is the when keyword. You can use it to apply a filter condition in the context of a switch statement or a try/catch block. Letโ€™s start by seeing how to use it in the context of a switch statement. Consider the following code:

private static string evaluateScore(int score)
{
  var result = "";

  switch (score)
  {
    case int n when (n < 6):
      result = "Your score doesn't look good...";
      break;

    case int n when (n >= 6 && n <= 7):
      result = "Your score is not bad!";
      break;
    
    case int n when (n > 7 && n <= 9):
      result = "Your score is pretty good!";
      break;
    
    case int n when (n > 9):
      result = "Your score is awesome!";
      break;
  }

  return result;
}

Here we have the evaluateScore() method that evaluates an integer value (score) and returns a message string. As you can see, its logic is totally based on a switch statement. Each case declares a variable n that takes the current value of the score parameter. This variable is inspected in the when clause to determine the message to be returned. Of course, you can use any boolean expression with the when clause.

You can also use the when keyword with catch, as shown in this example:

public static async Task<string> MakeRequest()
{
  var client = new System.Net.Http.HttpClient();
  var streamTask = client.GetStringAsync("https://auth0.com/this-page-doesnt-exist");

  try
  {
    var responseText = await streamTask;
    return responseText;
  }
  catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.MovedPermanently)
  {
    return "The page moved.";
  }
  catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
  {
    return "The page was not found";
  }
  catch (HttpRequestException e)
  {
    return e.Message;
  }
}

Here, you catch an HttpRequestException and analyze its status code through a when clause.

In both cases, the when keyword is extremely useful when you need to match a complex condition since it allows you to keep your code pretty readable.

Top-Level Statements

Typically, a minimal console application has at least the following code:

using System;

namespace standard_console
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Hello World!");
    }
  }
}

This program is composed of eleven lines of code, but only one line is actually operative. All the other lines are needed to set up the program infrastructure: a namespace, a class, and a method.

Do you know that you can get rid of this stuff and focus on just one line? In fact, you can replace the code shown above with just the following:

System.Console.WriteLine("Hello World!");

This is possible thanks to the top-level statement feature introduced by C# 9. You can try it starting with .NET 5.

Of course, you can use multiple top-level statements to create your console application, as in this example.

using System;

Console.Write("Enter your name: ");
var userName = Console.ReadLine();

Console.WriteLine($"Hello {userName}!");

However, remember that you can have only one file using this feature in your .NET project.

Using the top-level statements feature enables a script-oriented approach for C# programming. This may be a more friendly approach for new C# learners or a handy alternative to quickly set up a small console application. For complex applications, you may feel the top-level statements approach is inappropriate because of the lack of a structure.

The init Setter

Assume you have the following Person class:

public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string Address { get; set; }
}

It's a very simple class with just FirstName, LastName, and Address properties. As it usually happens in the real world, once you assign a first name and last name to a person, you do not want anyone to change it anymore. So, you would like to assign the values for the FirstName and the LastName properties to a Person instance at the creation time, and those values should not be changed anymore. How can you accomplish this behavior? If you define the Person class by using the usual get and set accessors, you will not be able to get the expected result. After creating an instance of the Person class, you can change the value of its FirstName and LastName properties whenever you want, as in the following example:

var person = new Person
{
  FirstName = "John",
  LastName = "Doe",
  Address = "124 Conch Street, Bikini Bottom, Pacific Ocean"
};

person.Address = "17 Cherry Tree Lane";
person.FirstName = "Jack";

Console.WriteLine($"Hello {person.FirstName} {person.LastName}!");

Fortunately, C# 9 provides you with the init accessor. If you apply the init accessor to the FirstName and LastName properties in place of set, you will achieve your goal:

public class Person
{
  public string FirstName { get; init; }  //๐Ÿ‘ˆ changed code
  public string LastName { get; init; }   //๐Ÿ‘ˆ changed code
  public string Address { get; set; }
}

This way, after the instance is created, the FirstName and LastName properties become read-only. Remember that this feature is supported only starting with C# 9.

Accessing Private Variables

Of course, you know that a private variable is not accessible from outside a class. In the following example, you have a Life class that defines the birthTime private variable:

public class Life
{
  private DateTime birthTime;

  public Life()
  {
    birthTime = DateTime.Now;
  }

  public int GetLength()
  {
    return DateTime.Now.Subtract(birthTime).Milliseconds;
  }
}

The birthTime private variable is assigned in the class constructor and is used within the GetLength() method. Normally, the birthTime variable is not accessible from outside the Life class. However, there is a special case where you can access the birthTime private variable. Take a look at this new YoungerThan() method:

public class Life
{
  // ...other code...

  public bool YoungerThan(Life livingBeing)
  {
    return livingBeing.birthTime < birthTime;
  }
}

Within this new method, you are actually accessing the birthTime private variable of a Life instance!

You may be scared by the consequences of accessing a private variable. Donโ€™t panic! Only instances of a class can access private variables of other instances of the same class. In other words, the context of this access possibility is limited to one specific class.

This feature is useful when you need to implement some sort of internal comparison methods like the one shown above. However, you should use this approach only when you don't want to expose a private value. Otherwise, you should expose the private variable through a public property or method and use it for any processing. This way, you keep the idiomatic Object-Oriented Programming approach.

Summary

In this article, you discovered five C# features that maybe you didn't know. The C# language and the .NET platform are growing and evolving very quickly to adapt to developers' needs and simplify their work. So there's nothing bad with missing some feature.

If you know other little-known C# features, please share them with us in the comments below.

Aside: Securing ASP.NET Core with Auth0

Securing ASP.NET Core applications with Auth0 is easy and brings a lot of great features to the table. With Auth0, you only have to write a few lines of code to get a solid identity management solution, single sign-on, support for social identity providers (like Facebook, GitHub, Twitter, etc.), and support for enterprise identity providers (like Active Directory, LDAP, SAML, custom, etc.).

On ASP.NET Core, you need to create an API in your Auth0 Management Dashboard and change a few things on your code. To create an API, you need to sign up for a free Auth0 account. After that, you need to go to the API section of the dashboard and click on "Create API". On the dialog shown, you can set the Name of your API as "Books", the Identifier as "http://books.mycompany.com", and leave the Signing Algorithm as "RS256".

Creating API on Auth0

After that, you have to add the call to services.AddAuthentication() in the ConfigureServices() method of the Startup class as follows:

string authority = $"https://{Configuration["Auth0:Domain"]}/";
string audience = Configuration["Auth0:Audience"];

services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
  options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
  options.Authority = authority;
  options.Audience = audience;
});

In the body of the Configure() method of the Startup class, you also need to add an invocation to app.UseAuthentication() and app.UseAuthorization() as shown below:

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

Make sure you invoke these methods in the order shown above. It is essential so that everything works properly.

Finally, add the following element to the appsettings.json configuration file:

{
  "Logging": {
    // ...
  },
  "Auth0": {
    "Domain": "YOUR_DOMAIN",
    "Audience": "YOUR_AUDIENCE"
  }
}

Note: Replace the placeholders YOUR_DOMAIN and YOUR_AUDIENCE with the actual values for the domain that you specified when creating your Auth0 account and the Identifier you assigned to your API.

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon