Repository: MessagePack-CSharp/MessagePack-CSharp

Issue:
v3: MsgPack003 Analyzer triggering on System.Collections.ObjectModel.Collection
#### Bug description
The MsgPack003 Analyzer triggers on `System.Collections.ObjectModel.Collection<>`  even though it can be handled through `IList<>` etc.

#### Repro steps
Create the following program
```csharp
using MessagePack;
using System.Collections.ObjectModel;

[MessagePackObject]
public partial class A
{
    [Key(0)]
    public Collection<int> SampleCollection { get; set; } = []; // Error is placed here
}
```

#### Expected behavior
The analyzer should not report an error.

#### Actual behavior
The compiler throws an error:
| Severity       | Code        | Description                                                       |
|----------------|-------------|-------------------------------------------------------------------|
| Error | MsgPack003 | Type must be marked with MessagePackObjectAttribute: Collection (https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/master/doc/analyzers/MsgPack003.md) |

- Version used: 3.0.233-rc.1
- Runtime: .NET 8

#### Additional context
If we extend the program with
```csharp
A test = new A();
test.SampleCollection.Add(1);
var output = MessagePackSerializer.Serialize(test);
var deserialized = MessagePackSerializer.Deserialize<A>(output);
if (deserialized.SampleCollection is not [1])
    Console.WriteLine("Error");
else
    Console.WriteLine("Success");
```
and use the play button in Visual Studio to skip the analyzers the program runs without issues.
Source generator output:
```csharp
// <auto-generated />

#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649

#pragma warning disable CS8669 // We may leak nullable annotations into generated code.

using MsgPack = global::MessagePack;

namespace MessagePack {
partial class GeneratedMessagePackResolver {

	internal sealed class AFormatter : MsgPack::Formatters.IMessagePackFormatter<global::A>
	{

		public void Serialize(ref MsgPack::MessagePackWriter writer, global::A value, MsgPack::MessagePackSerializerOptions options)
		{
			if (value == null)
			{
				writer.WriteNil();
				return;
			}

			MsgPack::IFormatterResolver formatterResolver = options.Resolver;
			writer.WriteArrayHeader(1);
			MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<global::System.Collections.ObjectModel.Collection<int>>(formatterResolver).Serialize(ref writer, value.SampleCollection, options);
		}

		public global::A Deserialize(ref MsgPack::MessagePackReader reader, MsgPack::MessagePackSerializerOptions options)
		{
			if (reader.TryReadNil())
			{
				return null;
			}

			options.Security.DepthStep(ref reader);
			MsgPack::IFormatterResolver formatterResolver = options.Resolver;
			var length = reader.ReadArrayHeader();
			var ____result = new global::A();

			for (int i = 0; i < length; i++)
			{
				switch (i)
				{
					case 0:
						____result.SampleCollection = MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<global::System.Collections.ObjectModel.Collection<int>>(formatterResolver).Deserialize(ref reader, options);
						break;
					default:
						reader.Skip();
						break;
				}
			}

			reader.Depth--;
			return ____result;
		}
	}
}
}

```



Hints:

Patch:
diff --git a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
index 50565ad91..0ed26db03 100644
--- a/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
+++ b/src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
@@ -541,7 +541,8 @@ private bool CollectGeneric(INamedTypeSymbol type)
         }
 
         // collection
-        if (KnownGenericTypes.TryGetValue(genericTypeDefinitionString, out string formatterFullName))
+        string? formatterFullName;
+        if (KnownGenericTypes.TryGetValue(genericTypeDefinitionString, out formatterFullName))
         {
             foreach (ITypeSymbol item in type.TypeArguments)
             {
@@ -609,6 +610,32 @@ private bool CollectGeneric(INamedTypeSymbol type)
             return true;
         }
 
+        if (type.AllInterfaces.FirstOrDefault(x => !x.IsUnboundGenericType && x.ConstructUnboundGenericType() is { Name: nameof(ICollection<int>) }) is INamedTypeSymbol collectionIface
+            && type.InstanceConstructors.Any(ctor => ctor.Parameters.Length == 0))
+        {
+            this.CollectCore(collectionIface.TypeArguments[0]);
+
+            if (isOpenGenericType)
+            {
+                return true;
+            }
+            else
+            {
+                GenericSerializationInfo info = GenericSerializationInfo.Create(type, this.options.Generator.Resolver) with
+                {
+                    Formatter = new(TypeKind.Class)
+                    {
+                        Container = new NamespaceTypeContainer("MsgPack::Formatters"),
+                        Name = "GenericCollectionFormatter",
+                        TypeParameters = ImmutableArray.Create<GenericTypeParameterInfo>(new("TElement"), new("TCollection")),
+                        TypeArguments = ImmutableArray.Create<QualifiedTypeName>(QualifiedTypeName.Create(collectionIface.TypeArguments[0]), QualifiedTypeName.Create(type)),
+                    },
+                };
+
+                this.collectedGenericInfo.Add(info);
+            }
+        }
+
         // Generic types
         if (type.IsDefinition)
         {


Test patch:
diff --git a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
index 857bbf193..61a152b25 100644
--- a/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
+++ b/tests/MessagePack.SourceGenerator.Tests/GenerationTests.cs
@@ -929,4 +929,22 @@ public partial class SampleObject
 
         await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
     }
+
+    [Fact]
+    public async Task SystemCollectionsObjectModelCollection()
+    {
+        string testSource = /* lang=c#-test */ """
+            using MessagePack;
+            using System.Collections.ObjectModel;
+
+            [MessagePackObject]
+            public class A
+            {
+                [Key(0)]
+                public Collection<int> SampleCollection { get; set; }
+            }
+            """;
+
+        await VerifyCS.Test.RunDefaultAsync(this.testOutputHelper, testSource);
+    }
 }
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/SystemCollectionsObjectModelCollection/Formatters.MessagePack.GeneratedMessagePackResolver.AFormatter.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/SystemCollectionsObjectModelCollection/Formatters.MessagePack.GeneratedMessagePackResolver.AFormatter.g.cs
new file mode 100644
index 000000000..a7f360882
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/SystemCollectionsObjectModelCollection/Formatters.MessagePack.GeneratedMessagePackResolver.AFormatter.g.cs
@@ -0,0 +1,58 @@
+﻿// <auto-generated />
+
+#pragma warning disable 618, 612, 414, 168, CS1591, SA1129, SA1309, SA1312, SA1403, SA1649
+
+#pragma warning disable CS8669 // We may leak nullable annotations into generated code.
+
+using MsgPack = global::MessagePack;
+
+namespace MessagePack {
+partial class GeneratedMessagePackResolver {
+
+	internal sealed class AFormatter : MsgPack::Formatters.IMessagePackFormatter<global::A>
+	{
+
+		public void Serialize(ref MsgPack::MessagePackWriter writer, global::A value, MsgPack::MessagePackSerializerOptions options)
+		{
+			if (value == null)
+			{
+				writer.WriteNil();
+				return;
+			}
+
+			MsgPack::IFormatterResolver formatterResolver = options.Resolver;
+			writer.WriteArrayHeader(1);
+			MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<global::System.Collections.ObjectModel.Collection<int>>(formatterResolver).Serialize(ref writer, value.SampleCollection, options);
+		}
+
+		public global::A Deserialize(ref MsgPack::MessagePackReader reader, MsgPack::MessagePackSerializerOptions options)
+		{
+			if (reader.TryReadNil())
+			{
+				return null;
+			}
+
+			options.Security.DepthStep(ref reader);
+			MsgPack::IFormatterResolver formatterResolver = options.Resolver;
+			var length = reader.ReadArrayHeader();
+			var ____result = new global::A();
+
+			for (int i = 0; i < length; i++)
+			{
+				switch (i)
+				{
+					case 0:
+						____result.SampleCollection = MsgPack::FormatterResolverExtensions.GetFormatterWithVerify<global::System.Collections.ObjectModel.Collection<int>>(formatterResolver).Deserialize(ref reader, options);
+						break;
+					default:
+						reader.Skip();
+						break;
+				}
+			}
+
+			reader.Depth--;
+			return ____result;
+		}
+	}
+}
+}
diff --git a/tests/MessagePack.SourceGenerator.Tests/Resources/SystemCollectionsObjectModelCollection/MessagePack.GeneratedMessagePackResolver.g.cs b/tests/MessagePack.SourceGenerator.Tests/Resources/SystemCollectionsObjectModelCollection/MessagePack.GeneratedMessagePackResolver.g.cs
new file mode 100644
index 000000000..daadab91c
--- /dev/null
+++ b/tests/MessagePack.SourceGenerator.Tests/Resources/SystemCollectionsObjectModelCollection/MessagePack.GeneratedMessagePackResolver.g.cs
@@ -0,0 +1,65 @@
+﻿// <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 global::System.Collections.Generic.Dictionary<global::System.Type, int>(2)
+		{
+			{ typeof(global::System.Collections.ObjectModel.Collection<int>), 0 },
+			{ typeof(global::A), 1 },
+		};
+
+		internal static object GetFormatter(global::System.Type t)
+		{
+			if (closedTypeLookup.TryGetValue(t, out int closedKey))
+			{
+				switch (closedKey)
+				{
+					case 0: return new MsgPack::Formatters.GenericCollectionFormatter<int, global::System.Collections.ObjectModel.Collection<int>>();
+					case 1: return new global::MessagePack.GeneratedMessagePackResolver.AFormatter();
+					default: return null; // unreachable
+				};
+			}
+
+			return null;
+		}
+	}
+}
+
+}