Visual Studio 2017.3 brought with it the first minor update to the C# language, C# 7.1. This update adds four new features to C#: async main, target-typed default literals, tuple name inference, and generic support for pattern-matching.
In this post, you'll learn how to enable the new C# 7.1 language features in your projects, everything you need to know to start using all four of the new features, and some gotchas with using C# 7.1 in razor views.
How to Enable C# 7.1
By default, Visual Studio 2017 enables the latest major language version, which is C# 7.0. To enable the C# 7.1 features, you need to tell Visual Studio to use the latest minor language version or to explicitly use C# 7.1.
This is set at the project-level and is stored in the csproj file. So different projects can target different versions of the C# language.
There are 3 different ways to enable C# 7.1:
- Project Properties
- Edit the csproj File
- Lightbulb Code Fix
Method 1 - Project Properties
Right click on the project in solution explorer, go to properties, then select the build tab, select advanced in the bottom right, and then set the language version value.
Method 2 - Edit the csproj File
For projects using the new-style csproj, currently .NET Core, .NET Standard, and older projects that you have upgraded to the new-style csproj:
- Right click on the project in solution explorer
- Select
Edit [projectname].csproj
For projects using the old-style csproj:
- Right click on the project in solution explorer
- Select
Unload Project
- Right click on the project in solution explorer
- Select
Edit [projectname].csproj
You'll then need to add the LangVersion
tag to the first PropertyGroup
in your projects csproj file:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
If your csproj includes multiple PropertyGroup
tags for different build configurations, for example, debug and release builds, you will need to add the LangVersion
tag to each of those tags:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
These are the values you can use for LangVersion
:
- default
- latest
- ISO-1
- ISO-2
- 3
- 4
- 5
- 6
- 7
- 7.1
default
picks the latest major version, currently C# 7.0. When C# 8.0 is available, default
will start to use that.
latest
picks the latest minor version, currently C# 7.1. When C# 7.2 is available, latest
will start to use that.
The explicit version choices will continue to use the selected version, even when newer versions are released. For example, 7.1 will continue using C# 7.1 when C# 8.0 is released.
Method 3 - Lightbulb Code Fix
The final way to enable C# 7.1 is to try and use one of the new language features. For example, you could try and use the new target-typed default literal. You will then get a lightbulb code fix that offers to enable C# 7.1.
The lightbulb lets you upgrade to either latest
or 7.1
. It also lets you choose to upgrade all C# projects. If you have a lot of projects to upgrade, this is the fastest way to upgrade them all at the same time.
New Feature: Async Main
C# 7.1 enables the use of async
/await
in the Main method. This makes it easier to use async code throughout your entire application.
To use async main, add the async
keyword to your main method and make it return either a Task
or a Task<int>
. Returning a Task
corresponds to main methods that currently return void
and Task<int>
corresponds to main methods that currently return int
.
Here is an example of a program using async main. The program waits for two seconds, then prints Hello World
.
public class Program
{
static async Task Main(string[] args)
{
await Task.Delay(2000);
Console.Out.WriteLine("Hello World");
}
}
This is a very handy feature when you're writing small test applications, such as for bug reports, as it lets you eliminate some boilerplate. Previously, you had to create a separate async method and call that from the Main
method.
New Feature: Target-Typed Default Literals
C# 7.1 adds a new target-typed default
literal that provides a shortcut for the default(T)
operator using type inference.
In earlier versions of C#, to get the default value for a type, you had to specify the type explicitly. For example, default(int)
returned 0. C# 7.1 allows you to drop the type and have it inferred automagically.
I predominantly use the default
operator with generics, but it is called for in other situations. In the following examples, I show seven different ways you can use the new target-typed default literal. Number seven is my personal favourite.
1. Local Variable Declaration
You can use default
when declaring local variables to initialize them.
int i = default;
2. Return Value
You can use default
as a return value in a method.
int defaultValue()
{
return default;
}
You can also use default
as the return value in a lambda method.
Func<int> defaultValue = () => default;
3. Optional Parameter
You can use default
to set the default value for an optional parameter on a method.
void DoSomething(int i = default)
{
Console.Out.WriteLine(i);
}
4. Object or Array Initializer
You can use default
inside an object or array initializer as one of the values.
In this example, we see default
used inside an object initializer:
void CreateX()
{
var x = new X
{
Y = default,
Z = default
};
}
class X
{
public int Y;
public int Z;
}
In this example, we see default
used inside two different array initializers:
var x = new[] { default, new List<string>() };
Console.Out.WriteLine(x[0] == null); // Prints "True"
var y = new[] { default, 5 };
Console.Out.WriteLine(y[0] == 0); // Prints "True"
In the first example, default
takes on the value null
, as it gets the default value of List<string>
. In the second example, default
takes on the value 0
, as it gets the default value of int
.
5. is operator
You can use default
on the right-hand side of the is
operator.
int i = 0;
Console.Out.WriteLine(i is default == true); // Prints "True"
Console.Out.WriteLine(default is i == true); // Compile Error
6. Generics
You can use default
with generic types. In this example, default
creates the default value of generic type T
.
public class History<T>
{
private readonly List<T> history = new List<T>();
public T Create()
{
T value = default;
this.history.Add(value);
return value;
}
}
7. Ternary Operator
You can use default
with the ternary operator. This is my favourite use case for the target-typed default literal.
Previously, it was annoying to assign a default value when using the ternary operator. You could not just assign null, you had to explicitly cast null onto the target type.
void method()
{
int? result = runTest() ? 10 : (int?)null; // OK
int? result = runTest() ? 10 : null; // Compile Error
}
bool runTest() => true;
If you do not explicitly cast null
onto the correct type, you get a compile error. In the previous example, the compile error is:
Type of conditional expression cannot be determined because there is no implicit conversion between '
int
' and '<null>
'.
The new target-typed default literal makes this a lot cleaner as you no longer need any casting.
void method()
{
int? result = runTest() ? 10 : default;
}
This might not look like much of an improvement. However, I often see this pattern in cases where the type name is very long and often these types involve multiple generic type parameters. For example, the type might be Dictionary<string, Dictionary<int, List<IDigitalDevice>>>
.
New Feature: Tuple Name Inference
Another new feature in C# 7.1 is tuple name inference. This is also known as tuple projection initializers.
This feature allows tuples to infer their element names from the inputs. For example, instead of (x: value.x, y: value.y)
, you can now write (value.x, value.y)
.
Behaviour
Tuple name inference works with identifiers (such as local variable x
), members (such as a property x.y
), and conditional members (such as a field x?.y
). In these three cases, the inferred name would be x
, y
, and y
respectively.
In other cases, such as the result of a method call, no inference occurs. If a tuple name is not specified in these cases, the value will only be accessible through the default reserved name, e.g. Item3 for the third element of a tuple.
Reserved tuple names such as ItemN
, Rest
, and ToString
are not inferred. This is to avoid conflicts with the existing usage of these on tuples.
Non-unique names are not inferred. For example, on a tuple declared as (x, t.x)
, no names will be assigned to either element, as the name x
is not unique. Note that this code still compiles, but the variables will only be accessible through Item1 and Item2. This ensures that this new feature is backwards compatible with existing tuple code.
Breaking Change
Despite efforts to preserve backwards compatibility, there is one breaking change in C# 7.1.
In C# 7.0 you might have used extension methods to define new behaviour on tuples; the behaviour of this may change when you upgrade to C# 7.1 due to tuple name inference.
Demonstration
The problem occurs if you have an extension method on tuples and the method name clashes with an inferred tuple name.
Here is a program that demonstrates the breaking change:
public class Program
{
static void Main(string[] args)
{
Action Output = () => Console.Out.WriteLine("Lambda");
var tuple = (5, Output);
tuple.Output();
}
}
public static class Extensions
{
public static void Output<T1, T2>(this ValueTuple<T1, T2> tuple)
{
Console.Out.WriteLine("Extention");
}
}
In C# 7.0, this program prints Extension
, but in C# 7.1 it prints Lambda
.
Minor Impact
This breaking change is very unlikely to affect you.
Firstly, as the code must use tuples to be affected, it only affects code written since C# 7.0 was released, which was not very long ago.
Secondly, if you're using the C# 7.1 compiler in Visual Studio 2017.3 to compile C# 7.0 code, you now get a compile error from problematic code. This occurs when you set <LangVersion>7.0</LangVersion>
. On the demonstration code, you would get this error:
Error CS8306 Tuple element name 'Output' is inferred. Please use language version 7.1 or greater to access an element by its inferred name.
Thirdly, it is unlikely you have added extension methods to tuples in this way. You may not even have known this was possible.
Finally, you normally want to use names with tuples for readability. You would have to be accessing the tuple values using the reserved names Item1 and Item2 for this to affect you.
How to Check Your Code
If you are worried about this breaking change. Just run the compiler targeting C# 7.0 before upgrading to C# 7.1 to ensure you have not done this anywhere in your code base. If you have, you'll get compile error CS8306 in the places you have done this.
Benefits
Tuple name inference can be quite beneficial in cases where you repeatedly transform, project, and reuse tuples: as is common when writing LINQ queries. It also means that tuples more closely mirror the behaviour of anonymous types.
Simplified LINQ Queries
Tuple name inference makes it a lot nicer to use tuples in lambda expressions and LINQ queries. For example, it lets you transform this query:
items.Select(i => (Name: i.Name, Age: i.Age)).Where(t => t.Age > 21);
into this simpler query:
items.Select(i => (i.Name, i.Age)).Where(t => t.Age > 21);
Since C# 7.0 was released, I've found that my LINQ queries benefit tremendously from tuples. Tuple name inference will improve these queries even further, by making them even more succinct and readable.
Mirrors Anonymous Types
The new tuple name inference behaviour makes the language more symmetric in the sense that tuples now more closely mirror the behaviour on an existing and similar language feature, anonymous types.
Anonymous types infer their names using the same algorithm that is used for tuples in C# 7.1. In this example, we see that tuples and anonymous types look very similar due to name inference behaving similarly:
// Tuples
var t = (value.x, value.y);
Console.Out.WriteLine(t.x == value.x); // Prints "True"
// Anonymous Types
var a = new { value.x, value.y };
Console.Out.WriteLine(a.x == value.x); // Prints "True"
New Feature: Generic Pattern-Matching
C# 7.0 added pattern matching and three kinds of pattern: constant patterns, type patterns, and var patterns. C# 7.0 also enhanced the is
expression and switch
statement to use these patterns.
However, in C# 7.0 these patterns fail when the variable being matched is a generic type parameter. For example, both if(t is int i)
and switch(t) { case int i: return i; }
can fail when t
is generic or more specifically, an open type.
C# 7.1 improves the situation by allowing open types to be matched against all types of pattern, rather than just a limited set.
What is an Open Type?
An open type is a type that involves type parameters. On a class that is generic in T
, (T
, T[]
, and List<T>
are all open types). As long as one argument is generic, the type is an open type. Therefore, Dictionary<string, T>
is also an open type.
Almost everything else is known as a closed type. The one exception is for unbound generic types, which are generic types with unspecified type arguments. For example, List<>
and Dictionary<,>
are unbound generic types. You're likely to encounter unbound generic types when using reflection.
For more information on open types, see this stack overflow answer, which precisely defines open types.
Better Generic Pattern-Matching
In C# 7.0, you could match open types against particular patterns, but not all. In C# 7.1, you can match open types against all the patterns you would expect.
Behavior in C# 7.0
In C# 7.0, you could match an open type T
against object or against a specific type that was specified in a generic type constraint on T
. For example, where T : License
, you could match again object
or License
, but not derivatives of License
such as DriversLicense
.
This behaviour was counter-intuitive. You would expect and want to be able to match against derivative types and in fact, you can with the as
operator. The problem occurs as there is no type conversion when the specified type is an open type. However, the as
operator is more lenient and works with open types.
New Behavior in C# 7.1
C# 7.1 changes pattern matching to work in cases where as
works, by changing what types are pattern compatible.
In C# 7.0, static type S
and type T
are pattern compatible when any of these conversions exist:
- identity conversion
- boxing conversion
- implicit reference conversion
- explicit reference conversion
- unboxing conversion from
S
toT
C# 7.1 additionally considers S
and T
to be pattern compatible when either:
S
is an open type, orT
is an open type
This means that in C# 7.1 you can pattern match generic types against derivatives such as DriversLicense
in is
expressions and switch
statements.
Example Code
In the following example, Print
is a generic method that uses pattern matching with generic type T
. If T
is an int
, it returns "int", if T
is a string
, it returns "string", otherwise it returns "unknown".
This code compiles and works as expected in C# 7.1, whereas in C# 7 it gives a compile error.
static string Print<T>(T input)
{
switch(input)
{
case int i:
return "int";
case string s:
return "string";
default:
return "unknown";
}
}
static void Main(string[] args)
{
string input = "Hello";
Console.WriteLine(Print(input));
}
C# 7.1 Support in Razor Views
Razor supports C# 7.1. This means you can use the new features within your views. However, these are some gotchas that might affect you if you previously enabled C# 7.0 in your razor views.
Using C# 7.1 in Razor Views
Prior to Visual Studio 2017.3, razor views used C# 6.0 by default. This was true, even when you were using C# 7.0 in your code. If you have never tried to use any C# 7.0 features such as tuples inside a razor view, then you might not have noticed.
To change this, you had to modify Startup.cs
and set the razor ParseOptions
on IMvcBuilder
. You would have done this using code like this:
services.AddMvc().AddRazorOptions(options =>
{
options.ParseOptions = new CSharpParseOptions(LanguageVersion.CSharp7);
});
This is no longer necessary. The language used by razor views is now determined by the LangVersion
tag in the csproj
file. So the language available in razor views will always be in sync with the C# language version used for code in an ASP.NET Core project.
If you have upgraded to ASP.NET Core 2.0, you will need to remove this ParseOptions
setting from your RazorOptions
, as it is no longer necessary nor available on the API.
Razor Models cannot be Tuples
If you previously enabled C# 7.0, you may have found that you could use C# 7's tuples for the model in your razor views. I found this was a convenient way to pass additional strongly-typed variables to a view, without creating a separate ViewModel.
Unfortunately, as of the latest update, this feature is no longer available. You will now get a runtime error and a warning or error inside razor views that use this feature.
The temporary solution is to create separate ViewModels for these views and pass in your parameters that way. You can still use tuples within your razor views, just not for the model.
Fortunately, this situation will only be temporary. Support for tuples on type directive tokens, such as Model, has already been merged into razor. You can track the progress in this issue on GitHub.
Conclusion
There are three ways to enable C# 7.1 in your projects. Of these three methods, the lightbulb code fix provides the fastest and easiest way to upgrade all of your C# projects at the same time.
C# 7.1 adds 4 new language features: async main, target-typed default literals, tuple name inference, and generic support for pattern-matching.
- You saw how async main lets you use async/await in your main method.
- You saw target-typed default literals used in seven different ways, including my personal favourite #7, which uses
default
to eliminate redundant casts when using the ternary operator. - You saw how to use tuple name inference, the benefits of it, how it mirrors name inference on anonymous types, how it is a breaking change, and how to detect any resulting issues.
- You saw how you can now perform pattern matching between generic types and derivatives in
is
expressions andswitch
statements.
If you previously enabled C# 7 in your razor views, you need to remove the razor ParseOptions
setting. If you used tuples for any razor view models, you need to temporarily replace those with class based models until support for tuple view models returns.
Discuss
If you've been using any of the new C# 7 or C# 7.1 features in your projects, I would love to hear from you.
Please share your experiences in the comments below.
Addendum
Update (10th September 2017): Added Example Code section to Generic Pattern-Matching to show what is possible with C# 7.1 that was not possible in C# 7.