developers

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.

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.