Repository: MessagePack-CSharp/MessagePack-CSharp

Issue:
Wrong type of formatter for record with string keys
#### Bug description

Source generator generates formatter with errors (unable to build) for record with one primary constructor. Only when string used as a key, with int everything works as expected. 

#### Repro steps

just to use this to generate the code.

```c#
[MessagePackObject]
public record Record([property: Key("c")] string SomeId);
```

But it works if Key == Parameter name, btw ive seen that it was tested with similar record when key and name are the same
```c#
[MessagePackObject]
public record Record([property: Key("SomeId")] string SomeId);
```

#### Expected behavior

Formatter should create an object and put all args to constructor

#### Actual behavior

Formatter tries to create an object without parameters and then tries to set it.

#### Additional context

Investigated and have found that when it collect Member info it use string key as dictionary key

https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/43f1b1c874bd224943a16555a483d6dc9de00c6c/src/MessagePack.GeneratorCore/CodeAnalysis/TypeCollector.cs#L806-L812

then it creates lookup by this string key

https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/43f1b1c874bd224943a16555a483d6dc9de00c6c/src/MessagePack.GeneratorCore/CodeAnalysis/TypeCollector.cs#L841

and then it tries to get member info using prop name

https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/43f1b1c874bd224943a16555a483d6dc9de00c6c/src/MessagePack.GeneratorCore/CodeAnalysis/TypeCollector.cs#L885

It seems to me that the solution is to simply use the property name in the stringMembers dictionary.


Hints:
Thanks for your report and your proposed fix. Would you please try our `develop` branch? `mpc` is gone, replaced with a proper C# source generator. Much of the code is the same, but a lot of refactoring has taken place.

If it repros in `develop` and you're interested, we'd be delighted if you'd add a test and a pull request to fix it.
Thanks that mentioned develop i tried to find source gen in master =) i checked develop, its almost the same in that place, will fix that and create pr.

Patch:
diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
index 6c405688d..2fe531b0e 100644
--- a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
+++ b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
@@ -933,7 +933,7 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
         var constructorParameters = new List<MemberSerializationInfo>();
         if (ctor != null)
         {
-            var constructorLookupDictionary = stringMembers.ToLookup(x => x.Key, x => x, StringComparer.OrdinalIgnoreCase);
+            var constructorLookupDictionary = stringMembers.ToLookup(x => x.Value.Name, x => x, StringComparer.OrdinalIgnoreCase);
             do
             {
                 constructorParameters.Clear();


Test patch:
diff --git a/tests/MessagePack.SourceGenerator.Tests/CustomStringKeyFormatterTests.cs b/tests/MessagePack.SourceGenerator.Tests/CustomStringKeyFormatterTests.cs
new file mode 100644
index 000000000..e5e2c631b
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/CustomStringKeyFormatterTests.cs
@@ -0,0 +1,61 @@
+// Copyright (c) All contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using VerifyCS = CSharpSourceGeneratorVerifier<MessagePack.SourceGenerator.MessagePackGenerator>;
+
+public class CustomStringKeyFormatterTests
+{
+    private readonly ITestOutputHelper testOutputHelper;
+
+    public CustomStringKeyFormatterTests(ITestOutputHelper testOutputHelper)
+    {
+        this.testOutputHelper = testOutputHelper;
+    }
+
+    [Fact]
+    public async Task RecordWithPrimaryConstructor()
+    {
+        string testSource = """
+using System;
+using System.Collections.Generic;
+using MessagePack;
+
+namespace TempProject
+{
+    [MessagePackObject]
+    public record MyMessagePackObject([property: Key("p")] string PhoneNumber, [property: Key("c")] int Count);
+}
+""";
+        await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
+    }
+
+    [Fact]
+    public async Task RecordWithWithInitOnlyProps()
+    {
+        string testSource = """
+using System;
+using System.Collections.Generic;
+using MessagePack;
+
+namespace TempProject
+{
+    [MessagePackObject]
+    public class MyMessagePackObject
+    {
+        public MyMessagePackObject(string phoneNumber, int count)
+        {
+            PhoneNumber = phoneNumber;
+            Count = count;
+        }
+
+        [Key("p")]
+        public string PhoneNumber { get; set; }
+
+        [Key("c")]
+        public int Count { get; set; }
+    };
+}
+""";
+        await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
+    }
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithPrimaryConstructor/Formatters.TempProject.MyMessagePackObjectFormatter.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithPrimaryConstructor/Formatters.TempProject.MyMessagePackObjectFormatter.g.cs
new file mode 100644
index 000000000..8a81a7655
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithPrimaryConstructor/Formatters.TempProject.MyMessagePackObjectFormatter.g.cs
@@ -0,0 +1,79 @@
+// <auto-generated />
+
+#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
+
+namespace MessagePack {
+
+using MsgPack = global::MessagePack;
+
+partial class GeneratedMessagePackResolver
+{
+private partial class TempProject { 
+	internal sealed class MyMessagePackObjectFormatter : global::MessagePack.Formatters.IMessagePackFormatter<global::TempProject.MyMessagePackObject>
+	{
+		// p
+		private static global::System.ReadOnlySpan<byte> GetSpan_PhoneNumber() => new byte[1 + 1] { 161, 112 };
+		// c
+		private static global::System.ReadOnlySpan<byte> GetSpan_Count() => new byte[1 + 1] { 161, 99 };
+
+		public void Serialize(ref global::MessagePack.MessagePackWriter writer, global::TempProject.MyMessagePackObject value, global::MessagePack.MessagePackSerializerOptions options)
+		{
+			if (value is null)
+			{
+				writer.WriteNil();
+				return;
+			}
+
+			var formatterResolver = options.Resolver;
+			writer.WriteMapHeader(2);
+			writer.WriteRaw(GetSpan_PhoneNumber());
+			MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<string>(formatterResolver).Serialize(ref writer, value.PhoneNumber, options);
+			writer.WriteRaw(GetSpan_Count());
+			writer.Write(value.Count);
+		}
+
+		public global::TempProject.MyMessagePackObject Deserialize(ref global::MessagePack.MessagePackReader reader, global::MessagePack.MessagePackSerializerOptions options)
+		{
+			if (reader.TryReadNil())
+			{
+				return null;
+			}
+
+			options.Security.DepthStep(ref reader);
+			var formatterResolver = options.Resolver;
+			var length = reader.ReadMapHeader();
+			var __PhoneNumber__ = default(string);
+			var __Count__ = default(int);
+
+			for (int i = 0; i < length; i++)
+			{
+				var stringKey = global::MessagePack.Internal.CodeGenHelpers.ReadStringSpan(ref reader);
+				switch (stringKey.Length)
+				{
+					default:
+					FAIL:
+					  reader.Skip();
+					  continue;
+					case 1:
+					    switch (global::MessagePack.Internal.AutomataKeyGen.GetKey(ref stringKey))
+					    {
+    					    default: goto FAIL;
+    					    case 112UL:
+        					    __PhoneNumber__ = MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<string>(formatterResolver).Deserialize(ref reader, options);
+        					    continue;
+    					    case 99UL:
+        					    __Count__ = reader.ReadInt32();
+        					    continue;
+					    }
+
+				}
+			}
+
+			var ____result = new global::TempProject.MyMessagePackObject(__PhoneNumber__, __Count__);
+			reader.Depth--;
+			return ____result;
+		}
+	}
+}}
+
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithPrimaryConstructor/MessagePack.GeneratedMessagePackResolver.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithPrimaryConstructor/MessagePack.GeneratedMessagePackResolver.g.cs
new file mode 100644
index 000000000..ff202b65b
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithPrimaryConstructor/MessagePack.GeneratedMessagePackResolver.g.cs
@@ -0,0 +1,63 @@
+﻿// <auto-generated />
+
+#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
+
+using MsgPack = global::MessagePack;
+
+[assembly: MsgPack::Internal.GeneratedAssemblyMessagePackResolverAttribute(typeof(MessagePack.GeneratedMessagePackResolver), 3, 0)]
+
+namespace MessagePack {
+
+/// <summary>A MessagePack resolver that uses generated formatters for types in this assembly.</summary>
+partial class GeneratedMessagePackResolver : MsgPack::IFormatterResolver
+{
+	/// <summary>An instance of this resolver that only returns formatters specifically generated for types in this assembly.</summary>
+	public static readonly MsgPack::IFormatterResolver Instance = new GeneratedMessagePackResolver();
+
+	private GeneratedMessagePackResolver()
+	{
+	}
+
+	public MsgPack::Formatters.IMessagePackFormatter<T> GetFormatter<T>()
+	{
+		return FormatterCache<T>.Formatter;
+	}
+
+	private static class FormatterCache<T>
+	{
+		internal static readonly MsgPack::Formatters.IMessagePackFormatter<T> Formatter;
+
+		static FormatterCache()
+		{
+			var f = GeneratedMessagePackResolverGetFormatterHelper.GetFormatter(typeof(T));
+			if (f != null)
+			{
+				Formatter = (MsgPack::Formatters.IMessagePackFormatter<T>)f;
+			}
+		}
+	}
+
+	private static class GeneratedMessagePackResolverGetFormatterHelper
+	{
+		private static readonly global::System.Collections.Generic.Dictionary<global::System.Type, int> closedTypeLookup = new(1)
+		{
+			{ typeof(global::TempProject.MyMessagePackObject), 0 },
+		};
+
+		internal static object GetFormatter(global::System.Type t)
+		{
+			if (closedTypeLookup.TryGetValue(t, out int closedKey))
+			{
+				return closedKey switch
+				{
+					0 => new TempProject.MyMessagePackObjectFormatter(),
+					_ => null, // unreachable
+				};
+			}
+
+			return null;
+		}
+	}
+}
+
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithWithInitOnlyProps/Formatters.TempProject.MyMessagePackObjectFormatter.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithWithInitOnlyProps/Formatters.TempProject.MyMessagePackObjectFormatter.g.cs
new file mode 100644
index 000000000..8a81a7655
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithWithInitOnlyProps/Formatters.TempProject.MyMessagePackObjectFormatter.g.cs
@@ -0,0 +1,79 @@
+// <auto-generated />
+
+#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
+
+namespace MessagePack {
+
+using MsgPack = global::MessagePack;
+
+partial class GeneratedMessagePackResolver
+{
+private partial class TempProject { 
+	internal sealed class MyMessagePackObjectFormatter : global::MessagePack.Formatters.IMessagePackFormatter<global::TempProject.MyMessagePackObject>
+	{
+		// p
+		private static global::System.ReadOnlySpan<byte> GetSpan_PhoneNumber() => new byte[1 + 1] { 161, 112 };
+		// c
+		private static global::System.ReadOnlySpan<byte> GetSpan_Count() => new byte[1 + 1] { 161, 99 };
+
+		public void Serialize(ref global::MessagePack.MessagePackWriter writer, global::TempProject.MyMessagePackObject value, global::MessagePack.MessagePackSerializerOptions options)
+		{
+			if (value is null)
+			{
+				writer.WriteNil();
+				return;
+			}
+
+			var formatterResolver = options.Resolver;
+			writer.WriteMapHeader(2);
+			writer.WriteRaw(GetSpan_PhoneNumber());
+			MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<string>(formatterResolver).Serialize(ref writer, value.PhoneNumber, options);
+			writer.WriteRaw(GetSpan_Count());
+			writer.Write(value.Count);
+		}
+
+		public global::TempProject.MyMessagePackObject Deserialize(ref global::MessagePack.MessagePackReader reader, global::MessagePack.MessagePackSerializerOptions options)
+		{
+			if (reader.TryReadNil())
+			{
+				return null;
+			}
+
+			options.Security.DepthStep(ref reader);
+			var formatterResolver = options.Resolver;
+			var length = reader.ReadMapHeader();
+			var __PhoneNumber__ = default(string);
+			var __Count__ = default(int);
+
+			for (int i = 0; i < length; i++)
+			{
+				var stringKey = global::MessagePack.Internal.CodeGenHelpers.ReadStringSpan(ref reader);
+				switch (stringKey.Length)
+				{
+					default:
+					FAIL:
+					  reader.Skip();
+					  continue;
+					case 1:
+					    switch (global::MessagePack.Internal.AutomataKeyGen.GetKey(ref stringKey))
+					    {
+    					    default: goto FAIL;
+    					    case 112UL:
+        					    __PhoneNumber__ = MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<string>(formatterResolver).Deserialize(ref reader, options);
+        					    continue;
+    					    case 99UL:
+        					    __Count__ = reader.ReadInt32();
+        					    continue;
+					    }
+
+				}
+			}
+
+			var ____result = new global::TempProject.MyMessagePackObject(__PhoneNumber__, __Count__);
+			reader.Depth--;
+			return ____result;
+		}
+	}
+}}
+
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithWithInitOnlyProps/MessagePack.GeneratedMessagePackResolver.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithWithInitOnlyProps/MessagePack.GeneratedMessagePackResolver.g.cs
new file mode 100644
index 000000000..ff202b65b
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/RecordWithWithInitOnlyProps/MessagePack.GeneratedMessagePackResolver.g.cs
@@ -0,0 +1,63 @@
+﻿// <auto-generated />
+
+#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
+
+using MsgPack = global::MessagePack;
+
+[assembly: MsgPack::Internal.GeneratedAssemblyMessagePackResolverAttribute(typeof(MessagePack.GeneratedMessagePackResolver), 3, 0)]
+
+namespace MessagePack {
+
+/// <summary>A MessagePack resolver that uses generated formatters for types in this assembly.</summary>
+partial class GeneratedMessagePackResolver : MsgPack::IFormatterResolver
+{
+	/// <summary>An instance of this resolver that only returns formatters specifically generated for types in this assembly.</summary>
+	public static readonly MsgPack::IFormatterResolver Instance = new GeneratedMessagePackResolver();
+
+	private GeneratedMessagePackResolver()
+	{
+	}
+
+	public MsgPack::Formatters.IMessagePackFormatter<T> GetFormatter<T>()
+	{
+		return FormatterCache<T>.Formatter;
+	}
+
+	private static class FormatterCache<T>
+	{
+		internal static readonly MsgPack::Formatters.IMessagePackFormatter<T> Formatter;
+
+		static FormatterCache()
+		{
+			var f = GeneratedMessagePackResolverGetFormatterHelper.GetFormatter(typeof(T));
+			if (f != null)
+			{
+				Formatter = (MsgPack::Formatters.IMessagePackFormatter<T>)f;
+			}
+		}
+	}
+
+	private static class GeneratedMessagePackResolverGetFormatterHelper
+	{
+		private static readonly global::System.Collections.Generic.Dictionary<global::System.Type, int> closedTypeLookup = new(1)
+		{
+			{ typeof(global::TempProject.MyMessagePackObject), 0 },
+		};
+
+		internal static object GetFormatter(global::System.Type t)
+		{
+			if (closedTypeLookup.TryGetValue(t, out int closedKey))
+			{
+				return closedKey switch
+				{
+					0 => new TempProject.MyMessagePackObjectFormatter(),
+					_ => null, // unreachable
+				};
+			}
+
+			return null;
+		}
+	}
+}
+
+}