What’s New in C# 10: Write Less Code and Reduce Repeated Using Directives

This is part of a series on the new features introduced with C# 10.

There are 2 related features in C# 10 that will reduce the clutter of repetitive using directives: global using directives and implicit global using directives.

C# 10 Global Usings

At the top of every code file you will usually find a number of using directives:

using ConsoleApp1.Configuration;

namespace ConsoleApp1
{
    internal class Calculator
    {
        public int Add(int a, int b)
        {
            if (CalculatorConfiguration.SomeConfigProperty)
            {
                // etc.
            }
            return a + b;
        }
    }
}

In the preceding code there is a single using ConsoleApp1.Configuration; to get access to a CalculatorConfiguration class.

For one file this is not much overhead in terms of repeated code.

Imagine however if the CalculatorConfiguration class was referenced in 100’s of code files throughout the project. This would mean we’d have 100 using ConsoleApp1.Configuration; lines throughout the project.

As an alternative you can use the new global using declarations. To use these just prefix the directive with global, for example: global using ConsoleApp1.Configuration;

Now all code files in the project will act as if though they have a using ConsoleApp1.Configuration; at the top of them. You only need one global using directive for any given namespace in the project.

You can add global usings to any code file, however it makes sense to centralize them. For example you could create a GlobalUsings.cs file in the project and inside that just have all your global using directives.

C# 10 Implicit Global Usings

If you create a new C# 10 project (e.g. a console app) and open the project file you’ll see a line: <ImplicitUsings>enable</ImplicitUsings>

This enables the new implicit global usings feature.

When this feature is enabled, a number of commonly used namespaces will automatically have global usings directives added for them.

For a Console project the following will be automatically added:

global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;

This means for example you you could create a List<string> anywhere in the project without needing to add a using System.Collections.Generic; to the top of your code files.

Implicit global usings work behind the scenes by generating a file.

Assuming you had a console app called ConsoleApp1, you would find this generated file as follows: "ConsoleApp1\obj\Debug\net6.0\ConsoleApp1.GlobalUsings.g.cs".

If you opened this file you’d see the global usings that are implicitly added to your project.

If you don’t like the idea of implicit global usings you can opt out for new projects by making the following change in the project file: <ImplicitUsings>disable</ImplicitUsings>

SHARE:

What’s New in C# 10: Simplify Argument Null Checking Code

This is part of a series on the new features introduced with C# 10.

Prior to C# 10 you may have had code similar to the following:

public static string Join(string a, string b)
{            
    if (a is null)
    {
        throw new ArgumentNullException(nameof(a));
    }

    if (b is null)
    {
        throw new ArgumentNullException(nameof(b));
    }

    return a + b;
}

If the parameters a or b are null then an ArgumentNullException will be thrown.

The nameof operator will create a string from the parameter name so the exception will contain information about what parameter was null.

One potential problem with this code is that it is easier to reference the incorrect parameter, for example:

if (a is null)
{
    throw new ArgumentNullException(nameof(b));
}

The if statement is checking a but the nameof is referencing b.

C# 10 (.NET 6) introduces an improved way:

public static string Join(string a, string b)
{
    ArgumentNullException.ThrowIfNull(a);
    ArgumentNullException.ThrowIfNull(b);

    return a + b;
}

In the preceding code, a new static method called ThrowIfNull has been added on the ArgumentNullException class and allows us to quickly check and throw ArgumentNullExceptions.

Notice in this new version of the code we do not need to use the nameof operator, the parameter name will “magically” be worked out for us if an exception is thrown.

As an example, take the following code that passes a null to parameter a:

try
{
    SomeClass.Join(null, "bbbb");
}
catch (Exception ex)
{
    Console.WriteLine(ex);
}

If we run this we’ll get the following console output:

System.ArgumentNullException: Value cannot be null. (Parameter 'a')
   at System.ArgumentNullException.Throw(String paramName)
   at System.ArgumentNullException.ThrowIfNull(Object argument, String paramName)
   at ConsoleApp1.SomeClass.Join(String a, String b)

Notice the message contains a reference to the parameter named ‘a’ automatically.(Behind the scenes this is due to the new [CallerArgumentExpression] attribute – but you don’t need to know about the implementation details to make use of the ThrowIfNull method.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

What’s New in C# 10: Reclaim Horizontal Editing Space and Simplify Nesting

This is the first part in a series on the new features introduced with C# 10.

Prior to C# 10, to define types as being part of a specific namespace you would use the following syntax:

namespace ConsoleAppCS9
{
    public class SomeClass
    {
        public void SomeMethod(bool b, string s)
        {
            if (b is true)
            {
                if (s is not null)
                {
                    // do something
                }
            }
        }
    }
    namespace SomeNestedNamespace
    {
        public class AClassInANestedNamespace
        {
            // etc.
        }
    }
}

 

With C# 10 we can use something called file-scoped namespace declarations.

All this means is that we can remove one level of nesting in { } brackets:

namespace ConsoleAppCS9;

public class SomeClass
{
    public void SomeMethod(bool b, string s)
    {
        if (b is true)
        {
            if (s is not null)
            {
                // do something
            }
        }
    }
}

The line namespace ConsoleAppCS9; means that any types defined in that one file will be inside the ConsoleAppCS9 namespace.

This has the effect in the editor of giving us more horizontal editing space and nesting to keep mental track of.

Also note in the second example that you can’t have nested namespaces in the file if file-scoped namespace declarations are being used – in this case you’d have to return to the traditional namespace syntax.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 9 New Features: Create Immutable Objects with Records

This is part of a series of articles on new features introduced in C# 9.

C# 9 introduced a new type of object that is neither a class or a struct. This new type is called a  record.

In C# 9 a record is a reference type that has value type equality semantics (more on this below).

The main purpose of defining record types is to indicate immutability for a type that is “data-centric” or in other words does not have rich behaviour (such as data transfer objects, database records, etc).

How to Define a Record in C# 9

To define a record type you use the record keyword:

record Message1
{
    public int Priority { get; set; }
    public string MessageBody { get; set; }
}

We could now create an instance and then write it to the console window:

var m1 = new Message1();
m1.Priority = 1;
m1.MessageBody = "Hi";

Console.WriteLine(m1);

This would produce the following output:

Message1 { Priority = 1, MessageBody = Hi }
Console.WriteLine automatically calls ToString() on the object passed to it, notice that we get built-in ToString() formatting support for all record types.

 

Notice in the preceding code that we are able to set Priority and MessageBody even after we have created the object – this is not immutable behaviour. To make a record immutable when declaring properties manually (see positional records below) you need to make the property setter init only:

record Message2
{
    public int Priority { get; init; }
    public string MessageBody { get; init; }
}

Now if you try and write the following code you’ll get a compiler error (“Init-only property or indexer … can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor”):

var m2 = new Message2();
m2.Priority = 2;
m2.MessageBody = "Hey there!";

To create Message2 instances you now need to set the properties when you create it, for example:

var m2 = new Message2()
{
    Priority = 2,
    MessageBody = "Hey there!"
};
You can also add constructors to record types if you want to.

What Are Positional Records in C#?

Positional records are a shorthand syntax for defining C# records. Behind the scenes they create init-only properties.

We could define a message class that is essentially the same as Message2 above with the following syntax:

record Message3(int Priority, string MessageBody);

Now we could create one with the following syntax:

var m3 = new Message3(3, "Good day sir!");

Or if you wanted to be explicit:

var m3 = new Message3(Priority: 3, MessageBody: "Good day sir!");

Even though behind the scenes we’re getting init-only properties, when you define a positional record you can’t use the following syntax:

var m3 = new Message3() // error missing arguments
{
    Priority = 3,
    MessageBody = "Good day sir!"
};

You can think of positional records as a shorthand syntax that creates init only properties and a parameterized constructor automatically behind the scenes.

Equality

Records have value-like equality semantics:

Record instances in C# 9 by default are considered equal if they store the same values and are of the same record type:

var m3a = new Message3(Priority: 3, MessageBody: "Good day sir!");
var m3b = new Message3(Priority: 3, MessageBody: "Good day sir!");
var m3c = new Message3(Priority: 3, MessageBody: "BOO!");

Console.WriteLine($"m3a == m3b : {m3a == m3b}"); // Outputs: TRUE
Console.WriteLine($"m3b == m3c : {m3b == m3c}"); // Outputs: FALSE

If you tried to compare a Message3 object with a Message2 object you’ll get a compiler error.

If you want to, you can override things like Object.Equals in a record.

Note: C# 10  will be introducing record structs .

Immutability of Record Types

One thing to be aware of is that immutability of records types is “shallow” for properties that are reference types.

In other words while you can’t change the value of a value type property you can change the properties of reference type properties in a record:

var m4 = new Message4(4, new[] { "Dear sir", "Good to see you.", "Good bye." });
Console.WriteLine(m4.MessageLines[0]); // OUTPUTS: Dear sir

m4.MessageLines[0] = "Yo yo!"; // NO COMPILER ERROR
Console.WriteLine(m4.MessageLines[0]); // OUTPUTS: Yo Yo!

m4.MessageLines = new[]; // ERROR MessageLines property object reference itself IS immutable

You can create a new immutable record object based on an existing immutable instance:

var one = new Message3(Priority: 3, MessageBody: "Good day sir!");
var two = one; 

Object two is a copy of one.

Note: record copies are “shallow” - any value type properties will have the value copied, but any reference type properties will only have the reference copied. That means that 2 record instances can have reference type properties that point to the same object. You change the object they point to and both records will be “updated” with (point to) the new value (because they share the reference to the same object in memory).

If a record is immutable, you can “update” it by creating a copy of it, and update some properties as required during the “copy” – you do this using the with keyword. For example to “update” the priority of an immutable record:

var priority3Message = new Message3(Priority: 3, MessageBody: "Good day sir!");
var priority1Message = priority3Message with { Priority = 1 };

As before, if you create a copy and use with a shallow copy is still created.

Custom C# Record Output Formatting

When you declare a record, under the hood a PrintMembers method is generated. You can also provide your own:

record Message5(int Priority, string[] MessageLines)
{
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append($"P:{Priority}");
        for (int i = 0; i < MessageLines.Length; i++)
        {
            builder.Append($" {MessageLines[i]} ");
        }

        return true;
    }
}

Now the following code:

var m5 = new Message5(5, new[] { "Dear sir", "Good to see you.", "Good bye." });

Console.WriteLine(m5);

Would output:

Message5 { P:5 Dear sir  Good to see you.  Good bye.  }

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

New Pluralsight Course Update: Working with Nulls in C#

A new version of my Working with Nulls in C# Pluralsight course was just released. This new version features updated demos in .NET 5 & C# 9 plus a brand new module has been added.

The course contains the following modules:

  • Working with Nullable Value Types and Strings
  • Accessing and Checking for Null Values
  • Eliminating Null Reference Exceptions with the Null Object Pattern
  • Understanding Nullable and Non-nullable Reference Types
  • Using Additional Attributes to Describe Nullability

From the course description: “Making use of an object when it is not supposed to be null can result in unexpected exceptions that can cause your program to crash. These unexpected null related errors may cause data loss or corruption, system downtime, and unhappy users. In this course, Working with Nulls in C#, you’ll gain the ability to reduce the likelihood of getting null related exceptions in production. First, you’ll explore which objects can be set to null and how to check for null values. Next, you’ll discover a design pattern to help eliminate null related exceptions. Finally, you’ll learn how to opt-in to the ability to create non-nullable reference types that enable the compiler to detect null-related problems before you even run your application. When you’re finished with this course, you’ll have the skills and knowledge of nulls in C# needed to reduce null related errors in your production code.”

You can start watching with a Pluralsight free trial.

SHARE:

ICYMI C# 9 New Features: Reduce Boilerplate Constructor Code with Init Only Property Setters

This is part of a series of articles on new features introduced in C# 9.

Prior to C# 9 if you wanted to create properties that can be set only when the object is created, you could make  the property setter private and use constructor arguments to set them:

class PaymentV1
{
    public Guid Id { get; private set; }
    public decimal Value { get; private set; }
    public string Notes { get; set; }
    public PaymentV1(Guid id, decimal value)
    {
        Id = id;
        Value = value;
    }
}

In the preceding code, the Id and Value properties can only be set when the object is created by supplying constructor parameters. Once the Payment has been created you can’t set the Id or Value properties from outside the object instance.

In the preceding code, we have to add the extra constructor code just to create the “externally immutable” properties (they can still be set from code inside the class).

C# 9 introduces the concept of init only setters. These allow you to create immutable properties without needing to write the extra constructor boilerplate code:

class PaymentV2
{
    public Guid Id { get; init; }
    public decimal Value { get; init; }
    public string Notes { get; set; }
}

Notice in the preceding code that the Id and Value properties now use the init keyword instead of the set keyword. Also notice that we no longer need to create a constructor.

To set these immutable properties you need to do so at the time of object construction/creation by using the existing C# property initialization syntax, for example:

var payment2 = new PaymentV2
{
    Id = Guid.NewGuid(),
    Value = 45.50m,
    Notes = "Initial send on Friday."
};

Once this code executes and the payment2 object is created, you will not be able to set Id or Value:

payment2.Id = Guid.NewGuid(); // ERROR - only settable in initializer
payment2.Value = 99.00m; // ERROR - only settable in initializer
payment2.Notes += " Second send on Sunday."; // OK

You can also set init only properties from the constructor of a derived type:

abstract class PaymentBase
{
    protected Guid Id { get; init; }
    protected decimal Value { get; init; }
}

class PaymentV3 : PaymentBase
{
    public string Notes { get; set; }

    public PaymentV3(Guid id, decimal value)
    {
        Id = id;
        Value = value;
    }
}

You could also use init only properties and set a default value if the property is not set at creation and also add validation logic:

class PaymentV4
{
    private readonly string _currencyCode = "USD";

    public string CurrencyCode
    {
        get
        {
            return _currencyCode;
        }

        init 
        {
            if (value is null)
            {
                throw new ArgumentNullException(nameof(CurrencyCode));
            }

            if (value.Length != 3)
            {
                throw new ArgumentOutOfRangeException(nameof(CurrencyCode), "Must be 3 long.");
            }

            // etc.
            _currencyCode = value;
        }
    }
}

With the preceding code, we could try the follow statements:

var payment4 = new PaymentV4(); // Default CurrencyCode of "USD"
var payment4 = new PaymentV4 { CurrencyCode = "AUD" };
var payment4 = new PaymentV4 { CurrencyCode = null }; // Exception
var payment4 = new PaymentV4 { CurrencyCode = "hello" }; // Exception

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 9 New Features: Adding foreach Support To Any Type

This is part of a series of articles on new features introduced in C# 9.

Prior to C# 9 you could add foreach support to a class by modifying the class and implementing the IEnumerable interface. With C# 9 you can add foreach support to an existing class without actually needing to modify that class.

As an example, suppose the following class exists in our codebase:

public class Choices
{
    public string FirstChoice { get; set; }
    public string SecondChoice { get; set; }
    public string ThirdChoice { get; set; }
    public string FourthChoice { get; set; }
    public string FifthChoice { get; set; }
}

Because the class is in our control we could add the ability to foreach through all the choices (FirstChoice, SecondChoice, etc.).

One way to do this would be to modify the Choices class and implement IEnumerable – we could also just create a method called GetEnumerator and then have it return a class which has a MoveNext and Current method:

public class ChoicesEnumerator
{
    public string[] _choices = new string[5];

    int position = -1;

    public ChoicesEnumerator(Choices choices)
    {
        // Additional null/validation/etc checking code omitted

        _choices[0] = choices.FirstChoice;
        _choices[1] = choices.SecondChoice;
        _choices[2] = choices.ThirdChoice;
        _choices[3] = choices.FourthChoice;
        _choices[4] = choices.FifthChoice;
    }

    public bool MoveNext()
    {
        position++;
        return position < _choices.Length;
    }

    public string Current
    {
        get
        {
            try
            {
                return _choices[position];
            }
            catch (IndexOutOfRangeException)
            {
                throw new InvalidOperationException();
            }
        }
    }
}

We could then modify the Choices class and add the GetEnumerator method:

public class Choices
{
    public string FirstChoice { get; set; }
    public string SecondChoice { get; set; }
    public string ThirdChoice { get; set; }
    public string FourthChoice { get; set; }
    public string FifthChoice { get; set; }
    
    public ChoicesEnumerator GetEnumerator()
    {
        return new ChoicesEnumerator(this);
    }
}

Now we can iterate through the choices in a foreach loop:

var choices = new Choices
{
    FirstChoice = "Orange",
    SecondChoice = "Blue",
    ThirdChoice = "Green",
    FourthChoice = "Cyan",
    FifthChoice = "Grey"
};

foreach (var choice in choices)
{
    WriteLine(choice);
}

But what if we were not able to modify the Choices class – for example if it is from an external library? From C# 9 you can follow a similar approach to the above but add the foreach support using an extension method. This way we don’t have to modify the original class.

If the original class that we couldn’t modify looked like the following:

public sealed class Choices
{
    public string FirstChoice { get; set; }
    public string SecondChoice { get; set; }
    public string ThirdChoice { get; set; }
    public string FourthChoice { get; set; }
    public string FifthChoice { get; set; }
}

We could once again create the ChoicesEnumerator class as above and then just create an extension method that adds the GetEnumerator method to the Choices class, without needing to modify the Choices class itself:

public static class ChoicesExtensions
{
    public static ChoicesEnumerator GetEnumerator(this Choices choices)
    {
        return new ChoicesEnumerator(choices);
    }
}

Now we can once again foreach through all the choices.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 9 New Features: Reducing Code with Target-typed New Expressions

This is part of a series of articles on new features introduced in C# 9.

C# 9 introduced some enhancements to reduce the amount of code you need when creating new instances of objects.These target-typed new expressions “Do not require type specification for constructors when the type is known.” [MS]

As an example in C# 8 with fields you need to explicitly create an instance:

class Preferences
{
    private List<string> _favoriteColors = new List<string>();
}

From C# 9 you can instead write:

class Preferences
{
    private List<string> _favoriteColors = new();
}

Notice in the preceding code you can simply write new() because the target type is known to be List<string>.

If you are calling a method (or constructor) that requires an instance of an object, you can also use new(). For example suppose you had the following method that requires a DisplayOptions instance:

public void DisplayColors(DisplayOptions options)
{
    Console.WriteLine(options.Title);
    foreach (var color in _favoriteColors)
    {
        Console.WriteLine(color);
    }
}

Prior to C# 9, if you wanted to just create a new instance of DisplayOptions and pass it in you would write:

var prefs = new Preferences();            
prefs.DisplayColors(new DisplayOptions());

With C# 9 you can simplify this to:

var prefs = new Preferences();
prefs.DisplayColors(new());

You could also write this using a target-typed new expression for the Preferences instance:

Preferences prefs = new();
prefs.DisplayColors(new());

If you have init only properties you can also use target-typed new expressions:

class DisplayOptions
{
    public string Title { get; init; }
}
DisplayOptions options = new() { Title = "Colors" };

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 9 New Features: More Pattern Matching Features

This is part of a series of articles on new features introduced in C# 9.

Pattern matching was introduced in earlier versions of C# and C# 9 continued to evolve this feature. Pattern matching in a more general sense allows you to write an expression and evaluate it to see whether or not it meets some specified characteristic or “pattern”. In some situations pattern matching can reduce the amount of code you need to write and improve readability.

Pattern matching is supported in is expressions, switch statements, and switch expressions.

C# 9 Type Pattern

The type pattern allows your code to perform a runtime check that an expression of a specified type. In C# 9, when using the type pattern you no longer need to assign to a variable or use a discard.

Prior to C# 9:

object o = "hello";
string message = o switch
{
    string _ => "it's a string",
    int _ => "it's an int",
    _ => "it's something else"
};

WriteLine(message);

ReadLine();

Notice in the preceding code the required discards (or variable assignments) _.

From C# 9 this can be simplified to:

object o = "hello";

string message = o switch
{
    string => "it's a string",
    int => "it's an int",
    _ => "it's something else"
};

WriteLine(message);

ReadLine();

Notice that now no discard is required.

Logical, Parenthesized, and Relational Patterns in C# 9

With C# 9 you can now create logical patterns using not, and, or.

Prior to C# 9 you had to write:

if (o != null)
{
    WriteLine(o);
}

From C# 9 you can write:

if (o is not null)
{
    WriteLine(o);
}

As another example, prior to C# 9 you could not write:

if (o is not string)
{
    WriteLine("not a string");
}

Examples of or, and:

string minuteHandDescription = minuteHandValue switch
{
    >=0 and <= 15 => "first quarter",
    >15 and <= 30 => "second quarter",
    >30 and <= 45 => "third quarter",
    (>45 and <=58) or 59 or 60 => "fourth quarter",
    _ => "unknown"
};

Also notice in the preceding code the use of the parenthesized pattern (>45 and <=58) or 59 or 60 to enforce precedence.

The code also makes use of the new C# 9 relational patterns  that include: <, >, <=, and >=.When using these “The right-hand part of a relational pattern must be a constant expression. The constant expression can be of an integer, floating-point, char, or enum type.” [Microsoft]

For more information on patterns check out the C# language reference.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 9 New Features: Top-level Statements

This is the first in a series of articles on new features introduced in C#9.

Top-level statements allow you to simplify and remove some of the “ceremony” in your code.

For example, take the following console application written in C#8:

using System.Linq;
using static System.Console;

namespace ConsoleAppCS8
{
    class Program
    {
        static int Main(string[] args)
        {
            string greeting = "";

            if (args.Any())
            {
                greeting = args[0];
            }

            WriteLine("Please enter your name");
            var name = ReadLine();

            var upperName = ConvertToUpper(name);

            WriteLine($"{greeting} {upperName}");

            return 42;
        }

        public static object ConvertToUpper(string name)
        {
            return name.ToUpperInvariant();
        }
    }
}

In the preceding code the “ceremony” consists of things such as the enclosing namespace, the Program class outline, and the Main method itself.

With top-level statements in C# 9 this code can be simplified to the following:

using System.Linq;
using static System.Console;


string greeting = "";

if (args.Any())
{
    greeting = args[0];
}

WriteLine("Please enter your name");
var name = ReadLine();

var upperName = ConvertToUpper(name);

WriteLine($"{greeting} {upperName}");

return 42;


static object ConvertToUpper(string name)
{
return name.ToUpperInvariant();
}

Notice in the C# 9 version that the structure is a lot “flatter” because there are no nested {} from the namespace, class, and Main method.

The application will still act in the same way, there is just less boilerplate code.

Notice that in the top-level version you can still have methods e.g. ConvertToUpper(), you can still return a value return 42; and you can still access any command line arguments greeting = args[0];

Some Considerations

Whilst top-level statement make the application seem more “script like” this may or may not be a good thing from someone learning C# for the very first time, depending on if they have any other prior programming experience.

Behind the scenes, a synthesized entry point into the application is created for you. If you return a value, the synthesized entry point (a bit like the Main method in the C# 8 version) will have a return value and if you use any async code the synthesized entry point will return Task or Task<int> – essentially using top-level statements doesn’t restrict you from the kind of code you can write.

Notice in the C# 8 version the method public static object ConvertToUpper(string name) could be accessed by another class in the console app:

class Class1
{
    public void XYZ()
    {
        Program.ConvertToUpper("ddd");
    }
}

In the C# 9 top-level version the method becomes:

static object ConvertToUpper(string name)
{
  return name.ToUpperInvariant();
}

If we try to write the same Class1 in the C# 9 project we’ll get an error “Error    CS0103    The name 'Program' does not exist in the current context “. We could of course refactor the ConvertToUpper() method into an explicit static class to make it accessible

You can only have one top-level file in a project, and you can’t also have an explicit Main method entry point at the same time without getting a compiler warning.

A file with top level statements can also include namespaces/classes but they must be after the top-level statements.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE: