🦠

C# positional recordsのパラメータ個数上限を探る

2022/12/03に公開

C# Advent Calendar 2022 3日目の記事です。

問題設定

Positional recordsとは、C# 9でrecordそれ自体と共に導入された、以下のような書き方です。

record Foo(int Id, int Value);

// ↑とだいたい同じ
/*
record Foo
{
    public int Id { get; init; }
    public int Value { get; init; }
    public Foo(int id, int value) => (Id, Value) = (id, value);
}*/

ここで、positional recordsでの引数の個数はどこまで増やせるでしょうか?

現実離れしたネタ的な調査に見えますが、一部実際に困ったことがありそれがきっかけです。

実験にはSource Generatorを活用しました。本記事における実装上の多少の目新しさはそこになると思います。

先に結論

  • JSONデシリアライズに使う場合は、限界が早く来る
    • System.Text.Json では 64個が上限 (仕様)
    • Newtonsoft.Json では 502個が上限 (筆者の環境にて)
  • 普通に使う場合は、2481個が上限 (筆者の環境にて)
    • 特にpositional recordsだから多い/少ないということはなさそう

サンプルコード

https://github.com/shimat/source_generator_sample

環境

  • .NET 7 または 6
  • Visual Studio 2022 17.4.1

実験用のSource Generatorの作成

仕様

実験には、たくさんの引数を持ったrecordが必要です。手打ちやスクリプト言語等で生成しても良いのですが、今回は Source Generatorでやってみます。 [1]

以下のような仕様にします。

// こう書くと...
[PropertyCount(10)]
public partial record Foo;
// これを自動生成する
public partial record Foo(int V0, int V1, int V2, int V3, int V4, int V5, int V6, int V7, int V8, int V9);

プロジェクト作成

基本的な設定等は他の記事に譲ります。私の挙げたサンプルコードもご参照ください。
https://learn.microsoft.com/ja-jp/dotnet/csharp/roslyn-sdk/source-generators-overview

プロジェクトを2つ作成します。

  • SourceGeneratorSample
    • コンソールアプリケーション
  • SourceGeneratorSample.Generators
    • Source Generatorのクラスライブラリ

PropertyCountAttribute

ここからSource Generatorの作成です。コンソールアプリケーションのコードの中から、[PropertyCount] 属性が付いたrecordを探すのが第一目標です。

まずはその属性を普通に定義します。Source Generatorプロジェクト側に置きます(意外とここもはまりポイント)。

using System;

namespace SourceGeneratorSample.Generators;

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class PropertyCountAttribute : Attribute
{
    public int Count { get; }

    public PropertyCountAttribute(int count)
    {
        Count = count;
    }
}

SyntaxContextReceiver

ISyntaxContextReceiver を実装したクラスを定義し登録すれば、コンパイラがソースコードから構文木を作り終えたところで呼び出してくれます。その時に必要な情報を取捨選択して取っておきます。[2]

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

namespace SourceGeneratorSample.Generators;

internal class SyntaxContextReceiver : ISyntaxContextReceiver
{
    private List<(INamedTypeSymbol TargetRecord, int PropertyCount)> workItems = new();
    public IReadOnlyList<(INamedTypeSymbol TargetRecord, int PropertyCount)> WorkItems => workItems;

    /// <summary>
    /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
    /// </summary>
    public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
    {
        if (context.Node is not RecordDeclarationSyntax rds) 
            return;
        var targetRecord = (INamedTypeSymbol?)context.SemanticModel.GetDeclaredSymbol(context.Node);
        if (targetRecord is null)
            return;
        var attributes = targetRecord.GetAttributes();
        var targetAttribute = attributes.FirstOrDefault(att => 
            att.AttributeClass.FullName() == "SourceGeneratorSample.Generators.PropertyCountAttribute");
        if (targetAttribute is null) 
            return;
        if (targetAttribute.ConstructorArguments.Length != 1)
            return;
        var value = targetAttribute.ConstructorArguments[0].Value;
        if (value is null) 
            return;

        workItems.Add((targetRecord, (int)value));
    }
}

Generator本体

Initializeで上記SyntaxContextReceiverを登録します。するとExecuteの前にOnVisitSyntaxNodeが回って、必要なデータを集め終わった状態になります。

あとは属性から得られたプロパティ個数と、対象クラス名等の情報を使って、record定義をstringで作って登録します。

ManyPropertiesSourceGenerator.cs
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;

namespace SourceGeneratorSample.Generators;

[Generator]
public class ManyPropertiesSourceGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxContextReceiver is not SyntaxContextReceiver receiver)
            return;

        var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);
        var namespaceName = mainMethod?.ContainingNamespace.ToDisplayString();

        foreach (var w in receiver.WorkItems)
        {
            var typeName = w.TargetRecord.Name;
            var parameterString = string.Join(", ", Enumerable.Range(0, w.PropertyCount).Select(i => $"int V{i}"));
            
            var source = $"""
                // auto-generated
                namespace {namespaceName};

                public partial record {typeName}({parameterString});
                """;

            context.AddSource($"{typeName}.generated.cs", source);
        }
    }    

    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new SyntaxContextReceiver());
    }
}

以上で基本的なSource Generatorの実装は完成です。

動作確認

コンソールアプリケーション側にて、例えば以下のようにすればSource Generatorが効き、動作することが確認できるはずです。

Program.cs
class Program
{
    static void Main()
    {
        var foo = new Foo(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
	Console.WriteLine(foo);
    }
}

[PropertyCount(10)]
public partial record Foo;

コンソールアプリケーションのプロジェクトフォルダ以下に Generated というフォルダができており、自動生成されたソースコードが確認できます。

SourceGeneratorSample/Generated/SourceGeneratorSample.Generators/SourceGeneratorSample.Generators.ManyPropertiesSourceGenerator/Foo.generated.cs
// auto-generated
namespace SourceGeneratorSample;

public partial record Foo(int V0, int V1, int V2, int V3, int V4, int V5, int V6, int V7, int V8, int V9);

デバッグ方法

結果だけ書くと簡単そうですが、Source Generatorの作成は非常に苦労しました。

デバッグしづらいのが難点の1つです。とはいえ当初よりは簡単になっています。以下に従うのが現状最も良い方法だと思います。
https://github.com/JoanComasFdz/dotnet-how-to-debug-source-generator-vs2022

実験

ようやく本題です。

普通に使う場合の上限

上記のコードについて、PropertyCountの数値を上げていくとどこまでいけるでしょうか。

試した結果[3]、筆者の環境では2481が上限でした。2482以上はコンパイルエラーになりました。万人がこの限界値なのかは定かではありません。

Program.cs
// OK
[PropertyCount(2481)]
public partial record Foo;

// CS8078: 式が長すぎるか複雑すぎるため、コンパイルできません
[PropertyCount(2482)]
public partial record Foo;

2481はビルドは通るのですが、Visual Studioがかなり重くなったり、最悪クラッシュすることもあります(ReSharperが原因かもしれません)。私は今回の実験中10回以上はVisual Studioを再起動しました・・・。

record定義でコンパイルエラーにならなければ、インスタンスももちろん作れます。数百数千だとnew Foo(1, 2, 3, ...);のように書くのも一苦労なので[4]、リフレクションで試してみます。(もちろん素朴に書いても成功します。)

var obj = (Foo?)Activator.CreateInstance(
    type: typeof(Foo),
    bindingAttr: BindingFlags.CreateInstance,
    binder: null,
    args: Enumerable.Range(0, 2481).Select(x => (object)x).ToArray(),
    culture: null);
Console.WriteLine(obj);
// Foo { V0 = 0, V1 = 1, V2 = 2, ..., V2480 = 2480 }

[PropertyCount(2481)]
public partial record Foo;

JSONデシリアライズに使う場合の上限 (System.Text.Json)

続いて、JSONデシリアライズにてこのrecordを使うことを考えます。数千個のフィールドを持つJSONは難なく作れますので、それをデシリアライズしてみます。

class Program
{
    public const int PropertyCount = 64;

    public void Main()
    {
        // {"V0":0, "V1":1, ... } のようなJSON
        var json = $"{{ {string.Join(", ", Enumerable.Range(0, PropertyCount).Select(i => $"\"V{i}\": {i}"))} }}";
	
        var obj = JsonSerializer.Deserialize<Foo>(json);
        Console.WriteLine(obj);
    }
}

[PropertyCount(Program.PropertyCount)]
public partial record Foo;

しかしこの場合、positional recordsのパラメータ数は64個が限界で、65個以上は実行時エラーになります。これに限っては万人がそうだと言えまして、.NET 7時点では仕様です。issueを読むと、.NET 8では解決が見込まれるようです。
https://github.com/dotnet/runtime/issues/71984

これは正確にはrecord特有の課題ではなく、classでもsetter経由ではなくコンストラクタを使うようになっていれば同じ問題にあたります。とはいえclassで65個以上も引数があるコンストラクタを定義するのは確かに稀というかしんどいですね。しかしpositional recordsでは全然有り得る状況になったと思います。

ちなみに本記事最初で述べた「一部実際に困ったこと」はこのことでした。使っている OpenSearch (Elasticsearch) のスキーマには100個以上のフィールドがあり、JSONデシリアライズにつまづいたという次第です。

解決策1: プロパティ定義

簡単な解決策は、コンストラクタを作らない(デシリアライズをコンストラクタ経由にしない)ことです。

もし.NET 7であれば、requiredがついたプロパティ定義を使うと使い勝手を保ちつつ回避できます。string等の参照型のときに = null!; を書くような真似は不要です。プロパティにした場合は、2481個まではできるようでした。繰り返しますが筆者の環境においてです。

record Foo
{
    public required int V0 { get; init; }
    public required int V1 { get; init; }
    public required int V2 { get; init; }
    ...
}

.NET 6の場合、requiredをなんとかして使う手もあるのですが[5]、おとなしくrequired無しでやるのが無難かと思います。

解決策2: カスタムシリアライザを定義する例

プロパティはいやだ、positional recordsをどうしても使いたい、という場合はJSONのカスタムコンバータを作る手はあります。
https://learn.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-6-0

やや脱線しますが雑な例を載せておきます。これを使えばpositional recordsでも2481個までいけました。

public class FooConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert == typeof(Foo);
    }

    public override JsonConverter CreateConverter(
        Type type,
        JsonSerializerOptions options)
    {
        return new FooConverter(options);
    }
}

public class FooConverter : JsonConverter<Foo>
{
    public FooConverter(JsonSerializerOptions options)
    {
    }

    public override Foo Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();

        var dictionary = new Dictionary<string, int>();

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                var ctor = typeof(Foo).GetConstructors().First();
                var paramPositions = ctor.GetParameters().ToDictionary(p => p.Name!, p => p.Position);
                
                return (Foo?)Activator.CreateInstance(
                    type: typeof(Foo),
                    bindingAttr: BindingFlags.CreateInstance,
                    binder: null,
                    args: dictionary.OrderBy(kv => paramPositions[kv.Key]).Select(kv => (object)kv.Value).ToArray(),
                    culture: null)
                    ?? throw new JsonException();
            }

            if (reader.TokenType != JsonTokenType.PropertyName)
                throw new JsonException();
            var propertyName = reader.GetString() ?? throw new JsonException();

            reader.Read();
            if (reader.TokenType != JsonTokenType.Number)
                throw new JsonException();
            var value = reader.GetInt32();

            dictionary.Add(propertyName, value);
        }

        throw new JsonException();
    }

    public override void Write(
        Utf8JsonWriter writer,
        Foo foo,
        JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}
var json = ...;
var obj = JsonSerializer.Deserialize<Foo>(json, new JsonSerializerOptions
{
    Converters = { new FooConverterFactory() }
});        

この例ではintしか無いrecord(class)にしか対応できませんから、完全に対応させるコスパを考えると実用性は疑問符が付きますね。

JSONデシリアライズに使う場合の上限 (Newtonsoft.Json)

JSON.NETの場合も調べてみました。JsonConvert.DeserializeObjectを使う場合、結果として502個が上限で、503個からは実行時エラーになりました (StackOverflowException)。これも万人がこの値かどうかはわかりません。

var json = ...;
var obj = Newtonsoft.Json.JsonConvert.DeserializeObject<Foo>(json);
Console.WriteLine(obj);

[PropertyCount(502)]
public partial record Foo;
StackOverflowExceptionのスタックトレース
Stack overflow.
   at SourceGeneratorSample.FooJsonNet..ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32)
   at DynamicClass.Void .ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32)(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32)(System.Object[])
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters(Newtonsoft.Json.JsonReader, Newtonsoft.Json.Serialization.JsonObjectContract, Newtonsoft.Json.Serialization.JsonProperty, Newtonsoft.Json.Serialization.ObjectConstructor`1<System.Object>, System.String)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject(Newtonsoft.Json.JsonReader, Newtonsoft.Json.Serialization.JsonObjectContract, Newtonsoft.Json.Serialization.JsonProperty, Newtonsoft.Json.Serialization.JsonProperty, System.String, Boolean ByRef)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(Newtonsoft.Json.JsonReader, System.Type, Newtonsoft.Json.Serialization.JsonContract, Newtonsoft.Json.Serialization.JsonProperty, Newtonsoft.Json.Serialization.JsonContainerContract, Newtonsoft.Json.Serialization.JsonProperty, System.Object)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(Newtonsoft.Json.JsonReader, System.Type, Newtonsoft.Json.Serialization.JsonContract, Newtonsoft.Json.Serialization.JsonProperty, Newtonsoft.Json.Serialization.JsonContainerContract, Newtonsoft.Json.Serialization.JsonProperty, System.Object)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(Newtonsoft.Json.JsonReader, System.Type, Boolean)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(Newtonsoft.Json.JsonReader, System.Type)
   at Newtonsoft.Json.JsonSerializer.Deserialize(Newtonsoft.Json.JsonReader, System.Type)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(System.String, System.Type, Newtonsoft.Json.JsonSerializerSettings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.String, Newtonsoft.Json.JsonSerializerSettings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.String)
   at SourceGeneratorSample.RunnerJsonNet.Run()
   at SourceGeneratorSample.Program.Main()

64個上限は稀に引っかかりうると考えますが、502個あればほぼ大丈夫そうとは思いました。System.Text.Jsonはだいぶ穴が埋まってきたとはいえ、前の件とあわせ、まだやはり信頼と実績のNewtonsoft.Jsonだなと感じるケースはぽつぽつ残ります。(とはいえこれも環境依存かもしれませんが。)

参考文献

脚注
  1. Source Generatorは情報の少なさと不慣れさのため数日溶けました。正直一般の開発者には、手作業の方がダントツで早く確実に目的を達せられるはずです。 ↩︎

  2. ISyntaxReceiverを使う実装の方が多く見つかりますが、ISyntaxContextReceiverのほうが多くの場合で使いやすいと思います。後からできたAPIのようです。 ↩︎

  3. 特に自動化せず、人力二分探索しました... ↩︎

  4. そこにもSource Generatorを作っても良かったかもしれませんが ↩︎

  5. .NET 6以下で使いたい場合: https://stackoverflow.com/questions/74447497/is-it-possible-to-use-the-c11-required-modifier-with-net-framework-4-8-and ↩︎

Discussion