들어가며

참고

https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history

C# versionVS version.NET versionCLR versionRelease dateEnd of SupportUnity
122022 (17.8).NET 8.Nov 2023Nov 2026
112022 (17.4).NET 7.Nov 2022May 2024
102022.NET 6.Nov 2021Nov 2024
9 (†)2019 (v16.8).NET 5.Nov 2020May 20222021.2
2019 (v16.4).NET Core 3.1.Dec 2019Dec 2022
2019 (v16.3).NET Core 3.0.Sep 2019Mar 2020
8.02019.Apr 2019
.NET standard 2.2.Dec 2018Dec 20192020.2
7.32017 (v15.7).NET standard 2.1.May 2018Aug 2021
7.12017 (v15.3).NET standard 2.0.Aug 2017Oct 2018
7.02017.Mar 2017
.NET standard 1.1.Nov 2016Jun 2019
2015 Update 3.NET standard 1.0.Jun 2016Jun 2019
== .NET Core== .NET Core== .NET Core== .NET Core== .NET Core== .NET Core
4.8.14Aug 2022.
8.020194.84Apr 2019.
7.32017 (v15.7)May 2018.
4.7.24Apr 2018.
7.22017 (v15.5)Dec 2017.
4.7.14Oct 2017.
7.12017 (v15.3)Aug 2017.
4.74May 2017.
7.02017Mar 2017.
4.6.24Aug 2016.
4.6.14Nov 2015.
6.020154.64Jul 2015.
4.5.24May 2014.
20134.5.14Oct 2013.
5.020124.54Aug 2012.
4.020104.04Apr 2010.
3.020083.52.0Nov 2007.
3.02.0Nov 2006.
2.020052.02.0Nov 2005.
1.220031.11.1Apr 2003.
1.020021.01.0Feb 2002.

9.0

top level statement

Tool

$ dotnet new tool-manifest
$ cat .config/dotnet-tools.json

$ dotnet tool install dotnet-format
$ cat .config/dotnet-tools.json
  • dotnet-format
  • coverlet.console
  • dotnet-reportgenerator-globaltool
  • microsoft.dotnet.xharness.cli
  • microsoft.visualstudio.slngen.tool

Ref

Source Generator

  • 새 코드를 추가 가능.
  • 기존 코드 수정 불가능
  • C# 혹은 추가 파일에 접근 가능

Unity

  • 버전
    • .NET 버전
    • IDE의 컴파일러 버전
    • Microsoft.CodeAnalysis.CSharp 버전
      • /Editor/Data/DotNetSdkRoslyn/Microsoft.CodeAnalysis.CSharp.dll 버전보다 높은 버전의 것을 참조하면 움직이지 않는다.

Roslyn - 4.3.1 Visual Studio 2022 - 17.3

.NET Compiler Platform SDK

  • etc: Improved Interpolated Strings가 C# 10.0

  • 빌드된.dll

    • 의존성 관리가 힘들 수 도 있으니 하나의 dll로 만드는게 좋음.
    • label: RoslynAnalyzer
    • dll배치는 Runtime/ 쪽에 놔두는 경향이 있음

ISourceGenerator & IIncrementalGenerator

  • deprecated - ISourceGenerator: 전체 코드 베이스
  • IIncrementalGenerator: 변경된 부분
Microsoft.CodeAnalysis.CSharpUnity
3.xISourceGenerator
4.xIIncrementalGenerator2022.2~, 2023.1~

Sample: SourceGenerator

mkdir SourceGenerator
cd SourceGenerator
dotnet new gitignore
dotnet new classlib -o SourceGeneratorGen
dotnet new console -o SourceGeneratorSrc
dotnet new sln
dotnet sln add SourceGeneratorGen
dotnet sln add SourceGeneratorSrc

SourceGeneratorGen

  • 우클릭 소스제너레이터 프로젝트
  • Properties > Debug > Open debug launch profiles UI
  • 보이는 프로파일 삭제
  • 추가 클릭
    • Roslyn component 선택
    • 타겟 프로젝트에서 콘솔 어플리케이션 프로젝트 선택
    • UI 닫기
  • 비주얼 스튜디오 재시작
  • 재생 버튼 옆 디버그 프로파일 드롭다운에서 소스제너레이터 프로젝트 선택
  • 디버거가 멈추는지 확인하기 위해 소스제너레이터에 브레이크 포인트를 설정
  • 재생 클릭
<!-- SourceGeneratorGen/SourceGeneratorGen.csproj-->

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <ImplicitUsings>disable</ImplicitUsings>
        <Nullable>disable</Nullable>

        <LangVersion>11</LangVersion>

        <!-- IsRoslynComponent: true => | Properties > Debug > Launch > Roslyn Component -->
        <IsRoslynComponent>true</IsRoslynComponent>
        <AnalyzerLanguage>cs</AnalyzerLanguage>

        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
    </ItemGroup>
</Project>
// SourceGeneratorGen/Generator.cs

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;

namespace SourceGeneratorGen
{
    [Generator(LanguageNames.CSharp)]
    public sealed class Generator : IIncrementalGenerator
    {
        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
            // === IncrementalValue[s]Provider<T>
            //context.CompilationProvider
            //context.AdditionalTextsProvider
            //context.AnalyzerConfigOptionsProvider
            //context.MetadataReferencesProvider
            //context.ParseOptionsProvider
            // === SyntaxValueProvider
            // context.SyntaxProvider
            // === Outputting values
            //context.RegisterSourceOutput               // 사용자 컴파일에 포함될 소스 파일과 진단을 생성할 수 있다
            //context.RegisterImplementationSourceOutput // RegisterSourceOutput랑 비슷. 단, 유저코드나 다른 변환기에 의해 실행되지 않음. 코드 분석에 영향을 주지 않음.
            //context.RegisterPostInitializationOutput   // 다른 변환이 실행되기 전에 컴파일에 포함됨

            context.RegisterPostInitializationOutput(Callback);

            IncrementalValuesProvider<GeneratorAttributeSyntaxContext> source = context.SyntaxProvider.ForAttributeWithMetadataName(
                fullyQualifiedMetadataName: "GeneratedNamespace.GenerateToStringAttribute",
                predicate: static (node, token) => true,
                transform: static (context, token) => context);

            context.RegisterSourceOutput(source, Emit);
        }

        private void Callback(IncrementalGeneratorPostInitializationContext context)
        {
            string code = """
using System;

namespace GeneratedNamespace
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
    internal sealed class GenerateToStringAttribute : Attribute
    {
    }
}
""";
            context.AddSource("Generated.cs", code);
        }

        private void Emit(SourceProductionContext context, GeneratorAttributeSyntaxContext source)
        {
            INamedTypeSymbol typeSymbol = (INamedTypeSymbol)source.TargetSymbol;
            TypeDeclarationSyntax typeNode = (TypeDeclarationSyntax)source.TargetNode;

            if (typeSymbol.GetMembers("ToString").Length != 0)
            {
                context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ExistsOverrideToString, typeNode.Identifier.GetLocation(), typeSymbol.Name));
                return;
            }

            string ns;
            if (typeSymbol.ContainingNamespace.IsGlobalNamespace)
            {
                ns = string.Empty;
            }
            else
            {
                ns = $"{typeSymbol.ContainingNamespace}";
            }

            string fullType = typeSymbol
                .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
                .Replace("global::", "")
                .Replace("<", "_")
                .Replace(">", "_");

            IEnumerable<string> publicMembers = typeSymbol
                .GetMembers()
                .Where(x => x is (IFieldSymbol or IPropertySymbol)
                             and { IsStatic: false, DeclaredAccessibility: Accessibility.Public, IsImplicitlyDeclared: false, CanBeReferencedByName: true })
                .Select(x => $"{x.Name}:{{{x.Name}}}"); // MyProperty:{MyProperty}

            string toString = string.Join(", ", publicMembers);

            // multiline string interpolation
            string code = $$"""
// ========================== auto-generated
#pragma warning disable CS8600
#pragma warning disable CS8601
#pragma warning disable CS8602
#pragma warning disable CS8603
#pragma warning disable CS8604

namespace {{ns}}
{
    partial class {{typeSymbol.Name}}
    {
        public override string ToString()
        {
            return $"{{toString}}";
        }
    }
}
#pragma warning restore CS8604
#pragma warning restore CS8603
#pragma warning restore CS8602
#pragma warning restore CS8601
#pragma warning restore CS8600
""";

            context.AddSource($"{fullType}.SampleGenerator.g.cs", code);
        }
    }


    public static class DiagnosticDescriptors
    {
        private const string CATEGORY = "GeneratedNamespace";

        public static readonly DiagnosticDescriptor ExistsOverrideToString = new DiagnosticDescriptor(
            id: "SAMPLE001",
            title: "ToString override",
            messageFormat: "The GenerateToString class '{0}' has ToString override but it is not allowed",
            category: CATEGORY,
            defaultSeverity: DiagnosticSeverity.Error,
            isEnabledByDefault: true
        );
    }
}

SourceGeneratorSrc

<!-- SourceGeneratorSrc/SourceGeneratorSrc.csproj -->
<!-- TODO: https://stevetalkscode.co.uk/debug-source-generators-with-vs2019-1610 -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\SourceGeneratorGen\SourceGeneratorGen.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
  </ItemGroup>
</Project>
// SourceGeneratorSrc/Program.cs

using GeneratedNamespace;
using System;

namespace SourceGeneratorSrc
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            MyClass mc = new MyClass() { Hoge = 10, Bar = "tako" };
            Console.WriteLine(mc);
        }
    }

    [GenerateToString]
    public partial class MyClass
    {
        public int Hoge { get; set; }
        public string Bar { get; set; }
    }
}

Etc

// IDE 경고내기

private const string CATEGORY = "GeneratedNamespace";
public static readonly DiagnosticDescriptor ExistsOverrideToString = new DiagnosticDescriptor(
    id:            "SAMPLE001",
    title:         "ToString override",
    messageFormat: "The GenerateToString class '{0}' has ToString override but it is not allowed",
    category: CATEGORY,
    defaultSeverity: DiagnosticSeverity.Error,
    isEnabledByDefault: true
);

SourceProductionContext context;
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ExistsOverrideToString, typeNode.Identifier.GetLocation(), typeSymbol.Name));
ReportDiagnostic Enum
Default0Report a diagnostic by default.
Error1Report a diagnostic as an error.
Warn2Report a diagnostic as a warning even though /warnaserror is specified.
Info3Report a diagnostic as an info.
Hidden4Report a diagnostic as hidden.
Suppress5Suppress a diagnostic.

로슬린

컴파일

Ref

Source Generator ex

Analyzer

Ref