June 26, 2022 / by Zsolt Soczó

Is compiling C# 10 code for .NET 4.7 possible?

I have asked this question today. Yes, it is possible. Here is the proof:

using System;
using System.Linq;
using System.Runtime.CompilerServices;

//C# 10
namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello C# 10");
        CallerArgumentExpressionDemo(args.Length == 0 && (args.FirstOrDefault() ?? "42") == "42");

        int x = 0;
        //C# 10 - Assignment and declaration in same deconstruction
        (x, string y) = new DemoStructRecord(2,"apple");
        Console.WriteLine($"{x}, {y}");
    }

    //C# 10 - CallerArgumentExpression
    public static void CallerArgumentExpressionDemo(bool b, [CallerArgumentExpression("b")] string message = null)
    {
        if (b)
        {
            Console.WriteLine($"Message from the caller expression: {message}");
        }
    }
}

//C# 10 - record struct
record struct DemoStructRecord(int A, string B)
{
    private const string SomeConstant = "Something";

    //C# 10 - Interpolated string constant
    private const string InterpolatedConstant = $"This is an interpolated constant {SomeConstant}";

    public readonly override string ToString()
    {
        return $"{nameof(A)}: {A}, {nameof(B)}: {B}";
    }
}

record DemoRecord(int A, string B)
{
    //C# 10 - sealed ToString
    public sealed override string ToString()
    {
        return $"{nameof(A)}: {A}, {nameof(B)}: {B}";
    }
}

public readonly struct Measurement
{
    //C# 10 - parameterless ctor in struct
    public Measurement()
    {
        Value = double.NaN;
        Description = "Undefined";
    }

    public Measurement(double value, string description)
    {
        Value = value;
        Description = description;
    }

    //C# 9 (init only props)
    public double Value { get; init; }
    public string Description { get; init; }

    public override string ToString() => $"{Value} ({Description})";
}

As you see, I packed several C#9 and 10 features into the code, and they compile happily. There were two tricky points, however. CallerArgumentExpression is not defined in .NET Framework; you have to add it to your program:

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class CallerArgumentExpressionAttribute : Attribute
{
    public CallerArgumentExpressionAttribute(string parameterName)
    {
        ParameterName = parameterName;
    }

    public string ParameterName { get; }
}

The other problematic feature is init-only properties. To be able to compile your code to .NET 4.7, you have to define it yourself:

namespace System.Runtime.CompilerServices
{
    internal static class IsExternalInit { }
}

For completeness, here is the project file:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <LangVersion>10.0</LangVersion>
    <ProjectGuid>{AFFA1FD0-4D9D-4801-B66C-26F232D32B3D}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <RootNamespace>ConsoleApp1</RootNamespace>
    <AssemblyName>ConsoleApp1</AssemblyName>
    <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="CallerArgumentExpressionAttribute.cs" />
    <Compile Include="IsExternalInit.cs" />
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

I intentionally used the old project file format for this demo. It is useful to convert older projects to the new SDK format; however, it needs extensive testing, so we cannot schedule it for a while.

(For a nuget package, if the target is an SDK project or the older project with package references, nuget won’t copy nuget content to the destination project; it only links to the files. This design decision has many severe implications, but I don’t diverge more in this article.)

Here is the output of the running code:

It’s interesting to see how the CallerArgumentExpressionAttribute works. It nicely receives the argument C# expression.

To make sure I compiled for .NET 4.7, I decompiled the output assembly by DotPeak:

It is compiled for .NET 4.7

LEAVE A COMMENT

Your email address will not be published.