Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent proxy types from being generated for an interface #739

Closed
bclothier opened this issue Jan 3, 2019 · 6 comments
Closed

Prevent proxy types from being generated for an interface #739

bclothier opened this issue Jan 3, 2019 · 6 comments

Comments

@bclothier
Copy link

I'm developing a mocking framework based on Moq for use in VBA (ref #rubberduck-vba/Rubberduck/pull/4681).

I'm running into a problem where proxies cannot be cast to their interfaces. From what I understand, that might be due to the fact that dynamic proxies don't actually implement the COM interfaces but create a substitute type with similar names & properties, which then cause problems in testing the mocking framework because I cannot cast it to the COM interface's interop type.

All mocks are using interfaces discovered via runtime as per this code, so I believe it falls into this conditional branch in Moq:
https://github.com/moq/moq4/blob/670123672fbc30d92971bcb83860c5a25b4e3ec6/src/Moq/ProxyFactories/CastleProxyFactory.cs#L62-L67

I'm not sure I fully understand the ramifications but from what I observed, this seems to create a proxy type that has a similar interface but do not in fact implement the original interfaces they were created for. My reading of Castle's DynamicProxy seems to suggest that creating one without a target will cause this behavior. Here's some code (adapted from this unit test) to demonstrate the issue:

var provider = new MockProvider();
var mock = provider.Mock("Scripting.FileSystemObject");
var obj = mock.Object;

pUnk = Marshal.GetIUnknownForObject(obj);
var iid = typeof(IFileSystem3).GUID;
var hr = Marshal.QueryInterface(pUnk, ref iid, out pMocked);
if (hr != 0)
{
    throw new InvalidCastException("QueryInterface failed on the mocked type");
}

var proxy =  (IFileSystem3)Marshal.GetObjectForIUnknown(pMocked);

The last line fails with an error:

System.InvalidCastException: 'Unable to cast object 
of type 'Castle.Proxies.FileSystemObjectProxy' 
to type 'Scripting.IFileSystem3'.'

Obviously that makes the test more complicated than it should be. However, this isn't a test-only issue. On the VBA side, we can get this error:
image

If we look at the mocked object, we can see that the 2nd proxy type was generated:
image

So code would work as long only one proxy type was generated but if 2nd proxy type for same COM interface gets created, we get the errors.

Is it possible for Moq to provide a means to explicitly control how proxy types should be created and to actually implement the interfaces as opposed to creating a mirror interface that can't be converted into the original interface? Or is this an issue with the Castle's implementation of the DynamicProxy?

@stakx
Copy link
Contributor

stakx commented Jan 4, 2019

@bclothier: TBH I haven't studied your code in great detail, I will start by offering a guess at what might go wrong:

COM type hierarchies often don't make much use of subclassing. Co-classes often implement a whole bunch of interfaces that aren't related to one another through subtyping / inheritance; each of those interfaces describes one aspect of the co-class and you use QueryInterface to hop from one to any other particular aspect.

Specifically, if you have a COM interface IFileSystemObject which does not inherit IFileSystem3, and you create a Mock<IFileSystemObject>, then a cast of the mock.Object to IFileSystem3 will fail because Moq / DynamicProxy had no way of knowing that you wanted the proxy to implement the latter interface as well.

You can tell Moq which additional (unrelated) interfaces the proxy should implement before you ask it for the mock object (proxy) through calls to .As<T>():

var mock = new Mock<TDefaultInterface>();
mock.As<TAdditionalInterface1>();
mock.As<TAdditionalInterface2>();
...
var mocked = mock.Object;

Have you tried this?

I might be completely off with the above, in which case I'll take a closer look. But using .As<T>() would be the simplest place to start.

Btw. if you're not using any other capabilities of Moq beyond new Mock<T>() (i.e. you're not using setups, verification etc.) then you might be better off using DynamicProxy directly.

@bclothier
Copy link
Author

Hi, in fact, yes. It is covered in this commit.

Even so, casts doesnt seem to always work. I partially put it off in an earlier commit but that was not a complete fix.

I may try modifying the cache to cache the final closed type but I worry that may devolve into a whack-a-mole game.

Regarding the last comment, the intention is to provide a COM compatible wrapper of Moq, exposing Setup, Returns, and Callback.

@stakx
Copy link
Contributor

stakx commented Jan 4, 2019

@bclothier - OK. In this case I am not super-sure whether I can be of much help, as I don't have much experience with how .NET exposes .NET objects to COM (CCWs). I can run some experiments during the next few days, but hopefully someone else can chime in and help you out before that.

If you're ready to dig into this more deeply, perhaps it would be good to run some experiments first by peeling the Moq layer away and see how it works when you create a DynamicProxy proxy directly and try to call QueryInterface directly on that—this way you might be able to pinpoint the problem more exactly. What I often do to track these things down is to set up a DynamicProxy ProxyGenerator such that you can save the generated dynamic assembly (containing the proxy types) to disk and inspecting it using e.g. ILSpy or ILDASM. This allows you to recreate the auto-generated proxy in C# source code, and find out what changes would be necessary to get things working. Then you can e.g. research DynamicProxy's (or Moq's) APIs to find out how to have them apply these changes for you.

@bclothier
Copy link
Author

OK, I've been doing some reading up on the DynamicProxy to understand how it works. I did come across this tidbit which suggests that if using ProxyInterfaceWithTargetInterface, this would provide the type equality. Whether that also resolves the InvalidCastException is not clear, however. I will see if I can put your suggestion in action.

@bclothier
Copy link
Author

Ok, I have an update - following your suggestion to set up a MCVE, I quickly found that the unit tests I wrote in a test solution was passing using strong-typed version. It still even passes even using reflection (as I was in my code originally). Still passes even with the wrapper object.

In the end, it turns out that it's not even Moq nor DynamicProxy at the play. It's all caused by this seemingly unrelated code:

                if (classType.Name == "__ComObject")
                {
                    var service = TypeLibQueryService.Instance;
                    if (service.TryGetTypeInfoFromProgId(ProgId, out var typeInfo))
                    {
                        var pUnk = Marshal.GetIUnknownForObject(typeInfo);
                        classType = Marshal.GetTypeForITypeInfo(pUnk);
                        Marshal.Release(pUnk);

                        if (classType == null)
                        {
                            throw new ArgumentOutOfRangeException(nameof(ProgId),
                                $"The supplied {ProgId} was found, but we could not acquire the required metadata on the type to mock it. The class may not support early-binding.");
                        }
                    }
                }

Basically, this covers the case where we do not get a type because the PIA isn't in GAC or whatever reasons. In that situation, we get a generic System.__ComObject which is useless for mocking purposes. I worked around this by doing a registry lookup for the type library and load a Type using Marshal.GetTypeForITypeInfo().

In that case, the type returned looks and acts like the actual COM interface that we're wanting but it's not the same interop type; trying to do a equality test or comparing hash code between the returned type vs. the PIA's Scripting.FileSystemObject will then fail. Hence the InvalidCastException.

I apologize; I got misled by the whole proxying affair, thinking it was a problem with how it was represnted. In end, this was a X-Y problem. For that reason, I'll close that issue now.

@stakx
Copy link
Contributor

stakx commented Jan 5, 2019

@bclothier: No need to apologize, glad to hear you figured this one out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants