Repository: dotnet/runtime (C#)

Issue:
Apartment threaded IDispatch server throws `InvalidOperationException` or fails to marshal
### Description

When implementing an IDispatch apartment threaded object on net7.0, an exception is thrown when calling `Marshal.GetIDispatchForObject`.  Non-.Net callers get `E_NOINTERFACE`.  The same code works on .Net Framework.

This seems to be related to removing `[ComVisible(true)]` from many types - which is understandable in general, but `StandardOleMarshalObject` is exclusively used with COM.  I would propose that `[ComVisible(true)]` be re-added to `StandardOleMarshalObject`, the requirement for `[ComVisible(true)]` to be reduced, or the need to re-implement `StandardOleMarshalObject` be documented.  In testing, adding the attribute is sufficient to avoid the exception.

### Reproduction Steps

Create a new `net7.0-windows` console program.  Add the following to Program.cs:

```c#
Marshal.GetIDispatchForObject(new Example());

[ComVisible(true)]
public class Example : StandardOleMarshalObject
{
}
```

### Expected behavior

The example program should not throw an exception (the type will be available to IDispatch callers).

### Actual behavior

The following exception is thrown:

> System.InvalidOperationException: Type 'DispTarget' has a ComVisible(false) parent 
 'System.Runtime.InteropServices.StandardOleMarshalObject' in its hierarchy, therefore QueryInterface calls for IDispatch or class interfaces are disallowed.<br/>&nbsp;&nbsp;&nbsp;&nbsp;at System.Runtime.InteropServices.Marshal.GetIDispatchForObjectNative(Object o)<br/>&nbsp;&nbsp;&nbsp;&nbsp;at Program.&lt;Main&gt;$(String[] args) in C:\dev\source\repos\ComDispatchDemo\DemoDispatchServer\Program.cs:line 8

If the type is passed to a COM server or registered with `CoRegisterClassObject` instead of `GetIDispatchForObject`, no exception is thrown in the server and `E_NOINTERFACE` is returned the caller.

### Regression?

Yes.  The same code on net48 runs without issue.

### Known Workarounds

Copy/paste the implementation of [StandardOleMarshalComObject.cs](https://github.com/dotnet/runtime/blob/d561f390b8085ffed309c49cf30ec205a47868c0/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/StandardOleMarshalObject.Windows.cs#L8) into the project, and add the `[ComVisible(true)]` attribute.

Alternately, if the object does not require apartment threading, remove `StandardOleMarshalComObject` as a base class.

### Configuration

dotnet 7.0.401
Windows 11 22H2 (22621.2428)
Reproducible with `Platform` x86 or x64
Presumed to be applicable to .Net 5..8 and all platforms

### Other information

Loosely related: https://github.com/dotnet/runtime/issues/80422


Discussion:
Tagging subscribers to this area: @dotnet/interop-contrib
See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
<details>
<summary>Issue Details</summary>
<hr />

### Description

When implementing an IDispatch apartment threaded object on net7.0, an exception is thrown when calling `Marshal.GetIDispatchForObject`.  Non-.Net callers get `E_NOINTERFACE`.  The same code works on .Net Framework.

This seems to be related to removing `[ComVisible(true)]` from many types - which is understandable in general, but `StandardOleMarshalObject` is exclusively used with COM.  I would propose that `[ComVisible(true)]` be re-added to `StandardOleMarshalObject`, the requirement for `[ComVisible(true)]` to be reduced, or the need to re-implement `StandardOleMarshalObject` be documented.  In testing, adding the attribute is sufficient to avoid the exception.

### Reproduction Steps

Create a new `net7.0-windows` console program.  Add the following to Program.cs:

```c#
Marshal.GetIDispatchForObject(new Example());

[ComVisible(true)]
public class Example : StandardOleMarshalObject
{
}
```

### Expected behavior

The example program should not throw an exception (the type will be available to IDispatch callers).

### Actual behavior

The following exception is thrown:

> System.InvalidOperationException: Type 'DispTarget' has a ComVisible(false) parent 
 'System.Runtime.InteropServices.StandardOleMarshalObject' in its hierarchy, therefore QueryInterface calls for IDispatch or class interfaces are disallowed.<br/>&nbsp;&nbsp;&nbsp;&nbsp;at System.Runtime.InteropServices.Marshal.GetIDispatchForObjectNative(Object o)<br/>&nbsp;&nbsp;&nbsp;&nbsp;at Program.&lt;Main&gt;$(String[] args) in C:\dev\source\repos\ComDispatchDemo\DemoDispatchServer\Program.cs:line 8

If the type is passed to a COM server or registered with `CoRegisterClassObject` instead of `GetIDispatchForObject`, no exception is thrown in the server and `E_NOINTERFACE` is returned the caller.

### Regression?

Yes.  The same code on net48 runs without issue.

### Known Workarounds

Copy/paste the implementation of [StandardOleMarshalComObject.cs](https://github.com/dotnet/runtime/blob/d561f390b8085ffed309c49cf30ec205a47868c0/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/StandardOleMarshalObject.Windows.cs#L8) into the project, and add the `[ComVisible(true)]` attribute.

Alternately, if the object does not require apartment threading, remove `StandardOleMarshalComObject` as a base class.

### Configuration

dotnet 7.0.401
Windows 11 22H2 (22621.2428)
Reproducible with `Platform` x86 or x64
Presumed to be applicable to .Net 5..8 and all platforms

### Other information

Loosely related: https://github.com/dotnet/runtime/issues/80422

<table>
  <tr>
    <th align="left">Author:</th>
    <td>mgaffigan</td>
  </tr>
  <tr>
    <th align="left">Assignees:</th>
    <td>-</td>
  </tr>
  <tr>
    <th align="left">Labels:</th>
    <td>

`area-System.Runtime.InteropServices`, `untriaged`

</td>
  </tr>
  <tr>
    <th align="left">Milestone:</th>
    <td>-</td>
  </tr>
</table>
</details>
This makes sense. Sounds like a simple .NET 9 fix-up. Also a good opportunity to add some tests.



Patch:
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/StandardOleMarshalObject.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/StandardOleMarshalObject.Windows.cs
index 9dbd739fd6fd3f..40bae7bc9259bc 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/StandardOleMarshalObject.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/StandardOleMarshalObject.Windows.cs
@@ -5,6 +5,7 @@
 
 namespace System.Runtime.InteropServices
 {
+    [ComVisible(true)]
     public class StandardOleMarshalObject : MarshalByRefObject, IMarshal
     {
         private static readonly Guid CLSID_StdMarshal = new Guid("00000017-0000-0000-c000-000000000046");


Test patch:
diff --git a/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System.Runtime.InteropServices.Tests.csproj b/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System.Runtime.InteropServices.Tests.csproj
index 31369aaca2a4a3..acb1d91dae2d82 100644
--- a/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System.Runtime.InteropServices.Tests.csproj
+++ b/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System.Runtime.InteropServices.Tests.csproj
@@ -160,6 +160,7 @@
     <Compile Include="System\Runtime\InteropServices\ProgIdAttributeTests.cs" />
     <Compile Include="System\Runtime\InteropServices\RuntimeEnvironmentTests.cs" />
     <Compile Include="System\Runtime\InteropServices\SafeBufferTests.cs" />
+    <Compile Include="System\Runtime\InteropServices\StandardOleMarshalObjectTests.cs" />
     <Compile Include="System\Runtime\InteropServices\StructLayoutAttributeTests.cs" />
     <Compile Include="System\Runtime\InteropServices\TypeIdentifierAttributeTests.cs" />
     <Compile Include="System\Runtime\InteropServices\UnknownWrapperTests.cs" />
diff --git a/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System/Runtime/InteropServices/StandardOleMarshalObjectTests.cs b/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System/Runtime/InteropServices/StandardOleMarshalObjectTests.cs
new file mode 100644
index 00000000000000..a09ca6a14c9595
--- /dev/null
+++ b/src/libraries/System.Runtime.InteropServices/tests/System.Runtime.InteropServices.UnitTests/System/Runtime/InteropServices/StandardOleMarshalObjectTests.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace System.Runtime.InteropServices.Tests
+{
+    [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsBuiltInComEnabled))]
+    public class StandardOleMarshalObjectTests
+    {
+        private static readonly Guid IID_IDispatch = new Guid("00020400-0000-0000-C000-000000000046");
+        [Fact]
+        public void CanGetIDispatchOfDerivedObject()
+        {
+            IntPtr disp = Marshal.GetIDispatchForObject(new DerivedObject());
+            Assert.NotEqual(IntPtr.Zero, disp);
+            Marshal.Release(disp);
+        }
+
+        [Fact]
+        public void CanQueryInterfaceForIDispatchOfDerivedObject()
+        {
+            IntPtr unk = Marshal.GetIUnknownForObject(new DerivedObject());
+            Assert.NotEqual(IntPtr.Zero, unk);
+
+            int hr = Marshal.QueryInterface(unk, IID_IDispatch, out IntPtr disp);
+            Assert.Equal(0, hr);
+            Assert.NotEqual(IntPtr.Zero, disp);
+ 
+            Marshal.Release(disp);
+            Marshal.Release(unk);
+        }
+
+        [ComVisible(true)]
+        public sealed class DerivedObject : StandardOleMarshalObject
+        {
+        }
+    }
+}
\ No newline at end of file